Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Is your Container Running? Catch It! lint:container

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

Symfony's service container is special... like super-powers special. Why? Because it's "compiled". That's a fancy way of saying that, instead of Symfony figuring out how to instantiate each service at runtime, when you build your cache, it figures out every argument to every service and dumps that info into a cache file - called the "compiled container". That's a major reason why Symfony is so fast.

But it has another benefit: if you misconfigured a service - like used a wrong class name - you don't have to go to a page that uses that service to notice the problem. Nope, every page of your app will be broken. That means less surprise bugs on production.

Another type of error that Symfony's container will catch immediately is a missing argument. For example, imagine you registered a service and forgot to configure an argument. Or, better example, Symfony couldn't figure out what to pass to this Mailer argument for some reason:

... lines 1 - 12
class Mailer
{
... lines 15 - 19
public function __construct(MailerInterface $mailer, Environment $twig, Pdf $pdf, EntrypointLookupInterface $entrypointLookup)
{
... lines 22 - 25
}
... lines 27 - 65
}

If that happened, you'll get an error when the container builds... meaning that every page will be broken - even if a page doesn't use this service.

Detecting Type Problems

Starting in Symfony 4.4, Symfony can now also detect if the wrong type will be passed for an argument. For example, if we type-hint an argument with MailerInterface, but due to some misconfiguration, some other object - or maybe a string or an integer - will be passed here, we can find out immediately. But this type of problem won't break the container build. Instead, you need to ask Symfony to check for type problems by running:

php bin/console lint:container

And... oh! Woh! This is a perfect example!

Invalid definition for service nexy_slack.client. Argument 1 of Nexy\Slack\Client accepts a Psr ClientInterface, HttpMethodsClient passed.

Apparently the container is configured to pass the wrong type of object to this service! This service comes from NexylanSlackBundle - I broke something when I upgraded that bundle... and didn't even realize it because I haven't navigated to a page that uses that service!

Fixing our lint Problem

After some digging, it turns out that the bundle has a tiny bug that allowed us to accidentally use a version of a dependency that is too old. Run:

composer why php-http/httplug

I won't bore you with the details, but basically the problem is that this library needs to be at version 2 to make the bundle happy. We have version 1 and a few other libraries depend on it.

The fix is to go to composer.json and change the guzzle6-adapter to version 2:

105 lines composer.json
{
... lines 2 - 3
"require": {
... lines 5 - 22
"php-http/guzzle6-adapter": "^2.0",
... lines 24 - 44
},
... lines 46 - 103
}

Why? Again, if you dug into this, you'd find that we need version 2 of guzzle6-adapter in order to be compatible with version 2 of httplug... which is needed to be compatible with the bundle. Sheesh.

Now run composer update with all three of these libraries: php-http/httplug, php-http/client-commmon - so that it can upgrade to a new version that allows version 2 of HTTPlug - and guzzle6-adapter:

composer update php-http/httplug php-http/client-commmon php-http/guzzle6-adapter

And... cool! Now run:

php bin/console lint:container

We get no output because now our container is happy. And because a few libraries had major version upgrades, if you looked in the CHANGELOGs, you'd find that we also need one more package to truly get things to work:

composer require "http-interop/http-factory-guzzle:^1.1"

The point is: lint:container is a free tool you can add to your continuous integration system to help catch errors earlier. The more type-hints you use in your code, the more it will catch. It's a win win!

And........ that's it! We upgraded to Symfony 4.4, fixed deprecations, upgraded to Symfony 5, jumped into some of the best new features and, ultimately, I think we became friends. Can you feel it?

If you have any upgrade problems, we're here for you in the comments. Let us know what's going on, tell us a funny story, or leave us a Symfony 5 Haiku:

Reading your comments After a long weekend break Brings joy to keyboards

Alright friends, seeya next time!

Leave a comment!

17
Login or Register to join the conversation
Ahaaje Avatar

Thanks for a great course! I've been upgrading an old legacy app all the way from 2.7, and this made the final leg that much smoother!

Reply

Hey Arne!

Thank you for this feedback! Wow, you did a great long way with upgrading your project, really good work! And we're really happy that our upgrading tutorials helps people with upgrading their own projects. Stay tuned with us for the future Symfony upgrades ;)

Cheers!

Reply
Kiuega Avatar

Hello ! Thank you for this superb training!
On the other hand there is a change you mentioned, the environment variable `PANTHER_APP_ENV = panther`

This means that if we set it equal to "test", Panther will rely on our test database (if defined) when it does its tests?

This is great because before that it was always relying on my ** dev ** database which was frustrating!

I had a chance to try, but unfortunately when I try to authenticate a user who is supposed to belong to the test database when I run Panther in ** test ** mode, it doesn't seem to work, authentication gives absolutely nothing. Have you ever experienced this?

Reply

Hey Kiuega!

First, I still haven't used Panther - just because we already use Behat + Mink in our site. So, I may not *fully* answer your question correctly ;).

Behind the scenes, Panther uses the PHP binary to start a built-in web server. Then, it makes requests to that running web server. And, indeed, the PANTHER_APP_ENV controls the environment that this built-in web server will be using. A few things about this:

A) Yes, if you set PANTHER_APP_ENV=test, then it would read your .env.test file (and config/packages/test/*). So if you have a custom database set up for the test environment, then your "running app" would use this. However, *also* realize that inside your test functions, if you boot Symfony's kernel and using the entity manager to insert data, that is an *entirely* different process and could use a different environment (though it uses the test environment by default).

B) The reason that PANTHER_APP_ENV is set to "panther" and not "test" by default can be found in the PR that added this to the recipe: https://github.com/symfony/... - as you can see, you actually *don't* want the "test" environment to be used with Panther because sessions aren't enabled correctly in the test environment. The reason is that normally the test environment (with functional tests) is meant for you to make "fake requests" (inside the same PHP process) from your PHPUnit test function into Symfony's kernel. There is no *real* request, so fake session handling can be used. But with Panther, there *is* a real web request. This may be why authentication gives you nothing - there is no session storage, so even if you authenticate, your successful authentication would never be set to a session cookie correctly.

Let me know if that helps!

Cheers!

Reply
Kiuega Avatar

Thank you for your reply ! I see, are you happy with the behat + mink combot? I have never used them yet.

Then, ok thanks for the explanations, it's clear now. But now in my use case which is the following:

Make real changes in the test database, authenticating each category of users and performing whatever actions they are supposed to be able to do in order to see if nothing is broken.

The solution I have to use for that is therefore not to define the variable PANTHER_APP_ENV (it will be at dev by default), and in my .env, switch the development database for the test database when I perform my tests. So what allows me to use Panther without any worries, it's simple, but not professional.

Now, since indeed, in test environment, there is no storage session, I would have to find something else.

I am wondering if it would be possible to create a new custom environment where I could take over all the configurations of the dev environment, but defining the test database. Thus, I will be able to use Panther, by defining the PANTHER_APP_ENV on the new environment created, which would therefore support the storage session, and the test database! What do you think ?

Hope my answer is clear forgive me I am using google translate to communicate

Reply

Hey Kiuega !

Thank you for your reply ! I see, are you happy with the behat + mink combot? I have never used them yet.

Definitely! But mostly it's because I like the BDD of Behat. Mink can actually be made to work with Selenium (which is what we use for historical reasons) or also Panther, though I haven't tried this yet - https://github.com/robertfausk/mink-panther-driver

I am wondering if it would be possible to create a new custom environment where I could take over all the configurations of the dev environment, but defining the test database

Yes, you are thinking exactly correct. What you really want is a "panther" environment that shares all the config with the test environment except for the session storage. And, hmm. I think the best way to do this might actually be to not create a new environment, but, kind of "fake it". Basically, use the "test" environment but with an extra flag that we can use to change the session storage. So, here is how:

A) Set PANTHER_APP_ENV=panther, which means that when the Symfony web server starts, APP_ENV will actually equal panther
B) In bootstrap.php, we're going to "short circuit" things. Before the .env files are loaded - so here - https://github.com/symfony/recipes/blob/38170f3d78c7e4f88fd26f8e3b153d96bb650445/symfony/framework-bundle/4.2/config/bootstrap.php#L10 - add this code:


if (isset($_SERVER['APP_ENV']) && $_SERVER['APP_ENV'] === 'panther') {
    $_SERVER['APP_ENV'] = 'test';
    $_SERVER['USING_PANTHER'] = true;
}

This changes the environment back to "test" before anything loads. BUT, we set a separate flag. We'll use this in Kernel.php

C) In src/Kernel.php, we will read the USING_PANTHER to load an extra config file. Right after this line - https://github.com/symfony/recipes/blob/38170f3d78c7e4f88fd26f8e3b153d96bb650445/symfony/framework-bundle/5.1/src/Kernel.php#L17


if (isset($_SERVER['USING_PANTHER']) && $_SERVER['USING_PANTHER']) {
    $container->import('../config/{packages}/panther/*.yaml');
}

D) Finally, we'll add a new `config/packages/panther/framework.yaml file with this:


framework:
    session:
        # if you have a custom value in your config/packages/framework.yaml, then use that value here instead
        storage_id: session.storage.native

I may have messed up a detail or three, but this is the idea. Let me know if this makes sense.

Cheers!

Reply
Kiuega Avatar
Kiuega Avatar Kiuega | weaverryan | posted 3 years ago | edited

Hello weaverryan !
Thanks for your answer !

Okay I see what you wanna do! However, on Symfony 5.1 we no longer have the /config/bootstrap.php file

Is there another place I could put the code from step B)?

EDIT : Ok I tried just like this :

A) Set PANTHER_APP_ENV=panther
B) In Kernel.php, before load environment depending APP_ENV :

php
        if (isset($_SERVER['APP_ENV']) && 'panther' === $_SERVER['APP_ENV']) {
            $container->import('../config/{services}.yaml');
            $container->import('../config/{services}_test.yaml');
            $container->import('../config/{services}_'.$this->environment.'.yaml');
        }

C) Add config/packages/panther/framework.yaml with

yaml
framework:
    session:
        # if you have a custom value in your config/packages/framework.yaml, then use that value here instead
        storage_id: session.storage.native

Result : In the end, it works halfway. I can combine the two environments well, do my tests, but I have a new error which occurs when for example I submit a form. This error looks like:


Facebook\WebDriver\Exception\WebDriverCurlException : Curl error thrown for http POST to /session/0eaf7cedb30e3616fe4bfe306a90e50d/element/590fb2dd-8d44-4357-92f7-04b7ea4d3d99/click
Reply

Hey Kiuega !

Nice debugging - I forgot about the bootstrap.php change - it's all hidden in the core now :).

About the error, I'm going to assume that this /session/0eaf... URL is a URL on your site (and the test is submitting the form TO this URL) - let me know if that's wrong :).

The tricky part is... what's the error? This very likely could be a 500 error, and it could be uncovering a bug in your app - or just the fact that the test database isn't setup or many other possible things. I'm not familiar with Panther and the Facebook WebDriver, so I'm not sure how you're supposed to see the error instead of just this one unhelpful line. You could also check your var/log/test.log file to see the error there - but you may need to sort through a lot of noise.

Cheers!

Reply
Kiuega Avatar
Kiuega Avatar Kiuega | weaverryan | posted 3 years ago | edited

Hi !

Well, I spent quite a bit of time on this problem, and in fact there were quite a few small problems!

A) First, I modified my Kernel.php so that it becomes:


        $container->import('../config/{packages}/*.yaml');

        if (isset($_SERVER['APP_ENV']) && 'panther' === $_SERVER['APP_ENV']) {
            $container->import('../config/{packages}/test/*.yaml');
        }

        $container->import('../config/{packages}/'.$this->environment.'/*.yaml');

// ...

B) Indeed, there was a bug with the database. Oddly, he kept using the dev database instead of the test database, I don't know why, even though we are in a panther environment. Therefore, I simply created the .env.panther file in which I put the variable DATABASE_URL for the test database. This made it possible to use the latter rather than that of dev.

C) The last problem, this error :


Facebook\WebDriver\Exception\WebDriverCurlException : Curl error thrown for http POST to /session/0eaf7cedb30e3616fe4bfe306a90e50d/element/590fb2dd-8d44-4357-92f7-04b7ea4d3d99/click

She only seemed to show up when I submitted a form with Panther. In my case, I wanted to add a coupon for Stripe (thanks for the training by the way). Before all that, when I simply switched environments for my tests, I had no problems. It's since I started wanting to fake an environment, I'm not sure why.

In this case, when I submitted a form, the URL should point to https://127.0.0.1:8000/admin/stripe/coupons with POST method

While looking on the internet, I came across this discussion: https://github.com/symfony/panther/issues/155
Same error, but no solutions for me. So bad.

I tried to switch back to the dev environment to see if it worked, and surprisingly, I got the same error!

In doubt I tried a composer recipes and... yes ! new update for symfony/framework-bundle

Update...... and


  - Configuring symfony/framework-bundle (>=5.1): From github.com/symfony/recipes:master
    Enabling the package as a Symfony bundle
    Copying files from recipe
      Created "./config/packages/cache.yaml"
      Created "./config/packages/framework.yaml"
      Created "./config/packages/test/framework.yaml"
      Created "./config/routes/dev/framework.yaml"
      Created "./config/services.yaml"
      Created "./public/index.php"
      Created "./src/Controller/.gitignore"
      Created "./src/Kernel.php"
    Adding environment variable defaults
    Adding entries to .gitignore

Wow...okay..I accepted the big changes, and tried again with the dev environment. No more mistakes.

I try again this time using the panther environment and ... it works!

In the end I am quite confused because there seemed to be quite a few small problems, and I don't really know where the real problem came from. Seems to be working, but I hope it will continue like this

Reply

Hey Kiuega!

A) The Kernel.php looks great!

B) This is probably because the .env files are loaded way before your Kernel.php code. What you would need to do (if you care enough), is what I said earlier (but with a change, because I forgot that bootstrap.php doesn't exist anymore). In index.php, right after the autoloader, you would add:


if (isset($_SERVER['APP_ENV']) && $_SERVER['APP_ENV'] === 'panther') {
    $_SERVER['APP_ENV'] = 'test';
    $_SERVER['USING_PANTHER'] = true;
}

Anyways, you don't need to do this - it sounds like you have things working, but this was my originally idea - to actually set APP_ENV to test, but set another flag so that we know panther is being used.

C) About updating the symfony/framework-bundle recipe and it working, haha, I really don't know :). Accepting that recipe would actually change your Kernel.php file back to how it looked before you changed it. So, I'm not really sure how your code looks in the end, but I'm happy it's working.

Overall, I'd say that this is definitely too hard, and it could almost definitely use some improvements to make it easier. I'll give it some thought.

Cheers!

Reply
Kiuega Avatar

Hey! Thanks for all your help! You are perfect !
I also think that we could do things more easily so as not to have to modify the index.php and kernel.php files which are still sensitive files. I imagine there is a better way to do it, but it will do for now.

Nevertheless, I still tell myself that it's a shame that Panther, which is a superb library, forces us to develop a lot of fixes to use it properly.

Reply

Thanks guys. This course was one of the best ever in my humble opinion. Cheers!

Reply

Hey Daniel!

Thank you for your feedback! You made our day! :)

Cheers!

Reply

Thank you for this tutorial. It worked like a charm. I had only minor issues due to now splitted packages as symfony/security and renaming a kernel variable by Gedmo logger configuration.

Reply

Hey Mumm

Thanks! I hope that wasn't a big problem :)

Reply

some chapter earlier it was mentioned in the tutorial that some packages in 5.0 are split. So it was easy to know where to search for more details eg. packagist. worked like a swiss knife. I never had a major upgrade of symfony that quick and easy

Reply

That's great to hear. Symfony devs do a great job trying to make as smooth as possible to upgrade the framework. Sometimes is not that easy as we would like to but with tutorials like this one, the upgrading process is even fun to do :)

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.2
        "aws/aws-sdk-php": "^3.87", // 3.110.11
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.0", // 2.0.6
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.1.2
        "doctrine/orm": "^2.5.11", // v2.7.2
        "doctrine/persistence": "^1.3.7", // 1.3.8
        "easycorp/easy-log-handler": "^1.0", // v1.0.9
        "http-interop/http-factory-guzzle": "^1.0", // 1.0.0
        "knplabs/knp-markdown-bundle": "^1.7", // 1.8.1
        "knplabs/knp-paginator-bundle": "^5.0", // v5.0.0
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.0
        "knplabs/knp-time-bundle": "^1.8", // v1.11.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.8.2
        "liip/imagine-bundle": "^2.1", // 2.3.0
        "nexylan/slack-bundle": "^2.1", // v2.2.1
        "oneup/flysystem-bundle": "^3.0", // 3.3.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.1
        "sensio/framework-extra-bundle": "^5.1", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.2
        "symfony/console": "5.0.*", // v5.0.2
        "symfony/dotenv": "5.0.*", // v5.0.2
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "5.0.*", // v5.0.2
        "symfony/framework-bundle": "5.0.*", // v5.0.2
        "symfony/mailer": "5.0.*", // v5.0.2
        "symfony/messenger": "5.0.*", // v5.0.2
        "symfony/monolog-bundle": "^3.5", // v3.5.0
        "symfony/security-bundle": "5.0.*", // v5.0.2
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.2
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "5.0.*", // v5.0.2
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "5.0.*", // v5.0.2
        "symfony/webpack-encore-bundle": "^1.4", // v1.7.2
        "symfony/yaml": "5.0.*", // v5.0.2
        "twig/cssinliner-extra": "^2.12", // v2.12.0
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.3.0
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/browser-kit": "5.0.*", // v5.0.2
        "symfony/debug-bundle": "5.0.*", // v5.0.2
        "symfony/maker-bundle": "^1.0", // v1.14.3
        "symfony/phpunit-bridge": "5.0.*", // v5.0.2
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "5.0.*" // v5.0.2
    }
}
userVoice