Tutoriel vidéo Laravel & Symfony : Laravel ou Symfony ?

Je vous propose aujourd’hui de voir les différences entre Laravel et Symfony en comparant comment les 2 frameworks approchent les tâches classique.

Sommaire de la vidéo

Installation

Dans les 2 cas l’installation peut se faire au travers de la commande create-project de composer.

# Laravel
composer create-project laravel/laravel example-app

# Symfony 
composer create-project symfony/skeleton:"6.3.*" my_project_directory
cd my_project_directory
composer require webapp

Symfony nécessite plusieurs commande car il s’installe en mode “microservice” par défaut en n’incluant que le strict minimum. La commande composer require webapp permet d’installer tous les composants nécessaire à la création d’une application web classique.

On notera aussi que les 2 frameworks intègre une commande (laravel pour Laravel et symfony pour Symfony) que l’on peut utiliser pour piloter le framework et faire certaines tâches (initialiser un projet par exemple).

Structure

La structure des 2 frameworks est similaire avec la présence d’un dossier public qui servira de racine au serveur HTTP. Les sources sont placé dans un dossier src pour Symfony et app dans le cas de Laravel. Dans les 2 cas la configuration se situera dans le dossier config avec comme principale différence le format utilisé.

  • Laravel utilise des fichiers des PHP qui retourne des tableaux
  • Symfony utilise par défaut des fichier yaml

Les routes

Maintenant que les framework on va pouvoir créer notre première route pour créer une page. Dans le cas de Laravel on commence par créer le controller

<?php
namespace App\Http\Controllers;

class HelloController extends Controller
{
    public function hello()
    {
        return 'Hello';
    }
}

Puis on déclare la route qui permet d’accéder à la méthode.

// routes/web.php
Route::get('/hello', [HelloController::class, 'hello'])->name('hello');

Côté Symfony il est possible de déclarer les routes au travers d’attribut dans le controller directement.

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    #[Route('/hello', name: 'hello')]
    public function hello(): Response
    {
        return new Response('hello');
    }
}

Gérer la requête

Pour les 2 frameworks il est possible d’injecter un paramètre dans nos controllers pour récupérer la requête.

public function hello(Request $request): Response
{
    dd($request->query->get('name'));
}

On notera que l’objet Request présent dans Laravel étend de la class Request de Symfony pour proposer des méthodes plus rapides pour effectuer des opérations de bases.

Paramètre dans l’URL

En plus du traitement de la requête il est aussi possible de passer des paramètre dans l’url. La syntaxe et le fonctionnement est similaire dans les 2 frameworks

# Laravel
Route::get('/hello/{name}', [HelloController::class, 'hello']);

# Symfony
#[Route('/hello/{name}')]

On pourra ensuite récupérer ce paramètre dans le controller sous forme de paramètre.

public function hello (string $name) { }

Moteur de template

Pour rendre des pages HTML il sera possible d’utiliser un moteur de template et c’est sur ce point qu’apparait une première différence entre les 2 frameworks.

Symfony utilise le moteur de template twig qui utilise sa propre syntaxe inspirée par Django. Il est possible d’étendre le moteur en ajoutant des fonctions, filtres, et balises

{% extends "base.html.twig" %}

{% block title %}Page Title{% endblock %}

{% block sidebar %}
    {{ parent() }}
    <p>This is appended to the master sidebar.</p>
{% endblock %}

{% block content %}
    {% for post in posts %}
        <article>
            <h2>{{ post.title }}</h2>
            <p>{{ post.excerpt }}</p>
            <p><a href="https://grafikart.fr/tutoriels/{{ path("post.show', {slug: post.slug}) }}">Lire la suite</a></p>
        </article>
{% endblock %}

Laravel utilise un moteur de template propre au framework : blade. Ce moteur étend la syntaxe de PHP avec de nouvelles fonctionnalités mais accepte accepte aussi du code PHP valide. Il peut être étendu à l’aide de nouvelles directives pour lesquelles il faudra définir le code PHP à générer.

@extends('layouts.app')

@section('title', 'Page Title')

@section('sidebar')
    @parent

    <p>This is appended to the master sidebar.</p>
@endsection

@section('content')

    @foreach($posts as $post)
        <article>
            <h2>{{ $post->title }}</h2>
            <p>{{ $post->excerpt }}</p>
            <p><a href="https://grafikart.fr/tutoriels/{{ route("post.show', ['slug' => $post->slug]) }}">Lire la suite</a></p>
        </article>
    @endforeach

@endsection

On notera que blade a offre un système de composants qui permet d’inclure un morceau de template en utilisant une syntaxe plus proche de l’HTML.

<form method="post">
    <x-input :value="$post->title" label="Titre">
</form>

Formulaire

Une des tâche récurrente lorsque l’on fait du backend est la création et le traitement de formulaire.

Symfony

Sur Symfony la création de formulaire passe par une classe dédiée qui va permettre de représenter les données de notre formulaire. On notera la possibilité d’ajouter des règle de validation sur les propriétés à l’aide d’attributs PHP.

<?php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class ContactDTO
{

    #[Assert\NotBlank()]
    #[Assert\Length(min: 3)]
    public string $name="";

    #[Assert\NotBlank()]
    #[Assert\Email()]
    public string $email="";

    #[Assert\NotBlank()]
    #[Assert\Length(min: 10)]
    public string $message="";

}

Ensuite on peut créer la classe qui va représenter notre formulaire et ces champs.

<?php

namespace App\Form;

use App\DTO\ContactDTO;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ContactForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class)
            ->add('email', TextType::class)
            ->add('message', TextareaType::class)
            ->add('save', SubmitType::class, ['label' => 'Nous contacter'])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => ContactDTO::class,
        ]);
    }
}

Ensuite, dans le controller il est possible de gérer le traitement du formulaire, et de l’envoyer à Twig pour gérer l’affichage.

public function contact(Request $request): Response
{
    $data = new ContactDTO();
    $form = $this->createForm(ContactForm::class, $$data);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // $data est muté avec les données du formulaire
        // On peut aussi utiliser $form->getData() pour récupérer l'objet modifié
    }

    return $this->render('contact.html.twig', [
        'form' => $form,
    ]);
}

Côté template le formulaire peut être généré à l’aide de la fonction form qui est injecté dans Twig par Symfony.

{{ form(form) }}

Laravel

Sur Laravel il n’existe pas de classe pour représenter le formulaire, ni pour générer le formulaire. Dans notre template blade on créera donc notre formulaire de manière classique (on pourra utiliser des includes ou des composants pour simplifier l’écirute des champs).

<form method="post">
    @csrf
    <div>
        <label for="name">Name :</label>
        <input type="text" name="name" id="name" value="{{ old('name') }}">
        @error('name')
            <div class="alert alert-danger">{{ $message }}</div>
        @enderror
    </div>
    <div>
        <label for="email">Email :</label>
        <input type="email" id="email" name="email" id="email" value="{{ old('email') }}">
        @error('email')
            <div class="alert alert-danger">{{ $message }}</div>
        @enderror
    </div>
    <div>
        <label for="message">Name :</label>
        <textarea type="text" id="message" value="">{{ old('message') }}</textarea>
        @error('message')
            <div class="alert alert-danger">{{ $message }}</div>
        @enderror
    </div>
    <button type="submit">Nous contacter</button>
</form>

Lorsque le formulaire est soumis, et que des erreurs sont présentes, Laravel redirigera automatiquement l’utilisateur vers le page précédente en sauvegardant les données et les erreurs en session.

  • La méthode old() permet de récupérer, depuis la session, la valeur de la précédente soumission du formulaire.
  • La directive @error permet de détecter si il y avait une erreur, et d’afficher quelquechose si c’est le cas.

Maintenant pour la partie traitement, on va commencer par créer un objet requête personnalisé qui permettra d’autoriser la requête et de valider son contenu.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ContactRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => 'required|min:3',
            'email' => 'required|email',
            'message' => 'required|min:10',
        ];
    }
}

Ensuite, on va créer une route pour la méthode POST et on utilisera notre objet requête en paramètre de la méthode.

public function form() {
    return view('contact/form');
}

public function store(ContactRequest $request) {
    $data = $request->validated();
    // $request->validated() contiendra les données validées sous forme de tableau.    
}

API

Un autre cas d’utilisation de ces frameworks est la création d’une API.

Parser la requête

Lorsque l’on reçoit un appel il faut être capable de comprendre le contenu de la requête et cela se fait très facilement pour les 2 frameworks. Côté Laravel on utilise le même système de FromRequest.

public function store(ContactRequest $request) {
    $data = $request->validated();
    // $request->validated() contiendra les données validées sous forme de tableau.    
}

Symfony permet lui aussi de traiter la requête facilement à travers un attribut au niveau des controllers.

<?php
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;

public function contact(
    #[MapRequestPayload]
    ContactDTO $data
): Response
{
    // $data contient les données du formulaire
}

Réponse d’API

Pour répondre correctement à une demande il est nécessaire de sérialiser les données pour transformer un objet en réponse valide (JSON par exemple). Laravel dispose d’un type de classe, dédié à la transformation des objets.

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{

    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Cette resource peut ensuite être utilisée pour renvoyer les données au niveau du controller.

// Pour un seul élément on peut instancier et renvoyer la resource
public function user (User $user) {
    return new UserResource($user);
}

// Pour plusieurs éléments on peut utiliser la méthode statique collection
public function users () {
    $users = User::all();
    return UserResource::collection($users);
}

Symfony utilise un système de serialisation pour transformer un objet et peut être utilisé sur n’importe quel objet. Dans le cas des controllers une méthode json permet de gérer la conversion d’une manière plus simple.

public function index(UserRepository $repository)
{
    $users = $repository->findAll();
    return $this->json($users, context: ['groups' => ['api:users']]);
}

Le paramètre “groups” permet de contrôler les propriétés que l’on souhaite exposer au travers d’attributs.

L’ORM

L’ORM est aussi un gros point de différence entre les 2 frameworks.

Laravel utilise par défaut Eloquent qui est un ORM basé sur Active Record où le Model est à la fois responsable de représenter une entité, mais aussi de gérer la persistence des informations.

// Récupération
$post = Post::find(1);
// Modification
$post->name = "Marc";
$post->save();
// Création
$post2 = Post::create(['name' => 'Jean']); 
// Suppression
$post3->destroy(); 

Symfony utilise par défaut Doctrine qui est un ORM basé sur le principe du Data Mapper où on sépare la notion d’entité (objet représentant les données), de Repository (objet servant à récupérer des entités) et de Manager (objet responsable de la persistance)

$em = $this->getDoctrine()->getManager(); // L'entity manager, normalement récupéré en paramètre du controller
// Récupération
$post = $em->getRepository(Post::class)->find(1);
// Modification
$post->setName('Marc');
// Création
$post2 = new Post();
$post2->setName('Jean');
$em->persist($post2);
// Suppression
$em->remove($post3); 
// Dans tous les cas les données ne sont pas envoyées en base de données
// Pour cela il faut utiliser la méthode flush
$em->flush();

Eloquent a une syntaxe plus courte et une logique qui semble plus naturelle mais cette apparente simplicité peut rapidement mener à des “fat models” car toute la logique va être stocké au même endroit. Doctrine permet une meilleur séparation mais s’avèrera relativement verbeux pour des cas simples.

Authentification

Pour l’authentification Laravel propose des “starter kits” qui permettent de mettre en place toutes les opérations classique de gestion de compte utilisateur. Un de ces starters kits est breeze qui va générer les controllers, models et template blade.

php artisan breeze:install

php artisan migrate
npm install
npm run dev

Le code sera généré dans le domaine de votre application et vous pouvez le modifier pour ajouter le comportement que vous souhaitez en modifiant les sources.

Symfony, quant à lui, dispose d’un composant sécurité qui va permettre de gérer l’authentification mais n’est pas configuré de base.

php bin/console make:user
php bin/console make:auth
php bin/cinsole make:registration-form

L’éxécution de ces commandes vous permettra de configurer la mise en place de l’authentification. En revanche, ce qui est offert par défaut est beaucoup plus minimal et il vous faudra développer la partie rappel de mot de passe, édition et suppression de compte

Le même coeur : Le Service Container

Bien qu’ayant une approche différente au niveau des méthodes fournies, les 2 frameworks renferment le même système de Service container pour faire communiquer les différents composants ensemble. Si par exemple on veut générer une page :

// Sur laravel
view('posts/index'); // On fait appelle à une fonction globale

// Sur Symfony
$this->render('posts/index.html.twig') // On fait appelle à une méthode sur le controller

Bien que très différentes, ces 2 méthodes éxécutent un code relativement similaire si on regarde ce qui se cache derrière :

// Sur Laravel 
// view('posts/index');
Container::getInstance()->get('view')->make('posts/index');

// Sur Symfony
// $this->render('posts/index.html.twig')
$this->container->get('twig')->render('posts/index.html.twig')

Dans les 2 cas, le framework va commencer par créer un container qu’il va ensuite remplir de différents services qui pourront ensuite être récupérés suivant les besoins au sein de l’application. Laravel se différencie de Symfony par le fait qu’il rende le container accessible n’importe où dans l’application gràce à l’utilisation d’un Singleton, là ou Symfony imposera une plus grande rigueur en forçant l’utilisateur à spécifier les dépendances via un fichier services.yml.

Donc Symfony ou Laravel ? les 2 !

Laravel se focalise sur la simplicité du code pour le développeur (arriver à la solution simplement) ce qui passe par l’utilisation de méthode magique de PHP afin d’offrir un code basé sur des conventions.

Symfony impose plus de rigueur et est plus proche d’un code PHP classique (à l’exception de la configuration yaml) et est donc en général plus verbeux. Sa faible utilisation de méthodes magique permet une meilleur navigation dans le code et une analyse statique simplifiée.

Au final le choix va surtout dépendre de votre affinité vis à vis de la voix choisie par ces 2 frameworks pour résoudre la problématique de la création d’application Web.