-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Debug] Trigger deprecation on @final
annotation in DebugClassLoader - prepare making some classes final
#20493
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
Conversation
@@ -18,6 +18,13 @@ | |||
*/ | |||
class NullContext implements ContextInterface | |||
{ | |||
public function __construct() | |||
{ | |||
if (__CLASS__ !== get_class($this)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this won't be called if the extended class doesn't call parent::construct()
will it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not very clear whether it's supported by sf's bc policy but in the past we have already supposed that the parent constructor must always be called to have a valid object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe to be safe you could add a @final
tag and give a thumb up for https://youtrack.jetbrains.com/issue/WI-31225 :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As that's not supported by IDEs i don't think that's worth it (without talking about the fact that having an annotation for a keyword supported by php looks weird to me :P).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without talking about the fact that having an annotation for a keyword supported by php looks weird to me :P
I strongly disagree here: if we were in a perfect world no problem, but if you are using proxy manager for example you can't proxy a final service because the proxying is done via inheritance. There is also the case where for practical reason you can't declare it as final, but you wish to convey the information that a given class/method is not meant to be extended/overridden.
And for the IDE auto-completion, Symfony used to use the @api
tag for a while and @internal
or @private
are equally used. IDE support comes with usage, not vice-versa.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stof then should we only make final classes that already have a constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly disagree here: if we were in a perfect world no problem, but if you are using proxy manager for example you can't proxy a final service because the proxying is done via inheritance
If you type hint against an interface, then proxy manager has to be smart enough to proxy an interface (I saw your issue few days ago and I hope it will be fixed on configuration level). After all, it's not that crucial to have ability to proxy everything. We are not in perfect world as you said.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@unkind proxying is defined at the service level, not at the argument level. So the ProxyManager does not know what are typehints in places using the service (and btw, if the service is public, nothing can know it without analyzing the full project)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stof I understand how it works, my point was just if you couple to an interface, you can fix it at infrastructure level. But if you are coupled to a final
class somewhere in your code, you are not able to use proxy manager.
By "fixing at infrastructure level" I mean your suggestion, for example (#20392):
This would not be hard to implement. We could just add another attribute alongside lazy (or allow passing lazy: className)
but I don't see what you mean, declaring a service which class is final and registered as lazy will cause the error
It causes the error now, but it can be fixed with @stof's idea, for instance.
Nice detection trick! The code sample in your PR body should use |
@greg0ire indeed, thanks ! ☺ |
On the same topic, maybe can we make all new classes We already follow this strategy for API Platform and are happy with the result. |
@dunglas 👍 |
If you mark a lot of classes as final then, even if people will use composition/decoration (or even traits) instead of extending, it will break unit-tests.
Therefore in my tests I won't be able to mock the Symfony objects that I inject in my classes. I know the quote above specifically mentions methods, but it applies to classes as well. Some of you will say to simply mock the corresponding interface, but the truth is that a loooot of Symfony functionality is not exposed through interfaces. |
@SoboLAN : I hit that problem on a project lately, but then there is this saying "don't mock what you don't own". Not mocking what you don't own can have its benefits indeed, for instance, in the case of symfony, you will get deprecation messages and realize that you are misusing the class, which will help you migrate to something forward compatible. |
@SoboLAN don't mock concrete classes, use the |
@SoboLAN most of the time mocking a third party object is useless (a class not mocked would work fine as well) or the corresponding test is almost a copy-paste of the tested class (test of every calls, etc.). In the worst case, if someone has a valid use-case about a final class that shouldn't be final, opening an issue and explaining the problem should be enough and it will be solved in the next version. Same apply for |
As @stof and @theofidry said, it looks hard to trigger deprecations when classes don't already have a constructor. If we agree to do this, we'll have to list classes that we want to make final. I'm first thinking about |
There is a different way to trigger these warnings: doing it in the DebugClassLoader, as we do for deprecated interfaces being implemented. This way, it does not rely on having a parent constructor being called. |
👍 for @stof proposal! DebugClassLoader should look for |
No, DebugClassLoader should not throw an exception in 4.0, as it would prevent adding |
that would not be always desired, esp. to play nice with lazy proxies. |
I doubt there is a valid case to make proxy for VOs, classes with interfaces, listeners, commands, controllers, etc. In other words: proxying concrete classes looks like a hack. |
@theofidry not first, there is no blocker, let's move forward. As @unkind noted there are many classes that have no reason to be turned into services/proxies. |
I moved deprecations to the |
@@ -187,6 +197,9 @@ public function loadClass($class) | |||
if ($parent && isset(self::$deprecated[$parent]) && strncmp($ns, $parent, $len)) { | |||
@trigger_error(sprintf('The %s class extends %s that is deprecated %s', $name, $parent, self::$deprecated[$parent]), E_USER_DEPRECATED); | |||
} | |||
if ($parent && isset(self::$final[$parent]) && strncmp($ns, $parent, $len)) { | |||
@trigger_error(sprintf('Extending the class %s in %s is deprecated and may break in the next major release.', $parent, $name), E_USER_DEPRECATED); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this message, do you see a better alternative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be wrong with it exactly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't contain the versions in which the deprecation was added and when it will be applied compared to deprecation messages but maybe that's enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you don't add a deprecation, you add a @final
tag, which does not have a reason to include a version IMO.
and btw, the next major version could decide to keep the class as virtually final instead of making the final for real, if they want to keep them mockable for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would imply changing the annotation to something like @final extending is deprecated since 42.3, to be made final in 43
and parse that… too complicated IMO and the value of this piece of information is questionable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@greg0ire we could do as for @deprecated
tags and prepend the triggered message with the annotation description. I'm not sure that's worth it too so let's leave it as is for now :)
@stof if people mock a class having a @final
tag, a deprecation will be triggered; I don't think that's acceptable if we want to allow it. If we want to keep some @final
annotation virtual, then I think we should add a special case when we want to allow mocks (we could add something special in the annotation description? what about @final only mocks are allowed
?).
@@ -165,9 +166,18 @@ public function loadClass($class) | |||
|
|||
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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this elseif
to an if
as there were no reason imo to not continue triggering deprecations after realizing that the name is not allowed on php7. I can't revert it if you think it should be done in another pr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not related to this PR, and we don't need to trigger two notices here, I'd revert
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, done
} elseif (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { | ||
} | ||
|
||
if (preg_match('#\n \* @final( .*)?\r?\n \*(?: @|/$)#s', $refl->getDocComment())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this does not need to be done for interfaces (marking an interface as final does not make any sense). Add $refl->isClass()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
\ReflectionClass::isClass()
doesn't exist and I didn't find a simple way to do the same.
After thinking a bit about it I decided to not had this check as no deprecation will be triggered later anyway (because of the get_parent_class
call).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it make sense to use the text after @final
is there is any?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know, do you think we should use it to indicate a version and/or what this annotation means?
self::$final[$name] = true; | ||
} | ||
|
||
if (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { | ||
self::$deprecated[$name] = preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using else
here looks wrong to me, as the logic now also deals with wanna-be-final parent classes.
Btw, the logic allowing a class to extend/implement a deprecated class/interface if it is in the same namespace does not apply to the logic for final classes IMO, so you should not mix this in the same code block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this else
was here to prevent deprecated classes to trigger more deprecations when they also implement/extend something else deprecated.
Btw, the logic allowing a class to extend/implement a deprecated class/interface if it is in the same namespace does not apply to the logic for final classes IMO, so you should not mix this in the same code block
I agree 👍, I moved it.
public function getClassMap() | ||
{ | ||
return array(__NAMESPACE__.'\Fixtures\NotPSR0bis' => __DIR__.'/Fixtures/notPsr0Bis.php'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing this looks wrong to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I saw this is not used and this class is already managed in findFile
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unrelated, should be reverted
if (preg_match('#\n \* @final( .*)?\r?\n \*(?: @|/$)#s', $refl->getDocComment())) { | ||
self::$final[$name] = true; | ||
} | ||
$parent = get_parent_class($class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can remove the call to get_parent_class below
|
||
$parent = get_parent_class($class); | ||
if ($parent && isset(self::$final[$parent])) { | ||
@trigger_error(sprintf('Extending the class %s in %s is deprecated and may break in the next major release.', $parent, $name), E_USER_DEPRECATED); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we throw here? DebugClassLoader is a dev tool so it's not like we're going to break a prod site.
But maybe throw in 4.0 yes. Then the message should be clear, in "may", don't you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you throw, it means that adding @final
somewhere is a BC break, as it forces everyone to update immediately (breaking the dev environment is as bad as breaking the prod one, as your BC would then be valid only for people not using the dev environment to continue working on their project, which means nobody sane).
And we will never throw an exception here (it would suffer from the same issue for new @final
annotations added in a package used alongside symfony/debug 4.x).
the may
is here because the class being wanna-be-final may be turned into a real final class in the next major version of its package (which has nothing to do with the next Symfony major version) if the goal was to move to real final classes. But some package owners may decide to keep them wanna-be-final forever to allow mocking them, so we cannot be sure that the code will break.
got it |
@final
annotation in DebugClassLoader - prepare making some classes final
@nicolas-grekas if you did not understand it, the message is not clear enough. |
LGTM |
|
||
$parent = get_parent_class($class); | ||
if ($parent && isset(self::$final[$parent])) { | ||
@trigger_error(sprintf('Extending the class %s in %s is deprecated. This class may be made final and break your code in the next major release.', $parent, $name), E_USER_DEPRECATED); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why may be made
instead of will be made
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As stated by @stof:
But some package owners may decide to keep them wanna-be-final forever to allow mocking them, so we cannot be sure that the code will break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the intention is to make it final, enforced or not. So, the message must say that extending this class must not be done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
message updated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment does not really match the tested behaviour, does it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it? From what I understood, when a class (let's say App\Controller\AppController
) is deprecated, deprecations won't be triggered when for example App\Controller\BlogController
extends it.
Would you prefer:
// Don't trigger deprecations for classes in the same sub namespace
// (e.g. a deprecated class `App\Foo\Deprecated` won't trigger a deprecation
// when extended by `App\Foo\NewClass` but when extended by `App\Quz\Bar`, it will).
// For Symfony components/bundles/bridges, this rule applies to the root
// namespace (e.g. `Symfony`).
?
If that locks us I can do it in another PR, it was just the occasion to clarify what this check does ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah no, I misunderstood the reasoning for adding it. Looks good to me.
Reading #21263 (comment), I think we need to add the version where the |
The message could be something like: |
If we will treat class with I'm totally for putting a deprecation notice, just let the notice not be an excuse to break BC. Indeed, sth like |
|
||
$parent = get_parent_class($class); | ||
if ($parent && isset(self::$final[$parent])) { | ||
@trigger_error(sprintf('Extending the class %s in %s is deprecated. This class will be made final in the next major release.', $parent, $name), E_USER_DEPRECATED); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed on Slack with @javiereguiluz, we propose: 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, $msgWithFirstUppercaseAndNewlinesAndSpacesAndFinalDotCleanedUp, $class)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
msgWithFirstUppercaseAndNewlinesAndSpacesAndFinalDotCleanedUp
🤷♂️🙈
Ps. It's Slack ;) nog Sack 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/nog/not/
(Muphry's law)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$msgWithFirstUppercaseAndNewlinesAndSpacesAndFinalDotCleanedUp: this means almost same process as done for deprecations should be applied on the message next to the annotation :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍, updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 with a minor comment (would be great to answer to @xabbuh thought)
} | ||
|
||
$parent = get_parent_class($class); | ||
if ($parent && array_key_exists($parent, self::$final)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"isset" is enough here
👍 |
This PR was merged into the 3.3-dev branch. Discussion ---------- [DI] Mark generated containers as final | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | yes | Tests pass? | no | Fixed tickets | - | License | MIT | Doc PR | - So that we don't have to care anymore about BC for protected methods in generated containers. Will leverage deprecations triggered in #20493 Commits ------- ce0ee1e [DI] Mark generated containers as final
@GuilhemN Can you squash your commits please? |
|
||
$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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So.
If the class is @final
, it means that one may not extend it, but still could use it.
If the class is @internal
, it means that he should not use it at all.
For me it's more like that case:
-'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.'
+'The %s class is considered final%s. It will be declared with `final` keyword in next major release. You should not extend it from %s.'
If one would like to ask user to not use that class (as he cannot rely on it's interface), I would rather mark that classes as @internal
ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is missing the fact that sometimes, a class cannot be made really final for technical reasons (see eg Doctrine EM) - because "final" forbids lazy proxies.
If one wants to express what you say, then the message after the annotation is done for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doctrine - valid scenario. good here.
Still, final
doesn't mean that Backwards Compatibility Promise
is not provided. It's not provided for internal
classes.
Consider some exception is final. One should not extend it, but he could still catch it and handle it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True! @final
says that extending is not covered by the BC policy.
Might be worth a PR on the BC policy. Would you mind doing it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nicolas-grekas
Policy is one thing, but then the message here is different.
I would rather want to first set up what is expected, then do it. Not the other way around.
I would propose for @final
:
-'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.'
+-'The %s class is considered final%s. You should not extend it from %s.'
Then, for @internal
annotation:
-
+'The %s class is considered internal%s. It may change without further notice as of its next major version. You should not use it'
What do you think ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For final, I think the message is OK as is now.
For internal, I think we should not trigger anything: the user can do it. It's just that you're on your own if doing so. It's like using reflection to access private props: you don't get a notice. But you're on you own and can't complain if this breaks.
So, nothing to change IMHO in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the thing with reflection is that the user will be aware that he is doing sth odd by accessing private props - as he needs to do sth special.
for internal class, with is only marked via annotation, in most cases he would not know
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's why we do not remove them in the same major where we added the tag
but if a new class is created and starts its life @internal
, there is no way the user is not aware: the class is not documented, thus one must have opened the code to know what it does.
add IDE for many people + UPGRADE when @internal
is added after a release.
I think that's good enough :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@keradus also unlike for @final
, most IDE do give you a visual warning when using a @internal
class. True that we should not rely on IDEs only as some people are not using them, but as one is very unlikely to use an internal class accidentally, I think it's an acceptable compromise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just met a lot of bad code in my life, some of them just because sb was not aware of sth.
The more defensive you are, the less people will make a mistake ;)
very well, let us stop on current state as good enough
/** | ||
* @final since version 3.3. | ||
*/ | ||
class FinalClass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add a test like
/** @final since version 3.3 */
final class FinalClass {}
(or better, add test checking that no final declared class has final annotation)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why ? If someone want redundancy, I don't think we should care.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not needed to trigger warning to the user that the class will be final if it already is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but we don't care: if the class is really final, then there is no way we end up in the code path where that final class is the parent of something else, isn't it?
done @fabpot |
Thank you @GuilhemN. |
…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 #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
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:I updated two classes for now but we can do much more if you like it.