Completed
Push — master ( 91fdab...75a7b9 )
by
unknown
13:37
created

EventListener/NodeTranslationListener.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Kunstmaan\NodeBundle\EventListener;
4
5
use Doctrine\Common\Persistence\Mapping\MappingException;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\Event\LifecycleEventArgs;
8
use Doctrine\ORM\Event\OnFlushEventArgs;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use Kunstmaan\AdminBundle\FlashMessages\FlashTypes;
11
use Kunstmaan\AdminBundle\Helper\DomainConfigurationInterface;
12
use Kunstmaan\NodeBundle\Entity\HasNodeInterface;
13
use Kunstmaan\NodeBundle\Entity\Node;
14
use Kunstmaan\NodeBundle\Entity\NodeTranslation;
15
use Kunstmaan\NodeBundle\Helper\PagesConfiguration;
16
use Kunstmaan\NodeBundle\Repository\NodeTranslationRepository;
17
use Kunstmaan\UtilitiesBundle\Helper\SlugifierInterface;
18
use Psr\Log\LoggerInterface;
19
use Symfony\Component\HttpFoundation\RequestStack;
20
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
21
22
/**
23
 * Class NodeTranslationListener
24
 * Listens to doctrine postFlush event and updates the urls if the entities are nodetranslations
25
 *
26
 * @package Kunstmaan\NodeBundle\EventListener
27
 */
28
class NodeTranslationListener
29
{
30
    /** @var FlashBagInterface */
31
    private $flashBag;
32
33
    /** @var LoggerInterface */
34
    private $logger;
35
36
    /** @var SlugifierInterface */
37
    private $slugifier;
38
39
    /** @var RequestStack */
40
    private $requestStack;
41
42
    /** @var DomainConfigurationInterface */
43
    private $domainConfiguration;
44
45
    /** @var PagesConfiguration */
46
    private $pagesConfiguration;
47
48
    /**
49
     * NodeTranslationListener constructor.
50
     *
51
     * @param FlashBagInterface            $flashBag
52
     * @param LoggerInterface              $logger
53
     * @param SlugifierInterface           $slugifier
54
     * @param RequestStack                 $requestStack
55
     * @param DomainConfigurationInterface $domainConfiguration
56
     * @param PagesConfiguration           $pagesConfiguration
57
     */
58
    public function __construct(
59
        FlashBagInterface $flashBag,
60
        LoggerInterface $logger,
61
        SlugifierInterface $slugifier,
62
        RequestStack $requestStack,
63
        DomainConfigurationInterface $domainConfiguration,
64
        PagesConfiguration $pagesConfiguration
65
    ) {
66
        $this->flashBag = $flashBag;
67
        $this->logger = $logger;
68
        $this->slugifier = $slugifier;
69
        $this->requestStack = $requestStack;
70
        $this->domainConfiguration = $domainConfiguration;
71
        $this->pagesConfiguration = $pagesConfiguration;
72
    }
73
74
    /**
75
     * @param LifecycleEventArgs $args
76
     */
77 View Code Duplication
    public function prePersist(LifecycleEventArgs $args)
0 ignored issues
show
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...
78
    {
79
        $entity = $args->getEntity();
80
81
        if ($entity instanceof NodeTranslation) {
82
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
83
            $this->ensureSlugIsSlugified($entity);
84
        }
85
    }
86
87
    /**
88
     * @param LifecycleEventArgs $args
89
     */
90 View Code Duplication
    public function preUpdate(LifecycleEventArgs $args)
0 ignored issues
show
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...
91
    {
92
        $entity = $args->getEntity();
93
94
        if ($entity instanceof NodeTranslation) {
95
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
96
            $this->ensureSlugIsSlugified($entity);
97
        }
98
    }
99
100
    /**
101
     * @param NodeTranslation        $nodeTranslation
102
     * @param EntityManagerInterface $em
103
     */
104
    private function setSlugWhenEmpty(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
105
    {
106
        $publicNode = $nodeTranslation->getRef($em);
107
108
        // Do nothing for StructureNode objects, skip.
109
        if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
110
            return;
111
        }
112
113
        // If no slug is set and no structure node, apply title as slug.
114 View Code Duplication
        if ($nodeTranslation->getSlug() === null && $nodeTranslation->getNode()->getParent() !== null) {
115
            $nodeTranslation->setSlug(
116
                $this->slugifier->slugify($nodeTranslation->getTitle())
117
            );
118
        }
119
    }
120
121
    /**
122
     * @param NodeTranslation $nodeTranslation
123
     */
124
    private function ensureSlugIsSlugified(NodeTranslation $nodeTranslation)
125
    {
126
        if ($nodeTranslation->getSlug() !== null) {
127
            $nodeTranslation->setSlug(
128
                $this->slugifier->slugify($nodeTranslation->getSlug())
129
            );
130
        }
131
    }
132
133
    /**
134
     * OnFlush doctrine event - updates the nodetranslation urls if needed
135
     *
136
     * @param OnFlushEventArgs $args
137
     */
138
    public function onFlush(OnFlushEventArgs $args)
139
    {
140
        try {
141
            $em = $args->getEntityManager();
142
143
            $class = $em->getClassMetadata(NodeTranslation::class);
144
145
            // Collect all nodetranslations that are updated
146
            foreach ($em->getUnitOfWork()->getScheduledEntityUpdates() as $entity) {
147
                if ($entity instanceof NodeTranslation) {
148
                    /** @var Node $publicNode */
149
                    $publicNode = $entity->getPublicNodeVersion()->getRef($em);
150
151
                    // Do nothing for StructureNode objects, skip.
152
                    if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
153
                        continue;
154
                    }
155
156
                    $entity = $this->updateUrl($entity, $em);
157
158 View Code Duplication
                    if ($entity !== false) {
159
                        $em->persist($entity);
160
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $entity);
161
    
162
                        $this->updateNodeChildren($entity, $em, $class);
163
                    }
164
                }
165
            }
166
        } catch (MappingException $e) {
167
            // Different entity manager without this entity configured in namespace chain. Ignore
168
        }
169
    }
170
171
    /**
172
     * Checks if a nodetranslation has children and update their url
173
     *
174
     * @param NodeTranslation        $node  The node
175
     * @param EntityManagerInterface $em    The entity manager
176
     * @param ClassMetadata          $class The class meta daat
177
     */
178
    private function updateNodeChildren(NodeTranslation $node, EntityManagerInterface $em, ClassMetadata $class)
179
    {
180
        $children = $node->getNode()->getChildren();
181
        if (\count($children) > 0) {
182
            /* @var Node $child */
183
            foreach ($children as $child) {
184
                $translation = $child->getNodeTranslation($node->getLang(), true);
185
                if ($translation) {
186
                    $translation = $this->updateUrl($translation, $em);
187
188 View Code Duplication
                    if ($translation !== false) {
189
                        $em->persist($translation);
190
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $translation);
191
192
                        $this->updateNodeChildren($translation, $em, $class);
193
                    }
194
                }
195
            }
196
        }
197
    }
198
199
    /**
200
     * Update the url for a nodetranslation
201
     *
202
     * @param NodeTranslation        $nodeTranslation The node translation
203
     * @param EntityManagerInterface $em              The entity manager
204
     *
205
     * @return NodeTranslation|bool Returns the node when all is well because it has to be saved.
206
     */
207
    private function updateUrl(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
208
    {
209
        $result = $this->ensureUniqueUrl($nodeTranslation, $em);
210
211
        if ($result) {
212
            return $nodeTranslation;
213
        }
214
215
        $this->logger->info(
216
            sprintf('Found NT %s needed NO change', $nodeTranslation->getId())
217
        );
218
219
        return false;
220
    }
221
222
    /**
223
     * A function that checks the URL and sees if it's unique.
224
     * It's allowed to be the same when the node is a StructureNode.
225
     * When a node is deleted it needs to be ignored in the check.
226
     * Offline nodes need to be included as well.
227
     *
228
     * It sluggifies the slug, updates the URL
229
     * and checks all existing NodeTranslations ([1]), excluding itself. If a
230
     * URL existsthat has the same url. If an existing one is found the slug is
231
     * modified, the URL is updated and the check is repeated until no prior
232
     * urls exist.
233
     *
234
     * NOTE: We need a way to tell if the slug has been modified or not.
235
     * NOTE: Would be cool if we could increment a number after the slug. Like
236
     * check if it matches -v# and increment the number.
237
     *
238
     * [1] For all languages for now. The issue is that we need a way to know
239
     * if a node's URL is prepended with the language or not. For now both
240
     * scenarios are possible so we check for all languages.
241
     *
242
     * @param NodeTranslation        $translation  Reference to the NodeTranslation.
243
     *                                             This is modified in place.
244
     * @param EntityManagerInterface $em           The entity manager
245
     * @param array                  $flashes      The flash messages array
246
     *
247
     * @return bool
248
     */
249
    private function ensureUniqueUrl(NodeTranslation $translation, EntityManagerInterface $em, array $flashes = [])
250
    {
251
        // Can't use GetRef here yet since the NodeVersions aren't loaded yet for some reason.
252
        $nodeVersion = $translation->getPublicNodeVersion();
253
        $page = $em->getRepository($nodeVersion->getRefEntityName())
254
            ->find($nodeVersion->getRefId());
255
256
        if (null === $page) {
257
            return false;
258
        }
259
260
        $isStructureNode = $page->isStructureNode();
261
262
        // If it's a StructureNode the slug and url should be empty.
263
        if ($isStructureNode) {
264
            $translation->setSlug('');
265
            $translation->setUrl($translation->getFullSlug());
266
267
            return true;
268
        }
269
270
        /* @var NodeTranslationRepository $nodeTranslationRepository */
271
        $nodeTranslationRepository = $em->getRepository(NodeTranslation::class);
272
273
        if ($translation->getUrl() === $translation->getFullSlug()) {
274
            $this->logger->debug(
275
                sprintf(
276
                    'Evaluating URL for NT %s getUrl: "%s" getFullSlug: "%s"',
277
                    $translation->getId(),
278
                    $translation->getUrl(),
279
                    $translation->getFullSlug()
280
                )
281
            );
282
283
            return false;
284
        }
285
286
        // Adjust the URL.
287
        $translation->setUrl($translation->getFullSlug());
288
289
        // Find all translations with this new URL, whose nodes are not deleted.
290
        $translations = $nodeTranslationRepository->getAllNodeTranslationsForUrl(
291
            $translation->getUrl(),
292
            $translation->getLang(),
293
            false,
294
            $translation,
295
            $this->domainConfiguration->getRootNode()
296
        );
297
298
        $this->logger->debug(
299
            sprintf(
300
                'Found %s node(s) that math url "%s"',
301
                \count($translations),
302
                $translation->getUrl()
303
            )
304
        );
305
306
        $translationsWithSameUrl = [];
307
308
        /** @var NodeTranslation $trans */
309
        foreach ($translations as $trans) {
310
            if (!$this->pagesConfiguration->isStructureNode($trans->getPublicNodeVersion()->getRefEntityName())) {
311
                $translationsWithSameUrl[] = $trans;
312
            }
313
        }
314
315
        if (\count($translationsWithSameUrl) > 0) {
316
            $oldUrl = $translation->getFullSlug();
317
            $translation->setSlug(
318
                $this->slugifier->slugify(
319
                    $this->incrementString($translation->getSlug())
320
                )
321
            );
322
            $newUrl = $translation->getFullSlug();
323
324
            $message = sprintf(
325
                'The URL of the page has been changed from %s to %s since another page already uses this URL',
326
                $oldUrl,
327
                $newUrl
328
            );
329
            $this->logger->info($message);
330
            $flashes[] = $message;
331
332
            $this->ensureUniqueUrl($translation, $em, $flashes);
333
        } elseif (\count($flashes) > 0 && $this->isInRequestScope()) {
334
            // No translations found so we're certain we can show this message.
335
            $flash = current(\array_slice($flashes, -1));
336
            $this->flashBag->add(FlashTypes::WARNING, $flash);
337
        }
338
339
        return true;
340
    }
341
342
    /**
343
     * Increment a string that ends with a number.
344
     * If the string does not end in a number we'll add the append and then add
345
     * the first number.
346
     *
347
     * @param string $string The string we want to increment.
348
     * @param string $append The part we want to append before we start adding
349
     *                       a number.
350
     *
351
     * @return string Incremented string.
352
     */
353 View Code Duplication
    private function incrementString($string, $append = '-v')
354
    {
355
        $finalDigitGrabberRegex = '/\d+$/';
356
        $matches = [];
357
358
        preg_match($finalDigitGrabberRegex, $string, $matches);
359
360
        if (\count($matches) > 0) {
361
            $digit = (int) $matches[0];
362
            ++$digit;
363
364
            // Replace the integer with the new digit.
365
            return preg_replace($finalDigitGrabberRegex, $digit, $string);
366
        }
367
368
        return $string.$append.'1';
369
    }
370
371
    /**
372
     * @return bool
373
     */
374
    private function isInRequestScope()
375
    {
376
        return $this->requestStack && $this->requestStack->getCurrentRequest();
377
    }
378
}
379