The bundle allows you to quickly and conveniently deploy JSON RPC API applications based on the Symfony 6 framework.
- easy api versioning
- easy bundle installation
- compatible with attributes
- compatible with POST, GET, PUT, PATCH, DELETE requests
- fully compatible with https://www.jsonrpc.org/specification
- swagger openapi out of the box
github: https://github.com/OtezVikentiy/symfony-jsonrpc-api-bundle
Instructions: https://otezvikentiy.tech/articles/symfony-json-rpc-api-bundle-prostoe-api-so-vsem-neobhodimym
- Require the bundle as a dependency.
$ composer require otezvikentiy/json-rpc-api
- Enable it in your application Kernel. ( not required if using flex )
<?php
// config/bundles.php
return [
//...
OV\JsonRPCAPIBundle\OVJsonRPCAPIBundle::class => ['all' => true],
];
- Create / update these config files
# config/routes/ov_json_rpc_api.yaml
ov_json_rpc_api:
resource: '@OVJsonRPCAPIBundle/config/routes/routes.yaml'
# config/services.yaml
services:
App\RPC\V1\:
resource: '../src/RPC/V1/{*Method.php}'
tags:
- { name: ov.rpc.method, namespace: App\RPC\V1\, version: 1 }
# config/packages/ov_json_rpc_api.yaml
ov_json_rpc_api:
swagger:
api_v1:
api_version: '1'
base_path: '%env(string:OV_JSON_RPC_API_BASE_URL)%'
auth_token_name: 'X-AUTH-TOKEN'
auth_token_test_value: '%env(string:OV_JSON_RPC_API_AUTH_TOKEN)%' #set blank for prod environment
info:
title: 'Some awesome api title here'
description: 'Some description about your api here would be appreciated if you like'
terms_of_service_url: 'https://terms_of_service_url.test/url'
contact:
name: 'John Doe'
url: 'https://john-doe.test'
email: 'john.doe@john-doe.test'
license: 'MIT license'
licenseUrl: 'https://john-doe.test/mit-license'
# .env
###> otezvikentiy/json-rpc-api ###
OV_JSON_RPC_API_SWAGGER_PATH=public/openapi/
OV_JSON_RPC_API_BASE_URL=http://localhost
OV_JSON_RPC_API_AUTH_TOKEN=2f1f6aee7d994528fde6e47a493cc097
###< otezvikentiy/json-rpc-api ###
During the installation process, we defined the src/RPC/V1/{*Method.php}
directory in the services and marked with
tags in it all the classes ending in *Method.php
- these will be our API endpoints.
└── src
└── RPC
└── V1
└── getProducts
├── GetProductsRequest.php
└── GetProductsResponse.php
└── GetProductsMethod.php
Create the following classes:
<?php
namespace App\RPC\V1\getProducts;
class GetProductsRequest
{
private int $id;
private string $title;
public function __construct(int $id)
{
$this->id = $id;
}
public function getId(): int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
}
<?php
namespace App\RPC\V1\getProducts;
class GetProductsResponse
{
private bool $success;
private string $title;
public function __construct(string $title, bool $success = true)
{
$this->success = $success;
$this->title = $title;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function isSuccess(): bool
{
return $this->success;
}
public function setSuccess(bool $success): void
{
$this->success = $success;
}
}
<?php
namespace App\RPC\V1;
use OV\JsonRPCAPIBundle\Core\Annotation\JsonRPCAPI;
use App\RPC\V1\getProducts\GetProductsRequest;
use App\RPC\V1\getProducts\GetProductsResponse;
#[JsonRPCAPI(methodName: 'getProducts', type: 'POST')]
class GetProductsMethod
{
public function call(GetProductsRequest $request): GetProductsResponse
{
return new GetProductsResponse($request->getTitle().'OLOLOLOLO');
}
}
And now you can execute curl request like this:
curl --header "Content-Type: application/json" --request POST --data '{"jsonrpc": "2.0","method": "getProducts","params": {"title": "AZAZAZA"},"id": 1}' http://localhost/api/v1
And the answer will be something like this:
{"jsonrpc":"2.0","result":{"title":"AZAZAZAOLOLOLOLO","success":true},"id":"1"}
In total, in order to create a new endpoint for your RPC API, you only need to add 3 classes - this is the method itself and the folder with the request and response.
If you wish to generate openapi swagger yaml file - then run this command:
bin/console ov:swagger:generate
It would generate a swagger file ( example public/openapi/api_v1.yaml ) which you can use in your swagger instance
You can also add token authorization like this:
- create src/Entity/ApiToken.php
<?php
namespace App\Entity;
use DateTime;
use DateTimeInterface;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class ApiToken
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
#[ORM\Column(type: 'string', length: 500, nullable: false)]
private string $token;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: false)]
private DateTimeInterface $expiresAt;
#[ORM\ManyToOne(inversedBy: 'apiTokens')]
#[ORM\JoinColumn(nullable: false)]
private User $user;
public function getId(): int
{
return $this->id;
}
public function setId(int $id): ApiToken
{
$this->id = $id;
return $this;
}
public function getToken(): string
{
return $this->token;
}
public function setToken(string $token): ApiToken
{
$this->token = $token;
return $this;
}
public function getExpiresAt(): DateTimeInterface
{
return $this->expiresAt;
}
public function setExpiresAt(DateTimeInterface $expiresAt): ApiToken
{
$this->expiresAt = $expiresAt;
return $this;
}
public function getUser(): User
{
return $this->user;
}
public function setUser(User $user): ApiToken
{
$this->user = $user;
return $this;
}
public function isValid(): bool
{
return (new DateTime())->getTimestamp() > $this->expiresAt->getTimestamp();
}
}
- create src/Security/ApiKeyAuthenticator.php
<?php
namespace App\Security;
use App\Entity\ApiToken;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class ApiKeyAuthenticator extends AbstractAuthenticator
{
public function __construct(
private readonly EntityManagerInterface $em
){
}
public function supports(Request $request): bool
{
return str_contains($request->getRequestUri(), '/api/v');
}
public function authenticate(Request $request): Passport
{
$apiToken = $request->headers->get('X-AUTH-TOKEN');
if (null === $apiToken) {
throw new CustomUserMessageAuthenticationException('No API token provided');
}
$apiTokenEntity = $this->em->getRepository(ApiToken::class)->findOneBy(['token' => $apiToken]);
if (is_null($apiTokenEntity)) {
throw new CustomUserMessageAuthenticationException('No API token provided');
}
return new SelfValidatingPassport(new UserBadge(
$apiTokenEntity->getUser()->getId(),
function () use ($apiTokenEntity) {
return $this->em->getRepository(User::class)->find($apiTokenEntity->getUser()->getId());
}
));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$data = [
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
}
- add new firewall to security section security.firewalls.api...
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
api:
pattern: ^/api
provider: app_user_provider
custom_authenticators:
- App\Security\ApiKeyAuthenticator
- run migration to create a table and add a token for a user - that's it! It is a standard way to create token authentication in symfony: https://symfony.com/doc/current/security/custom_authenticator.html