Completed
Pull Request — develop (#179)
by Wachter
14:46
created

RoutableSubscriber::handleReorder()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.1502

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 9
cts 11
cp 0.8182
rs 8.5906
c 0
b 0
f 0
cc 5
eloc 14
nc 4
nop 1
crap 5.1502
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\DocumentManagerBundle\Bridge\PropertyEncoder;
21
use Sulu\Bundle\RouteBundle\Entity\RouteRepositoryInterface;
22
use Sulu\Bundle\RouteBundle\Exception\RouteIsNotUniqueException;
23
use Sulu\Bundle\RouteBundle\Generator\ChainRouteGeneratorInterface;
24
use Sulu\Bundle\RouteBundle\Manager\ConflictResolverInterface;
25
use Sulu\Bundle\RouteBundle\Manager\RouteManagerInterface;
26
use Sulu\Bundle\RouteBundle\Model\RouteInterface;
27
use Sulu\Component\Content\Exception\ResourceLocatorAlreadyExistsException;
28
use Sulu\Component\Content\Metadata\Factory\StructureMetadataFactoryInterface;
29
use Sulu\Component\DocumentManager\Behavior\Mapping\ChildrenBehavior;
30
use Sulu\Component\DocumentManager\Behavior\Mapping\ParentBehavior;
31
use Sulu\Component\DocumentManager\DocumentManagerInterface;
32
use Sulu\Component\DocumentManager\Event\AbstractMappingEvent;
33
use Sulu\Component\DocumentManager\Event\CopyEvent;
34
use Sulu\Component\DocumentManager\Event\PublishEvent;
35
use Sulu\Component\DocumentManager\Event\RemoveEvent;
36
use Sulu\Component\DocumentManager\Event\ReorderEvent;
37
use Sulu\Component\DocumentManager\Events;
38
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
39
40
/**
41
 * Handles document-manager events to create/update/remove routes.
42
 */
43
class RoutableSubscriber implements EventSubscriberInterface
44
{
45
    const ROUTE_FIELD = 'routePath';
46
    const ROUTES_PROPERTY = 'suluRoutes';
47
    const TAG_NAME = 'sulu_article.article_route';
48
49
    /**
50
     * @var ChainRouteGeneratorInterface
51
     */
52
    private $chainRouteGenerator;
53
54
    /**
55
     * @var RouteManagerInterface
56
     */
57
    private $routeManager;
58
59
    /**
60
     * @var RouteRepositoryInterface
61
     */
62
    private $routeRepository;
63
64
    /**
65
     * @var EntityManagerInterface
66
     */
67
    private $entityManager;
68
69
    /**
70
     * @var DocumentManagerInterface
71
     */
72
    private $documentManager;
73
74
    /**
75
     * @var DocumentInspector
76
     */
77
    private $documentInspector;
78
79
    /**
80
     * @var PropertyEncoder
81
     */
82
    private $propertyEncoder;
83
84
    /**
85
     * @var StructureMetadataFactoryInterface
86
     */
87
    private $metadataFactory;
88
89
    /**
90
     * @var ConflictResolverInterface
91
     */
92
    private $conflictResolver;
93
94
    /**
95
     * @param ChainRouteGeneratorInterface $chainRouteGenerator
96
     * @param RouteManagerInterface $routeManager
97
     * @param RouteRepositoryInterface $routeRepository
98
     * @param EntityManagerInterface $entityManager
99
     * @param DocumentManagerInterface $documentManager
100
     * @param DocumentInspector $documentInspector
101
     * @param PropertyEncoder $propertyEncoder
102
     * @param StructureMetadataFactoryInterface $metadataFactory
103 58
     * @param ConflictResolverInterface $conflictResolver
104
     */
105
    public function __construct(
106
        ChainRouteGeneratorInterface $chainRouteGenerator,
107
        RouteManagerInterface $routeManager,
108
        RouteRepositoryInterface $routeRepository,
109
        EntityManagerInterface $entityManager,
110
        DocumentManagerInterface $documentManager,
111
        DocumentInspector $documentInspector,
112
        PropertyEncoder $propertyEncoder,
113
        StructureMetadataFactoryInterface $metadataFactory,
114 58
        ConflictResolverInterface $conflictResolver
115 58
    ) {
116 58
        $this->chainRouteGenerator = $chainRouteGenerator;
117 58
        $this->routeManager = $routeManager;
118 58
        $this->routeRepository = $routeRepository;
119 58
        $this->entityManager = $entityManager;
120 58
        $this->documentManager = $documentManager;
121 58
        $this->documentInspector = $documentInspector;
122 58
        $this->propertyEncoder = $propertyEncoder;
123 58
        $this->metadataFactory = $metadataFactory;
124
        $this->conflictResolver = $conflictResolver;
125
    }
126
127
    /**
128 52
     * {@inheritdoc}
129
     */
130
    public static function getSubscribedEvents()
131 52
    {
132 52
        return [
133
            Events::HYDRATE => ['handleHydrate'],
134
            Events::PERSIST => [
135
                // low priority because all other subscriber should be finished
136 52
                ['handlePersist', -2000],
137
            ],
138
            Events::REMOVE => [
139
                // high priority to ensure nodes are not deleted until we iterate over children
140 52
                ['handleRemove', 1024],
141 52
            ],
142
            Events::PUBLISH => ['handlePublish', -2000],
143
            Events::REORDER => ['handleReorder', -1000],
144
            Events::COPY => ['handleCopy', -2000],
145
        ];
146
    }
147
148
    /**
149
     * Load route.
150 52
     *
151
     * @param AbstractMappingEvent $event
152 52
     */
153 52
    public function handleHydrate(AbstractMappingEvent $event)
154 49
    {
155
        $document = $event->getDocument();
156
        if (!$document instanceof RoutablePageBehavior) {
157 51
            return;
158 51
        }
159 51
160
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
161 51
        $routePath = $event->getNode()->getPropertyValueWithDefault($propertyName, null);
162 51
        $document->setRoutePath($routePath);
163 18
164
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $event->getLocale());
165 51
        if ($route) {
166
            $document->setRoute($route);
167
        }
168
    }
169
170
    /**
171
     * Generate route and save route-path.
172 53
     *
173
     * @param AbstractMappingEvent $event
174 53
     */
175 53
    public function handlePersist(AbstractMappingEvent $event)
176 8
    {
177
        $document = $event->getDocument();
178
        if (!$document instanceof RoutablePageBehavior) {
179 52
            return;
180
        }
181 52
182 52
        $document->setUuid($event->getNode()->getIdentifier());
183
184 52
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
185 52
        $routePath = $event->getNode()->getPropertyValueWithDefault($propertyName, null);
186
187 52
        $route = $this->conflictResolver->resolve($this->chainRouteGenerator->generate($document, $routePath));
188 52
        $document->setRoutePath($route->getPath());
189
190
        $event->getNode()->setProperty($propertyName, $route->getPath());
191
    }
192
193
    public function handleReorder(ReorderEvent $event)
194
    {
195
        $document = $event->getDocument();
196
        if (!$document instanceof RoutablePageBehavior || !$document instanceof ParentBehavior) {
197 22
            return;
198
        }
199 22
200 22
        $parentDocument = $document->getParent();
201 8
        if (!$parentDocument instanceof ChildrenBehavior) {
202
            return;
203
        }
204 16
205
        $locale = $this->documentInspector->getLocale($parentDocument);
206
        $propertyName = $this->getRoutePathPropertyName($parentDocument->getStructureType(), $locale);
207 16
        foreach ($parentDocument->getChildren() as $childDocument) {
208
            $node = $this->documentInspector->getNode($childDocument);
209
210
            $route = $this->chainRouteGenerator->generate($childDocument);
211
            $childDocument->setRoutePath($route->getPath());
212 16
213 16
            $node->setProperty($propertyName, $route->getPath());
214
        }
215 16
    }
216 16
217 16
    /**
218
     * Handle publish event and generate route and the child-routes.
219
     *
220 16
     * @param PublishEvent $event
221
     *
222
     * @throws ResourceLocatorAlreadyExistsException
223 16
     */
224 16
    public function handlePublish(PublishEvent $event)
225
    {
226 16
        $document = $event->getDocument();
227 16
        if (!$document instanceof RoutableBehavior) {
228
            return;
229 16
        }
230
231
        $node = $this->documentInspector->getNode($document);
232
233 16
        try {
234 16
            $route = $this->createOrUpdateRoute($document, $event->getLocale());
235 16
        } catch (RouteIsNotUniqueException $exception) {
236
            throw new ResourceLocatorAlreadyExistsException($exception->getRoute()->getPath(), $document->getPath());
237
        }
238
239
        $document->setRoutePath($route->getPath());
240
        $this->entityManager->persist($route);
0 ignored issues
show
Bug introduced by
It seems like $route defined by $this->createOrUpdateRou...t, $event->getLocale()) on line 234 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...
241
242
        $node->setProperty(
243
            $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale()),
244
            $route->getPath()
245
        );
246
247
        $propertyName = $this->getPropertyName($event->getLocale(), self::ROUTES_PROPERTY);
248
249
        // check if nodes previous generated routes exists and remove them if not
250
        $oldRoutes = $event->getNode()->getPropertyValueWithDefault($propertyName, []);
251
        $this->removeOldChildRoutes($event->getNode()->getSession(), $oldRoutes, $event->getLocale());
252
253
        $routes = [];
254
        if ($document instanceof ChildrenBehavior) {
255
            // generate new routes of children
256
            $routes = $this->generateChildRoutes($document, $event->getLocale());
257
        }
258
259
        // save the newly generated routes of children
260
        $event->getNode()->setProperty($propertyName, $routes);
261
        $this->entityManager->flush();
262
    }
263
264
    /**
265
     * Create or update for given document.
266
     *
267
     * @param RoutablePageBehavior $document
268
     * @param string $locale
269
     *
270 16
     * @return RouteInterface
271
     */
272 16 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...
273
    {
274 16
        $route = $document->getRoute();
275 16
276
        $this->removeRouteIfExists($document->getRoutePath(), $locale);
277
278 16
        if (!$route) {
279 1
            $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $locale);
280
        }
281 1
282
        if ($route) {
283
            $document->setRoute($route);
284 16
285
            return $this->routeManager->update($document, null, false);
286
        }
287
288
        return $this->routeManager->create($document);
289
    }
290
291
    private function removeRouteIfExists($routePath, $locale)
292
    {
293
        $route = $this->routeRepository->findByPath($routePath, $locale);
294 16
        if (!$route) {
295
            return;
296 16
        }
297
298
        $this->entityManager->remove($route);
299
        $this->entityManager->flush($route);
0 ignored issues
show
Unused Code introduced by
The call to EntityManagerInterface::flush() has too many arguments starting with $route.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
300
    }
301
302
    /**
303 16
     * Create or update for given document.
304 16
     *
305
     * @param RoutableBehavior $document
306
     * @param string $locale
307
     *
308
     * @return RouteInterface
309
     */
310 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...
311
    {
312
        $route = $document->getRoute();
313
314 16
        if (!$route) {
315
            $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $locale);
316 16
        }
317 16
318
        if ($route) {
319
            $document->setRoute($route);
320
321
            return $this->routeManager->update($document, $document->getRoutePath(), false);
322
        }
323
324
        return $this->routeManager->create($document, $document->getRoutePath(), false);
325
    }
326
327
    /**
328
     * Removes old-routes where the node does not exists anymore.
329
     *
330
     * @param SessionInterface $session
331
     * @param array $oldRoutes
332
     * @param string $locale
333
     */
334 16
    private function removeOldChildRoutes(SessionInterface $session, array $oldRoutes, $locale)
335
    {
336
        foreach ($oldRoutes as $oldRoute) {
337
            $oldRouteEntity = $this->routeRepository->findByPath($oldRoute, $locale);
338
            if ($oldRouteEntity && !$this->nodeExists($session, $oldRouteEntity->getEntityId())) {
339
                $this->entityManager->remove($oldRouteEntity);
340
            }
341
        }
342 6
343
        $this->entityManager->flush();
344 6
    }
345 6
346 2
    /**
347
     * Generates child routes.
348
     *
349 4
     * @param ChildrenBehavior $document
350 4
     * @param string $locale
351 4
     *
352
     * @return string[]
353 4
     */
354 4
    private function generateChildRoutes(ChildrenBehavior $document, $locale)
355 4
    {
356 4
        $routes = [];
357
        foreach ($document->getChildren() as $child) {
358 4
            if (!$child instanceof RoutablePageBehavior) {
359 2
                continue;
360
            }
361
362 2
            $childRoute = $this->createOrUpdatePageRoute($child, $locale);
363
            $this->entityManager->persist($childRoute);
0 ignored issues
show
Bug introduced by
It seems like $childRoute defined by $this->createOrUpdatePageRoute($child, $locale) on line 362 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...
364 2
365 2
            $child->setRoutePath($childRoute->getPath());
366
            $childNode = $this->documentInspector->getNode($child);
367
368
            $propertyName = $this->getRoutePathPropertyName($child->getStructureType(), $locale);
369 4
            $childNode->setProperty($propertyName, $childRoute->getPath());
370 4
371
            $routes[] = $childRoute->getPath();
372
        }
373
374
        return $routes;
375
    }
376
377 1
    /**
378
     * Removes route.
379 1
     *
380 1
     * @param RemoveEvent $event
381
     */
382
    public function handleRemove(RemoveEvent $event)
383
    {
384 1
        $document = $event->getDocument();
385 1
        if (!$document instanceof RoutableBehavior) {
386 1
            return;
387
        }
388 1
389 1
        $locales = $this->documentInspector->getLocales($document);
390
        foreach ($locales as $locale) {
391 1
            $localizedDocument = $this->documentManager->find($document->getUuid(), $locale);
392 1
393 1
            $route = $this->routeRepository->findByEntity(
394 1
                $localizedDocument->getClass(),
395
                $localizedDocument->getUuid(),
396
                $locale
397 1
            );
398 1
            if (!$route) {
399 1
                continue;
400
            }
401 1
402 1
            $this->entityManager->remove($route);
403
404
            if ($document instanceof ChildrenBehavior) {
405 1
                $this->removeChildRoutes($document, $locale);
406
            }
407
        }
408
409
        $this->entityManager->flush();
410
    }
411
412
    /**
413 1
     * Update routes for copied article.
414
     *
415 1
     * @param CopyEvent $event
416 1
     */
417 1
    public function handleCopy(CopyEvent $event)
418
    {
419
        $document = $event->getDocument();
420 1
        if (!$document instanceof RoutableBehavior) {
421 1
            return;
422
        }
423
424 1
        $locales = $this->documentInspector->getLocales($document);
425
        foreach ($locales as $locale) {
426
            $localizedDocument = $this->documentManager->find($event->getCopiedPath(), $locale);
427
428
            $route = $this->conflictResolver->resolve($this->chainRouteGenerator->generate($localizedDocument));
429
            $localizedDocument->setRoutePath($route->getPath());
430
431
            $node = $this->documentInspector->getNode($localizedDocument);
432 1
            $node->setProperty(
433
                $this->getRoutePathPropertyName($localizedDocument->getStructureType(), $locale),
434 1
                $route->getPath()
435 1
            );
436 1
437
            $propertyName = $this->getRoutePathPropertyName($localizedDocument->getStructureType(), $locale);
438 1
            $node = $this->documentInspector->getNode($localizedDocument);
439
            $node->setProperty($propertyName, $route->getPath());
440
441
            if ($localizedDocument instanceof ChildrenBehavior) {
442
                $this->generateChildRoutes($localizedDocument, $locale);
443
            }
444
        }
445
    }
446
447
    /**
448 55
     * Iterate over children and remove routes.
449
     *
450 55
     * @param ChildrenBehavior $document
451
     * @param string $locale
452 55
     */
453 1
    private function removeChildRoutes(ChildrenBehavior $document, $locale)
454
    {
455
        foreach ($document->getChildren() as $child) {
456 54
            if ($child instanceof RoutablePageBehavior) {
457
                $this->removeChildRoute($child, $locale);
458
            }
459
460
            if ($child instanceof ChildrenBehavior) {
461
                $this->removeChildRoutes($child, $locale);
462
            }
463
        }
464
    }
465
466
    /**
467 55
     * Removes route if exists.
468
     *
469 55
     * @param RoutablePageBehavior $document
470
     * @param string $locale
471
     */
472
    private function removeChildRoute(RoutablePageBehavior $document, $locale)
473
    {
474
        $route = $this->routeRepository->findByPath($document->getRoutePath(), $locale);
475
        if ($route) {
476
            $this->entityManager->remove($route);
477
        }
478
    }
479
480
    /**
481
     * Returns encoded "routePath" property-name.
482
     *
483
     * @param string $structureType
484
     * @param string $locale
485
     *
486
     * @return string
487
     */
488 View Code Duplication
    private function getRoutePathPropertyName($structureType, $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...
489
    {
490
        $metadata = $this->metadataFactory->getStructureMetadata('article', $structureType);
491
492
        if ($metadata->hasTag(self::TAG_NAME)) {
493
            return $this->getPropertyName($locale, $metadata->getPropertyByTagName(self::TAG_NAME)->getName());
494
        }
495
496
        return $this->getPropertyName($locale, self::ROUTE_FIELD);
497
    }
498
499
    /**
500
     * Returns encoded property-name.
501
     *
502
     * @param string $locale
503
     * @param string $field
504
     *
505
     * @return string
506
     */
507
    private function getPropertyName($locale, $field)
508
    {
509
        return $this->propertyEncoder->localizedSystemName($field, $locale);
510
    }
511
512
    /**
513
     * Returns true if given uuid exists.
514
     *
515
     * @param SessionInterface $session
516
     * @param string $uuid
517
     *
518
     * @return bool
519
     */
520
    private function nodeExists(SessionInterface $session, $uuid)
521
    {
522
        try {
523
            $session->getNodeByIdentifier($uuid);
524
525
            return true;
526
        } catch (ItemNotFoundException $exception) {
527
            return false;
528
        }
529
    }
530
}
531