Fixing Deprecations In Our Own Code


Deprecations Of Our Own Creation

The previous changes we made were to fix problems with code provided by Symfony.

Most of the warnings in most of my applications come from code of my own creation, or from bundles I am using.

Fixing our own problems is fairly straightforward in the majority - if quite a lot of extra 'chore' work.

Fixing bundle problems, as we shall see shortly, is potentially harder.

Now, one thing to note before continuing:

In a real world project making changes as sweeping as these is risky. If you do not have a reliable test suite then there is a high probability of introducing bugs at this stage. My strongest advice would be to make these changes on a new git branch, at the very least.

User Deprecated: The github_api service is private, getting it from the container is deprecated since Symfony 3.2 and will fail in 4.0. You should either make the service public, or stop using the container directly and use dependency injection instead.

The github_api service is something specific to our project.

The advice we have been given is useful:

You should either make the service public, or stop using the container directly and use dependency injection instead.

Well, it's useful if you've been using Symfony for a while. If you're new then it's not quite so useful.

What this is telling us is that we're doing this:

<?php

// src/AppBundle/Controller/GitHutController.php

namespace AppBundle\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class GitHutController extends Controller
{
    // ...

    /**
     * @Route(/profile/{username}, name=profile)
     */
    public function profileAction($username)
    {
        $profileData = $this->get('github_api')->getProfile($username);

        return $this->render('githut/profile.html.twig', $profileData);
    }

And instead we should be doing this:

<?php

// src/AppBundle/Controller/GitHutController.php

namespace AppBundle\Controller;

use AppBundle\Service\GitHubApi;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class GitHutController extends Controller
{
    // ...

    /**
     * @Route(/profile/{username}, name=profile)
     */
    public function profileAction($username, GitHubApi $api)
    {
        $profileData = $api->getProfile($username);

        return $this->render('githut/profile.html.twig', $profileData);
    }

The change is somewhat subtle.

We now inject the GitHubApi service, rather than ask for it in the controller action. Tell, don't ask.

Injecting dependencies makes some kinds of testing easier, arguably makes our code more explicit, and is the new, preferred approach.

So far, so good.

The problem is this code won't work without adding in the correct config added to services.yml.

Enter Autowiring By Default

I've have a video on the Dependency Injection Changes introduced in Symfony 3.3.

The gist of this change is that the default provided services.yml file changed from:

# app/config/services.yml

parameters:
#    parameter_name: value

services:
#    service_name:
#        class: AppBundle\Directory\ClassName
#        arguments: [@another_service_name, plain_value, %parameter_name%]

to:

# app/config/services.yml

parameters:
    #parameter_name: value

services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Repository,Tests}'

    # controllers are imported separately to make sure they're public
    # and have a tag that allows actions to type-hint services
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

    # add more services, or override services that need manual wiring
    # AppBundle\Service\ExampleService:
    #     arguments:
    #         $someArgument: 'some_value'

If any of this is unclear to you then please watch the linked video above, and / or ask questions in the comments section below this video.

What this means is we need to bring our service configuration in line with the new way things work.

Here's what we have currently:

# app/config/services.yml

parameters:
#    parameter_name: value

services:
    guzzle_http_client:
        class: AppBundle\Service\GuzzleHttpClient
        arguments:
            - @guzzle.client.8p_guzzle_client

    buzz_http_client:
        class: AppBundle\Service\BuzzHttpClient

    github_api:
        class: AppBundle\Service\GitHubApi
        arguments:
            - @guzzle_http_client

If any of this is unclear to you, please watch the earlier videos in this series where all of this is covered in greater depth.

Updating Service Definitions For Symfony 3.4 Onwards

We've updated our controller code:

    public function profileAction($username, GitHubApi $api)
    {
        $profileData = $api->getProfile($username);

We now want to inject the GitHubApi service into the method.

We were using the now legacy github_api service definition.

If we merge in the new services.yml config:

services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Tests}'

    # controllers are imported separately to make sure they're public
    # and have a tag that allows actions to type-hint services
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

    guzzle_http_client:
        class: AppBundle\Service\GuzzleHttpClient
        arguments:
            - @guzzle.client.8p_guzzle_client

    buzz_http_client:
        class: AppBundle\Service\BuzzHttpClient

    github_api:
        alias: AppBundle\Service\GitHubApi
        public: true

And then we refresh our web page, we get a RuntimeException:

RuntimeException

Cannot autowire service AppBundle\Service\GitHubApi: argument $httpClient of method __construct() references interface AppBundle\Service\HttpClientInterface but no such service exists. You should maybe alias this interface to one of these existing services: AppBundle\Service\BuzzHttpClient, AppBundle\Service\GuzzleHttpClient, guzzle_http_client, buzz_http_client.

This is where the real fun starts.

This is a peel-the-onion problem. There are layers of fixes we will need to apply. Each in turn is not individually difficult, so let's try our best to break this down.

Cannot autowire service AppBundle\Service\GitHubApi

Ok, we asked for this one. We have no explicit service definition for this fully qualified class name, so Symfony is going to go ahead and try to autowire it for us.

It tries to autowire because our new services.yml file is telling it to attempt autowiring on everything bar classes in \AppBundle\Entity or \AppBundle\Tests.

argument $httpClient of method __construct() references interface AppBundle\Service\HttpClientInterface but no such service exists

Symfony implicitly tells us that it found the class but that it couldn't determine how to instantiate it.

Behind the scenes the autowiring logic has looked for, and found, our class:

AppBundle\Service\GitHubApi

It looks for this class because we injected it into our controller method.

It looks for this class inside the src/AppBundle directory because we told Symfony to do just that with:

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'

We know it found the class because firstly, if it didn't, it would have blown up with a different error message.

Secondly, we know it found the class because it tried - in vain - to __construct() it, or new it up.

When it tried to create a new GitHubApi, the classes constructor said: hey, I need something implementing HttpClientInterface, can you give it to me?

<?php

// src/AppBundle/Service/GitHubApi.php

namespace AppBundle\Service;

class GitHubApi
{
    /**
     * @var HttpClientInterface
     */
    private $httpClient;

    public function __construct(HttpClientInterface $httpClient)
    {
        $this->httpClient = $httpClient;
    }

Symfony says sure, can you please hold the line whilst I figure out just which is the right implementation of HttpClientInterface that I'm supposed to give you?

It then goes and looks through all the available service definitions and tries to find the right implementation to hand back.

Here's the problem:

We have two implementations of HttpClientInterface:

  • class GuzzleHttpClient implements HttpClientInterface
  • class BuzzHttpClient implements HttpClientInterface

This is right, right?

We're supposed to work to an interface. We are all told this in Programmer College (I assume you have all been, it's right next door to Clown College).

But think about it: how is Symfony supposed to figure out which one we want it to use?

Well, rather than take a guess, it throws this RuntimeException we're seeing and says hey, you, yeah you! You need to figure this out and tell me, cus I ain't no bleedin' mindreader.

Now, actually Symfony is a well mannered young individual and rather than be nasty about it like I just was, it tries to help us:

You should maybe alias this interface to one of these existing services: AppBundle\Service\BuzzHttpClient, AppBundle\Service\GuzzleHttpClient, guzzle_http_client, buzz_http_client.

psst, you dun goofed, but I'm a bro and this is maybe how you can start fixing it?

But if you look a little closer at that message, and maybe use a bit nicer formatting, something not so immediately obvious (at least, to me) reveals itself:

You should maybe alias this interface to one of these existing services:

  • AppBundle\Service\BuzzHttpClient
  • AppBundle\Service\GuzzleHttpClient
  • guzzle_http_client
  • buzz_http_client

We have four implementations, not the two we may expect.

We have the two legacy definitions, and then because of autowiring, we have the two autowired instances of the same legacy services.

Is this an issue?

Not in this case.

It can be an issue in bigger applications.

Imagine this is an Event Listener implementation.

Imagine that your event listener sends a welcome email, or logs a message to your log files, or charges a customer.

It is highly likely that your event listener will now get triggered twice: once per implementation.

This is a major gotcha so be very careful during your migration. As mentioned earlier, a decent test suite is going to save your bacon here, but it's still possible to miss stuff like this with even with tests.

Choosing a Default Implementation

What we need to do is to tell Symfony that when it encounters a requirement for HttpClientInterface, it should give one of the two configured services by default.

We can override this with a more specific service definition, but that's outside the scope of this tutorial. There's a section in the docs about this very subject, click here to find out more.

In our case, initially we're going to tell Symfony that our legacy @guzzle_http_client should be considered the default implementation when injecting something implementing HttpClientInterface.

To do this we need to add a new entry to services.yml:

services:

    # ... etc

    AppBundle\Service\HttpClientInterface: "@guzzle_http_client"

This should be enough to get our page loading properly once more.

Unfortunately we still have the two deprecation warnings to address.

Autowiring services based on the types they implement is deprecated since Symfony 3.3 and won't be supported in version 4.0. You should rename (or alias) the "guzzle.client.8p_guzzle_client" service to "GuzzleHttp\Client" instead.

We saw earlier that Symfony has already autowired two new services for us:

  • AppBundle\Service\BuzzHttpClient
  • AppBundle\Service\GuzzleHttpClient

We don't want, or really need the two service definitions we configured by hand. Let's remove them:

services:

    # remove this
    # guzzle_http_client:
    #    class: AppBundle\Service\GuzzleHttpClient
    #    arguments:
    #        - "@guzzle.client.8p_guzzle_client"

    # and remove this
    # buzz_http_client:
    #    class: AppBundle\Service\BuzzHttpClient

We can also remove the github_api definition:

services:

    # remove this
    # github_api:
    #    alias: AppBundle\Service\GitHubApi
    #    public: true

Autowiring is doing all of this for us. We don't need explicit configuration - though we could continue to have explicit configuration if we really desired. Autowiring is optional, after all.

This does bring us back in a circle, however.

If we try reloading our page now we get the error:

ServiceNotFoundException

You have requested a non-existent service "guzzle_http_client".

And yes, that's exactly what we just added to try and solve the problem:

services:

    # ... etc

    AppBundle\Service\HttpClientInterface: "@guzzle_http_client"

We must now update this to the rather unusual syntax of:

services:

    # ... etc

    AppBundle\Service\HttpClientInterface: '@AppBundle\Service\GuzzleHttpClient'

Be super careful here. Notice single quotes, double quotes will not work.

We solved yet another headache, but still, the deprecation warning around "guzzle.client.8p_guzzle_client" remains.

We're going to update the Eightpoints Guzzle Bundle shortly, so for now I'm going with the easiest solution:

services:

    # ... etc

    GuzzleHttp\Client: "@guzzle.client.8p_guzzle_client"

Ok, only one deprecation remains.

User Deprecated: Implementing "Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface" without the "reset()" method is deprecated since version 3.4 and will be unsupported in 4.0 for class "EightPoints\Bundle\GuzzleBundle\DataCollector\HttpDataCollector".

This is a problem with the bundle itself, which as we've just said, we're going to update. We can't just bump the version up on this bundle as it requires PHP 7, which is a non-trivial change. That said, Symfony 4 also requires PHP 7.1, so let's get on to that now.

Code For This Video

Get the code for this video.

Episodes

# Title Duration
1 What happened to my Web Server? 04:09
2 Fixing Generic Symfony Deprecations 07:11
3 Fixing Deprecations In Our Own Code 08:52
4 Fixing Third Party Bundle Deprecations (Indirectly) 05:38
5 [Part 1/2] Migrating to Symfony 4.0 with Flex 06:44
6 [Part 2/2] Migrating to Symfony 4.0 with Flex 10:44