Completed
Pull Request — develop (#147)
by Wachter
14:07
created

RoutableSubscriber::handleHydrate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 6
cts 7
cp 0.8571
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
crap 3.0261
1
<?php
2
3
/*
4
 * This file is part of Sulu.
5
 *
6
 * (c) MASSIVE ART WebServices GmbH
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Sulu\Bundle\ArticleBundle\Document\Subscriber;
13
14
use Doctrine\ORM\EntityManagerInterface;
15
use PHPCR\ItemNotFoundException;
16
use PHPCR\SessionInterface;
17
use Sulu\Bundle\ArticleBundle\Document\Behavior\RoutableBehavior;
18
use Sulu\Bundle\ArticleBundle\Document\Behavior\RoutablePageBehavior;
19
use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector;
20
use Sulu\Bundle\RouteBundle\Entity\RouteRepositoryInterface;
21
use Sulu\Bundle\RouteBundle\Generator\ChainRouteGeneratorInterface;
22
use Sulu\Bundle\RouteBundle\Manager\RouteManagerInterface;
23
use Sulu\Bundle\RouteBundle\Model\RouteInterface;
24
use Sulu\Component\Content\Metadata\Factory\StructureMetadataFactoryInterface;
25
use Sulu\Component\DocumentManager\Behavior\Mapping\ChildrenBehavior;
26
use Sulu\Component\DocumentManager\Event\AbstractMappingEvent;
27
use Sulu\Component\DocumentManager\Event\ConfigureOptionsEvent;
28
use Sulu\Component\DocumentManager\Event\PublishEvent;
29
use Sulu\Component\DocumentManager\Event\RemoveEvent;
30
use Sulu\Component\DocumentManager\Events;
31
use Sulu\Component\DocumentManager\PropertyEncoder;
32
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
33
34
/**
35
 * Handles document-manager events to create/update/remove routes.
36
 */
37
class RoutableSubscriber implements EventSubscriberInterface
38
{
39
    const ROUTE_FIELD = 'routePath';
40
    const ROUTES_PROPERTY = 'suluRoutes';
41
    const TAG_NAME = 'sulu_article.article_route';
42
43
    /**
44
     * @var ChainRouteGeneratorInterface
45
     */
46
    private $chainRouteGenerator;
47
48
    /**
49
     * @var RouteManagerInterface
50 35
     */
51
    private $routeManager;
52 35
53 35
    /**
54 35
     * @var RouteRepositoryInterface
55 35
     */
56
    private $routeRepository;
57
58
    /**
59
     * @var EntityManagerInterface
60 31
     */
61
    private $entityManager;
62
63 31
    /**
64 31
     * @var PropertyEncoder
65 31
     */
66 31
    private $propertyEncoder;
67 31
68
    /**
69
     * @var StructureMetadataFactoryInterface
70
     */
71
    private $metadataFactory;
72
73
    /**
74
     * @var DocumentInspector
75
     */
76 31
    private $documentInspector;
77
78 31
    /**
79 31
     * @param ChainRouteGeneratorInterface $chainRouteGenerator
80 29
     * @param RouteManagerInterface $routeManager
81
     * @param RouteRepositoryInterface $routeRepository
82
     * @param EntityManagerInterface $entityManager
83 31
     * @param PropertyEncoder $propertyEncoder
84 31
     * @param StructureMetadataFactoryInterface $metadataFactory
85 30
     * @param DocumentInspector $documentInspector
86
     */
87
    public function __construct(
88 31
        ChainRouteGeneratorInterface $chainRouteGenerator,
89 31
        RouteManagerInterface $routeManager,
90
        RouteRepositoryInterface $routeRepository,
91
        EntityManagerInterface $entityManager,
92
        PropertyEncoder $propertyEncoder,
93
        StructureMetadataFactoryInterface $metadataFactory,
94
        DocumentInspector $documentInspector
95
    ) {
96 32
        $this->chainRouteGenerator = $chainRouteGenerator;
97
        $this->routeManager = $routeManager;
98 32
        $this->routeRepository = $routeRepository;
99 32
        $this->entityManager = $entityManager;
100 9
        $this->propertyEncoder = $propertyEncoder;
101
        $this->metadataFactory = $metadataFactory;
102
        $this->documentInspector = $documentInspector;
103 32
    }
104
105 32
    /**
106 32
     * {@inheritdoc}
107 32
     */
108 32 View Code Duplication
    public static function getSubscribedEvents()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
109
    {
110
        return [
111
            Events::HYDRATE => ['handleHydrate'],
112
            Events::PERSIST => [
113
                // low priority because all other subscriber should be finished
114
                ['handlePersist', -2000],
115 31
            ],
116
            Events::REMOVE => [
117 31
                // high priority to ensure nodes are not deleted until we iterate over children
118 31
                ['handleRemove', 1024],
119 31
            ],
120 31
            Events::PUBLISH => ['handlePublish', -2000],
121
            Events::CONFIGURE_OPTIONS => 'configureOptions',
122 30
        ];
123
    }
124
125 1
    /**
126 1
     * Load route.
127 1
     *
128 1
     * @param AbstractMappingEvent $event
129 1
     */
130
    public function handleHydrate(AbstractMappingEvent $event)
131
    {
132
        $document = $event->getDocument();
133
        if (!$document instanceof RoutablePageBehavior) {
134
            return;
135
        }
136 3
137
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
138 3
        $routePath = $event->getNode()->getPropertyValueWithDefault($propertyName, null);
139 3
        $document->setRoutePath($routePath);
140
141
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $event->getLocale());
142
        if ($route) {
143 3
            $document->setRoute($route);
144 3
        }
145 2
    }
146
147
    /**
148 1
     * Generate route and save route-path.
149 1
     *
150 1
     * @param AbstractMappingEvent $event
151
     */
152
    public function handlePersist(AbstractMappingEvent $event)
153
    {
154
        $document = $event->getDocument();
155
        if (!$document instanceof RoutablePageBehavior) {
156
            return;
157 30
        }
158
159 30
        $document->setUuid($event->getNode()->getIdentifier());
160 1
161
        $generatedRoute = $this->chainRouteGenerator->generate(
162
            $document,
163 30
            $event->getOption('route_path') ?: $document->getRoutePath()
164 30
        );
165 30
166
        $document->setRoutePath($generatedRoute->getPath());
167 30
168
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
169
        $event->getNode()->setProperty($propertyName, $generatedRoute->getPath());
170
    }
171 30
172
    /**
173
     * Handle publish event and generate route and the child-routes.
174
     *
175
     * @param PublishEvent $event
176
     */
177
    public function handlePublish(PublishEvent $event)
178 30
    {
179
        $document = $event->getDocument();
180 30
        if (!$document instanceof RoutableBehavior) {
181 30
            return;
182 30
        }
183
184
        $this->entityManager->persist($this->createOrUpdateRoute($document, $event->getLocale()));
0 ignored issues
show
Bug introduced by
It seems like $this->createOrUpdateRou...t, $event->getLocale()) can be null; however, persist() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
185
186
        $propertyName = $this->getPropertyName($event->getLocale(), self::ROUTES_PROPERTY);
187
188
        // check if nodes previous generated routes exists and remove them if not
189
        $oldRoutes = $event->getNode()->getPropertyValueWithDefault($propertyName, []);
190
        $this->removeOldChildRoutes($event->getNode()->getSession(), $oldRoutes, $event->getLocale());
191
192
        $routes = [];
193
        if ($document instanceof ChildrenBehavior) {
194
            // generate new routes of children
195
            $routes = $this->generateChildRoutes($document, $event->getLocale());
196
        }
197
198
        // save the newly generated routes of children
199
        $event->getNode()->setProperty($this->getPropertyName($event->getLocale(), self::ROUTES_PROPERTY), $routes);
200
        $this->entityManager->flush();
201
    }
202
203
    /**
204
     * Create or update for given document.
205
     *
206
     * @param RoutablePageBehavior $document
207
     * @param string $locale
208
     *
209
     * @return RouteInterface
210
     */
211 View Code Duplication
    private function createOrUpdatePageRoute(RoutablePageBehavior $document, $locale)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
212
    {
213
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $locale);
214
        if ($route) {
215
            $document->setRoute($route);
216
217
            return $this->routeManager->update($document);
218
        }
219
220
        return $this->routeManager->create($document);
221
    }
222
223
    /**
224
     * Create or update for given document.
225
     *
226
     * @param RoutableBehavior $document
227
     * @param string $locale
228
     *
229
     * @return RouteInterface
230
     */
231 View Code Duplication
    private function createOrUpdateRoute(RoutableBehavior $document, $locale)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
232
    {
233
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $locale);
234
        if ($route) {
235
            $document->setRoute($route);
236
237
            return $this->routeManager->update($document, $document->getRoutePath());
238
        }
239
240
        return $this->routeManager->create($document, $document->getRoutePath());
241
    }
242
243
    /**
244
     * Removes old-routes where the node does not exists anymore.
245
     *
246
     * @param SessionInterface $session
247
     * @param array $oldRoutes
248
     * @param string $locale
249
     */
250
    private function removeOldChildRoutes(SessionInterface $session, array $oldRoutes, $locale)
251
    {
252
        foreach ($oldRoutes as $oldRoute) {
253
            $oldRouteEntity = $this->routeRepository->findByPath($oldRoute, $locale);
254
            if ($oldRouteEntity && !$this->nodeExists($session, $oldRouteEntity->getEntityId())) {
255
                $this->entityManager->remove($oldRouteEntity);
256
            }
257
        }
258
259
        $this->entityManager->flush();
260
    }
261
262
    /**
263
     * Generates child routes.
264
     *
265
     * @param ChildrenBehavior $document
266
     * @param string $locale
267
     *
268
     * @return string[]
269
     */
270
    private function generateChildRoutes(ChildrenBehavior $document, $locale)
271
    {
272
        $routes = [];
273
        foreach ($document->getChildren() as $child) {
274
            if (!$child instanceof RoutablePageBehavior) {
275
                continue;
276
            }
277
278
            $childRoute = $this->createOrUpdatePageRoute($child, $locale);
279
            $this->entityManager->persist($childRoute);
0 ignored issues
show
Bug introduced by
It seems like $childRoute defined by $this->createOrUpdatePageRoute($child, $locale) on line 278 can be null; however, Doctrine\Common\Persiste...bjectManager::persist() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
280
281
            $child->setRoutePath($childRoute->getPath());
282
            $childNode = $this->documentInspector->getNode($child);
283
284
            $propertyName = $this->getRoutePathPropertyName($child->getStructureType(), $locale);
285
            $childNode->setProperty($propertyName, $childRoute->getPath());
286
287
            $routes[] = $childRoute->getPath();
288
        }
289
290
        return $routes;
291
    }
292
293
    /**
294
     * Removes route.
295
     *
296
     * @param RemoveEvent $event
297
     */
298
    public function handleRemove(RemoveEvent $event)
299
    {
300
        $document = $event->getDocument();
301
        if (!$document instanceof RoutableBehavior) {
302
            return;
303
        }
304
305
        $route = $this->routeRepository->findByPath($document->getRoutePath(), $document->getOriginalLocale());
306
        if (!$route) {
307
            return;
308
        }
309
310
        $this->entityManager->remove($route);
311
312
        if ($document instanceof ChildrenBehavior) {
313
            $this->removeChildRoutes($document);
314
        }
315
316
        $this->entityManager->flush();
317
    }
318
319
    /**
320
     * Iterate over children and remove routes.
321
     *
322
     * @param ChildrenBehavior $document
323
     */
324
    private function removeChildRoutes(ChildrenBehavior $document)
325
    {
326
        foreach ($document->getChildren() as $child) {
327
            if ($child instanceof RoutablePageBehavior) {
328
                $this->removeChildRoute($child);
329
            }
330
331
            if ($child instanceof ChildrenBehavior) {
332
                $this->removeChildRoutes($child);
333
            }
334
        }
335
    }
336
337
    /**
338
     * Removes route if exists.
339
     *
340
     * @param RoutablePageBehavior $document
341
     */
342
    private function removeChildRoute(RoutablePageBehavior $document)
343
    {
344
        $route = $this->routeRepository->findByPath($document->getRoutePath(), $document->getOriginalLocale());
345
        if ($route) {
346
            $this->entityManager->remove($route);
347
        }
348
    }
349
350
    /**
351
     * Add route-path to options.
352
     *
353
     * @param ConfigureOptionsEvent $event
354
     */
355
    public function configureOptions(ConfigureOptionsEvent $event)
356
    {
357
        $options = $event->getOptions();
358
        $options->setDefaults(['route_path' => null]);
359
    }
360
361
    /**
362
     * Returns encoded "routePath" property-name.
363
     *
364
     * @param string $structureType
365
     * @param string $locale
366
     *
367
     * @return string
368
     */
369
    private function getRoutePathPropertyName($structureType, $locale)
370
    {
371
        $metadata = $this->metadataFactory->getStructureMetadata('article', $structureType);
372
373
        if ($metadata->hasTag(self::TAG_NAME)) {
374
            return $this->getPropertyName($locale, $metadata->getPropertyByTagName(self::TAG_NAME)->getName());
375
        }
376
377
        return $this->getPropertyName($locale, self::ROUTE_FIELD);
378
    }
379
380
    /**
381
     * Returns encoded property-name.
382
     *
383
     * @param string $locale
384
     * @param string $field
385
     *
386
     * @return string
387
     */
388
    private function getPropertyName($locale, $field)
389
    {
390
        return $this->propertyEncoder->localizedSystemName($field, $locale);
391
    }
392
393
    /**
394
     * Returns true if given uuid exists.
395
     *
396
     * @param SessionInterface $session
397
     * @param string $uuid
398
     *
399
     * @return bool
400
     */
401
    private function nodeExists(SessionInterface $session, $uuid)
402
    {
403
        try {
404
            $session->getNodeByIdentifier($uuid);
405
406
            return true;
407
        } catch (ItemNotFoundException $exception) {
408
            return false;
409
        }
410
    }
411
}
412