Passed
Pull Request — feature/publishable (#53)
by Vincent
06:15
created

PublishableEventListener::getValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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