DEV Community

spO0q
spO0q

Posted on

PHP: errors to avoid with constructors

PHP developers love constructors 🫢.

You only need to implement a method named __construct() in your class.

Such method is considered "magical" in PHP, but it's even more special, as you can make it private unlike most magic methods.

Basic example

class MyClass
{
    public function __construct()
    {
    }
}

$myClass = new MyClass;
Enter fullscreen mode Exit fullscreen mode

__construct() is automatically called when the object class is instantiated.

Very common use cases

  • constructor property promotion (since PHP 8)
  • default values
  • setup and configuration

In a nutshell, it's mostly used to initialize things (e.g., properties), and you can also pass arbitrary arguments.

However, there are some traps to avoid.

Inheritance: implicit call to parent properties

❌ You may have seen the following implementation:

class MyParent
{
    public function __construct(public string $url = 'https://dev.to')
    {
    }
}

class MyChild extends MyParent
{
    public function __construct()
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

It's fine if you don't want to use the parent property in your child class:

$child = new MyChild();
echo $child->url;
Enter fullscreen mode Exit fullscreen mode

The above lines would trigger a fatal error:

Fatal error: Uncaught Error: Typed property MyParent::$url must not be accessed before initialization
Enter fullscreen mode Exit fullscreen mode

βœ… To fix that, call parent::__construct(); explicitly in your child constructor:

class MyParent
{
    public function __construct(public string $url = 'https://dev.to')
    {
    }
}

class MyChild extends MyParent
{
    public function __construct()
    {
        parent::__construct();
    }
}
Enter fullscreen mode Exit fullscreen mode

Parent constructors are not called implicitly if the child class defines a constructor. In order to run a parent constructor, a call to parent::__construct() within the child constructor is required.

Source : PHP documentation - constructors & desctructors

Inheritance: bad parents

❌ The following lines contain a sneaky error:

class MyParent {}

class MyChild extends MyParent
{
    public function __construct()
    {
        parent::__construct();
    }
}

$child = new MyChild();
Enter fullscreen mode Exit fullscreen mode

It triggers a fatal error:

Uncaught Error: Cannot call constructor
Enter fullscreen mode Exit fullscreen mode

βœ… To fix that, you have 2 choices:

  1. Adding a constructor to MyParent:
class MyParent 
{
    public function __construct() {}
}

class MyChild extends MyParent
{
    public function __construct()
    {
        parent::__construct();
    }
}

$child = new MyChild();
Enter fullscreen mode Exit fullscreen mode
  1. Removing all the unnecessary lines:
class MyParent {}

class MyChild extends MyParent {}

$child = new MyChild();
Enter fullscreen mode Exit fullscreen mode

If you want to use parent::__construct();, the parent must define it explicitly.

Don't get confused with the error message.

Inheritance: bad sequence

❌ The following implementation can introduce unexpected behaviors:

class MyParent 
{
    public function __construct()
    {
        // setup & config
    }
}

class MyChild extends MyParent
{
    public function __construct()
    {
        /* some code or additional initialization */

        parent::__construct();
    }
}
Enter fullscreen mode Exit fullscreen mode

That's because parent::__construct(); is at the end of the child constructor.

βœ… To prevent any issues, just call parent::__construct(); at the beginning of the child constructor:

class MyParent
{
    public function __construct()
    {
        // setup & config
    }
}

class MyChild extends MyParent
{
    public function __construct()
    {
        parent::__construct();

        /* some code or additional initialization */
    }
}
Enter fullscreen mode Exit fullscreen mode

Constructor property promotion: type callable

❌ When using constructor property promotion, some types become incorrect:

class MyClass
{
    public function __construct(public callable $func) {}
}
Enter fullscreen mode Exit fullscreen mode

You get a fatal error:

Fatal error: Property MyClass::$func cannot have type callable

βœ… For now, if you want to type with callable, use the old way:

class MyClass
{
    private $callback;

    public function __construct(callable $callback)
    {
        $this->callback = $callback;
    }
}
Enter fullscreen mode Exit fullscreen mode

Constructor property promotion: untyped properties

❌ When using constructor property promotion, you can actually use "untyped" properties:

class MyClass 
{
    public function __construct(public $title = 'untitled') {}
}

$class = new MyClass;
print_r($class->title);
Enter fullscreen mode Exit fullscreen mode

It does not throw any error.

βœ… However, it's recommended to type your property for better safety.

You only want strings for a title :

class MyClass 
{
    public function __construct(public string $title = 'untitled') {}
}
Enter fullscreen mode Exit fullscreen mode

Constructor property promotion: readonly

❌ You cannot promote readonly properties without typing them:

class MyClass 
{
    public function __construct(public readonly $title) {}
}

$class = new MyClass(title: 'untitled');
print_r($class->title);
Enter fullscreen mode Exit fullscreen mode

In this case, you get a fatal error:

Fatal error: Readonly property MyClass::$title must have type

βœ… Unlike "classic" properties, promoted readonly properties must be typed:

class MyClass 
{
    public function __construct(public readonly string $title) {}
}

$class = new MyClass(title: 'untitled');
print_r($class->title);
Enter fullscreen mode Exit fullscreen mode

Constructor: promoted readonly properties and default values

The following implementation is possible in PHP:

class MyClass 
{
    public function __construct(public readonly string $title = 'untitled') {}
}

$class = new MyClass;
print_r($class->title);
Enter fullscreen mode Exit fullscreen mode

Promoted readonly properties can have default values, unlike "classic" readonly properties.

In this specific case, it does not change the nature of readonly properties (immutability).

If you try to modify $title after object creation, you'll get a fatal error:

$class = new MyClass;
$class->title = 'title';
print_r($class->title);// Fatal error: Uncaught Error: Cannot modify readonly property MyClass::$title
Enter fullscreen mode Exit fullscreen mode

Inheritance & Visibility

❌ When extending a class that injects a protected dependency, you may want to modify the visibility to private:

class MyParent
{
    public function __construct(protected LoggerInterface $logger) {}
}

class MyChild extends MyParent
{
    public function __construct(private LoggerInterface $logger)
    {
        parent::__construct($logger);
    }
}
Enter fullscreen mode Exit fullscreen mode

It triggers a fatal error:

Fatal error: Access level to MyChild::$logger must be protected

Note that you may use a more permissive level: public.

While it does not trigger any error in this case and does not change anything for the parent class, there's no reason to do it.

βœ… Respect the parent class design
βœ… Don't remove the visibility keyword (it makes it public by default)

Don't promote all properties!

❌ Because constructor property promotion makes your life easier, you might not resist the temptation to use it everywhere.

There are use cases where it does not make sense:

  • complex logic for initialization
  • properties not initialized through the constructor
  • callable properties (for now, impossible)
  • abstract constructors (for now, impossible)

Frameworks: missing/impossible injection

You way encounter autowiring issues and more complex errors with frameworks.

❌ Not using dependency injection (e.g., manual instantiation) is a bad practice to avoid, as you bypass the autowiring system.

❌ Frameworks provide some magic, but you if you don't register your service properly (e.g., services.yaml), you cannot inject your dependencies.

βœ… Ensure that all dependencies are correctly injected in the constructor and added to the framework's service container.

Laravel: be careful with traits

There are obvious mistakes (e.g., fatal errors) and hidden errors (e.g., untyped properties).

Laravel runs internal optimizations, for example, during the boot process.

It also interacts with specific ORMs (e.g., Eloquent).

❌ When using specific PHP features, such as traits, instead of injecting custom services, you may trigger unexpected behaviors.

More practically, if you add a constructor to your trait:

  • you may kill observers and boot-time logic (e.g., Eloquent models)
  • you may encounter constructor collisions when the class uses multiple traits
  • you may break inheritance chains if the class uses the trait while extending another class
  • unit tests can be more challenging with traits

Source: Vincent Schmalbach - Laravel: Don’t Use Constructors in Traits.

βœ… Don't add constructors to your traits and use traits sparingly.

You'd rather rely on custom services (composition over inheritance) or value objects when it makes sense.

How to catch most errors with constructors

Static analysis is very efficient for automatic detection.

You cannot catch everything, especially if you're a new team member.

The idea is to run static analysis automatically to save time and energy, for example, in your pipelines.

It's best if it blocks any merge in the main branch when there are errors (e.g., when submitting pull requests).

There are robust tools you can plug to your CI/CD:

  • Psalm: composer require --dev vimeo/psalm
  • PHPStan: composer require --dev phpstan/phpstan

As these packages provide binaries, you can also analyze your code locally before pushing anything.

Conclusion

There's no coding convention you can apply blindly with constructors.

While some errors are quite explicit (fatal errors, warnings), other might be more subtle.

Fortunately, you can leverage static analysis to catch some of them.

Top comments (0)