Completed
Pull Request — develop (#161)
by Wachter
15:19
created

RoutableSubscriber::removeChildRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 6
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 37
    /**
76
     * @var PropertyEncoder
77
     */
78
    private $propertyEncoder;
79
80
    /**
81
     * @var StructureMetadataFactoryInterface
82
     */
83 37
    private $metadataFactory;
84 37
85 37
    /**
86 37
     * @var ConflictResolverInterface
87 37
     */
88 37
    private $conflictResolver;
89 37
90
    /**
91
     * @param ChainRouteGeneratorInterface $chainRouteGenerator
92
     * @param RouteManagerInterface $routeManager
93
     * @param RouteRepositoryInterface $routeRepository
94 32
     * @param EntityManagerInterface $entityManager
95
     * @param DocumentManagerInterface $documentManager
96
     * @param DocumentInspector $documentInspector
97 32
     * @param PropertyEncoder $propertyEncoder
98 32
     * @param StructureMetadataFactoryInterface $metadataFactory
99 32
     * @param ConflictResolverInterface $conflictResolver
100 32
     */
101 32
    public function __construct(
102 32
        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 32
    ) {
112
        $this->chainRouteGenerator = $chainRouteGenerator;
113 32
        $this->routeManager = $routeManager;
114 32
        $this->routeRepository = $routeRepository;
115 30
        $this->entityManager = $entityManager;
116
        $this->documentManager = $documentManager;
117
        $this->documentInspector = $documentInspector;
118 32
        $this->propertyEncoder = $propertyEncoder;
119 32
        $this->metadataFactory = $metadataFactory;
120 31
        $this->conflictResolver = $conflictResolver;
121
    }
122
123 32
    /**
124 32
     * {@inheritdoc}
125
     */
126 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
            Events::HYDRATE => ['handleHydrate'],
130
            Events::PERSIST => [
131 33
                // low priority because all other subscriber should be finished
132
                ['handlePersist', -2000],
133 33
            ],
134 33
            Events::REMOVE => [
135 9
                // high priority to ensure nodes are not deleted until we iterate over children
136
                ['handleRemove', 1024],
137
            ],
138 33
            Events::PUBLISH => ['handlePublish', -2000],
139
            Events::COPY => ['handleCopy', -2000],
140 33
        ];
141 33
    }
142 33
143 33
    /**
144
     * Load route.
145
     *
146
     * @param AbstractMappingEvent $event
147
     */
148
    public function handleHydrate(AbstractMappingEvent $event)
149
    {
150 32
        $document = $event->getDocument();
151
        if (!$document instanceof RoutablePageBehavior) {
152 32
            return;
153 32
        }
154 32
155 32
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
156
        $routePath = $event->getNode()->getPropertyValueWithDefault($propertyName, null);
157 31
        $document->setRoutePath($routePath);
158
159
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $event->getLocale());
160 1
        if ($route) {
161 1
            $document->setRoute($route);
162 1
        }
163 1
    }
164 1
165
    /**
166
     * Generate route and save route-path.
167
     *
168
     * @param AbstractMappingEvent $event
169
     */
170
    public function handlePersist(AbstractMappingEvent $event)
171 3
    {
172
        $document = $event->getDocument();
173 3
        if (!$document instanceof RoutablePageBehavior) {
174 3
            return;
175
        }
176
177
        $document->setUuid($event->getNode()->getIdentifier());
178 3
179 3
        $propertyName = $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale());
180 2
        $routePath = $event->getNode()->getPropertyValueWithDefault($propertyName, null);
181
182
        $route = $this->chainRouteGenerator->generate($document, $routePath);
183 1
        $document->setRoutePath($route->getPath());
184 1
185 1
        $event->getNode()->setProperty($propertyName, $route->getPath());
186
    }
187
188
    /**
189
     * Handle publish event and generate route and the child-routes.
190
     *
191
     * @param PublishEvent $event
192 1
     */
193
    public function handlePublish(PublishEvent $event)
194 1
    {
195 1
        $document = $event->getDocument();
196
        if (!$document instanceof RoutableBehavior) {
197
            return;
198
        }
199 1
200 1
        $node = $this->documentInspector->getNode($document);
201
202 1
        $route = $this->createOrUpdateRoute($document, $event->getLocale());
203
        $document->setRoutePath($route->getPath());
204 1
        $this->entityManager->persist($route);
205 1
206 1
        $node->setProperty(
207 1
            $this->getRoutePathPropertyName($document->getStructureType(), $event->getLocale()),
208
            $route->getPath()
209 1
        );
210 1
211 1
        $propertyName = $this->getPropertyName($event->getLocale(), self::ROUTES_PROPERTY);
212
213
        // check if nodes previous generated routes exists and remove them if not
214
        $oldRoutes = $event->getNode()->getPropertyValueWithDefault($propertyName, []);
215 1
        $this->removeOldChildRoutes($event->getNode()->getSession(), $oldRoutes, $event->getLocale());
216 1
217
        $routes = [];
218
        if ($document instanceof ChildrenBehavior) {
219
            // generate new routes of children
220
            $routes = $this->generateChildRoutes($document, $event->getLocale());
221
        }
222
223 31
        // save the newly generated routes of children
224
        $event->getNode()->setProperty($propertyName, $routes);
225 31
        $this->entityManager->flush();
226 1
    }
227
228
    /**
229 31
     * Create or update for given document.
230 31
     *
231 31
     * @param RoutablePageBehavior $document
232
     * @param string $locale
233 31
     *
234 31
     * @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 31
    {
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 31
245
        return $this->conflictResolver->resolve($this->routeManager->create($document));
246 31
    }
247 31
248 31
    /**
249
     * Create or update for given document.
250
     *
251
     * @param RoutableBehavior $document
252
     * @param string $locale
253
     *
254
     * @return RouteInterface
255
     */
256 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
        $route = $this->routeRepository->findByEntity($document->getClass(), $document->getUuid(), $locale);
259
        if ($route) {
260
            $document->setRoute($route);
261
262
            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
        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
    private function removeOldChildRoutes(SessionInterface $session, array $oldRoutes, $locale)
276
    {
277
        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
        $this->entityManager->flush();
285
    }
286
287
    /**
288
     * Generates child routes.
289
     *
290
     * @param ChildrenBehavior $document
291
     * @param string $locale
292
     *
293
     * @return string[]
294
     */
295
    private function generateChildRoutes(ChildrenBehavior $document, $locale)
296
    {
297
        $routes = [];
298
        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
        return $routes;
316
    }
317
318
    /**
319
     * Removes route.
320
     *
321
     * @param RemoveEvent $event
322
     */
323
    public function handleRemove(RemoveEvent $event)
324
    {
325
        $document = $event->getDocument();
326
        if (!$document instanceof RoutableBehavior) {
327
            return;
328
        }
329
330
        $locales = $this->documentInspector->getLocales($document);
331
        foreach ($locales as $locale) {
332
            $localizedDocument = $this->documentManager->find($document->getUuid(), $locale);
333
334
            $route = $this->routeRepository->findByEntity(
335
                $localizedDocument->getClass(),
336
                $localizedDocument->getUuid(),
337
                $locale
338
            );
339
            if (!$route) {
340
                continue;
341
            }
342
343
            $this->entityManager->remove($route);
344
345
            if ($document instanceof ChildrenBehavior) {
346
                $this->removeChildRoutes($document);
347
            }
348
        }
349
350
        $this->entityManager->flush();
351
    }
352
353
    /**
354
     * Update routes for copied article.
355
     *
356
     * @param CopyEvent $event
357
     */
358
    public function handleCopy(CopyEvent $event)
359
    {
360
        $document = $event->getDocument();
361
        if (!$document instanceof RoutableBehavior) {
362
            return;
363
        }
364
365
        $locales = $this->documentInspector->getLocales($document);
366
        foreach ($locales as $locale) {
367
            $localizedDocument = $this->documentManager->find($event->getCopiedPath(), $locale);
368
369
            $route = $this->conflictResolver->resolve($this->chainRouteGenerator->generate($localizedDocument));
370
            $localizedDocument->setRoutePath($route->getPath());
371
372
            $node = $this->documentInspector->getNode($localizedDocument);
373
            $node->setProperty(
374
                $this->getRoutePathPropertyName($localizedDocument->getStructureType(), $locale),
375
                $route->getPath()
376
            );
377
378
            $propertyName = $this->getRoutePathPropertyName($localizedDocument->getStructureType(), $locale);
379
            $node = $this->documentInspector->getNode($localizedDocument);
380
            $node->setProperty($propertyName, $route->getPath());
381
382
            if ($localizedDocument instanceof ChildrenBehavior) {
383
                $this->generateChildRoutes($localizedDocument, $locale);
384
            }
385
        }
386
    }
387
388
    /**
389
     * Iterate over children and remove routes.
390
     *
391
     * @param ChildrenBehavior $document
392
     */
393
    private function removeChildRoutes(ChildrenBehavior $document)
394
    {
395
        foreach ($document->getChildren() as $child) {
396
            if ($child instanceof RoutablePageBehavior) {
397
                $this->removeChildRoute($child);
398
            }
399
400
            if ($child instanceof ChildrenBehavior) {
401
                $this->removeChildRoutes($child);
402
            }
403
        }
404
    }
405
406
    /**
407
     * Removes route if exists.
408
     *
409
     * @param RoutablePageBehavior $document
410
     */
411
    private function removeChildRoute(RoutablePageBehavior $document)
412
    {
413
        $route = $this->routeRepository->findByPath($document->getRoutePath(), $document->getOriginalLocale());
414
        if ($route) {
415
            $this->entityManager->remove($route);
416
        }
417
    }
418
419
    /**
420
     * Returns encoded "routePath" property-name.
421
     *
422
     * @param string $structureType
423
     * @param string $locale
424
     *
425
     * @return string
426
     */
427 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
        $metadata = $this->metadataFactory->getStructureMetadata('article', $structureType);
430
431
        if ($metadata->hasTag(self::TAG_NAME)) {
432
            return $this->getPropertyName($locale, $metadata->getPropertyByTagName(self::TAG_NAME)->getName());
433
        }
434
435
        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
    private function getPropertyName($locale, $field)
447
    {
448
        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