Passed
Pull Request — feature/publishable (#44)
by Daniel
05:32
created

PublishableEventListener::onPostRead()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 13
ccs 0
cts 8
cp 0
rs 10
cc 4
nc 2
nop 1
crap 20
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\Persistence\ManagerRegistry;
17
use Silverback\ApiComponentBundle\Entity\Utility\PublishableTrait;
18
use Silverback\ApiComponentBundle\Publishable\ClassMetadataTrait;
19
use Silverback\ApiComponentBundle\Publishable\PublishableHelper;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpKernel\Event\RequestEvent;
22
use Symfony\Component\HttpKernel\Event\ResponseEvent;
23
use Symfony\Component\HttpKernel\Event\ViewEvent;
24
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
25
26
/**
27
 * @author Vincent Chalamon <[email protected]>
28
 */
29
final class PublishableEventListener
30
{
31
    use ClassMetadataTrait;
32
33
    private PublishableHelper $publishableHelper;
34
    private ManagerRegistry $registry;
35
36
    public function __construct(PublishableHelper $publishableHelper, ManagerRegistry $registry)
37
    {
38
        $this->publishableHelper = $publishableHelper;
39
        // not unused, used by the trait
40
        $this->registry = $registry;
41
    }
42
43
    public function onPreWrite(ViewEvent $event): void
44
    {
45
        $request = $event->getRequest();
46
        $data = $request->attributes->get('data');
47
        if (
48
            empty($data) ||
49
            !$this->publishableHelper->isPublishable($data) ||
50
            $request->isMethod(Request::METHOD_DELETE)
51
        ) {
52
            return;
53
        }
54
55
        $publishable = $this->checkMergeDraftIntoPublished($request, $data);
56
        $event->setControllerResult($publishable);
57
    }
58
59
    public function onPostRead(RequestEvent $event): void
60
    {
61
        $request = $event->getRequest();
62
        $data = $request->attributes->get('data');
63
        if (
64
            empty($data) ||
65
            !$this->publishableHelper->isPublishable($data) ||
66
            !$request->isMethod(Request::METHOD_GET)
67
        ) {
68
            return;
69
        }
70
71
        $this->checkMergeDraftIntoPublished($request, $data, true);
72
    }
73
74
    public function onPostDeserialize(RequestEvent $event): void
75
    {
76
        $request = $event->getRequest();
77
        $data = $request->attributes->get('data');
78
        if (
79
            empty($data) ||
80
            !$this->publishableHelper->isPublishable($data) ||
81
            !($request->isMethod(Request::METHOD_PUT) || $request->isMethod(Request::METHOD_PATCH))
82
        ) {
83
            return;
84
        }
85
86
        $configuration = $this->publishableHelper->getConfiguration($data);
87
88
        // User cannot change the publication date of the original resource
89
        if (
90
            true === $request->query->getBoolean('published', false) &&
91
            $this->getValue($request->attributes->get('previous_data'), $configuration->fieldName) !== $this->getValue($data, $configuration->fieldName)
92
        ) {
93
            throw new BadRequestHttpException('You cannot change the publication date of a published resource.');
94
        }
95
    }
96
97
    public function onPostRespond(ResponseEvent $event): void
98
    {
99
        $request = $event->getRequest();
100
        /** @var PublishableTrait $data */
101
        $data = $request->attributes->get('data');
102
        if (
103
            empty($data) ||
104
            !$this->publishableHelper->isPublishable($data)
105
        ) {
106
            return;
107
        }
108
        $response = $event->getResponse();
109
110
        $configuration = $this->publishableHelper->getConfiguration($data);
111
        $classMetadata = $this->getClassMetadata($data);
112
113
        $draftResource = $classMetadata->getFieldValue($data, $configuration->reverseAssociationName) ?? $data;
114
115
        /** @var \DateTime|null $publishedAt */
116
        $publishedAt = $classMetadata->getFieldValue($draftResource, $configuration->fieldName);
117
        if (!$publishedAt || $publishedAt <= new \DateTime()) {
118
            return;
119
        }
120
121
        $response->setExpires($publishedAt);
122
    }
123
124
    private function getValue(object $object, string $property)
125
    {
126
        return $this->getClassMetadata($object)->getFieldValue($object, $property);
127
    }
128
129
    private function checkMergeDraftIntoPublished(Request $request, object $data, bool $flushDatabase = false): object
130
    {
131
        if (!$this->publishableHelper->isActivePublishedAt($data)) {
132
            return $data;
133
        }
134
135
        $configuration = $this->publishableHelper->getConfiguration($data);
136
        $classMetadata = $this->getClassMetadata($data);
137
138
        $publishedResourceAssociation = $classMetadata->getFieldValue($data, $configuration->associationName);
139
        $draftResourceAssociation = $classMetadata->getFieldValue($data, $configuration->reverseAssociationName);
140
        if (
141
            !$publishedResourceAssociation &&
142
            (!$draftResourceAssociation || !$this->publishableHelper->isActivePublishedAt($draftResourceAssociation))
143
        ) {
144
            return $data;
145
        }
146
147
        // the request is for a resource with an active publish date
148
        // either a draft, if so it may be a published version we need to replace with
149
        // or a published resource which may have a draft that has an active publish date
150
        $entityManager = $this->getEntityManager($data);
151
        $meta = $entityManager->getClassMetadata(\get_class($data));
152
        $identifierFieldName = $meta->getSingleIdentifierFieldName();
0 ignored issues
show
Bug introduced by
The method getSingleIdentifierFieldName() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
        /** @scrutinizer ignore-call */ 
153
        $identifierFieldName = $meta->getSingleIdentifierFieldName();
Loading history...
153
154
        if ($publishedResourceAssociation) {
155
            // retrieving a draft that is now published
156
            $draftResource = $data;
157
            $publishedResource = $publishedResourceAssociation;
158
159
            $publishedId = $classMetadata->getFieldValue($publishedResource, $identifierFieldName);
160
            $request->attributes->set('id', $publishedId);
161
            $request->attributes->set('data', $publishedResource);
162
            $request->attributes->set('previous_data', clone $publishedResource);
163
        } else {
164
            // retrieving a published resource and draft should now replace it
165
            $publishedResource = $data;
166
            $draftResource = $draftResourceAssociation;
167
        }
168
169
        $classMetadata->setFieldValue($publishedResource, $configuration->reverseAssociationName, null);
170
        $classMetadata->setFieldValue($draftResource, $configuration->associationName, null);
171
172
        $this->mergeDraftIntoPublished($identifierFieldName, $draftResource, $publishedResource, $flushDatabase);
173
174
        return $publishedResource;
175
    }
176
177
    private function mergeDraftIntoPublished(string $identifierFieldName, object $draftResource, object $publishedResource, bool $flushDatabase): void
178
    {
179
        $draftReflection = new \ReflectionClass($draftResource);
180
        $publishedReflection = new \ReflectionClass($publishedResource);
181
        $properties = $publishedReflection->getProperties();
182
183
        foreach ($properties as $property) {
184
            $property->setAccessible(true);
185
            $name = $property->getName();
186
            if ($identifierFieldName === $name) {
187
                continue;
188
            }
189
            $draftProperty = $draftReflection->hasProperty($name) ? $draftReflection->getProperty($name) : null;
190
            if ($draftProperty) {
191
                $draftProperty->setAccessible(true);
192
                $draftValue = $draftProperty->getValue($draftResource);
193
                $property->setValue($publishedResource, $draftValue);
194
            }
195
        }
196
197
        $entityManager = $this->getEntityManager($draftResource);
198
        $entityManager->remove($draftResource);
199
        if ($flushDatabase) {
200
            $entityManager->flush();
201
        }
202
    }
203
}
204