If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeCongrats on making it so far! Seriously: your work on this tutorial is going to make everything else you do make a lot more sense. Now, it's time to celebrate!
One of the best parts of Symfony is that it has a killer code generator. It's called "MakerBundle"... because shouting "make me a controller!" is more fun than saying "generate me a controller".
Let's get it installed. Find your terminal and run:
composer require "maker:^1.30" --dev
We're adding --dev
because we won't need the MakerBundle
in production, but that's a minor detail.
As I've mentioned so many times - sorry - bundles give you services. In this case, MakerBundle
doesn't give you services that you will use directly, like in your controller. Nope, it gives you services that power a huge list of new console commands.
When the install finishes, run:
php bin/console make:
Woh! Our app suddenly has a bunch of commands that start with make:
, like make:command
, make:controller
, make:crud
, make:entity
, which will be a database entity and more.
Let's try one of these! Let's make our own custom console command!
Because, in Symfony, you can hook into anything. The bin/console
executable
php bin/console
is no exception: we can add our own command here. I do this all the time for CRON jobs or data importing. To get started, run:
php bin/console make:command
Ok: it asks us for a command name. I like to prefix mine with app
: how about app:random-spell
. Our new command will output a random magical spell - very useful!
And... we're done! You can see that this created a new src/Command/RandomSpellCommand.php
file. Let's go check it out!
... lines 1 - 11 | |
class RandomSpellCommand extends Command | |
{ | |
protected static $defaultName = 'app:random-spell'; | |
protected function configure() | |
{ | |
$this | |
->setDescription('Add a short description for your command') | |
->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description') | |
->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') | |
; | |
} | |
protected function execute(InputInterface $input, OutputInterface $output): int | |
{ | |
$io = new SymfonyStyle($input, $output); | |
$arg1 = $input->getArgument('arg1'); | |
if ($arg1) { | |
$io->note(sprintf('You passed an argument: %s', $arg1)); | |
} | |
if ($input->getOption('option1')) { | |
// ... | |
} | |
$io->success('You have a new command! Now make it your own! Pass --help to see your options.'); | |
return 0; | |
} | |
} |
Cool! We can see the name on top, it has a description, some options... and, at the bottom, it ultimately prints a message. We'll start customizing this in a minute.
But before we do that... guess what? The new command already works! Run:
php bin/console app:random-spell
It's alive! This message is coming from the bottom of the new class.
But wait: how did Symfony instantly see our new command class and start using it? Is it because... it lives in a Command/
directory and Symfony scans for classes that live there? Nope! We could rename this to directory to DefinitelyNoCommandsInHere/
and it would still see the command.
The way this works is way cooler. Open up config/services.yaml
and look at the _defaults
section. We talked about what autowire: true
means, but I did not explain the purpose of autoconfigure: true
:
... lines 1 - 8 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
autowire: true # Automatically injects dependencies in your services. | |
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. | |
... lines 14 - 33 |
Because this is below _defaults
, autoconfiguration is active on all of our services, including our new command.
When autoconfiguration is enabled for a service, it basically tells Symfony:
Yo! Please look at the base class or interface of this service and if it looks like it should be a command, or an event listener or something else that hooks into Symfony, please automatically integrate it into that system. Thanks!
In other words, Symfony sees our service, notices that it extends Command
:
... lines 1 - 4 | |
use Symfony\Component\Console\Command\Command; | |
... lines 6 - 11 | |
class RandomSpellCommand extends Command | |
{ | |
... lines 14 - 41 | |
} |
And thinks:
I bet this is meant to be a console command. I'll just... hook it into that system automatically.
I love this because it means there is zero configuration needed to get things working. And you'll see this in a bunch of places in Symfony: you create a class, make it implement an interface and... it'll just start working. We'll see another example in a few minutes with a Twig extension.
Ok! Now that our command is working, let's customize it! Playing with console commands is one of my favorite things to do in Symfony. Let's go!
// composer.json
{
"require": {
"php": "^7.3.0 || ^8.0.0",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"sentry/sentry-symfony": "^4.0", // 4.0.3
"symfony/asset": "5.0.*", // v5.0.11
"symfony/console": "5.0.*", // v5.0.11
"symfony/debug-bundle": "5.0.*", // v5.0.11
"symfony/dotenv": "5.0.*", // v5.0.11
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.0.*", // v5.0.11
"symfony/monolog-bundle": "^3.0", // v3.6.0
"symfony/profiler-pack": "*", // v1.0.5
"symfony/routing": "5.1.*", // v5.1.11
"symfony/twig-pack": "^1.0", // v1.0.1
"symfony/var-dumper": "5.0.*", // v5.0.11
"symfony/webpack-encore-bundle": "^1.7", // v1.8.0
"symfony/yaml": "5.0.*" // v5.0.11
},
"require-dev": {
"symfony/maker-bundle": "^1.15", // v1.23.0
"symfony/profiler-pack": "^1.0" // v1.0.5
}
}