[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 `routing.loader.service`.
|
||||
* Service route loaders must be tagged with `routing.route_loader`.
|
||||
* Added `slugger` service and `SluggerInterface` alias
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
@ -105,6 +105,7 @@ use Symfony\Component\Serializer\Encoder\EncoderInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
||||
use Symfony\Component\Translation\Translator;
|
||||
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'])) {
|
||||
$container->setParameter('kernel.secret', $config['secret']);
|
||||
}
|
||||
|
@ -120,5 +120,11 @@
|
||||
<argument type="service" id="request_stack" />
|
||||
<tag name="kernel.event_subscriber" />
|
||||
</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>
|
||||
</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/serializer": "^4.4|^5.0",
|
||||
"symfony/stopwatch": "^4.4|^5.0",
|
||||
"symfony/string": "^5.0",
|
||||
"symfony/translation": "^5.0",
|
||||
"symfony/twig-bundle": "^4.4|^5.0",
|
||||
"symfony/validator": "^4.4|^5.0",
|
||||
|
@ -331,18 +331,6 @@ abstract class AbstractUnicodeString extends AbstractString
|
||||
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
|
||||
{
|
||||
$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