How Symfony 4 Makes You A Better Developer


We've covered a lot of topics in just a few short videos, and you're now well on your way towards a better understanding of Symfony 4.

To finish off we're going to look at one of the most fun, yet potentially confusing parts of Symfony: services.

Throughout this short series we've frequently referred back to Symfony's Best Practices.

Before going further: These best practices are guidelines, not rules. Rules can be bent, or broken. But adhering to them will make your and your fellow team member's lives easier, most of the time.

Best practices should be treated exactly the same way, in my opinion.

symfony-4-controller-best-practices-abstractcontroller

Extend AbstractController

We'll start with a change we made all the way back in the second video.

We generated our WelcomeController, and then immediately made the following two changes:

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
-use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\HttpFoundation\Response;

-class WelcomeController extends Controller
+class WelcomeController extends AbstractController
{
    /**
     * @Route("/welcome", name="welcome")
     */
    public function index()
    {
        // replace this line with your own code!
        return $this->render('@Maker/demoPage.html.twig',
          'path' => str_replace($this->getParameter('kernel.project_dir').'/', '', __FILE__)
        ]);
    }
}

I mentioned we remove the use ... Response statement as it's unused in the generated code.

We didn't cover why we made the AbstractController change.

Lets.

Why AbstractController

Symfony can be a mysterious beast.

We, as consumers of the framework, get immediate access to the wisdom and benefit of code created, and peer-reviewed by some of the smartest developers writing PHP code today.

For this reason, and many more, I'm sure you will all join with me in saying a huge THANK YOU to everyone involved in the Symfony community.

Thinking about this, it's implicit that we trust these developers to do smart things on our behalf.

It doesn't necessarily follow that we understand why they do the things they do. It also doesn't mean that everything they do will directly benefit us.

As a side note: This is part of the downside to using a framework. We get given all the stuff we do need, and potentially a lot of extra stuff we don't.

That's why Symfony 4 originally only came with the symfony/skeleton.

We should only install the bits we want.

But this makes life harder for a beginner, or person less involved in the intricacies of the framework.

Some of us just want to use a good set of defaults, and compromise that perhaps we get a little extra bloat but are happy to pay that small price.

Ok, but what's all that got to do with AbstractController, specifically?

The New Approach

A big push in Symfony 3.x was to get autowiring and autoconfiguration added. Whether you have an opinion on these, or not, that's the way things have gone.

Controllers are now services by default.

They do have some special configuration:

# config/services.yaml

parameters:
    locale: 'en'

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.
        public: false       # Allows optimizing the container by removing unused services; this also means
                            # fetching services directly from the container via $container->get() won't work.
                            # The best practice is to be explicit about your dependencies anyway.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/*'
        exclude: '../src/{Entity,Migrations,Tests}'

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones

This likely brings many more benefits than directly affect me, but let me cover the part that I most appreciate:

One set way.

As has been covered already in this short series, there have been a few times where there are multiple ways to achieve one outcome.

Sometimes this is good. Increased flexibility, and the ease of being able to "roll our own" is a big selling point.

Other times we make our code accidentally worse, but still "within the rules".

Am example of this would be in using both parameters, and services in a typical Symfony 3 application:

<?php

namespace AppBundle\Controller;

use AppBundle\Service\SomeService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class SupportController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(SomeService $someService)
    {
        $someParameter = $this->container->getParameter('app.my.parameter');

        $someService->doSomethingWith($someParameter);

Again, not saying this is good or bad code. This is valid code.

We're injecting one thing but asking for another.

Most of the time, my life as a maintainer of code has been made easier when a function does one thing.

We have the option here of telling our indexAction function / method about our dependents.

Or we can make it ask for them.

My suggestion to you here is: Tell, don't ask.

Pick one, and stick to it.

Symfony suggest injection (that's Tell btw).

You can inject parameters. But there is a bit of extra work required.

Example

Let's add our own custom parameter:

# config/services.yaml

parameters:
    locale: 'en'
+   app.mySweetParam: 'nerding out on Symfony is fun'

Before we can pass this parameter into a service, we need to create a new service.

As we currently send email from our controller method, now seems as good a time as any to extract this process to a service. Our Emailer service.

mkdir src/Mailer
touch src/Mailer/Emailer.php

The Emailer is just an empty file at this point. There are no built in generators to make services for us. Every service is different, as every application's purpose is different.

The plan is our Emailer will create Swift_Message for us, which to begin with, we will also send via the Emailer.

Now open up src/Mailer/Emailer.php and add the following:

<?php

namespace App\Mailer;

class Emailer
{
    public function create()
    {

    }
}

Our Emailer::create method needs to do whatever it is we need it to do, and crucially return us an instance of \Swift_Message. That's the gist of our "business requirement" for this process.

Let's use a PHP 7 feature, and declare a return type on our method signature:

<?php

namespace App\Mailer;

class Emailer
{
-   public function create()
+   public function create() : \Swift_Message

Note you don't need a use statement for \Swift_Message as it is, somewhat unusually, in the root namespace.

Before implementing any further functionality, let's inject our new parameter.

We'll do this by declaring a __construct function for our Emailer class:

<?php

namespace App\Mailer;

class Emailer
{
+   public function __construct()
+   {
+   }
+
    public function create() : \Swift_Message
    {

    }
}

And we know our parameter is called mySweetParam, so how can we inject that?

<?php

namespace App\Mailer;

class Emailer
{
-   public function __construct()
+   public function __construct(string $mySweetParam)

I've said already in this series, Symfony, as good as it is, it is not a mind reader.

Yes, we have named our variable $mySweetParam. Yes, we have type hinted it as a string.

No, this is not - quite - sufficient for Symfony to connect the dots between the app.mySweetParam parameter we defined in config/services.yaml, and this variable name.

We need to give it some help.

We're going to add a custom entry under the services key in config/services.yaml:

services:
    # other stuff

    App\Mailer\Emailer:
        arguments:
            $mySweetParam: "%app.mySweetParam%"

This is enough to tell Symfony what we mean by $mySweetParam. Your variable name is not important. But if you change it in the arguments to your service definition, be sure to use the same name in the Emailer::__construct method.

We're not actually using this service anywhere though.

And Symfony doesn't new up every configured service in our project for every incoming request. That would be bad.

We can trick Symfony into constructing the Emailer though.

All we need to do is inject it into a method that is being called, and we can test this.

Let's use src/Controller/WelcomeController.php.

I will inject Emailer, but not actually use it anywhere in the controller method:

<?php

namespace App\Controller;

+use App\Mailer\Emailer;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class WelcomeController extends AbstractController
{
    /**
     * @Route("/", name="welcome")
     */
-   public function index()
+   public function index(Emailer $emailer)
    {

We do need a way to visually validate what we think should happen here.

There are numerous ways.

We could dump the variable right there in the __construct method body.

Or we could inject the configured Psr\Log\LoggerInterface implementation (which is Monolog btw, in Symfony).

Let's do both, because why not?

<?php

namespace App\Mailer;

+use Psr\Log\LoggerInterface;

class Emailer
{
-    public function __construct(string $mySweetParam)
+   public function __construct(string $mySweetParam, LoggerInterface $logger)
    {
+       $logger->alert('BOOM!');
+       $logger->debug($mySweetParam);
+
+       dump($mySweetParam);
    }

Now, from your terminal / shell:

cd {project_root}
tail -f var/log/dev.log

Then browse to:

http://127.0.0.1:8000 - aka the "Welcome Page", and we see two things:

  • On the web debug toolbar, we see the crosshair icon, which mousing over shows "nerding out on Symfony is fun" (aka the dump output)
  • On the shell you should see:
tail -f var/log/dev.log

[2018-01-19 12:40:00] app.ALERT: BOOM [] []
[2018-01-19 12:40:00] app.DEBUG: nerding out on Symfony is fun [] []

Notice that at no point did we have to write any service configuration for the Emailer. The default service configuration in Symfony 4 is automatically wiring up these services with any dependencies they may need. We only need to intervene if it needs any help, or our needs are quite specific.

Coming Full Circle

"That's all very interesting, Chris", you might say, "but what's any of this got to do with AbstractController?"

Let's go back to our Symfony 3.x example:

<?php

namespace AppBundle\Controller;

use AppBundle\Service\SomeService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class SupportController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(SomeService $someService)
    {
        $someParameter = $this->container->getParameter('app.my.parameter');

        $someService->doSomethingWith($someParameter);

We know now that we don't need to ask the container for a parameter.

We can inject it.

If we don't need to ask for things from the container, we really don't need to be ContainerAware anymore.

The generated controller class extends Controller, which is use ContainerAwareTrait;.

We don't need that trait.

We're injecting everything.

Therefore we can remove that dependency.

AbstractController is essentially giving us the same functionality, but removing the ability for us two mix tell and ask.

It is more restrictive.

But it has your best interests in mind. Even if you didn't know it.

symfony-4-abstractcontroller-poll

Anyway even if you didn't read any of that, or your read it and need confirmation then don't take my word for it, take Nicholas'.

That said, because we continue to use the ControllerTrait, if we use any of the convenience methods then we are still using the container as before, just indirectly. You can remove your reliance on even AbstractController and explicitly define any specific dependencies, if you wish to do so.

Code For This Course

Get the code for this course.

Episodes