PHP - Symfony vs Laravel : mon expérience

Récemment dans mon parcours professionnel, j’ai eu l’occasion de travailler sur une application Laravel 8. Moi qui était plutôt habitué à coder avec Symfony, c’était un challenge sympa de découvrir ce framework. Je dis découvrir mais en réalité j’avais déjà bossé avec Laravel (v4.2 et v5) en 2016 pour développer une petite API REST. Mais honnêtement cette expérience n’était pas suffisament poussée pour maitriser ce framework.

Dans ce billet, j’ai envie de poser par écrit ce que j’ai compris des différences entre les deux frameworks. Attention je ne prétends pas que ce sera exhaustif, ni même que je ne vais pas me tromper, ce sera juste mon expérience. Et d’ailleurs, je ne me prétend pas expert Symfony non plus.

Je peux déjà poser quelques biais :

  • je suis probablement influencé par la façon de faire de mon équipe et des “guidelines” de l’entreprise (et je ne peux pas dire si c’est optimal, classique, etc)
  • je ne pourrai évoquer que ce que j’ai eu l’occasion d’utiliser (sur quelques mois) donc il y a énormément d’aspects de Laravel que je n’ai pas vu

Le contexte : j’étais sur le développement d’un micro service qui expose une API REST (pas de vue, donc pas de twig vs blade par exemple).

REST

Construire une API REST avec Laravel c’est natif et plus rapide qu’avec Symfony (dis comme ça, ça ne veut pas dire grand chose, je parle bien d’exposer des endpoints uniquement, pas de coder une logique derrière). Alors bien sûr dans l’univers Symfony on fait plutôt du API-Platform (c’est la façon recommandé par Symfony). Laravel a fait le choix de rendre ça natif au framework. Je ne vais pas comparer Laravel à API-Platform pour la création d’API REST, ça pourrait être un article entier (spoiler : API-Platform est beaucoup plus complet).

Les facades 😱

La facades dans Laravel sont une façon d’accéder à des services via des méthodes statiques. Et des facades, il y en a pleins : App, Cache, Route, View, Date, Config, DB, Log… Globalement toutes les fonctionnalités de Laravel sont plus ou moins accessibles via une facade (même le cli avec Artisan::). L’intérêt principal, c’est qu’elles sont très faciles à utiliser :

Log::info('foobar');
Cache::get('key');
Config::get('elem');

Un autre intérêt des facades, c’est qu’elles sont très simples à mocker depuis un test :

Cache::shouldReceive('get')
    ->once()
    ->with('key')
    ->andReturn('value');

J’avoue qu’on se trouve en pleine magie ici et je conçois assez bien qu’on puisse avoir du mal avec ce genre de chose. De plus, je trouve que ça cache complétement les dépendances utilisées par une classe et on peut se retrouver avec énormément de couplage sans vraiment s’en rendre compte. C’est donc à utiliser avec parcimonie je pense.

Facade

Injection de dépendances

J’ai eu beaucoup de mal au début avec l’injection de dépendances de Laravel mais j’ai compris par la suite que c’était plus avec la façon de faire de l’équipe qu’avec le framework lui-même (on met de côté les facades dont j’ai déjà parlé). En effet, j’étais habitué avec Symfony à injecter mes dépendances (sans configuration spécifique généralement) dans le constructeur. Un des avantages de cette façon de faire, c’est qu’on observe très vite justement des dépendances de la classe (et potentiellement on se rend compte qu’il y en a trop 😅).

Or, quand je suis arrivé sur le projet, j’ai vu des choses comme ça pour récupérer un service :

$monService = App::make(MonService::class)

Un peu à la manière de Symfony 2 où on pouvait accéder à n’importe quel service du conteneur depuis n’importe où (on peut toujours si le service est public mais c’est fortement déconseillé). Je me suis dit “wow pas top pour répèrer les dépendances d’une classe”.

Mais en fait c’était plus une habitude de l’équipe, qu’une contrainte de Laravel puisqu’il est tout à fait possible d’injecter nos services dans un constructeur de la même manière que Symfony.

Architecture

Au lieu d’un dossier src on a un dossier app dans Laravel. Plus sérieusement, il est tout à fait possible de pratiquer le type d’architecture que l’on souhaite (archi hexagonale ou archi plus “classique”), de la même manière qu’avec Symfony.

Configuration

Toute la configuration Laravel se fait en PHP (je crois que c’est historiquement comme ça). Et je dois dire que le YAML ne m’a pas manqué 😄. Alors il est vrai que Symfony préconise depuis un moment d’utiliser du PHP pour sa configuration mais les habitudes ont la vie dure et je n’ai quasiment jamais vu de projet Symfony dont la configuration soit en PHP voir en XML.

Au passage, il y a des outils comme Rector qui peuvent convertir votre config YAML en PHP pour Symfony sans souci.

Hormis ça, pas grand chose d’autre à dire sur le sujet.

ORM : Doctrine vs Eloquent

Une différence des plus importantes a été d’utiliser un nouvel ORM (moi qui ne connaissait que Doctrine). Eloquent fonctionne suivant le “pattern” Active Record. Sans rentrer dans les détails, ça veut dire qu’une classe du modèle représente une table de la base de données et qu’elle est à la fois responsable de porter ses données mais aussi de faire toutes les actions de lecture/écriture sur la bdd. Dans le cas de Doctrine (qui suit le “pattern” Data Mapper), une entité porte les données “persistables” et un “entity manager” va être chargé de réaliser les requêtes vers la base de données (notamment en utilisant l’“unitOfWork” qui va stocker les modifications des entités). Bref, ça change pas mal !

Je n’ai globalement pas apprécié Eloquent. Les modèles peuvent vite devenir fouillis et mélange un peu trop les responsabilités. Et puis, c’est un peu déstabilisant d’avoir des entités sans propriétés, Eloquent allant les résoudre dynamiquement. On peut se retrouver avec ce genre de chose :

class Foo extends Model {}

$foo = new Foo();
$foo->bar; // bar étant une colonne de la table Foo mais jamais déclarée dans la classe Foo

On peut aussi rajouter des scopes dans les modèles qui vont permettre de filtrer les requêtes.

class Foo extends Model
{
    public function scopeRed($query)
    {
        return $query->where('color', '=', 'red');
    }
}
Foo::red(); // va récupérer la liste des "Foo" dont la couleur est "red"

C’est pratique, mais ça me perturbe quand même de mettre ça dans un modèle. Et des trucs comme ça, il y en a pleins, on se retrouve avec des modèles potentiellements sans ses propriétés mais avec pleins de méthodes plus ou moins orientées métier 😶.

Enfin, les migrations sont beaucoup plus agréables à mon sens côté Doctrine :

php bin/console make:migration

Cette simple commande va vous générer toutes les requêtes SQL nécessaire pour que votre schéma de base de données soit cohérent avec vos entités. Il ne me semble pas que ce genre de chose existe chez Laravel / Eloquent. Il faut décrire à la main tous les changements que l’on veut exécuter.

Date

Laravel utilise une librairie pour la gestion des dates très efficace : Carbon. Ce n’est pas à proprement parler une feature de Laravel puisqu’on peut utiliser Carbon sans Laravel, mais comme il est livré avec le framework ça m’a permis de le découvrir.

$mutable = Carbon::now();
$immutable = CarbonImmutable::now();
$modifiedMutable = $mutable->add(1, 'day');
$modifiedImmutable = CarbonImmutable::now()->add(1, 'day');
Carbon::now()->isWeekend();
$tomorrow = Carbon::now()->addDay();
$lastWeek = Carbon::now()->subWeek();

Il y a aussi pleins de parser assez puissants, la possibilité de mocker la date dans les tests et encore pleins de choses super pratiques !

Tests

J’ai fait du PHPUnit (pas de Pest qui est proche de l’écosystème Laravel). Laravel a une API de test (reposant sur PHPUnit) plutôt très agréable à utiliser ! Il y a pleins de helpers pratiques et explicites qui rendent les tests vraiment lisibles. Il y a également pas mal d’assertions qui simplifient le test de contenu d’une réponse HTTP en JSON par exemple, ou pour tester l’état de la base de données.

$response
    ->assertStatus(201)
    ->assertJsonPath('team.owner.name', 'Darian');
$this->assertDatabaseCount('users', 5);
$this->assertDatabaseHas('users', [
    'email' => 'sally@example.com',
]);

Liées aux tests, Laravel propose aussi les Factories pour construire des objets facilement (déjà hydratés) dans nos tests. Encore une fois c’est le genre de chose très pratique lorsqu’on code les tests.

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

$user = User::factory()->create();
[$user1, $user2] = User::factory()->count(2)->create();

Il y a des tas de possibilités, de méthodes “fluents” pour customiser la création d’objets.

Côté mock, il y a aussi une surcouche Laravel facilitant les choses. Celle-ci repose sur Mockery. Ayant l’habitude de travailler avec Mockery, ça a été presque plus pénible pour moi de me faire à cette nouvelle API, mais avec le temps, je m’y suis habitué et j’admets que pour un débutant, la surcouche de Laravel est probablement plus facile à prendre en main (moins technique) que Mockery.

Tooling

Laravel vient avec une cli qui s’appelle artisan. Pour ce que j’ai eu à utiliser, c’est très semblable à la console Symfony.

Je n’ai malheureusement pas eu l’occasion d’utiliser d’autres outils donc je n’ai pas grand chose à dire de plus.

Documentation

De mon point de vue, la documentation officielle de Symfony est nettement meilleure que celle de Laravel. Elle me parait beaucoup plus poussée et notamment sur un aspect : avec un composant Symfony, il y a souvent 3 parties (réparties sur plusieurs pages) dans la documentation. On a une présentation (classique) du composant, de ses possibilités, sa prise en main, etc. On a ensuite du contenu sur sa configuration et ses fonctionnalités de façon très exhaustive. Enfin, on a régulièrement des pages entières sur des usages plus concrets, des vrais “use cases” qu’on peut rencontrer, et ça c’est “game changer” pour moi. C’est bien de savoir comment fonctionne un outil, mais savoir l’utiliser de façon optimal c’est encore mieux.

Chez Laravel, il me semble pour ce que j’en ai vu, que la document s’arrête à la première partie cité au-dessus et donc on se retrouve régulièrement à aller chercher des infos sur des forums ou autres pour savoir qu’elle est la meilleure façon de résoudre notre problème en utilisant tel ou tel composant.

Conclusion

Il y a beaucoup de choses que je ne peux pas comparer : par exemple l’ecosystème Laravel très développé (et plutôt bien mis en avant il me semble) que je n’ai pas du tout eu l’occasion de tester.

Globalement, les deux frameworks répondent au même besoin et le choix se fait à priori sur la stack technique existante ou sur la préférence des developpeurs. Laravel se distingue surtout dans les helpers que l’API fournis pour accélerér le developpement et rendre plus plaisant certaines actions c’est certain. En parlant de l’API, c’est une des difficultés que j’ai rencontrée : j’ai par moment eu l’impression d’avoir pleins de façons différentes de faire les mêmes actions ce qui m’a posé 2 problèmes. D’abord, j’ai parfois eu du mal à intégrer quelle était la “meilleure solution”. Ensuite, j’avais également un peu de mal à comprendre quelles étaient les différences internes dans ces cas là.

J’ai souvent entendu que Laravel permettait de coder une application rapidement mais de façon un peu sale, contrairement à Symfony qui était beaucoup plus lourd, complexe à prendre en main mais qui permettait de faire un code de qualité. Je suis totalement en désaccord avec ça : une bonne équipe de développeurs fera une bonne application avec du Laravel ou du Symfony, et de mauvais développeurs feront une application pas terrible avec Symfony.