PHP 7 return type hinting: class name vs self

Posted by Lukas Hajdu on Sun, Mar 3, 2019

The return type declarations require functions to return certain data type at call time. It clarifies expected usage of code and helps IDEs with autocomplete functionality without need of PhpDoc comments.

As of version 7, PHP added support for return type declaration and as of version 7.1 void return type and nullable return values.

Let’s create an imaginary car factory class CarFactory to play with the return type hinting.

When using fluent interface for a method chaining, you might be using class name in a return type hint similar to this:

1<?php
2
3public function addEngine(EngineInterface $engine): CarFactory
4{
5    $this->engine = $engine;
6
7    return $this;
8}

You might save some typing using self keyword, which refers to the class in which it is called, instead of a class name. The method declaration might look similar to this:

1<?php
2
3public function addWheel(WheelInterface $wheel): self
4{
5    $this->wheels[] = $wheel;
6
7    return $this;
8}

The question is. Can we save some typing at interface and trait declarations?

Let’s create the CarFactoryInterface and let’s declare a method to add a car body:

1public function addBody(BodyInterface $body): CarFactory;

The declaration defines a return type CarFactory for the method.

Now we might add an engine to the body and use the self keyword instead:

1public function addEngine(EngineInterface $engine): self;

If we try to make a car now:

1<?php
2
3$carFactory = new \Lh\CarFactory();
4$carFactory
5    ->addBody(new \Lh\SportBody())
6    ->addEngine(new \Lh\V8Engine())
7;

We will get a PHP Fatal error:

1 PHP Fatal error:  Declaration of Lh\CarFactory::addEngine(Lh\EngineInterface $engine): Lh\CarFactory must be compatible with Lh\CarFactoryInterface::addEngine(Lh\EngineInterface $engine)

This is because the self keyword refers to the CarFactoryInterface instead of the CarFactory.

Are we going to get the same result if we use self in a trait?

Let’s create a door adding functionality to our CarFactory class and create a trait for this:

 1<?php
 2
 3trait DoorTrait
 4{
 5    /** @var DoorInterface[] */
 6    private $doors;
 7
 8    public function addDoor(DoorInterface $door): self
 9    {
10        $this->doors[] = $door;
11
12        return $this;
13    }
14}
15
16// ---
17
18class CarFactory implements CarFactoryInterface
19{
20    use DoorTrait;
21    
22    ...
23}

We can update the car manufacturing process and run our script:

1<?php
2
3$carFactory = new \Lh\CarFactory();
4$carFactory
5    ->addBody(new \Lh\SportBody())
6    ->addEngine(new \Lh\V8Engine())
7    ->addDoor(new \Lh\SportDoor())
8;

We will find that no errors occur this time. This is because the traits are essentially language assisted copy and paste, which happens at the code execution. The self keyword thus refers to the CarFactory where it is used instead of the DoorTrait itself.

This can be useful if we use traits to reuse some functionality with fluent interface and various return types.

How to use declare methods in an interface if we want to use the self as our return type hint then?

We might declare methods with a concrete return type hint or don’t declare a type hint and override the declaration in a class which implements the interface:

 1<?php
 2
 3interface CarFactoryInterface
 4{
 5    public function addEngine(EngineInterface $engine): CarFactory;
 6    
 7    public function addDoor(DoorInterface $door);
 8    
 9    ...
10}

The whole code example can by found here



comments powered by Disqus