Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5491b6f

Browse files
committed
[String] Introduce a locale-aware Slugger in the String component with FrameworkBundle wiring
1 parent a176d1a commit 5491b6f

File tree

13 files changed

+324
-12
lines changed

13 files changed

+324
-12
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ CHANGELOG
1919
* Removed `SecurityUserValueResolver`, use `UserValueResolver` instead
2020
* Removed `routing.loader.service`.
2121
* Service route loaders must be tagged with `routing.route_loader`.
22+
* Added `slugger` service and `SluggerInterface` alias
2223

2324
4.4.0
2425
-----

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
106106
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
107107
use Symfony\Component\Stopwatch\Stopwatch;
108+
use Symfony\Component\String\Slugger\SluggerInterface;
108109
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
109110
use Symfony\Component\Translation\Translator;
110111
use Symfony\Component\Validator\ConstraintValidatorInterface;
@@ -194,6 +195,12 @@ public function load(array $configs, ContainerBuilder $container)
194195
}
195196
}
196197

198+
// If the slugger is used but the String component is not available, we should throw an error
199+
if (!class_exists(SluggerInterface::class)) {
200+
$container->register('slugger', 'stdClass')
201+
->addError('You cannot use the "slugger" since the String component is not installed. Try running "composer require symfony/string".');
202+
}
203+
197204
if (isset($config['secret'])) {
198205
$container->setParameter('kernel.secret', $config['secret']);
199206
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,11 @@
120120
<argument type="service" id="request_stack" />
121121
<tag name="kernel.event_subscriber" />
122122
</service>
123+
124+
<service id="slugger" class="Symfony\Component\String\Slugger\AsciiSlugger">
125+
<argument>%kernel.default_locale%</argument>
126+
<tag name="kernel.locale_aware" />
127+
</service>
128+
<service id="Symfony\Component\String\Slugger\SluggerInterface" alias="slugger" />
123129
</services>
124130
</container>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger;
13+
14+
use Symfony\Component\String\Slugger\SluggerInterface;
15+
16+
class SlugConstructArgService
17+
{
18+
private $slugger;
19+
20+
public function __construct(SluggerInterface $slugger)
21+
{
22+
$this->slugger = $slugger;
23+
}
24+
25+
public function hello(): string
26+
{
27+
return $this->slugger->slug('Стойността трябва да бъде лъжа');
28+
}
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
13+
14+
/**
15+
* @group functional
16+
*/
17+
class SluggerLocaleAwareTest extends AbstractWebTestCase
18+
{
19+
public function testLocalizedSlugger()
20+
{
21+
$kernel = static::createKernel(['test_case' => 'Slugger', 'root_config' => 'config.yml']);
22+
$kernel->boot();
23+
24+
$service = $kernel->getContainer()->get('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger\SlugConstructArgService');
25+
26+
$this->assertSame('Stoinostta-tryabva-da-bude-luzha', $service->hello());
27+
}
28+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
13+
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
14+
15+
return [
16+
new FrameworkBundle(),
17+
new TestBundle(),
18+
];
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
imports:
2+
- { resource: ../config/default.yml }
3+
- { resource: services.yml }
4+
5+
framework:
6+
secret: '%secret%'
7+
default_locale: '%env(LOCALE)%'
8+
translator:
9+
fallbacks:
10+
- '%env(LOCALE)%'
11+
12+
parameters:
13+
env(LOCALE): bg
14+
secret: test
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
_defaults:
3+
public: true
4+
5+
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger\SlugConstructArgService:
6+
arguments: ['@slugger']

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"symfony/security-http": "^4.4|^5.0",
5151
"symfony/serializer": "^4.4|^5.0",
5252
"symfony/stopwatch": "^4.4|^5.0",
53+
"symfony/string": "^5.0",
5354
"symfony/translation": "^5.0",
5455
"symfony/twig-bundle": "^4.4|^5.0",
5556
"symfony/validator": "^4.4|^5.0",

src/Symfony/Component/String/AbstractUnicodeString.php

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -336,18 +336,6 @@ public function replaceMatches(string $fromPattern, $to): parent
336336
return $str;
337337
}
338338

339-
/**
340-
* @return static
341-
*/
342-
public function slug(string $separator = '-'): self
343-
{
344-
return $this
345-
->ascii()
346-
->replace('@', $separator.'at'.$separator)
347-
->replaceMatches('/[^A-Za-z0-9]++/', $separator)
348-
->trim($separator);
349-
}
350-
351339
public function snake(): parent
352340
{
353341
$str = $this->camel()->title();
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\String\Slugger;
13+
14+
use Symfony\Component\String\AbstractUnicodeString;
15+
use Symfony\Component\String\GraphemeString;
16+
use Symfony\Contracts\Translation\LocaleAwareInterface;
17+
18+
/**
19+
* @author Titouan Galopin <[email protected]>
20+
*
21+
* @experimental in 5.0
22+
*/
23+
class AsciiSlugger implements SluggerInterface, LocaleAwareInterface
24+
{
25+
private const LOCALE_TO_TRANSLITERATOR_ID = [
26+
'am' => 'Amharic-Latin',
27+
'ar' => 'Arabic-Latin',
28+
'az' => 'Azerbaijani-Latin',
29+
'be' => 'Belarusian-Latin',
30+
'bg' => 'Bulgarian-Latin',
31+
'bn' => 'Bengali-Latin',
32+
'de' => 'de-ASCII',
33+
'el' => 'Greek-Latin',
34+
'fa' => 'Persian-Latin',
35+
'he' => 'Hebrew-Latin',
36+
'hy' => 'Armenian-Latin',
37+
'ka' => 'Georgian-Latin',
38+
'kk' => 'Kazakh-Latin',
39+
'ky' => 'Kirghiz-Latin',
40+
'ko' => 'Korean-Latin',
41+
'mk' => 'Macedonian-Latin',
42+
'mn' => 'Mongolian-Latin',
43+
'or' => 'Oriya-Latin',
44+
'ps' => 'Pashto-Latin',
45+
'ru' => 'Russian-Latin',
46+
'sr' => 'Serbian-Latin',
47+
'sr_Cyrl' => 'Serbian-Latin',
48+
'th' => 'Thai-Latin',
49+
'tk' => 'Turkmen-Latin',
50+
'uk' => 'Ukrainian-Latin',
51+
'uz' => 'Uzbek-Latin',
52+
'zh' => 'Han-Latin',
53+
];
54+
55+
private $defaultLocale;
56+
57+
/**
58+
* Cache of transliterators per locale.
59+
*
60+
* @var \Transliterator[]
61+
*/
62+
private $transliterators = [];
63+
64+
public function __construct(string $defaultLocale = null)
65+
{
66+
$this->defaultLocale = $defaultLocale;
67+
}
68+
69+
/**
70+
* {@inheritdoc}
71+
*/
72+
public function setLocale($locale)
73+
{
74+
$this->defaultLocale = $locale;
75+
}
76+
77+
/**
78+
* {@inheritdoc}
79+
*/
80+
public function getLocale()
81+
{
82+
return $this->defaultLocale;
83+
}
84+
85+
/**
86+
* {@inheritdoc}
87+
*/
88+
public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString
89+
{
90+
$locale = $locale ?? $this->defaultLocale;
91+
92+
$transliterator = [];
93+
if ('de' === $locale || 0 === strpos($locale, 'de_')) {
94+
$transliterator = ['de-ASCII'];
95+
} elseif (\function_exists('transliterator_transliterate') && $locale) {
96+
$transliterator = (array) $this->createTransliterator($locale);
97+
}
98+
99+
return (new GraphemeString($string))
100+
->ascii($transliterator)
101+
->replace('@', $separator.'at'.$separator)
102+
->replaceMatches('/[^A-Za-z0-9]++/', $separator)
103+
->trim($separator)
104+
;
105+
}
106+
107+
private function createTransliterator(string $locale): ?\Transliterator
108+
{
109+
if (isset($this->transliterators[$locale])) {
110+
return $this->transliterators[$locale];
111+
}
112+
113+
// Exact locale supported, cache and return
114+
if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) {
115+
return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
116+
}
117+
118+
// Locale not supported and no parent, fallback to any-latin
119+
if (false === $str = strrchr($locale, '_')) {
120+
return null;
121+
}
122+
123+
// Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales
124+
$parent = substr($locale, 0, -\strlen($str));
125+
126+
if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) {
127+
$transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
128+
$this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator;
129+
130+
return $transliterator;
131+
}
132+
133+
return null;
134+
}
135+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\String\Slugger;
13+
14+
use Symfony\Component\String\AbstractUnicodeString;
15+
16+
/**
17+
* Create a URL-friendly slug from a given string.
18+
*
19+
* @author Titouan Galopin <[email protected]>
20+
*
21+
* @experimental in 5.0
22+
*/
23+
interface SluggerInterface
24+
{
25+
/**
26+
* Create a slug based on a given string and an given locale, using appropriate
27+
* transliteration when needed.
28+
*/
29+
public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString;
30+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\String\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\String\Slugger\AsciiSlugger;
16+
17+
class SluggerTest extends TestCase
18+
{
19+
/**
20+
* @requires extension intl
21+
* @dataProvider provideStringsToSlug
22+
*/
23+
public function testSlug(string $string, string $locale, string $expectedSlug)
24+
{
25+
$slugger = new AsciiSlugger($locale);
26+
27+
$this->assertSame($expectedSlug, (string) $slugger->slug($string));
28+
}
29+
30+
public static function provideStringsToSlug(): array
31+
{
32+
return [
33+
['Стойността трябва да бъде лъжа', 'bg', 'Stoinostta-tryabva-da-bude-luzha'],
34+
['Dieser Wert sollte größer oder gleich', 'de', 'Dieser-Wert-sollte-groesser-oder-gleich'],
35+
['Dieser Wert sollte größer oder gleich', 'de_AT', 'Dieser-Wert-sollte-groesser-oder-gleich'],
36+
['Αυτή η τιμή πρέπει να είναι ψευδής', 'el', 'Avti-i-timi-prepi-na-inai-psevdhis'],
37+
['该变量的值应为', 'zh', 'gai-bian-liang-de-zhi-ying-wei'],
38+
['該變數的值應為', 'zh_TW', 'gai-bian-shu-de-zhi-ying-wei'],
39+
];
40+
}
41+
42+
public function testSeparatorWithoutLocale()
43+
{
44+
$slugger = new AsciiSlugger();
45+
46+
$this->assertSame('hello-world', (string) $slugger->slug('hello world'));
47+
$this->assertSame('hello_world', (string) $slugger->slug('hello world', '_'));
48+
}
49+
}

0 commit comments

Comments
 (0)