Work in the constructor

The constructor should put an object into a workable state. If this is not possible, the constructor should throw an exception. This prevents an object from existing in an invalid state.

<?php declare(strict_types=1);

final class SomethingPositive
{
    private int $positiveValue;

    public function __construct(int $mustBePositive)
    {
        if ($mustBePositive < 0) {
            throw new InvalidArgumentException('The greatest teacher, failure is.');
        }

        $this->positiveValue = $mustBePositive;
    }

    // ...
}
SomethingPositive.php

In this example, we know that the value stored in the positiveValue attribute is positive if the SomethingPositive object exists. The object is in a guaranteed valid state or we could not create it in the first place:

createObject.php:6:
class SomethingPositive#2 (1) {
  private int $positiveValue =>
  int(1)
}
Output of createObject.php
Execute createObject.php

As long as the constructor parameter is positive, this works. But woe betide us if we try to sell our software a negative value as positive:

PHP Fatal error:  Uncaught InvalidArgumentException: The greatest teacher, failure is. in SomethingPositive.php:10
Stack trace:
#0 createObject-will-fail.php(5): SomethingPositive->__construct()
#1 {main}
  thrown in SomethingPositive.php on line 10
Output of createObject-will-fail.php
Execute createObject-will-fail.php

However, a constructor should only initialize an object and not do any "real work". "Real work" would be expensive calculations, for example, because these make object creation time-consuming and therefore slow.

No real work in the constructor, and certainly no I/O.

Expensive initialization

If you are of the opinion that you have an object that needs to be initialized expensively, then you should opt for lazy initialization instead. In this case, the expense is only incurred when an object is actually used and we do not speculatively execute code whose results are not needed at all.

In the following example, we generate random numbers in the constructor, which can then be retrieved from the object one after the other. The whole thing is admittedly a bit pointless and not really expensive, but I wanted to have a non-trivial example, hence the position. And let's just pretend that generating 1000 random numbers is super expensive:

<?php declare(strict_types=1);

final class ExpensiveConstructor
{
    private int   $position     = 0;
    private array $randomValues = [];

    public function __construct()
    {
        for ($i = 0; $i < 1000; $i++) {
            $this->randomValues[] = random_int(1, 1000);
        }
    }

    public function getRandomValue(): int
    {
        $position = $this->position;
        $this->position++;

        return $this->randomValues[$position];
    }
}
ExpensiveConstructor.php

Let's try it out:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$randomValues = new ExpensiveConstructor;

for ($i = 0; $i < 3; $i++) {
    var_dump($randomValues->getRandomValue());
}
expensiveRandomValues.php
expensiveRandomValues.php:8:
int(99)
expensiveRandomValues.php:8:
int(335)
expensiveRandomValues.php:8:
int(422)
Output of expensiveRandomValues.php
Execute expensiveRandomValues.php

Works, but was expensive. We have generated 997 random numbers for free.

We can modify this so that the array is only initialized when we actually access it:

<?php declare(strict_types=1);

final class LazyInitialization
{
    private int    $position     = 0;
    private ?array $randomValues = null;

    public function getRandomValue(): int
    {
        $this->initialize();

        $position = $this->position;
        $this->position++;

        return $this->randomValues[$position];
    }

    private function initialize(): void
    {
        if ($this->randomValues !== null) {
            return;
        }

        for ($i = 0; $i < 1000; $i++) {
            $this->randomValues[] = random_int(1, 1000);
        }
    }
}
LazyInitialization.php

Essentially, we have only moved what was previously in the constructor to the initialize() method. This is a very good example of how we often write the right code but do so in the wrong place.

Of course, different random numbers come out this time, but this does not detract from the fact that the whole thing works as expected:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$randomValues = new LazyInitialization;

for ($i = 0; $i < 3; $i++) {
    var_dump($randomValues->getRandomValue());
}
lazyRandomValues.php
lazyRandomValues.php:8:
int(513)
lazyRandomValues.php:8:
int(761)
lazyRandomValues.php:8:
int(737)
Output of lazyRandomValues.php
Execute lazyRandomValues.php

If we look at LazyInitialization with understanding, it becomes clear that there is no point in generating 1000 random numbers in advance. We can simply generate them individually on demand:

<?php declare(strict_types=1);

final class AdHoc
{
    public function getRandomValue(): int
    {
        return random_int(1, 1000);
    }
}
AdHoc.php

This should also work:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$randomValues = new AdHoc;

for ($i = 0; $i < 3; $i++) {
    var_dump($randomValues->getRandomValue());
}
adHocRandomValues.php
adHocRandomValues.php:8:
int(959)
adHocRandomValues.php:8:
int(207)
adHocRandomValues.php:8:
int(45)
Output of adHocRandomValues.php
Execute adHocRandomValues.php

Yes. It's nice when it becomes so clear that the original example was not only stupid, but also unnecessarily complicated. But it is, of course, a nice plea for not initializing objects in advance, but rather executing calculations on demand if possible.

Under no circumstances should the constructor do I/O, for example by establishing a database connection, communicating with a remote service or accessing the file system. This would not only be particularly slow, but would also make it more difficult to reuse the object because we would have to fulfill external dependencies before creating the object.

Here is an example of the lazy initialization design pattern in conjunction with database connections.