Skip to content

[Routing] Remove Doctrine annotations support #51082

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions UPGRADE-7.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ Routing
-------

* Add argument `$routeParameters` to `UrlMatcher::handleRouteRequirements()`
* Remove Doctrine annotations support in favor of native attributes
* Change the constructor signature of `AnnotationClassLoader` to `__construct(?string $env = null)`, passing an annotation reader as first argument is not supported anymore

Security
--------
Expand Down
2 changes: 0 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@
"async-aws/sqs": "^1.0",
"async-aws/sns": "^1.0",
"cache/integration-tests": "dev-master",
"doctrine/annotations": "^1.13.1|^2",
"doctrine/collections": "^1.0|^2.0",
"doctrine/data-fixtures": "^1.1",
"doctrine/dbal": "^3.6",
Expand Down Expand Up @@ -159,7 +158,6 @@
"conflict": {
"ext-psr": "<1.1|>=2",
"async-aws/core": "<1.5",
"doctrine/annotations": "<1.13.1",
"doctrine/dbal": "<3.6",
"doctrine/orm": "<2.15",
"egulias/email-validator": "~3.0.0",
Expand Down
6 changes: 0 additions & 6 deletions src/Symfony/Component/Routing/Annotation/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@
namespace Symfony\Component\Routing\Annotation;

/**
* Annotation class for @Route().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"CLASS", "METHOD"})
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Alexander M. Turek <me@derrabus.de>
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Routing/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ CHANGELOG
---

* Add argument `$routeParameters` to `UrlMatcher::handleRouteRequirements()`
* Remove Doctrine annotations support in favor of native attributes
* Change the constructor signature of `AnnotationClassLoader` to `__construct(?string $env = null)`, passing an annotation reader as first argument is not supported anymore

6.4
---
Expand Down
134 changes: 32 additions & 102 deletions src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

namespace Symfony\Component\Routing\Loader;

use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolverInterface;
use Symfony\Component\Config\Resource\FileResource;
Expand Down Expand Up @@ -52,49 +51,12 @@
*/
abstract class AnnotationClassLoader implements LoaderInterface
{
/**
* @var Reader|null
*
* @deprecated in Symfony 6.4, this property will be removed in Symfony 7.
*/
protected $reader;

/**
* @var string|null
*/
protected $env;

/**
* @var string
*/
protected $routeAnnotationClass = RouteAnnotation::class;

/**
* @var int
*/
protected $defaultRouteIndex = 0;

private bool $hasDeprecatedAnnotations = false;

/**
* @param string|null $env
*/
public function __construct($env = null)
{
if ($env instanceof Reader || null === $env && \func_num_args() > 1 && null !== func_get_arg(1)) {
trigger_deprecation('symfony/routing', '6.4', 'Passing an instance of "%s" as first and the environment as second argument to "%s" is deprecated. Pass the environment as first argument instead.', Reader::class, __METHOD__);
protected string $routeAnnotationClass = RouteAnnotation::class;
protected int $defaultRouteIndex = 0;

$this->reader = $env;
$env = \func_num_args() > 1 ? func_get_arg(1) : null;
}

if (\is_string($env) || null === $env) {
$this->env = $env;
} elseif ($env instanceof \Stringable || \is_scalar($env)) {
$this->env = (string) $env;
} else {
throw new \TypeError(__METHOD__.sprintf(': Parameter $env was expected to be a string or null, "%s" given.', get_debug_type($env)));
}
public function __construct(
protected readonly ?string $env = null,
) {
}

/**
Expand All @@ -121,48 +83,38 @@ public function load(mixed $class, string $type = null): RouteCollection
throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class->getName()));
}

$this->hasDeprecatedAnnotations = false;

try {
$globals = $this->getGlobals($class);
$collection = new RouteCollection();
$collection->addResource(new FileResource($class->getFileName()));
if ($globals['env'] && $this->env !== $globals['env']) {
return $collection;
}
$fqcnAlias = false;
foreach ($class->getMethods() as $method) {
$this->defaultRouteIndex = 0;
$routeNamesBefore = array_keys($collection->all());
foreach ($this->getAnnotations($method) as $annot) {
$this->addRoute($collection, $annot, $globals, $class, $method);
if ('__invoke' === $method->name) {
$fqcnAlias = true;
}
}

if (1 === $collection->count() - \count($routeNamesBefore)) {
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
}
}
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
$globals = $this->resetGlobals();
foreach ($this->getAnnotations($class) as $annot) {
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
$globals = $this->getGlobals($class);
$collection = new RouteCollection();
$collection->addResource(new FileResource($class->getFileName()));
if ($globals['env'] && $this->env !== $globals['env']) {
return $collection;
}
$fqcnAlias = false;
foreach ($class->getMethods() as $method) {
$this->defaultRouteIndex = 0;
$routeNamesBefore = array_keys($collection->all());
foreach ($this->getAnnotations($method) as $annot) {
$this->addRoute($collection, $annot, $globals, $class, $method);
if ('__invoke' === $method->name) {
$fqcnAlias = true;
}
}
if ($fqcnAlias && 1 === $collection->count()) {
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
}

if ($this->hasDeprecatedAnnotations) {
trigger_deprecation('symfony/routing', '6.4', 'Class "%s" uses Doctrine Annotations to configure routes, which is deprecated. Use PHP attributes instead.', $class->getName());
if (1 === $collection->count() - \count($routeNamesBefore)) {
$newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore));
$collection->addAlias(sprintf('%s::%s', $class->name, $method->name), $newRouteName);
}
} finally {
$this->hasDeprecatedAnnotations = false;
}
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
$globals = $this->resetGlobals();
foreach ($this->getAnnotations($class) as $annot) {
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
$fqcnAlias = true;
}
}
if ($fqcnAlias && 1 === $collection->count()) {
$collection->addAlias($class->name, $invokeRouteName = key($collection->all()));
$collection->addAlias(sprintf('%s::__invoke', $class->name), $invokeRouteName);
}

return $collection;
Expand Down Expand Up @@ -291,15 +243,9 @@ protected function getGlobals(\ReflectionClass $class): array
{
$globals = $this->resetGlobals();

$annot = null;
if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
$annot = $attribute->newInstance();
}
if (!$annot && $annot = $this->reader?->getClassAnnotation($class, $this->routeAnnotationClass)) {
$this->hasDeprecatedAnnotations = true;
}

if ($annot) {
if (null !== $annot->getName()) {
$globals['name'] = $annot->getName();
}
Expand Down Expand Up @@ -387,21 +333,5 @@ private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection):
foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
yield $attribute->newInstance();
}

if (!$this->reader) {
return;
}

$annotations = $reflection instanceof \ReflectionClass
? $this->reader->getClassAnnotations($reflection)
: $this->reader->getMethodAnnotations($reflection);

foreach ($annotations as $annotation) {
if ($annotation instanceof $this->routeAnnotationClass) {
$this->hasDeprecatedAnnotations = true;

yield $annotation;
}
}
}
}
36 changes: 3 additions & 33 deletions src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,19 @@

namespace Symfony\Component\Routing\Tests\Annotation;

use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\FooController;
use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\FooController as FooAttributesController;
use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\FooController;

class RouteTest extends TestCase
{
private function getMethodAnnotation(string $method, bool $attributes): Route
{
$class = $attributes ? FooAttributesController::class : FooController::class;
$reflection = new \ReflectionMethod($class, $method);

if ($attributes) {
$attributes = $reflection->getAttributes(Route::class);
$route = $attributes[0]->newInstance();
} else {
$reader = new AnnotationReader();
$route = $reader->getMethodAnnotation($reflection, Route::class);
}

if (!$route instanceof Route) {
throw new \Exception('Can\'t parse annotation');
}

return $route;
}

/**
* @dataProvider getValidParameters
*/
public function testLoadFromAttribute(string $methodName, string $getter, $expectedReturn)
public function testLoadFromAttribute(string $methodName, string $getter, mixed $expectedReturn)
{
$route = $this->getMethodAnnotation($methodName, true);
$this->assertEquals($route->$getter(), $expectedReturn);
}
$route = (new \ReflectionMethod(FooController::class, $methodName))->getAttributes(Route::class)[0]->newInstance();

/**
* @dataProvider getValidParameters
*/
public function testLoadFromDoctrineAnnotation(string $methodName, string $getter, $expectedReturn)
{
$route = $this->getMethodAnnotation($methodName, false);
$this->assertEquals($route->$getter(), $expectedReturn);
}

Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading