Completed
Push — master ( 06c1ce...67d37c )
by Jeroen
06:20
created

NodeTranslationListener   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 358
Duplicated Lines 15.08 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 17
dl 54
loc 358
ccs 0
cts 191
cp 0
rs 9.1199
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 22 3
A prePersist() 9 9 2
A preUpdate() 9 9 2
A setSlugWhenEmpty() 6 16 5
A ensureSlugIsSlugified() 0 8 2
B onFlush() 6 32 7
A updateNodeChildren() 6 20 5
A updateUrl() 0 14 2
C ensureUniqueUrl() 0 92 9
A incrementString() 17 17 2
A isInRequestScope() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like NodeTranslationListener often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use NodeTranslationListener, and based on these observations, apply Extract Interface, too.

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
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
        } 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)
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...
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)
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...
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);
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...
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) {
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...
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
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...
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) {
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...
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
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...
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')
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...
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