There are numerous resources available about the Symfony 3.x version, from Living on the Edge on the Symfony blog (which has over 30 individual posts) to code examples with services.yml refactoring and UPGRADE.md files in Symfony repository on Github.
Many of these resources include features that are nice to have for convenience sake, some of them are BC breaks that you have to fix, and some of them are must-have features which can rapidly decrease your technical debt.
Today we will demonstrate some real-life applications of these resources and how to migrate a PHP e-commerce platform from Symfony 2.8 to 3.4.
1. PSR-4 Service Autodiscovery
If there’s one single feature that makes all the hassle of upgrading worth it, it’s this one. Since you probably already use composer, it’s likely that you have PSR-4 autoload in the composer.json
It tells you to load App\SomeController class in app/SomeController.php file.
How is this related to service registration? Well, imagine you have to autoload classes like this:
1 class = 1 line in autoload. That would be crazy, right?
But if you use Symfony 3.2 or earlier versions, you might have no other option but to apply this type of approach to service registration:
PSR-4 Service Autodiscover just ported the PSR-4 approach to services.yml config. To make things simpler than the approach above, you can instead write it like this:
Do you need to add a service? Just place it according to the same way you’d want composer to find it — respect PSR-4. And that’s it. No configuration, no classes that are missing in your container, no dead-classes that are not used. But make no mistake, it’s still configurable (parameters, decorator, method calls, tags, etc.) like before.
How awesome is that?
We won’t describe all the related features like autoconfigure, _defaults and instanceof keywords, since we’ve already covered that in another article. Thanks to these handy tricks, you can pare down your service config to make it slimmer and more efficient.
But you’re still going to want to be careful when it comes to…
2. PSR-4 Trap: Autoloaded Lazy Commands
Let’s say we want to use PSR-4 Service Autodiscovery and autoconfigure commands:
That makes sense, right? And it works. But we decided we wanted to make use of a Symfony 3.4 feature — lazy commands. Such a method allows the command dependency tree to be constructed in such a way that only one service per call is created, as opposed to all services being created, thereby resulting in faster performance.
This idea was an issue for Symfony back in September 2014 and many attempts for this feature failed. Finally in 2017 they developed a solution, so we were really eager to try this feature in our code:
Commands are lazy-loaded — yay! But we started to question other aspects of the feature:
- Should we use PSR-4 all the time?
- Are lazy-commands more important if you have a lazy-developer who wants to skip per-service configuration?
- How much faster are lazy-commands over non-lazy in our application, and is it worth it to use this pattern?
- How do we measure its effectiveness?
- How do we explain/tutor new team members in its use?
What would you pick — a PSR-4 lazy-developer, or lazy-commands that give a slight performance boost to your application?
We are so lazy we chose both.
As one wise man said: “There Are No Solutions, Only Trade-Offs”, so we decided to go for it. If you follow the Symfony/demo package — which is the best showcase for new Symfony features before they’re even released — you might notice a pull-request with an upgrade to Symfony 3.4 with the following trick:
And then go back one step to the clean config:
Congratulations!
- your commands are lazy loaded
- configuration is as simple as it can be
- it’s consistent with the registration of all other services
- you can move onto other applications
- your new team-mates can now focus on other problems
See also do Symfony documentation for more.
3. From *ContainerAware and *ContainerAwareInterface to Constructor Injection
Symfony is moving more and more towards adopting a standardized method of coding. Symfony Flex simplifies and unites package registration among other things, and thanks to the fact that there is now a singular way to work with services. The result is that the codebase is getting slimmer because there are fewer cases to cover, and developer experience overall is improving.
This also extends to performance: Fabien has reported that “according to those benchmarks, a “hello world” page on Symfony 4.0 is almost twice as fast as the same code using Symfony 3.4”. If you’ve managed to get through the last 2 steps above, then you’re already well on your way towards having a more productive experience with Symfony.
What is the next step?
From many services locators, inject helpers, and string types…
- extends *ContainerAware
- extends ContainerAwareCommand
- implements *ContainerAwareInterface
- $this->getContainer()->get(‘…’)
- $this->container->get(‘…’)
- $this->get(‘some-service’)
- $this->get(‘SomeClass’)
- $this->get(SomeClass::class)
…to 1 strong-typed, contract-based constructor injection:
Symfony 3.4 now makes this super easy for services, repositories, commands and partially controllers (see point 4 bellow), which makes it easier to adopt SOLID patterns:
- Class type over string name of service
- Interface type over class type
- Constructor dependency over container injection
From 8 possible paths to a single one! This makes it easy for even junior coders to pick it up. To see how it’s done take a look at our PR, or for a more advanced walkthrough use Rector to do it for you.
4. Simpler Controller Without Container?
We haven’t quite gotten to this step yet, but we think you should know about it. In a nutshell:
$this->get(‘…’) doesn’t work anymore in Controller, so how can you get dependencies without the container in controllers?
Also, controllers are similar to commands, in that they should be lazy because they’re only delegators. Imagine you have a controller with 10 actions. Each action has uses 2–3 other services. The benefit of using a lazy $this->get(‘…’) approach is that you only get the ‘tools’ or services you intend to use, as opposed to having to run all your services through a constructor.
In the ideal world, you’d use Invokable Controllers, with one __invoke() action and whatever dependencies the controller needs. This is a trend that became popular with EventListeners as well, but even though we would potentially like to see our preexisting 30 controllers expand to 150 this is currently not feasible.
3 years after Laravel introduced method injection there is action method autowiring in Symfony 3.3. It looks like this:
Then certain typed actions are required:
This is a fast, useful way to incorporate lazy dependencies, but it’s important to note — only attempt this with controllers. You don’t want to use this pattern for your entire application, because it can be extremely difficult to decouple.
5. Abstract Dependencies Without Container?
The $this->get() approach also solved one more problem. If you have a parent class that has dependencies and you extend it with another which has even more dependencies, how do you pass them both?
Every time you extend AbstractParentClass you need to remember to pass some dependency to its parent. I wonder if that will last past 3 PRs with this class.
It’s possible that this particular code will be phased out with later PRs, since it can easily backfire — in our opinion, it should be code-embedded to free up the developer having to remember it.
How to let the code work for you?
Nette had the same issue that was solved by @inject annotation on public properties or the inject*() method prefix. Symfony uses @required annotation above setter method that will be autowired:
<?php abstract class AbstractParentClass { /** * @var SomeDependency */ private $someDependency; /** * @required */ public function setSomeDependency(SomeDependency $someDependency) { $this->someDependency = $someDependency; } }
<?php class SomeClass extends AbstractParentClass { /** * @var AnotherDependency */ private $anotherDependency; public function __construct(AnotherDependency $anotherDependency) { $this->anotherDependency = $anotherDependency; } }
No more parent forgetting!
6. Valid Forms
Let’s take a break from all these injections and look at a much more simpler code — in Forms.
Simple PhpStorm Find & Replace will do.
Additional Changes
The following changes are not as groundbreaking, but we refer to them as “Butterfly Effects” — they seem like tiny changes, but they can suck up a considerable amount of time in terms of coming up with solutions.
So, here is just list of them:
- Fix deprecations to allow an update to Symfony to 3
- options to entry_options Form parameter rename
- users are logged out of all the sessions except the current one on password change
- trusted_proxies configuration
Did we forget the one that gave you a headache? Tell us in the comments so we can update our list help out other developers who might be experiencing the same problems.
Stay tuned for more useful tips from our roadmap.
EDIT: Not enough? Maybe your application uses different Symfony code and requires different changes. Mickaël Andrieu shares his tips for upgrading.