Выпущен релиз второй версии Sunrise Router и Скелетона на его основе

Обзор новой версии инструмента для разработки REST API на PHP 7.1+, с поддержкой OpenAPI спецификации для документирования и JSON Schema спецификации для валидации запросов и ответов.

Sunrise Router – маршрутизатор написанный на PHP, базирующийся на PSR-7 и PSR-15, с поддержкой аннотаций, и с недавнего времени, поддержкой OpenAPI спецификации и частично JSON Schema спецификации.

OpenAPI (в прошлом Swagger) – спецификация позволяющая описывать REST API (далее просто API), в том числе, принимаемые и возвращаемые структуры данных использую JSON Schema спецификацию (точнее ее расширенное подмножество).

JSON Schema – спецификация позволяющая описывать JSON структуры данных, в том числе, описывать правила валидации таких данных. Следовательно, имея JSON схему, набор данных и определенный инструмент (justinrainbow/json-schema), можно провалидировать такие данные.

Swagger (сегодня) – это инструментарий, служащий упростить разработку API, где самым примечательным, назовем его инструментом, я бы назвал Swagger UI, который в свою очередь, может стать для вас заменой таких REST клиентов, как Postman, Paw и т.д.

Мы затронули ряд спецификаций, но не упомянули JSON API спецификацию, о ней в статье речь не пойдет, однако если вы разработчик API, и вас тяготят сомнения, что отдаваемые вашим приложением структуры данных правильные, попробуйте ознакомится с выше упомянутой спецификацией.

Лень читать, что там?

Поддержка OpenAPI и JSON Schema спецификаций

В моем понимании, API должно быть задокументировано, и это ключевая проблема, точнее, на мой взгляд, проблема в переключении контекстов для разработчика.

Случай из практики: вы разработали некий контроллер в приложении, далее, вы переключаете контекст, описываете такой контроллер, допустим, в Postman или любом другом REST клиенте, далее, вы открываете некий README.md, где дополнительно описываете работу такого контроллера… Оставим за скобками процессы валидации, но вернемся к ним чуть позже… Вот только в какой-то момент, все пойдет не по плану, и документация не будет соответствовать действительно.

Рассмотрим другую ситуацию: вы в курсе выше упомянутых спецификаций, используете, допустим, инструмент zircote/swagger-php, и сталкиваетесь с несколькими проблемами:

  1. Маршрутизация и пакет упомянутый выше ничего не знают друг о друге, я вынужден дублировать данные маршрутизации;
  2. Описание объекта RequestBody несет исключительно информационный характер, я вынужден дублировать описание валидации на уровне кода;
  3. Описание объекта Response несет исключительно информационный характер, я вынужден дублировать описание валидации, скажем, на уровне кода, но уже интеграционных тестов;
  4. Reference Object ссылается в никуда, главное, чтобы это «никуда», было описано где-нибудь. Лично меня такой подход не устраивает;
  5. Описание самого приложения происходит также как и описание, скажем, операций, то есть на уровне аннотаций. Лично меня такой подход, также не устраивает.

Что предлагает Sunrise Router:

  1. Описывая контроллер, вы описываете его как операцию (Operation Object), опуская объекты Paths и Path Item, тем самым вы не дублируете имя маршрута, HTTP метод(ы), URI, атрибуты и правила их валидации;
  2. Описание RequestBody объекта будет использовано для валидации запросов, путем конвертации OpenAPI Schema Object в JSON Schema Validation;
  3. Описание Response объекта может быть использовано для валидации ответов на уровне интеграционных тестов за пределами приложения, путем конвертации OpenAPI Schema Object в JSON Schema Validation;
  4. Reference Object ссылается на классы или их методы/свойства, кому-то это покажется логикой в аннотациях, для меня это равносильно конструкции @Entity(repositoryClass=""), то есть, нормальным явлением;
  5. Описание самого приложения происходит уже на уровне кода, а не аннотаций.

Таким образом, в моем представлении, этапы разработки API могут быть следующие:

  1. Вы создаете и описываете сущности без логики;
  2. Вы создаете и описываете контроллеры возвращающие фейковые данные;
  3. Зависимые от API люди могут включаться в работу, так как на этом этапе у нас имеется документация и валидация;
    • Front разработчик может приступать к реализации интерфейсов;
    • Тестер может начинать писать интеграционные тесты;
    • Вы спокойно реализуете логику в API, так как на этом этапе, в теории, все согласовано со всех сторон.

Считайте схему выше, не более чем, мыслями в слух, это примерно та схема, к которой я пытаюсь прийти сегодня. То есть, уделять больше времени именно проектированию, и договариваться с командой в процессе проектирования.

От теории к практике, типичный контроллер:

/**  * @Route(  *   name="api.entry.update",  *   path="/api/v1/entry/{id<\d+>}",  *   methods={"PATCH"},  * )  */ final class EntryUpdateController implements RequestHandlerInterface {     public function handle(ServerRequestInterface $request) : ResponseInterface     {         return (new ResponseFactory)->createResponse(200);     } } 

Как видим, контроллер связан с маршрутом, который в свою очередь, имеет: имя, HTTP метод(ы), URI, атрибут id и регулярное выражение \d+ для его валидации. Теперь, не дублируя эти данные, опишем наш контроллер:

/**  * @Route(...)  *  * @OpenApi\Operation(  *   requestBody=@OpenApi\RequestBody(  *     content={  *       "application/json": @OpenApi\MediaType(  *         schema=@OpenApi\Schema(  *           type="object",  *           required={"name"},  *           properties={  *             "name"=@OpenApi\Schema(  *               type="string",  *               minLength=1,  *               maxLength=255,  *               nullable=false,  *             ),  *           },  *         ),  *       ),  *     },  *   ),  *   responses={  *     200: @OpenApi\Response(  *       description="OK",  *     )  *   },  * )  */ final class EntryUpdateController implements RequestHandlerInterface {     // some code... }

Как видно из примера выше, мы имеем правило валидации для тела запроса, дублировать его, скажем в сервисе, я тоже не хочу, поэтому, я подключу промежуточное ПО:

/**  * @Route(...,  *   middlewares={  *     "Sunrise\Http\Router\OpenApi\Middleware\RequestBodyValidationMiddleware",  *   },  * )  *  * @OpenApi\Operation(...)  */ final class EntryUpdateController implements RequestHandlerInterface {     // some code... }

Конечно, от валидации в коде избавиться полностью не удастся, но тем не менее, нам остается только проверить существование записи в хранилище.

Для тех, кому пример с подключением промежуточного ПО показался неприемлемым

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

 use App\Controller\EntryUpdateController; use Sunrise\Http\Router\OpenApi\Middleware\RequestBodyValidationMiddleware;  $collection->patch('api.entry.update', '/api/v1/entry/{id<\d+>}', new EntryUpdateController())     ->addMiddleware(new RequestBodyValidationMiddleware()); 

Исходя из примеров выше, можно предположить, что схема свойства name может повторяться от контроллера к контроллеру, и дублировать такую информацию, мне бы тоже не хотелось. Поэтому я воспользуюсь Reference объектом…

Допустим, у нас имеется сущность Entry:

final class Entry {      /**      * @OpenApi\Schema(      *   refName="EntryName",      *   type="string",      *   minLength=1,      *   maxLength=255,      *   nullable=false,      * )      */     private $name; }

Теперь изменим описание нашего контроллера:

/**  * @Route(...)  *  * @OpenApi\Operation(  *   requestBody=@OpenApi\RequestBody(  *     content={  *       "application/json": @OpenApi\MediaType(  *         schema=@OpenApi\Schema(  *           type="object",  *           required={"name"},  *           properties={  *             "name"=@OpenApi\SchemaReference(  *               class="App\Entity\Entry",  *               property="name",  *             ),  *           },  *         ),  *       ),  *     },  *   ),  *   responses={  *     200: @OpenApi\Response(  *       description="OK",  *     )  *   },  * )  */ final class EntryUpdateController implements RequestHandlerInterface {     // some code... }

Обратите внимание на аннотацию @OpenApi\SchemaReference(), объект Reference применим не только к схемам, но и ко всем другим объектам, которые допускает OpenAPI спецификация. Однако, если описание вам кажется все равно длинным, вы можете пойти дальше, и всю схему тела запроса вынести в метод, скажем, сервиса: EntryService::updateById(...)… Можно пойти еще дальше, и воспользоваться объектом RequestBodyReference… но я бы не стал выносить его за пределы контроллера… По личным соображениям, ограничений на этот счет никаких нет.

Больше примеров вы можете найти в репозитории скелетона, который доступен по ссылке.

Про поддержку аннотаций в IDE

Краткий список ключевых изменений в маршрутизаторе

  • Контроллер теперь имплементирует RequestHandlerInterface, а не MiddlewareInterface – позволило отказаться от ResponseFactory из коробки и абстрагироваться от Sunrise PSR-7 implementation;
  • Маршрут теперь имплементирует RequestHandlerInterface – позволило производить редиректы на уровне маршрутизатора;
  • Маршрутизатор теперь наряду с прочим имплементирует MiddlewareInterface – дает больше возможностей для обработки ошибок и интеграции в уже существующую архитектуру;
  • Маршрутизатор научился собирать URI маршрутов;
  • Появилась возможность загрузки маршрутов из конфигов;
  • Изменилась логика разбора URI, второй и выше уровень вложенности необязательных частей URI больше недопустим – ранее допускалась конструкция в виде /(foo/(bar/(baz))), сейчас это приведет к ошибке;
  • Появилась возможность вешать промежуточное ПО (и не только) на группы.

Краткий список ключевых изменений в скелетоне

  • Поставляется с воркером для RoadRunner и командой для генерации Systemd Unit – ранее использовался другой репозиторий;
  • Изменена логика хранения конфигурационных файлов;
  • Интегрирован Symfony Console компонент;
  • Решена проблема с настройкой окружения для запуска тестов;
  • Настроена обработка ошибок.

Несколько слов о DI и RoadRunner

Крайне не рекомендую, ведя разработку на RoadRunner, использовать внедрение любых зависимостей через инъекции, за исключением самого контейнера. Это обусловлено глобальным состоянием объектов.


В игру «зачем писать это, когда есть это» можно играть бесконечно, но как бы там ни было, open-source все стерпит…

Спасибо всем кто принимал прямое или косвенное участие в разработке, особенно WinterSilence за его толчки в нужный момент!

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

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

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