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

PublishableEventListener::onPostRespond()   B

Complexity

Conditions 10
Paths 15

Size

Total Lines 46
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 27
nc 15
nop 1
dl 0
loc 46
ccs 0
cts 25
cp 0
crap 110
rs 7.6666
c 0
b 0
f 0

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