Aller au contenu

Lire du code Laravel

Séance 5 · ~1 période

Vous avez écrit des classes, des traits et des enums dans un donjon. Voici six extraits d'un projet Laravel 12. Aucune notion nouvelle : ce sont les mots des chapitres 1 à 8, dans du code que vous n'avez pas écrit.

À la fin de ce chapitre, vous serez capables de :

  1. nommer le concept POO derrière chaque ligne d'un fichier Laravel ;
  2. repérer l'héritage, les traits, les enums, les interfaces et les relations dans un fichier inconnu ;
  3. distinguer ce que Laravel fabrique pour vous de ce que vous écrivez ;
  4. retrouver le chapitre du cours qui explique une ligne que vous ne comprenez pas.

Les quatre premiers extraits sont des fichiers écrits dans un projet. Les deux derniers viennent du code du framework lui-même. Laravel n'écrit pas declare(strict_types=1) en tête de ses fichiers : c'est notre convention à nous, pas la sienne.

1. Un modèle Eloquent

Eloquent est la partie de Laravel qui relie une classe PHP à une table de la base de données. Voici un modèle et l'enum qui l'accompagne.

<?php

namespace App\Enums;

enum PostStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}
<?php

namespace App\Models;

use App\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'body', 'status'];

    protected function casts(): array
    {
        return [
            'status' => PostStatus::class,
        ];
    }

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}
La ligne Le mot du cours Chapitre
namespace App\Models; puis use ...\Model; Namespace et import. Le chemin du fichier suit le namespace (PSR-4) 2
enum PostStatus: string Un enum dont chaque cas porte une valeur, ici une chaîne 6
class Post extends Model extends : héritage. Post reçoit les méthodes de Model sans en écrire une seule 5
use HasFactory; use à l'intérieur d'une classe : un trait. Ses méthodes sont ajoutées à Post 6
protected $fillable protected : lisible dans la classe et dans ses classes filles, pas depuis l'extérieur 1, 5
protected function casts(): array Redéfinition. Model a déjà une méthode casts() ; Post donne la sienne 5
'status' => PostStatus::class ::class donne le nom complet d'une classe, sous forme de chaîne 2, 6
public function comments(): HasMany Type de retour, et une association vers Comment 3, 4
return $this->hasMany(...) $this désigne cet article-ci. hasMany() vient de Model 1, 5

Le même modèle en diagramme de classes :

@startuml
class Model
class Post {
  +comments(): HasMany
}
class Comment
Model <|-- Post
Model <|-- Comment
Post --> "0..*" Comment : comments
@enduml

$fillable est une liste blanche. Post::create() n'écrit que les colonnes qui y figurent, pour qu'un formulaire trafiqué ne puisse pas modifier n'importe quelle colonne.

Note

protected $fillable n'a pas de type, contrairement à toutes les propriétés écrites depuis le chapitre 1. Cette propriété vient d'une version de Laravel antérieure aux propriétés typées de PHP 7.4. Lui ajouter un type aujourd'hui casserait des milliers d'applications.

À l'usage :

$post = Post::create(['title' => 'Bonjour', 'body' => '…', 'status' => 'draft']);

if ($post->status === PostStatus::Draft) {
    foreach ($post->comments as $comment) {
        echo $comment->body;
    }
}

$post->status ne contient pas la chaîne 'draft' mais l'objet PostStatus::Draft. C'est le travail de casts(). Le même mécanisme, sans Laravel :

<?php

declare(strict_types=1);

enum PostStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}

$status = PostStatus::from('draft');

echo $status === PostStatus::Draft ? 'oui' : 'non', "\n";
echo $status->value, "\n";
oui
draft

Attention

Post::create() s'écrit avec ::, mais aucune méthode create() n'est déclarée dans Model. Eloquent redirige l'appel vers son constructeur de requêtes. Retenez la règle d'écriture, pas le mécanisme : :: s'adresse à la classe, -> à un objet.

Une chose à retenir : un modèle Eloquent est une classe fille de Model qui utilise un trait.

2. Un contrôleur

Un contrôleur reçoit une requête HTTP et renvoie une réponse.

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function store(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'title' => ['required', 'string', 'max:255'],
            'body' => ['required', 'string'],
        ]);

        $post = Post::create($validated);

        return redirect()->route('posts.show', $post);
    }
}

La classe mère est dans votre projet, et elle est abstraite :

<?php

namespace App\Http\Controllers;

abstract class Controller
{
    //
}
La ligne Le mot du cours Chapitre
class PostController extends Controller extends encore. La classe mère est abstraite : on ne fait jamais new Controller 5
store(Request $request) Paramètre typé par une classe. Vous n'écrivez jamais new Request : Laravel fabrique l'objet et vous le donne 4
: RedirectResponse Type de retour : la méthode promet un objet de ce type 1
$request->validate([...]) Appel de méthode sur un objet reçu. Si la validation échoue, la méthode lève une exception et Laravel l'attrape 5
$validated Un tableau, pas un objet. Tout n'est pas un objet, même en POO 1
redirect()->route(...) Une fonction qui renvoie un objet, sur lequel on appelle aussitôt une méthode 1

Une chose à retenir : un paramètre typé par une classe, c'est Laravel qui fabrique l'objet et vous le donne.

3. Une migration

Une migration décrit une table de la base de données en PHP.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->string('status')->default('draft');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
La ligne Le mot du cours Chapitre
return new class extends Migration Une classe anonyme : elle hérite de Migration et n'a pas de nom, parce qu'on ne l'appellera nulle part ailleurs 5
Schema::create(...) Appel avec :: : on s'adresse à la classe Schema 1
function (Blueprint $table) Une fonction anonyme donnée en argument. Laravel l'appelle en lui passant un Blueprint 8
$table->string('status')->default('draft') Chaînage : la première méthode renvoie un objet, la seconde s'appelle dessus 8
up(): void et down(): void Deux méthodes publiques : appliquer la migration, l'annuler 1

Une chose à retenir : une migration est une classe fille de Migration, et la base de données s'y décrit avec des objets.

4. Une route

Une route relie une adresse à une méthode d'un contrôleur.

<?php

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
La ligne Le mot du cours Chapitre
use App\Http\Controllers\PostController; Import, en dehors de toute classe. C'est le use du chapitre 2, pas celui d'un trait 2
Route::get(...) Appel avec :: : on s'adresse à la classe Route 1
PostController::class Le nom complet de la classe, sous forme de chaîne. Aucun objet n'est créé ici 2, 6
[PostController::class, 'show'] Un tableau de deux chaînes : la classe et la méthode. Laravel fabriquera l'objet à l'arrivée de la requête 4
->name('posts.show') Chaînage : get() renvoie un objet, on lui donne son nom 8

Une chose à retenir : un fichier de routes ne crée aucun objet, il dit quelle méthode de quelle classe répondra à quelle adresse.

5. Une Collection

Post::all() ne renvoie pas un tableau mais un objet Illuminate\Support\Collection. Voici sa déclaration et celle de l'interface principale qu'elle réalise, recopiées du framework.

class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerable
interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
La ligne Le mot du cours Chapitre
implements Réalisation d'une interface : la classe s'engage à fournir les méthodes du contrat 6
Countable C'est pour cela que count($posts) fonctionne 6, 8
IteratorAggregate C'est pour cela que foreach ($posts as $post) fonctionne 6, 8
JsonSerializable C'est pour cela que json_encode($posts) renvoie ce qu'on attend 6
interface Enumerable extends ... Une interface peut en étendre d'autres. Une seule ligne en apporte cinq 6

C'est votre collection du chapitre 8, en plus grand :

$titles = Post::all()
    ->filter(fn (Post $post) => $post->status === PostStatus::Published)
    ->map(fn (Post $post) => $post->title);

Une chose à retenir : trois interfaces natives du chapitre 6 expliquent à elles seules pourquoi une Collection se compte, se parcourt et se convertit en JSON.

6. Cinq lignes du framework

Le début de Illuminate\Auth\SessionGuard.

class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
    use GuardHelpers, Macroable;

    public readonly string $name;

    protected $lastAttempted;
La ligne Le mot du cours Chapitre
implements StatefulGuard, SupportsBasicAuth Deux interfaces réalisées par une même classe 6
use GuardHelpers, Macroable; Deux traits utilisés dans une même classe 6
public readonly string $name; readonly : la valeur est écrite une fois, dans le constructeur, et plus jamais ensuite 1
protected $lastAttempted; Encore une propriété sans type, pour la même raison qu'au premier extrait 1, 5

Une chose à retenir : cinq lignes du framework contiennent quatre mots du cours et aucun mot nouveau.

7. La carte du bloc

Le mot du cours Chapitre Où vous le voyez dans Laravel
Classe, objet, $this 1 Chaque Post, Request, Collection manipulé
protected 1, 5 $fillable, $hidden, casts()
:: sur une classe 1 Post::all(), Route::get(), Schema::create()
readonly 1 SessionGuard::$name
Namespace, use, PSR-4 2 Les premières lignes de tout fichier
Association et multiplicités 3 hasMany(), belongsTo(), belongsToMany()
Objet reçu en paramètre 4 store(Request $request), function (Blueprint $table)
extends, redéfinition 5 extends Model, extends Controller, extends Migration
Classe abstraite 5 App\Http\Controllers\Controller
Exceptions 5 validate() qui échoue, et la page d'erreur qui suit
implements, interface 6 Collection, SessionGuard
Trait 6 HasFactory, Notifiable, Macroable
Enum 6 casts() avec PostStatus::class
Fonction anonyme, chaînage 8 function (Blueprint $table), ->filter()->map()

À retenir

  • :: s'adresse à la classe, -> à un objet.
  • Un fichier de Laravel commence presque toujours par des use, puis un extends.
  • Un paramètre typé par une classe : Laravel fabrique l'objet et vous le donne.
  • Une relation Eloquent est une association UML, avec ses multiplicités.
  • Une ligne que vous ne savez pas nommer a un chapitre : la carte ci-dessus dit lequel.

Exercices

  1. Ouvrez vendor/laravel/framework/src/Illuminate/Http/Request.php. Notez le extends, chaque trait et chaque interface, avec le chapitre correspondant. Comptez 15 minutes.
  2. En une phrase et avec le vocabulaire du chapitre 6 : pourquoi foreach ($post->comments as $comment) fonctionne-t-il ?
  3. Reprenez le diagramme de la section 1 et ajoutez-y PostController, avec une dépendance vers Post. N'ajoutez aucune relation qui n'apparaît pas dans les extraits de ce chapitre.