Interfaces with constructors

PHP sometimes allows us to do things that we shouldn't do. That's why we often have a discussion with other developers who tell us: "if PHP allows me to do it, then I'll do it". Here it helps to have good arguments as to why you shouldn't do something.

A very good example of this is the question of whether we should declare constructors in an interface. Technically, this is possible in the first place:

<?php declare(strict_types=1);

interface SomeInterface
{
    public function __construct(int $a, string $b);

    public function doWork(): void;
}
SomeInterface.php

If we want to implement this interface, then we have to implement both the constructor and the doWork() method:

<?php declare(strict_types=1);

class SomeImplementation implements SomeInterface
{
    private int    $a;
    private string $b;

    public function __construct(int $a, string $b)
    {
        $this->a = $a;
        $this->b = $b;
    }

    public function doWork(): void
    {
        var_dump('doing work');
    }
}
SomeImplementation.php

Now we can instantiate the object:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$object = new SomeImplementation(1, 'the-string');
$object->doWork();
createObject.php
SomeImplementation.php:16:
string(10) "doing work"
Output of createObject.php

What is the problem now? Does everything work?

Loose coupling

The problem is that interfaces are there to achieve loose coupling. Instead of making myself dependent on a concrete class, I only make myself dependent on an interface. This is why we normally use interfaces as type hints instead of concrete classes. An important exception to this rule are value objects, for which we generally do not define interfaces.

When we declare a constructor method in an interface, we are cementing assumptions about how an object must be created. In the example above, this is an integer as the first parameter and a string as the second parameter.

Normally PHP is relatively relaxed when we change the signature of a constructor:

<?php declare(strict_types=1);

class BaseClass
{
    private int    $a;
    private string $b;

    public function __construct(int $a, string $b)
    {
        $this->a = $a;
        $this->b = $b;
    }
}

class ChangingSignature extends BaseClass
{
    private array $x;
    private float $y;

    public function __construct(array $x, float $y)
    {
        $this->x = $x;
        $this->y = $y;
    }
}

var_dump(new BaseClass(1, 'the-string'));
var_dump(new ChangingSignature([], 1.5));
changingSignature.php

Here, the subclass declares a completely different constructor signature than the base class. Nevertheless, this works without PHP complaining:

changingSignature.php:27:
class BaseClass#1 (2) {
  private int $a =>
  int(1)
  private string $b =>
  string(10) "the-string"
}
changingSignature.php:28:
class ChangingSignature#1 (4) {
  private int $a =>
  *uninitialized*
  private string $b =>
  *uninitialized*
  private array $x =>
  array(0) {
  }
  private float $y =>
  double(1.5)
}
Output of changingSignature.php

You can of course discuss the extent to which the subclass in this example makes sense in terms of content, but that is not the point here. Instead, we ask ourselves whether this changing of the constructor signature still works if we have already defined the constructor in the interface?

<?php declare(strict_types=1);

class SomeOtherImplementation implements SomeInterface
{
    private array $x;
    private float $y;

    public function __construct(array $x, float $y)
    {
        $this->x = $x;
        $this->y = $y;
    }

    public function doWork(): void
    {
        var_dump('doing work');
    }
}
SomeOtherImplementation.php

Let's try our luck:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$object = new SomeOtherImplementation([], 1.5);
$object->doWork();
createOtherObject-will-fail.php
PHP Fatal error:  Declaration of SomeOtherImplementation::__construct(array $x, float $y) must be compatible with SomeInterface::__construct(int $a, string $b) in SomeOtherImplementation.php on line 8
PHP Stack trace:
PHP   1. {main}() createOtherObject-will-fail.php:0
PHP   2. {closure:autoload.php:6-27}($class = 'SomeOtherImplementation') createOtherObject-will-fail.php:5
PHP   3. require() autoload.php:25
Output of createOtherObject-will-fail.php

Of course, this does not work because the object SomeOtherImplementation does not fulfill the interface. Depending on the configuration, the IDE has already told us this:

So we restrict ourselves unnecessarily when we make a constructor method part of an interface. We could also say that object creation is a "separate concern".

Different implementations

It is usually the case that the different implementations of an interface have different dependencies. As an example, let's take an interface with which we can load Something from somewhere:

<?php declare(strict_types=1);

interface SomethingReaderInterface
{
    public function findById(string $id): Something;
}
SomethingReaderInterface.php

If we want to load Something from a database, we need a database connection. So:

<?php declare(strict_types=1);

class DatabaseSomethingReader implements SomethingReaderInterface
{
    public function __construct(
        private readonly DatabaseConnection $db
    ) {}

    public function findById(string $id): Something
    {
        return new Something($this->db->query('...'));
    }
}
DatabaseSomethingReader.php

Another reader, on the other hand, would have completely different dependencies:

<?php declare(strict_types=1);

class HttpSomethingReader implements SomethingReaderInterface
{
    public function __construct(
        private readonly HttpClient $http
    ) {}

    public function findById(string $id): Something
    {
        return new Something($this->http->get('...'));
    }
}
HttpSomethingReader.php

To make it really clear that the two readers are interchangeable, let's build a piece of code that randomly uses both of them alternately:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$db = new DatabaseConnection;
$readers[] = new DatabaseSomethingReader($db);

$http = new HttpClient;
$readers[] = new HttpSomethingReader($http);

for ($i = 0; $i < 5; $i++) {
    var_dump($readers[random_int(0, 1)]->findById('the-id'));
}

readSomething.php
readSomething.php:12:
class Something#6 (1) {
  private readonly array $state =>
  array(0) {
  }
}
readSomething.php:12:
class Something#6 (1) {
  private readonly array $state =>
  array(0) {
  }
}
readSomething.php:12:
class Something#6 (1) {
  private readonly array $state =>
  array(0) {
  }
}
readSomething.php:12:
class Something#6 (1) {
  private readonly array $state =>
  array(0) {
  }
}
readSomething.php:12:
class Something#6 (1) {
  private readonly array $state =>
  array(0) {
  }
}
Output of readSomething.php

If you now say that we can no longer see from which reader each Something was read: that's exactly the joke. That's exactly the loose coupling we wanted to achieve here.

So if we declare a constructor in the interface, we make life difficult for ourselves because we prevent ourselves from creating different implementations of this interface, each with different dependencies. Any attempt to save this, for example by not doing the dependency injection via the constructor or by defining a signature that wildly combines different dependencies as optional parameters, is impractical.

Conclusion: If we want loose coupling and therefore use an interface, then this interface must not make any restrictions with regard to object creation.

Never declare a constructor in an interface.