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 SubscribeSymfony'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.
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 ofNexy\Slack\Client
accepts a PsrClientInterface
,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!
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:
{ | |
... 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!
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!
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?
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!
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
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!
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
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!
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
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!
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.
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.
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
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 :)
// 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
}
}
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!