Frameworks

Introduction

Jusqu'ici, nous avons programmé en PHP, et découvert le fonctionnement de la programmation côté serveur, et quelques problèmes:

  • Duplication de code
  • Capacité de capitaliser des morceaux d'une application à une autre
  • Aucune organisation claire du code (difficulté de travailler en équipe)
  • .. et on ne parle même pas de sécurité!

C'est pour cette raison qu'il existe des frameworks, dont l'utilisation devient indispensable pour des gros projets. Nous allons notamment utiliser deux d'entre eux en TD:

Mais avant, présentons les fonctionnalités de ces outils logiciels!

Principe MVC

Un mauvais exemple

Imaginons la page PHP suivante:

<ul>
<?php if ($_GET['page'] == 'show') { 
    $sql = 'SELECT title FROM series ORDER by TITLE ASC';
    $query = $pdo->query($sql);

    foreach ($query as $row) { ?>
        <li><?= $row['title']; ?></li>
<? } 
}
?>
</ul>

Ici, de nombreuses choses sont mélangées ensemble, rendant le code compact, difficile à lire et peu réutilisable.

On peut d'ailleurs faire une liste des problèmes:

  • La logique de vue, ainsi que la requête SQL sont "en dur" et donc pas réutilisable par un autre composant
    • Imaginez par exemple que l'on souhaite changer la source de données (on voudrait réutiliser la vue)
    • Ou encore écrire une API JSON en plus du site web (on voudrait réutiliser la requête)
  • Si la requête échoue, la page arrêtera son chargement en plein milieu du rendu
  • L'alternance des <?php rend le code difficile à lire lorsqu'il y a de la logique autour
  • D'un point de vue maintenance, impossible de trouver par exemple tout le code qui concerne la base de données dans une telle application
  • Ce code n'est pas testable unitairement, car fonctionnellement fusionné.

Principe

Le principe MVC, pour Modèle, Vue, Contrôleur, propose une séparation des responsabilités:

  • Le Modèle concerne les données de l'application,
  • La Vue concerne l'interface graphique,
  • Les Contrôleurs traîtent les actions de l'utilisateur.

Exemple

Par exemple, pour réécrire ce code en respectant cette séparation, on pourrait tout d'abord écrire dans une classe Model:

// Model.php, classe Model
protected getSeries(): array
{
    $sql = 'SELECT title FROM series ORDER by TITLE ASC';
    $query = $this->pdo->query($sql);

    return $query->fetchAll();
}

Et ensuite, récupérer ces valeurs dans le contrôleur, si le paramètre GET est passé à la bonne valeur:

if ($_GET['page'] == 'show') {
    $series = $model->getSeries();
}

Et enfin l'afficher dans la vue:

<ul>
    <?php foreach ($series as $serie) { ?>
        <li><?= $serie['title']; ?></li>
    <?php } ?>
</ul>

Cependant, un framework n'est pas qu'une philosophie d'organisation, mais aussi un ensemble d'outils logiciels qui vous assistent dans chacune de ces tâches. Nous allons présenter certains de ces composants.

Routeur

Principe

Jusqu'ici, voici comment nous avons procédé:

Ici, le travail implicite de routage est effectué par le serveur, car les scripts PHP sont des ressources différentes.

Le problème avec cette approche:

  • Dans chaque fichier .php, il faudra ré-inclure les différentes parties du code (boilerplate), ce qui rendra l'application peu maintenable
  • Les noms des pages apparaîtront dans la barre d'URL, ce qui n'est pas forcément adapté au référencement et ne permet pas de flexibilité sur le nommage des adresses

Une solution pour avoir de belles URLs est d'utiliser le mécanisme de réécriture d'URL du serveur web:

Cette solution nous permet d'obtenir des URLs arbitraires, mais implique de maintenir un fichier de réécriture d'URLs, qui est de plus spécifique au serveur web utilisé.

De plus, elle ne résout pas le problème des multiples fichiers PHP.

Lorsqu'on utilise un framework, la réécriture d'URL est utilisée mais vers une page frontale unique:

De cette façon, le routage est réalisé en PHP au sein de l'application.

Le problème du routage correspond à l'association entre les adresses (URL) et le contrôleur (le morceau de code PHP) qui va être invoqué.

Le routeur est donc un ensemble de règles de correspondance.

Par exemple:

  • /homeDefaultController::home()
  • /loginSecurityController::login()
  • /product/{id}ProductController::show($id)

Exemple

<?php

class DefaultController
{
    /**
    * @Route("/hello/{name}", name="hello")
    */
    public function hello($name)
    {
        return new Response('Hello '.$name);
    }
}

Ici:

  • L'annotation @Route("/hello/{name}") est une manière de faire correspondre la route /hello/{name} avec la fonction hello($name)
  • La fonction retourne simplement une réponse contenant le nom
  • La route est également dotée d'un nom (hello)

Génération de routes

Les routes permettent de faire correspondre une adresse à un contrôleur, mais également de produire des URLs.

On pourra par exemple à partir du nom de la route demander au routeur de générer l'adresse correspondante:

<?php

$homeUrl = $this->generateUrl('home');
$helloBobUrl = $this->generateUrl('hello', ['name' => 'Bob']);

De cette façon, les noms externes visibles de tous (/hello/Bob) sont dissociés du nom interne de la route (hello)

Avantages

  • Ne pas avoir une page PHP par page effective, ni de règles complexes de réécriture d'URLs
  • Capturer les paramètres d'URL (ex: /product/{id})
  • Pouvoir changer facilement les URLs des pages, sans devoir les changer partout ailleurs
  • Invoquer des actions différentes selon la méthode
  • Appliquer des règles de sécurité au moment du routage (par exemple restreindre /admin/*)

Moteur de template

Problème

Il est tout à fait possible d'écrire des pages PHP comme système de template.

Cependant, on peut lui faire quelques reproches:

  • La syntaxe, un peu lourde (<?php...)
  • Le fait de devoir échapper systématiquement les variables (cf failles XSS)
  • L'absence d'héritage (nous allons expliquer de quoi il s'agit)

Présentation

Un exemple de moteur de template est Twig.

Ce système permet de simplifier l'écriture des vues, c'est à dire du contenu des pages HTML qui seront rendues.

Twig supporte l'héritage, l'échappement par défaut et de nombreuses astuces syntaxiques pour simplifier l'écriture des templates.

Utilisation

Voici un exemple de template:

<html>
    <head>
        <title>
        {% block title %}Mon titre{% endblock %}
        </title>
    </head>
    <body>
        <h1>{{ block('title') }}</h1>
        {% block content %}
        Bonjour {{ name }} !
        {% endblock %}
    </body>
</html>

Comme vous le voyez, Twig permet d'écrire des documents directements en HTML, à l'exception de certains tags qui permettent d'y ajouter de la structure, à l'instar du PHP.

Dans cet exemple:

  • {% block contents %} est un bloc qui pourra être surchargé dans les templates filles
  • {% block('title') %} sert à ré-afficher le contenu du block title précédement utilisé
  • {{ name }} correspond à l'affichage d'une variable

Héritage

La template précédente peut être héritée comme cela:

{% extends 'index.html.twig' %}

{% block title %}
    {{ parent() }} - Ma page
{% endblock %}

{% block contents %}
    Bienvenue sur cette page!
{% endblock %}

Le mot clé extends permet de décrire que cette page hérite de index.html.twig, de la même manière que l'héritage des classes votre template se basera alors sur cette template mère et pourra redéfinir son comportement.

Les blocs peuvent alors être surchargés, c'est à dire modifié en les redéfinissant. Il est aussi possible d'utiliser le mot clé parent() pour faire appel à la template mère et utiliser son contenu, comme dans le cas du titre qui deviendra ici "Mon titre - Ma page"

Boucles, conditions

Il est également possible d'effectuer des tests et des boucles avec Twig:

{% if not users|length %}
<i>Aucun utilisateur</i>
{% else %}
<ul>
    {% for user in users %}
        <li>{{ user.name }}</li>
    {% endfor %}
</ul>
{% endif %}

Pour une documentation plus exhaustive, vous pouvez consulter la documentation officielle de Twig.

Avantages

  • Une rédaction simplifiée des vues
  • Moins de porosité entre le rôle du contrôleur et de la vue
  • Une sécurisation avec l'échappement par défaut des variables
  • Des fonctionnalités supplémentaires que du PHP "brut" telles que l'héritage
  • La résolution des propriétés (user.name peut être $user['name'] ou $user->getName())

Mapping objet/relationnel

Présentation

Un ORM, pour Object Relational Mapping, désigne le fait de réaliser un mapping, ou une association entre le monde relationnel (tables, lignes, champs ...) et le monde objet (classes, instances, attributs ...).

Ce mapping est généralement fait à l'aide de fichiers de configuration ou d'annotations.

Correspondance

RelationnelObjet
TableClasse (ou entité)
LigneInstance
ColonneAttribut
Clé étrangèreRéférence

Cette correspondance ressort si l'on compare un schéma entité association (MCD) avec un schéma UML.

Database First vs Code First

Il existe deux paradigmes en matière d'ORM:

  • Database First, dans lequel la base est créée avant l'application qui l'utilise. Cette méthode est intéressante lorsque la base est déjà présente pour des raisons historiques, ou alors qu'elle est également utilisée par d'autres applications par exemple.
  • Code First, où le code est écrit avant la base de données et cette dernière peut même être générée par l'ORM lui-même.

Exemple: Doctrine

Un exemple est Doctrine, l'ORM présent dans le framework Symfony.

En Database First, ce dernier est par exemple capable de générer à partir de la base shows un ensemble de classes qui correspondront aux tables de la base de données.

Requêtage via Doctrine

Il est alors possible d'effectuer des requêtes en utilisant Doctrine:

public function indexAction(SeriesRepository $repository)
{
    $series = $repository->findBy(['year_start' => '2011']);

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

Puis de les afficher dans la vue:

<ul>
    {% for serie in series %}
        <li>{{ serie.title }}</li>
    {% endfor %}
</ul>

Il est intéressant de remarquer que series est ici un ensemble d'instances de la classe Series.

Insertion

On peut également créer des entrées à l'aide de persist():

<?php
$entityManager = $this->getDoctrine()->getManager();
$user = new User();
$user
    ->setName('Toto')
    ->setEmail('toto@toto.com')
    ->setPassword('totopass')
    ;
$entityManager->persist($user);
$entityManager->flush();

La plupart des opérations simples peuvent être effectuées sans écrire de requête SQL!

Requêtage

En général, un ORM fournit une API pour effectuer le requêtage via des méthodes, et aussi un langage spécifique.

Par exemple, Doctrine propose le langage DQL pour requêter la base de données:

<?php
// On ne requête ici pas dans les tables mais les "classes" de l'ORM
// Le langage est indépendant du vrai SGBD
$query = $entityManager->createQuery('SELECT s
    FROM App:Series s
    WHERE s.year_start = "2011"');
$series = $query->getResult();

Avantages

  • La persistence des objets
  • Le requêtage, parfois à travers une couche d'abstraction supplémentaire
  • La notion de transaction est préservée
  • La création et la mise à jour de la structure de la base de données à partir de la définition des entités
  • Possibilité de faire abstraction du système de gestion de base de données sous-jacent