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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

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

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

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

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

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-&gt;id = $id;
    }
    
    /**
     * @return int
     */
    public function getId(): int
    {
        return $this-&gt;id;
    }
    
    /**
     * @param int $id
     */
    public function setId(int $id): void
    {
        $this-&gt;id = $id;
    }
    
    /**
     * @return string
     */
    public function getTitle(): string
    {
        return $this-&gt;title;
    }
    
    /**
     * @param string $title
     */
    public function setTitle(string $title): void
    {
        $this-&gt;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-&gt;success = $success;
        $this-&gt;title = $title;
    }
    
    /**
     * @return string
     */
    public function getTitle(): string
    {
        return $this-&gt;title;
    }
    
    /**
     * @param string $title
     */
    public function setTitle(string $title): void
    {
        $this-&gt;title = $title;
    }
    
    /**
     * @return bool
     */
    public function isSuccess(): bool
    {
        return $this-&gt;success;
    }
    
    /**
     * @param bool $success
     */
    public function setSuccess(bool $success): void
    {
        $this-&gt;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-&gt;id;
    }
    
    /**
     * @param int $id
     *
     * @return ApiToken
     */
    public function setId(int $id): ApiToken
    {
        $this-&gt;id = $id;
    
        return $this;
    }
    
    /**
     * @return string
     */
    public function getToken(): string
    {
        return $this-&gt;token;
    }
    
    /**
     * @param string $token
     *
     * @return ApiToken
     */
    public function setToken(string $token): ApiToken
    {
        $this-&gt;token = $token;
    
        return $this;
    }
    
    /**
     * @return DateTimeInterface
     */
    public function getExpiresAt(): DateTimeInterface
    {
        return $this-&gt;expiresAt;
    }
    
    /**
     * @param DateTimeInterface $expiresAt
     *
     * @return ApiToken
     */
    public function setExpiresAt(DateTimeInterface $expiresAt): ApiToken
    {
        $this-&gt;expiresAt = $expiresAt;
    
        return $this;
    }
    
    /**
     * @return User
     */
    public function getUser(): User
    {
        return $this-&gt;user;
    }
    
    /**
     * @param User $user
     *
     * @return ApiToken
     */
    public function setUser(User $user): ApiToken
    {
        $this-&gt;user = $user;
    
        return $this;
    }
    
    /**
     * @return bool
     */
    public function isValid(): bool
    {
        return (new DateTime())-&gt;getTimestamp() &gt; $this-&gt;expiresAt-&gt;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-&gt;getRequestUri(), '/api/v');
    }
    
    /**
     * @param Request $request
     *
     * @return Passport
     */
    public function authenticate(Request $request): Passport
    {
        $apiToken = $request-&gt;headers-&gt;get('X-AUTH-TOKEN');
        if (null === $apiToken) {
            throw new CustomUserMessageAuthenticationException('No API token provided');
        }
    
        $apiTokenEntity = $this-&gt;em-&gt;getRepository(ApiToken::class)-&gt;findOneBy(['token' =&gt; $apiToken]);
        if (is_null($apiTokenEntity)) {
            throw new CustomUserMessageAuthenticationException('No API token provided');
        }
    
        return new SelfValidatingPassport(new UserBadge(
            $apiTokenEntity-&gt;getUser()-&gt;getId(),
            function () use ($apiTokenEntity) {
                return $this-&gt;em-&gt;getRepository(User::class)-&gt;find($apiTokenEntity-&gt;getUser()-&gt;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' =&gt; strtr($exception-&gt;getMessageKey(), $exception-&gt;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/


Интересные статьи

Интересные статьи

Здравствуйте, дорогие читатели. В данной статье описана реализация ещё одного, очередного, JSON-парсера, который способен извлекать целые JSON-объекты с содержимым из текста. Данный парсер использует ...
Это непростое условное выполнение Читать далее
Потребность во временной локализации продукта возникает, когда продукт вырастает до таких масштабов, при которых необходима работа в разных временных зонах (очевидность). Хочется описать вариант ...
Всем привет! В данной статье рассмотрим разработку фронта простенького блога на Vue с использованием всех прелестей Vue включая Vuex и Router. А также поговорим про структуру приложения и раб...
Три года назад Марина Вязовска из Швейцарского федерального технологического института в Лозанне поразила математиков, обнаружив самый плотный способ упаковки сфер одинакового размера в восьм...