Passed
Push — master ( 43a943...86b561 )
by Daniel
12:01
created

PublishableNormalizer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 4
dl 0
loc 6
ccs 0
cts 5
cp 0
crap 2
rs 10
c 0
b 0
f 0
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\Helper\Publishable\PublishableStatusChecker;
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 PublishableStatusChecker $publishableStatusChecker;
47
    private ManagerRegistry $registry;
48
    private RequestStack $requestStack;
49
    private ValidatorInterface $validator;
50
51
    public function __construct(PublishableStatusChecker $publishableStatusChecker, ManagerRegistry $registry, RequestStack $requestStack, ValidatorInterface $validator)
52
    {
53
        $this->publishableStatusChecker = $publishableStatusChecker;
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->publishableStatusChecker->isActivePublishedAt($object);
63
64
        // display soft validation violations in the response
65
        if ($this->publishableStatusChecker->isGranted($object)) {
66
            try {
67
                $this->validator->validate($object, [PublishableValidator::PUBLISHED_KEY => true]);
68
            } catch (ValidationException $exception) {
69
                $context[MetadataNormalizer::METADATA_CONTEXT]['violation_list'] = $this->normalizer->normalize($exception->getConstraintViolationList(), $format);
70
            }
71
        }
72
73
        return $this->normalizer->normalize($object, $format, $context);
74
    }
75
76
    public function supportsNormalization($data, $format = null, $context = []): bool
77
    {
78
        return !isset($context[self::ALREADY_CALLED]) &&
79
            \is_object($data) &&
80
            !$data instanceof \Traversable &&
81
            $this->publishableStatusChecker->getAnnotationReader()->isConfigured($data);
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function denormalize($data, $type, $format = null, array $context = [])
88
    {
89
        $context[self::ALREADY_CALLED] = true;
90
        $configuration = $this->publishableStatusChecker->getAnnotationReader()->getConfiguration($type);
91
92
        $data = $this->unsetRestrictedData($type, $data, $configuration);
93
94
        $request = $this->requestStack->getMasterRequest();
95
        if ($request && true === $this->publishableStatusChecker->isPublishedRequest($request)) {
96
            return $this->denormalizer->denormalize($data, $type, $format, $context);
97
        }
98
99
        // It's a new object
100
        if (!isset($context[AbstractNormalizer::OBJECT_TO_POPULATE])) {
101
            // User doesn't have draft access: force publication date
102
            if (!$this->publishableStatusChecker->isGranted($type)) {
103
                $data[$configuration->fieldName] = date('Y-m-d H:i:s');
104
            }
105
106
            return $this->denormalizer->denormalize($data, $type, $format, $context);
107
        }
108
109
        $object = $context[AbstractNormalizer::OBJECT_TO_POPULATE];
110
        $data = $this->setPublishedAt($data, $configuration, $object);
111
112
        // No field has been updated (after publishedAt verified and cleaned/unset if needed): nothing to do here anymore
113
        // or User doesn't have draft access: update the original object
114
        if (
115
            empty($data) ||
116
            !$this->publishableStatusChecker->isActivePublishedAt($object) ||
117
            !$this->publishableStatusChecker->isGranted($type)
118
        ) {
119
            return $this->denormalizer->denormalize($data, $type, $format, $context);
120
        }
121
122
        // Any field has been modified: create a draft
123
        $draft = $this->createDraft($object, $configuration, $type);
124
125
        $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $draft;
126
127
        return $this->denormalizer->denormalize($data, $type, $format, $context);
128
    }
129
130
    private function setPublishedAt(array $data, Publishable $configuration, object $object): array
131
    {
132
        if (isset($data[$configuration->fieldName])) {
133
            $publicationDate = new \DateTimeImmutable($data[$configuration->fieldName]);
134
135
            // User changed the publication date with an earlier one on a published resource: ignore it
136
            if (
137
                $this->publishableStatusChecker->isActivePublishedAt($object) &&
138
                new \DateTimeImmutable() >= $publicationDate
139
            ) {
140
                unset($data[$configuration->fieldName]);
141
            }
142
        }
143
144
        return $data;
145
    }
146
147
    private function unsetRestrictedData($type, array $data, Publishable $configuration): array
148
    {
149
        // It's not possible to change the publishedResource and draftResource properties
150
        unset($data[$configuration->associationName], $data[$configuration->reverseAssociationName]);
151
152
        // User doesn't have draft access: cannot set or change the publication date
153
        if (!$this->publishableStatusChecker->isGranted($type)) {
154
            unset($data[$configuration->fieldName]);
155
        }
156
157
        return $data;
158
    }
159
160
    private function createDraft(object $object, Publishable $configuration, string $type): object
161
    {
162
        $em = $this->registry->getManagerForClass($type);
163
        if (!$em) {
164
            throw new InvalidArgumentException(sprintf('Could not find entity manager for class %s', $type));
165
        }
166
167
        /** @var ClassMetadataInfo $classMetadata */
168
        $classMetadata = $em->getClassMetadata($type);
169
170
        // Resource is a draft: nothing to do here anymore
171
        if (null !== $classMetadata->getFieldValue($object, $configuration->associationName)) {
172
            return $object;
173
        }
174
175
        $draft = clone $object; // Identifier(s) should be reset from AbstractComponent::__clone method
176
177
        // Empty publishedDate on draft
178
        $classMetadata->setFieldValue($draft, $configuration->fieldName, null);
179
180
        // Set publishedResource on draft
181
        $classMetadata->setFieldValue($draft, $configuration->associationName, $object);
182
183
        // Set draftResource on data
184
        $classMetadata->setFieldValue($object, $configuration->reverseAssociationName, $draft);
185
186
        // Add draft object to UnitOfWork
187
        $em->persist($draft);
188
189
        return $draft;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
196
    {
197
        return !isset($context[self::ALREADY_CALLED]) && $this->publishableStatusChecker->getAnnotationReader()->isConfigured($type);
198
    }
199
200
    public function hasCacheableSupportsMethod(): bool
201
    {
202
        return false;
203
    }
204
}
205