Passed
Pull Request — master (#50)
by Daniel
07:30
created

PublishableNormalizer::denormalize()   B

Complexity

Conditions 10
Paths 7

Size

Total Lines 56
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 27
c 1
b 0
f 0
dl 0
loc 56
ccs 0
cts 27
cp 0
rs 7.6666
cc 10
nc 7
nop 4
crap 110

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Publishable\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->isPublishable($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
        // No field has been updated: nothing to do here anymore
96
        if (empty($data)) {
97
            return $this->denormalizer->denormalize($data, $type, $format, $context);
98
        }
99
100
        $object = $context[AbstractNormalizer::OBJECT_TO_POPULATE];
101
        $data = $this->setPublishedAt($data, $configuration, $object);
102
103
        $em = $this->registry->getManagerForClass($type);
104
        if (!$em) {
105
            throw new InvalidArgumentException(sprintf('Could not find entity manager for class %s', $type));
106
        }
107
        /** @var ClassMetadataInfo $classMetadata */
108
        $classMetadata = $em->getClassMetadata($type);
109
110
        // Resource is a draft: nothing to do here anymore
111
        // or User doesn't have draft access: update the original object
112
        if (
113
            null !== $classMetadata->getFieldValue($object, $configuration->associationName) ||
114
            !$this->publishableHelper->isActivePublishedAt($object) ||
115
            !$this->publishableHelper->isGranted()
116
        ) {
117
            return $this->denormalizer->denormalize($data, $type, $format, $context);
118
        }
119
120
        // Any field has been modified: create a draft
121
        $draft = $this->createDraft($object, $configuration, $classMetadata);
122
123
        // Add draft object to UnitOfWork
124
        $em->persist($draft);
125
126
        $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $draft;
127
128
        return $this->denormalizer->denormalize($data, $type, $format, $context);
129
    }
130
131
    private function setPublishedAt(array $data, Publishable $configuration, object $object): array
132
    {
133
        if (isset($data[$configuration->fieldName])) {
134
            $publicationDate = new \DateTimeImmutable($data[$configuration->fieldName]);
135
136
            // User changed the publication date with an earlier one on a published resource: ignore it
137
            if (
138
                $this->publishableHelper->isActivePublishedAt($object) &&
139
                new \DateTimeImmutable() >= $publicationDate
140
            ) {
141
                unset($data[$configuration->fieldName]);
142
            }
143
        }
144
145
        return $data;
146
    }
147
148
    private function unsetRestrictedData(array $data, Publishable $configuration): array
149
    {
150
        // It's not possible to change the publishedResource and draftResource properties
151
        unset($data[$configuration->associationName], $data[$configuration->reverseAssociationName]);
152
153
        // User doesn't have draft access: cannot set or change the publication date
154
        if (!$this->publishableHelper->isGranted()) {
155
            unset($data[$configuration->fieldName]);
156
        }
157
158
        return $data;
159
    }
160
161
    private function createDraft(object $object, Publishable $configuration, ClassMetadataInfo $classMetadata)
162
    {
163
        $draft = clone $object; // Identifier(s) should be reset from AbstractComponent::__clone method
164
165
        // Empty publishedDate on draft
166
        $classMetadata->setFieldValue($draft, $configuration->fieldName, null);
167
168
        // Set publishedResource on draft
169
        $classMetadata->setFieldValue($draft, $configuration->associationName, $object);
170
171
        // Set draftResource on data
172
        $classMetadata->setFieldValue($object, $configuration->reverseAssociationName, $draft);
173
174
        return $draft;
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
181
    {
182
        return !isset($context[self::ALREADY_CALLED]) && $this->publishableHelper->isPublishable($type);
183
    }
184
185
    public function hasCacheableSupportsMethod(): bool
186
    {
187
        return false;
188
    }
189
}
190