ApiPlatform - 规范化后将 DTO 的字段传递给不同的服务
ApiPlatform - Passing DTO's fields to different service after normalization
我有如下结构。
----------------
MESSAGE
----------------
id
subject
body
----------------
----------------
USER
----------------
id
name
category
region
----------------
----------------
RECIPIENT
----------------
user_id
message_id
is_read
read_at
----------------
所以Message 1:n Recipient m:1 User
。
收件人不是 @ApiResource
.
Backoffice 用户将“编写”消息并根据一组特定标准(用户区域、用户类别、用户标签...)选择受众。
到POST
我正在使用Dto
的消息
class MessageInputDto
{
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $subject;
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $body;
/**
* @var bool
*
* @Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* @var DateTimeInterface
*
* @Groups({"msg_message:write"})
*/
public DateTimeInterface $publishDate;
/**
* @var DateTimeInterface|null
*
* @Groups({"msg_message:write"})
*/
public ?DateTimeInterface $expiryDate = null;
/**
* @var MessageCategory|null
*
* @Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* @var array
*/
public array $criteria = [];
}
$criteria
字段用于选择该消息的受众并被 DataTransformer
跳过,因为它不是映射字段,消息实体的 属性由变压器返回。
class MessageInputDataTransformer implements \ApiPlatform\Core\DataTransformer\DataTransformerInterface
{
/**
* @var MessageInputDto $object
* @inheritDoc
*/
public function transform($object, string $to, array $context = [])
{
$message = new Message($object->subject, $object->body);
$message->setIsPublished($object->isPublished);
$message->setPublishDate($object->publishDate);
$message->setExpiryDate($object->expiryDate);
$message->setCategory($object->category);
return $message;
}
/**
* @inheritDoc
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
// if it's a book we transformed the data already
if ($data instanceof Message) {
return false;
}
return Message::class === $to && null !== ($context['input']['class'] ?? null);
}
}
作为副作用,将在联接 table(收件人)中执行批量插入,以保持消息和用户之间的 m:n 关系。
我的问题是 how/where 执行此批量插入以及如何将 $criteria
传递给将管理它的服务。
我现在找到的唯一解决方案(它正在工作,但我认为这不是一个好的做法)是将批量插入过程放在 [=21= 的 POST_WRITE
事件中], 获取 Request
对象并处理其中包含的 $criteria
。
class MessageSubscriber implements EventSubscriberInterface
{
/**
* @inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
['handleCriteria', EventPriorities::POST_WRITE]
],
];
}
public function handleCriteria(ViewEvent $event)
{
/** @var Message $message */
$message = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$e = $event->getRequest();
$collectionOperation = $e->get('_api_collection_operation_name');
if (!$message instanceof Message ||
$method !== Request::METHOD_POST ||
$collectionOperation !== 'post') {
return;
}
$content = json_decode($event->getRequest()->getContent(), true);
if(array_key_exists('audienceCriteria', $content)){
$criteria = Criteria::createFromArray($content['audienceCriteria']);
// Todo: Create the audience
}
}
}
所以想法是,当消息被持久化时,系统必须生成“关系”public。
这就是为什么我认为 post 写入事件可能是一个不错的选择,但正如我所说,我不确定这是否是一个好的做法。
有什么想法吗?谢谢。
当您必须生成多个自定义实体时,一种解决方案是使用数据持久性:https://api-platform.com/docs/core/data-persisters/
你有两个选择:
- 装饰 Doctrine 持久化器 - 这意味着消息仍然会被 Doctrine 保存,但是你可以在之前或之后做一些事情。
- 实施自定义持久化器 - 保存您喜欢的消息和其他相关实体。或者做一些完全自定义的事情,根本不调用 Doctrine。
正如关于 DTO 状态的文档:“在大多数情况下,DTO 模式应该使用 API 资源 class 来实现,代表 public 通过 [=39] 公开的数据模型=] 和自定义数据提供程序。在这种情况下,标有 @ApiResource 的 class 将充当 DTO。"
指定输入或输出数据表示和 DataTransformer 的 IOW 是例外。如果 DTO 持有比实体更多的数据,或者如果 dto 与实体不是一对一的(例如,使用分组依据的报告),它就不起作用。
这是您的 DTO class 作为资源:
namespace App\DTO;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use App\Entity\Message;
/**
* Class defining Message data transfer
*
* @ApiResource(
* denormalizationContext= {"groups" = {"msg_message:write"}},
* itemOperations={
* },
* collectionOperations={
* "post"={
* "path"="/messages",
* "openapi_context" = {
* "summary" = "Creates a Message",
* "description" = "Creates a Message"
* }
* }
* },
* output=Message::class
* )
*/
class MessageInputDto
{
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $subject;
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $body;
/**
* @var bool
*
* @Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* @var \DateTimeInterface
*
* @Groups({"msg_message:write"})
*/
public \DateTimeInterface $publishDate;
/**
* @var \DateTimeInterface|null
*
* @Groups({"msg_message:write"})
*/
public ?\DateTimeInterface $expiryDate = null;
/**
* @var MessageCategory|null
*
* @Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* @var array
* @Groups({"msg_message:write"})
*/
public array $criteria = [];
}
确保 class 所在的文件夹在 api/config/packages/api_platform.yaml 的路径列表中。通常有如下配置:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
如果 MessageInputDto 在 /src/DTO 中,使其像:
api_platform:
mapping:
paths:
- '%kernel.project_dir%/src/Entity'
- '%kernel.project_dir%/src/DTO'
post 操作可能与消息资源上的默认 post 操作具有相同的路径。通过在没有“post”的情况下为您的 Message 资源显式定义 collectionOperations 来删除它。
MessageInputDto的post操作会反序列化MessageInputDto。您的 DataTransformer 不会对其进行操作,因此它会按原样到达 DataPersister:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\MessageInputDto;
use App\Entity\Message;
use Doctrine\Persistence\ManagerRegistry;
use App\DataTransformer\MessageInputDataTransformer;
use ApiPlatform\Core\Exception\InvalidArgumentException;
class MessageDataPersister implements ContextAwareDataPersisterInterface
{
private $dataPersister;
private $entityManager;
private $dataTransformer;
public function __construct(ContextAwareDataPersisterInterface $dataPersister, ManagerRegistry $managerRegistry, MessageInputDataTransformer $dataTransformer)
{
$this->dataPersister = $dataPersister;
$this->entityManager = $managerRegistry->getManagerForClass(Message::class);
$this->dataTransformer = $dataTransformer;
}
public function supports($data, array $context = []): bool
{
$transformationContext = ['input' => ['class' => Message::class]];
return get_class($data) == MessageInputDto::class
&& $this->dataTransformer->supportsTransformation($data, Message::class, $transformationContext)
&& null !== $this->entityManager;
}
public function persist($data, array $context = [])
{
$message = $this->dataTransformer->transform($data, Message::class);
// dataPersister will flush the entityManager but we do not want incomplete data inserted
$this->entityManager->beginTransaction();
$commit = true;
$result = $this->dataPersister->persist($message, []);
if(!empty($data->criteria)){
$criteria = Criteria::createFromArray($data->criteria);
try {
// Todo: Create the audience, preferably with a single INSERT query SELECTing FROM user_table WHERE meeting the criteria
// (Or maybe better postpone until message is really sent, user region, category, tags may change over time)
} catch (\Exception $e) {
$commit = false;
$this->entityManager->rollback();
}
}
if ($commit) {
$this->entityManager->commit();
}
return $result;
}
public function remove($data, array $context = [])
{
throw new InvalidArgumentException('Operation not supported: delete');
}
}
(也许它应该被称为 MessageInputDtoDataPersister - 取决于您如何看待它)
即使启用了服务自动装配和自动配置,您仍然必须配置它以获得正确的 dataPersister 以委托给:
# api/config/services.yaml
services:
# ...
'App\DataPersister\MessageDataPersister':
arguments:
$dataPersister: '@api_platform.doctrine.orm.data_persister'
这样您就不需要 MessageSubscriber。
请注意,反序列化和数据持久化之间的所有其他阶段(验证、安全性 post 非规范化)都在 MessageInputDto 上工作。
我有如下结构。
----------------
MESSAGE
----------------
id
subject
body
----------------
----------------
USER
----------------
id
name
category
region
----------------
----------------
RECIPIENT
----------------
user_id
message_id
is_read
read_at
----------------
所以Message 1:n Recipient m:1 User
。
收件人不是 @ApiResource
.
Backoffice 用户将“编写”消息并根据一组特定标准(用户区域、用户类别、用户标签...)选择受众。
到POST
我正在使用Dto
class MessageInputDto
{
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $subject;
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $body;
/**
* @var bool
*
* @Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* @var DateTimeInterface
*
* @Groups({"msg_message:write"})
*/
public DateTimeInterface $publishDate;
/**
* @var DateTimeInterface|null
*
* @Groups({"msg_message:write"})
*/
public ?DateTimeInterface $expiryDate = null;
/**
* @var MessageCategory|null
*
* @Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* @var array
*/
public array $criteria = [];
}
$criteria
字段用于选择该消息的受众并被 DataTransformer
跳过,因为它不是映射字段,消息实体的 属性由变压器返回。
class MessageInputDataTransformer implements \ApiPlatform\Core\DataTransformer\DataTransformerInterface
{
/**
* @var MessageInputDto $object
* @inheritDoc
*/
public function transform($object, string $to, array $context = [])
{
$message = new Message($object->subject, $object->body);
$message->setIsPublished($object->isPublished);
$message->setPublishDate($object->publishDate);
$message->setExpiryDate($object->expiryDate);
$message->setCategory($object->category);
return $message;
}
/**
* @inheritDoc
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
// if it's a book we transformed the data already
if ($data instanceof Message) {
return false;
}
return Message::class === $to && null !== ($context['input']['class'] ?? null);
}
}
作为副作用,将在联接 table(收件人)中执行批量插入,以保持消息和用户之间的 m:n 关系。
我的问题是 how/where 执行此批量插入以及如何将 $criteria
传递给将管理它的服务。
我现在找到的唯一解决方案(它正在工作,但我认为这不是一个好的做法)是将批量插入过程放在 [=21= 的 POST_WRITE
事件中], 获取 Request
对象并处理其中包含的 $criteria
。
class MessageSubscriber implements EventSubscriberInterface
{
/**
* @inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
['handleCriteria', EventPriorities::POST_WRITE]
],
];
}
public function handleCriteria(ViewEvent $event)
{
/** @var Message $message */
$message = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$e = $event->getRequest();
$collectionOperation = $e->get('_api_collection_operation_name');
if (!$message instanceof Message ||
$method !== Request::METHOD_POST ||
$collectionOperation !== 'post') {
return;
}
$content = json_decode($event->getRequest()->getContent(), true);
if(array_key_exists('audienceCriteria', $content)){
$criteria = Criteria::createFromArray($content['audienceCriteria']);
// Todo: Create the audience
}
}
}
所以想法是,当消息被持久化时,系统必须生成“关系”public。
这就是为什么我认为 post 写入事件可能是一个不错的选择,但正如我所说,我不确定这是否是一个好的做法。
有什么想法吗?谢谢。
当您必须生成多个自定义实体时,一种解决方案是使用数据持久性:https://api-platform.com/docs/core/data-persisters/
你有两个选择:
- 装饰 Doctrine 持久化器 - 这意味着消息仍然会被 Doctrine 保存,但是你可以在之前或之后做一些事情。
- 实施自定义持久化器 - 保存您喜欢的消息和其他相关实体。或者做一些完全自定义的事情,根本不调用 Doctrine。
正如关于 DTO 状态的文档:“在大多数情况下,DTO 模式应该使用 API 资源 class 来实现,代表 public 通过 [=39] 公开的数据模型=] 和自定义数据提供程序。在这种情况下,标有 @ApiResource 的 class 将充当 DTO。"
指定输入或输出数据表示和 DataTransformer 的 IOW 是例外。如果 DTO 持有比实体更多的数据,或者如果 dto 与实体不是一对一的(例如,使用分组依据的报告),它就不起作用。
这是您的 DTO class 作为资源:
namespace App\DTO;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use App\Entity\Message;
/**
* Class defining Message data transfer
*
* @ApiResource(
* denormalizationContext= {"groups" = {"msg_message:write"}},
* itemOperations={
* },
* collectionOperations={
* "post"={
* "path"="/messages",
* "openapi_context" = {
* "summary" = "Creates a Message",
* "description" = "Creates a Message"
* }
* }
* },
* output=Message::class
* )
*/
class MessageInputDto
{
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $subject;
/**
* @var string
*
* @Groups({"msg_message:write"})
*/
public string $body;
/**
* @var bool
*
* @Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* @var \DateTimeInterface
*
* @Groups({"msg_message:write"})
*/
public \DateTimeInterface $publishDate;
/**
* @var \DateTimeInterface|null
*
* @Groups({"msg_message:write"})
*/
public ?\DateTimeInterface $expiryDate = null;
/**
* @var MessageCategory|null
*
* @Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* @var array
* @Groups({"msg_message:write"})
*/
public array $criteria = [];
}
确保 class 所在的文件夹在 api/config/packages/api_platform.yaml 的路径列表中。通常有如下配置:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
如果 MessageInputDto 在 /src/DTO 中,使其像:
api_platform:
mapping:
paths:
- '%kernel.project_dir%/src/Entity'
- '%kernel.project_dir%/src/DTO'
post 操作可能与消息资源上的默认 post 操作具有相同的路径。通过在没有“post”的情况下为您的 Message 资源显式定义 collectionOperations 来删除它。
MessageInputDto的post操作会反序列化MessageInputDto。您的 DataTransformer 不会对其进行操作,因此它会按原样到达 DataPersister:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\MessageInputDto;
use App\Entity\Message;
use Doctrine\Persistence\ManagerRegistry;
use App\DataTransformer\MessageInputDataTransformer;
use ApiPlatform\Core\Exception\InvalidArgumentException;
class MessageDataPersister implements ContextAwareDataPersisterInterface
{
private $dataPersister;
private $entityManager;
private $dataTransformer;
public function __construct(ContextAwareDataPersisterInterface $dataPersister, ManagerRegistry $managerRegistry, MessageInputDataTransformer $dataTransformer)
{
$this->dataPersister = $dataPersister;
$this->entityManager = $managerRegistry->getManagerForClass(Message::class);
$this->dataTransformer = $dataTransformer;
}
public function supports($data, array $context = []): bool
{
$transformationContext = ['input' => ['class' => Message::class]];
return get_class($data) == MessageInputDto::class
&& $this->dataTransformer->supportsTransformation($data, Message::class, $transformationContext)
&& null !== $this->entityManager;
}
public function persist($data, array $context = [])
{
$message = $this->dataTransformer->transform($data, Message::class);
// dataPersister will flush the entityManager but we do not want incomplete data inserted
$this->entityManager->beginTransaction();
$commit = true;
$result = $this->dataPersister->persist($message, []);
if(!empty($data->criteria)){
$criteria = Criteria::createFromArray($data->criteria);
try {
// Todo: Create the audience, preferably with a single INSERT query SELECTing FROM user_table WHERE meeting the criteria
// (Or maybe better postpone until message is really sent, user region, category, tags may change over time)
} catch (\Exception $e) {
$commit = false;
$this->entityManager->rollback();
}
}
if ($commit) {
$this->entityManager->commit();
}
return $result;
}
public function remove($data, array $context = [])
{
throw new InvalidArgumentException('Operation not supported: delete');
}
}
(也许它应该被称为 MessageInputDtoDataPersister - 取决于您如何看待它)
即使启用了服务自动装配和自动配置,您仍然必须配置它以获得正确的 dataPersister 以委托给:
# api/config/services.yaml
services:
# ...
'App\DataPersister\MessageDataPersister':
arguments:
$dataPersister: '@api_platform.doctrine.orm.data_persister'
这样您就不需要 MessageSubscriber。
请注意,反序列化和数据持久化之间的所有其他阶段(验证、安全性 post 非规范化)都在 MessageInputDto 上工作。