Skip to content

Commit 8657591

Browse files
[Sluggable] Use TranslationWalker for Translatable objects when looking for similar slugs (#2620)
* fix(Sluggable): Use TranslationWalker for Translatable object when looking for similar slugs Force translation walker to look for slug translations to avoid duplicated slugs. --- Fixes #100, fixes #2530 * chore: Added CHANGELOG notice * chore: Missing issue references to CHANGELOG entry * chore: Added Sluggable/Issue/Issue100Test * chore: Rewrite changelog entry and phpstan fix * feat(Sluggable): Added new option `uniqueOverTranslations` to enable uniqueness across translated slug as well * chore: Move `$uniqueOverTranslations` new argument at the end for BC * chore: Missing `uniqueOverTranslations` option in legacy annotation * docs: Moved Changelog entry to Unreleased section * Update Changelog * Fix PHPStan types * Fix tests * Make mandatory keys in SlugConfiguration * Add a check for YAML driver --------- Co-authored-by: Fran Moreno <[email protected]>
1 parent b31d2cf commit 8657591

File tree

13 files changed

+293
-41
lines changed

13 files changed

+293
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ a release.
2323
- Blameable: Allow ascii_string to validTypes (issue #2726)
2424
- Sluggable: Allow ascii_string to validTypes
2525
- IpTraceable: Allow ascii_string to validTypes
26+
- Sluggable: Use `TranslationWalker` hint when looking for similar slugs (`getSimilarSlugs` method) for entities which implement `Translatable` interface and have `uniqueOverTranslations: true` Slug option (#100, #2530)
2627

2728
## [3.15.0]
2829
### Added

doc/sluggable.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ echo $article->getSlug();
278278
- **suffix** (optional, default="") - suffix which will be added to the generated slug
279279
- **style** (optional, default="default") - **"default"** all letters will be lowercase, **"camel"** - first word letter will be uppercase, **"upper"**- all word letter will be uppercase and **"lower"**- all word letter will be lowercase
280280
- **handlers** (only available in annotations, optional, default=[]) - list of slug handlers, like tree path slug, or customized, for example see bellow
281+
- **uniqueOverTranslations** (optional, default=false) - **true** if slug should be unique over translations and if identical it will be suffixed, **false** - otherwise
281282

282283
**Note**: handlers are totally optional
283284

@@ -584,7 +585,7 @@ class Article
584585
/**
585586
* @Gedmo\Translatable
586587
* @Gedmo\Slug(fields={"title", "code"})
587-
* @ORM\Column(length=128, unique=true)
588+
* @ORM\Column(length=128, unique=true, uniqueOverTranslations=true)
588589
*/
589590
private $slug;
590591

@@ -595,7 +596,7 @@ class Article
595596

596597
/**
597598
* @Gedmo\Slug(fields={"uniqueTitle"}, prefix="some-prefix-")
598-
* @ORM\Column(type="string", length=128, unique=true)
599+
* @ORM\Column(type="string", length=128, unique=true, uniqueOverTranslations=true)
599600
*/
600601
private $uniqueSlug;
601602

@@ -642,6 +643,8 @@ Now the generated slug will be translated by Translatable behavior
642643

643644
<a name="slug-handlers"></a>
644645

646+
`uniqueOverTranslations` option is used to ensure that the slug is unique for each translated slugs.
647+
645648
## Using slug handlers:
646649

647650
There are built-in slug handlers like described in configuration options of slug, but there

src/Mapping/Annotation/Slug.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ final class Slug implements GedmoAnnotation
3838
public bool $updatable = true;
3939
public string $style = 'default'; // or "camel"
4040
public bool $unique = true;
41+
public bool $uniqueOverTranslations = false;
4142
/** @var string|null */
4243
public $unique_base;
4344
public string $separator = '-';
@@ -63,7 +64,8 @@ public function __construct(
6364
string $prefix = '',
6465
string $suffix = '',
6566
array $handlers = [],
66-
string $dateFormat = 'Y-m-d-H:i'
67+
string $dateFormat = 'Y-m-d-H:i',
68+
bool $uniqueOverTranslations = false
6769
) {
6870
if ([] !== $data) {
6971
Deprecation::trigger(
@@ -85,6 +87,7 @@ public function __construct(
8587
$this->suffix = $this->getAttributeValue($data, 'suffix', $args, 8, $suffix);
8688
$this->handlers = $this->getAttributeValue($data, 'handlers', $args, 9, $handlers);
8789
$this->dateFormat = $this->getAttributeValue($data, 'dateFormat', $args, 10, $dateFormat);
90+
$this->uniqueOverTranslations = $this->getAttributeValue($data, 'uniqueOverTranslations', $args, 11, $uniqueOverTranslations);
8891

8992
return;
9093
}
@@ -93,6 +96,7 @@ public function __construct(
9396
$this->updatable = $updatable;
9497
$this->style = $style;
9598
$this->unique = $unique;
99+
$this->uniqueOverTranslations = $uniqueOverTranslations;
96100
$this->unique_base = $unique_base;
97101
$this->separator = $separator;
98102
$this->prefix = $prefix;

src/Sluggable/Handler/SlugHandlerInterface.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
* Sluggable extension and should not be used elsewhere.
2121
*
2222
* @author Gediminas Morkevicius <[email protected]>
23+
*
24+
* @phpstan-import-type SlugConfiguration from SluggableListener
2325
*/
2426
interface SlugHandlerInterface
2527
{
@@ -32,10 +34,10 @@ public function __construct(SluggableListener $sluggable);
3234
* Hook on slug handlers before the decision is made whether
3335
* the slug needs to be recalculated.
3436
*
35-
* @param array<string, mixed> $config
36-
* @param object $object
37-
* @param string $slug
38-
* @param bool $needToChangeSlug
37+
* @param SlugConfiguration $config
38+
* @param object $object
39+
* @param string $slug
40+
* @param bool $needToChangeSlug
3941
*
4042
* @return void
4143
*/
@@ -44,9 +46,9 @@ public function onChangeDecision(SluggableAdapter $ea, array &$config, $object,
4446
/**
4547
* Hook on slug handlers called after the slug is built.
4648
*
47-
* @param array<string, mixed> $config
48-
* @param object $object
49-
* @param string $slug
49+
* @param SlugConfiguration $config
50+
* @param object $object
51+
* @param string $slug
5052
*
5153
* @return void
5254
*/
@@ -55,9 +57,9 @@ public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$s
5557
/**
5658
* Hook for slug handlers called after the slug is completed.
5759
*
58-
* @param array<string, mixed> $config
59-
* @param object $object
60-
* @param string $slug
60+
* @param SlugConfiguration $config
61+
* @param object $object
62+
* @param string $slug
6163
*
6264
* @return void
6365
*/

src/Sluggable/Handler/SlugHandlerWithUniqueCallbackInterface.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,25 @@
1010
namespace Gedmo\Sluggable\Handler;
1111

1212
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
13+
use Gedmo\Sluggable\SluggableListener;
1314

1415
/**
1516
* This adds the ability for a slug handler to change the slug just before its
1617
* uniqueness is ensured. It is also called if the unique options are _not_
1718
* set.
1819
*
1920
* @author Gediminas Morkevicius <[email protected]>
21+
*
22+
* @phpstan-import-type SlugConfiguration from SluggableListener
2023
*/
2124
interface SlugHandlerWithUniqueCallbackInterface extends SlugHandlerInterface
2225
{
2326
/**
2427
* Hook for slug handlers called before it is made unique.
2528
*
26-
* @param array<string, mixed> $config
27-
* @param object $object
28-
* @param string $slug
29+
* @param SlugConfiguration $config
30+
* @param object $object
31+
* @param string $slug
2932
*
3033
* @return void
3134
*/

src/Sluggable/Mapping/Driver/Annotation.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,18 @@ private function retrieveSlug(ClassMetadata $meta, array &$config, \ReflectionPr
174174
if (!is_bool($slug->unique)) {
175175
throw new InvalidMappingException("Slug annotation [unique], type is not valid and must be 'boolean' in class - {$meta->getName()}");
176176
}
177+
if (!is_bool($slug->uniqueOverTranslations)) {
178+
throw new InvalidMappingException("Slug annotation [uniqueOverTranslations], type is not valid and must be 'boolean' in class - {$meta->getName()}");
179+
}
177180
if ([] !== $meta->getIdentifier() && $meta->isIdentifier($fieldName) && !(bool) $slug->unique) {
178181
throw new InvalidMappingException("Identifier field - [{$fieldName}] slug must be unique in order to maintain primary key in class - {$meta->getName()}");
179182
}
180183
if (false === $slug->unique && $slug->unique_base) {
181184
throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'");
182185
}
186+
if (false === $slug->unique && $slug->uniqueOverTranslations) {
187+
throw new InvalidMappingException("Slug annotation [uniqueOverTranslations] can not be set if unique is unset or 'false'");
188+
}
183189
if ($slug->unique_base && !$meta->hasField($slug->unique_base) && !$meta->hasAssociation($slug->unique_base)) {
184190
throw new InvalidMappingException("Unable to find [{$slug->unique_base}] as mapped property in entity - {$meta->getName()}");
185191
}
@@ -201,6 +207,7 @@ private function retrieveSlug(ClassMetadata $meta, array &$config, \ReflectionPr
201207
'prefix' => $slug->prefix,
202208
'suffix' => $slug->suffix,
203209
'handlers' => $handlers,
210+
'uniqueOverTranslations' => $slug->uniqueOverTranslations,
204211
];
205212

206213
return $config;

src/Sluggable/Mapping/Driver/Xml.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ private function buildFieldConfiguration(ClassMetadata $meta, string $field, \Si
144144
'suffix' => $this->_isAttributeSet($slug, 'suffix') ?
145145
$this->_getAttribute($slug, 'suffix') : '',
146146
'handlers' => $handlers,
147+
'uniqueOverTranslations' => $this->_isAttributeSet($slug, 'uniqueOverTranslations') ?
148+
$this->_getBooleanAttribute($slug, 'uniqueOverTranslations') : false,
147149
];
148150
if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) {
149151
throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->getName()}");

src/Sluggable/Mapping/Event/Adapter/ORM.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
namespace Gedmo\Sluggable\Mapping\Event\Adapter;
1111

1212
use Doctrine\ORM\Mapping\ClassMetadataInfo;
13+
use Doctrine\ORM\Query;
1314
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
1415
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
1516
use Gedmo\Tool\Wrapper\AbstractWrapper;
1617
use Gedmo\Tool\Wrapper\EntityWrapper;
18+
use Gedmo\Translatable\Query\TreeWalker\TranslationWalker;
19+
use Gedmo\Translatable\Translatable;
1720

1821
/**
1922
* Doctrine event adapter for ORM adapted
@@ -76,7 +79,18 @@ public function getSimilarSlugs($object, $meta, array $config, $slug)
7679
}
7780
}
7881

79-
return $qb->getQuery()->getArrayResult();
82+
$query = $qb->getQuery();
83+
$query->setHydrationMode(Query::HYDRATE_ARRAY);
84+
// Force translation walker to look for slug translations to avoid duplicated slugs
85+
// TODO: Remove isset when removing support of YAML driver
86+
if (isset($config['uniqueOverTranslations']) && $config['uniqueOverTranslations'] && $object instanceof Translatable) {
87+
$query->setHint(
88+
Query::HINT_CUSTOM_OUTPUT_WALKER,
89+
TranslationWalker::class
90+
);
91+
}
92+
93+
return $query->getArrayResult();
8094
}
8195

8296
public function replaceRelative($object, array $config, $target, $replacement)

src/Sluggable/Mapping/Event/SluggableAdapter.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* @author Gediminas Morkevicius <[email protected]>
2020
*
2121
* @phpstan-import-type SluggableConfiguration from SluggableListener
22+
* @phpstan-import-type SlugConfiguration from SluggableListener
2223
*/
2324
interface SluggableAdapter extends AdapterInterface
2425
{
@@ -29,7 +30,7 @@ interface SluggableAdapter extends AdapterInterface
2930
* @param ClassMetadata $meta
3031
* @param string $slug
3132
*
32-
* @phpstan-param SluggableConfiguration $config
33+
* @phpstan-param SlugConfiguration $config
3334
*
3435
* @return array<int, array<string, mixed>>
3536
*/
@@ -42,7 +43,7 @@ public function getSimilarSlugs($object, $meta, array $config, $slug);
4243
* @param string $target
4344
* @param string $replacement
4445
*
45-
* @phpstan-param SluggableConfiguration $config
46+
* @phpstan-param SlugConfiguration $config
4647
*
4748
* @return int the number of updated records
4849
*/

src/Sluggable/Sluggable.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface Sluggable
3535
* suffix (optional, default="") - suffix which will be added to the generated slug
3636
* style (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase
3737
* dateFormat (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase
38+
* uniqueOverTranslations (optional, default=false) - true if slug should be unique over translations and if identical it will be prefixed, false - otherwise
3839
*
3940
* example:
4041
*

0 commit comments

Comments
 (0)