Passed
Pull Request — main (#152)
by Daniel
06:28
created

PublishableNormalizer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 4
b 0
f 0
nc 1
nop 9
dl 0
loc 12
ccs 0
cts 2
cp 0
crap 2
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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\Api\IriConverterInterface;
17
use ApiPlatform\Symfony\Validator\Exception\ValidationException;
18
use ApiPlatform\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\Entity\Core\AbstractComponent;
24
use Silverback\ApiComponentsBundle\EventListener\Doctrine\PurgeHttpCacheListener;
25
use Silverback\ApiComponentsBundle\Exception\InvalidArgumentException;
26
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
27
use Silverback\ApiComponentsBundle\Helper\Uploadable\UploadableFileManager;
28
use Silverback\ApiComponentsBundle\Mercure\MercureResourcePublisher;
29
use Silverback\ApiComponentsBundle\Serializer\ResourceMetadata\ResourceMetadataInterface;
30
use Silverback\ApiComponentsBundle\Validator\PublishableValidator;
31
use Symfony\Component\HttpFoundation\RequestStack;
32
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
33
use Symfony\Component\PropertyAccess\PropertyAccess;
34
use Symfony\Component\PropertyAccess\PropertyAccessor;
35
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
36
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
37
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
38
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
39
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
40
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
41
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
42
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
43
44
/**
45
 * Adds `published` property on response, if not set.
46
 *
47
 * @author Vincent Chalamon <[email protected]>
48
 */
49
final class PublishableNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface, DenormalizerInterface, DenormalizerAwareInterface
50
{
51
    use DenormalizerAwareTrait;
52
    use NormalizerAwareTrait;
53
54
    private const ALREADY_CALLED = 'PUBLISHABLE_NORMALIZER_ALREADY_CALLED';
55
    private const ASSOCIATION = 'PUBLISHABLE_ASSOCIATION';
56
57
    private PropertyAccessor $propertyAccessor;
58
59
    public function __construct(
60
        private PublishableStatusChecker $publishableStatusChecker,
61
        private ManagerRegistry $registry,
62
        private RequestStack $requestStack,
63
        private ValidatorInterface $validator,
64
        private IriConverterInterface $iriConverter,
65
        private UploadableFileManager $uploadableFileManager,
66
        private ResourceMetadataInterface $resourceMetadata,
67
        private ?PurgeHttpCacheListener $purgeHttpCacheListener = null,
68
        private ?MercureResourcePublisher $mercureResourcePublisher = null
69
    ) {
70
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
71
    }
72
73
    public function normalize($object, $format = null, array $context = []): float|array|\ArrayObject|bool|int|string|null
74
    {
75
        $context[self::ALREADY_CALLED][] = $this->propertyAccessor->getValue($object, 'id');
76
77
        if (isset($context[self::ASSOCIATION]) && $context[self::ASSOCIATION] === $object) {
78
            return $this->iriConverter->getIriFromResource($object);
79
        }
80
81
        $isPublished = $this->publishableStatusChecker->isActivePublishedAt($object);
82
        $this->resourceMetadata->setPublishable($isPublished);
83
84
        $type = \get_class($object);
85
        $configuration = $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($type);
86
        $em = $this->getManagerFromType($type);
87
        $classMetadata = $this->getClassMetadataInfo($em, $type);
88
89
        $publishedAtDateTime = $classMetadata->getFieldValue($object, $configuration->fieldName);
90
        if ($publishedAtDateTime instanceof \DateTimeInterface) {
91
            $publishedAtDateTime = $publishedAtDateTime->format(\DateTimeInterface::RFC3339_EXTENDED);
92
        }
93
94
        // using static name 'publishedAt' for predictable API and easy metadata object instead of dynamic $configuration->fieldName
95
        if ($publishedAtDateTime) {
96
            $this->resourceMetadata->setPublishable($isPublished, $publishedAtDateTime);
97
        }
98
99
        if (\is_object($assocObject = $classMetadata->getFieldValue($object, $configuration->associationName))) {
100
            $context[self::ASSOCIATION] = $assocObject;
101
        } elseif (\is_object($reverseAssocObject = $classMetadata->getFieldValue($object, $configuration->reverseAssociationName))) {
102
            $context[self::ASSOCIATION] = $reverseAssocObject;
103
        }
104
105
        // display soft validation violations in the response
106
        if ($this->publishableStatusChecker->isGranted($object)) {
107
            try {
108
                $this->validator->validate($object, [PublishableValidator::PUBLISHED_KEY => true]);
109
            } catch (ValidationException $exception) {
110
                $this->resourceMetadata->setViolationList($exception->getConstraintViolationList());
111
            }
112
        }
113
114
        return $this->normalizer->normalize($object, $format, $context);
115
    }
116
117
    public function supportsNormalization($data, $format = null, $context = []): bool
118
    {
119
        if (!\is_object($data) || $data instanceof \Traversable) {
120
            return false;
121
        }
122
        if (!isset($context[self::ALREADY_CALLED])) {
123
            $context[self::ALREADY_CALLED] = [];
124
        }
125
        try {
126
            $id = $this->propertyAccessor->getValue($data, 'id');
127
        } catch (NoSuchPropertyException $e) {
128
            return false;
129
        }
130
131
        return !\in_array($id, $context[self::ALREADY_CALLED], true) &&
132
            $this->publishableStatusChecker->getAnnotationReader()->isConfigured($data);
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function denormalize($data, $type, $format = null, array $context = []): mixed
139
    {
140
        $context[self::ALREADY_CALLED] = true;
141
        $configuration = $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($type);
142
143
        $data = $this->unsetRestrictedData($type, $data, $configuration);
144
145
        $request = $this->requestStack->getMainRequest();
146
        if ($request && true === $this->publishableStatusChecker->isPublishedRequest($request)) {
147
            return $this->denormalizer->denormalize($data, $type, $format, $context);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type object; however, parameter $type of Symfony\Component\Serial...nterface::denormalize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

147
            return $this->denormalizer->denormalize($data, /** @scrutinizer ignore-type */ $type, $format, $context);
Loading history...
148
        }
149
150
        // It's a new object
151
        if (!isset($context[AbstractNormalizer::OBJECT_TO_POPULATE])) {
152
            // User doesn't have draft access: force publication date
153
            if (!$this->publishableStatusChecker->isGranted($type)) {
154
                $data[$configuration->fieldName] = date('Y-m-d H:i:s');
155
            }
156
157
            return $this->denormalizer->denormalize($data, $type, $format, $context);
158
        }
159
160
        $object = $context[AbstractNormalizer::OBJECT_TO_POPULATE];
161
        $data = $this->setPublishedAt($data, $configuration, $object);
162
163
        // No field has been updated (after publishedAt verified and cleaned/unset if needed): nothing to do here anymore
164
        // or User doesn't have draft access: update the original object
165
        if (
166
            empty($data) ||
167
            !$this->publishableStatusChecker->isActivePublishedAt($object) ||
168
            !$this->publishableStatusChecker->isGranted($type)
169
        ) {
170
            return $this->denormalizer->denormalize($data, $type, $format, $context);
171
        }
172
173
        // Any field has been modified: create a draft
174
        $draft = $this->createDraft($object, $configuration, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type object; however, parameter $type of Silverback\ApiComponents...rmalizer::createDraft() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
        $draft = $this->createDraft($object, $configuration, /** @scrutinizer ignore-type */ $type);
Loading history...
175
176
        $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $draft;
177
178
        return $this->denormalizer->denormalize($data, $type, $format, $context);
179
    }
180
181
    private function setPublishedAt(array $data, Publishable $configuration, object $object): array
182
    {
183
        if (isset($data[$configuration->fieldName])) {
184
            $publicationDate = new \DateTimeImmutable($data[$configuration->fieldName]);
185
186
            // User changed the publication date with an earlier one on a published resource: ignore it
187
            if (
188
                $this->publishableStatusChecker->isActivePublishedAt($object) &&
189
                new \DateTimeImmutable() >= $publicationDate
190
            ) {
191
                unset($data[$configuration->fieldName]);
192
            }
193
        }
194
195
        return $data;
196
    }
197
198
    private function unsetRestrictedData($type, array $data, Publishable $configuration): array
199
    {
200
        // It's not possible to change the publishedResource and draftResource properties
201
        unset($data[$configuration->associationName], $data[$configuration->reverseAssociationName]);
202
203
        // User doesn't have draft access: cannot set or change the publication date
204
        if (!$this->publishableStatusChecker->isGranted($type)) {
205
            unset($data[$configuration->fieldName]);
206
        }
207
208
        return $data;
209
    }
210
211
    public function createDraft(object $object, Publishable $configuration, string $type): object
212
    {
213
        $em = $this->getManagerFromType($type);
214
        $classMetadata = $this->getClassMetadataInfo($em, $type);
215
216
        // Resource is a draft: nothing to do here anymore
217
        if (null !== $classMetadata->getFieldValue($object, $configuration->associationName)) {
218
            return $object;
219
        }
220
221
        $draft = clone $object; // Identifier(s) should be reset from AbstractComponent::__clone method
222
223
        // Empty publishedDate on draft
224
        $classMetadata->setFieldValue($draft, $configuration->fieldName, null);
225
226
        // Set publishedResource on draft
227
        $classMetadata->setFieldValue($draft, $configuration->associationName, $object);
228
229
        // Set draftResource on data if we have permission
230
        $classMetadata->setFieldValue($object, $configuration->reverseAssociationName, $draft);
231
232
        try {
233
            $this->uploadableFileManager->processClonedUploadable($object, $draft);
234
        } catch (\InvalidArgumentException $e) {
235
            // ok exception, it may not be uploadable...
236
        }
237
238
        // Add draft object to UnitOfWork
239
        $em->persist($draft);
240
241
        // Clear the cache of the published resource because it should now also return an associated draft
242
        if ($this->purgeHttpCacheListener) {
243
            $this->purgeHttpCacheListener->addTagsFor($object);
244
        }
245
246
        if ($this->mercureResourcePublisher) {
247
            $this->mercureResourcePublisher->publishResourceUpdate($object);
248
249
            if ($object instanceof AbstractComponent) {
250
                foreach ($object->getComponentPositions() as $componentPosition) {
251
                    $this->mercureResourcePublisher->publishResourceUpdate($componentPosition);
252
                }
253
            }
254
        }
255
256
        return $draft;
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262
    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
263
    {
264
        return !isset($context[self::ALREADY_CALLED]) && $this->publishableStatusChecker->getAnnotationReader()->isConfigured($type);
265
    }
266
267
    public function hasCacheableSupportsMethod(): bool
268
    {
269
        return false;
270
    }
271
272
    private function getManagerFromType(string $type): ObjectManager
273
    {
274
        $em = $this->registry->getManagerForClass($type);
275
        if (!$em) {
276
            throw new InvalidArgumentException(sprintf('Could not find entity manager for class %s', $type));
277
        }
278
279
        return $em;
280
    }
281
282
    private function getClassMetadataInfo(ObjectManager $em, string $type): ClassMetadataInfo
283
    {
284
        $classMetadata = $em->getClassMetadata($type);
285
        if (!$classMetadata instanceof ClassMetadataInfo) {
286
            throw new InvalidArgumentException(sprintf('Class metadata for %s was not an instance of %s', $type, ClassMetadataInfo::class));
287
        }
288
289
        return $classMetadata;
290
    }
291
}
292