Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6f09f3f

Browse filesBrowse files
feature #64079 [ErrorHandler] Trigger @method deprecation notices for abstract classes (lacatoire)
This PR was merged into the 8.1 branch. Discussion ---------- [ErrorHandler] Trigger `@method` deprecation notices for abstract classes | Q | A | ------------- | --- | Branch? | 8.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Fixes #63633 | License | MIT `DebugClassLoader` already collects ```@method``` annotations from interfaces so that subclasses receive a deprecation notice when they fail to implement the announced method. The downstream code (the comment around `$parentInterfaces` near `class_implements`) explicitly assumes that abstract parent classes accumulate methods in `self::$method`, but the population step at the top of `checkClass` only runs when `isInterface()` is true. As a result, ```@method``` annotations on abstract classes were silently ignored. Extend the population check to ``isInterface() || isAbstract()`` so abstract classes can announce upcoming abstract methods just like interfaces can. When the abstract class itself already declares a method matching the annotation, the corresponding entry is skipped: the annotation is then plain documentation and subclasses do not need to be deprecated. Adds two tests covering both cases (subclass missing the method, subclass implementing it). Commits ------- 831405a [ErrorHandler] Trigger `@method` deprecation notices for abstract classes too
2 parents dab4e95 + 831405a commit 6f09f3f
Copy full SHA for 6f09f3f

4 files changed

+64-1Lines changed: 64 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Symfony/Component/ErrorHandler/CHANGELOG.md‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/ErrorHandler/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add argument `$deprecationsNamespacesMapping` to `DebugClassLoader::enable()` to configure namespace-to-vendor remapping for deprecation checks
8+
* Trigger `@method` deprecation notices on abstract classes
89

910
7.3
1011
---
Collapse file

‎src/Symfony/Component/ErrorHandler/DebugClassLoader.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/ErrorHandler/DebugClassLoader.php
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
436436
}
437437
}
438438

439-
if ($refl->isInterface() && isset($doc['method'])) {
439+
if (($refl->isInterface() || $refl->isAbstract()) && isset($doc['method'])) {
440440
foreach ($doc['method'] as $name => [$static, $returnType, $signature, $description]) {
441441
if ($refl->hasMethod($static ? '__callStatic' : '__call')) {
442442
// When the interface has "virtual" @method declarations but at the same time contains a __call/__callStatic magic method,
@@ -446,6 +446,11 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
446446
// (missing notices) in the case that such interfaces are later amended with actual (real) methods.
447447
continue;
448448
}
449+
if ($refl->hasMethod($name)) {
450+
// The abstract class already declares the method (abstract or with a default implementation),
451+
// so the @method annotation is just documentation; no deprecation should be triggered for subclasses.
452+
continue;
453+
}
449454
self::$method[$class][] = [$class, $static, $returnType, $name.$signature, $description];
450455

451456
if ('' !== $returnType) {
Collapse file

‎src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php
+46Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,44 @@ class_exists('Test\\'.ExtendsVirtualMagicCall::class, true);
410410
], $deprecations);
411411
}
412412

413+
public function testVirtualUseWithAbstractClass()
414+
{
415+
// An abstract class can announce @method annotations the same way an interface does, to give
416+
// subclasses time to implement the method before it becomes a real abstract requirement.
417+
// ExtendsVirtualAbstractClass extends VirtualAbstract (abstract) and does not implement any of its
418+
// @method annotations.
419+
420+
$deprecations = [];
421+
set_error_handler(static function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
422+
$e = error_reporting(\E_USER_DEPRECATED);
423+
424+
class_exists('Test\\'.ExtendsVirtualAbstractClass::class, true);
425+
426+
error_reporting($e);
427+
restore_error_handler();
428+
429+
$this->assertSame([
430+
'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualAbstractClass" should implement method "Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualAbstract::abstractClassMethod(): string".',
431+
'Class "Test\Symfony\Component\ErrorHandler\Tests\ExtendsVirtualAbstractClass" should implement method "static Symfony\Component\ErrorHandler\Tests\Fixtures\VirtualAbstract::abstractStaticMethod(): \stdClass": Description.',
432+
], $deprecations);
433+
}
434+
435+
public function testVirtualUseWithAbstractClassImplementingTheMethod()
436+
{
437+
// When the concrete subclass already declares the announced @method, no deprecation is raised.
438+
439+
$deprecations = [];
440+
set_error_handler(static function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
441+
$e = error_reporting(\E_USER_DEPRECATED);
442+
443+
class_exists('Test\\'.ExtendsVirtualAbstractClassImpl::class, true);
444+
445+
error_reporting($e);
446+
restore_error_handler();
447+
448+
$this->assertSame([], $deprecations);
449+
}
450+
413451
public function testVirtualUseWithMagicCallInterface()
414452
{
415453
// When an interface uses "@method" annotations and, at the same time, requires the __call method to be
@@ -722,6 +760,14 @@ public function ownAbstractBaseMethod() { }
722760
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualMagicCallInterface implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterfaceWithCall {
723761
public function __call(string $name, array $arguments): mixed { return null; }
724762
}');
763+
} elseif ('Test\\'.ExtendsVirtualAbstractClass::class === $class) {
764+
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualAbstractClass extends \\'.__NAMESPACE__.'\Fixtures\VirtualAbstract {
765+
}');
766+
} elseif ('Test\\'.ExtendsVirtualAbstractClassImpl::class === $class) {
767+
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualAbstractClassImpl extends \\'.__NAMESPACE__.'\Fixtures\VirtualAbstract {
768+
public function abstractClassMethod(): string { return ""; }
769+
public static function abstractStaticMethod(): \stdClass { return new \stdClass(); }
770+
}');
725771
} elseif ('Test\\'.ReturnType::class === $class) {
726772
return $fixtureDir.\DIRECTORY_SEPARATOR.'ReturnType.php';
727773
} elseif ('Test\\'.ReturnTypePhp83::class === $class) {
Collapse file
+11Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\ErrorHandler\Tests\Fixtures;
4+
5+
/**
6+
* @method string abstractClassMethod()
7+
* @method static \stdClass abstractStaticMethod() Description
8+
*/
9+
abstract class VirtualAbstract
10+
{
11+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.