Symfony Json RPC API Bundle — простое API со всем необходимым

—

от автора

Как легко и быстро развернуть API на фреймворке Symfony с уже встроенной валидацией и авторизацией по токену?

Можно воспользоваться бандлом для Symfony 6+

Посмотреть на packagist

Посмотреть на github

Установка бандла

Как и любая другая библиотека ставится этот бандл с помощью композера:

composer require otezvikentiy/json-rpc-api

Далее проверяем, что бандл попал в инсталляцию в файле config/bundles.php файле:

<?php // config/bundles.php return [     //...     OV\JsonRPCAPIBundle\OVJsonRPCAPIBundle::class => ['all' => true], ];

После этого нам нужно создать файл config/routes/ov_json_rpc_api.yaml со следующим содержимым:

# config/routes/ov_json_rpc_api.yaml ov_json_rpc_api:    resource: '@OVJsonRPCAPIBundle/config/routes/routes.yaml'

И в файлике config/services.yaml в секцию services добавить раздел с той папкой, которая предполагается для реализации всех ваших дальнейших методов API. Например так:

# config/services.yaml services:     App\RPC\V1\:         resource: '../src/RPC/V1/{*Method.php}'         tags:             - { name: ov.rpc.method, namespace: App\RPC\V1\, version: 1 }

На этом всё! Установка закончена — вы можете создавать методы своего API! 

Испытания и первые методы

Теперь вы можете создать ваш первый метод API. Давайте попробуем реализовать какой-нибудь тестовый простой метод. Для этого нам потребуется создать 3 файлика вот по такой вот схеме:

└── src     └── RPC         └── V1             └── getProducts                 ├── GetProductsRequest.php                 └── GetProductsResponse.php             └── GetProductsMethod.php
<?php  namespace App\RPC\V1\getProducts;  class GetProductsRequest {     private int $id;     private string $title;      /**      * @param int $id      */     public function __construct(int $id)     {         $this->id = $id;     }          /**      * @return int      */     public function getId(): int     {         return $this->id;     }          /**      * @param int $id      */     public function setId(int $id): void     {         $this->id = $id;     }          /**      * @return string      */     public function getTitle(): string     {         return $this->title;     }          /**      * @param string $title      */     public function setTitle(string $title): void     {         $this->title = $title;     }  }
<?php  namespace App\RPC\V1\getProducts;  class GetProductsResponse {     private bool $success;     private string $title;      /**      * @param string $title      * @param bool $success      */     public function __construct(string $title, bool $success = true)     {         $this->success = $success;         $this->title = $title;     }          /**      * @return string      */     public function getTitle(): string     {         return $this->title;     }          /**      * @param string $title      */     public function setTitle(string $title): void     {         $this->title = $title;     }          /**      * @return bool      */     public function isSuccess(): bool     {         return $this->success;     }          /**      * @param bool $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")  */ #[JsonRPCAPI(methodName: 'getProducts')] class GetProductsMethod {     /**      * @param GetProductsRequest $request      * @return GetProductsResponse      */     public function call(GetProductsRequest $request): GetProductsResponse     {         // здесь осуществляете всю логику вашего метода API         $id = $request->getId();         return new GetProductsResponse($request->getTitle().'OLOLOLOLO');     } }

Теперь вы можете выполнить curl-запрос например так:

curl --header "Content-Type: application/json" --request POST --data '{"jsonrpc": "2.0","method": "getProducts","params": {"title": "AZAZAZA"},"id": 1}' http://localhost/api/v1

И получите ответ:

{"jsonrpc":"2.0","result":{"title":"AZAZAZAOLOLOLOLO","success":true},"id":null}

Получается для реализации простейшего Json RPC API вам потребуется создать всего 3 класса.

Авторизация по токену

Авторизация по токену реализуется стандартно по документации symfony, но все же расскажу еще разок. 

Чтобы доступ в ваше API был по токену — вот пример реализации что для этого нужно сделать:

1) создаем сущность нашего токена в БД в файле 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;      /**      * @return int      */     public function getId(): int     {         return $this->id;     }          /**      * @param int $id      *      * @return ApiToken      */     public function setId(int $id): ApiToken     {         $this->id = $id;              return $this;     }          /**      * @return string      */     public function getToken(): string     {         return $this->token;     }          /**      * @param string $token      *      * @return ApiToken      */     public function setToken(string $token): ApiToken     {         $this->token = $token;              return $this;     }          /**      * @return DateTimeInterface      */     public function getExpiresAt(): DateTimeInterface     {         return $this->expiresAt;     }          /**      * @param DateTimeInterface $expiresAt      *      * @return ApiToken      */     public function setExpiresAt(DateTimeInterface $expiresAt): ApiToken     {         $this->expiresAt = $expiresAt;              return $this;     }          /**      * @return User      */     public function getUser(): User     {         return $this->user;     }          /**      * @param User $user      *      * @return ApiToken      */     public function setUser(User $user): ApiToken     {         $this->user = $user;              return $this;     }          /**      * @return bool      */     public function isValid(): bool     {         return (new DateTime())->getTimestamp() > $this->expiresAt->getTimestamp();     }  }

2) создаем кастомный аутентификатор в файле 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 {     /**      * @param EntityManagerInterface $em      */     public function __construct(         private readonly EntityManagerInterface $em     ){     }      /**      * @param Request $request      *      * @return bool      */     public function supports(Request $request): bool     {         return str_contains($request->getRequestUri(), '/api/v');     }          /**      * @param Request $request      *      * @return Passport      */     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());             }         ));     }          /**      * @param Request        $request      * @param TokenInterface $token      * @param string         $firewallName      *      * @return Response|null      */     public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response     {         return null;     }          /**      * @param Request                 $request      * @param AuthenticationException $exception      *      * @return Response|null      */     public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response     {         $data = [             'message' => strtr($exception->getMessageKey(), $exception->getMessageData())         ];              return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);     }  }

3) добавляем новый firewall в файле config/security.yaml

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

4) создаем миграцию для токена, проводим миграцию, создаем какой-нибудь тестовый токен и можем пользоваться. ))) С токеном запрос curl будет выглядеть примерно так:

curl --header "X-AUTH-TOKEN: your-token-here" --header "Content-Type: application/json" --request POST --data '{"jsonrpc": "2.0","method": "getProducts","params": {"title": "AZAZAZA"},"id": 1}' http://localhost/api/v1

Оригинал статьи


ссылка на оригинал статьи https://habr.com/ru/articles/754134/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *