[RFC Proposal] Allow methods to 'become' static

php.internals

Gert

5 years ago
Hey Internals, Recently i've been working on an older code base, where we have a lot of classes with all static methods. We've been moving to injecting the classes, and calling the methods as if they were not static. I wanted to add interfaces to these classes, with non static methods, so we can pretend in our implementation they are not static, and allow us to easier switch to non static methods in the future. This is (currently) not allowed by the language: https://3v4l.org/WKdvM Is there any chance of this being possible in future versions of PHP? This would only allow 'normal' methods to become static. Making static methods non static would break Liskov substitution principle in PHP as you may not be able to call the method in the same way. This means you can't have a 'normal' method become static in an inherited class, and then become 'normal' again in another inherited class. This works the same way with co/contra-variance in return/parameter types: (https://3v4l.org/j1SO9)

Matthew Brown

5 years ago
On Sun, 14 Mar 2021 at 12:02, Gert de Pagter <gertp93@gmail.com> wrote:
> Hey Internals, > > Recently i've been working on an older code base, where we have a lot > of classes with all > static methods. We've been moving to injecting the classes, and > calling the methods as if they > were not static. > > I wanted to add interfaces to these classes, with non static methods, > so we can pretend in our implementation they are not static, and allow > us to easier switch to non static methods in the future. > > This is (currently) not allowed by the language: https://3v4l.org/WKdvM > Is there any chance of this being possible in future versions of PHP? > This would only allow 'normal' methods to become static. Making static > methods non static would break Liskov substitution principle in PHP as > you may not be able to call the method in the same way. This means you > can't have a 'normal' method become static in an inherited class, and > then become 'normal' again in another inherited class. This works the > same way with co/contra-variance in return/parameter types: > (https://3v4l.org/j1SO9) > > -- > PHP Internals - PHP Runtime Development Mailing List > To unsubscribe, visit: https://www.php.net/unsub.php > >
It's maybe possible in the special case of implementing interfaces, but it's unsound for general inheritance: <?php class A { public int $someInt = 0; public function getSomeInt() : int { return $this->someInt; } } class B extends A { public static function getSomeInt() : int { return parent::getSomeInt(); } } https://3v4l.org/WnaES

Rowan Collins

5 years ago
On 14/03/2021 19:40, Matthew Brown wrote:
> It's maybe possible in the special case of implementing interfaces, but > it's unsound for general inheritance: > > <?php > > class A { > public int $someInt = 0; > > public function getSomeInt() : int { > return $this->someInt; > } > } > > class B extends A { > public static function getSomeInt() : int { > return parent::getSomeInt(); > } > }
Can you explain what this is illustrating? Right now, it doesn't compile, but that's what Gert was suggesting be changed, so the 3v4l link isn't very enlightening. From the point of view of the contract of the class, it's fine: $foo->getSomeInt() is still a legal call if $foo is actually an instance of B. Are you saying that having the parent::getSomeInt() call fail would be problematic? Regards,
-- Rowan Tommins [IMSoP]

Matthew Brown

5 years ago
On Sun, 14 Mar 2021 at 18:09, Rowan Tommins <rowan.collins@gmail.com> wrote:
> Are you saying that having the parent::getSomeInt() call fail would be > problematic? > >
Yes, that's where this becomes unsound – you can call the child method statically, but the parent call assumes a dynamic instance. This is just my perspective (as someone building a tool that helps prevent people from shooting themselves in the foot), but PHP should not allow _more_ unsound behaviour than it already does. Best wishes, Matt

Nikita Popov

5 years ago
On Mon, Mar 15, 2021 at 2:16 PM Matthew Brown <matthewmatthew@gmail.com> wrote:
> On Sun, 14 Mar 2021 at 18:09, Rowan Tommins <rowan.collins@gmail.com> > wrote: > > > Are you saying that having the parent::getSomeInt() call fail would be > > problematic? > > > > > Yes, that's where this becomes unsound – you can call the child method > statically, but the parent call assumes a dynamic instance. > > This is just my perspective (as someone building a tool that helps prevent > people from shooting themselves in the foot), but PHP should not allow > _more_ unsound behaviour than it already does. >
I'm not sure I follow your point. The fact that something is compatible, does not mean that you can just blindly perform a forwarding call. For example, consider this: class A { public function method(string $x) {} } class B extends A { public function method(string|int $x) { parent::method($x); } } B::method() is compatible with A::method() from a typesystem perspective. But that doesn't mean that a parent::method() call will work. The above code is simply an implementation bug in B::method(), but doesn't say anything fundamental about the compatibility of the signatures. I would argue that the same holds in your example. Disclaimer: I have some serious doubts that allowing non-static->static changes is worthwhile, I just don't see why it would be outright unsound. Regards, Nikita

Matthew Brown

5 years ago
On Mon, 15 Mar 2021 at 09:36, Nikita Popov <nikita.ppv@gmail.com> wrote:
> I'm not sure I follow your point. The fact that something is compatible, > does not mean that you can just blindly perform a forwarding call. For > example, consider this: > > class A { > public function method(string $x) {} > } > > class B extends A { > public function method(string|int $x) { > parent::method($x); > } > } > > B::method() is compatible with A::method() from a typesystem perspective. > But that doesn't mean that a parent::method() call will work. The above > code is simply an implementation bug in B::method(), but doesn't say > anything fundamental about the compatibility of the signatures. I would > argue that the same holds in your example. > > Disclaimer: I have some serious doubts that allowing non-static->static > changes is worthwhile, I just don't see why it would be outright unsound. > > Regards, > Nikita >
Ah OK! When I say "unsound" I mean "the type system implied by PHP's behaviour allows code that a static analysis tool cannot catch". It's trivial for a static analysis tool to find a bug in your example: https://psalm.dev/r/3cd498b539 If PHP changed to allow child methods of instance methods to become static, static analysis tools could follow suit, but that would invite undetectable unsound behaviour. The most well-known example of PHP's existing unsoundness is its treatment of array keys, something that static analysis tools cannot detect: https://psalm.dev/r/4640ef8f22 vs https://3v4l.org/XQTWc

Rowan Collins

5 years ago
On 15/03/2021 13:16, Matthew Brown wrote:
> On Sun, 14 Mar 2021 at 18:09, Rowan Tommins <rowan.collins@gmail.com > <mailto:rowan.collins@gmail.com>> wrote: > > Are you saying that having the parent::getSomeInt() call fail > would be > problematic? > > >  Yes, that's where this becomes unsound – you can call the child > method statically, but the parent call assumes a dynamic instance.
This isn't really to do with changing the signature of the method though; it will give you exactly the same error as if you gave the static and non-static methods different names: class A {     public int $someInt = 0;     public function getSomeInt() : int {         return $this->someInt;     } } class B extends A {     public static function getSomeIntStatically() : int {         return parent::getSomeInt(); // This call is invalid; this is currently only reported when the method is run     } } Regards,
-- Rowan Tommins [IMSoP]

Alexandru Pătrănescu

5 years ago
On Sun, Mar 14, 2021 at 6:02 PM Gert de Pagter <gertp93@gmail.com> wrote:
> Hey Internals, > > Recently i've been working on an older code base, where we have a lot > of classes with all > static methods. We've been moving to injecting the classes, and > calling the methods as if they > were not static. > > I wanted to add interfaces to these classes, with non static methods, > so we can pretend in our implementation they are not static, and allow > us to easier switch to non static methods in the future. >
I've encountered this issue working with legacy codebase. I recommend having two versions of the class, the old with static methods and the new with non-static methods. Logic would stay only in one of them, having the other as a wrapper. Migrating from static to non-static would change the injection as well as the calling style.
> > This is (currently) not allowed by the language: https://3v4l.org/WKdvM > Is there any chance of this being possible in future versions of PHP? > This would only allow 'normal' methods to become static. Making static > methods non static would break Liskov substitution principle in PHP as > you may not be able to call the method in the same way. This means you > can't have a 'normal' method become static in an inherited class, and > then become 'normal' again in another inherited class. This works the > same way with co/contra-variance in return/parameter types: > (https://3v4l.org/j1SO9) >
The issue exists also for class extended, not only for interface implemented. Even if in general, static and non-static method have different rules for inheritance and they must stay in general separated, this idea is not completely bad in PHP where the concept of late static binding that happens when one calls $object->staticMethod(); could allow such a thing to have a sense of existence for upgrading from non-static methods to a static method in the inheritance chain. As my experience with C is not vast, I'm not sure how the inheritance chain for static and non-static methods is represented internally to allow such change. Also the calling place logic would maybe need changes to seamlessly use polymorphism logic or late static binding depending on how the method is defined in the class of the object called, if that isn't already done in some way. As I want to learn more here, I will try looking it up to understand it. But overall, it sounds like it can be avoided by choosing a separate chain of inheritance approach. Composition over inheritance. Regards, Alex