One line in composer.json took us most of a week to settle: "symfony/symfony": "2.8.*" or "3.0.*". The application is the back office the bank’s operations staff live in, it runs Symfony 2.7 on PHP 5.6, and the upgrade branch has been open since the second week of January. We took 2.8. The reason is not the one I expected when I started reading.
Two releases, one set of features
The thing that confuses people about November 2015 is that two versions came out of it. Symfony 2.8 and Symfony 3.0 were both released that month, and 3.0 is not the one with the new toys. The release process document spells out why: for a major version, Symfony develops two versions at once, the new major and the last minor of the old branch, and “both versions have the same new features, but they differ in the deprecated features”. The oldest one keeps everything that was deprecated. The new one deletes it.
So the choice is not between features. It is between carrying your old code with deprecation notices, or deleting your old code first. And because 2.8 is the last minor of the 2.x branch, it is the long term support release, which is where the argument actually lives.
The support dates are the argument
Read the two release pages side by side and the decision mostly makes itself. 2.8 requires PHP 5.3.9 or higher, gets bug fixes until November 2018 and security fixes until November 2019. 3.0 requires PHP 5.5.9 or higher, gets bug fixes until July 2016 and security fixes until January 2017. The maintenance table in the release process explains the shape: a standard version gets eight months of bug fixes, a long term support version gets three years of bug fixes and four years of security fixes.
July 2016 is six months from now. Picking 3.0 in January means committing to 3.1 before the middle of the year and to another minor every six months after that, forever, or accepting a framework with no bug fixes underneath a system that moves money. We get two production release slots a month and every one of them goes through a change request with a named approver. A framework minor every six months is not a technical cost here, it is a calendar cost, and the calendar is the scarcest thing we have.
The PHP floor decided nothing for us. Our app servers are on 5.6, so both 5.3.9 and 5.5.9 were already behind us. If you still have a 5.4 box somewhere in the estate, 3.0 is not available to you, and that is the entire conversation.
What moving to var/ actually costs
The visible difference when you open a 3.0 project is the directory layout. The Standard Edition’s composer.json grew symfony-var-dir and symfony-bin-dir entries next to the old symfony-app-dir, and the kernel now overrides two methods it never used to:
<?php
// app/AppKernel.php in the Symfony 3.0 Standard Edition
class AppKernel extends Kernel
{
// registerBundles() and registerContainerConfiguration() unchanged
public function getCacheDir()
{
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
}
public function getLogDir()
{
return dirname(__DIR__).'/var/logs';
}
}
Cache and logs move from app/ to var/, tests move to tests/, and app/console becomes bin/console. Twenty lines of work in the repository. Outside the repository it is a different job. Our app/console path appears in nine cron entries, four supervisord program definitions, two Jenkins jobs and a deploy script, and app/logs/prod.log appears in a log shipper configuration and a monitoring check that neither I nor anyone on my team owns. Every one of those is a ticket to another group with its own queue. I spent an afternoon just listing them, and that afternoon is what turned me against doing the framework jump and the layout jump in the same change window.
You can, incidentally, move to var/ on 2.8 whenever you like, because those two methods are just methods. Splitting the work that way is the plan we have now.
The removals we would have hit
UPGRADE-3.0.md is long, and most of it does not apply to any given application. Three entries in it are ours. Form type names are gone, so ->add('reference', 'text') has to become ->add('reference', TextType::class) and every type has to replace getName() with getBlockPrefix(); we have 38 form types and a handful of Twig blocks keyed on the old names. The concept of scopes was removed from the dependency injection container, along with enterScope(), leaveScope() and the prototype scope, which is now shared: false. And the console dialog helper is gone in favour of question, with ProgressHelper and TableHelper replaced by ProgressBar and Table, which touches four of our commands.
The fourth one is my own fault and the most annoying. We use SecurityContext in about twenty places. The Security component’s changelog deprecated it in 2.6, in favour of AuthorizationChecker and TokenStorage, and 3.0 “removed all deprecated code”. That code was written in 2014 against a 2.3 application and nobody has looked at it since.
On 2.8 all four of those still work, and they all shout while they do it. That is the whole value of the LTS: it is 3.0’s feature set with a warning label on everything 3.0 deletes. Point the PHPUnit bridge at the suite and you get a count you can manage against, with SYMFONY_DEPRECATIONS_HELPER set to weak while the number is embarrassing and set to a ceiling once it is not.
Ours after the first run on the 2.8 branch: 213 remaining deprecation notices, 41 of them triggered from inside a bundle we do not maintain and cannot patch this quarter. My recommendation to anyone else standing where we are in January is 2.8 and a deprecation budget, not 3.0 and a release calendar. The caveat is that 2.8 makes it very easy to stop there, and a supported framework that you never clean up is how you end up doing this exact reading again in 2018.
Sources
- Symfony 2.8 release page, for the long term support status, the PHP 5.3.9 requirement and the November 2018 and November 2019 end of support dates.
- Symfony 3.0 release page, for the PHP 5.5.9 requirement and the July 2016 and January 2017 end of support dates.
- The release process, for the maintenance table (eight months of bug fixes for a standard version, three and four years for an LTS) and for the rule that a major and the previous branch’s last minor ship the same features.
- UPGRADE-3.0.md, for form type names becoming fully qualified class names,
getName()becominggetBlockPrefix(), the removal of container scopes and the console helper replacements. - Security component changelog, for
SecurityContextbeing deprecated in 2.6 in favour ofAuthorizationCheckerandTokenStorage, and removed in 3.0. - Standard Edition composer.json for 3.0, for the
symfony-var-dirandsymfony-bin-direntries and thephp >=5.5.9requirement. - Standard Edition AppKernel for 3.0, for the
getCacheDir()andgetLogDir()overrides quoted above. - The PHPUnit bridge deprecation handler, for the reporting modes behind
SYMFONY_DEPRECATIONS_HELPER.