Symfony 3.4 et 4.0 sont sortis le même jour : le 30 novembre 2017. Ce n’est pas une coïncidence, c’est la stratégie.
3.4 n’est pas une version de fonctionnalités. Elle livre exactement les mêmes fonctionnalités que 3.3, plus chaque avertissement de dépréciation que 4.0 va rendre obligatoire. Son seul objectif est d’être l’outil de migration : monter de 3.3 à 3.4, corriger ce qui apparaît dans les logs, puis passer à 4.0 proprement.
Pourquoi les versions LTS comptent dans le modèle Symfony
Symfony publie une nouvelle version mineure tous les six mois. Ce rythme serait brutal pour les applications en production, donc le projet désigne chaque quatrième mineure comme LTS : trois ans de corrections de bugs, quatre de correctifs de sécurité. Ce qui signifie que les équipes peuvent cibler 3.4 et arrêter de penser aux mises à jour pendant un moment.
3.4 est la dernière LTS de la ligne 3.x. Si on est encore sur 2.x ou un 3.x ancien, c’est la zone d’atterrissage.
La couche de dépréciations
Chaque fonctionnalité supprimée par 4.0 est dépréciée dans 3.4. Faire tourner son application sur 3.4 avec les notices de dépréciation activées transforme les logs en une liste de tâches. Les plus courantes :
- Les services sans visibilité explicite (public/private) génèrent des warnings — 4.0 rend tous les services privés par défaut
ControllerTraitest déprécié au profit deAbstractController- Les anciennes interfaces d’authentificateur de sécurité sont marquées pour suppression
- La configuration de services YAML seule sans annotations d’autowiring déclenche des warnings
Le workflow prévu : monter sur 3.4, faire tourner la suite de tests avec les notices de dépréciation comme erreurs (SYMFONY_DEPRECATIONS_HELPER=max[self]=0 dans PHPUnit), corriger tout ce qui échoue. Après ça, la montée vers 4.0 est essentiellement mécanique.
La fenêtre de support
3.4 LTS reçoit des corrections de bugs jusqu’en novembre 2020 et des correctifs de sécurité jusqu’en novembre 2021. C’est une marge confortable pour les applications qui ne peuvent pas suivre chaque version. Le coût : rester sur l’architecture 3.x, sans Flex, sans structure micro-framework, sans autowiring zéro-config par défaut.
Le pont est là. Savoir si et quand on le traverse est une décision business, pas technique.
Les services passent privés
3.4 a inversé la visibilité par défaut des services de public à privé. Avant, $container->get('app.my_service') était du code parfaitement normal. Après, c’est un anti-pattern qui génère un warning de dépréciation dans 3.4 et casse complètement dans 4.0.
La raison est simple : récupérer des services directement depuis le conteneur masque les dépendances et déjoue l’analyse statique. En injectant via le constructeur, le conteneur peut optimiser le graphe, supprimer les services inutilisés, et détecter les erreurs à la compilation. En les récupérant à l’exécution, il ne peut pas.
Pour les applications qui utilisent déjà l’autowiring, la migration est généralement légère. Le point délicat ce sont les contrôleurs qui étendent Controller et appellent $this->get('quelque-chose'). La correction consiste à passer à AbstractController, qui fournit les mêmes raccourcis mais via des service locators paresseux plutôt que l’accès direct au conteneur.
Pour les services qui ont vraiment besoin d’être publics (accédés depuis du code legacy ou des tests fonctionnels), les marquer explicitement :
services:
App\Service\LegacyAdapter:
public: true
Lier les arguments scalaires une seule fois
Un point de friction classique avec l’autowiring : les arguments de constructeur scalaires. Si dix services ont tous besoin de $projectDir, il fallait configurer chacun individuellement. La clé bind sous _defaults règle ça :
services:
_defaults:
autowire: true
autoconfigure: true
bind:
$projectDir: '%kernel.project_dir%'
$mailerDsn: '%env(MAILER_DSN)%'
Psr\Log\LoggerInterface $auditLogger: '@monolog.logger.audit'
Tout service avec un paramètre de constructeur nommé $projectDir reçoit la valeur liée automatiquement. On peut aussi lier par type-hint, ce qui gère le cas courant où plusieurs canaux de logger existent et on en a besoin d’un spécifique. Les liaisons dans _defaults s’appliquent à tous les services du fichier ; on peut surcharger par service si nécessaire.
Injecter les services taggués sans compiler pass
Avant 3.4, collecter tous les services avec un tag donné nécessitait d’écrire un compiler pass. Il y a maintenant un raccourci YAML :
services:
App\Chain\TransformerChain:
arguments:
$transformers: !tagged app.transformer
class TransformerChain
{
public function __construct(private iterable $transformers) {}
}
La notation !tagged crée un IteratorArgument : les services sont instanciés paresseusement au fil de l’itération, donc les transformers non utilisés ne sont jamais construits. Pour l’ordonnancement, ajouter un attribut priority à la définition du tag sur chaque service.
Un logger livré avec le framework
Pas de Monolog ? Pas de problème. Symfony 3.4 inclut un logger PSR-3 qui écrit sur php://stderr par défaut. On l’injecte avec Psr\Log\LoggerInterface :
use Psr\Log\LoggerInterface;
class MyService
{
public function __construct(private LoggerInterface $logger) {}
public function doSomething(): void
{
$this->logger->warning('Quelque chose de douteux s\'est produit', ['context' => 'ici']);
}
}
Le niveau minimum par défaut est warning. La cible est les workloads container et Kubernetes où stderr est le puits de logs naturel. C’est délibérément minimal : pas de handlers, pas de processors, pas de channels. Quand on en a besoin, on installe Monolog.
Les Guard authenticators ont reçu une méthode supports()
La méthode getCredentials() du composant Guard jouait un double rôle : décider si l’authentificateur devait gérer la requête, et extraire les credentials. Retourner null était le signal pour passer. Ça rendait le contrat confus.
3.4 a ajouté supports() pour séparer ces responsabilités :
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request): bool
{
return $request->headers->has('X-API-TOKEN');
}
public function getCredentials(Request $request): array
{
// N'est appelé que quand supports() retourne true.
// Doit toujours retourner des credentials maintenant.
return ['token' => $request->headers->get('X-API-TOKEN')];
}
}
L’ancienne GuardAuthenticatorInterface est dépréciée. L’avantage pratique : les classes de base peuvent implémenter la logique partagée getUser() et checkCredentials(), tandis que les sous-classes ne surchargent que supports() et getCredentials(). Une responsabilité chacune.
Deux nouvelles commandes de debug
debug:autowiring remplace l’ancien debug:container --types pour découvrir quels type-hints fonctionnent avec l’autowiring :
$ bin/console debug:autowiring log
Autowirable Services
====================
Psr\Log\LoggerInterface
alias to monolog.logger
Psr\Log\LoggerInterface $auditLogger
alias to monolog.logger.audit
Passer un mot-clé pour filtrer. Fini de deviner si c’est LoggerInterface ou Logger.
debug:form donne la même capacité d’introspection pour les types de formulaires :
$ bin/console debug:form App\Form\OrderType label_attr
Option: label_attr
Required: false
Default: []
Allowed types: array
Sans arguments, il liste tous les types de formulaires enregistrés, extensions et guessers. Avec un nom de type et un nom d’option, il montre toutes les contraintes sur cette option. Avant ça, on lisait le source ou on tâtonnait.
Les sessions sont devenues plus strictes par défaut
3.4 implémente SessionUpdateTimestampHandlerInterface de PHP 7.0, ce qui apporte deux choses : les écritures de session paresseuses (écrites seulement quand les données ont vraiment changé) et la validation stricte des ID de session (les IDs qui n’existent pas dans le store sont rejetés plutôt que créés silencieusement, ce qui bloque une classe d’attaques de fixation de session).
Les anciennes classes WriteCheckSessionHandler, NativeSessionHandler et NativeProxy sont dépréciées. Le MemcacheSessionHandler (note : pas Memcached) est supprimé, puisque l’extension PECL sous-jacente a arrêté de recevoir des mises à jour pour PHP 7.
Les thèmes de formulaires Twig peuvent maintenant être scopés
Les thèmes de formulaires globaux s’appliquent à tous les formulaires dans l’application. Si un formulaire a besoin d’un look complètement différent, il n’y avait pas de moyen propre de se désinscrire. Le mot-clé only gère ça :
{% raw %}{% form_theme orderForm with ['form/order_layout.html.twig'] only %}{% endraw %}
Le mot-clé only désactive tous les thèmes globaux pour ce formulaire, y compris le form_div_layout.html.twig de base. Le thème personnalisé doit alors soit fournir tous les blocs qu’il utilise, soit les importer explicitement avec {% raw %}{% use 'form_div_layout.html.twig' %}{% endraw %}.
Surcharger les templates de bundle sans boucles infinies
Surcharger un template de bundle qu’on avait aussi besoin d’étendre causait autrefois une erreur de référence circulaire. Surcharger @TwigBundle/Exception/error404.html.twig et essayer aussi d’en hériter ? L’ancienne résolution de namespace suivait la surcharge et bouclait indéfiniment.
3.4 a introduit le préfixe @! pour référencer explicitement le template de bundle original, en contournant toutes les surcharges :
{% raw %}{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
{% extends '@!Twig/Exception/error404.html.twig' %}
{% block title %}Page non trouvée{% endblock %}{% endraw %}
@TwigBundle résout vers la surcharge si elle existe. @!TwigBundle résout toujours vers l’original. Surcharger-et-étendre, sans les acrobaties.