Passed
Pull Request — feature/publishable (#53)
by Daniel
31:07
created

PublishableEventListener::onPostDeserialize()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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