diff --git a/CHANGELOG.md b/CHANGELOG.md index e6fd39c05c..775afd0f1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ a release. ## [Unreleased] +### Changed +- Replace the `behat/transliterator` with `symfony/string` for the Sluggable extension for its default transliteration and urlization steps" + ## [3.20.0] - 2025-04-04 ### Fixed - SoftDeleteable: Resolved a bug where a soft-deleted object isn't remove from the ObjectManager (#2930) diff --git a/composer.json b/composer.json index eabf64906c..bbf9357ad5 100644 --- a/composer.json +++ b/composer.json @@ -41,16 +41,17 @@ }, "require": { "php": "^7.4 || ^8.0", - "behat/transliterator": "^1.2", "doctrine/collections": "^1.2 || ^2.0", "doctrine/deprecations": "^1.0", "doctrine/event-manager": "^1.2 || ^2.0", "doctrine/persistence": "^2.2 || ^3.0 || ^4.0", "psr/cache": "^1 || ^2 || ^3", "psr/clock": "^1", - "symfony/cache": "^5.4 || ^6.0 || ^7.0" + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/string": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { + "behat/transliterator": "^1.2", "doctrine/annotations": "^1.13 || ^2.0", "doctrine/cache": "^1.11 || ^2.0", "doctrine/common": "^2.13 || ^3.0", @@ -72,6 +73,7 @@ "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "conflict": { + "behat/transliterator": "<1.2 || >=2.0", "doctrine/annotations": "<1.13 || >=3.0", "doctrine/common": "<2.13 || >=4.0", "doctrine/dbal": "<3.7 || >=5.0", diff --git a/src/Sluggable/Handler/InversedRelativeSlugHandler.php b/src/Sluggable/Handler/InversedRelativeSlugHandler.php index c68f191aba..b8ac0483bb 100644 --- a/src/Sluggable/Handler/InversedRelativeSlugHandler.php +++ b/src/Sluggable/Handler/InversedRelativeSlugHandler.php @@ -38,14 +38,6 @@ class InversedRelativeSlugHandler implements SlugHandlerInterface */ protected $sluggable; - /** - * $options = array( - * 'relationClass' => 'objectclass', - * 'inverseSlugField' => 'slug', - * 'mappedBy' => 'relationField' - * ) - * {@inheritdoc} - */ public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; diff --git a/src/Sluggable/Handler/RelativeSlugHandler.php b/src/Sluggable/Handler/RelativeSlugHandler.php index 802806c651..2fdac40b9a 100644 --- a/src/Sluggable/Handler/RelativeSlugHandler.php +++ b/src/Sluggable/Handler/RelativeSlugHandler.php @@ -45,24 +45,15 @@ class RelativeSlugHandler implements SlugHandlerInterface * * @var array */ - private $usedOptions; + private array $usedOptions = []; /** - * Callable of original transliterator - * which is used by sluggable + * Callable of original transliterator which is used by the sluggable listener. * - * @var callable + * @var callable(string, string, object): string */ private $originalTransliterator; - /** - * $options = array( - * 'separator' => '/', - * 'relationField' => 'something', - * 'relationSlugField' => 'slug' - * ) - * {@inheritdoc} - */ public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; @@ -120,6 +111,10 @@ public function transliterate($text, $separator, $object) $this->originalTransliterator, [$text, $separator, $object] ); + $result = call_user_func_array( + $this->sluggable->getUrlizer(), + [$result, $separator, $object] + ); $wrapped = AbstractWrapper::wrap($object, $this->om); $relation = $wrapped->getPropertyValue($this->usedOptions['relationField']); if ($relation) { @@ -135,6 +130,7 @@ public function transliterate($text, $separator, $object) $result = $slug.$this->usedOptions['separator'].$result; } + $this->sluggable->setTransliterator($this->originalTransliterator); return $result; diff --git a/src/Sluggable/Handler/TreeSlugHandler.php b/src/Sluggable/Handler/TreeSlugHandler.php index 8ddbb97635..65616da7dd 100644 --- a/src/Sluggable/Handler/TreeSlugHandler.php +++ b/src/Sluggable/Handler/TreeSlugHandler.php @@ -17,6 +17,8 @@ use Gedmo\Sluggable\SluggableListener; use Gedmo\Tool\Wrapper\AbstractWrapper; +use function Symfony\Component\String\u; + /** * Sluggable handler which slugs all parent nodes * recursively and synchronizes on updates. For instance @@ -40,36 +42,24 @@ class TreeSlugHandler implements SlugHandlerWithUniqueCallbackInterface */ protected $sluggable; - /** - * @var string - */ - private $prefix; + private string $prefix = ''; - /** - * @var string - */ - private $suffix; + private string $suffix = ''; /** * True if node is being inserted - * - * @var bool */ - private $isInsert = false; + private bool $isInsert = false; /** * Transliterated parent slug - * - * @var string */ - private $parentSlug; + private string $parentSlug = ''; /** * Used path separator - * - * @var string */ - private $usedPathSeparator; + private string $usedPathSeparator = self::SEPARATOR; public function __construct(SluggableListener $sluggable) { @@ -106,11 +96,7 @@ public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$s // if needed, remove suffix from parentSlug, so we can use it to prepend it to our slug if (isset($options['suffix'])) { - $suffix = $options['suffix']; - - if (substr($this->parentSlug, -strlen($suffix)) === $suffix) { // endsWith - $this->parentSlug = substr_replace($this->parentSlug, '', -1 * strlen($suffix)); - } + $this->parentSlug = u($this->parentSlug)->trimSuffix($options['suffix'])->toString(); } } } diff --git a/src/Sluggable/SluggableListener.php b/src/Sluggable/SluggableListener.php index 485804505a..1f0df30657 100644 --- a/src/Sluggable/SluggableListener.php +++ b/src/Sluggable/SluggableListener.php @@ -20,7 +20,9 @@ use Gedmo\Sluggable\Handler\SlugHandlerInterface; use Gedmo\Sluggable\Handler\SlugHandlerWithUniqueCallbackInterface; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; -use Gedmo\Sluggable\Util\Urlizer; +use Symfony\Component\String\Slugger\AsciiSlugger; + +use function Symfony\Component\String\u; /** * The SluggableListener handles the generation of slugs @@ -60,6 +62,7 @@ * relationField?: string, * relationSlugField?: string, * separator?: string, + * urilize?: bool, * }>, * uniqueOverTranslations: bool, * useObjectClass?: class-string, @@ -78,20 +81,16 @@ class SluggableListener extends MappedEventSubscriber /** * Transliteration callback for slugs * - * @var callable - * - * @phpstan-var callable(string $text, string $separator, object $object): string + * @var callable(string, string, object): string */ - private $transliterator = [Urlizer::class, 'transliterate']; + private $transliterator; /** * Urlize callback for slugs * - * @var callable - * - * @phpstan-var callable(string $text, string $separator, object $object): string + * @var callable(string, string, object): string */ - private $urlizer = [Urlizer::class, 'urlize']; + private $urlizer; /** * List of inserted slugs for each object class. @@ -121,6 +120,28 @@ class SluggableListener extends MappedEventSubscriber */ private array $managedFilters = []; + public function __construct() + { + parent::__construct(); + + $this->setTransliterator( + static fn (string $text, string $separator, object $object): string => u($text)->ascii()->toString() + ); + + /* + * Note - Requiring the call to `lower()` in this chain contradicts with the `style` configuration + * which doesn't require or enforce lowercase styling by default, but the Behat transliterator applied + * this styling so it is used for B/C + */ + + $this->setUrlizer( + static fn (string $text, string $separator, object $object): string => (new AsciiSlugger()) + ->slug($text, $separator) + ->lower() + ->toString() + ); + } + /** * Specifies the list of events to listen * @@ -412,25 +433,21 @@ private function generateSlug(SluggableAdapter $ea, object $object): void switch ($options['style']) { case 'camel': $quotedSeparator = preg_quote($options['separator']); - $slug = preg_replace_callback('/^[a-z]|'.$quotedSeparator.'[a-z]/smi', static fn ($m) => strtoupper($m[0]), $slug); + $slug = preg_replace_callback( + '/^[a-z]|'.$quotedSeparator.'[a-z]/smi', + static fn (array $m): string => u($m[0])->upper()->toString(), + $slug + ); break; case 'lower': - if (function_exists('mb_strtolower')) { - $slug = mb_strtolower($slug); - } else { - $slug = strtolower($slug); - } + $slug = u($slug)->lower()->toString(); break; case 'upper': - if (function_exists('mb_strtoupper')) { - $slug = mb_strtoupper($slug); - } else { - $slug = strtoupper($slug); - } + $slug = u($slug)->upper()->toString(); break; diff --git a/src/Sluggable/Util/Urlizer.php b/src/Sluggable/Util/Urlizer.php index 33b38cd5f0..ba3d0710a6 100644 --- a/src/Sluggable/Util/Urlizer.php +++ b/src/Sluggable/Util/Urlizer.php @@ -10,10 +10,17 @@ namespace Gedmo\Sluggable\Util; use Behat\Transliterator\Transliterator; +use Gedmo\Exception\RuntimeException; + +if (!class_exists(Transliterator::class)) { + throw new RuntimeException(sprintf('Cannot use the "%s" class when the "behat/transliterator" package is not installed.', Urlizer::class)); +} /** * Transliteration utility * + * @deprecated since gedmo/doctrine-extensions 3.21, will be removed in version 4.0. + * * @final since gedmo/doctrine-extensions 3.11 */ class Urlizer extends Transliterator diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php b/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php index b12927f05d..8707e7e4ee 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/People/Occupation.php @@ -30,8 +30,6 @@ class Occupation { /** - * @var int|null - * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") @@ -39,7 +37,7 @@ class Occupation #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: Types::INTEGER)] - private $id; + private ?int $id = null; /** * @ORM\Column(length=64) @@ -48,8 +46,6 @@ class Occupation private ?string $title = null; /** - * @var string|null - * * @Gedmo\Slug(handlers={ * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={ * @Gedmo\SlugHandlerOption(name="parentRelationField", value="parent"), @@ -68,7 +64,7 @@ class Occupation #[Gedmo\SlugHandler(class: TreeSlugHandler::class, options: ['parentRelationField' => 'parent', 'separator' => '/'])] #[Gedmo\SlugHandler(class: InversedRelativeSlugHandler::class, options: ['relationClass' => Person::class, 'mappedBy' => 'occupation', 'inverseSlugField' => 'slug'])] #[ORM\Column(length: 64, unique: true)] - private $slug; + private ?string $slug = null; /** * @Gedmo\TreeParent @@ -87,48 +83,40 @@ class Occupation private Collection $children; /** - * @var int|null - * * @Gedmo\TreeLeft * * @ORM\Column(type="integer") */ #[ORM\Column(type: Types::INTEGER)] #[Gedmo\TreeLeft] - private $lft; + private ?int $lft = null; /** - * @var int|null - * * @Gedmo\TreeRight * * @ORM\Column(type="integer") */ #[ORM\Column(type: Types::INTEGER)] #[Gedmo\TreeRight] - private $rgt; + private ?int $rgt = null; /** - * @var int|null - * * @Gedmo\TreeRoot * * @ORM\Column(type="integer") */ #[ORM\Column(type: Types::INTEGER)] #[Gedmo\TreeRoot] - private $root; + private ?int $root = null; /** - * @var int|null - * * @Gedmo\TreeLevel * * @ORM\Column(name="lvl", type="integer") */ #[ORM\Column(name: 'lvl', type: Types::INTEGER)] #[Gedmo\TreeLevel] - private $level; + private ?int $level = null; public function __construct() { diff --git a/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php b/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php index f793c4944a..2920aacd39 100644 --- a/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php +++ b/tests/Gedmo/Sluggable/Fixture/Handler/People/Person.php @@ -23,8 +23,6 @@ class Person { /** - * @var int|null - * * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") @@ -32,7 +30,7 @@ class Person #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: Types::INTEGER)] - private $id; + private ?int $id = null; /** * @ORM\Column(length=64) @@ -41,8 +39,6 @@ class Person private ?string $name = null; /** - * @var string|null - * * @Gedmo\Slug(handlers={ * @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={ * @Gedmo\SlugHandlerOption(name="relationField", value="occupation"), @@ -56,7 +52,7 @@ class Person #[Gedmo\Slug(separator: '-', updatable: true, fields: ['name'])] #[Gedmo\SlugHandler(class: RelativeSlugHandler::class, options: ['relationField' => 'occupation', 'relationSlugField' => 'slug', 'separator' => '/'])] #[ORM\Column(name: 'slug', type: Types::STRING, length: 64, unique: true)] - private $slug; + private ?string $slug = null; /** * @ORM\ManyToOne(targetEntity="Occupation") diff --git a/tests/Gedmo/Sluggable/TransliterationTest.php b/tests/Gedmo/Sluggable/TransliterationTest.php index d3dbdbeebc..45c8fe9fd0 100644 --- a/tests/Gedmo/Sluggable/TransliterationTest.php +++ b/tests/Gedmo/Sluggable/TransliterationTest.php @@ -45,7 +45,7 @@ public function testInsertedNewSlug(): void static::assertSame('tova-e-testovo-zaglavie-bg', $bulgarian->getSlug()); $russian = $repo->findOneBy(['code' => 'ru']); - static::assertSame('eto-testovyi-zagolovok-ru', $russian->getSlug()); + static::assertSame('eto-testovyj-zagolovok-ru', $russian->getSlug()); $german = $repo->findOneBy(['code' => 'de']); static::assertSame('fuhren-aktivitaten-haglofs-de', $german->getSlug());