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

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
364
    {
365
        if ($this->flashBag instanceof SessionInterface) {
366
            return $this->flashBag->getFlashBag();
367
        }
368
369
        return $this->flashBag;
370
    }
371
}
372