Completed
Push — master ( 3fc8ad...9eac4c )
by Jeroen
143:21 queued 137:13
created

NodeTranslationListener::getFlashBag()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 7
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 6
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
Bug introduced by
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
     * @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);
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...
69
        }
70
71
        $this->logger = $logger;
72
        $this->slugifier = $slugifier;
73
        $this->requestStack = $requestStack;
74
        $this->domainConfiguration = $domainConfiguration;
75
        $this->pagesConfiguration = $pagesConfiguration;
76
    }
77
78
    /**
79
     * @param LifecycleEventArgs $args
80
     */
81 View Code Duplication
    public function prePersist(LifecycleEventArgs $args)
0 ignored issues
show
Duplication introduced by
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...
82
    {
83
        $entity = $args->getEntity();
84
85
        if ($entity instanceof NodeTranslation) {
86
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
87
            $this->ensureSlugIsSlugified($entity);
88
        }
89
    }
90
91
    /**
92
     * @param LifecycleEventArgs $args
93
     */
94 View Code Duplication
    public function preUpdate(LifecycleEventArgs $args)
0 ignored issues
show
Duplication introduced by
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...
95
    {
96
        $entity = $args->getEntity();
97
98
        if ($entity instanceof NodeTranslation) {
99
            $this->setSlugWhenEmpty($entity, $args->getEntityManager());
100
            $this->ensureSlugIsSlugified($entity);
101
        }
102
    }
103
104
    /**
105
     * @param NodeTranslation        $nodeTranslation
106
     * @param EntityManagerInterface $em
107
     */
108
    private function setSlugWhenEmpty(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
109
    {
110
        $publicNode = $nodeTranslation->getRef($em);
0 ignored issues
show
Compatibility introduced by
$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...
111
112
        // Do nothing for StructureNode objects, skip.
113
        if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
114
            return;
115
        }
116
117
        // If no slug is set and no structure node, apply title as slug.
118 View Code Duplication
        if ($nodeTranslation->getSlug() === null && $nodeTranslation->getNode()->getParent() !== null) {
0 ignored issues
show
Duplication introduced by
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...
119
            $nodeTranslation->setSlug(
120
                $this->slugifier->slugify($nodeTranslation->getTitle())
121
            );
122
        }
123
    }
124
125
    /**
126
     * @param NodeTranslation $nodeTranslation
127
     */
128
    private function ensureSlugIsSlugified(NodeTranslation $nodeTranslation)
129
    {
130
        if ($nodeTranslation->getSlug() !== null) {
131
            $nodeTranslation->setSlug(
132
                $this->slugifier->slugify($nodeTranslation->getSlug())
133
            );
134
        }
135
    }
136
137
    /**
138
     * OnFlush doctrine event - updates the nodetranslation urls if needed
139
     *
140
     * @param OnFlushEventArgs $args
141
     */
142
    public function onFlush(OnFlushEventArgs $args)
143
    {
144
        try {
145
            $em = $args->getEntityManager();
146
147
            $class = $em->getClassMetadata(NodeTranslation::class);
148
149
            // Collect all nodetranslations that are updated
150
            foreach ($em->getUnitOfWork()->getScheduledEntityUpdates() as $entity) {
151
                if ($entity instanceof NodeTranslation) {
152
                    /** @var Node $publicNode */
153
                    $publicNode = $entity->getPublicNodeVersion()->getRef($em);
154
155
                    // Do nothing for StructureNode objects, skip.
156
                    if ($publicNode instanceof HasNodeInterface && $publicNode->isStructureNode()) {
157
                        continue;
158
                    }
159
160
                    $entity = $this->updateUrl($entity, $em);
161
162 View Code Duplication
                    if ($entity !== false) {
0 ignored issues
show
Duplication introduced by
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...
163
                        $em->persist($entity);
164
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $entity);
165
166
                        $this->updateNodeChildren($entity, $em, $class);
167
                    }
168
                }
169
            }
170
        } catch (MappingException $e) {
171
            // Different entity manager without this entity configured in namespace chain. Ignore
172
        }
173
    }
174
175
    /**
176
     * Checks if a nodetranslation has children and update their url
177
     *
178
     * @param NodeTranslation        $node  The node
179
     * @param EntityManagerInterface $em    The entity manager
180
     * @param ClassMetadata          $class The class meta daat
181
     */
182
    private function updateNodeChildren(NodeTranslation $node, EntityManagerInterface $em, ClassMetadata $class)
183
    {
184
        $children = $node->getNode()->getChildren();
185
        if (\count($children) > 0) {
186
            /* @var Node $child */
187
            foreach ($children as $child) {
188
                $translation = $child->getNodeTranslation($node->getLang(), true);
189
                if ($translation) {
190
                    $translation = $this->updateUrl($translation, $em);
191
192 View Code Duplication
                    if ($translation !== false) {
0 ignored issues
show
Duplication introduced by
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...
193
                        $em->persist($translation);
194
                        $em->getUnitOfWork()->recomputeSingleEntityChangeSet($class, $translation);
195
196
                        $this->updateNodeChildren($translation, $em, $class);
197
                    }
198
                }
199
            }
200
        }
201
    }
202
203
    /**
204
     * Update the url for a nodetranslation
205
     *
206
     * @param NodeTranslation        $nodeTranslation The node translation
207
     * @param EntityManagerInterface $em              The entity manager
208
     *
209
     * @return NodeTranslation|bool returns the node when all is well because it has to be saved
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use NodeTranslation|false.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
210
     */
211
    private function updateUrl(NodeTranslation $nodeTranslation, EntityManagerInterface $em)
212
    {
213
        $result = $this->ensureUniqueUrl($nodeTranslation, $em);
214
215
        if ($result) {
216
            return $nodeTranslation;
217
        }
218
219
        $this->logger->info(
220
            sprintf('Found NT %s needed NO change', $nodeTranslation->getId())
221
        );
222
223
        return false;
224
    }
225
226
    /**
227
     * A function that checks the URL and sees if it's unique.
228
     * It's allowed to be the same when the node is a StructureNode.
229
     * When a node is deleted it needs to be ignored in the check.
230
     * Offline nodes need to be included as well.
231
     *
232
     * It sluggifies the slug, updates the URL
233
     * and checks all existing NodeTranslations ([1]), excluding itself. If a
234
     * URL existsthat has the same url. If an existing one is found the slug is
235
     * modified, the URL is updated and the check is repeated until no prior
236
     * urls exist.
237
     *
238
     * NOTE: We need a way to tell if the slug has been modified or not.
239
     * NOTE: Would be cool if we could increment a number after the slug. Like
240
     * check if it matches -v# and increment the number.
241
     *
242
     * [1] For all languages for now. The issue is that we need a way to know
243
     * if a node's URL is prepended with the language or not. For now both
244
     * scenarios are possible so we check for all languages.
245
     *
246
     * @param NodeTranslation        $translation Reference to the NodeTranslation.
247
     *                                            This is modified in place.
248
     * @param EntityManagerInterface $em          The entity manager
249
     * @param array                  $flashes     The flash messages array
250
     *
251
     * @return bool
252
     */
253
    private function ensureUniqueUrl(NodeTranslation $translation, EntityManagerInterface $em, array $flashes = [])
254
    {
255
        // Can't use GetRef here yet since the NodeVersions aren't loaded yet for some reason.
256
        $nodeVersion = $translation->getPublicNodeVersion();
257
        $page = $em->getRepository($nodeVersion->getRefEntityName())
258
            ->find($nodeVersion->getRefId());
259
260
        if (null === $page) {
261
            return false;
262
        }
263
264
        $isStructureNode = $page->isStructureNode();
265
266
        // If it's a StructureNode the slug and url should be empty.
267
        if ($isStructureNode) {
268
            $translation->setSlug('');
269
            $translation->setUrl($translation->getFullSlug());
270
271
            return true;
272
        }
273
274
        /* @var NodeTranslationRepository $nodeTranslationRepository */
275
        $nodeTranslationRepository = $em->getRepository(NodeTranslation::class);
276
277
        if ($translation->getUrl() === $translation->getFullSlug()) {
278
            $this->logger->debug(
279
                sprintf(
280
                    'Evaluating URL for NT %s getUrl: "%s" getFullSlug: "%s"',
281
                    $translation->getId(),
282
                    $translation->getUrl(),
283
                    $translation->getFullSlug()
284
                )
285
            );
286
287
            return false;
288
        }
289
290
        // Adjust the URL.
291
        $translation->setUrl($translation->getFullSlug());
292
293
        // Find all translations with this new URL, whose nodes are not deleted.
294
        $translations = $nodeTranslationRepository->getAllNodeTranslationsForUrl(
295
            $translation->getUrl(),
296
            $translation->getLang(),
297
            false,
298
            $translation,
299
            $this->domainConfiguration->getRootNode()
300
        );
301
302
        $this->logger->debug(
303
            sprintf(
304
                'Found %s node(s) that math url "%s"',
305
                \count($translations),
306
                $translation->getUrl()
307
            )
308
        );
309
310
        $translationsWithSameUrl = [];
311
312
        /** @var NodeTranslation $trans */
313
        foreach ($translations as $trans) {
314
            if (!$this->pagesConfiguration->isStructureNode($trans->getPublicNodeVersion()->getRefEntityName())) {
315
                $translationsWithSameUrl[] = $trans;
316
            }
317
        }
318
319
        if (\count($translationsWithSameUrl) > 0) {
320
            $oldUrl = $translation->getFullSlug();
321
            $translation->setSlug(
322
                $this->slugifier->slugify(
323
                    $this->incrementString($translation->getSlug())
324
                )
325
            );
326
            $newUrl = $translation->getFullSlug();
327
328
            $message = sprintf(
329
                'The URL of the page has been changed from %s to %s since another page already uses this URL',
330
                $oldUrl,
331
                $newUrl
332
            );
333
            $this->logger->info($message);
334
            $flashes[] = $message;
335
336
            $this->ensureUniqueUrl($translation, $em, $flashes);
337
        } elseif (\count($flashes) > 0 && $this->isInRequestScope()) {
338
            // No translations found so we're certain we can show this message.
339
            $flash = current(\array_slice($flashes, -1));
340
            $this->getFlashBag()->add(FlashTypes::WARNING, $flash);
341
        }
342
343
        return true;
344
    }
345
346
    /**
347
     * Increment a string that ends with a number.
348
     * If the string does not end in a number we'll add the append and then add
349
     * the first number.
350
     *
351
     * @param string $string the string we want to increment
352
     * @param string $append the part we want to append before we start adding
353
     *                       a number
354
     *
355
     * @return string incremented string
356
     */
357 View Code Duplication
    private function incrementString($string, $append = '-v')
0 ignored issues
show
Duplication introduced by
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...
358
    {
359
        $finalDigitGrabberRegex = '/\d+$/';
360
        $matches = [];
361
362
        preg_match($finalDigitGrabberRegex, $string, $matches);
363
364
        if (\count($matches) > 0) {
365
            $digit = (int) $matches[0];
366
            ++$digit;
367
368
            // Replace the integer with the new digit.
369
            return preg_replace($finalDigitGrabberRegex, $digit, $string);
370
        }
371
372
        return $string.$append.'1';
373
    }
374
375
    /**
376
     * @return bool
377
     */
378
    private function isInRequestScope()
379
    {
380
        return $this->requestStack && $this->requestStack->getCurrentRequest();
381
    }
382
383
    private function getFlashBag()
0 ignored issues
show
Documentation introduced by
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...
384
    {
385
        if ($this->flashBag instanceof SessionInterface) {
386
            return $this->flashBag->getFlashBag();
387
        }
388
389
        return $this->flashBag;
390
    }
391
}
392