Completed
Pull Request — 5.6 (#2830)
by Jeroen
14:14
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
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
0 ignored issues
show
There is no parameter named $session. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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