Reading Generated Code

How to review a generated form class and migration before you commit them, so nothing enters the project unread.

Syntaxphp bin/console make:migration [--formatted]

Generated code is yours the moment it is written. Nobody will come back to it; no upgrade will fix it. The habit that keeps a project honest is small: generate, read every file, edit, then commit.

Reviewing a generated form

Three things to check. First, the field list: the generator adds one field per property of the class, including the ones no user should set, such as a timestamp or a slug. Delete those. Second, the field types: ->add('title') with no type leaves the type to be worked out from the class definition at runtime, which is convenient and invisible. Writing the type out, ->add('title', TextType::class), is the same field with the guess recorded. Third, the labels, which default to a humanised property name.

Reviewing a generated migration

A migration is a PHP class holding the SQL that moves a database from one shape to the next. make:migration writes one by comparing your classes against the database your CLI is connected to. So read the SQL, not the fact that a file appeared. Check that up() contains only changes you meant, that nothing is dropped by accident, and that down() really reverses up(). Fill in getDescription(), which is generated empty.

The habit

Commit generated files in one commit and your edits in the next. Then the diff a reviewer reads is the part a human wrote.

Example

Example · php
<?php
// src/Form/ArticleType.php - after review

declare(strict_types=1); // the generator does not write this; add it

namespace App\Form;

use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // Generated shorthand: ->add('title')
            // No type given, so the type is worked out at runtime.
            // Longhand: the same field with the type and label written down.
            ->add('title', TextType::class, [
                'label' => 'Headline',
            ])
            ->add('body', TextareaType::class, [
                'required' => false,
            ])
            // Generated and removed: ->add('createdAt') and ->add('slug').
            // Neither is for a user to type; both are set in code.
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            // Binds the form to Article, so a submitted form fills an
            // Article object instead of handing you an array.
            'data_class' => Article::class,
        ]);
    }
}

When to use it

  • Strip fields such as slug or createdAt out of a generated form before it is ever shown.
  • Read a generated migration for unintended DROP statements before it runs anywhere shared.
  • Write the migration description that the generator leaves empty.
  • Split a generator's output and your edits into two commits so the review is readable.

More examples

A generated migration, with the review notes in place

The class name is a timestamp, which is also the order migrations run in. The generated file is correct PHP and may still be the wrong SQL, because it describes the difference between your classes and one particular database. Review it the way you would review SQL a colleague wrote: statement by statement, and with the question of what happens if it runs against a table that already holds rows. Writing a real description costs a line and pays back the first time someone reads the migration list.

Example · php
<?php
// migrations/Version20261003091500.php - as generated by make:migration

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
final class Version20261003091500 extends AbstractMigration
{
    public function getDescription(): string
    {
        // Generated empty. Say what this does, for whoever reads the list.
        return 'Create the article table';
    }

    public function up(Schema $schema): void
    {
        // this up() migration is auto-generated, please modify it to your needs
        // Exact SQL depends on the database platform; trimmed here.
        $this->addSql('CREATE TABLE article (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
        // Read every statement. A DROP you did not expect usually means your
        // local database was ahead of your classes, not that the column should go.
    }

    public function down(Schema $schema): void
    {
        // this down() migration is auto-generated, please modify it to your needs
        $this->addSql('DROP TABLE article');
    }
}

The generated form, before the review

Compare this with the reviewed version: no `declare(strict_types=1)`, no types, no labels, and two fields that should never have been on a public form. None of it is a bug in the generator, which did the only thing it could with the information it had. It is a list of decisions handed to you, and the review is where you make them.

Example · php
<?php
// src/Form/ArticleType.php - exactly as make:crud wrote it

namespace App\Form;

use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title')
            ->add('body')
            ->add('slug')
            ->add('createdAt')
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Article::class,
        ]);
    }
}

Seeing what the generator touched

`git add -N .` registers new files without staging their contents, which is what makes them show up in `git diff` at all; a generated file is otherwise invisible to it. Splitting the commits is the part worth keeping as a habit. A single commit of nine generated files plus your three edits is unreviewable, so nobody reviews it, and that is how a project fills up with code nobody has read.

Example · bash
# generate, then read before anything else
php bin/console make:crud Article

# every new and changed file in one diff
git status --short
git add -N .      # include new files in the diff without staging them
git diff

# commit the generated code on its own
git add src/Controller/ArticleController.php src/Form/ArticleType.php templates/article
git commit -m "Add generated Article CRUD"

# then edit, and commit again: this diff is the part a human wrote

Discussion

  • Be the first to comment on this lesson.