Return 个案例中的实体(如果它已经存在)- API 平台
Return entity in a case if it already exist - API Platform
我有实体 Tag
,它有唯一的 属性 tagValue
。当我用已经存在的 tagValue
创建 POST
时,我想得到它作为响应。
config/validator/tag.yaml
:
App\Entity\Tag:
constraints:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: tagValue
properties:
tagValue:
- NotBlank: ~
src/Entity/Tag.php
:
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use DateTimeInterface;
use DateTime;
use Exception;
/**
* @ORM\Table(name="tag")
* @ORM\Entity(repositoryClass="App\Repository\TagRepository")
* @ORM\HasLifecycleCallbacks
*/
class Tag
{
/**
* @var int
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string
* @ORM\Column(type="string", length=255)
*/
private $tagValue;
// ...
}
当我做一个 POST
:
curl --request POST \
--url http://127.0.0.1:8888/api/tags \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-auth-token: xxxxxxxxxxxxxxxx' \
--data '{
"tagValue": "test"
}'
我收到了刚刚创建的实体和代码 201 的响应。一切正常,但如果我再次发出此请求,如预期的那样,我将收到响应代码 400 和响应正文:
{
"type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
"title": "An error occurred",
"detail": "tagValue: This value is already used.",
"violations": [
{
"propertyPath": "tagValue",
"message": "This value is already used."
}
]
}
但我希望将现有实体包含到该响应中。
有什么想法可以在不违反 REST 规则的情况下做到这一点吗?
(Symfony 4.2.5, api-platform/api-pack 1.2.0)
我终于从 maks-rafalko (I really appreciate him for that) and if someone will stuck on the same issue, here's his solution 那里得到了关于 GitHub 的答案:
You are lucky man, we have just implemented it inside our application. There is no built in functionality in API-Platform for this feature, we had to override some classes in order to add it.
首先,这是当 Unique 约束被取消时我们的响应现在的样子:
{
"type": "https://tools.ietf.org/html/rfc2616#section-10",
"title": "An error occurred",
"detail": "number: This Usage Reference already exists with the same number and channel.",
"violations": [
{
"propertyPath": "number",
"message": "This Usage Reference already exists with the same number and channel."
}
],
"existingUniqueEntities": [
{
"uniquePropertyPaths": [
"number",
"channel"
],
"entity": {
"id": 1101,
"number": "23423423435",
"channel": "/api/channels/1",
"createdAt": "2019-07-17T07:25:50.721Z"
}
}
]
}
请注意,您可能有许多独特的违规行为,并且这种架构可能会 return 许多 实体已经存在并与提供的请求冲突(例如实体可以有 2 对唯一键,一对通过电子邮件,另一对通过引用)
另外,我们的实现恰好使用了那些将通过执行 GET /resource 使用的序列化组,其中 resource 是您尝试创建的资源。我们从 api 平台元数据
中获取这些序列化组
代码如下:
<?php
declare(strict_types=1);
namespace App\Serializer;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Serializer\AbstractConstraintViolationListNormalizer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* This class completely overrides `ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer` class
* since it's final
*
* Goal of overriding is to add `existingUniqueEntities` key when ViolationList contains unique entity violations
*
* @see \ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer
*/
class ConstraintViolationListNormalizer extends AbstractConstraintViolationListNormalizer implements NormalizerAwareInterface
{
public const FORMAT = 'jsonproblem';
public const TYPE = 'type';
public const TITLE = 'title';
/**
* @var array<string, string>
*/
private $defaultContext = [
self::TYPE => 'https://tools.ietf.org/html/rfc2616#section-10',
self::TITLE => 'An error occurred',
];
/**
* @var ResourceMetadataFactoryInterface
*/
private $resourceMetadataFactory;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var NormalizerInterface
*/
private $normalizer;
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $serializePayloadFields = null, NameConverterInterface $nameConverter = null, array $defaultContext = [])
{
parent::__construct($serializePayloadFields, $nameConverter);
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function setNormalizer(NormalizerInterface $normalizer): void
{
$this->normalizer = $normalizer;
}
/**
* @param mixed $object
* @param string|null $format
* @param array $context
*
* @return array
*/
public function normalize($object, $format = null, array $context = []): array
{
[$messages, $violations] = $this->getMessagesAndViolations($object);
$response = [
'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE],
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
'detail' => $messages ? implode("\n", $messages) : (string) $object,
'violations' => $violations,
];
$existingUniqueEntities = $this->getExistingUniqueEntities($object);
return \count($existingUniqueEntities) > 0 ?
array_merge($response, ['existingUniqueEntities' => $existingUniqueEntities])
: $response;
}
private function getExistingUniqueEntities(ConstraintViolationListInterface $constraintViolationList): array
{
$existingUniqueEntities = [];
/** @var ConstraintViolation $violation */
foreach ($constraintViolationList as $violation) {
$constraint = $violation->getConstraint();
if (!$constraint instanceof UniqueEntity) {
continue;
}
$rootEntity = \is_object($violation->getRoot()) ? $violation->getRoot() : null;
if ($rootEntity === null) {
continue;
}
$existingEntityCausedViolation = $violation->getCause()[0];
$metadata = $this->resourceMetadataFactory->create(\get_class($existingEntityCausedViolation));
// get normalization groups for `GET /resource` operation, fallback to global resource groups
$normalizationContext = $metadata->getItemOperationAttribute('get', 'normalization_context', [], true);
$groups = $normalizationContext['groups'] ?? [];
$entityNormalizationContext = \count($groups) > 0 ? ['groups' => $groups] : [];
$existingUniqueEntities[] = [
'uniquePropertyPaths' => $constraint->fields,
'entity' => $this->normalizer->normalize($existingEntityCausedViolation, null, $entityNormalizationContext),
];
}
return $existingUniqueEntities;
}
}
一切都在 getExistingUniqueEntities 中,但不幸的是,我们不得不完全覆盖 ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer class 因为它是最终的,我们无法扩展它。
我们设法使用 Compiler Pass 覆盖了它:
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
{
private const CONSTRAINT_VIOLATION_LIST_NORMALIZER_PRIORITY = -780;
...
public function process(ContainerBuilder $container)
{
...
$constraintViolationListNormalizerDefinition = new Definition(
ConstraintViolationListNormalizer::class,
[
$container->getDefinition('api_platform.metadata.resource.metadata_factory.cached'),
$container->getParameter('api_platform.validator.serialize_payload_fields'),
$container->hasDefinition('api_platform.name_converter') ? $container->getDefinition('api_platform.name_converter') : null,
[],
]
);
$constraintViolationListNormalizerDefinition->addTag('serializer.normalizer', ['priority' => self::CONSTRAINT_VIOLATION_LIST_NORMALIZER_PRIORITY]);
$container->setDefinition('api_platform.problem.normalizer.constraint_violation_list', $constraintViolationListNormalizerDefinition);
}
So, this solution is based on Symfony Validator and "listens"
UniqueEntity vailoations. And if there are such violations, this
normalizer adds already existing entity(ies) to the response.
Hope it helps!
我有实体 Tag
,它有唯一的 属性 tagValue
。当我用已经存在的 tagValue
创建 POST
时,我想得到它作为响应。
config/validator/tag.yaml
:
App\Entity\Tag:
constraints:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: tagValue
properties:
tagValue:
- NotBlank: ~
src/Entity/Tag.php
:
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use DateTimeInterface;
use DateTime;
use Exception;
/**
* @ORM\Table(name="tag")
* @ORM\Entity(repositoryClass="App\Repository\TagRepository")
* @ORM\HasLifecycleCallbacks
*/
class Tag
{
/**
* @var int
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string
* @ORM\Column(type="string", length=255)
*/
private $tagValue;
// ...
}
当我做一个 POST
:
curl --request POST \
--url http://127.0.0.1:8888/api/tags \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-auth-token: xxxxxxxxxxxxxxxx' \
--data '{
"tagValue": "test"
}'
我收到了刚刚创建的实体和代码 201 的响应。一切正常,但如果我再次发出此请求,如预期的那样,我将收到响应代码 400 和响应正文:
{
"type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
"title": "An error occurred",
"detail": "tagValue: This value is already used.",
"violations": [
{
"propertyPath": "tagValue",
"message": "This value is already used."
}
]
}
但我希望将现有实体包含到该响应中。
有什么想法可以在不违反 REST 规则的情况下做到这一点吗?
(Symfony 4.2.5, api-platform/api-pack 1.2.0)
我终于从 maks-rafalko (I really appreciate him for that) and if someone will stuck on the same issue, here's his solution 那里得到了关于 GitHub 的答案:
You are lucky man, we have just implemented it inside our application. There is no built in functionality in API-Platform for this feature, we had to override some classes in order to add it.
首先,这是当 Unique 约束被取消时我们的响应现在的样子:
{
"type": "https://tools.ietf.org/html/rfc2616#section-10",
"title": "An error occurred",
"detail": "number: This Usage Reference already exists with the same number and channel.",
"violations": [
{
"propertyPath": "number",
"message": "This Usage Reference already exists with the same number and channel."
}
],
"existingUniqueEntities": [
{
"uniquePropertyPaths": [
"number",
"channel"
],
"entity": {
"id": 1101,
"number": "23423423435",
"channel": "/api/channels/1",
"createdAt": "2019-07-17T07:25:50.721Z"
}
}
]
}
请注意,您可能有许多独特的违规行为,并且这种架构可能会 return 许多 实体已经存在并与提供的请求冲突(例如实体可以有 2 对唯一键,一对通过电子邮件,另一对通过引用)
另外,我们的实现恰好使用了那些将通过执行 GET /resource 使用的序列化组,其中 resource 是您尝试创建的资源。我们从 api 平台元数据
中获取这些序列化组代码如下:
<?php
declare(strict_types=1);
namespace App\Serializer;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Serializer\AbstractConstraintViolationListNormalizer;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* This class completely overrides `ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer` class
* since it's final
*
* Goal of overriding is to add `existingUniqueEntities` key when ViolationList contains unique entity violations
*
* @see \ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer
*/
class ConstraintViolationListNormalizer extends AbstractConstraintViolationListNormalizer implements NormalizerAwareInterface
{
public const FORMAT = 'jsonproblem';
public const TYPE = 'type';
public const TITLE = 'title';
/**
* @var array<string, string>
*/
private $defaultContext = [
self::TYPE => 'https://tools.ietf.org/html/rfc2616#section-10',
self::TITLE => 'An error occurred',
];
/**
* @var ResourceMetadataFactoryInterface
*/
private $resourceMetadataFactory;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var NormalizerInterface
*/
private $normalizer;
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $serializePayloadFields = null, NameConverterInterface $nameConverter = null, array $defaultContext = [])
{
parent::__construct($serializePayloadFields, $nameConverter);
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function setNormalizer(NormalizerInterface $normalizer): void
{
$this->normalizer = $normalizer;
}
/**
* @param mixed $object
* @param string|null $format
* @param array $context
*
* @return array
*/
public function normalize($object, $format = null, array $context = []): array
{
[$messages, $violations] = $this->getMessagesAndViolations($object);
$response = [
'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE],
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE],
'detail' => $messages ? implode("\n", $messages) : (string) $object,
'violations' => $violations,
];
$existingUniqueEntities = $this->getExistingUniqueEntities($object);
return \count($existingUniqueEntities) > 0 ?
array_merge($response, ['existingUniqueEntities' => $existingUniqueEntities])
: $response;
}
private function getExistingUniqueEntities(ConstraintViolationListInterface $constraintViolationList): array
{
$existingUniqueEntities = [];
/** @var ConstraintViolation $violation */
foreach ($constraintViolationList as $violation) {
$constraint = $violation->getConstraint();
if (!$constraint instanceof UniqueEntity) {
continue;
}
$rootEntity = \is_object($violation->getRoot()) ? $violation->getRoot() : null;
if ($rootEntity === null) {
continue;
}
$existingEntityCausedViolation = $violation->getCause()[0];
$metadata = $this->resourceMetadataFactory->create(\get_class($existingEntityCausedViolation));
// get normalization groups for `GET /resource` operation, fallback to global resource groups
$normalizationContext = $metadata->getItemOperationAttribute('get', 'normalization_context', [], true);
$groups = $normalizationContext['groups'] ?? [];
$entityNormalizationContext = \count($groups) > 0 ? ['groups' => $groups] : [];
$existingUniqueEntities[] = [
'uniquePropertyPaths' => $constraint->fields,
'entity' => $this->normalizer->normalize($existingEntityCausedViolation, null, $entityNormalizationContext),
];
}
return $existingUniqueEntities;
}
}
一切都在 getExistingUniqueEntities 中,但不幸的是,我们不得不完全覆盖 ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer class 因为它是最终的,我们无法扩展它。
我们设法使用 Compiler Pass 覆盖了它:
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
{
private const CONSTRAINT_VIOLATION_LIST_NORMALIZER_PRIORITY = -780;
...
public function process(ContainerBuilder $container)
{
...
$constraintViolationListNormalizerDefinition = new Definition(
ConstraintViolationListNormalizer::class,
[
$container->getDefinition('api_platform.metadata.resource.metadata_factory.cached'),
$container->getParameter('api_platform.validator.serialize_payload_fields'),
$container->hasDefinition('api_platform.name_converter') ? $container->getDefinition('api_platform.name_converter') : null,
[],
]
);
$constraintViolationListNormalizerDefinition->addTag('serializer.normalizer', ['priority' => self::CONSTRAINT_VIOLATION_LIST_NORMALIZER_PRIORITY]);
$container->setDefinition('api_platform.problem.normalizer.constraint_violation_list', $constraintViolationListNormalizerDefinition);
}
So, this solution is based on Symfony Validator and "listens" UniqueEntity vailoations. And if there are such violations, this normalizer adds already existing entity(ies) to the response.
Hope it helps!