Skip to content

Instantly share code, notes, and snippets.

@simensen
Last active December 9, 2019 11:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simensen/729c4ac9056b06d60038377f59a92cfe to your computer and use it in GitHub Desktop.
Save simensen/729c4ac9056b06d60038377f59a92cfe to your computer and use it in GitHub Desktop.

Support static Return Type

Support a static return type that behaves the same way as the keyword for late static binding.

Existing self return type behavior and expectations should not change.

Problem

At runtime, PHP handles the self return type quite flexibly. However, some code intel platforms have more difficulty.

Given the table below, if callers and code intel can only rely on the defined return type of self, it is possible to get unexpected results.

Code intel can leverage phpdoc-style annotation comments to add a @return static type hint. One can also use @var $cat Cat phpdoc-style annotation comments to "cast" an object back to its proper type.

Solution

Class designers should be able to specify a static return type that would make it clear to the caller and code intel what type of object will be returned without requiring additional phpdoc-style annotation comments.

Example

Example assuming class Cat extends class Animal with functions defined in Animal:

Function in Animal : self : static
return new self() Animal Error
return new Animal() Animal Error
return new static() Cat Cat
return $this Cat Cat
return clone($this) Cat Cat

Use Cases

Immutable Fluent Interfaces

The primary use case would be for building immutable fluent interfaces. The examples above are entirely inspired by this use case. A typical example of a with* method is as follows:

abstract class AbstractAnimal
{
    private $name;
    public function withName(string $name): self
    {
        $instance = clone($this);
        $instance->name = $name;
    
        return $instance;
    }
}

class Cat extends AbstractAnimal
{
}

$snowball = (new Cat())->withName('Snowball');

In this case, the php runtime and some code intel engines will successfully treat the variable $snowball as an instance of Cat. However, some code intel will treat $snowball as an instance of AbstractAnimal only due to the self return type.

In these cases, it would be more clear and explicit to allow for the designer of the AbstractAnimal class to use a static return type instead. Callers and code intel would then continue to treat $snowball as an instance of Cat.

It would also be impossible for the parent class to accidently return an instance of itself instead of an instance of the subclass.

@mindplay-dk
Copy link

mindplay-dk commented Dec 6, 2019

static in return types (and parameter types) is for late static binding - how does it "make sense" to further overload this keyword with new meaning that has to do with the instance type?

That has nothing to do with late static binding, which ought to only work and resolve in a static call expression - so that is specifically not what static means.

If you want a special type hint that constrains the return type to "same instance", it might make more sense to overload the this keyword instead?

function withName($name: string): this

@nicolas-grekas
Copy link

nicolas-grekas commented Dec 6, 2019

Using static makes perfect sense because if you look at existing practices, e.g. PSR-7 uses @return static for what is described here. Symfony does the same - and also uses @return $this to express the fact that the very same instance is returned - aka fluent interfaces. This needs to be expressed because that's two different contracts.

@simensen
Copy link
Author

simensen commented Dec 6, 2019

how does it "make sense" to further overload this keyword with new meaning that has to do with the instance type?

I always assumed that static was there to help determine the effective instance type at runtime. From the docs, it seems like that is a legit case and not overloading what it means?

More precisely, late static bindings work by storing the class named in the last "non-forwarding call". In case of static method calls, this is the class explicitly named (usually the one on the left of the :: operator); in case of non static method calls, it is the class of the object.

To me, this means static::/static can very much about instance type.

This feature was named "late static bindings" with an internal perspective in mind. "Late binding" comes from the fact that static:: will not be resolved using the class where the method is defined but it will rather be computed using runtime information. It was also called a "static binding" as it can be used for (but is not limited to) static method calls.

"[...] will not be resolved using the class where the method is defined but it will rather be computed using runtime information," is the best way to describe my understanding of what static means in the context of the "late static binding" feature of PHP. Effectively, "what is the class of the instance (or type) being called (static)" versus "what is the class in which this function was defined (self)."

So, I don't really see it as overloading the keyword?

https://www.php.net/manual/en/language.oop5.late-static-bindings.php

For my part, this seems overloaded and I'd interpret this to mean the method is actually returning $this; that is, the same exact object on which the method was called.

If static is too controversial, I'd not be opposed to finding a different name, but I don't think this would be a particularly good alternative. In the end, though, I'm more interested in exploring the functionality than sticking completely to static as the keyword. :)

@nicolas-grekas
Copy link

nicolas-grekas commented Dec 6, 2019

I think static is what everyone that hit the problem is using - it's the perfect keyword, look at e.g.
https://github.com/php-fig/http-message/blob/efd67d1dc14a7ef4fc4e518e7dee91c271d524e4/src/RequestInterface.php#L59

@mindplay-dk
Copy link

For the record, I had completely misunderstood the issue - I got side-tracked by a different discussion on Twitter and arrived here with a different understanding of the problem you were trying to solve entirely. 🤦‍♂️

You seem to be on the right track here - please ignore my previous comment and carry on. 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment