Controllers: Boring, Beautiful Services

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Head back to our trusty controller: src/Controller/QuestionController.php. It may be obvious, but it's worth mentioning that controllers are also services that live in the container. Yep, they're good, old, normal boring services that behave just like anything else. Well except that they have that one extra superpower that no other service has: the ability to autowire arguments into its methods. That normally only works for the constructor.

Using bind in Controller Arguments

Open up config/services.yaml. A few minutes ago, we added this global "bind" called bool $isDebug:

... lines 1 - 8
services:
# default configuration for services in *this* file
_defaults:
... lines 12 - 13
bind:
bool $isDebug: '%kernel.debug%'
... lines 16 - 33

Thanks to that, we can add a bool $isDebug argument to the constructor of any service and Symfony will pass us this value. But can we also add this argument to a controller method? Absolutely! In the controller, add another with this name: bool $isDebug. I'll dump that down here:

... lines 1 - 10
class QuestionController extends AbstractController
{
... lines 13 - 30
public function show($slug, MarkdownHelper $markdownHelper, bool $isDebug)
{
dump($isDebug);
... lines 34 - 47
}
}

Now, find you browser, go back to our show page, refresh and... that works wonderfully.

The point is: this ability to autowire arguments into a method is unique to controllers, but it works exactly the same as normal, constructor autowiring.

Constructor Injection

And because a controller is a normal, boring service, we can also use normal dependency injection. Remove the $isDebug argument. Let's pretend that we want to log something. This time, create a public function __construct() and give it two arguments LoggerInterface $logger and bool $isDebug. Like last time, I'll put my cursor on one of the arguments, hit Alt+Enter, and go to "Initialize properties" to create both of those properties and set them below:

... lines 1 - 5
use Psr\Log\LoggerInterface;
... lines 7 - 11
class QuestionController extends AbstractController
{
private $logger;
private $isDebug;
public function __construct(LoggerInterface $logger, bool $isDebug)
{
$this->logger = $logger;
$this->isDebug = $isDebug;
}
... lines 22 - 62
}

Down in the show() method, we can say something like if $this->isDebug, then $this->logger->info():

We are in debug mode!

If you refresh now, open the Profiler, and go to logs... there it is!

So... yeah! Controllers are normal services and, if you want to, you can entirely use "normal" dependency injection through the constructor. Heck the biggest reason that autowiring was added to the method was convenience. I usually autowire into my methods, but if you need a service in every method, using the constructor can help clean things up.

Next, let's talk about the final missing piece to configuration: environment variables!

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "sensio/framework-extra-bundle": "^5.5", // v5.5.4
        "sentry/sentry-symfony": "^3.4", // 3.4.4
        "symfony/asset": "5.0.*", // v5.0.8
        "symfony/console": "5.0.*", // v5.0.8
        "symfony/debug-bundle": "5.0.*", // v5.0.8
        "symfony/dotenv": "5.0.*", // v5.0.8
        "symfony/flex": "^1.3.1", // v1.9.10
        "symfony/framework-bundle": "5.0.*", // v5.0.8
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/var-dumper": "5.0.*", // v5.0.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.0.*" // v5.0.8
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.15", // v1.15.0
        "symfony/profiler-pack": "^1.0" // v1.0.4
    }
}