symfony/maker-bundle
The Symfony MakerBundle is the fastest way to generate common Symfony code like commands, controllers, form classes, and event subscribers. It produces working skeletons that follow current best practices and target the latest Symfony LTS.
MakerBundle is a dev-time scaffolder: it registers make:* console commands that write working, best-practice boilerplate into your app. In a Flex project it's already there; otherwise:
composer require --dev symfony/maker-bundle
php bin/console list make
Look first at the command help for whatever you're about to build (php bin/console make:controller --help) and at the official docs on symfony.com (/bundles/SymfonyMakerBundle/). The README's "Scope" section is the most important page: the bundle intentionally targets the latest Symfony LTS only (1.68 bumped to Symfony 7.4+ / PHP 8.2+) and generates code against that LTS's recommended defaults.
First use case — the classic vertical slice in a fresh app:
That's make:entity → make:migration → make:crud → make:test, and it's the flow worth internalizing because every later maker is a variation on "generate a class + wire config + optionally generate a test."
Generate, then edit — don't re-generate. Makers are one-shot skeletons, not a source of truth. Run once, own the file, and only re-run for new artifacts (a new entity, a new field). Re-running over an edited class produces conflicts or overwrites.
Incremental make:entity is the normal workflow. You run it repeatedly as the model grows, and 1.68 added non-interactive --field / --relation options so field additions can be scripted:
php bin/console make:entity Product --field=title:string:255 --no-interaction
php bin/console make:entity Product --relation=category:ManyToOne:Category --no-interaction
Also new: PHP 8.4 property-hook support, --with-uuid / --with-ulid honoured with a name argument, and generate_final_entities config applied to generated entities.
Auth stack as a sequence. make:user → make:security:form-login (or make:security:custom) → make:registration-form → make:reset-password → make:voter. Each step reads what the previous one wrote (security.yaml, the User class), so the order matters and hand-editing security.yaml mid-sequence confuses the interactive helpers.
Companion makers for day-two work: make:command (now invokable by default, and it asks for arguments/options — a real time saver), make:message + make:handler for Messenger, make:schedule, make:subscriber/make:listener, make:twig-extension (attribute-based since 1.68), make:webhook, and the new make:decorator for service decoration. make:migration --nowdoc if you want heredoc-free SQL.
Scripted/CI usage. Treat maker invocations like any other console command: put them in a Makefile or a bootstrap script with --no-interaction, and always pass the class name as an argument so nothing prompts. Since 1.68 fixed --no-interaction crashes across CRUD, controller, security, form-login, registration, reset-password, schedule, twig-extension, webhook and message, pin >=1.68 if you automate — older 1.6x versions will blow up on typed properties.
Only two supported config knobs. config/packages/maker.yaml exposes root_namespace (now validated) and generate_final_entities. Everything else — custom templates, alternative target dirs, pluggable generators — is explicitly out of scope per the README, which points you at Symfony Mate / coding agents instead. If you genuinely need a new generator, write a maker class extending AbstractMaker, delegate from a console command, and test it with MakerTestCase (improved in 1.66 to discover the root package and resolve makers via a ServiceLocator).
The BC promise is deliberately loose. Both the command's input arguments/options and the generated code itself may change in minor releases. Consequences: never diff generated output in CI, never assume a flag is stable, and expect tutorials to be stale. Recent examples of churn: make:form re-added the Type suffix (1.64, reverting 1.63), make:crud switched to CamelCase getters (1.66), and make:command flipped to invokable-by-default (1.68).
The --no-interaction trap (if you write makers). Command::run() skips interact() entirely, so any value stored only on a typed property is uninitialized in generate() → Typed property ... must not be accessed before initialization. The rule from the bundle's own AGENTS.md: never keep a value on a property; put it on the InputInterface as an option/argument, have interact() fill it only when missing, and have generate() read from the input with a fallback:
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
$class = $input->getOption('controller-class') ?: self::getDefaultControllerClass();
}
Corollaries: extract the "silent half" of guessers like InteractiveSecurityHelper::guessUserClass() into a method returning null rather than prompting, and have generate() throw when it comes back null. Throw RuntimeCommandException for user-facing failures — ConsoleErrorSubscriber renders it on stdout with exit 1; an \InvalidArgumentException (e.g. from Validator::*) goes to stderr and is invisible to anyone capturing stdout.
Booleans must be VALUE_NONE flags. No maker file declares strict_types, so a string fed to a bool parameter coerces silently — "false" and "no" both become true.
Never call installDependencyIfNeeded() from both interact() and generate(). Its class_exists() guard can't see a package installed earlier in the same process, so the second call runs composer require again and corrupts the DI container cache mid-run.
Debugging a maker run. Use -vvv for the full stack, and check php bin/console make:foo --help before assuming an option name — several were renamed/added recently (make:crud --controller-class, fixed make:controller -i conflict with APP_ID in 1.67). If PHPUnit isn't installed, --with-tests used to crash; that's fixed in 1.68, so upgrade rather than hand-patching.
Cross-platform nit. In tests and scripts, Windows cmd.exe doesn't treat single quotes as quoting — use double quotes for maker arguments.
How can I help you explore Laravel packages today?