Test Failed
Pull Request — main (#152)
by Daniel
04:35
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\ResourceMetadataProvider;
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 ResourceMetadataProvider $resourceMetadataProvider,
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
83
        $resourceMetadata = $this->resourceMetadataProvider->findResourceMetadata($object);
84
        $resourceMetadata->setPublishable($isPublished);
85
86
        $type = \get_class($object);
87
        $configuration = $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($type);
88
        $em = $this->getManagerFromType($type);
89
        $classMetadata = $this->getClassMetadataInfo($em, $type);
90
91
        $publishedAtDateTime = $classMetadata->getFieldValue($object, $configuration->fieldName);
92
        if ($publishedAtDateTime instanceof \DateTimeInterface) {
93
            $publishedAtDateTime = $publishedAtDateTime->format(\DateTimeInterface::RFC3339_EXTENDED);
94
        }
95
96
        // using static name 'publishedAt' for predictable API and easy metadata object instead of dynamic $configuration->fieldName
97
        if ($publishedAtDateTime) {
98
            $resourceMetadata->setPublishable($isPublished, $publishedAtDateTime);
99
        }
100
101
        if (\is_object($assocObject = $classMetadata->getFieldValue($object, $configuration->associationName))) {
102
            $context[self::ASSOCIATION] = $assocObject;
103
        } elseif (\is_object($reverseAssocObject = $classMetadata->getFieldValue($object, $configuration->reverseAssociationName))) {
104
            $context[self::ASSOCIATION] = $reverseAssocObject;
105
        }
106
107
        // display soft validation violations in the response
108
        if ($this->publishableStatusChecker->isGranted($object)) {
109
            try {
110
                $this->validator->validate($object, [PublishableValidator::PUBLISHED_KEY => true]);
111
            } catch (ValidationException $exception) {
112
                $resourceMetadata->setViolationList($exception->getConstraintViolationList());
113
            }
114
        }
115
116
        return $this->normalizer->normalize($object, $format, $context);
117
    }
118
119
    public function supportsNormalization($data, $format = null, $context = []): bool
120
    {
121
        if (!\is_object($data) || $data instanceof \Traversable) {
122
            return false;
123
        }
124
        if (!isset($context[self::ALREADY_CALLED])) {
125
            $context[self::ALREADY_CALLED] = [];
126
        }
127
        try {
128
            $id = $this->propertyAccessor->getValue($data, 'id');
129
        } catch (NoSuchPropertyException $e) {
130
            return false;
131
        }
132
133
        return !\in_array($id, $context[self::ALREADY_CALLED], true) &&
134
            $this->publishableStatusChecker->getAnnotationReader()->isConfigured($data);
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140
    public function denormalize($data, $type, $format = null, array $context = []): mixed
141
    {
142
        $context[self::ALREADY_CALLED] = true;
143
        $configuration = $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($type);
144
145
        $data = $this->unsetRestrictedData($type, $data, $configuration);
146
147
        $request = $this->requestStack->getMainRequest();
148
        if ($request && true === $this->publishableStatusChecker->isPublishedRequest($request)) {
149
            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

149
            return $this->denormalizer->denormalize($data, /** @scrutinizer ignore-type */ $type, $format, $context);
Loading history...
150
        }
151
152
        // It's a new object
153
        if (!isset($context[AbstractNormalizer::OBJECT_TO_POPULATE])) {
154
            // User doesn't have draft access: force publication date
155
            if (!$this->publishableStatusChecker->isGranted($type)) {
156
                $data[$configuration->fieldName] = date('Y-m-d H:i:s');
157
            }
158
159
            return $this->denormalizer->denormalize($data, $type, $format, $context);
160
        }
161
162
        $object = $context[AbstractNormalizer::OBJECT_TO_POPULATE];
163
        $data = $this->setPublishedAt($data, $configuration, $object);
164
165
        // No field has been updated (after publishedAt verified and cleaned/unset if needed): nothing to do here anymore
166
        // or User doesn't have draft access: update the original object
167
        if (
168
            empty($data) ||
169
            !$this->publishableStatusChecker->isActivePublishedAt($object) ||
170
            !$this->publishableStatusChecker->isGranted($type)
171
        ) {
172
            return $this->denormalizer->denormalize($data, $type, $format, $context);
173
        }
174
175
        // Any field has been modified: create a draft
176
        $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

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