Ques/Help/Req Обзор библиотеки API Platform v3.0

XakeR

Member
Регистрация
13.05.2006
Сообщения
1 912
Реакции
0
Баллы
16
Местоположение
Ukraine
В сентябре 2022 года была выпущена новая версия библиотеки API Platform. Она включала в себя несколько серьезных модификаций, меняющих основной подход разработки API с использованием этой библиотеки, которого предпочитали придерживаться многие разработчики.

Веб-разработчик Noveo Александр знакомит читателей с новыми стратегиями разработки, представленными в версии v3. Кроме того, он уделит некоторое внимание инструментам и подходам, которые с тех пор были удалены или объявлены устаревшими.

Некоторые из них были довольно полезными при решении многих задач, поэтому Александр предложит собственное решение по интеграции старых тактик в новую систему API Platform.

Ниже приведены основные изменения, которые будут приняты во внимание в этой статье:

  • При объявлении операции больше нет разграничений между коллекциями сущностей и единичными сущностями.
  • ApiPlatformCoreDataTransformerDataTransformerInterface получил пометку deprecated и будет полностью удалён в версии 3.0. В качестве альтернативы будет использоваться State Providers.
  • Операции с подресурсами теперь обозначаются в отдельном атрибуте #[ApiResource].

В статье разработчик Новео Александр расскажет вам об этих изменениях и рассмотрит каждое из них на примерах.

Обзор библиотеки API Platform v3.00


State Processors и State Providers​


Как было отмечено ранее, Data Transformers были упразднены, а в качестве альтернативы представлены State Providers и State Processors, основные задачи которых — предоставлять данные клиенту и обрабатывать данные от клиента соответственно.

Если вкратце, основная цель State Providers — предоставить доступ к объекту, хранящемуся в базе данных. State Processors, наоборот, нужны для обработки поступающих данных и сохранения их в базе данных (если необходимо) при обработке http-запроса.

Для того, чтобы API Platform могла получать данные извне или извлекать их из базы данных, по умолчанию используются State Providers и State Processors, взаимодействующие с Doctrine ORM.

Вы можете настроить свои собственные State Providers, если хотите получать данные из другого источника (Elasticsearch, MongoDB и т. д.) или изменять данные перед отправкой ответа сервера.

State Provider​


Получение объекта

Предположим, у нас есть объект User. У него есть поля, которые мы хотим отобразить в запросе GET. Например:

{ «id»: integer, «firstName»: «string», «lastName»: «string», «email»: «string», «phone»: «string», «createdAt»: «string» }

Мы имеем следующие способы реализации:

  • Обозначить группы нормализации для операции GET и назначить группы нормализации с помощью аннотации #[Groups()] в классе сущности для полей, которые необходимо отобразить.
  • Создать объект передачи данных (DTO), назначить #[Groups()] полям DTO и назначить State Provider для операции GET.

Первый подход детально описан в официальной документации API Platform, поэтому мы сосредоточимся на втором. Во-первых, давайте начнём с создания класса DTO для этого объекта. Код класса DTO показан ниже:

<?php declare(strict_types=1); namespace AppDtoApiUser; use DateTimeInterface; use SymfonyComponentSerializerAnnotationGroups; class UserOutputDto { #[Groups([‘User:read’])] public int $id; #[Groups([‘User:read’])] public string $firstname; #[Groups([‘User:read’])] public ?string $lastname = null; #[Groups([‘User:read’])] public ?string $email = null; #[Groups([‘User:read’])] public string $phone; #[Groups([‘User:read’])] public DateTimeInterface $createdAt; public function __construct( int $id, string $firstname, ?string $lastname, ?string $email, string $phone, DateTimeInterface $createdAt ) { $this->id = $id; $this->firstname = $firstname; $this->lastname = $lastname; $this->email = $email; $this->phone = $phone; $this->createdAt = $createdAt; } }

В приведённом выше коде мы определили поля, которые мы хотим отображать для запроса GET, и группы сериализации для каждого свойства. Следующим шагом является создание класса State Provider, который позволит нам извлекать пользователя из базы данных. Все поставщики данных должны реализовать ApiPlatformStateProviderInterface, который применяется как к операциям с коллекциями, так и к операциям с единичными объектами. С этого момента я предлагаю создать CollectionProviderInterface и ItemProviderInterface, которые расширяют ApiPlatformStateProviderInterface.

Код интерфейса CollectionProviderInterface:

<?php declare(strict_types=1); namespace AppStateProvider; use ApiPlatformStateProviderInterface; interface CollectionProviderInterface extends ProviderInterface { }

Код интерфейса ItemProviderInterface:

<?php declare(strict_types=1); namespace AppStateProvider; use ApiPlatformStateProviderInterface; interface ItemProviderInterface extends ProviderInterface { }

После этого добавим в файл config/services.yaml следующую конфигурацию:

… parameters: … services: … _instanceof: AppStateProviderCollectionProviderInterface: bind: $collectionProvider: ‘@api_platform.doctrine.orm.state.collection_provider’ AppStateProviderItemProviderInterface: bind: $itemProvider: ‘@api_platform.doctrine.orm.state.item_provider’ …

Мы указали, что State Providers, ответственные за получение одного объекта, должны получать Doctrine Item Provider в качестве аргумента. В то же время State Providers, ответственные за получение коллекции объектов, должны получать Doctrine Collection Provider для извлечения необходимых объектов из базы данных.

Перед созданием нашего первого класса State Provider я советую создать класс DataTransformer, основная роль которого заключается в преобразовании сущности в DTO. Код класса UserOutputGetDataTransformer показан ниже:

<?php declare(strict_types=1); namespace AppDataTransformerApiUser; use AppDtoApiUserUserOutputDto; use AppEntityUser; class UserOutputGetDataTransformer { public function transform(User $user): UserOutputDto { return new UserOutputDto( $user->getId(), $user->getFirstname(), $user->getLastname(), $user->getEmail(), $user->getPhone(), $user->getCreatedAt() ); } }

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

Код класса представлен ниже:

<?php declare(strict_types=1); namespace AppStateProviderUser; use ApiPlatformExceptionItemNotFoundException; use ApiPlatformMetadataOperation; use ApiPlatformStateProviderInterface; use AppDataTransformerApiUserUserOutputGetDataTransformer; use AppDtoApiUserUserOutputDto; use AppStateProviderItemProviderInterface; class UserProvider implements ItemProviderInterface { public function __construct( private ProviderInterface $itemProvider, private UserOutputGetDataTransformer $dataTransformer ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): UserOutputDto { $user = $this->itemProvider->provide($operation, $uriVariables, $context) ?? throw new ItemNotFoundException(‘Not Found’); return $this->dataTransformer->transform($user); } }

Метод __construct класса UserProvider принимает два аргумента:

  • $itemProvider будет использоваться для получения сущности из базы данных.
  • $dataTransformer будет преобразовывать исходный объект в представление DTO.

Проще простого, не так ли?

Наконец, мы должны назначить UserProvider методу GET класса User. Здесь мы указываем вывод: UserOutputDto::class и провайдер: UserProvider::class:

<?php declare(strict_types=1); namespace AppEntity; use ApiPlatformMetadataApiResource; … use SymfonyComponentSecurityCoreUserPasswordAuthenticatedUserInterface; use SymfonyComponentSecurityCoreUserUserInterface; #[ORMEntity(repositoryClass: UserRepository::class)] #[ORMTable(name: ‘`user`’)] #[ApiResource( operations: [ new Get( uriTemplate: ‘/users/{id<d+>}’, output: UserOutputDto::class, provider: UserProvider::class, ), ], normalizationContext: [‘groups’ => [‘User:read’]], )] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORMId] #[ORMGeneratedValue] #[ORMColumn] private int $id; #[ORMColumn(length: 255, nullable: true, unique: true)] private ?string $email = null; #[ORMColumn(length: 255, unique: true)] private string $phone; #[ORMColumn(length: 255)] private string $password; #[ORMColumn(length: 255)] private string $firstname; #[ORMColumn(length: 255, nullable: true)] private ?string $lastname = null; #[ORMColumn(nullable: false)] private array $roles = [‘ROLE_USER’]; #[ORMColumn] private DateTimeImmutable $createdAt; #[ORMColumn(nullable: true)] private ?DateTimeImmutable $updatedAt = null; … }

Получение коллекции объектов​


<?php declare(strict_types=1); namespace AppStateProviderUser; use ApiPlatformMetadataOperation; use ApiPlatformStateProviderInterface; use AppDataTransformerApiUserUserOutputGetDataTransformer; use AppDtoApiUserUserOutputDto; use AppEntityUser; use AppStateProviderCollectionProviderInterface; class UsersProvider implements CollectionProviderInterface { public function __construct( private ProviderInterface $collectionProvider, private UserOutputGetDataTransformer $dataTransformer, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { return array_map( fn (User $user): UserOutputDto => $this->dataTransformer->transform($user), iterator_to_array(($this->collectionProvider->provide($operation, $uriVariables, $context))->getIterator()) ); } }

Далее давайте рассмотрим тот же подход для операции сбора. Во-первых, мы создадим класс UsersProvider, реализующий наш CollectionProviderInterface. Задача State Provider – получить пользователей из базы данных, в то время как UserOutputGetDataTransformer должен преобразовать коллекцию пользователей в коллекцию из объектов DTO. Код класса UsersProvider показан ниже:

Пагинация к результату уже применена, поэтому вам не нужно об этом беспокоиться. Последнее, что нужно сделать, — это назначить этому провайдеру операцию GetCollection в классе User:

… #[ApiResource( operations: [ new Get( uriTemplate: ‘/users/{id<d+>}’, output: UserOutputDto::class, provider: UserProvider::class, ), new GetCollection( output: UserOutputDto::class, provider: UsersProvider::class ), ], normalizationContext: [‘groups’ => [‘User:read’]], )] …

State Processor​


Создание объекта

Предположим, что нам необходимо создать нового пользователя с помощью метода POST. Входные данные JSON для метода POST должны выглядеть следующим образом:

{ «firstname»: «string», «lastname»: «string», «email»: «string», «phone»: «string» }

Давайте начнём с создания класса входных данных DTO UserInputPostDto.

<?php declare(strict_types=1); namespace AppDtoApiUser; use SymfonyComponentSerializerAnnotationGroups; use SymfonyComponentValidatorConstraints as Assert; class UserInputPostDto { #[Groups([‘User:write’])] #[AssertNotBlank()] public string $firstname; #[Groups([‘User:write’])] #[AssertNotBlank(allowNull: true)] public ?string $lastname = null; #[Groups([‘User:write’])] #[AssertNotBlank(allowNull: true)] public ?string $email = null; #[Groups([‘User:write’])] #[AssertNotBlank()] public string $phone; #[Groups([‘User:write’])] #[AssertNotBlank()] public string $password; }

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

Кроме того, давайте создадим класс UserInputPostDataTransformer, который будет преобразовывать входящие данные из тела запроса в новый объект класса:

<?php declare(strict_types=1); namespace AppDataTransformerApiUser; use ApiPlatformValidatorValidatorInterface; use AppDtoApiUserUserInputPostDto; use AppEntityUser; use SymfonyComponentPasswordHasherHasherUserPasswordHasherInterface; class UserInputPostDataTransformer { public function __construct( private ValidatorInterface $validator, private UserPasswordHasherInterface $hasher ) { } public function transform(UserInputPostDto $data): User { $this->validator->validate($data); return new User( $data->email, $data->phone, $data->firstname, $data->lastname, $data->password, $this->hasher ); } }

Что касается обработчиков состояний, то все обработчики данных должны реализовывать ApiPlatformStateProcessorInterface. Для дальнейшего удобства создадим PersistProcessorInterface со следующим кодом:

<?php declare(strict_types=1); namespace AppStateProcessor; use ApiPlatformStateProcessorInterface; interface PersistProcessorInterface extends ProcessorInterface { }

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

… services: … AppStateProcessorPersistProcessorInterface: bind: $persistProcessor: ‘@api_platform.doctrine.orm.state.persist_processor’ …

Затем создадим класс PostUserProcessor, реализующий PersistProcessorInterface. Код класса показан ниже:

<?php declare(strict_types=1); namespace AppStateProcessorUser; use ApiPlatformDoctrineCommonStatePersistProcessor; use ApiPlatformMetadataOperation; use AppDataTransformerApiUserUserInputPostDataTransformer; use AppDataTransformerApiUserUserOutputGetDataTransformer; use AppDtoApiUserUserInputPostDto; use AppDtoApiUserUserOutputDto; use AppStateProcessorPersistProcessorInterface; class PostUserProcessor implements PersistProcessorInterface { public function __construct( private UserInputPostDataTransformer $postDataTransformer, private UserOutputGetDataTransformer $getDataTransformer, private PersistProcessor $persistProcessor, ) { } /** * @param UserInputPostDto $data */ public function process($data, Operation $operation, array $uriVariables = [], array $context = []): UserOutputDto { $user = $this->postDataTransformer->transform($data); $this->persistProcessor->process($user, $operation, $uriVariables, $context); return $this->getDataTransformer->transform($user); } }

Метод __construct класса PostUserProcessor принимает три аргумента:

  • $postDataTransformer используется для преобразования входных данных из DTO в новый экземпляр сущности User.
  • $getDataTransformer используется для преобразования созданного объекта User в представление DTO для ответа от сервера.
  • $persistProcessor используется для сохранения нового объекта User в базу данных.

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

Последним этапом является конфигурация операции в атрибуте #[ApiResource()] класса User:

  • input: UserInputPostDto::class
  • processor: PostUserProcessor::class
  • uriTemplate: /register

… #[ApiResource( operations: [ new Get( uriTemplate: ‘/users/{id<d+>}’, output: UserOutputDto::class, provider: UserProvider::class, ), new GetCollection( output: UserOutputDto::class, provider: UsersProvider::class ), new Post( uriTemplate: ‘/register’, input: UserInputPostDto::class, processor: PostUserProcessor::class, ), ], normalizationContext: [‘groups’ => [‘User:read’]], denormalizationContext: [‘groups’ => [‘User:write’]] )] …

Обновление объекта​


Инициализация DTO в методе PATCH

Иногда при обновлении объекта может потребоваться изменить только определенные поля объекта, не затрагивая другие поля. В предыдущих версиях API Platform можно было использовать интерфейс DataTransformerInitializerInterface, который позволял инициализировать DTO с необходимыми предварительно инициализированными полями. Однако DataTransformers вместе с DataTransformerInitializerInterface устарели и больше не присутствуют в API Platform v3. В настоящее время нам не удалось определить подходящую альтернативу для DataTransformerInitializerInterface, поэтому мы предлагаем вам наше личное решение этой проблемы. Начнем с создания PersistProcessorInitializerInterface, реализующего созданный ранее PersistProcessorInterface. Интерфейс содержит один метод инициализации и имеет следующую структуру:

interface PersistProcessorInitializerInterface extends PersistProcessorInterface { public function initialize( mixed $data, string $class, string $format = null, array $context = [] ): object; }

После этого давайте создадим класс декоратора, который украшает нормализатор элементов API Platform. Наша цель — изменить процесс денормализации. Чтобы добиться этого, мы должны сделать собственную реализацию метода денормализации и при необходимости сохранить логику декорированного класса. Для реализации необходимо выполнить следующие действия:

  • Сохранить основную логику декорируемого класса.
  • Предоставить классам Provider общий интерфейс, содержащий в себе initialize метод.

Ниже вы можете увидеть реализацию вышеупомянутых действий:

<?php declare(strict_types=1); namespace AppApiPlatformDecorator; use ApiPlatformMetadataPatch; use ApiPlatformSerializerAbstractItemNormalizer; use SymfonyComponentHttpKernelExceptionUnprocessableEntityHttpException; use SymfonyComponentSerializerNormalizerDenormalizerInterface; use SymfonyComponentSerializerSerializerAwareInterface; use SymfonyComponentSerializerSerializerAwareTrait; class InitializerDecorator implements DenormalizerInterface, SerializerAwareInterface { use SerializerAwareTrait; public function __construct( private AbstractItemNormalizer $decoratedNormalizer, private iterable $stateProcessors ) { } public function denormalize(mixed $data, string $class, string $format = null, array $context = []): mixed { $this->decoratedNormalizer->setSerializer($this->serializer); if (!($operation = $context[‘operation’]) instanceof Patch || !$operation->getInput()) { return $this->decoratedNormalizer->denormalize($data, $class, $format, $context); } foreach ($this->stateProcessors as $stateProcessor) { if ($stateProcessor::class === $operation->getProcessor()) { $initializedObject = $stateProcessor->initialize($data, $class, $format, $context); foreach ($data as $inputField => $inputValue) { if (property_exists($initializedObject, $inputField)) { try { $initializedObject->$inputField = $inputValue; } catch (TypeError $error) { throw new UnprocessableEntityHttpException(‘The field «‘ . $inputField . ‘» was not expected’); } } } return $initializedObject; } } return $this->decoratedNormalizer->denormalize($data, $class, $format, $context); } public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { return $this->decoratedNormalizer->supportsDenormalization($data, $type, $format); } }

Наконец, нам нужно добавить конфигурацию в файл config/services.yaml, чтобы указать, что наш класс украшает нормализатор элементов API Platform. Для этого добавьте следующий код в файл config/services.yaml:

AppApiPlatformDecoratorInitializerDecorator: decorates: ‘api_platform.serializer.normalizer.item’ arguments: — ‘@.inner’ — !tagged app.denormalize_initializer public: false

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

Обновление сущности​


Механизм обновления объекта соответствует тому же шаблону, что и решения выше.

Во-первых, давайте создадим класс UserInputPatchDto, содержащий все поля, которые можно изменить в нашей сущности:

<?php declare(strict_types=1); namespace AppDtoApiUser; use SymfonyComponentSerializerAnnotationGroups; class UserInputPatchDto { #[Groups([‘User:write’])] public ?string $firstname = null; #[Groups([‘User:write’])] public ?string $lastname = null; #[Groups([‘User:write’])] public ?string $password = null; }

Затем давайте создадим класс UserInputPatchDataTransformer, который обрабатывает входящие данные и изменяет целевой объект:

<?php declare(strict_types=1); namespace AppDataTransformerApiUser; use ApiPlatformValidatorValidatorInterface; use AppDtoApiUserUserInputPatchDto; use AppEntityUser; use SymfonyComponentPasswordHasherHasherUserPasswordHasherInterface; class UserInputPatchDataTransformer { public function __construct( private ValidatorInterface $validator, private UserPasswordHasherInterface $hasher, ) { } public function transform(UserInputPatchDto $data, User $user): User { $this->validator->validate($data); $user->setFirstname($data->firstname); $user->setLastname($data->lastname); return $user; } }

Напоследок создадим класс PatchUserProcessor, который реализует наш PersistProcessorInitializerInterface. Код класса показан ниже:

<?php declare(strict_types=1); namespace AppStateProcessorUser; use ApiPlatformMetadataOperation; use ApiPlatformStateProcessorInterface; use AppDataTransformerApiUserUserInputPatchDataTransformer; use AppDataTransformerApiUserUserOutputGetDataTransformer; use AppDtoApiUserUserInputPatchDto; use AppRepositoryUserRepository; use AppStateProcessorPersistProcessorInitializerInterface; use SymfonyComponentSerializerNormalizerAbstractNormalizer; class PatchUserProcessor implements PersistProcessorInitializerInterface { public function __construct( private ProcessorInterface $persistProcessor, private UserRepository $userRepository, private UserInputPatchDataTransformer $patchDataTransformer, private UserOutputGetDataTransformer $getDataTransformer, ) { } /** * @param UserInputPatchDto $data */ public function process($data, Operation $operation, array $uriVariables = [], array $context = []): array|object|null { $user = $this->userRepository->find($uriVariables[‘id’]); $user = $this->patchDataTransformer->transform($data, $user); $this->persistProcessor->process($user, $operation, $uriVariables, $context); return $this->getDataTransformer->transform($user); } public function initialize(mixed $data, string $class, ?string $format = null, array $context = []): object { $user = $context[AbstractNormalizer::OBJECT_TO_POPULATE]; $dto = new UserInputPatchDto() $dto->firstname = $user->getFirstname(); $dto->lastname = $user->getLastname(); return $dto; } }

Мы создали функцию инициализации, которая позволяет предварительно заполнить DTO существующими данными. После инициализации DTO передается функции процесса, где целевой объект должен быть обновлен.

Добавим необходимую конфигурацию для метода PATCH в атрибут #[ApiResource()] класса User:

#[ApiResource( operations: [ new Get( uriTemplate: ‘/users/{id<d+>}’, output: UserOutputDto::class, provider: UserProvider::class, ), new GetCollection( output: UserOutputDto::class, provider: UsersProvider::class ), new Post( uriTemplate: ‘/register’, input: UserInputPostDto::class, processor: PostUserProcessor::class, ), new Patch( uriTemplate: ‘/users/{id<d+>}’, input: UserInputPatchDto::class, processor: PatchUserProcessor::class, output: UserOutputDto::class, security: ‘object === user or is_granted(«ROLE_ADMIN»)’, ), ], normalizationContext: [‘groups’ => [‘User:read’]], denormalizationContext: [‘groups’ => [‘User:write’]] )]

Мы указали uriTemplate для метода PATCH, а также параметры: input, processor, output и security. Параметр security в данном случае используется для ограничения доступа, чтобы доступ к объекту был либо у администратора, либо у текущего авторизованного пользователя, если он редактирует свои собственные данные.

Subresources​


Подресурс — это еще один способ объявления ресурса, который обычно включает более сложный URI. Например, у нас есть такие сущности, как пользователь и встреча. У каждого пользователя может быть несколько встреч (OneToMany).

Сущность Appointment имеет следующую структуру:

class Appointment { … #[ORMId] #[ORMGeneratedValue] #[ORMColumn] private int $id; #[ORMManyToOne(inversedBy: ‘appointments’)] private User $user; #[ORMColumn(length: 255)] private string $title; #[ORMColumn(type: Types::TEXT, nullable: true)] private ?string $description = null; #[ORMColumn] private DateTimeImmutable $schedule; #[ORMColumn] private float $price; #[ORMColumn(length: 255)] private string $status; #[ORMColumn] private DateTimeImmutable $createdAt; #[ORMColumn(nullable: true)] private ?DateTimeImmutable $updatedAt = null; … }

Для начала давайте продолжим наш сценарий разработки и создадим выходной DTO для класса Appointment. Код класса AppointmentOutputDto показан ниже:

<?php declare(strict_types=1); namespace AppDtoApiAppointment; use AppEntityAppointment; use DateTimeImmutable; use SymfonyComponentSerializerAnnotationGroups; class AppointmentOutputDto { #[Groups([‘Appointment:read’])] public int $id; #[Groups([‘Appointment:read’])] public string $title; #[Groups([‘Appointment:read’])] public ?string $description; #[Groups([‘Appointment:read’])] public DateTimeImmutable $schedule; #[Groups([‘Appointment:read’])] public float $price; #[Groups([‘Appointment:read’])] public string $status; #[Groups([‘Appointment:read’])] public DateTimeImmutable $createdAt; #[Groups([‘Appointment:read’])] public ?DateTimeImmutable $updatedAt; public function __construct( int $id, string $title, ?string $description, DateTimeImmutable $schedule, float $price, string $status, DateTimeImmutable $createdAt, ?DateTimeImmutable $updatedAt, ) { $this->id = $id; $this->title = $title; $this->description = $description; $this->schedule = $schedule; $this->price = $price; $this->status = $status; $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; } }

Кроме того, давайте создадим AppointmentOutputDataTransformer, который преобразует экземпляр Appointment в DTO. Код показан ниже:

<?php declare(strict_types=1); namespace AppDataTransformerApiAppointment; use AppDtoApiAppointmentAppointmentOutputDto; use AppEntityAppointment; class AppointmentOutputDataTransformer { public function transform(Appointment $appointment): AppointmentOutputDto { return new AppointmentOutputDto( $appointment->getId(), $appointment->getTitle(), $appointment->getDescription(), $appointment->getSchedule(), $appointment->getPrice(), $appointment->getStatus(), $appointment->getCreatedAt(), $appointment->getUpdatedAt(), ); } }

Затем создадим State Provider для извлечения объектов из базы данных:

<?php declare(strict_types=1); namespace AppStateProviderAppointment; use ApiPlatformMetadataOperation; use ApiPlatformStateProviderInterface; use AppDataTransformerApiAppointmentAppointmentOutputDataTransformer; use AppDtoApiAppointmentAppointmentOutputDto; use AppEntityAppointment; use AppStateProviderCollectionProviderInterface; class AppointmentsProvider implements CollectionProviderInterface { public function __construct( private ProviderInterface $collectionProvider, private AppointmentOutputDataTransformer $dataTransformer, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { return array_map( fn (Appointment $user): AppointmentOutputDto => $this->dataTransformer->transform($user), iterator_to_array(($this->collectionProvider->provide($operation, $uriVariables, $context))->getIterator()) ); } }

И наконец, нам нужно добавить следующий атрибут в класс Appointment:

#[ApiResource( uriTemplate: ‘/users/{userId<d+>}/appointments’, uriVariables: [ ‘userId’ => new Link(fromClass: User::class, toProperty: ‘user’), ], operations: [new GetCollection()], normalizationContext: [‘groups’ => [‘Appointment:read’]], output: AppointmentOutputDto::class, provider: AppointmentsProvider::class, )]

В операциях указываем тип запроса GetCollection. Вы также можете настроить файл config/security.yaml и разрешить или запретить доступ по URI, указанному в аннотации класса. Более того, внутри операции можно указать:

operations: [ new Get(security: «is_granted(‘ROLE_ADMIN’)»)]

Примечание. Если вы ограничиваете доступ, например, к пользователю объекта, вы все равно можете получать встречи этого пользователя через /users/{userId}/appointments/{appointmentId}.

API Platform не может создавать URI длиннее двух сущностей. Например, API Platform не может создать путь, состоящий из 3 и более объектов:

/users/{userId}/appointments/{appointmentId}/media_objects/{objectId}.

Заключение​


Мы рассмотрели основные изменения в новой версии API Platform и открыли для себя новые инструменты и то, как они работают. Общая стратегия развития во многом остается такой же, как и в предыдущей версии платформы API.
 
198 237Темы
635 209Сообщения
3 618 425Пользователи
Pandar96Новый пользователь
Верх