PublishableEventListener   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 212
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 117
c 1
b 0
f 0
dl 0
loc 212
ccs 0
cts 108
cp 0
rs 9.0399
wmc 42

8 Methods

Rating   Name   Duplication   Size   Complexity  
B checkMergeDraftIntoPublished() 0 47 6
A getValue() 0 3 1
A onPostRead() 0 14 5
A onPreWrite() 0 15 5
A mergeDraftIntoPublished() 0 25 6
B onPostDeserialize() 0 20 7
A __construct() 0 6 1
B onPostRespond() 0 53 11

How to fix   Complexity   

Complex Class

Complex classes like PublishableEventListener often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PublishableEventListener, and based on these observations, apply Extract Interface, too.

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\EventListener\Api;
15
16
use ApiPlatform\Metadata\CollectionOperationInterface;
17
use ApiPlatform\Symfony\Validator\Exception\ValidationException;
18
use ApiPlatform\Validator\ValidatorInterface;
19
use Doctrine\Persistence\ManagerRegistry;
20
use Silverback\ApiComponentsBundle\AttributeReader\PublishableAttributeReader;
21
use Silverback\ApiComponentsBundle\Entity\Utility\PublishableTrait;
22
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
23
use Silverback\ApiComponentsBundle\Utility\ClassMetadataTrait;
24
use Silverback\ApiComponentsBundle\Validator\PublishableValidator;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpKernel\Event\RequestEvent;
27
use Symfony\Component\HttpKernel\Event\ResponseEvent;
28
use Symfony\Component\HttpKernel\Event\ViewEvent;
29
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
30
31
/**
32
 * @author Vincent Chalamon <[email protected]>
33
 */
34
final class PublishableEventListener
35
{
36
    use ApiEventListenerTrait;
0 ignored issues
show
Bug introduced by
The trait Silverback\ApiComponents...i\ApiEventListenerTrait requires the property $attributes which is not provided by Silverback\ApiComponents...ublishableEventListener.
Loading history...
37
    use ClassMetadataTrait;
38
39
    public const VALID_TO_PUBLISH_HEADER = 'valid-to-publish';
40
    public const VALID_PUBLISHED_QUERY = 'validate_published';
41
42
    private PublishableStatusChecker $publishableStatusChecker;
43
    private ValidatorInterface $validator;
44
    private PublishableAttributeReader $publishableAnnotationReader;
45
46
    public function __construct(PublishableStatusChecker $publishableStatusChecker, ManagerRegistry $registry, ValidatorInterface $validator)
47
    {
48
        $this->publishableAnnotationReader = $publishableStatusChecker->getAttributeReader();
49
        $this->publishableStatusChecker = $publishableStatusChecker;
50
        $this->initRegistry($registry);
51
        $this->validator = $validator;
52
    }
53
54
    public function onPreWrite(ViewEvent $event): void
55
    {
56
        $request = $event->getRequest();
57
        $attributes = $this->getAttributes($request);
58
        if (
59
            empty($attributes['data'])
60
            || !$this->publishableAnnotationReader->isConfigured($attributes['class'])
61
            || $request->isMethod(Request::METHOD_DELETE)
62
            || $attributes['operation'] instanceof CollectionOperationInterface
63
        ) {
64
            return;
65
        }
66
67
        $publishable = $this->checkMergeDraftIntoPublished($request, $attributes['data']);
68
        $event->setControllerResult($publishable);
69
    }
70
71
    public function onPostRead(RequestEvent $event): void
72
    {
73
        $request = $event->getRequest();
74
        $attributes = $this->getAttributes($request);
75
        if (
76
            empty($attributes['data'])
77
            || !$this->publishableAnnotationReader->isConfigured($attributes['class'])
78
            || !$request->isMethod(Request::METHOD_GET)
79
            || $attributes['operation'] instanceof CollectionOperationInterface
80
        ) {
81
            return;
82
        }
83
84
        $this->checkMergeDraftIntoPublished($request, $attributes['data'], true);
85
    }
86
87
    public function onPostDeserialize(RequestEvent $event): void
88
    {
89
        $request = $event->getRequest();
90
        $attributes = $this->getAttributes($request);
91
        if (
92
            empty($attributes['data'])
93
            || !$this->publishableAnnotationReader->isConfigured($attributes['class'])
94
            || !($request->isMethod(Request::METHOD_PUT) || $request->isMethod(Request::METHOD_PATCH))
95
        ) {
96
            return;
97
        }
98
99
        $configuration = $this->publishableAnnotationReader->getConfiguration($attributes['class']);
100
101
        // User cannot change the publication date of the original resource
102
        if (
103
            true === $this->publishableStatusChecker->isRequestForPublished($request)
104
            && $this->getValue($request->attributes->get('previous_data'), $configuration->fieldName) !== $this->getValue($attributes['data'], $configuration->fieldName)
105
        ) {
106
            throw new UnprocessableEntityHttpException('You cannot change the publication date of a published resource.');
107
        }
108
    }
109
110
    public function onPostRespond(ResponseEvent $event): void
111
    {
112
        $request = $event->getRequest();
113
114
        $attributes = $this->getAttributes($request);
115
116
        /**
117
         * @var PublishableTrait|null $data
118
         */
119
        $data = $attributes['data'];
120
121
        if (
122
            null === $data
123
            || !$this->publishableAnnotationReader->isConfigured($attributes['class'])
124
            || $attributes['operation'] instanceof CollectionOperationInterface
125
        ) {
126
            return;
127
        }
128
        $response = $event->getResponse();
129
130
        $configuration = $this->publishableAnnotationReader->getConfiguration($attributes['class']);
131
        $classMetadata = $this->getClassMetadata($attributes['class']);
132
        $draftResource = $classMetadata->getFieldValue($data, $configuration->reverseAssociationName) ?? $data;
133
134
        // Add Expires HTTP header
135
        /** @var \DateTime|null $publishedAt */
136
        $publishedAt = $classMetadata->getFieldValue($draftResource, $configuration->fieldName);
137
        if ($publishedAt && $publishedAt > new \DateTime()) {
138
            $response->setExpires($publishedAt);
139
        }
140
141
        if (!$this->publishableStatusChecker->isGranted($attributes['class'])) {
142
            return;
143
        }
144
145
        if ($response->isClientError()) {
146
            $response->headers->set(self::VALID_TO_PUBLISH_HEADER, '0');
147
148
            return;
149
        }
150
151
        // Force validation from querystring, and/or add validate-to-publish custom HTTP header
152
        try {
153
            $this->validator->validate($data, [PublishableValidator::PUBLISHED_KEY => true]);
154
            $response->headers->set(self::VALID_TO_PUBLISH_HEADER, '1');
155
        } catch (ValidationException $exception) {
156
            $response->headers->set(self::VALID_TO_PUBLISH_HEADER, '0');
157
158
            if (
159
                true === $request->query->getBoolean(self::VALID_PUBLISHED_QUERY, false)
160
                && \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT], true)
161
            ) {
162
                throw $exception;
163
            }
164
        }
165
    }
166
167
    private function getValue(object $object, string $property)
168
    {
169
        return $this->getClassMetadata($object)->getFieldValue($object, $property);
170
    }
171
172
    private function checkMergeDraftIntoPublished(Request $request, object $data, bool $flushDatabase = false): object
173
    {
174
        if (!$this->publishableStatusChecker->isActivePublishedAt($data)) {
175
            return $data;
176
        }
177
178
        $configuration = $this->publishableAnnotationReader->getConfiguration($data);
179
        $classMetadata = $this->getClassMetadata($data);
180
181
        $publishedResourceAssociation = $classMetadata->getFieldValue($data, $configuration->associationName);
182
        $draftResourceAssociation = $classMetadata->getFieldValue($data, $configuration->reverseAssociationName);
183
        if (
184
            !$publishedResourceAssociation
185
            && (!$draftResourceAssociation || !$this->publishableStatusChecker->isActivePublishedAt($draftResourceAssociation))
186
        ) {
187
            return $data;
188
        }
189
190
        // the request is for a resource with an active publish date
191
        // either a draft, if so it may be a published version we need to replace with
192
        // or a published resource which may have a draft that has an active publish date
193
        $entityManager = $this->getEntityManager($data);
194
195
        $meta = $entityManager->getClassMetadata($data::class);
196
        $identifierFieldName = $meta->getSingleIdentifierFieldName();
197
198
        if ($publishedResourceAssociation) {
199
            // retrieving a draft that is now published
200
            $draftResource = $data;
201
            $publishedResource = $publishedResourceAssociation;
202
203
            $publishedId = $classMetadata->getFieldValue($publishedResource, $identifierFieldName);
204
            $request->attributes->set('id', $publishedId);
205
            $request->attributes->set('data', $publishedResource);
206
            $request->attributes->set('previous_data', clone $publishedResource);
207
        } else {
208
            // retrieving a published resource and draft should now replace it
209
            $publishedResource = $data;
210
            $draftResource = $draftResourceAssociation;
211
        }
212
213
        $classMetadata->setFieldValue($publishedResource, $configuration->reverseAssociationName, null);
214
        $classMetadata->setFieldValue($draftResource, $configuration->associationName, null);
215
216
        $this->mergeDraftIntoPublished($identifierFieldName, $draftResource, $publishedResource, $flushDatabase);
217
218
        return $publishedResource;
219
    }
220
221
    private function mergeDraftIntoPublished(string $identifierFieldName, object $draftResource, object $publishedResource, bool $flushDatabase): void
222
    {
223
        $draftReflection = new \ReflectionClass($draftResource);
224
        $publishedReflection = new \ReflectionClass($publishedResource);
225
        $properties = $publishedReflection->getProperties();
226
227
        foreach ($properties as $property) {
228
            $property->setAccessible(true);
229
            $name = $property->getName();
230
            if ($identifierFieldName === $name) {
231
                continue;
232
            }
233
            $draftProperty = $draftReflection->hasProperty($name) ? $draftReflection->getProperty($name) : null;
234
            if ($draftProperty) {
235
                $draftProperty->setAccessible(true);
236
                $draftValue = $draftProperty->getValue($draftResource);
237
                $property->setValue($publishedResource, $draftValue);
238
            }
239
        }
240
241
        $entityManager = $this->getEntityManager($draftResource);
242
        $entityManager->remove($draftResource);
243
244
        if ($flushDatabase) {
245
            $entityManager->flush();
246
        }
247
    }
248
}
249