Test Failed
Pull Request — main (#152)
by Daniel
06:32 queued 02:15
created

PublishableNormalizer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
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 8
dl 0
loc 11
rs 10
ccs 0
cts 1
cp 0
crap 2

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\Event\ResourceChangedEvent;
24
use Silverback\ApiComponentsBundle\Exception\InvalidArgumentException;
25
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
26
use Silverback\ApiComponentsBundle\Helper\Uploadable\UploadableFileManager;
27
use Silverback\ApiComponentsBundle\Serializer\ResourceMetadata\ResourceMetadataProvider;
28
use Silverback\ApiComponentsBundle\Validator\PublishableValidator;
29
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
30
use Symfony\Component\HttpFoundation\RequestStack;
31
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
32
use Symfony\Component\PropertyAccess\PropertyAccess;
33
use Symfony\Component\PropertyAccess\PropertyAccessor;
34
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
35
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
36
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
37
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
38
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
39
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
40
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
41
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
42
43
/**
44
 * Adds `published` property on response, if not set.
45
 *
46
 * @author Vincent Chalamon <[email protected]>
47
 */
48
final class PublishableNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface, DenormalizerInterface, DenormalizerAwareInterface
49
{
50
    use DenormalizerAwareTrait;
51
    use NormalizerAwareTrait;
52
53
    private const ALREADY_CALLED = 'PUBLISHABLE_NORMALIZER_ALREADY_CALLED';
54
    private const ASSOCIATION = 'PUBLISHABLE_ASSOCIATION';
55
56
    private PropertyAccessor $propertyAccessor;
57
58
    public function __construct(
59
        private readonly PublishableStatusChecker $publishableStatusChecker,
60
        private readonly ManagerRegistry $registry,
61
        private readonly RequestStack $requestStack,
62
        private readonly ValidatorInterface $validator,
63
        private readonly IriConverterInterface $iriConverter,
64
        private readonly UploadableFileManager $uploadableFileManager,
65
        private readonly ResourceMetadataProvider $resourceMetadataProvider,
66
        private readonly EventDispatcherInterface $eventDispatcher
67
    ) {
68
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
69
    }
70
71
    public function normalize($object, $format = null, array $context = []): float|array|\ArrayObject|bool|int|string|null
72
    {
73
        $context[self::ALREADY_CALLED][] = $this->propertyAccessor->getValue($object, 'id');
74
75
        if (isset($context[self::ASSOCIATION]) && $context[self::ASSOCIATION] === $object) {
76
            return $this->iriConverter->getIriFromResource($object);
77
        }
78
79
        $isPublished = $this->publishableStatusChecker->isActivePublishedAt($object);
80
81
        $resourceMetadata = $this->resourceMetadataProvider->findResourceMetadata($object);
82
        $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
            $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
                $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
        $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $draft;
176
177
        return $this->denormalizer->denormalize($data, $type, $format, $context);
178
    }
179
180
    private function setPublishedAt(array $data, Publishable $configuration, object $object): array
181
    {
182
        if (isset($data[$configuration->fieldName])) {
183
            $publicationDate = new \DateTimeImmutable($data[$configuration->fieldName]);
184
185
            // User changed the publication date with an earlier one on a published resource: ignore it
186
            if (
187
                $this->publishableStatusChecker->isActivePublishedAt($object) &&
188
                new \DateTimeImmutable() >= $publicationDate
189
            ) {
190
                unset($data[$configuration->fieldName]);
191
            }
192
        }
193
194
        return $data;
195
    }
196
197
    private function unsetRestrictedData($type, array $data, Publishable $configuration): array
198
    {
199
        // It's not possible to change the publishedResource and draftResource properties
200
        unset($data[$configuration->associationName], $data[$configuration->reverseAssociationName]);
201
202
        // User doesn't have draft access: cannot set or change the publication date
203
        if (!$this->publishableStatusChecker->isGranted($type)) {
204
            unset($data[$configuration->fieldName]);
205
        }
206
207
        return $data;
208
    }
209
210
    public function createDraft(object $object, Publishable $configuration, string $type): object
211
    {
212
        $em = $this->getManagerFromType($type);
213
        $classMetadata = $this->getClassMetadataInfo($em, $type);
214
215
        // Resource is a draft: nothing to do here anymore
216
        if (null !== $classMetadata->getFieldValue($object, $configuration->associationName)) {
217
            return $object;
218
        }
219
220
        $draft = clone $object; // Identifier(s) should be reset from AbstractComponent::__clone method
221
222
        // Empty publishedDate on draft
223
        $classMetadata->setFieldValue($draft, $configuration->fieldName, null);
224
225
        // Set publishedResource on draft
226
        $classMetadata->setFieldValue($draft, $configuration->associationName, $object);
227
228
        // Set draftResource on data if we have permission
229
        $classMetadata->setFieldValue($object, $configuration->reverseAssociationName, $draft);
230
231
        try {
232
            $this->uploadableFileManager->processClonedUploadable($object, $draft);
233
        } catch (\InvalidArgumentException $e) {
234
            // ok exception, it may not be uploadable...
235
        }
236
237
        // Add draft object to UnitOfWork
238
        $em->persist($draft);
239
240
        // Clear the cache of the published resource because it should now also return an associated draft
241
        $event = new ResourceChangedEvent($object);
242
        $this->eventDispatcher->dispatch($event);
243
244
        return $draft;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250
    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
251
    {
252
        return !isset($context[self::ALREADY_CALLED]) && $this->publishableStatusChecker->getAnnotationReader()->isConfigured($type);
253
    }
254
255
    public function hasCacheableSupportsMethod(): bool
256
    {
257
        return false;
258
    }
259
260
    private function getManagerFromType(string $type): ObjectManager
261
    {
262
        $em = $this->registry->getManagerForClass($type);
263
        if (!$em) {
264
            throw new InvalidArgumentException(sprintf('Could not find entity manager for class %s', $type));
265
        }
266
267
        return $em;
268
    }
269
270
    private function getClassMetadataInfo(ObjectManager $em, string $type): ClassMetadataInfo
271
    {
272
        $classMetadata = $em->getClassMetadata($type);
273
        if (!$classMetadata instanceof ClassMetadataInfo) {
274
            throw new InvalidArgumentException(sprintf('Class metadata for %s was not an instance of %s', $type, ClassMetadataInfo::class));
275
        }
276
277
        return $classMetadata;
278
    }
279
}
280