How should we provide return type declarations for fluent interfaces?

With the move to PHP 7.1, we can now document scalar method arguments, as well as return type declarations. This latter is of particular importance for end users, as they can then be assured that what they expect is what they will receive.

However, this poses a problem with fluent interfaces, particularly when inheritance of any type is involved, due to how PHP implements return type declarations: they are implemented as invariants, which leads to issues when implementations or extensions need to include the return type declaration.

As examples, given either of the following:

interface A
{
    public function select(string $table) : self;
}

class B
{
    private $address;

    public function to(string $address) : self
    {
        $this->address = $address;
        return $this;
    }
}

Implementing A or extending B runs into issues, as we cannot use self as a return type declaration when implementing either select() or overriding to(). For a single layer of extension, this is not a problem, as we can use parent instead:

class C extends B implements A
{
    private $table;
    private $where;

    public function select(string $table) : parent
    {
        $this->table = $table;
        return $this;
    }

    public function to(string $address) : parent
    {
        $this->where = $address;
        parent::to($address);
        return $this;
    }
}

IDE problems

While the above parses correctly, it poses a problem in IDEs, as they now assume that the return values are of type A and B, respectively and not C. As such, they will not allow expansion of methods in C on the return value.

However, if we have another level of inheritance — e.g., class D extending C — neither self nor parent now work as return type declarations, and you instead need to provide the return type of the root declaration:

class D extends C
{
    private $myOwnTable;
    private $myCriteria;

    public function select(string $table) : A
    {
        $this->myOwhTable = $table;
        parent::select($table);
        return $this;
    }

    public function to(string $address) : B
    {
        $this->myCritieria = $address;
        parent::to($address);
        return $this;
    }
}

As noted above, this then means that IDEs now think the return value is of another type entirely, meaning they cannot hint on the methods from the actual class.


Some possible solutions:

  1. Use annotations instead: @return self or @return static or @return $this. These, however, mean that the contract is not enforced at the engine level. I largely feel this is a non-starter, as it goes against our reasons for moving to 7.1 in the first place.
  2. Don’t worry about it, and just use the root declaration. This indicates an “is-a” relationship, and enforces the idea that a class operates as a general type, promoting the idea of re-use of generic types, and not concrete types. This would be the simplest way and pose the least amount of backwards compatibility breakage, but could pose issues for consumers in components such as zend-form that have multiple levels of inheritance when using IDEs.
  3. Deprecate usage of fluent interfaces except when used for implementing immutability (e.g., PSR-7’s with*() methods), where return type declarations are indicating the interface type returned, and not the instance type. Most fluent interfaces within the framework would then instead return void. This would pose the most BC breakage, but forward-proof the APIs the most.

Please let us know in the comments what solution you prefer, and why.

1 Like

I don’t see too much of a problem using a combination of 1 and 2. Annotations can be used “as well” rather than “instead” in places where a type hint would help to clarify the type for IDE.

Just for clarifications, are you suggesting the following?

/**
 * @return self
 */
public function select(string $table) : A
{
}

I guess I’m suggesting:

interface A 
{
    public function select(string $table) : A
}

class B extends A
{
    /**
     * @return B
     */
    public function select(string $table) : A
}

Because B::select would return interface A but IDE would only give you completion on that interface not any other methods added in B. The return type hint gives type safety, the annotation gives proper completion.

2 Likes

Pretty much what @GeeH said:

interface A 
{
    public function select(string $table) : self
}

class B extends A
{
    /**
     * @return B
     */
    public function select(string $table) : parent
}

Deprecate usage of fluent interfaces except when used for implementing immutability (e.g., PSR-7’s with*() methods), where return type declarations are indicating the interface type returned, and not the instance type. Most fluent interfaces within the framework would then instead return void. This would pose the most BC breakage, but forward-proof the APIs the most.

Where do I sign up for this? :slight_smile: I’m wondering if we can automate detection of those - will ask around.

4 Likes

I am scared that we agree on something.

1 Like

I personally tend to solution #2 exclusively (that is, for immutable modifier methods), but apart from that would chose path #3. When an interface says that a method should return an object implementing an interface, and nothing more, that’s all an end-user should have to know. I know that this imposes a problem with fluid interfaces themself, as implementations can have quite different functionalities, which is why i prefer solution #3.

The recommendation by @GeeH is a nice workaround, but as I said, I’d try to design the APIs to avoid that entirely.

1 Like

just typo fix: class can’t extends interface :imp: https://3v4l.org/VGnR1 . I believe it should be “implements”

or “class B” should be “interface B”

I would vote for 3, except for cases where fluent interface is not a mere convenience, like zend-db query builder fluent api.

Besides, i think parent return typehint should not be allowed in any case.

I agree with @GeeH: IDEs are only tools for development; type hint gives us language (and execution) type safety.

Damn… Thinking about my message, I tried to apply fluent interfaces in another strongly typed language: Java.

Now, I understand why Java source codes doesn’t use fluent interfaces, because of casting! HAHAHA

public interface A
{
    public A select(String table);
}

public class B implements A
{
    public A select(String table)
    {
        return this; // automatic upcasting
    }

    public B to(String address)
    {
        return this;
    }
}

B objectB = new B();
objectB
    .select("FooBar")
    .to("BazQux"); // Unknown Method "A::to"

PHP is not strongly typed but, now, I don’t have an opinion! XD

~mindblowing~

I know that I might have repeated this ad nauseam, but if somebody is wondering about why we’re considering dropping fluent interfaces, see https://ocramius.github.io/blog/fluent-interfaces-are-evil/

2 Likes

Mixed feelings, I would not encourage using fluent interfaces, just like @ocramius detailed. That said, they are nice to use in a “Builder” context (not using zend-forms but I really like to work with Zend\Db\Select.... for example).

So I would say, solution 3 to not encourage the use of fluent interfaces as a general rule. But I would extend the exception of immutability to builders as well (covered by solution 2). BC-break them can potentially bc-break my brain too :wink: Of course I’m aware of the contradiction, and the risk is that the builder term might not be interpreted or understood the same way by different people.

@wandersonwhcr, not sure about your example in Java. your method would be public B select(String table) in Java ? (EDIT: sorry I just got it, you refer to GeeH example - Java supports covariance for return types, so that’s how you would write it in Java. PHP does not)

But referring to Java, just wanted to share the Calendar.Builder (inner)class example:

Calendar cal = new Calendar.Builder()
                      .setCalendarType("iso8601")
                      .setWeekDate(2013, 1, MONDAY) // ...
                    .build();

or symfony forms (sorry don’t know about zend-forms):

$form = $this->createFormBuilder($task)
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, array('label' => 'Create Post'))
            ->getForm();

Where I feel more ok with fluent interfaces, I see clearly I’m working with some kind of Builder… and that all methods should (must) be fluent (except: the build() method in java or getForm() in symfony)…

The drawback, is that when one rule have exceptions -> confusion (which is fluent which is not ? which is supposed to be immutable which is not ?)… Class naming, conventions ?

So to me, if instinctively identifiable and correctly chainable, builders -> solution 2 is fine, the semi-fluent-evil way :wink:

And yes, phpdoc annotations could be used for IDE’s to refine the return type… (just for those cases, another exception :wink:)

PS:

Correct chainability may eventually be catched with code analysis tools (phpstan)… supposing a naming convention (XXXBuilder), could be possible to scan all extending classes methods to check for chain breakability

@belgattitude, I’m sorry for it. I agree my example is not self explanatory.

But, here it is: if B is instance of A, we must implement that interface. Java doesn’t enable overload of return type, only arguments. So, implement public B select(String table) isn’t allowed (I think there’s no strongly typed language with that feature).

Always when I see Java codes, I find repeated variables like this:

B objectB = new B();
objectB.select("FooBar");
objectB.to("BarQux");
objectB.setFoo("Foo");
objectB.setBar("Bar");

“objectB, objectB, objectB 1000x, objectB.”

It happens because /^set.*$/ methods always return void. I always questioned myself why not use fluent interfaces and now I found a case where they are evil (paraphrasing @ocramius).

The method select has a behavior: always return an A type object. Downcasting is needed if you are aware of B type object. If we think in a OOP world, we must use downcast, breaking (evil) fluent interfaces.

@wandersonwhcr, depends on your object… but yes in Java you’ll see a lot of code using getter/setter… and in those cases fluent interfaces are more than evil (any getter misplaced will break the chainability)… Still you’ll find them, they’re just used in certain circumstances and/or where applicable +/- safely (builder…). See the protocol buffer home for example:

Person john = Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .build();
output = new FileOutputStream(args[0]);
john.writeTo(output);

I confess I like chaining methods at least on well designed and identifiable objects. That’s why I had mixed feelings between solution 3 (drop fluent interfaces) and 2 (fluent interfaces return type for inheritance / invariant limits -> return type=root declaration+annotation).

It’s just about when to apply 2 or 3 and what is the rule ?

BTW: In java, when overriding methods, return types are covariant… meaning that you can declare a subtype. PHP return types are invariant. But that will hopefuly evolve in future versions.

I like chaining methods too, but, after this conversation, I think all cases where I use fluent interfaces are “builder” cases. BTW, I think we must use option 3, but adding the “builder” as an except too.

1 Like