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) { |
|
|
|
|
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
|
|
|
|
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 methodfinale(...)
.The most likely cause is that the parameter was removed, but the annotation was not.