Виммельбух, 3, перетяжка
Виммельбух, 3, перетяжка
Виммельбух, 3, перетяжка

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

Аватарка пользователя Елизавета Ржевская
Отредактировано

В сентябре 2022 года была выпущена новая версия библиотеки API Platform. Составили обзор с изменениями в третьей версии API Platform.

1К открытий3К показов

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

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

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

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

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

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

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

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 App\Dto\Api\User;

use DateTimeInterface;
use Symfony\Component\Serializer\Annotation\Groups;

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, который позволит нам извлекать пользователя из базы данных. Все поставщики данных должны реализовать ApiPlatform\State\ProviderInterface, который применяется как к операциям с коллекциями, так и к операциям с единичными объектами. С этого момента я предлагаю создать CollectionProviderInterface и ItemProviderInterface, которые расширяют ApiPlatform\State\ProviderInterface.

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

			<?php

declare(strict_types=1);

namespace App\State\Provider;

use ApiPlatform\State\ProviderInterface;

interface CollectionProviderInterface extends ProviderInterface
{
}
		

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

			<?php

declare(strict_types=1);

namespace App\State\Provider;

use ApiPlatform\State\ProviderInterface;


interface ItemProviderInterface extends ProviderInterface
{
}
		

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

			…
parameters:
…
services:
…
_instanceof:
    		App\State\Provider\CollectionProviderInterface:
        		bind:
            		$collectionProvider: '@api_platform.doctrine.orm.state.collection_provider'
    		App\State\Provider\ItemProviderInterface:
        		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 App\DataTransformer\Api\User;

use App\Dto\Api\User\UserOutputDto;
use App\Entity\User;

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 App\State\Provider\User;

use ApiPlatform\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\DataTransformer\Api\User\UserOutputGetDataTransformer;
use App\Dto\Api\User\UserOutputDto;
use App\State\Provider\ItemProviderInterface;

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 App\Entity;

use ApiPlatform\Metadata\ApiResource;
…
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(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
{
	#[ORM\Id]
	#[ORM\GeneratedValue]
	#[ORM\Column]
	private int $id;

	#[ORM\Column(length: 255, nullable: true, unique: true)]
	private ?string $email = null;

	#[ORM\Column(length: 255, unique: true)]
	private string $phone;

	#[ORM\Column(length: 255)]
	private string $password;

	#[ORM\Column(length: 255)]
	private string $firstname;

	#[ORM\Column(length: 255, nullable: true)]
	private ?string $lastname = null;

	#[ORM\Column(nullable: false)]
	private array $roles = ['ROLE_USER'];

	#[ORM\Column]
	private DateTimeImmutable $createdAt;

	#[ORM\Column(nullable: true)]
	private ?DateTimeImmutable $updatedAt = null;
...
}
		

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

			<?php

declare(strict_types=1);

namespace App\State\Provider\User;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\DataTransformer\Api\User\UserOutputGetDataTransformer;
use App\Dto\Api\User\UserOutputDto;
use App\Entity\User;
use App\State\Provider\CollectionProviderInterface;

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 App\Dto\Api\User;

use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

class UserInputPostDto
{
	#[Groups(['User:write'])]
	#[Assert\NotBlank()]
	public string $firstname;

	#[Groups(['User:write'])]
	#[Assert\NotBlank(allowNull: true)]
	public ?string $lastname = null;

	#[Groups(['User:write'])]
	#[Assert\NotBlank(allowNull: true)]
	public ?string $email = null;

	#[Groups(['User:write'])]
	#[Assert\NotBlank()]
	public string $phone;

	#[Groups(['User:write'])]
	#[Assert\NotBlank()]
	public string $password;
}
		

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

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

			<?php

declare(strict_types=1);

namespace App\DataTransformer\Api\User;

use ApiPlatform\Validator\ValidatorInterface;
use App\Dto\Api\User\UserInputPostDto;
use App\Entity\User;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

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
    	);
	}
}
		

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

			<?php

declare(strict_types=1);

namespace App\State\Processor;

use ApiPlatform\State\ProcessorInterface;

interface PersistProcessorInterface extends ProcessorInterface
{
}
		

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

			…
services:
…
	App\State\Processor\PersistProcessorInterface:
        	bind:
            	$persistProcessor: '@api_platform.doctrine.orm.state.persist_processor'
…
		

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

			<?php

declare(strict_types=1);

namespace App\State\Processor\User;

use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Metadata\Operation;
use App\DataTransformer\Api\User\UserInputPostDataTransformer;
use App\DataTransformer\Api\User\UserOutputGetDataTransformer;
use App\Dto\Api\User\UserInputPostDto;
use App\Dto\Api\User\UserOutputDto;
use App\State\Processor\PersistProcessorInterface;

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 App\ApiPlatform\Decorator;

use ApiPlatform\Metadata\Patch;
use ApiPlatform\Serializer\AbstractItemNormalizer;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;

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:

			App\ApiPlatform\Decorator\InitializerDecorator:
    	decorates: 'api_platform.serializer.normalizer.item'
    	arguments:
        	- '@.inner'
        	- !tagged app.denormalize_initializer
    	public: false
		

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

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

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

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

			<?php

declare(strict_types=1);

namespace App\Dto\Api\User;

use Symfony\Component\Serializer\Annotation\Groups;

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 App\DataTransformer\Api\User;

use ApiPlatform\Validator\ValidatorInterface;
use App\Dto\Api\User\UserInputPatchDto;
use App\Entity\User;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

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 App\State\Processor\User;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\DataTransformer\Api\User\UserInputPatchDataTransformer;
use App\DataTransformer\Api\User\UserOutputGetDataTransformer;
use App\Dto\Api\User\UserInputPatchDto;
use App\Repository\UserRepository;
use App\State\Processor\PersistProcessorInitializerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

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
{
…
#[ORM\Id]
	#[ORM\GeneratedValue]
	#[ORM\Column]
	private int $id;

	#[ORM\ManyToOne(inversedBy: 'appointments')]
	private User $user;

	#[ORM\Column(length: 255)]
	private string $title;

	#[ORM\Column(type: Types::TEXT, nullable: true)]
	private ?string $description = null;

	#[ORM\Column]
	private DateTimeImmutable $schedule;

	#[ORM\Column]
	private float $price;

	#[ORM\Column(length: 255)]
	private string $status;

	#[ORM\Column]
	private DateTimeImmutable $createdAt;

	#[ORM\Column(nullable: true)]
	private ?DateTimeImmutable $updatedAt = null;
…
}
		

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

			<?php

declare(strict_types=1);

namespace App\Dto\Api\Appointment;

use App\Entity\Appointment;
use DateTimeImmutable;
use Symfony\Component\Serializer\Annotation\Groups;

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 App\DataTransformer\Api\Appointment;

use App\Dto\Api\Appointment\AppointmentOutputDto;
use App\Entity\Appointment;

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 App\State\Provider\Appointment;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\DataTransformer\Api\Appointment\AppointmentOutputDataTransformer;
use App\Dto\Api\Appointment\AppointmentOutputDto;
use App\Entity\Appointment;
use App\State\Provider\CollectionProviderInterface;

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.

Следите за новыми постами
Следите за новыми постами по любимым темам
1К открытий3К показов