[String] Introduce a locale-aware Slugger in the String component with FrameworkBundle wiring
This commit is contained in:
parent
4a547c5663
commit
056d8ceed9
@ -19,6 +19,7 @@ CHANGELOG
|
|||||||
* Removed `SecurityUserValueResolver`, use `UserValueResolver` instead
|
* Removed `SecurityUserValueResolver`, use `UserValueResolver` instead
|
||||||
* Removed `routing.loader.service`.
|
* Removed `routing.loader.service`.
|
||||||
* Service route loaders must be tagged with `routing.route_loader`.
|
* Service route loaders must be tagged with `routing.route_loader`.
|
||||||
|
* Added `slugger` service and `SluggerInterface` alias
|
||||||
|
|
||||||
4.4.0
|
4.4.0
|
||||||
-----
|
-----
|
||||||
|
@ -105,6 +105,7 @@ use Symfony\Component\Serializer\Encoder\EncoderInterface;
|
|||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Symfony\Component\Stopwatch\Stopwatch;
|
use Symfony\Component\Stopwatch\Stopwatch;
|
||||||
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||||
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
||||||
use Symfony\Component\Translation\Translator;
|
use Symfony\Component\Translation\Translator;
|
||||||
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
@ -194,6 +195,12 @@ class FrameworkExtension extends Extension
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the slugger is used but the String component is not available, we should throw an error
|
||||||
|
if (!class_exists(SluggerInterface::class)) {
|
||||||
|
$container->register('slugger', 'stdClass')
|
||||||
|
->addError('You cannot use the "slugger" since the String component is not installed. Try running "composer require symfony/string".');
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($config['secret'])) {
|
if (isset($config['secret'])) {
|
||||||
$container->setParameter('kernel.secret', $config['secret']);
|
$container->setParameter('kernel.secret', $config['secret']);
|
||||||
}
|
}
|
||||||
|
@ -120,5 +120,11 @@
|
|||||||
<argument type="service" id="request_stack" />
|
<argument type="service" id="request_stack" />
|
||||||
<tag name="kernel.event_subscriber" />
|
<tag name="kernel.event_subscriber" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="slugger" class="Symfony\Component\String\Slugger\AsciiSlugger">
|
||||||
|
<argument>%kernel.default_locale%</argument>
|
||||||
|
<tag name="kernel.locale_aware" />
|
||||||
|
</service>
|
||||||
|
<service id="Symfony\Component\String\Slugger\SluggerInterface" alias="slugger" />
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger;
|
||||||
|
|
||||||
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||||
|
|
||||||
|
class SlugConstructArgService
|
||||||
|
{
|
||||||
|
private $slugger;
|
||||||
|
|
||||||
|
public function __construct(SluggerInterface $slugger)
|
||||||
|
{
|
||||||
|
$this->slugger = $slugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hello(): string
|
||||||
|
{
|
||||||
|
return $this->slugger->slug('Стойността трябва да бъде лъжа');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group functional
|
||||||
|
*/
|
||||||
|
class SluggerLocaleAwareTest extends AbstractWebTestCase
|
||||||
|
{
|
||||||
|
public function testLocalizedSlugger()
|
||||||
|
{
|
||||||
|
$kernel = static::createKernel(['test_case' => 'Slugger', 'root_config' => 'config.yml']);
|
||||||
|
$kernel->boot();
|
||||||
|
|
||||||
|
$service = $kernel->getContainer()->get('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger\SlugConstructArgService');
|
||||||
|
|
||||||
|
$this->assertSame('Stoinostta-tryabva-da-bude-luzha', $service->hello());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
|
||||||
|
|
||||||
|
return [
|
||||||
|
new FrameworkBundle(),
|
||||||
|
new TestBundle(),
|
||||||
|
];
|
@ -0,0 +1,14 @@
|
|||||||
|
imports:
|
||||||
|
- { resource: ../config/default.yml }
|
||||||
|
- { resource: services.yml }
|
||||||
|
|
||||||
|
framework:
|
||||||
|
secret: '%secret%'
|
||||||
|
default_locale: '%env(LOCALE)%'
|
||||||
|
translator:
|
||||||
|
fallbacks:
|
||||||
|
- '%env(LOCALE)%'
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
env(LOCALE): bg
|
||||||
|
secret: test
|
@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
_defaults:
|
||||||
|
public: true
|
||||||
|
|
||||||
|
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger\SlugConstructArgService:
|
||||||
|
arguments: ['@slugger']
|
@ -50,6 +50,7 @@
|
|||||||
"symfony/security-http": "^4.4|^5.0",
|
"symfony/security-http": "^4.4|^5.0",
|
||||||
"symfony/serializer": "^4.4|^5.0",
|
"symfony/serializer": "^4.4|^5.0",
|
||||||
"symfony/stopwatch": "^4.4|^5.0",
|
"symfony/stopwatch": "^4.4|^5.0",
|
||||||
|
"symfony/string": "^5.0",
|
||||||
"symfony/translation": "^5.0",
|
"symfony/translation": "^5.0",
|
||||||
"symfony/twig-bundle": "^4.4|^5.0",
|
"symfony/twig-bundle": "^4.4|^5.0",
|
||||||
"symfony/validator": "^4.4|^5.0",
|
"symfony/validator": "^4.4|^5.0",
|
||||||
|
@ -331,18 +331,6 @@ abstract class AbstractUnicodeString extends AbstractString
|
|||||||
return $str;
|
return $str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return static
|
|
||||||
*/
|
|
||||||
public function slug(string $separator = '-'): self
|
|
||||||
{
|
|
||||||
return $this
|
|
||||||
->ascii()
|
|
||||||
->replace('@', $separator.'at'.$separator)
|
|
||||||
->replaceMatches('/[^A-Za-z0-9]++/', $separator)
|
|
||||||
->trim($separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function snake(): parent
|
public function snake(): parent
|
||||||
{
|
{
|
||||||
$str = $this->camel()->title();
|
$str = $this->camel()->title();
|
||||||
|
136
src/Symfony/Component/String/Slugger/AsciiSlugger.php
Normal file
136
src/Symfony/Component/String/Slugger/AsciiSlugger.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\String\Slugger;
|
||||||
|
|
||||||
|
use Symfony\Component\String\AbstractUnicodeString;
|
||||||
|
use Symfony\Component\String\GraphemeString;
|
||||||
|
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.0
|
||||||
|
*/
|
||||||
|
class AsciiSlugger implements SluggerInterface, LocaleAwareInterface
|
||||||
|
{
|
||||||
|
private const LOCALE_TO_TRANSLITERATOR_ID = [
|
||||||
|
'am' => 'Amharic-Latin',
|
||||||
|
'ar' => 'Arabic-Latin',
|
||||||
|
'az' => 'Azerbaijani-Latin',
|
||||||
|
'be' => 'Belarusian-Latin',
|
||||||
|
'bg' => 'Bulgarian-Latin',
|
||||||
|
'bn' => 'Bengali-Latin',
|
||||||
|
'de' => 'de-ASCII',
|
||||||
|
'el' => 'Greek-Latin',
|
||||||
|
'fa' => 'Persian-Latin',
|
||||||
|
'he' => 'Hebrew-Latin',
|
||||||
|
'hy' => 'Armenian-Latin',
|
||||||
|
'ka' => 'Georgian-Latin',
|
||||||
|
'kk' => 'Kazakh-Latin',
|
||||||
|
'ky' => 'Kirghiz-Latin',
|
||||||
|
'ko' => 'Korean-Latin',
|
||||||
|
'mk' => 'Macedonian-Latin',
|
||||||
|
'mn' => 'Mongolian-Latin',
|
||||||
|
'or' => 'Oriya-Latin',
|
||||||
|
'ps' => 'Pashto-Latin',
|
||||||
|
'ru' => 'Russian-Latin',
|
||||||
|
'sr' => 'Serbian-Latin',
|
||||||
|
'sr_Cyrl' => 'Serbian-Latin',
|
||||||
|
'th' => 'Thai-Latin',
|
||||||
|
'tk' => 'Turkmen-Latin',
|
||||||
|
'uk' => 'Ukrainian-Latin',
|
||||||
|
'uz' => 'Uzbek-Latin',
|
||||||
|
'zh' => 'Han-Latin',
|
||||||
|
];
|
||||||
|
|
||||||
|
private $defaultLocale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of transliterators per locale.
|
||||||
|
*
|
||||||
|
* @var \Transliterator[]
|
||||||
|
*/
|
||||||
|
private $transliterators = [];
|
||||||
|
|
||||||
|
public function __construct(string $defaultLocale = null)
|
||||||
|
{
|
||||||
|
$this->defaultLocale = $defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function setLocale($locale)
|
||||||
|
{
|
||||||
|
$this->defaultLocale = $locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getLocale()
|
||||||
|
{
|
||||||
|
return $this->defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString
|
||||||
|
{
|
||||||
|
$locale = $locale ?? $this->defaultLocale;
|
||||||
|
|
||||||
|
$transliterator = [];
|
||||||
|
if ('de' === $locale || 0 === strpos($locale, 'de_')) {
|
||||||
|
// Use the shortcut for German in GraphemeString::ascii() if possible (faster and no requirement on intl)
|
||||||
|
$transliterator = ['de-ASCII'];
|
||||||
|
} elseif (\function_exists('transliterator_transliterate') && $locale) {
|
||||||
|
$transliterator = (array) $this->createTransliterator($locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new GraphemeString($string))
|
||||||
|
->ascii($transliterator)
|
||||||
|
->replace('@', $separator.'at'.$separator)
|
||||||
|
->replaceMatches('/[^A-Za-z0-9]++/', $separator)
|
||||||
|
->trim($separator)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createTransliterator(string $locale): ?\Transliterator
|
||||||
|
{
|
||||||
|
if (isset($this->transliterators[$locale])) {
|
||||||
|
return $this->transliterators[$locale];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact locale supported, cache and return
|
||||||
|
if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) {
|
||||||
|
return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locale not supported and no parent, fallback to any-latin
|
||||||
|
if (false === $str = strrchr($locale, '_')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales
|
||||||
|
$parent = substr($locale, 0, -\strlen($str));
|
||||||
|
|
||||||
|
if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) {
|
||||||
|
$transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
|
||||||
|
$this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator;
|
||||||
|
|
||||||
|
return $transliterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
29
src/Symfony/Component/String/Slugger/SluggerInterface.php
Normal file
29
src/Symfony/Component/String/Slugger/SluggerInterface.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\String\Slugger;
|
||||||
|
|
||||||
|
use Symfony\Component\String\AbstractUnicodeString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a URL-friendly slug from a given string.
|
||||||
|
*
|
||||||
|
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||||
|
*
|
||||||
|
* @experimental in 5.0
|
||||||
|
*/
|
||||||
|
interface SluggerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a slug for the given string and locale, using appropriate transliteration when needed.
|
||||||
|
*/
|
||||||
|
public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString;
|
||||||
|
}
|
49
src/Symfony/Component/String/Tests/SluggerTest.php
Normal file
49
src/Symfony/Component/String/Tests/SluggerTest.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\String\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||||
|
|
||||||
|
class SluggerTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @requires extension intl
|
||||||
|
* @dataProvider provideSlug
|
||||||
|
*/
|
||||||
|
public function testSlug(string $string, string $locale, string $expectedSlug)
|
||||||
|
{
|
||||||
|
$slugger = new AsciiSlugger($locale);
|
||||||
|
|
||||||
|
$this->assertSame($expectedSlug, (string) $slugger->slug($string));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideSlug(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['Стойността трябва да бъде лъжа', 'bg', 'Stoinostta-tryabva-da-bude-luzha'],
|
||||||
|
['Dieser Wert sollte größer oder gleich', 'de', 'Dieser-Wert-sollte-groesser-oder-gleich'],
|
||||||
|
['Dieser Wert sollte größer oder gleich', 'de_AT', 'Dieser-Wert-sollte-groesser-oder-gleich'],
|
||||||
|
['Αυτή η τιμή πρέπει να είναι ψευδής', 'el', 'Avti-i-timi-prepi-na-inai-psevdhis'],
|
||||||
|
['该变量的值应为', 'zh', 'gai-bian-liang-de-zhi-ying-wei'],
|
||||||
|
['該變數的值應為', 'zh_TW', 'gai-bian-shu-de-zhi-ying-wei'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSeparatorWithoutLocale()
|
||||||
|
{
|
||||||
|
$slugger = new AsciiSlugger();
|
||||||
|
|
||||||
|
$this->assertSame('hello-world', (string) $slugger->slug('hello world'));
|
||||||
|
$this->assertSame('hello_world', (string) $slugger->slug('hello world', '_'));
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user