|
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
|
|
|
* @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
|
|
|
} |
|
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) |
|
|
|
|
|
|
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) |
|
|
|
|
|
|
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); |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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 |
|
|
|
|
|
|
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') |
|
|
|
|
|
|
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() |
|
|
|
|
|
|
384
|
|
|
{ |
|
385
|
|
|
if ($this->flashBag instanceof SessionInterface) { |
|
386
|
|
|
return $this->flashBag->getFlashBag(); |
|
387
|
|
|
} |
|
388
|
|
|
|
|
389
|
|
|
return $this->flashBag; |
|
390
|
|
|
} |
|
391
|
|
|
} |
|
392
|
|
|
|
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
$italyis not defined by the methodfinale(...).The most likely cause is that the parameter was removed, but the annotation was not.