-
Notifications
You must be signed in to change notification settings - Fork 7.7k
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
Check abstract method signatures coming from traits #5068
Conversation
This will break legitimate use of such "incompatible" methods. Please reconsider. There are other tools to enforce signatures: (abstract) classes and interfaces. |
@nicolas-grekas Can you please provide an example of a "legitimate use"? Note that this only affects abstract methods. I find it hard to imagine why a trait would be specifying illegal abstract signatures. |
Sure: check this trait: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpClient/Response/ResponseTrait.php It declares: Which is implemented in different forms in consuming classes: Depending on the use case. The implementations inside the trait don't care about these details. |
@nicolas-grekas For exotic use cases like that, why not just drop that method from the trait (that is, move to |
(The type system deficiency being that you can't write |
Could work, but I don't have to right now, so we can use abstract methods for documentation purpose. I find it better than annotations because e.g. syntax is enforced. Note that a trait should never enforce any method's visibility to their consumers. All in all, I think this provides no benefits over existing tool: if one wants to enforce a contract, there are tools for it, (abstract) classes and interface. That a trait needs some stubs to be filled in, fine, but abstract methods shouldn't be used for that. Would allowing private abstract methods on traits work for this purpose? |
You are making use of the exactly the thing this PR intends to provide: You are declaring an abstract method in the trait with the requirement that it be implemented by the using class. Your particular case just has a fuzzy notion of what "implement" means (due to type system deficiencies) and makes use of the incomplete validation to make that slip through. People regularly want to do that kind of stuff independently of traits: interface EventHandler {
public function handle(Event $e);
}
class SpecificEventHandler implements EventHandler {
public function handle(SpecificEvent $e);
} That's basically the same situation as you have, just without the trait, and it's just about as incorrect here as with the trait.
What else could abstract methods in traits possibly be used for? Unless they are used to actually provide abstract methods (i.e. the trait is used in abstract classes only) this is the only purpose they can serve that I can see.
Private abstract methods would make sense in addition to provide signatures for purely internal requirements of the trait. |
I mostly agree with you. My main concern emerging from this discussion is about method visibility. private abstract methods on traits look really the correct way to me, they have the perfect semantics. |
@nicolas-grekas you're also violating another (currently unenforced) contract:
Here the trait is saying "I want users of this trait to define a I don't think it's terrible, but it's now something that Psalm (latest master) differentiates from more serious method access overriding: https://psalm.dev/r/a2e30a5828 |
Hello @muglug. I would have used a private abstract declaration if it were allowed by the engine. But the code is not violating any contracts because by their very definition, traits do not define any contracts. That's the reason why you can rename or change the visibility of any of their methods when using them, check https://www.php.net/manual/en/language.oop5.traits.php |
@smarr are you the author of the original PHP RFC that introduced traits? We would be super happy to have your feedback here! Thank you :) |
@nicolas-grekas sorry it has been a very long time. So, my opinion is probably not as useful as you may hope. If I understand the discussion correctly, the underlying question here seems to be how precisely a trait can express its requirements. Given that traits where designed before type annotations where a thing in PHP, I never really thought about it before. And I'll admit that I haven't spent nearly enough time on the issue, before starting to type this comment. However, my general feeling is that abstract methods in traits are intended to express the requirements that the trait has on an implementing class. As such, I would expect a typed language to validate these requirements, and decide whether what is offered is actually compatible. Back in the day, this was simply whether a required method was there, and perhaps checking that it had the right number of arguments. Now with types being a thing, I would expect those also to be validated as in any other case. Again, I am fairly out of touch with what has been happening, and I do not know PHP's type system, or philosophy on how to combine types with the dynamic nature of the language. So, please take this comment with the necessary grain of salt. |
Thanks a lot for your feedback @smarr. I think that clears my objections. The validation done by traits should not account for the visibility, isn't it @nikic? Having abstract private methods (in traits only) would still be nice IMHO for expressing the need to implement a method without forcing a public/protected visibility. Maybe that's for a separate PR? |
Just to add another note on visibility: Visibility requirements would seem odd to me, because the trait is going to be flattened into the class and thus has full access. So, I would think that a trait should be satisfied when the class provides a private method to it. |
8576e9e
to
e8a05c4
Compare
I've update the PR to now:
I think visibility in traits should follow the same rules we have everywhere else, i.e. if you have If you don't care about the visibility of the method, then you can use |
This is not required from the pov of the trait. I think this should be removed. |
I kinda see your point, but don't see the value in making this behave differently from all the rest of PHP. The visibility is generally considered part of the method contract just like everything else, and there already is a way (under this proposal) to say that you're fine with any visibility: Something to keep in mind is that there's also the possibility that the using class is abstract, in which case a child class may be providing the implementation of the abstract trait method. That's a scenario where visibility concerns do very much matter, and I think it would be odd to treat that case differently from the others. |
It's a matter of responsibility: let's say a trait is provided by a third-party library and it defines a public or protected method. Then a consumer is forced to provide that same method in their API. This reduces the usefulness of traits for no reasons - ie traits are meant for horizontal reusability, and this rule makes them less reusable, while they don't require it to ensure they will work. |
To reiterate: If the trait does not care about the visibility of the implementing method, it can use an abstract private method:
The only reason why the trait would use something other than Why would the trait want to enforce a minimum visibility? Maybe the implementation requires it:
I'd not write that code and use More likely though the trait is a helper to implement some contract, in which case the method may need to be public. However, this should usually be enforced by a separate implementation of an interface, so enforcing it from the trait side is not strictly necessary. Now, there is one more reason why one would use an |
Yes, that's what I mean. A library author should not be able to force a consumer to adopt a public/protected method. That's way past the responsibilities the language should pass to authors of libraries (when using traits).
This is a side effect of the previous point. Let's say Symfony is defining abstract protected method in traits: turning that into a private method would be a BC break. i.e PHP8 would force a major version on Symfony OR it would force a BC break on code that alias these protected methods to private and are just fine. I think you really need to reconsider. |
Don't libraries do this all the time with abstract classes? Why should traits have a different behavior?
It sounds like this is a hypothetical. But regardless, if a consumer implements the method as private, that seems likely to be a bug. If a library trait defines a protected or public method, there may be other parts of the library that expect to be able to call the method. |
Traits are supposed to be horizontal reusability units. The fact that we can alias or change the visibility of a method when using them is the big hint here if one doesn't get why they shouldn't enforce visibility. To enforce some visibility, there are already dedicated language constructs: classes and interfaces. |
I really don't see why the visibility shouldn't be enforced, as said by @nikic if the methods within the trait don't care about the visibility of the method they depend on, it can just be declared as abstract private. And to me being able to restrict the visibility is clearly nonsensical, horizontal re-usability or not. Edit: also it is possibly to explicitly change the visibility of methods in traits currently: https://www.php.net/manual/en/language.oop5.traits.php#language.oop5.traits.visibility |
@nikic Would this be possible? trait T
{
abstract private function execute(): void;
}
class C
{
use T {
execute as executeForT;
}
private function executeForT(): void
{
// do stuff for the trait implementation
}
public function execute(Foo $foo): Bar
{
// public method for doing something other
}
} |
@gharlan No, this would not be possible. This is the reason why |
Ooops, closed the wrong PR by accident. |
Normally it's enough to mark the class as "abstract" if an abstract method was not implemented. However, if there are abstract private methods, then even they really must be implemented, or it must be forwarded to a method with higher visibility.
4e2a249
to
ed3b360
Compare
I've updated the PR and RFC to no longer validate visibility, for better backwards-compatibility with the existing |
class C { | ||
use T; | ||
|
||
/* For backwards-compatiblity reasons, visibility is not enforced here. */ |
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 backwards-compatiblity reasons, visibility is not enforced here. */ | |
/* For backwards-compatibility reasons, visibility is not enforced here. */ |
if (ai->cnt < MAX_ABSTRACT_INFO_CNT) { | ||
ai->afn[ai->cnt] = fn; | ||
} | ||
if (fn->common.fn_flags & ZEND_ACC_CTOR) { |
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.
EDIT: Never mind, created 5433 to document the change to ReflectionMethod
EDIT: Sorry, I forgot this was mentioned in https://externals.io/message/109377 , but documentation still applies
The way this affects ReflectionMethod->isConstructor should be documented in UPGRADING
(e.g. for public function __construct()
in an interface), and https://www.php.net/manual/en/reflectionmethod.isconstructor.php .
In php <= 7.4, it returned false for abstract constructors. I'd still think this change of returning true is more correct.
In php 8.0, it now returns true.
protected function canMockMethod(ReflectionMethod $method)
{
if ($method->isConstructor() ||
$method->isFinal() ||
$method->isPrivate() ||
isset($this->blacklistedMethodNames[$method->getName()])) {
return false;
}
return true;
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.
Uh, how is this related to this patch? This code is about generating error messages.
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.
Sorry, I was in a hurry and misread what this was doing and posted the comment for the wrong PR before reading the thread - it's reading ZEND_ACC_CTOR, but not setting it
The relevant change[1] is that now the class entry members
serialize_func, unserialize_func, constructor, destructor and clone are
set, if the respective methods are declared on an interface.https://github.com/php/php-src/pull/3846/files#diff-3a8139128d4026ce0cb0c86beba4e6b9L5549-R5605
RFC: https://wiki.php.net/rfc/abstract_trait_method_validation
Abstract method signatures coming from traits are currently not being validated, or at least not in the most common case where the abstract method is implemented directly by the using class. If it is implemented by a parent class, or by a child class, then the method is already validated.
This fixes the behavior to always validate abstract methods from traits. Additionally it removes the requirement that abstract methods in multiple traits must be bidirectionally compatible. That is, the code from https://3v4l.org/VrpaV will now compile without error.