Passed
Push — feature/publishable ( c26268...7bd202 )
by Daniel
18:12 queued 11:31
created

PublishableNormalizer::createDraft()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 30
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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