Symfony/Doctrine’s Docs has caused more bugs than anything else.

Symfony and Doctrine’s documentation has a slight flaw: they don’t use constructors for DTOs.

At first blush, this seems rather benign. However, after reviewing code for several candidates and even on my own team that was here before me, I see this pattern more and more. After some research, I discovered that the fact you can even use constructors is buried in Doctrine’s documentation. It’s not a very well-known fact.

For example, let’s consider this bit of documentation from Symfony on “persisting objects to the database”:

class ProductController extends AbstractController
{
    #[Route('/product', name: 'create_product')]
    public function createProduct(EntityManagerInterface $entityManager): Response
    {
        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(1999);
        $product->setDescription('Ergonomic and stylish!');

        // tell Doctrine you want to (eventually) save the Product (no queries yet)
        $entityManager->persist($product);

        // actually executes the queries (i.e. the INSERT query)
        $entityManager->flush();

        return new Response('Saved new product with id '.$product->getId());
    }
}

Whoever created the Product object and database schema, hopefully considered that a name, price, and description could be unset. From here, we have no idea what Product requires to actually be persisted or even be a valid Product. We can construct an arbitrary invalid Product without even knowing. Worse, we can change Product to have new required properties and we may not know until after the code is deployed to production — though hopefully, a test will catch it.

Worse, when the error does show up, it’ll be from when the object persisted, which may be long after the object was created. The stack trace will be virtually worthless.

Instead, you should be using constructors to enforce the fact that only a valid object can be created:

class ProductController extends AbstractController
{
    #[Route('/product', name: 'create_product')]
    public function createProduct(EntityManagerInterface $entityManager): Response
    {
        $product = new Product(
            name: 'Keyboard', 
            price: 1999,
            description: 'Ergonomic and stylish!'),
        );

        // tell Doctrine you want to (eventually) save the Product (no queries yet)
        $entityManager->persist($product);

        // actually executes the queries (i.e. the INSERT query)
        $entityManager->flush();

        return new Response('Saved new product with id '.$product->getId());
    }
}

From there, if you add a new ‘required’ property, any static analysis will catch it immediately. You don’t even need tests to discover what broke.

The number of bugs I’ve seen from simply missing a construction, is uncountable. It makes development harder than it needs to be, and is plain annoying to go digging through the definition of an object.

Their documentation really needs to be updated, showing use of constructors instead of getters/setters for required properties, so that new devs using the framework A) know they can even use them, and B) we all end up with a better developer experience and less bugs.

Want an inside scoop?

Check out PHP Shenanigans for only €5/mo or €30/yr