Skip to content

Commit

Permalink
Merge pull request #1039 from spiral/hotfix/router-cyrilic-symbols
Browse files Browse the repository at this point in the history
Implement Custom Path Segment Encoding in Router
  • Loading branch information
butschster committed Dec 21, 2023
2 parents ae5fc1b + df9599c commit 2854d3f
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 17 deletions.
29 changes: 24 additions & 5 deletions src/Router/src/UriHandler.php
Expand Up @@ -35,8 +35,6 @@ final class UriHandler

private ?string $pattern = null;

/** @internal */
private readonly SlugifyInterface $slugify;
private readonly RoutePatternRegistryInterface $patternRegistry;
private array $constrains = [];
private array $defaults = [];
Expand All @@ -49,13 +47,34 @@ final class UriHandler
private ?string $template = null;
private array $options = [];

private \Closure $pathSegmentEncoder;

/**
* Note: SlugifyInterface will be removed in next major release.
* @see UriHandler::withPathSegmentEncoder() for more details.
*/
public function __construct(
private readonly UriFactoryInterface $uriFactory,
SlugifyInterface $slugify = null,
?RoutePatternRegistryInterface $patternRegistry = null,
) {
$this->patternRegistry = $patternRegistry ?? new DefaultPatternRegistry();
$this->slugify = $slugify ?? new Slugify();

$slugify ??= new Slugify();
$this->pathSegmentEncoder = static fn (string $segment): string => $slugify->slugify($segment);
}

/**
* Set custom path segment encoder.
*
* @param \Closure(non-empty-string): non-empty-string $callable Callable must accept string and return string.
*/
public function withPathSegmentEncoder(\Closure $callable): self
{
$uriHandler = clone $this;
$uriHandler->pathSegmentEncoder = $callable;

return $uriHandler;
}

public function getPattern(): ?string
Expand Down Expand Up @@ -220,9 +239,9 @@ private function fetchOptions(iterable $parameters, ?array &$query): array
continue;
}

//String must be normalized here
// String must be normalized here
if (\is_string($parameter) && !\preg_match('/^[a-z\-_0-9]+$/i', $parameter)) {
$result[$key] = $this->slugify->slugify($parameter);
$result[$key] = ($this->pathSegmentEncoder)($parameter);
continue;
}

Expand Down
55 changes: 43 additions & 12 deletions src/Router/tests/UriTest.php
Expand Up @@ -4,6 +4,7 @@

namespace Spiral\Tests\Router;

use PHPUnit\Framework\Attributes\DataProvider;
use Spiral\Router\Exception\UndefinedRouteException;
use Spiral\Router\Route;
use Spiral\Router\Target\Group;
Expand All @@ -18,7 +19,7 @@ public function testCastRoute(): void
'group',
new Route('/<controller>[/<action>[/<id>]]', new Group([
'test' => TestController::class,
]))
])),
);

$uri = $router->uri('group/test:test');
Expand All @@ -32,7 +33,7 @@ public function testQuery(): void
'group',
new Route('/<controller>[/<action>[/<id>]]', new Group([
'test' => TestController::class,
]))
])),
);

$uri = $router->uri('group/test:id', ['id' => 100, 'data' => 'hello']);
Expand All @@ -47,7 +48,7 @@ public function testDirect(): void
'group',
new Route('/<controller>[/<action>[/<id>]]', new Group([
'test' => TestController::class,
]))
])),
);

$uri = $router->getRoute('group')->uri(['test', 'id', 100]);
Expand All @@ -61,7 +62,7 @@ public function testSlug(): void
'group',
new Route('/<controller>[/<action>[/<id>[-<title>]]]', new Group([
'test' => TestController::class,
]))
])),
);

$uri = $router->getRoute('group')->uri(['test', 'id', 100, 'Hello World']);
Expand All @@ -74,7 +75,7 @@ public function testSlugDefault(): void
$router->setDefault(
new Route('/<controller>[/<action>[/<id>[-<title>]]]', new Group([
'test' => TestController::class,
]))
])),
);

$uri = $router->uri('test:id', ['id' => 100, 'title' => 'Hello World']);
Expand All @@ -97,16 +98,46 @@ public function testObject(): void
$router->setDefault(
new Route('/<controller>[/<action>[/<id>[-<title>]]]', new Group([
'test' => TestController::class,
]))
])),
);

$uri = $router->uri('test:id', ['id' => 100, 'title' => new class implements \Stringable {
public function __toString()
{
return 'hello-world';
}
}]);
$uri = $router->uri('test:id', [
'id' => 100,
'title' => new class implements \Stringable {
public function __toString()
{
return 'hello-world';
}
},
]);

$this->assertSame('/test/id/100-hello-world', $uri->getPath());
}

#[DataProvider('provideSegmentInDifferentLanguages')]
public function testCustomPathSegmentEncoder(string $segment, string $expected): void
{
$router = $this->makeRouter();
$router->setRoute(
'group',
new Route('/<controller>[/<action>[/<id>]]', new Group([
'test' => TestController::class,
])),
);

$route = $router->getRoute('group');
$uriHandler = $route->getUriHandler()->withPathSegmentEncoder(fn(string $segment) => \rawurlencode($segment));
$route = $route->withUriHandler($uriHandler);

$uri = $route->uri(['controller' => 'test', 'action' => $segment]);
$this->assertSame($expected, $uri->getPath());
}

public static function provideSegmentInDifferentLanguages(): iterable
{
yield 'English' => ['test', '/test/test'];
yield 'Russian' => ['тест', '/test/%D1%82%D0%B5%D1%81%D1%82'];
yield 'Japanese' => ['テスト', '/test/%E3%83%86%E3%82%B9%E3%83%88'];
yield 'Chinese' => ['测试', '/test/%E6%B5%8B%E8%AF%95'];
}
}

0 comments on commit 2854d3f

Please sign in to comment.