Aller au contenu principal

Afficher des données

Nous disposons à présent de données aléatoires générées automatiquement dans notre base de données.

Nous allons donc pouvoir les afficher à l'écran !

Pour les afficher, il faut donc que notre vue dispose d'une collection d'articles à afficher.

MVC

Dans le pattern MVC, le contrôleur est chargé d'agir en tant que "glue" entre le modèle et la vue. Dans Symfony, il va donc se charger de récupérer les articles auprès d'une couche de service (les repositories), puis transmettre le résultat obtenu à la vue

Les repositories​

Dans Symfony, les repositories agissent comme une couche de service.

Un service, de manière générale dans une application, fournit des fonctionnalités que nous pouvons consommer depuis un autre endroit de l'application. Un service sera généralement représenté par une classe, et les fonctionnalités fournies, par des méthodes.

Dans notre cas, un repository peut donc être qualifié de service applicatif nous permettant de discuter avec notre base de données, à propos d'un sujet particulier (une entité).

On retrouve cette "sectorisation" sur une entité dans le constructeur, par exemple :

// src/Repository/ArticleRepository.php
//...
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class); // <-- Ici la référence à l'entité Article
}
//...

Si on explore un peu plus cette classe, on remarque qu'elle hérite d'une classe ServiceEntityRepository, qui elle-même hérite d'une classe EntityRepository. Ces 2 derniers fichiers appartiennent au package Doctrine, nous ne devons donc absolument pas tenter de les modifier (ils se trouvent quelque part dans les vendors).

Une rapide revue de la classe EntityRepository nous indique donc que notre ArticleRepository, par exemple, hérite de méthodes publiques telles que find, findAll, findBy, etc...

L'intérêt d'un ORM

Ce sont ces méthodes que nous voulons utiliser dans notre contrôleur, sur une instance d'un repository, afin de pouvoir récupérer nos articles sans écrire nous-mêmes le SQL. Nous conservons une approche objet, et c'est Doctrine qui va se charger du reste pour nous

Introduction à l'injection de dépendances​

Prenons un exemple : si on veut afficher tous les articles. La problématique principale réside donc dans la nécessité pour le contrôleur de disposer d'une instance de notre ArticleRepository, afin d'exploiter les méthodes offertes par ce service. Notre contrôleur est alors dépendant de notre repository : pour pouvoir fonctionner, il lui faut une instance du repository sur laquelle il peut appeler les méthodes dont il a besoin.

Le premier moyen que nous pourrions utiliser est l'instanciation d'un repository dans notre contrôleur :

//...
class IndexController extends AbstractController
{
#[Route('/', name: 'app_index')]
public function index(): Response
{
$articleRepository = new ArticleRepository();
}
//...
}

Le souci est que la signature du constructeur attend un paramètre ! Nous devrions donc nous poser une seconde problématique pour instancier ce paramètre. Mais celui-ci est de type ManagerRegistry, qui est une interface ! Nous ne pouvons pas instancier une interface...

Il existe un moyen plus simple pour disposer d'une instance de notre ArticleRepository : nous allons l'injecter directement dans notre contrôleur. Pour gérer l'injection de l'instance, nous allons simplement nous reposer sur le container de Symfony.

Le container​

Le container de Symfony est un ensemble de classes compilées dans le dossier var/cache de notre application. Il est compilé automatiquement par Symfony.

Il va contenir diverses informations sur notre application, et notamment la liste des services de notre application.

Pour les découvrir, il va explorer notre dossier src/ à la recherche des classes utilisables en tant que services.

Le fait qu'il aille chercher dans le dossier src/ n'est pas magique, et est issu de la configuration par défaut de Symfony :

# config/services.yaml

#...

services:
# ...

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: "../src/"
exclude:
- "../src/DependencyInjection/"
- "../src/Entity/"
- "../src/Kernel.php"
#...
YAML Format

Attention au format des fichiers Yaml : il s'agit d'un format permettant de déclarer des éléments de configuration. Ces éléments peuvent être regroupés en sections. Dans une section seront regroupés tous les éléments avec un niveau d'indentation supplémentaire. Une erreur dans la gestion de vos niveaux d'indentation peut tout simplement rendre votre fichier invalide !

Ici, dans la section services, le container va pouvoir aller explorer l'ensemble des classes présentes dans src/, à l'exception de certains fichiers et dossiers que nous ne voulons pas enregistrer dans le container en tant que services (directive exclude).

Ainsi, nous pouvons en déduire que notre container connaît l'ensemble de nos repositories en tant que services applicatifs, utilisables là où on en a besoin.

Par exemple, si je souhaite communiquer avec ma base de données à propos d'articles dans mon contrôleur, je peux donc injecter un argument de type ArticleRepository afin de consommer les méthodes que ce service nous offre. Je vais donc injecter le service dans mon contrôleur :

class IndexController extends AbstractController
{
//...
public function index(ArticleRepository $articleRepository): Response
{
// 1 - Je récupère les articles en discutant avec ma couche de service
$articles = $articleRepository->findAll();

// 2 - Je transmets les articles à la vue que je souhaite afficher
return $this->render('index/index.html.twig', [
'articles' => $articles,
]);
}
//...
}
Type-hinting

Cette méthode s'appelle le type-hinting : il s'agit de spécifier le type attendu pour un argument de méthode. Ensuite, le container reconnaît ce type, vu qu'il l'a déjà découvert en explorant nos fichiers dans src/, et est donc capable d'injecter le service correspondant à ce type, s'il le connaît

Pour résumer, nous pouvons donc dire que ArticleRepository est une dépendance de notre contrôleur, puisque ce dernier a besoin de ce service pour pouvoir fonctionner correctement, c'est-à-dire pour pouvoir récupérer les articles qu'il va transmettre à la vue.

Nous venons donc d'injecter cette dépendance dans notre contrôleur.

Inversion Of Control (IoC)

Ce que nous utilisons également quand on injecte un service, c'est l'inversion de contrôle : en quelque sorte, nous ne contrôlons plus l'instanciation du service et la déléguons à un module applicatif autre (ici, probablement le composant symfony/dependency-injection)

Explorer le container de services​

Avec la console applicative, on peut demander à Symfony de nous montrer les services dont il dispose :

php bin/console debug:container

La liste qui va s'afficher est un peu longue. En effet, le container de services contient les services que nous avons définis au niveau de notre application, mais également les services déclarés par les packages tiers que nous avons installés, donc les dépendances de notre projet.

C'est pour cela que si on regarde un peu la liste proposée, on retrouvera des services de log, d'envoi d'email, de traduction, de sécurité, etc...

On peut demander à Symfony de filtrer cette liste, par exemple si on voulait les informations sur notre ArticleRepository :

php bin/console debug:container Article

Article Repository informations

Pour le moment, ce qu'on va retenir des informations ci-dessus, c'est qu'au sein du container de services, les services que l'on écrit sont privés. On ne peut donc pas utiliser le container manuellement pour les récupérer, comme ça, de n'importe où. A la place, on va utiliser un type-hint, directement au sein de notre contrôleur.

Afficher les éléments d'une collection dans une vue Twig​

Notre contrôleur dispose d'un moyen de récupérer la collection d'articles, et peut la transmettre à la vue. Ceci s'effectue avec la méthode render :

public function index(ArticleRepository $articleRepository): Response
{
$articles = $articleRepository->findAll();

return $this->render('index/index.html.twig', [
'articles' => $articles,
]);
}

On définit alors la ou les variables que le template pourra manipuler, en deuxième argument de la méthode render : un tableau associatif définissant le nom d'une variable en tant que clé, et la valeur associée.

Dans notre template Twig correspondant, nous allons itérer sur la collection afin d'afficher chaque élément. Pour ce faire, nous allons utiliser une boucle for :

<ul>
{% for article in articles %}
<li>{{ article.title }}</li>
{% endfor %}
</ul>

On remarque que pour afficher un champ d'un objet Article, Twig nous permet d'accéder à l'attribut d'un objet à l'aide d'un point .

En réalité, il faut bien avoir en tête le mécanisme mis en oeuvre par Twig pour arriver à afficher l'attribut correspondant. En effet, dans notre exemple, l'attribut title est marqué comme private par exemple. Comment Twig peut-il donc deviner comment accéder à sa valeur ?

La réponse se trouve derrière ce lien : Twig dispose de plusieurs possibilités à explorer sur une variable lorsqu'on tente d'accéder à un attribut de celle-ci.

Dans notre cas, il s'arrête sur le cas "if foo is an object, check that getBar is a valid method", où foo est notre variable article et getBar notre méthode getTitle.

Pour une liste de catégories, alors on réutiliserait le même type de fonctionnement dans le contrôleur, mais avec le CategoryRepository. Par la suite, l'idée serait donc de passer les catégories à la vue :

class CategoryController extends AbstractController
{
/**
* @Route("/categories", name="categories_list")
*/
public function list(CategoryRepository $categoryRepository): Response
{
$categories = $categoryRepository->findAll();

return $this->render('category/list.html.twig', [
'categories' => $categories
]);
}
}

Ensuite, dans la vue :

<ul>
{% for category in categories %}
<li>
{{ category.name }} ({{ category.articles|length }} articles)
</li>
{% else %}
<li>Aucune catégorie trouvée</li>
{% endfor %}
</ul>

Deux nouveaux éléments ici :

  • la structure for de Twig intègre également un élément else permettant de définir un affichage si aucun élément n'a été trouvé dans la liste
  • On peut utiliser les filtres avec Twig : avec l'opérateur |, on applique un filtre sur un élément. Dans notre cas, sur la collection d'articles de chaque catégorie, on affiche simplement la longueur de cette collection avec le filtre length