diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 1c469a4f23046..b5eab774ca145 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -28,6 +28,7 @@ class DebugClassLoader private $isFinder; private static $caseCheck; private static $final = array(); + private static $finalMethods = array(); private static $deprecated = array(); private static $php7Reserved = array('int', 'float', 'bool', 'string', 'true', 'false', 'null'); private static $darwinCache = array('/' => array('/', array())); @@ -164,13 +165,40 @@ 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); + + // Not an interface nor a trait + if (class_exists($name, false)) { + if (preg_match('#\n \* @final(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { + self::$final[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; + } + + 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); + } + + // Inherit @final annotations + self::$finalMethods[$name] = $parent && isset(self::$finalMethods[$parent]) ? self::$finalMethods[$parent] : array(); + + foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) { + if ($method->class !== $name) { + continue; + } + + if ($parent && isset(self::$finalMethods[$parent][$method->name])) { + @trigger_error(sprintf('%s It may change without further notice as of its next major version. You should not extend it from %s.', self::$finalMethods[$parent][$method->name], $name), E_USER_DEPRECATED); + } + + $doc = $method->getDocComment(); + if (false === $doc || false === strpos($doc, '@final')) { + continue; + } + + if (preg_match('#\n\s+\* @final(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) { + $message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; + self::$finalMethods[$name][$method->name] = sprintf('The %s::%s() method is considered final%s.', $name, $method->name, $message); + } + } } if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) { diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index b529f277cf382..64d0087dc13c6 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -289,6 +289,28 @@ class_exists('Test\\'.__NAMESPACE__.'\\ExtendsFinalClass', true); $this->assertSame($xError, $lastError); } + + public function testExtendedFinalMethod() + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists(__NAMESPACE__.'\\Fixtures\\ExtendedFinalMethod', 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\FinalMethod::finalMethod() method 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 Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod.', + ); + + $this->assertSame($xError, $lastError); + } } class ClassLoader @@ -324,6 +346,10 @@ public function findFile($class) return $fixtureDir.'DeprecatedInterface.php'; } elseif (__NAMESPACE__.'\Fixtures\FinalClass' === $class) { return $fixtureDir.'FinalClass.php'; + } elseif (__NAMESPACE__.'\Fixtures\FinalMethod' === $class) { + return $fixtureDir.'FinalMethod.php'; + } elseif (__NAMESPACE__.'\Fixtures\ExtendedFinalMethod' === $class) { + return $fixtureDir.'ExtendedFinalMethod.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) { diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/ExtendedFinalMethod.php b/src/Symfony/Component/Debug/Tests/Fixtures/ExtendedFinalMethod.php new file mode 100644 index 0000000000000..2bd337e5a2db0 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/ExtendedFinalMethod.php @@ -0,0 +1,17 @@ + 'Network Authentication Required', // RFC6585 ); - private static $deprecatedMethods = array( - 'setDate', 'getDate', - 'setExpires', 'getExpires', - 'setLastModified', 'getLastModified', - 'setProtocolVersion', 'getProtocolVersion', - 'setStatusCode', 'getStatusCode', - 'setCharset', 'getCharset', - 'setPrivate', 'setPublic', - 'getAge', 'getMaxAge', 'setMaxAge', 'setSharedMaxAge', - 'getTtl', 'setTtl', 'setClientTtl', - 'getEtag', 'setEtag', - 'hasVary', 'getVary', 'setVary', - 'isInvalid', 'isSuccessful', 'isRedirection', - 'isClientError', 'isOk', 'isForbidden', - 'isNotFound', 'isRedirect', 'isEmpty', - 'isCacheable', 'isFresh', 'isValidateable', - 'mustRevalidate', 'setCache', 'setNotModified', - 'isNotModified', 'isInformational', 'isServerError', - 'closeOutputBuffers', 'ensureIEOverSSLCompatibility', - ); - private static $deprecationsTriggered = array( - __CLASS__ => true, - BinaryFileResponse::class => true, - JsonResponse::class => true, - RedirectResponse::class => true, - StreamedResponse::class => true, - ); - /** * Constructor. * @@ -229,23 +201,6 @@ public function __construct($content = '', $status = 200, $headers = array()) $this->setContent($content); $this->setStatusCode($status); $this->setProtocolVersion('1.0'); - - // Deprecations - $class = get_class($this); - if ($this instanceof \PHPUnit_Framework_MockObject_MockObject || $this instanceof \Prophecy\Doubler\DoubleInterface) { - $class = get_parent_class($class); - } - if (isset(self::$deprecationsTriggered[$class])) { - return; - } - - self::$deprecationsTriggered[$class] = true; - foreach (self::$deprecatedMethods as $method) { - $r = new \ReflectionMethod($class, $method); - if (__CLASS__ !== $r->getDeclaringClass()->getName()) { - @trigger_error(sprintf('Extending %s::%s() in %s is deprecated since version 3.2 and won\'t be supported anymore in 4.0 as it will be final.', __CLASS__, $method, $class), E_USER_DEPRECATED); - } - } } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index 1a832a1dcba10..c08a4fe31a8cc 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -843,25 +843,6 @@ public function testSettersAreChainable() } } - public function testNoDeprecationsAreTriggered() - { - new DefaultResponse(); - $this->getMockBuilder(Response::class)->getMock(); - } - - /** - * @group legacy - * @expectedDeprecation Extending Symfony\Component\HttpFoundation\Response::getDate() in Symfony\Component\HttpFoundation\Tests\ExtendedResponse is deprecated %s. - * @expectedDeprecation Extending Symfony\Component\HttpFoundation\Response::setLastModified() in Symfony\Component\HttpFoundation\Tests\ExtendedResponse is deprecated %s. - */ - public function testDeprecations() - { - new ExtendedResponse(); - - // Deprecations should not be triggered twice - new ExtendedResponse(); - } - public function validContentProvider() { return array(