Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Cours Laravel 5.3 – les données – manipuler les données
Lundi 28 novembre 2016 14:00
Dans les précédents chapitres on a rencontré de nombreux exemples d'utilisation d'Eloquent pour manipuler des données. On a vu que sa capacité d'abstraction permet de réaliser facilement de nombreuses tâches sur les tables. Avec Eloquent chaque table est représentée par un modèle qui sert à interagir avec elle. On peut ainsi aller chercher des données, en insérer, les modifier, les supprimer... Il y a aussi dans Laravel un Query Builder qui est une puissante interface pour effectuer des requêtes sur les bases de données. Comme Eloquent et le Query Builder sont intimement liés on a parfois du mal à les distinguer. En gros Eloquent utilise le Query Builder pour constituer et exécuter les requêtes. Dans ce chapitre on va faire un peu le point de ce qu'on a vu et on va évoquer d'autres possibilités.

Les données

Pour effectuer des tests, nous aurons besoin de données. Dans le précédent chapitre on a construit avec le concepteur de schéma 4 tables reliées par des relations. On a ainsi récupéré des migrations et des modèles. On a aussi utilisé les migrations pour créer les tables dans la base de données. On va avoir besoin de tout ça pour ce chapitre. Pour rappel voici ce qu'on a dans la base : Au niveau des relations :
  • une relation 1:n entre les éditeurs et les livres
  • une relation n:n entre les auteurs et les livres
La structure est en place mais on va avoir également besoin de données. On va encore utiliser la librairie Faker qui est chargée par défaut dans Laravel. Modifiez ainsi le code du fichier des fabriques (models factories) :
<?php

$factory->define(App\Editeur::class, function (Faker\Generator $faker) {
    return [
        'nom' => $faker->name,
    ];
});

$factory->define(App\Auteur::class, function (Faker\Generator $faker) {
    return [
        'nom' => $faker->name,
    ];
});

$factory->define(App\Livre::class, function (Faker\Generator $faker) {
    return [
        'titre' => $faker->sentence(rand(2, 3)),
        'description' => $faker->text,
        'editeur_id' => $faker->numberBetween(1, 40),
    ];
});
Ensuite on prévoit ce code dans DatabaseSeeder :
<?php

use Illuminate\Database\Seeder;
use Faker\Factory;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(App\Editeur::class, 40)->create();
        factory(App\Auteur::class, 40)->create();  
        factory(App\Livre::class, 80)->create(); 
        
        for ($i = 1; $i < 41; $i++) {
            $number = rand(2, 8);
            for ($j = 1; $j <= $number; $j++) {
                DB::table('auteur_livre')->insert([
                    'livre_id' => rand(1, 40),
                    'auteur_id' => $i
                ]);
            }
        }
    }
}
Lancez ensuite la population :
php artisan db:seed
On aura ainsi 40 éditeurs et 40 auteurs avec des noms aléatoires. On aura aussi 80 livres affectés à des éditeurs et aussi des relations entre les livres et les auteurs. Donc de quoi effectuer tranquillement des requêtes.

N'oubliez pas de placer aussi les 3 modèles dans l'application.

Tinker

Artisan est plein de possibilités. Il y a en particulier un outil console, Tinker, qui permet d'effectuer des actions élémentaires. On appelle cela un REPL (Read-Eval-Print Loop). On peut entrer une expression, l'évaluer et on obtient le résultat. Pour démarrer Tinker c'est tout simple : Et maintenant il n'y a plus qu'à entrer des expressions, elles seront évaluée dans le contexte de Laravel. Voici un exemple avec notre application : Tinker est bien pratique pour faire des actions simples de ce genre. Vous pouvez donc l'utiliser s'il vous convient, en particulier pour les exemples de ce chapitre.

Les sélections simples

L'action la plus fréquente est sans doute le fait d'aller chercher des informations dans la base, on parle de sélection. On a rencontré de nombreux exemples dans les chapitres précédents.

Liste

Commençons par des choses simples, on veut tous les éditeurs. Avec Eloquent c'est facile : Les réponses d'Eloquent sont toujours des collections (Illuminate\Database\Eloquent\Collection). Ce sont des objets bien plus puissants et pratiques que de simples tableaux. Pour vous donner une idée des possibilités regardez toutes les méthodes disponibles. Autrement dit vous avez la possibilité d'effectuer simplement des traitements puissants sur les données retournées par Eloquent.

Enregistrement particulier

On peut retrouver un éditeur avec son identifiant :

Lorsqu'un seul résultat est retourné on n'a pas une collection mais un seul modèle.

On peut aussi le retrouver par son nom : On peut aussi sélectionner les colonnes qu'on veut :

Lignes distinctes

On peut aussi utiliser la méthode distinct pour avoir des lignes distinctes :

Plusieurs conditions

On peut combiner des where :

Encadrer des valeurs

On peut encadrer des valeurs avec whereBetween :

Prendre des valeurs dans un tableau

On peut aussi prendre des valeurs dans un tableau avec whereIn :

Aggrégations

On peut aussi compter, calculer...

Les erreurs

Que se passe-t-il si on ne trouve pas un enregistrement avec find ou first ? Regardez ces exemples : Donc si vous voulez générer une erreur Illuminate\Database\Eloquent\ModelNotFoundException utilisez findOrFail ou firstOrFail.

Les sélections avec plusieurs tables

Pour le moment on a vu des requêtes qui ne concernent qu'une seule table, ce qui n'est pas le plus répandu. Lorsque deux tables sont concernées on doit classiquement faire une jointure. Mais on a vu qu'Eloquent nous offre des possibilités bien plus pratiques avec les relations.

Trouver les titres des livres pour un éditeur dont on a l'id

Par exemple pour trouver tous les livres de l'éditeur avec l'identifiant 11 : Sans Eloquent le Query Builder devrait recourir à une jointure :

Remarquez que le Query Builder retourne lui aussi une collection. C'est une nouveauté bienvenue de la version 5.3.

Trouver les livres d'un auteur dont on connaît le nom

Maintenant cherchons les livres d'un auteur dont on connaît le nom. On s'en sort avec la méthode whereHas :

Attention aux requêtes imbriquées !

Il faut être prudent dans certaines situations. Par exemple supposez que vous voulez avoir la liste des auteurs avec pour chaque nom d'auteur la liste de ses ouvrages. Vous pourriez écrire ce genre de code :
$auteurs = App\Auteur::all();
foreach ($auteurs as $auteur) {
    echo '<h1>' . $auteur->nom . '</h1>';
	foreach($auteur->livres as $livre) {
		echo $livre->titre, '<br>';
	}
}
Tout se passe correctement à l'affichage :

Ça fonctionne très bien mais... si vous regardez vos requêtes vous allez être effrayé !

Dans mon cas j'en trouve 41 ! Tout simplement parce que pour chaque auteur vous lancez une requête pour trouver ses livres. Dans ce genre de situation il faut absolument utiliser le chargement lié (eager loading), c'est-à-dire demander à Eloquent de charger la table livres avec la méthode with :
$auteurs = App\Auteur::with('livres')->get();
foreach ($auteurs as $auteur) {
    echo '<h1>' . $auteur->nom . '</h1>';
	foreach($auteur->livres as $livre) {
		echo $livre->titre, '<br>';
	}
}
Le changement peut paraître minime mais on n'a plus que 2 requêtes maintenant :
select * from `auteurs`

select `livres`.*, `auteur_livre`.`auteur_id` as `pivot_auteur_id`, `auteur_livre`.`livre_id` as `pivot_livre_id` from `livres` inner join `auteur_livre` on `livres`.`id` = `auteur_livre`.`livre_id` where `auteur_livre`.`auteur_id` in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40')
Comment faire la même chose avec le Query Builder ? Ce n'est pas si simple, voici une solution avec utilisation d'une expression brute :
$results = DB::table('auteurs')
->select('nom', DB::raw('group_concat(titre) as titres'))
->groupBy('nom')
->join('auteur_livre', 'auteurs.id', '=', 'auteur_livre.auteur_id')
->join('livres', 'livres.id', '=', 'auteur_livre.livre_id')
->get();
foreach ($results as $result) {
    echo '<h1>' . $result->nom . '</h1>';
	$titres = explode(',', $result->titres);
	foreach($titres as $titre) {
		echo $titre, '<br>';
	}
}
Maintenant on n'a plus qu'une seule requête :
select `nom`, group_concat(titre) as titres from `auteurs` inner join `auteur_livre` on `auteurs`.`id` = `auteur_livre`.`auteur_id` inner join `livres` on `livres`.`id` = `auteur_livre`.`livre_id` group by `nom`

Si vous utilisez une expression brute dans une requête (par exemple ci-dessus avec DB::raw) celle-ci n'est pas immunisée contre les injections SQL, vous devez donc prendre vous-même des mesures de sécurité.

Le chargement lié peut même utiliser plusieurs relations successives. Regardez cet exemple : Ici on charge les livres et les auteurs liés en même temps que les éditeurs en une seule fois. On s'en sort avec 3 requêtes :
select * from `editeurs` limit 2

select * from `livres` where `livres`.`editeur_id` in ('1', '2')

select `auteurs`.*, `auteur_livre`.`livre_id` as `pivot_livre_id`, `auteur_livre`.`auteur_id` as `pivot_auteur_id` from `auteurs` inner join `auteur_livre` on `auteurs`.`id` = `auteur_livre`.`auteur_id` where `auteur_livre`.`livre_id` in ('21')

Remarquez l'utilisation de la méthode take pour limiter le nombre de résultats.

Vous n'arriverez pas toujours à réaliser ce que vous désirez avec seulement Eloquent, il vous faudra alors utiliser le Query Builder.

Insérer des enregistrements

Méthode "save"

Une façon simple d'ajouter un enregistrement est d'utiliser la méthode save : On crée une instance du modèle, on renseigne les attribut, on enregistre dans la base avec save. Si tout se passe bien il est retourné true. Avec cette méthode save on peut aussi ajouter un enregistrement à travers une relation pour renseigner automatiquement la clé étrangère : On voit que la colonne editeur_id a été renseignée.

On peut d'ailleurs mettre un tableau de modèles comme argument de la méthode save pour ajouter plusieurs enregistrements d'un coup.

Méthode "create"

Une autre façon de procéder est d'utiliser la méthode create : On transmet les attributs sous la forme d'un tableau.

On voit qu'il est retourné une instance du modèle, ce qui est pratique si on veut par exemple connaître l'identifiant de l'enregistrement créé.

Toutefois cette façon de procéder constitue ce qu'on appelle un assignement de masse. Imaginez que le tableau soit constitué de la saisie d'un formulaire et qu'on transmette ainsi le paquet à la méthode. Qu'est-ce qui empêche un utilisateur mal intentionné d'ajouter un attribut ? Pour éviter ça on doit définir précisément les colonnes qui sont autorisées à être ainsi renseignées avec la méthode $fillable dans le modèle. Regardez cet exemple : Apparemment la colonne editeur_id n'est pas renseignée alors qu'on l'a bien prévue dans le tableau. Regardez dans le modèle App\Auteur :
protected $fillable = ['titre', 'description'];
On a prévu seulement titre et description, du coup le reste ne passe pas et on a un erreur à la création.

Il y a des cas ou aucune erreur n'est déclenchée et vous ne vous apercevez de rien dans l'immédiat !

Si vous modifiez ainsi la valeur de la propriété :
protected $fillable = ['titre', 'description', 'editeur_id'];
Cette fois ça fonctionne :

Il existe la propriété $guarded qui est exactement l'inverse de $fillable.

A vous de voir quelles colonnes peuvent présenter des risques de sécurité de ce genre ! Avec cette méthode create on peut aussi ajouter un enregistrement à travers une relation pour renseigner automatiquement la clé étrangère :

Création si un enregistrement n'existe pas

On n'est pas toujours sûr de ce qui se trouve dans la base. Il arrive des fois où on aimerait créer un enregistrement si celui-ci n'existe pas déjà.

On peut évidemment commencer par aller vérifier sa présence et le créer au besoin. Mais Laravel nous offre une possibilité beaucoup plus simple avec les méthodes firstOrNew et firstOrCreate : A partir de ce scénario je vous laisse deviner la différence entre les deux méthodes.

Agir sur la table pivot

Attacher

Pour ajouter un enregistrement dans la table pivot on dispose de la méthode attach. Par exemple mon auteur 1 est relié à 4 livres : Je veux le relier à un autre livre, voilà comment faire : On trouve bien l'enregistrement dans la table pivot :

Détacher

A l'inverse on peut détacher avec la méthode detach :

Synchroniser

Parfois on veut mettre à jour globalement les attachements, on le fait avec la méthode sync : On attache les nouveaux, on détache ce que l'on ne veut plus.

On peut synchroniser sans détacher avec la méthode syncWithoutDetaching.

Mettre à jour des enregistrements

Méthode "save"

On peut utiliser la méthode save pour aussi mettre à jour des enregistrements :

Méthode "update"

On peut aussi utiliser la méthode update : L'avantage de cette méthode est qu'on peut mettre à jour plusieurs enregistrements : On voit ici qu'on a modifié 5 enregistrements en changeant l'éditeur de ces livres.

Supprimer des enregistrements

Méthode "delete"

On peut utiliser la méthode delete pour supprimer un enregistrement. Etant donné que j'ai prévu de ne pas avoir de suppression en cascade il devient plus difficile de donner un exemple. Par exemple pour supprimer un au‌teur il faut commencer par détacher tous ses livres : Ce chapitre est loin d'épuiser le sujet pour Eloquent et le Query Builder. Vous pourrez trouver tous les compléments utiles sur cette page de la documentation pour le Query Builder et sur cette page et celle-ci pour Eloquent.

En résumé

  • Eloquent permet de faire beaucoup de manipulations sur les tables et est à l'aise avec les relations.
  • Le Query Builder est le compagnon parfait pour Eloquent.
  • Il faut se méfier des requêtes imbriquées et utiliser le chargement lié (eager loading) pour limiter le nombre de requêtes générées.
  • Parfois on doit utiliser des expressions brutes dans les requêtes mais il faut alors penser à se protéger des injections SQL.


Par bestmomo

Nombre de commentaires : 2