Passed
Push — feature/publishable ( 878fc4...f86055 )
by Daniel
05:21
created

PublishableEventListener::onPostRespond()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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