Passed
Push — feature/uploadable ( 7c6d25...a7ed20 )
by Daniel
11:07
created

PublishableNormalizer::denormalize()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 41
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
eloc 20
c 0
b 0
f 0
dl 0
loc 41
ccs 0
cts 20
cp 0
rs 8.4444
cc 8
nc 5
nop 4
crap 72
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\Bridge\Symfony\Validator\Exception\ValidationException;
17
use ApiPlatform\Core\Validator\ValidatorInterface;
18
use Doctrine\ORM\Mapping\ClassMetadataInfo;
19
use Doctrine\Persistence\ManagerRegistry;
20
use Silverback\ApiComponentsBundle\Annotation\Publishable;
21
use Silverback\ApiComponentsBundle\Exception\InvalidArgumentException;
22
use Silverback\ApiComponentsBundle\Publishable\PublishableHelper;
23
use Silverback\ApiComponentsBundle\Validator\PublishableValidator;
24
use Symfony\Component\HttpFoundation\RequestStack;
25
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
26
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
27
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
28
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
29
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
30
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
31
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
32
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
33
34
/**
35
 * Adds `published` property on response, if not set.
36
 *
37
 * @author Vincent Chalamon <[email protected]>
38
 */
39
final class PublishableNormalizer implements ContextAwareNormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface, ContextAwareDenormalizerInterface, DenormalizerAwareInterface
40
{
41
    use NormalizerAwareTrait;
42
    use DenormalizerAwareTrait;
43
44
    private const ALREADY_CALLED = 'PUBLISHABLE_NORMALIZER_ALREADY_CALLED';
45
46
    private PublishableHelper $publishableHelper;
47
    private ManagerRegistry $registry;
48
    private RequestStack $requestStack;
49
    private ValidatorInterface $validator;
50
51
    public function __construct(PublishableHelper $publishableHelper, ManagerRegistry $registry, RequestStack $requestStack, ValidatorInterface $validator)
52
    {
53
        $this->publishableHelper = $publishableHelper;
54
        $this->registry = $registry;
55
        $this->requestStack = $requestStack;
56
        $this->validator = $validator;
57
    }
58
59
    public function normalize($object, $format = null, array $context = [])
60
    {
61
        $context[self::ALREADY_CALLED] = true;
62
        $context[MetadataNormalizer::METADATA_CONTEXT]['published'] = $this->publishableHelper->isActivePublishedAt($object);
63
64
        if ($this->publishableHelper->isGranted($object)) {
65
            try {
66
                $this->validator->validate($object, [PublishableValidator::PUBLISHED_KEY => true]);
67
            } catch (ValidationException $exception) {
68
                $context[MetadataNormalizer::METADATA_CONTEXT]['violation_list'] = $this->normalizer->normalize($exception->getConstraintViolationList(), $format);
69
            }
70
        }
71
72
        return $this->normalizer->normalize($object, $format, $context);
73
    }
74
75
    public function supportsNormalization($data, $format = null, $context = []): bool
76
    {
77
        return !isset($context[self::ALREADY_CALLED]) &&
78
            \is_object($data) &&
79
            !$data instanceof \Traversable &&
80
            $this->publishableHelper->getAnnotationReader()->isConfigured($data);
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function denormalize($data, $type, $format = null, array $context = [])
87
    {
88
        $context[self::ALREADY_CALLED] = true;
89
        $configuration = $this->publishableHelper->getAnnotationReader()->getConfiguration($type);
90
91
        $data = $this->unsetRestrictedData($type, $data, $configuration);
92
93
        $request = $this->requestStack->getMasterRequest();
94
        if ($request && true === $this->publishableHelper->isPublishedRequest($request)) {
95
            return $this->denormalizer->denormalize($data, $type, $format, $context);
96
        }
97
98
        // It's a new object
99
        if (!isset($context[AbstractNormalizer::OBJECT_TO_POPULATE])) {
100
            // User doesn't have draft access: force publication date
101
            if (!$this->publishableHelper->isGranted($type)) {
102
                $data[$configuration->fieldName] = date('Y-m-d H:i:s');
103
            }
104
105
            return $this->denormalizer->denormalize($data, $type, $format, $context);
106
        }
107
108
        $object = $context[AbstractNormalizer::OBJECT_TO_POPULATE];
109
        $data = $this->setPublishedAt($data, $configuration, $object);
110
111
        // No field has been updated (after publishedAt verified and cleaned/unset if needed): nothing to do here anymore
112
        // or User doesn't have draft access: update the original object
113
        if (
114
            empty($data) ||
115
            !$this->publishableHelper->isActivePublishedAt($object) ||
116
            !$this->publishableHelper->isGranted($type)
117
        ) {
118
            return $this->denormalizer->denormalize($data, $type, $format, $context);
119
        }
120
121
        // Any field has been modified: create a draft
122
        $draft = $this->createDraft($object, $configuration, $type);
123
124
        $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $draft;
125
126
        return $this->denormalizer->denormalize($data, $type, $format, $context);
127
    }
128
129
    private function setPublishedAt(array $data, Publishable $configuration, object $object): array
130
    {
131
        if (isset($data[$configuration->fieldName])) {
132
            $publicationDate = new \DateTimeImmutable($data[$configuration->fieldName]);
133
134
            // User changed the publication date with an earlier one on a published resource: ignore it
135
            if (
136
                $this->publishableHelper->isActivePublishedAt($object) &&
137
                new \DateTimeImmutable() >= $publicationDate
138
            ) {
139
                unset($data[$configuration->fieldName]);
140
            }
141
        }
142
143
        return $data;
144
    }
145
146
    private function unsetRestrictedData($type, array $data, Publishable $configuration): array
147
    {
148
        // It's not possible to change the publishedResource and draftResource properties
149
        unset($data[$configuration->associationName], $data[$configuration->reverseAssociationName]);
150
151
        // User doesn't have draft access: cannot set or change the publication date
152
        if (!$this->publishableHelper->isGranted($type)) {
153
            unset($data[$configuration->fieldName]);
154
        }
155
156
        return $data;
157
    }
158
159
    private function createDraft(object $object, Publishable $configuration, string $type): object
160
    {
161
        $em = $this->registry->getManagerForClass($type);
162
        if (!$em) {
163
            throw new InvalidArgumentException(sprintf('Could not find entity manager for class %s', $type));
164
        }
165
166
        /** @var ClassMetadataInfo $classMetadata */
167
        $classMetadata = $em->getClassMetadata($type);
168
169
        // Resource is a draft: nothing to do here anymore
170
        if (null !== $classMetadata->getFieldValue($object, $configuration->associationName)) {
171
            return $object;
172
        }
173
174
        $draft = clone $object; // Identifier(s) should be reset from AbstractComponent::__clone method
175
176
        // Empty publishedDate on draft
177
        $classMetadata->setFieldValue($draft, $configuration->fieldName, null);
178
179
        // Set publishedResource on draft
180
        $classMetadata->setFieldValue($draft, $configuration->associationName, $object);
181
182
        // Set draftResource on data
183
        $classMetadata->setFieldValue($object, $configuration->reverseAssociationName, $draft);
184
185
        // Add draft object to UnitOfWork
186
        $em->persist($draft);
187
188
        return $draft;
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194
    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
195
    {
196
        return !isset($context[self::ALREADY_CALLED]) && $this->publishableHelper->getAnnotationReader()->isConfigured($type);
197
    }
198
199
    public function hasCacheableSupportsMethod(): bool
200
    {
201
        return false;
202
    }
203
}
204