Passed
Pull Request — feature/publishable (#43)
by Daniel
05:46
created

PublishableEventListener::onPreWrite()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 14
ccs 0
cts 9
cp 0
rs 9.9666
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
    private function checkMergeDraftIntoPublished(Request $request, object $data, bool $flushDatabase = false): object
44
    {
45
        $configuration = $this->publishableHelper->getConfiguration($data);
46
        $classMetadata = $this->getClassMetadata($data);
47
48
        $publishedResourceAssociation = $classMetadata->getFieldValue($data, $configuration->associationName);
49
        $draftResourceAssociation = $classMetadata->getFieldValue($data, $configuration->reverseAssociationName);
50
51
        // the request is for a resource with an active publish date
52
        // either a draft, if so it may be a published version we need to replace with
53
        // or a published resource which may have a draft that has an active publish date
54
        if (
55
            $this->publishableHelper->isActivePublishedAt($data) &&
56
            (
57
                $publishedResourceAssociation ||
58
                ($draftResourceAssociation && $this->publishableHelper->isActivePublishedAt($draftResourceAssociation))
59
            )
60
        ) {
61
            $entityManager = $this->getEntityManager($data);
62
            $meta = $entityManager->getClassMetadata(\get_class($data));
63
            $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

63
            /** @scrutinizer ignore-call */ 
64
            $identifierFieldName = $meta->getSingleIdentifierFieldName();
Loading history...
64
65
            if ($publishedResourceAssociation) {
66
                // retrieving a draft that is now published
67
                $draftResource = $data;
68
                $publishedResource = $publishedResourceAssociation;
69
70
                $publishedId = $classMetadata->getFieldValue($publishedResource, $identifierFieldName);
71
                $request->attributes->set('id', $publishedId);
72
                $request->attributes->set('data', $publishedResource);
73
                $request->attributes->set('previous_data', clone $publishedResource);
74
            } else {
75
                // retrieving a published resource and draft should now replace it
76
                $publishedResource = $data;
77
                $draftResource = $draftResourceAssociation;
78
            }
79
            $classMetadata->setFieldValue($publishedResource, $configuration->reverseAssociationName, null);
80
            $classMetadata->setFieldValue($draftResource, $configuration->associationName, null);
81
82
            $this->mergeDraftIntoPublished($identifierFieldName, $draftResource, $publishedResource, $flushDatabase);
83
84
            return $publishedResource;
85
        }
86
87
        return $data;
88
    }
89
90
    private function mergeDraftIntoPublished(string $identifierFieldName, object $draftResource, object $publishedResource, bool $flushDatabase): void
91
    {
92
        $draftReflection = new \ReflectionClass($draftResource);
93
        $publishedReflection = new \ReflectionClass($publishedResource);
94
        $properties = $publishedReflection->getProperties();
95
96
        foreach ($properties as $property) {
97
            $property->setAccessible(true);
98
            $name = $property->getName();
99
            if ($identifierFieldName === $name) {
100
                continue;
101
            }
102
            $draftProperty = $draftReflection->hasProperty($name) ? $draftReflection->getProperty($name) : null;
103
            if ($draftProperty) {
104
                $draftProperty->setAccessible(true);
105
                $draftValue = $draftProperty->getValue($draftResource);
106
                $property->setValue($publishedResource, $draftValue);
107
            }
108
        }
109
110
        $entityManager = $this->getEntityManager($draftResource);
111
        $entityManager->remove($draftResource);
112
        if ($flushDatabase) {
113
            $entityManager->flush();
114
        }
115
    }
116
117
    public function onPreWrite(ViewEvent $event): void
118
    {
119
        $request = $event->getRequest();
120
        $data = $request->attributes->get('data');
121
        if (
122
            empty($data) ||
123
            !$this->publishableHelper->isPublishable($data) ||
124
            $request->isMethod(Request::METHOD_DELETE)
125
        ) {
126
            return;
127
        }
128
129
        $publishable = $this->checkMergeDraftIntoPublished($request, $data);
130
        $event->setControllerResult($publishable);
131
    }
132
133
    public function onPostRead(RequestEvent $event): void
134
    {
135
        $request = $event->getRequest();
136
        $data = $request->attributes->get('data');
137
        if (
138
            empty($data) ||
139
            !$this->publishableHelper->isPublishable($data) ||
140
            !$request->isMethod(Request::METHOD_GET)
141
        ) {
142
            return;
143
        }
144
145
        $this->checkMergeDraftIntoPublished($request, $data, true);
146
    }
147
148
    public function onPostDeserialize(RequestEvent $event): void
149
    {
150
        $request = $event->getRequest();
151
        $data = $request->attributes->get('data');
152
        if (
153
            empty($data) ||
154
            !$this->publishableHelper->isPublishable($data) ||
155
            !($request->isMethod(Request::METHOD_PUT) || $request->isMethod(Request::METHOD_PATCH))
156
        ) {
157
            return;
158
        }
159
160
        $configuration = $this->publishableHelper->getConfiguration($data);
161
162
        // User cannot change the publication date of the original resource
163
        if (
164
            true === $request->query->getBoolean('published', false) &&
165
            $this->getValue($request->attributes->get('previous_data'), $configuration->fieldName) !== $this->getValue($data, $configuration->fieldName)
166
        ) {
167
            throw new BadRequestHttpException('You cannot change the publication date of a published resource.');
168
        }
169
    }
170
171
    public function onPostRespond(ResponseEvent $event): void
172
    {
173
        $request = $event->getRequest();
174
        /** @var PublishableTrait $data */
175
        $data = $request->attributes->get('data');
176
        if (
177
            empty($data) ||
178
            !$this->publishableHelper->isPublishable($data)
179
        ) {
180
            return;
181
        }
182
        $response = $event->getResponse();
183
184
        $configuration = $this->publishableHelper->getConfiguration($data);
185
        $classMetadata = $this->getClassMetadata($data);
186
187
        $draftResource = $classMetadata->getFieldValue($data, $configuration->reverseAssociationName) ?? $data;
188
189
        /** @var \DateTime|null $publishedAt */
190
        $publishedAt = $classMetadata->getFieldValue($draftResource, $configuration->fieldName);
191
        if (!$publishedAt || $publishedAt <= new \DateTime()) {
192
            return;
193
        }
194
195
        $response->setExpires($publishedAt);
196
    }
197
198
    private function getValue(object $object, string $property)
199
    {
200
        return $this->getClassMetadata($object)->getFieldValue($object, $property);
201
    }
202
}
203