Skip to content

Commit 8399208

Browse files
committed
feature symfony#20493 [Debug] Trigger deprecation on @final annotation in DebugClassLoader - prepare making some classes final (GuilhemN)
This PR was merged into the 3.3-dev branch. Discussion ---------- [Debug] Trigger deprecation on `@final` annotation in DebugClassLoader - prepare making some classes final | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | follows symfony#19734 | License | MIT | Doc PR | | BC promises become quickly huge but making classes `final` can limit these promises. At the same time, many classes of the symfony codebase are not meant to be extended and could be `final`; that's the goal of this PR: prepare making them final in 4.0 by triggering deprecations in their constructor: ```php public function __construct() { if (__CLASS__ !== get_class($this)) { @trigger_error(sprintf('Extending %s is deprecated since 3.3 and won\'t be supported in 4.0 as it will be final.', __CLASS__), E_USER_DEPRECATED); } } ``` I updated two classes for now but we can do much more if you like it. Commits ------- c2ff111 [Debug] Trigger deprecation on `@final` annotation in DebugClassLoader
2 parents f7679f7 + c2ff111 commit 8399208

File tree

5 files changed

+51
-1
lines changed

5 files changed

+51
-1
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class DebugClassLoader
2727
private $classLoader;
2828
private $isFinder;
2929
private static $caseCheck;
30+
private static $final = array();
3031
private static $deprecated = array();
3132
private static $php7Reserved = array('int', 'float', 'bool', 'string', 'true', 'false', 'null');
3233
private static $darwinCache = array('/' => array('/', array()));
@@ -163,11 +164,21 @@ public function loadClass($class)
163164
throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name));
164165
}
165166

167+
if (preg_match('#\n \* @final(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) {
168+
self::$final[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
169+
}
170+
171+
$parent = get_parent_class($class);
172+
if ($parent && isset(self::$final[$parent])) {
173+
@trigger_error(sprintf('The %s class is considered final%s. It may change without further notice as of its next major version. You should not extend it from %s.', $parent, self::$final[$parent], $name), E_USER_DEPRECATED);
174+
}
175+
166176
if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) {
167177
@trigger_error(sprintf('%s uses a reserved class name (%s) that will break on PHP 7 and higher', $name, $refl->getShortName()), E_USER_DEPRECATED);
168178
} elseif (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) {
169179
self::$deprecated[$name] = preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]);
170180
} else {
181+
// Don't trigger deprecations for classes in the same vendor
171182
if (2 > $len = 1 + (strpos($name, '\\', 1 + strpos($name, '\\')) ?: strpos($name, '_'))) {
172183
$len = 0;
173184
$ns = '';
@@ -181,7 +192,6 @@ public function loadClass($class)
181192
break;
182193
}
183194
}
184-
$parent = get_parent_class($class);
185195

186196
if (!$parent || strncmp($ns, $parent, $len)) {
187197
if ($parent && isset(self::$deprecated[$parent]) && strncmp($ns, $parent, $len)) {

src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,28 @@ class_exists('Test\\'.__NAMESPACE__.'\\Float', true);
267267

268268
$this->assertSame($xError, $lastError);
269269
}
270+
271+
public function testExtendedFinalClass()
272+
{
273+
set_error_handler(function () { return false; });
274+
$e = error_reporting(0);
275+
trigger_error('', E_USER_NOTICE);
276+
277+
class_exists('Test\\'.__NAMESPACE__.'\\ExtendsFinalClass', true);
278+
279+
error_reporting($e);
280+
restore_error_handler();
281+
282+
$lastError = error_get_last();
283+
unset($lastError['file'], $lastError['line']);
284+
285+
$xError = array(
286+
'type' => E_USER_DEPRECATED,
287+
'message' => 'The Symfony\Component\Debug\Tests\Fixtures\FinalClass class is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from Test\Symfony\Component\Debug\Tests\ExtendsFinalClass.',
288+
);
289+
290+
$this->assertSame($xError, $lastError);
291+
}
270292
}
271293

272294
class ClassLoader
@@ -300,6 +322,8 @@ public function findFile($class)
300322
return $fixtureDir.'notPsr0Bis.php';
301323
} elseif (__NAMESPACE__.'\Fixtures\DeprecatedInterface' === $class) {
302324
return $fixtureDir.'DeprecatedInterface.php';
325+
} elseif (__NAMESPACE__.'\Fixtures\FinalClass' === $class) {
326+
return $fixtureDir.'FinalClass.php';
303327
} elseif ('Symfony\Bridge\Debug\Tests\Fixtures\ExtendsDeprecatedParent' === $class) {
304328
eval('namespace Symfony\Bridge\Debug\Tests\Fixtures; class ExtendsDeprecatedParent extends \\'.__NAMESPACE__.'\Fixtures\DeprecatedClass {}');
305329
} elseif ('Test\\'.__NAMESPACE__.'\DeprecatedParentClass' === $class) {
@@ -310,6 +334,8 @@ public function findFile($class)
310334
eval('namespace Test\\'.__NAMESPACE__.'; class NonDeprecatedInterfaceClass implements \\'.__NAMESPACE__.'\Fixtures\NonDeprecatedInterface {}');
311335
} elseif ('Test\\'.__NAMESPACE__.'\Float' === $class) {
312336
eval('namespace Test\\'.__NAMESPACE__.'; class Float {}');
337+
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsFinalClass' === $class) {
338+
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsFinalClass extends \\'.__NAMESPACE__.'\Fixtures\FinalClass {}');
313339
}
314340
}
315341
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
/**
6+
* @final since version 3.3.
7+
*/
8+
class FinalClass
9+
{
10+
}

src/Symfony/Component/Serializer/Encoder/ChainDecoder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* @author Jordi Boggiano <j.boggiano@seld.be>
2020
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
2121
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
22+
*
23+
* @final since version 3.3.
2224
*/
2325
class ChainDecoder implements DecoderInterface
2426
{

src/Symfony/Component/Serializer/Encoder/ChainEncoder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* @author Jordi Boggiano <j.boggiano@seld.be>
2020
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
2121
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
22+
*
23+
* @final since version 3.3.
2224
*/
2325
class ChainEncoder implements EncoderInterface
2426
{

0 commit comments

Comments
 (0)