Completed
Push — master ( aba493...5356ed )
by Ruud
315:38 queued 305:00
created

EventListener/NodeTranslationListener.php (3 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
class NodeTranslationListener
27
{
28
    /** @var FlashBagInterface */
29
    private $flashBag;
30
31
    /** @var LoggerInterface */
32
    private $logger;
33
34
    /** @var SlugifierInterface */
35
    private $slugifier;
36
37
    /** @var RequestStack */
38
    private $requestStack;
39
40
    /** @var DomainConfigurationInterface */
41
    private $domainConfiguration;
42
43
    /** @var PagesConfiguration */
44
    private $pagesConfiguration;
45
46
    /**
47
     * NodeTranslationListener constructor.
48
     *
49
     * @param FlashBagInterface            $flashBag
50
     * @param LoggerInterface              $logger
51
     * @param SlugifierInterface           $slugifier
52
     * @param RequestStack                 $requestStack
53
     * @param DomainConfigurationInterface $domainConfiguration
54
     * @param PagesConfiguration           $pagesConfiguration
55
     */
56
    public function __construct(
57
        FlashBagInterface $flashBag,
58
        LoggerInterface $logger,
59
        SlugifierInterface $slugifier,
60
        RequestStack $requestStack,
61
        DomainConfigurationInterface $domainConfiguration,
62
        PagesConfiguration $pagesConfiguration
63
    ) {
64
        $this->flashBag = $flashBag;
65
        $this->logger = $logger;
66
        $this->slugifier = $slugifier;
67
        $this->requestStack = $requestStack;
68
        $this->domainConfiguration = $domainConfiguration;
69
        $this->pagesConfiguration = $pagesConfiguration;
70
    }
71
72
    /**
73
     * @param LifecycleEventArgs $args
74
     */
75 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...
76
    {
77
        $entity = $args->getEntity();
78
79
        if ($entity instanceof NodeTranslation) {
80
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
81
            $this->ensureSlugIsSlugified($entity);
82
        }
83
    }
84
85
    /**
86
     * @param LifecycleEventArgs $args
87
     */
88 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...
89
    {
90
        $entity = $args->getEntity();
91
92
        if ($entity instanceof NodeTranslation) {
93
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
94
            $this->ensureSlugIsSlugified($entity);
95
        }
96
    }
97
98
    /**
99
     * @param NodeTranslation        $nodeTranslation
100
     * @param EntityManagerInterface $em
101
     */
102
    private function setSlugWhenEmpty(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
103
    {
104
        $publicNode = $nodeTranslation->getRef($em);
0 ignored issues
show
$em of type object<Doctrine\ORM\EntityManagerInterface> is not a sub-type of object<Doctrine\ORM\EntityManager>. It seems like you assume a concrete implementation of the interface Doctrine\ORM\EntityManagerInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
105
106
        // Do nothing for StructureNode objects, skip.
107
        if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
108
            return;
109
        }
110
111
        // If no slug is set and no structure node, apply title as slug.
112 View Code Duplication
        if ($nodeTranslation->getSlug() === null && $nodeTranslation->getNode()->getParent() !== null) {
113
            $nodeTranslation->setSlug(
114
                $this->slugifier->slugify($nodeTranslation->getTitle())
115
            );
116
        }
117
    }
118
119
    /**
120
     * @param NodeTranslation $nodeTranslation
121
     */
122
    private function ensureSlugIsSlugified(NodeTranslation $nodeTranslation)
123
    {
124
        if ($nodeTranslation->getSlug() !== null) {
125
            $nodeTranslation->setSlug(
126
                $this->slugifier->slugify($nodeTranslation->getSlug())
127
            );
128
        }
129
    }
130
131
    /**
132
     * OnFlush doctrine event - updates the nodetranslation urls if needed
133
     *
134
     * @param OnFlushEventArgs $args
135
     */
136
    public function onFlush(OnFlushEventArgs $args)
137
    {
138
        try {
139
            $em = $args->getEntityManager();
140
141
            $class = $em->getClassMetadata(NodeTranslation::class);
142
143
            // Collect all nodetranslations that are updated
144
            foreach ($em->getUnitOfWork()->getScheduledEntityUpdates() as $entity) {
145
                if ($entity instanceof NodeTranslation) {
146
                    /** @var Node $publicNode */
147
                    $publicNode = $entity->getPublicNodeVersion()->getRef($em);
148
149
                    // Do nothing for StructureNode objects, skip.
150
                    if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
151
                        continue;
152
                    }
153
154
                    $entity = $this->updateUrl($entity, $em);
155
156 View Code Duplication
                    if ($entity !== false) {
157
                        $em->persist($entity);
158
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $entity);
159
160
                        $this->updateNodeChildren($entity, $em, $class);
161
                    }
162
                }
163
            }
164
        } catch (MappingException $e) {
165
            // Different entity manager without this entity configured in namespace chain. Ignore
166
        }
167
    }
168
169
    /**
170
     * Checks if a nodetranslation has children and update their url
171
     *
172
     * @param NodeTranslation        $node  The node
173
     * @param EntityManagerInterface $em    The entity manager
174
     * @param ClassMetadata          $class The class meta daat
175
     */
176
    private function updateNodeChildren(NodeTranslation $node, EntityManagerInterface $em, ClassMetadata $class)
177
    {
178
        $children = $node->getNode()->getChildren();
179
        if (\count($children) > 0) {
180
            /* @var Node $child */
181
            foreach ($children as $child) {
182
                $translation = $child->getNodeTranslation($node->getLang(), true);
183
                if ($translation) {
184
                    $translation = $this->updateUrl($translation, $em);
185
186 View Code Duplication
                    if ($translation !== false) {
187
                        $em->persist($translation);
188
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $translation);
189
190
                        $this->updateNodeChildren($translation, $em, $class);
191
                    }
192
                }
193
            }
194
        }
195
    }
196
197
    /**
198
     * Update the url for a nodetranslation
199
     *
200
     * @param NodeTranslation        $nodeTranslation The node translation
201
     * @param EntityManagerInterface $em              The entity manager
202
     *
203
     * @return NodeTranslation|bool returns the node when all is well because it has to be saved
204
     */
205
    private function updateUrl(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
206
    {
207
        $result = $this->ensureUniqueUrl($nodeTranslation, $em);
208
209
        if ($result) {
210
            return $nodeTranslation;
211
        }
212
213
        $this->logger->info(
214
            sprintf('Found NT %s needed NO change', $nodeTranslation->getId())
215
        );
216
217
        return false;
218
    }
219
220
    /**
221
     * A function that checks the URL and sees if it's unique.
222
     * It's allowed to be the same when the node is a StructureNode.
223
     * When a node is deleted it needs to be ignored in the check.
224
     * Offline nodes need to be included as well.
225
     *
226
     * It sluggifies the slug, updates the URL
227
     * and checks all existing NodeTranslations ([1]), excluding itself. If a
228
     * URL existsthat has the same url. If an existing one is found the slug is
229
     * modified, the URL is updated and the check is repeated until no prior
230
     * urls exist.
231
     *
232
     * NOTE: We need a way to tell if the slug has been modified or not.
233
     * NOTE: Would be cool if we could increment a number after the slug. Like
234
     * check if it matches -v# and increment the number.
235
     *
236
     * [1] For all languages for now. The issue is that we need a way to know
237
     * if a node's URL is prepended with the language or not. For now both
238
     * scenarios are possible so we check for all languages.
239
     *
240
     * @param NodeTranslation        $translation Reference to the NodeTranslation.
241
     *                                            This is modified in place.
242
     * @param EntityManagerInterface $em          The entity manager
243
     * @param array                  $flashes     The flash messages array
244
     *
245
     * @return bool
246
     */
247
    private function ensureUniqueUrl(NodeTranslation $translation, EntityManagerInterface $em, array $flashes = [])
248
    {
249
        // Can't use GetRef here yet since the NodeVersions aren't loaded yet for some reason.
250
        $nodeVersion = $translation->getPublicNodeVersion();
251
        $page = $em->getRepository($nodeVersion->getRefEntityName())
252
            ->find($nodeVersion->getRefId());
253
254
        if (null === $page) {
255
            return false;
256
        }
257
258
        $isStructureNode = $page->isStructureNode();
259
260
        // If it's a StructureNode the slug and url should be empty.
261
        if ($isStructureNode) {
262
            $translation->setSlug('');
263
            $translation->setUrl($translation->getFullSlug());
264
265
            return true;
266
        }
267
268
        /* @var NodeTranslationRepository $nodeTranslationRepository */
269
        $nodeTranslationRepository = $em->getRepository(NodeTranslation::class);
270
271
        if ($translation->getUrl() === $translation->getFullSlug()) {
272
            $this->logger->debug(
273
                sprintf(
274
                    'Evaluating URL for NT %s getUrl: "%s" getFullSlug: "%s"',
275
                    $translation->getId(),
276
                    $translation->getUrl(),
277
                    $translation->getFullSlug()
278
                )
279
            );
280
281
            return false;
282
        }
283
284
        // Adjust the URL.
285
        $translation->setUrl($translation->getFullSlug());
286
287
        // Find all translations with this new URL, whose nodes are not deleted.
288
        $translations = $nodeTranslationRepository->getAllNodeTranslationsForUrl(
289
            $translation->getUrl(),
290
            $translation->getLang(),
291
            false,
292
            $translation,
293
            $this->domainConfiguration->getRootNode()
294
        );
295
296
        $this->logger->debug(
297
            sprintf(
298
                'Found %s node(s) that math url "%s"',
299
                \count($translations),
300
                $translation->getUrl()
301
            )
302
        );
303
304
        $translationsWithSameUrl = [];
305
306
        /** @var NodeTranslation $trans */
307
        foreach ($translations as $trans) {
308
            if (!$this->pagesConfiguration->isStructureNode($trans->getPublicNodeVersion()->getRefEntityName())) {
309
                $translationsWithSameUrl[] = $trans;
310
            }
311
        }
312
313
        if (\count($translationsWithSameUrl) > 0) {
314
            $oldUrl = $translation->getFullSlug();
315
            $translation->setSlug(
316
                $this->slugifier->slugify(
317
                    $this->incrementString($translation->getSlug())
318
                )
319
            );
320
            $newUrl = $translation->getFullSlug();
321
322
            $message = sprintf(
323
                'The URL of the page has been changed from %s to %s since another page already uses this URL',
324
                $oldUrl,
325
                $newUrl
326
            );
327
            $this->logger->info($message);
328
            $flashes[] = $message;
329
330
            $this->ensureUniqueUrl($translation, $em, $flashes);
331
        } elseif (\count($flashes) > 0 && $this->isInRequestScope()) {
332
            // No translations found so we're certain we can show this message.
333
            $flash = current(\array_slice($flashes, -1));
334
            $this->flashBag->add(FlashTypes::WARNING, $flash);
335
        }
336
337
        return true;
338
    }
339
340
    /**
341
     * Increment a string that ends with a number.
342
     * If the string does not end in a number we'll add the append and then add
343
     * the first number.
344
     *
345
     * @param string $string the string we want to increment
346
     * @param string $append the part we want to append before we start adding
347
     *                       a number
348
     *
349
     * @return string incremented string
350
     */
351 View Code Duplication
    private function incrementString($string, $append = '-v')
352
    {
353
        $finalDigitGrabberRegex = '/\d+$/';
354
        $matches = [];
355
356
        preg_match($finalDigitGrabberRegex, $string, $matches);
357
358
        if (\count($matches) > 0) {
359
            $digit = (int) $matches[0];
360
            ++$digit;
361
362
            // Replace the integer with the new digit.
363
            return preg_replace($finalDigitGrabberRegex, $digit, $string);
364
        }
365
366
        return $string.$append.'1';
367
    }
368
369
    /**
370
     * @return bool
371
     */
372
    private function isInRequestScope()
373
    {
374
        return $this->requestStack && $this->requestStack->getCurrentRequest();
375
    }
376
}
377