Skip to content

Commit 3f84bc3

Browse files
committed
[Routing] PSR-4 directory loader
1 parent 9da7be4 commit 3f84bc3

14 files changed

+407
-0
lines changed

Loader/Psr4DirectoryLoader.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Routing\Loader;
13+
14+
use Symfony\Component\Config\FileLocatorInterface;
15+
use Symfony\Component\Config\Loader\FileLoader;
16+
use Symfony\Component\Routing\RouteCollection;
17+
18+
/**
19+
* A loader that discovers controller classes in a directory that follows PSR-4.
20+
*
21+
* @author Alexander M. Turek <me@derrabus.de>
22+
*/
23+
final class Psr4DirectoryLoader extends FileLoader
24+
{
25+
public function __construct(FileLocatorInterface $locator)
26+
{
27+
// PSR-4 directory loader has no env-aware logic, so we drop the $env constructor parameter.
28+
parent::__construct($locator);
29+
}
30+
31+
public function load(mixed $resource, string $type = null): ?RouteCollection
32+
{
33+
$path = $this->locator->locate($resource);
34+
if (!is_dir($path)) {
35+
return new RouteCollection();
36+
}
37+
38+
return $this->loadFromDirectory($path, trim(substr($type, 10), ' \\'));
39+
}
40+
41+
public function supports(mixed $resource, string $type = null): bool
42+
{
43+
return \is_string($resource) && null !== $type && str_starts_with($type, 'attribute@');
44+
}
45+
46+
private function loadFromDirectory(string $directory, string $psr4Prefix): RouteCollection
47+
{
48+
$collection = new RouteCollection();
49+
50+
/** @var \SplFileInfo $file */
51+
foreach (new \FilesystemIterator($directory) as $file) {
52+
if ($file->isDir()) {
53+
$collection->addCollection($this->loadFromDirectory($file->getPathname(), $psr4Prefix.'\\'.$file->getFilename()));
54+
55+
continue;
56+
}
57+
if ('php' !== $file->getExtension() || !class_exists($className = $psr4Prefix.'\\'.$file->getBasename('.php'))) {
58+
continue;
59+
}
60+
61+
$collection->addCollection($this->import($className, 'attribute'));
62+
}
63+
64+
return $collection;
65+
}
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Routing\Tests\Fixtures\Psr4Controllers;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\Routing\Annotation\Route;
16+
17+
#[Route('/my/route', name: 'my_route')]
18+
final class MyController
19+
{
20+
public function __invoke(): Response
21+
{
22+
return new Response(status: Response::HTTP_NO_CONTENT);
23+
}
24+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Routing\Tests\Fixtures\Psr4Controllers;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
16+
final class MyUnannotatedController
17+
{
18+
public function myAction(): Response
19+
{
20+
return new Response(status: Response::HTTP_NO_CONTENT);
21+
}
22+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace\EvenDeeperNamespace;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\Routing\Annotation\Route;
16+
17+
#[Route('/my/other/route', name: 'my_other_controller_', methods: ['PUT'])]
18+
final class MyOtherController
19+
{
20+
#[Route('/first', name: 'one')]
21+
public function firstAction(): Response
22+
{
23+
return new Response(status: Response::HTTP_NO_CONTENT);
24+
}
25+
26+
#[Route('/second', name: 'two')]
27+
public function secondAction(): Response
28+
{
29+
return new Response(status: Response::HTTP_NO_CONTENT);
30+
}
31+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace;
4+
5+
interface IrrelevantInterface
6+
{
7+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace;
13+
14+
use Symfony\Component\Routing\Annotation\Route;
15+
16+
#[Route('/my/controller/with/a/trait', name: 'my_controller_')]
17+
final class MyControllerWithATrait implements IrrelevantInterface
18+
{
19+
use SomeSharedImplementation;
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Routing\Tests\Fixtures\Psr4Controllers\SubNamespace;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\Routing\Annotation\Route;
16+
17+
trait SomeSharedImplementation
18+
{
19+
#[Route('/a/route/from/a/trait', name: 'with_a_trait')]
20+
public function someAction(): Response
21+
{
22+
return new Response(status: Response::HTTP_NO_CONTENT);
23+
}
24+
}

Tests/Fixtures/class-attributes.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<routes xmlns="http://symfony.com/schema/routing"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/routing
5+
https://symfony.com/schema/routing/routing-1.0.xsd">
6+
7+
<import resource="Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController" prefix="/my-prefix"
8+
type="attribute" />
9+
</routes>

Tests/Fixtures/class-attributes.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
my_controllers:
2+
resource: Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers\MyController
3+
type: attribute
4+
prefix: /my-prefix

Tests/Fixtures/psr4-attributes.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<routes xmlns="http://symfony.com/schema/routing"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/routing
5+
https://symfony.com/schema/routing/routing-1.0.xsd">
6+
7+
<import resource="./Psr4Controllers" prefix="/my-prefix"
8+
type="attribute@Symfony\Component\Routing\Tests\Fixtures\Psr4Controllers" />
9+
</routes>

0 commit comments

Comments
 (0)