1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Silverback API Components Bundle Project |
5
|
|
|
* |
6
|
|
|
* (c) Daniel West <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
declare(strict_types=1); |
13
|
|
|
|
14
|
|
|
namespace Silverback\ApiComponentsBundle\Serializer\Normalizer; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Api\IriConverterInterface; |
17
|
|
|
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException; |
18
|
|
|
use ApiPlatform\Core\Validator\ValidatorInterface; |
19
|
|
|
use Doctrine\ORM\Mapping\ClassMetadataInfo; |
20
|
|
|
use Doctrine\Persistence\ManagerRegistry; |
21
|
|
|
use Doctrine\Persistence\ObjectManager; |
22
|
|
|
use Silverback\ApiComponentsBundle\Annotation\Publishable; |
23
|
|
|
use Silverback\ApiComponentsBundle\Exception\InvalidArgumentException; |
24
|
|
|
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker; |
25
|
|
|
use Silverback\ApiComponentsBundle\Validator\PublishableValidator; |
26
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
27
|
|
|
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; |
28
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccess; |
29
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessor; |
30
|
|
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; |
31
|
|
|
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; |
32
|
|
|
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; |
33
|
|
|
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; |
34
|
|
|
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; |
35
|
|
|
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; |
36
|
|
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; |
37
|
|
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Adds `published` property on response, if not set. |
41
|
|
|
* |
42
|
|
|
* @author Vincent Chalamon <[email protected]> |
43
|
|
|
*/ |
44
|
|
|
final class PublishableNormalizer implements ContextAwareNormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface, ContextAwareDenormalizerInterface, DenormalizerAwareInterface |
45
|
|
|
{ |
46
|
|
|
use DenormalizerAwareTrait; |
47
|
|
|
use NormalizerAwareTrait; |
48
|
|
|
|
49
|
|
|
private const ALREADY_CALLED = 'PUBLISHABLE_NORMALIZER_ALREADY_CALLED'; |
50
|
|
|
private const ASSOCIATION = 'PUBLISHABLE_ASSOCIATION'; |
51
|
|
|
|
52
|
|
|
private PublishableStatusChecker $publishableStatusChecker; |
53
|
|
|
private ManagerRegistry $registry; |
54
|
|
|
private RequestStack $requestStack; |
55
|
|
|
private ValidatorInterface $validator; |
56
|
|
|
private PropertyAccessor $propertyAccessor; |
57
|
|
|
private IriConverterInterface $iriConverter; |
58
|
|
|
|
59
|
|
|
public function __construct( |
60
|
|
|
PublishableStatusChecker $publishableStatusChecker, |
61
|
|
|
ManagerRegistry $registry, |
62
|
|
|
RequestStack $requestStack, |
63
|
|
|
ValidatorInterface $validator, |
64
|
|
|
IriConverterInterface $iriConverter |
65
|
|
|
) { |
66
|
|
|
$this->publishableStatusChecker = $publishableStatusChecker; |
67
|
|
|
$this->registry = $registry; |
68
|
|
|
$this->requestStack = $requestStack; |
69
|
|
|
$this->validator = $validator; |
70
|
|
|
$this->propertyAccessor = PropertyAccess::createPropertyAccessor(); |
71
|
|
|
$this->iriConverter = $iriConverter; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
public function normalize($object, $format = null, array $context = []) |
75
|
|
|
{ |
76
|
|
|
$context[self::ALREADY_CALLED][] = $this->propertyAccessor->getValue($object, 'id'); |
77
|
|
|
$context[MetadataNormalizer::METADATA_CONTEXT]['published'] = $this->publishableStatusChecker->isActivePublishedAt($object); |
78
|
|
|
|
79
|
|
|
if (isset($context[self::ASSOCIATION]) && $context[self::ASSOCIATION] === $object) { |
80
|
|
|
return $this->iriConverter->getIriFromItem($object); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
$type = \get_class($object); |
84
|
|
|
$configuration = $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($type); |
85
|
|
|
$em = $this->getManagerFromType($type); |
86
|
|
|
$classMetadata = $this->getClassMetadataInfo($em, $type); |
87
|
|
|
|
88
|
|
|
$context[MetadataNormalizer::METADATA_CONTEXT][$configuration->fieldName] = $classMetadata->getFieldValue($object, $configuration->fieldName); |
89
|
|
|
if (\is_object($assocObject = $classMetadata->getFieldValue($object, $configuration->associationName))) { |
90
|
|
|
$context[self::ASSOCIATION] = $assocObject; |
91
|
|
|
} |
92
|
|
|
if (\is_object($reverseAssocObject = $classMetadata->getFieldValue($object, $configuration->reverseAssociationName))) { |
93
|
|
|
$context[self::ASSOCIATION] = $reverseAssocObject; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
// display soft validation violations in the response |
97
|
|
|
if ($this->publishableStatusChecker->isGranted($object)) { |
98
|
|
|
try { |
99
|
|
|
$this->validator->validate($object, [PublishableValidator::PUBLISHED_KEY => true]); |
100
|
|
|
} catch (ValidationException $exception) { |
101
|
|
|
$context[MetadataNormalizer::METADATA_CONTEXT]['violation_list'] = $this->normalizer->normalize($exception->getConstraintViolationList(), $format); |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
return $this->normalizer->normalize($object, $format, $context); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
public function supportsNormalization($data, $format = null, $context = []): bool |
109
|
|
|
{ |
110
|
|
|
if (!\is_object($data) || $data instanceof \Traversable) { |
111
|
|
|
return false; |
112
|
|
|
} |
113
|
|
|
if (!isset($context[self::ALREADY_CALLED])) { |
114
|
|
|
$context[self::ALREADY_CALLED] = []; |
115
|
|
|
} |
116
|
|
|
try { |
117
|
|
|
$id = $this->propertyAccessor->getValue($data, 'id'); |
118
|
|
|
} catch (NoSuchPropertyException $e) { |
119
|
|
|
return false; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
return !\in_array($id, $context[self::ALREADY_CALLED], true) && |
123
|
|
|
$this->publishableStatusChecker->getAnnotationReader()->isConfigured($data); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* {@inheritdoc} |
128
|
|
|
*/ |
129
|
|
|
public function denormalize($data, $type, $format = null, array $context = []) |
130
|
|
|
{ |
131
|
|
|
$context[self::ALREADY_CALLED] = true; |
132
|
|
|
$configuration = $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($type); |
133
|
|
|
|
134
|
|
|
$data = $this->unsetRestrictedData($type, $data, $configuration); |
135
|
|
|
|
136
|
|
|
$request = $this->requestStack->getMasterRequest(); |
|
|
|
|
137
|
|
|
if ($request && true === $this->publishableStatusChecker->isPublishedRequest($request)) { |
138
|
|
|
return $this->denormalizer->denormalize($data, $type, $format, $context); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
// It's a new object |
142
|
|
|
if (!isset($context[AbstractNormalizer::OBJECT_TO_POPULATE])) { |
143
|
|
|
// User doesn't have draft access: force publication date |
144
|
|
|
if (!$this->publishableStatusChecker->isGranted($type)) { |
145
|
|
|
$data[$configuration->fieldName] = date('Y-m-d H:i:s'); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
return $this->denormalizer->denormalize($data, $type, $format, $context); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$object = $context[AbstractNormalizer::OBJECT_TO_POPULATE]; |
152
|
|
|
$data = $this->setPublishedAt($data, $configuration, $object); |
153
|
|
|
|
154
|
|
|
// No field has been updated (after publishedAt verified and cleaned/unset if needed): nothing to do here anymore |
155
|
|
|
// or User doesn't have draft access: update the original object |
156
|
|
|
if ( |
157
|
|
|
empty($data) || |
158
|
|
|
!$this->publishableStatusChecker->isActivePublishedAt($object) || |
159
|
|
|
!$this->publishableStatusChecker->isGranted($type) |
160
|
|
|
) { |
161
|
|
|
return $this->denormalizer->denormalize($data, $type, $format, $context); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
// Any field has been modified: create a draft |
165
|
|
|
$draft = $this->createDraft($object, $configuration, $type); |
166
|
|
|
|
167
|
|
|
$context[AbstractNormalizer::OBJECT_TO_POPULATE] = $draft; |
168
|
|
|
|
169
|
|
|
return $this->denormalizer->denormalize($data, $type, $format, $context); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
private function setPublishedAt(array $data, Publishable $configuration, object $object): array |
173
|
|
|
{ |
174
|
|
|
if (isset($data[$configuration->fieldName])) { |
175
|
|
|
$publicationDate = new \DateTimeImmutable($data[$configuration->fieldName]); |
176
|
|
|
|
177
|
|
|
// User changed the publication date with an earlier one on a published resource: ignore it |
178
|
|
|
if ( |
179
|
|
|
$this->publishableStatusChecker->isActivePublishedAt($object) && |
180
|
|
|
new \DateTimeImmutable() >= $publicationDate |
181
|
|
|
) { |
182
|
|
|
unset($data[$configuration->fieldName]); |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
return $data; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
private function unsetRestrictedData($type, array $data, Publishable $configuration): array |
190
|
|
|
{ |
191
|
|
|
// It's not possible to change the publishedResource and draftResource properties |
192
|
|
|
unset($data[$configuration->associationName], $data[$configuration->reverseAssociationName]); |
193
|
|
|
|
194
|
|
|
// User doesn't have draft access: cannot set or change the publication date |
195
|
|
|
if (!$this->publishableStatusChecker->isGranted($type)) { |
196
|
|
|
unset($data[$configuration->fieldName]); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
return $data; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
private function createDraft(object $object, Publishable $configuration, string $type): object |
203
|
|
|
{ |
204
|
|
|
$em = $this->getManagerFromType($type); |
205
|
|
|
$classMetadata = $this->getClassMetadataInfo($em, $type); |
206
|
|
|
|
207
|
|
|
// Resource is a draft: nothing to do here anymore |
208
|
|
|
if (null !== $classMetadata->getFieldValue($object, $configuration->associationName)) { |
209
|
|
|
return $object; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
$draft = clone $object; // Identifier(s) should be reset from AbstractComponent::__clone method |
213
|
|
|
|
214
|
|
|
// Empty publishedDate on draft |
215
|
|
|
$classMetadata->setFieldValue($draft, $configuration->fieldName, null); |
216
|
|
|
|
217
|
|
|
// Set publishedResource on draft |
218
|
|
|
$classMetadata->setFieldValue($draft, $configuration->associationName, $object); |
219
|
|
|
|
220
|
|
|
// Set draftResource on data if we have permission |
221
|
|
|
$classMetadata->setFieldValue($object, $configuration->reverseAssociationName, $draft); |
222
|
|
|
|
223
|
|
|
// Add draft object to UnitOfWork |
224
|
|
|
$em->persist($draft); |
225
|
|
|
|
226
|
|
|
return $draft; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* {@inheritdoc} |
231
|
|
|
*/ |
232
|
|
|
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool |
233
|
|
|
{ |
234
|
|
|
return !isset($context[self::ALREADY_CALLED]) && $this->publishableStatusChecker->getAnnotationReader()->isConfigured($type); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
public function hasCacheableSupportsMethod(): bool |
238
|
|
|
{ |
239
|
|
|
return false; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
private function getManagerFromType(string $type): ObjectManager |
243
|
|
|
{ |
244
|
|
|
$em = $this->registry->getManagerForClass($type); |
245
|
|
|
if (!$em) { |
246
|
|
|
throw new InvalidArgumentException(sprintf('Could not find entity manager for class %s', $type)); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
return $em; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
private function getClassMetadataInfo(ObjectManager $em, string $type): ClassMetadataInfo |
253
|
|
|
{ |
254
|
|
|
$classMetadata = $em->getClassMetadata($type); |
255
|
|
|
if (!$classMetadata instanceof ClassMetadataInfo) { |
256
|
|
|
throw new InvalidArgumentException(sprintf('Class metadata for %s was not an instance of %s', $type, ClassMetadataInfo::class)); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
return $classMetadata; |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.