diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 9fd688718da6f..1c469a4f23046 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -27,6 +27,7 @@ class DebugClassLoader private $classLoader; private $isFinder; private static $caseCheck; + private static $final = array(); private static $deprecated = array(); private static $php7Reserved = array('int', 'float', 'bool', 'string', 'true', 'false', 'null'); private static $darwinCache = array('/' => array('/', array())); @@ -163,11 +164,21 @@ public function loadClass($class) throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name)); } + if (preg_match('#\n \* @final(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { + self::$final[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; + } + + $parent = get_parent_class($class); + if ($parent && isset(self::$final[$parent])) { + @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); + } + if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) { @trigger_error(sprintf('%s uses a reserved class name (%s) that will break on PHP 7 and higher', $name, $refl->getShortName()), E_USER_DEPRECATED); } elseif (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { self::$deprecated[$name] = preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]); } else { + // Don't trigger deprecations for classes in the same vendor if (2 > $len = 1 + (strpos($name, '\\', 1 + strpos($name, '\\')) ?: strpos($name, '_'))) { $len = 0; $ns = ''; @@ -181,7 +192,6 @@ public function loadClass($class) break; } } - $parent = get_parent_class($class); if (!$parent || strncmp($ns, $parent, $len)) { if ($parent && isset(self::$deprecated[$parent]) && strncmp($ns, $parent, $len)) { diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 6bdbaaf8393d7..b529f277cf382 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -267,6 +267,28 @@ class_exists('Test\\'.__NAMESPACE__.'\\Float', true); $this->assertSame($xError, $lastError); } + + public function testExtendedFinalClass() + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsFinalClass', true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $xError = array( + 'type' => E_USER_DEPRECATED, + '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.', + ); + + $this->assertSame($xError, $lastError); + } } class ClassLoader @@ -300,6 +322,8 @@ public function findFile($class) return $fixtureDir.'notPsr0Bis.php'; } elseif (__NAMESPACE__.'\Fixtures\DeprecatedInterface' === $class) { return $fixtureDir.'DeprecatedInterface.php'; + } elseif (__NAMESPACE__.'\Fixtures\FinalClass' === $class) { + return $fixtureDir.'FinalClass.php'; } elseif ('Symfony\Bridge\Debug\Tests\Fixtures\ExtendsDeprecatedParent' === $class) { eval('namespace Symfony\Bridge\Debug\Tests\Fixtures; class ExtendsDeprecatedParent extends \\'.__NAMESPACE__.'\Fixtures\DeprecatedClass {}'); } elseif ('Test\\'.__NAMESPACE__.'\DeprecatedParentClass' === $class) { @@ -310,6 +334,8 @@ public function findFile($class) eval('namespace Test\\'.__NAMESPACE__.'; class NonDeprecatedInterfaceClass implements \\'.__NAMESPACE__.'\Fixtures\NonDeprecatedInterface {}'); } elseif ('Test\\'.__NAMESPACE__.'\Float' === $class) { eval('namespace Test\\'.__NAMESPACE__.'; class Float {}'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsFinalClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsFinalClass extends \\'.__NAMESPACE__.'\Fixtures\FinalClass {}'); } } } diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/FinalClass.php b/src/Symfony/Component/Debug/Tests/Fixtures/FinalClass.php new file mode 100644 index 0000000000000..2cf26b19e4fc8 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/FinalClass.php @@ -0,0 +1,10 @@ + * @author Johannes M. Schmitt * @author Lukas Kahwe Smith + * + * @final since version 3.3. */ class ChainDecoder implements DecoderInterface { diff --git a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php index 5ee352ab27932..ca7e71aa3016a 100644 --- a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php @@ -19,6 +19,8 @@ * @author Jordi Boggiano * @author Johannes M. Schmitt * @author Lukas Kahwe Smith + * + * @final since version 3.3. */ class ChainEncoder implements EncoderInterface {