Doctrine Entity Typed Properties With PHP 7.4

This post was updated at November 2020 with fresh know-how.
What is new?

Switch from deprecated --set option to rector.php config. Updated with PHPStorm and PHPStan friendly Collection syntax and Rector rule that handles the change for you.


Recently we've upgraded our Czech PHP community website to PHP 7.4. As a side effect, it broke most of our entities. Do you love how making language more strict reveals weak points in your code just by using it?

Today we'll look at the impact of typed properties on weak points of Doctrine entities and how to solve them.

The Collections

In the Czech PHP community, we have skilled trainers that share their knowledge with others on training. We help them to share the knowledge by handling the tedious organization processes for them and let them enjoy the training day itself.

Each trainer has many trainings, so the Trainer entity looks like this in PHP 7.3-:

<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Trainer
{
    /**
     * @ORM\OneToMany(targetEntity=Training::class, mappedBy="trainer")
     * @var Collection|Trainer[]
     */
    private $trainings = [];

    /**
     * @param Collection<int, Training>|Training[] $collection
     */
    public function setTrainings(array $collection): void
    {
        $this->trainings = $collection;
    }

    /**
     * @return Collection<int, Training>|Training[]
     */
    public function getTrainings(): iterable
    {
        return $this->trainings;
    }
}

What can we say about this code?

How do we add property types without breaking everything?

1. The Property

In PHP 7.4, the property is the king - the rest of the code has to respect the type, and it's the default value. Let's start with that.

 /**
  * @ORM\OneToMany(targetEntity=Training::class, mappedBy="trainer")
  * @var Collection|Trainer[]
  */
-private $trainings = [];
+private array $trainings = [];
+private iterable $trainings = [];
+private Collection $trainings = [];

What is the type here?

Pick one...




 /**
  * @ORM\OneToMany(targetEntity=Training::class, mappedBy="trainer")
  * @var Collection<int, Trainer>|Trainer[]
  */
-private $trainings = [];
+private Collection $trainings = [];

This one worked the best (= code worked).

But PHP 7.4 now complains that the Collection object cannot be [] by default.

 /**
  * @ORM\OneToMany(targetEntity=Training::class, mappedBy="trainer")
  * @var Collection<int, Trainer>|Trainer[]
  */
-private Collection $trainings = [];
+private Collection $trainings;

But PHP 7.4 now complains that it's null by default, so it has to nullable.

If it's not enforced, nobody cares.

Here is where best practices become defaults. Do you know the "initialize collections in the constructor" best practice?

This was an optional improvement to help Doctrine work in a more reliable way. Well, it was. Now we have to use it to make our code work:

 <?php

 declare(strict_types=1);

 namespace App\Entity;

 use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;

 /**
  * @ORM\Entity
  */
 class Trainer
 {
     // ...

+    public function __construct()
+    {
+        $this->trainings = new ArrayCollection();
+    }

     // ...
 }

All right, we have the correct type, it's initialized in the constructor.

Our property is ready!


2. Getter Method?

<?php

// ...

/**
 * @return Collection<int, Training>|Training[]
 */
public function getTrainings(): iterable
{
    return $this->trainings;
}

What about the return type? Look at the property type:

 <?php

 /**
  * @return Collection<int, Training>|Training[]
  */
-public function getTrainings(): iterable
+public function getTrainings(): Collection
 {
     return $this->trainings;
 }

And we're ready to go!

3. Setter Method?

I bet you handled this already from the top of your head, so let's compare:

 <?php

 /**
  * @param Collection<int, Training>|Training[] $trainings
  */
-public function setTrainings(array $trainings): void
+public function setTrainings(Collection $trainings): void
 {
     $this->trainings = $trainings;
 }

Do you want to see real code? Look at the full pull request:

EasyAdminBundle?

Have you tried EasyAdminBundle to delegate your full administration? If not, give it go.

It creates data grids, forms, edit/update/add/delete controllers. All this with simple and beautiful UX. All you need to do is define your entities and register them in YAML config. I love it!


Huge thanks to Javier Eguiluz, creator and maintainer of this bundle, who's also behind amazing 690 issues of Weeks of Symfony.


To make this all happen, the design choice is to allow every property to be nullable. Some people disagree and try to make EasyAdminBundle work with value objects. But they fail by killing the simplicity and re-inventing admin again.

There are no best solutions, there are just trade offs.

So what does every property is nullable mean for PHP 7.4 typed properties?

<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Training
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
-    * @var int
     */
-   private $id;
+   private ?int $id = null;

    // ...
}

Do you prefer code over text? Here is the PHP 7.4 upgrade pull-request.

Upgrade Instantly with Rector

I did not do the upgrade pull-request myself (I'm way too lazy for that), Rector did. Thanks to testing out Rector on Doctrine entities, its PHP 7.4 set got much more precise.

Do you want to see how Rector can upgrade your code?

  1. Install Rector
composer require rector/rector --dev
  1. Create rector.php config
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\Set\ValueObject\SetList;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->import(SetList::PHP_74);

    // Protip: Do you want to update your `Collection` syntax for PHPStorm and PHPStan friendly?
    $rectorConfig->import(DoctrineSetList::DOCTRINE_CODE_QUALITY);
};

If you got any troubles, let us know on GitHub. That's all, folks.


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!