Completed
Push — master ( 6d6774...64f3ed )
by Jeroen
11:23 queued 05:13
created

EventListener/NodeTranslationListener.php (1 issue)

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