Completed
Pull Request — develop (#161)
by Wachter
22:54 queued 09:56
created

RoutableSubscriber::removeOldChildRoutes()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 5.2596

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 4
cts 7
cp 0.5714
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 3
nop 3
crap 5.2596
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\Generator\ChainRouteGeneratorInterface;
23
use Sulu\Bundle\RouteBundle\Manager\ConflictResolverInterface;
24
use Sulu\Bundle\RouteBundle\Manager\RouteManagerInterface;
25
use Sulu\Bundle\RouteBundle\Model\RouteInterface;
26
use Sulu\Component\Content\Metadata\Factory\StructureMetadataFactoryInterface;
27
use Sulu\Component\DocumentManager\Behavior\Mapping\ChildrenBehavior;
28
use Sulu\Component\DocumentManager\DocumentManagerInterface;
29
use Sulu\Component\DocumentManager\Event\AbstractMappingEvent;
30
use Sulu\Component\DocumentManager\Event\CopyEvent;
31
use Sulu\Component\DocumentManager\Event\PublishEvent;
32
use Sulu\Component\DocumentManager\Event\RemoveEvent;
33
use Sulu\Component\DocumentManager\Events;
34
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
35
36
/**
37
 * Handles document-manager events to create/update/remove routes.
38
 */
39
class RoutableSubscriber implements EventSubscriberInterface
40
{
41
    const ROUTE_FIELD = 'routePath';
42
    const ROUTES_PROPERTY = 'suluRoutes';
43
    const TAG_NAME = 'sulu_article.article_route';
44
45
    /**
46
     * @var ChainRouteGeneratorInterface
47
     */
48
    private $chainRouteGenerator;
49
50
    /**
51
     * @var RouteManagerInterface
52
     */
53
    private $routeManager;
54
55
    /**
56
     * @var RouteRepositoryInterface
57
     */
58
    private $routeRepository;
59
60
    /**
61
     * @var EntityManagerInterface
62
     */
63
    private $entityManager;
64
65
    /**
66
     * @var DocumentManagerInterface
67
     */
68
    private $documentManager;
69
70
    /**
71
     * @var DocumentInspector
72
     */
73
    private $documentInspector;
74
75
    /**
76
     * @var PropertyEncoder
77
     */
78
    private $propertyEncoder;
79
80
    /**
81
     * @var StructureMetadataFactoryInterface
82
     */
83
    private $metadataFactory;
84
85
    /**
86
     * @var ConflictResolverInterface
87
     */
88
    private $conflictResolver;
89
90
    /**
91
     * @param ChainRouteGeneratorInterface $chainRouteGenerator
92
     * @param RouteManagerInterface $routeManager
93
     * @param RouteRepositoryInterface $routeRepository
94
     * @param EntityManagerInterface $entityManager
95
     * @param DocumentManagerInterface $documentManager
96
     * @param DocumentInspector $documentInspector
97
     * @param PropertyEncoder $propertyEncoder
98
     * @param StructureMetadataFactoryInterface $metadataFactory
99
     * @param ConflictResolverInterface $conflictResolver
100
     */
101 52
    public function __construct(
102
        ChainRouteGeneratorInterface $chainRouteGenerator,
103
        RouteManagerInterface $routeManager,
104
        RouteRepositoryInterface $routeRepository,
105
        EntityManagerInterface $entityManager,
106
        DocumentManagerInterface $documentManager,
107
        DocumentInspector $documentInspector,
108
        PropertyEncoder $propertyEncoder,
109
        StructureMetadataFactoryInterface $metadataFactory,
110
        ConflictResolverInterface $conflictResolver
111
    ) {
112 52
        $this->chainRouteGenerator = $chainRouteGenerator;
113 52
        $this->routeManager = $routeManager;
114 52
        $this->routeRepository = $routeRepository;
115 52
        $this->entityManager = $entityManager;
116 52
        $this->documentManager = $documentManager;
117 52
        $this->documentInspector = $documentInspector;
118 52
        $this->propertyEncoder = $propertyEncoder;
119 52
        $this->metadataFactory = $metadataFactory;
120 52
        $this->conflictResolver = $conflictResolver;
121 52
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 45 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...
127
    {
128
        return [
129 45
            Events::HYDRATE => ['handleHydrate'],
130 45
            Events::PERSIST => [
131
                // low priority because all other subscriber should be finished
132
                ['handlePersist', -2000],
133
            ],
134 45
            Events::REMOVE => [
135
                // high priority to ensure nodes are not deleted until we iterate over children
136
                ['handleRemove', 1024],
137
            ],
138 45
            Events::PUBLISH => ['handlePublish', -2000],
139 45
            Events::COPY => ['handleCopy', -2000],
140
        ];
141
    }
142
143
    /**
144
     * Load route.
145
     *
146
     * @param AbstractMappingEvent $event
147
     */
148 46
    public function handleHydrate(AbstractMappingEvent $event)
149
    {
150 46
        $document = $event->getDocument();
151 46
        if (!$document instanceof RoutablePageBehavior) {
152 43
            return;
153
        }
154
155 46
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
156 46
        $routePath = $event->getNode()->getPropertyValueWithDefault($propertyName, null);
157 46
        $document->setRoutePath($routePath);
158
159 46
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $event->getLocale());
160 46
        if ($route) {
161 13
            $document->setRoute($route);
162
        }
163 46
    }
164
165
    /**
166
     * Generate route and save route-path.
167
     *
168
     * @param AbstractMappingEvent $event
169
     */
170 47
    public function handlePersist(AbstractMappingEvent $event)
171
    {
172 47
        $document = $event->getDocument();
173 47
        if (!$document instanceof RoutablePageBehavior) {
174 6
            return;
175
        }
176
177 47
        $document->setUuid($event->getNode()->getIdentifier());
178
179 47
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
180 47
        $routePath = $event->getNode()->getPropertyValueWithDefault($propertyName, null);
181
182 47
        $route = $this->chainRouteGenerator->generate($document, $routePath);
183 47
        $document->setRoutePath($route->getPath());
184
185 47
        $event->getNode()->setProperty($propertyName, $route->getPath());
186 47
    }
187
188
    /**
189
     * Handle publish event and generate route and the child-routes.
190
     *
191
     * @param PublishEvent $event
192
     */
193 16
    public function handlePublish(PublishEvent $event)
194
    {
195 16
        $document = $event->getDocument();
196 16
        if (!$document instanceof RoutableBehavior) {
197 6
            return;
198
        }
199
200 11
        $node = $this->documentInspector->getNode($document);
201
202 11
        $route = $this->createOrUpdateRoute($document, $event->getLocale());
203 11
        $document->setRoutePath($route->getPath());
204 11
        $this->entityManager->persist($route);
205
206 11
        $node->setProperty(
207 11
            $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale()),
208 11
            $route->getPath()
209
        );
210
211 11
        $propertyName = $this->getPropertyName($event->getLocale(), self::ROUTES_PROPERTY);
212
213
        // check if nodes previous generated routes exists and remove them if not
214 11
        $oldRoutes = $event->getNode()->getPropertyValueWithDefault($propertyName, []);
215 11
        $this->removeOldChildRoutes($event->getNode()->getSession(), $oldRoutes, $event->getLocale());
216
217 11
        $routes = [];
218 11
        if ($document instanceof ChildrenBehavior) {
219
            // generate new routes of children
220 11
            $routes = $this->generateChildRoutes($document, $event->getLocale());
221
        }
222
223
        // save the newly generated routes of children
224 11
        $event->getNode()->setProperty($propertyName, $routes);
225 11
        $this->entityManager->flush();
226 11
    }
227
228
    /**
229
     * Create or update for given document.
230
     *
231
     * @param RoutablePageBehavior $document
232
     * @param string $locale
233
     *
234
     * @return RouteInterface
235
     */
236 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...
237
    {
238
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $locale);
239
        if ($route) {
240
            $document->setRoute($route);
241
242
            return $this->conflictResolver->resolve($this->routeManager->update($document));
0 ignored issues
show
Bug introduced by
It seems like $this->routeManager->update($document) can be null; however, resolve() 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...
243
        }
244
245
        return $this->conflictResolver->resolve($this->routeManager->create($document));
246
    }
247
248
    /**
249
     * Create or update for given document.
250
     *
251
     * @param RoutableBehavior $document
252
     * @param string $locale
253
     *
254
     * @return RouteInterface
255
     */
256 11 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...
257
    {
258 11
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $locale);
259 11
        if ($route) {
260 1
            $document->setRoute($route);
261
262 1
            return $this->conflictResolver->resolve($this->routeManager->update($document, $document->getRoutePath()));
0 ignored issues
show
Bug introduced by
It seems like $this->routeManager->upd...cument->getRoutePath()) can be null; however, resolve() 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...
263
        }
264
265 11
        return $this->conflictResolver->resolve($this->routeManager->create($document, $document->getRoutePath()));
266
    }
267
268
    /**
269
     * Removes old-routes where the node does not exists anymore.
270
     *
271
     * @param SessionInterface $session
272
     * @param array $oldRoutes
273
     * @param string $locale
274
     */
275 11
    private function removeOldChildRoutes(SessionInterface $session, array $oldRoutes, $locale)
276
    {
277 11
        foreach ($oldRoutes as $oldRoute) {
278
            $oldRouteEntity = $this->routeRepository->findByPath($oldRoute, $locale);
279
            if ($oldRouteEntity && !$this->nodeExists($session, $oldRouteEntity->getEntityId())) {
280
                $this->entityManager->remove($oldRouteEntity);
281
            }
282
        }
283
284 11
        $this->entityManager->flush();
285 11
    }
286
287
    /**
288
     * Generates child routes.
289
     *
290
     * @param ChildrenBehavior $document
291
     * @param string $locale
292
     *
293
     * @return string[]
294
     */
295 11
    private function generateChildRoutes(ChildrenBehavior $document, $locale)
296
    {
297 11
        $routes = [];
298 11
        foreach ($document->getChildren() as $child) {
299
            if (!$child instanceof RoutablePageBehavior) {
300
                continue;
301
            }
302
303
            $childRoute = $this->createOrUpdatePageRoute($child, $locale);
304
            $this->entityManager->persist($childRoute);
305
306
            $child->setRoutePath($childRoute->getPath());
307
            $childNode = $this->documentInspector->getNode($child);
308
309
            $propertyName = $this->getRoutePathPropertyName($child->getStructureType(), $locale);
310
            $childNode->setProperty($propertyName, $childRoute->getPath());
311
312
            $routes[] = $childRoute->getPath();
313
        }
314
315 11
        return $routes;
316
    }
317
318
    /**
319
     * Removes route.
320
     *
321
     * @param RemoveEvent $event
322
     */
323 6
    public function handleRemove(RemoveEvent $event)
324
    {
325 6
        $document = $event->getDocument();
326 6
        if (!$document instanceof RoutableBehavior) {
327 2
            return;
328
        }
329
330 4
        $locales = $this->documentInspector->getLocales($document);
331 4
        foreach ($locales as $locale) {
332 4
            $localizedDocument = $this->documentManager->find($document->getUuid(), $locale);
333
334 4
            $route = $this->routeRepository->findByEntity(
335 4
                $localizedDocument->getClass(),
336 4
                $localizedDocument->getUuid(),
337
                $locale
338
            );
339 4
            if (!$route) {
340 2
                continue;
341
            }
342
343 2
            $this->entityManager->remove($route);
344
345 2
            if ($document instanceof ChildrenBehavior) {
346 2
                $this->removeChildRoutes($document);
347
            }
348
        }
349
350 4
        $this->entityManager->flush();
351 4
    }
352
353
    /**
354
     * Update routes for copied article.
355
     *
356
     * @param CopyEvent $event
357
     */
358 1
    public function handleCopy(CopyEvent $event)
359
    {
360 1
        $document = $event->getDocument();
361 1
        if (!$document instanceof RoutableBehavior) {
362
            return;
363
        }
364
365 1
        $locales = $this->documentInspector->getLocales($document);
366 1
        foreach ($locales as $locale) {
367 1
            $localizedDocument = $this->documentManager->find($event->getCopiedPath(), $locale);
368
369 1
            $route = $this->conflictResolver->resolve($this->chainRouteGenerator->generate($localizedDocument));
370 1
            $localizedDocument->setRoutePath($route->getPath());
371
372 1
            $node = $this->documentInspector->getNode($localizedDocument);
373 1
            $node->setProperty(
374 1
                $this->getRoutePathPropertyName($localizedDocument->getStructureType(), $locale),
375 1
                $route->getPath()
376
            );
377
378 1
            $propertyName = $this->getRoutePathPropertyName($localizedDocument->getStructureType(), $locale);
379 1
            $node = $this->documentInspector->getNode($localizedDocument);
380 1
            $node->setProperty($propertyName, $route->getPath());
381
382 1
            if ($localizedDocument instanceof ChildrenBehavior) {
383 1
                $this->generateChildRoutes($localizedDocument, $locale);
384
            }
385
        }
386 1
    }
387
388
    /**
389
     * Iterate over children and remove routes.
390
     *
391
     * @param ChildrenBehavior $document
392
     */
393 1
    private function removeChildRoutes(ChildrenBehavior $document)
394
    {
395 1
        foreach ($document->getChildren() as $child) {
396 1
            if ($child instanceof RoutablePageBehavior) {
397 1
                $this->removeChildRoute($child);
398
            }
399
400 1
            if ($child instanceof ChildrenBehavior) {
401 1
                $this->removeChildRoutes($child);
402
            }
403
        }
404 1
    }
405
406
    /**
407
     * Removes route if exists.
408
     *
409
     * @param RoutablePageBehavior $document
410
     */
411 1
    private function removeChildRoute(RoutablePageBehavior $document)
412
    {
413 1
        $route = $this->routeRepository->findByPath($document->getRoutePath(), $document->getOriginalLocale());
414 1
        if ($route) {
415 1
            $this->entityManager->remove($route);
416
        }
417 1
    }
418
419
    /**
420
     * Returns encoded "routePath" property-name.
421
     *
422
     * @param string $structureType
423
     * @param string $locale
424
     *
425
     * @return string
426
     */
427 50 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...
428
    {
429 50
        $metadata = $this->metadataFactory->getStructureMetadata('article', $structureType);
430
431 50
        if ($metadata->hasTag(self::TAG_NAME)) {
432 1
            return $this->getPropertyName($locale, $metadata->getPropertyByTagName(self::TAG_NAME)->getName());
433
        }
434
435 49
        return $this->getPropertyName($locale, self::ROUTE_FIELD);
436
    }
437
438
    /**
439
     * Returns encoded property-name.
440
     *
441
     * @param string $locale
442
     * @param string $field
443
     *
444
     * @return string
445
     */
446 50
    private function getPropertyName($locale, $field)
447
    {
448 50
        return $this->propertyEncoder->localizedSystemName($field, $locale);
449
    }
450
451
    /**
452
     * Returns true if given uuid exists.
453
     *
454
     * @param SessionInterface $session
455
     * @param string $uuid
456
     *
457
     * @return bool
458
     */
459
    private function nodeExists(SessionInterface $session, $uuid)
460
    {
461
        try {
462
            $session->getNodeByIdentifier($uuid);
463
464
            return true;
465
        } catch (ItemNotFoundException $exception) {
466
            return false;
467
        }
468
    }
469
}
470