Несколько месяцев назад я написал эту статью (https://medium.com/@rebolon/symfony-is-not-dead-thanks-to-vuejs-99cdf75f57b) о Symfony 4 и VueJS (и более глобально обо всех современных фреймворках Javascript ). С этого времени я работаю неполный рабочий день над доказательством концепции, цель которой - объяснить, как создать API с помощью Symfony4 для внешнего интерфейса JS. Потому что, когда вы работаете с интерфейсом на Javascript, вам понадобится API на вашем сервере для получения информации и сохранения данных. Итак, теперь, когда Symfony 4 доступен уже несколько месяцев, как мне создать API?

Исторически сложилось так, что разработчики Symfony использовали для интеграции FOSRestBundle для реализации веб-сервисов. Но проблема в том, что FOSRest требует множества конфигураций и обычно бывает излишним для огромного большинства проектов.
2 года назад появился новый проект под названием ApiPlatform. Целью этого проекта было предоставить дистрибутив Symfony, созданный для проектов API. Многие люди приветствуют этот проект, потому что он многое сделал из коробки. Поэтому я решил дать ему шанс.

Платформа Api действительно очень проста в установке с использованием новой архитектуры Flex. Просто взгляните на этот файл для дополнительных объяснений.

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

#open the following file and change the prefix to make ApiPlatform available on /api route
#config/routes/api_platform.yaml
api_platform:
    resource: .
    type: api_platform
    prefix: api

Затем вам нужно немного настроить api_platform следующим образом:

#config/packages/api_platform.yaml
api_platform:
    title: 'My comics library'
    description: 'Demo of an API built with Api-Platform v2 (use REST or GraphQL). My comics library allow to manage your comics collection in a simple way'
    version: '1.0.0'
    mapping: # path list to your entities which should be available on API
        paths: ['%kernel.project_dir%/src/Entity']
    formats: #which format is managed by your API
        jsonld:  ['application/ld+json']
        json:    ['application/json']
        html:    ['text/html']
    graphql: #need GraphQL (instead of or with REST) ? just add this
        graphiql:
            enabled: true
# more about configuration on this page: https://api-platform.com/docs/core/configuration

Затем вам нужно будет создать свои сущности и не забыть установить ограничения для свойств этих сущностей. Просто не забудьте создать свои объекты по путям, указанным в файле конфигурации (выше), или добавить новый путь !!!
Например: (полный файл объекта доступен здесь)

<?php
namespace App\Entity\Library;

use ...

/**
 * @ApiResource(
 *     iri="http://bib.schema.org/ComicStory",
 *     attributes={"access_control"="is_granted('ROLE_USER')"},
 *     collectionOperations={"get"={"method"="GET"},"post"={"method"="POST"}},
 *     itemOperations={
 *         "get"={"method"="GET"},"put"={"method"="PUT"},"delete"={"method"="delete"},
 *         "special_3"={"route_name"="book_special_sample3"},
 *     }
 * )
 * @ORM\Entity
 */
class Book implements LibraryInterface
{
    /**
     * @ApiProperty(
     *     iri="http://schema.org/identifier"
     * )
     *
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ApiProperty(
     *     iri="http://schema.org/headline"
     * )
     *
     * @ORM\Column(type="string", length=255, nullable=false)
     *
     * @Assert\NotBlank()
     * @Assert\Length(max="512")
     *
     */
    private $title;

    /**
     * @ApiProperty(
     *     iri="http://schema.org/reviews"
     * )
     * @ApiSubresource(maxDepth=1)
     *
     * @ORM\OneToMany(targetEntity="App\Entity\Library\Review", mappedBy="book", orphanRemoval=true)
     */
    private $reviews;
...

    /**
     * Book constructor.
     */
    public function __construct()
    {
        $this->reviews = new ArrayCollection();
    }
...
}

Что важно в этой сущности?

  • Чтобы представить свою сущность с помощью ApiPlatform, вы должны добавить аннотацию @ApiResource над классом
  • Эта аннотация будет описывать:
    * iri для описания типа объекта на основе словаря schema.org (необязательно)
    * access_control для защиты ваших маршрутов с помощью ролевой системы Symfony
    * collectionOperations to указать ApiPlatform, какие глаголы из GET / POST разрешены, и определить пользовательские маршруты (special_3 в POST) для этих глаголов
    * itemsOperations, чтобы указать ApiPlatform, какие глаголы из GET / PUT / DELETE являются разрешено и определить пользовательские маршруты для этих глаголов (special_1 и 2 в GET)
  • Чтобы упростить проверку, используйте ограничения Symfony, как это сделано здесь, со свойством `title` (NotBlank и Length‹ 512)
  • Пусть вас не смущают аннотации @ ORM и @ Assert, потому что одна предназначена для ORM (Doctrine) для настройки схемы базы данных, а другая - для проверки сущности с помощью компонента Symfony Validator. Многие разработчики обычно пропускали Assert, потому что думали, что ORM может использоваться валидатором, но это ошибка.

Вот и все ! просто с этими вещами ваши объекты будут доступны ApiPlatform. Но вы быстро обнаружите, что этот образец слишком мал. В реальной жизни вам нужны собственные маршруты, в которые вы можете добавлять книги с их редакторами и авторами. И вы не будете отправлять эту информацию с помощью системы ключ / значение (что обычно делается с помощью Symfony Forms), а с чистым объектом JSON, который содержит вложенные сущности.
И вот тут-то и наступит сложная часть. Информации об этом не так много, за исключением того факта, что ApiPlatform основана на шаблоне Action Domain Responder (по сравнению с MVC) и что вы можете указать новую конечную точку из своей сущности (см. Аннотацию ApiResource.collectionsOperations / itemsOperations с именем special_1 / 2/3). Но информации о том, как построить новую конечную точку, немного (возможно, она есть, но я ее не нашла…). Я хотел добавить дополнительную информацию в Swagger, я также хотел использовать прослушиватели ApiPlatform для сериализации исключений в красивый контент Json ...

Поэтому мне пришлось изучить код и вот что я решил:
* я использую ParamConverter для восстановления объекта из содержимого json, полученного в запросе
* я использую ApiPlatform \ Core \ Bridge \ Symfony \ Validator \ Exception \ ValidationException, чтобы инкапсулировать мои исключения. Таким образом, слушатель ApiPlatform примет исключение и сериализует его в желаемом формате.
* Я попытался перегрузить определение Swagger через это объяснение.

Теперь я могу написать новое действие для своих пользовательских маршрутов следующим образом:

// src\App\Action\Bookspecial.php
/**
 * Custom route to do POST operation over Book entity with all nested relations
 * It uses ParamConverter usage to reduce the responsability of the controller
 *
 * @Route(
 *     name="book_special_sample3",
 *     path="/api/booksiu/special_3"
 * )
 * @ParamConverter(name="book", converter="book")
 * @Method("POST")
 *
 * @param Book $book
 * @return JsonResponse
 */
public function special3(Book $book)
{
    if ($book) {
        $this->em->persist($book);

        $this->em->flush();

        $iris = $this->router->generate('api_books_get_item', ['id' => $book->getId(), ]);

        $response = $this->serializer->serialize($book, 'json');
    } else {
        return new Response('No Content', 204);
    }

    // todo return a 201 with iris to book, use the router to build the iris
    return new JsonResponse($response, 201, [], true);
}

Вы можете видеть, что контроллер ничего не делает, кроме как пытается сохранить сущность Book, если она хорошо предоставлена ​​запросом. Все управляется моими ParamConverters.
Эти ParamConverters объявлены как служба с правильными тегами, например:

# config/services.yaml
App\Request\ParamConverter\Library\BookConverter:
    public: true
    arguments:
        - '@validator'
    tags:
        - { name: request.param_converter, priority: -2, converter: book }

И вот как я решил управлять ParamConverter. Прежде всего, я думаю, что мне нужно использовать Symfony Serializer для улучшения процесса, и я могу использовать его в будущем. Затем вы должны знать, что если вы не используете ApiPlatform \ Core \ Bridge \ Symfony \ Validator \ Exception \ ValidationException, ваше исключение не будет перехвачено прослушивателями ApiPlatform, и в вашем ответе не будет сериализованных ошибок. это может быть довольно неприятно, если вы ожидаете JSON!
Каждый ParamConverter расширяет AbstractConverter. И всем придется реализовать ParamConverterInterface.

Что для этого нужно?

  • По крайней мере, константа NAME. Он будет использоваться для идентификации ParamConverter в аннотации Action / Controller в ключе имени, но он также будет использоваться для идентификации корневого свойства объекта json.
  • Каждый ParamConverter должен будет реализовать getEzPropsName / getManyRelPropsName / getOneRelPropsName и initFromRequest.

getEzPropsName: - это просто список строк, представляющих поля, доступные в содержимом json из запроса. Для книги у вас есть

/**
 * {@inheritdoc}
 * for this kind of json:
 * { 
 *   "author": {
 *     "firstname": "Paul", 
 *     "lastname": "Smith"
 *   }
 * }
 */
function getEzPropsName(): array
{
    return ['id', 'firstname', 'lastname', ];
}

getManyRelPropsName / getOneRelPropsName: они могут возвращать список строк или ассоциативный массив, где ключом является имя свойства json, а значение может иметь следующие ключи:

// src/App/Request/ParamConverter/Library/ProjectBookEditionConverter.php
/**
 * {@inheritdoc}
 * for this kind of json:
 * {
 *   "editors": {
 *     "editor": { ... }
 *   }
 * }
 */
function getOneRelPropsName():array {
    return [
        'editor' => [
            'converter' => $this->editorConverter, 
            'registryKey' => 'editor', 
        ],
    ];
}
// src/App/Request/ParamConverter/Library/BookConverter.php
/**
 * {@inheritdoc}
 * for this kind of json:
 * {
 *   "book": {
 *     "authors": { ... }
 *   }
 * }
 */
function getManyRelPropsName():array
{
    return [
        'authors' => [
            'converter' => $this->projectBookCreationConverter, 
            'setter' => 'setAuthor',
            'cb' => function ($relation, $entity) {
                $relation->setBook($entity);
            },
        ],
    ];
}
  • преобразователь: экземпляр преобразователя для использования для связанного свойства (преобразователь вводится с помощью Symfony Dependancy Injection)
  • RegistryKey: где хранить объект, чтобы предотвратить дублирование вставки (когда вы публикуете новую Книгу с двумя редакциями, которые связаны с одним и тем же редактором, например).
  • setter: метод, который нужно вызвать для добавления новой дочерней сущности к родительской.
  • cb: вызываемый объект, который позволяет добавить родителя в новую подобъекту (или любые другие операции, требующие обратного вызова).

Наконец, initRequest является основным организатором ParamConverter. Он должен создать связанную сущность, проверить достоверность содержимого json, вызвать методы сборки *, проверить новую сущность и вернуть эту новую сущность. В этом методе вы также должны управлять исключениями, которые затем будут перехватывать прослушиватели ApiPlatform.

Что мне еще нужно сделать:
* управлять радужной оболочкой или идентификаторами из json, отправленных в запросе POST (например, я могу просто создать новые объекты и дедуплицировать их из json, но я не управляю узлом Json, который не является объектом, но просто строка с идентификатором существующего объекта или IRIS для этого объекта)
* играйте с фильтрами, потому что это кажется мощным
* поиск для собственной проверки ApiPlatform: изначально ApiPlatform просто использует аннотацию Orm для проверки свойство, но это когда вы пишете свои собственные маршруты, для которых вам могут понадобиться ограничения Symfony (что я сделал). Возможно, можно было бы использовать ApiPlatform в моих пользовательских маршрутах, но я не исследовал это, и, возможно, мне придется.
*
использовать безопасность через JWT или ApiKey
* поиграйте с GraphQL из ApiPlatform для выполнения пакетных запросов и пакетных мутаций

Итак, буду ли я использовать ApiPlatform в будущем? Может быть. Продукт кажется интересным, но для небольших компаний требуется много исследований, чтобы его можно было полностью использовать в реальной жизни. Может быть, если компания Les-Tilleuls, стоящая за этим проектом, проведет повышение квалификации, я смогу это сделать. Или, если есть больше руководств о том, как эффективно использовать его с большим проектом, чем образец с Книгой и N обзорами, я также могу пересмотреть свою позицию. Потому что очень часто в небольших компаниях у вас не так много времени, чтобы работать, поэтому вы должны действовать эффективно и вам не нужно проводить мозговой штурм о том, как использовать инструмент. Небольшой проект, над которым я работаю, чтобы продемонстрировать возможности Symfony 4 на API, - это только начало, но я не уверен, что у него будет больше времени для изучения всех функций Api-Platform. Я надеюсь, что этот проект может помочь вам использовать Symfony и ApiPlatform с интерфейсом JS. Тут уж точно не хватает системы аутентификации. JWT vs ApiKey - моя следующая задача на бэкэнде, чтобы иметь возможность создавать эффективные приложения.

Хотите узнать о проекте, который иллюстрирует мою статью? Посмотрите здесь https://github.com/Rebolon/php-sf-flex-webpack-encore-vuejs

Шаблон, использованный здесь, определенно не идеален. Я думаю, что могу улучшить AbstractConverter и многие другие вещи, но не забывайте, что это результат Proof Of Concept. Я не участвовал в разработке ядра ApiPlatform, поэтому эта статья - всего лишь мой небольшой вклад в помощь разработчикам.

И если у вас есть дополнительная информация о том, как улучшить мои небольшие навыки работы с Symfony 4 и платформой Api, добро пожаловать.
Теперь я обязательно буду работать над фронтендом с DevXpress / Sencha или даже Quasar Framework. Цель состоит в том, чтобы показать, как сегодня создавать сложные интерфейсы с меньшими затратами на компоненты.

[Edit 1:] Я извлек AbstractParamConverter из POC в автономный репозиторий. Вы можете использовать его, просто добавив его с помощью composer в свой проект Symfony4: `rebolon / api-json-param-converter`

[Edit 2:] Я добавил образец DevXpress (с angular5) в проект poc и могу сказать, что Sf4 + ApiPlatform + DevXpress - это довольно круто. Скорее буду использовать его в будущем.