Passed
Pull Request — feature/publishable (#53)
by Vincent
08:00 queued 57s
created

mergeDraftIntoPublished()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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