Plan du site  
pixel
pixel

Articles - Étudiants SUPINFO

Créer une architecture MVC complète sans framework en PHP

Par Olivier SEROR-DROIN Publié le 16/11/2016 à 11:45:00 Noter cet article:
(0 votes)
Avis favorable du comité de lecture

Introduction

Nous allons voir comment créer une architecture MVC, semblable à ce que l'on trouve dans les grands frameworks. Ceci est à la fois intéressant pour pouvoir développer une application indépendante de tout framework, mais également éviter à terme les soucis de migrations, abandons de frameworks, ... Ainsi que d'améliorer notre compréhension des méchanismes derrière les fonctionnements MVC des frameworks.

En effet, nous avons de plus en plus tendance à nous reposer sur ces frameworks, mais il peut être utile de savoir recréer ces méchanismes, ne serait-ce que pour être indépendant technologiquement, ou si vous avez pour objectif de concevoir un framework en y apportant vos plus-values.

Notez que ceci a des avantages et des inconvénients :

Avantages :

  • Amélioration de nos connaissances en PHP et sur le fonctionnement des frameworks, namespaces, ...

  • Indépendance (ne pas avoir à se soucier d'un abandon d'un framework ou de l'arrivée d'une nouvelle version majeure sans rétro-compatibilité, ce qui oblige à retravailler beaucoup de codes, ...)

  • Modularité (choix de certains composants comme les ORMs et les moteurs de templates)

  • Performance (selon la qualité de notre code et selon nos besoins, il est possible d'optimiser davantage que ne le permettraient certains frameworks)

  • Codes réutilisables (en ne profitant pas des spécificités des frameworks et en comprenant notre code, il devient plus facile à porter vers un framework MVC que dans le cadre d'une migration d'un framework à un autre totalement différent)

Inconvénients :

  • Sécurité (il faut faire attention au code que l'on écrit. Ce dernier n'ayant pas la maturité des frameworks qui ont connu beaucoup de correctifs à ce niveau, il est également bon de faire attention à ne pas créer soi-même des failles)

  • Temps de développement initial (il est nécessaire de "réinventer la roue" à sa façon, ce qui prendra plus de temps que d'avoir un framework prêt à l'usage)

Pré-requis

Voici la liste des logiciels dont vous aurez besoin :

  • Un environnement serveur web avec PHP 5.4 ou supérieur

    • Windows : WAMP, XAMPP ou environnement similaire

    • Mac OS X : XAMPP

    • Linux/UNIX : environnement type LAMP (un serveur web comme Apache ou NGINX, une installation de PHP et un serveur de base de donnée) ou XAMPP

  • Composer

  • Un éditeur de texte avancé tel que Atom.io, Sublime Text ou encore un IDE comme PHPStorm

Mise en place de l'architecture MVC

Nous allons dans un premier temps créer les dossiers qui contiendront les fichiers de notre architecture MVC : "models", "views", "controllers". Vous pouvez également rajouter un dossier "components" ou "includes" (pour les classes appelées par différents contrôleurs et n'intéragissant pas directement avec l'utilisateur).

Ensuite rendez vous avec une ligne de commande dans le dossier de votre projet, et lancez la commande suivante pour initialiser le projet :

composer init

Et répondez aux différentes questions de composer afin d'initialiser

Utilisation de l'autoloader PSR-4 de Composer

Composer n'est pas seulement un système permettant de gérer des modules en PHP au sein d'un projet, il peut être également utilisé pour son "autoloader" PSR-4.

Un autoloader est un code en PHP qui permet en une seule fonction require de charger de nombreux fichiers différents en PHP. Ceci est indispensable au sein d'un grand projet ou au sein d'un projet pouvant être amené à grossir, sans avoir à s'inquiéter de la lourdeur des inclusions.

Composer en fournit automatiquement un qui, de plus, fonctionne dans le respect de la norme PSR-4. Nous verrons dans la partie suivante en quoi ceci consiste. Mais pour l'instant, vous allez devoir modifier le fichier composer.json de votre projet afin d'y ajouter une section "autoload" avec une sous-section "psr-4" contenant la liste des namespaces et dossiers correspondant à charger, par exemple ainsi :

{
    "name": "olivier/mvcphp",
    "description": "project MVC PHP",
    "type": "project",
    "require": {},
    "autoload": {
      "psr-4": {
        "Components\\": "components/",
        "Controllers\\": "controllers/",
        "Models\\": "models/"
      }
    }
}

Une fois ce fichier modifié ainsi, vous devez forcer Composer à créer son autoloader (il ne s'agit avant ça que d'une configuration). Pour cela, toujours depuis une ligne de commande en étant situé dans le répertoire de votre projet, tapez la commande :

composer update

Puis patientez pendant que Composer vous crée et remplit un dossier "vendor" dans votre projet.

La partie "autoload" / "psr-4" de ce fichier composer.json fonctionne ainsi :

On permet le chargement de fichiers par leur namespace, en définissant que le namespace "Components\" appelera un fichier dans "components/".

Ainsi si vous voulez appeler un modèle "Users" qui sera la classe d'un fichier "models/User.php", il suffira d'écrire dans le fichier exploitant ce modèle :

use Models\Users;

Vous pouvez par ailleurs rajouter un alias à cette ligne ainsi (si par exemple dans votre fichier tous les appels à ce modèle concernent un "author"). Cependant, ce n'est pas forcément le plus cohérent pour savoir rapidement quel modèle est utilisé :

use Models\Users as Author;

Nous allons voir cet exemple en détail à présent dans l'utilisation des namespaces en PHP.

Utilisation des namespaces en PHP

Quand nous faisons de la programmation orienté objet en PHP, nous travaillons avec des classes afin de faciliter la réutilisation de code via l'héritage. Nous allons commencer par réaliser un contrôleur "UserController.php" qui nous servira à afficher une liste d'utilisateurs à terme. Le squelette de base de notre classe User est le code suivant :

<?php

namespace Controllers;

use Models\Users;

class UserController
{
    public function index()
    {
        echo "Hello User Page!";
    }
}

Nous allons également créer un contrôleur "IndexController.php" qui servira à afficher la page d'accueil de notre projet :

<?php

namespace Controllers;

class IndexController
{
    public function index()
    {
        echo "Hello World!";
    }
}

Dans ces fichiers, la ligne "namespace" permet d'établir un espace de nommage pour notre classe. Elle s'écrit en commençant par l'espace de nommage défini pour ce dossier dans notre autoloader (par respect des normes et simplicités, on utilise les noms des dossiers pour y faire une sorte de chemin relatif à la racine du projet).

Il est bien sûr possible de passer des paramètres à la fonction index ou utiliser d'autres fonctions (comme "add", "edit", ... qui agiraient sur nos Users). Nous organisons notre MVC comme nous le voulons. Cependant pour mes exemples, je ferai appel à la fonction index en priorité.

Si nécessaire, la ligne "use" va charger une classe "Users" que nous créerons dans le dossier "models". Elle nous autorisera alors à utiliser cette classe dans notre contrôleur. Nous reviendrons sur son usage après l'avoir créer dans la partie sur les ORM plus bas dans cet article.

Un index.php pour les charger tous

En PHP, nous utilisons un index.php à la racine de notre site, qui est (selon la configuration du serveur) généralement servi par défaut. Quand nous utilisons un framework, il y en a généralement un aussi, mais nous n'y touchons que très peu, il comporte un require pour l'autoloader, ainsi qu'éventuellement des configurations. Il peut également contenir le code qui permettra de rediriger les requêtes à travers notre architecture MVC ou l'appel à un routeur s'en chargeant... Mais il ne comportera pas plus de logique que cela, son rôle se limitant à un point d'entrée vers le reste.

Ainsi, votre utilisateur pourra avoir des URLs du style :

index.php?c=user&t=index&params[param1]=test&params[param2]=test2

Notez qu'il est possible de gérer ceci différemment à l'aide d'un routeur, mais également de rendre plus jolies ces URLs à l'aide de redirections partiellement en PHP et en redirections niveau serveur (htaccess, configuration nginx, ...).

Voici un index.php très basique capable d'effectuer ce travail :

<?php

require_once "vendor/autoload.php";

$class = "Controllers\\" . (isset($_GET['c']) ? ucfirst($_GET['c']) . 'Controller' : 'IndexController');
$target = isset($_GET['t']) ? $_GET['t'] : "index";
$getParams = isset($_GET['params']) ? $_GET['params'] : null;
$postParams = isset($_POST['params']) ? $_POST['params'] : null;
$params = [
    "get"  => $getParams,
    "post" => $postParams
];

if (class_exists($class, true)) {
    $class = new $class();
    if (in_array($target, get_class_methods($class))) {
        call_user_func_array([$class, $target], $params);
    } else {
        call_user_func([$class, "index"]);
    }
} else {
    echo "404 - Error";
}

Il commence par appeler l'autoloader de Composer. Ce dernier permettra de charger nos classes à travers l'usage des namespaces sans autre requires/includes.

Il récupère la classe qu'il faudra instancier dans une chaîne de caractères.

Notez que PHP a une capacité un peu spéciale à ce niveau : si vous mettez des parenthèses après une variable contenant une chaîne de caractères, il agira comme si son code contenait un appel à une méthode dont le nom est le contenu de la chaîne. Cependant pour des raisons de lisibilité et de simplicité dans la gestion des paramètres... il vaut mieux favoriser l'utilisation des fonctions PHP call_user_func et call_user_func_array.

Enfin il récupère la fonction cible que nous appelerons dans le contrôleur, puis stockera les paramètres dans un tableau.

Si on appelle une classe inexistante, il affichera simplement 404 (nous pourrions effectuer une redirection vers une page 404 ou renvoyer à un contrôleur "Index" ...)

Si on appelle une méthode non présente au sein d'une classe, elle appellera par défaut la méthode "index".

Vous pouvez déjà tester ces codes. L'URL de votre site devrait vous afficher Hello World! :

Tandis qu'avec un appel au contrôleur "User" vous devriez voir :

Nous avons donc déjà le code nécessaire au fonctionnement de nos contrôleurs. Nous allons à présent nous pencher sur les modèles.

Installation d'un ORM pour vos Models

Il est bien entendu possible de développer soi-même un lien entre ses modèles et sa base de donnée. Cependant il est davantage intéressant de se reposer sur quelque chose d'éprouvé et fiable, surtout pour des raisons de sécurité. Il convient donc d'utiliser un ORM (Object Relational Mapper). Il y en a généralement dans les frameworks PHP, mais il est parfois possible de les utiliser hors de leurs frameworks, comme Doctrine (utilisé avec Symfony) ou Eloquent (utilisé avec Laravel).

Il existe également des ORMs totalement indépendants des frameworks, comme Propel, RedBeanPHP ou encore Spot ORM. C'est par ailleurs ce dernier que nous allons utiliser dans cet article.

Nous allons privilégier une installation par Composer. Nous l'utilisons dans notre projet. Afin de l'exploiter davantage, il nous suffit d'aller dans notre projet via une ligne de commande et d'écrire :

composer require vlucas/spot2

Pour information, si vous voulez rechercher d'autres modules ou ORMs à installer depuis Composer, rendez vous sur le site packagist.org qui vous permet de consulter tout ce que Composer est capable d'installer à l'aide de cette commande.

Avant toute chose, veuillez créer une base de donnée dans votre serveur MySQL. Dans le cadre de cet article, j'appellerai la mienne "mvctest".

Une fois la base de donnée créée, il faut introduire dans notre code la configuration qui permettra à l'ORM de s'y connecter. Nous allons donc modifier notre fichier index.php ainsi :

<?php

require_once "vendor/autoload.php";

// Spot2 ORM Configuration
function spot() {
    static $spot;
    if ($spot === null) {
      $cfg = new \Spot\Config();
      $cfg->addConnection('mysql', [
          'dbname' => 'mvctest',
          'user' => 'root',
          'password' => '',
          'host' => 'localhost',
          'driver' => 'pdo_mysql',
      ]);
      $spot = new \Spot\Locator($cfg);
    }

    return $spot;
}
//

$class = "Controllers\\" . (isset($_GET['c']) ? ucfirst($_GET['c']) . 'Controller' : 'IndexController');
$target = isset($_GET['t']) ? $_GET['t'] : "index";
$getParams = isset($_GET['params']) ? $_GET['params'] : null;
$postParams = isset($_POST['params']) ? $_POST['params'] : null;
$params = [
    "get"  => $getParams,
    "post" => $postParams
];

if (class_exists($class, true)) {
    $class = new $class();
    if (in_array($target, get_class_methods($class))) {
        call_user_func_array([$class, $target], $params);
    } else {
        call_user_func([$class, "index"]);
    }
} else {
    echo "404 - Error";
}

Maintenant, passons au plus intéressant : les modèles eux-même. Nous allons donc nous rendre dans le dossier "models" et y créer un premier fichier Users.php, dont le squelette de base sera :

<?php

namespace Models;

class Users extends \Spot\Entity
{
    protected static $table = 'users';

    public static function fields()
    {
      return [
        'id'        => ['type' => 'integer', 'primary' => true, 'autoincrement' => true],
        'name'      => ['type' => 'string', 'required' => true, 'unique' => true],
        'email'     => ['type' => 'string', 'required' => true],
        'password'  => ['type' => 'string', 'required' => true],
        'admin'     => ['type' => 'boolean', 'default' => false, 'value' => false]
      ];
    }
}

Nous définissons au sein de notre modèle son schéma pour la base de donnée. De ce fait, nous n'avons pas besoin de créer manuellement notre base de donnée, nous pouvons tout concevoir du côté du code !

Bien entendu, ceci peut être différent selon l'ORM utilisé (différentes logiques, syntaxes, gestion des relations par foreign keys, ...). Dans le cas de Spot, vous pouvez trouver les différents types de champs supportés par l'ORM à cette adresse : http://phpdatamapper.com/docs/types/

Vous trouverez également plus de détails sur le fonctionnement de cet ORM sur sa page GitHub : https://github.com/vlucas/spot2

Dans le cas de cet ORM, il n'est pas nécessaire de définir des attributs à notre modèle, vu que tout va se faire à travers un "mapper" (ce n'est, par exemple, pas le cas avec Eloquent qui fonctionne sur un autre principe). Nous allons donc nous rendre dans notre fichier UserController.php afin d'ajouter deux nouvelles méthodes à ce contrôleur :

    public function create($params)
    {
      if (!isset($params['name'])) {
        $name = "Example";
      } else {
        $name = $params['name'];
      }
      $userMapper = spot()->mapper('Models\Users');
      $userMapper->migrate();
      $myNewUser = $userMapper->create([
        'name'      => $name,
        'email'     => 'example@example.example',
        'password'  => '123456789'
      ]);
      echo "A new user has been created: " . $myNewUser->name;
    }

    public function list()
    {
      $userMapper = spot()->mapper('Models\Users');
      $userMapper->migrate();
      $userList = $userMapper->all();
      echo "List: <br />";
      foreach ($userList as $user) {
        echo $user->name . "<br />";
      }
      echo "---";
    }

Nous pouvons dès à présent tester le résultat, ce qui nous donne :

Ainsi que :

Nous sommes donc en mesure d'utiliser des modèles dans nos contrôleurs, ce qui nous fournit les deux tiers de l'architecture MVC ! Il ne nous reste en effet plus que la Vue.

Mise en place d'un moteur de template

Comme nous venons de le voir, nous arrivons à lire et écrire dans notre base de donnée et à effectuer de la logique dans nos contrôleurs. Cependant, le résultat n'est pas des plus "beaux" visuellement, et c'est là qu'un moteur de template trouve son intérêt. Nous pouvons utiliser Smarty, Twig, Blade, ... Il existe comme pour les ORMs un choix assez large.

Pour cet article, je vais utiliser Twig. Nous pouvons donc, comme pour notre ORM, l'installer via la commande Composer suivante :

composer require twig/twig

Après l'avoir installé, nous allons, pour des raisons d'organisation, initialiser et configurer Twig dynamiquement au sein d'une classe parent aux contrôleurs. Créez donc un fichier "Controller.php" dans le dossier "Controllers", et remplissez-le ainsi :

<?php

namespace Controllers;

use \Twig_Loader_Filesystem;
use \Twig_Environment;

class Controller
{
    protected $twig;

    function __construct()
    {
      $className = substr(get_class($this), 12, -10);
      // Twig Configuration
      $loader = new Twig_Loader_Filesystem('./views/' . strtolower($className));
      $this->twig = new Twig_Environment($loader, array(
          'cache' => false,
      ));
      //
    }
}

Et modifiez vos autres fichiers du dossier "Controllers", afin que leurs classes héritent ("extends") de celle-ci. Par exemple pour "UserController.php", modifiez la ligne :

class UserController

en :

class UserController extends Controller

Ce code étant assez particulier, il est intéressant de comprendre son fonctionnement :

  1. Lorsqu'une de nos classes contrôleurs telle que UserController est instanciée, elle va commencer par instancier sa classe parente, soit Controller.

  2. La méthode __construct() est une méthode magique de PHP qui est systématiquement instanciée à la création de la classe (ceci vaut également du coup pour une classe enfant).

  3. La fonction get_class() retourne le nom de la classe où elle est appelée ou de l'objet qu'on lui donne en paramètre, en spécifiant $this en paramètre, on ne lui donne pas une instance de Controller, mais une instance de la classe enfant qui a appelé Controller. Nous obtenons donc, dans le cas de UserController, la valeur "Controllers\UserController".

  4. Pour des raisons d'organisation, afin de séparer les vues de chaque contrôleur dans des sous-dossiers, nous voulons un nom spécifique à ce contrôleur, soit "User". De ce fait, nous utilisons "substr" de façon à ne découper que cette partie de la valeur de get_class($this).

  5. L'environnement Twig est stocké dans l'attribut de Controller $twig qui, étant protected, est accessible depuis UserController.

Maintenant, nous allons organiser les sous-dossiers du dossier "views". Commencez donc par créer un sous-dossier "index" et un sous-dossier "user" à votre dossier "views".

Pour cet article, nous allons nous concentrer pour démontrer l'utilité des views dans le MVC à notre action "list" du contrôleur view. Pour cela, créez un fichier "list.html" dans "views/user/" et remplissez-le du code suivant :

<!DOCTYPE html>
<html>
    <head>
        <title>MVC Test - Index</title>
        <style>
          body {
            background-color: black;
            color: yellow;
          }
          .username {
            color: green;
          }
        </style>
    </head>
    <body>
      <h2>List: </h2>
      {% for user in userList %}
        <li class="username">{{ user.name }}</li>
      {% endfor %}
      <hr />
      <p>Quantity: {{ quantity }} users.</p>
    </body>
</html>

Il s'agit de code HTML reconnaissable, mais avec les balises supplémentaires fournies par Twig permettant d'afficher des données provenant de variables et un peu de logique dans nos Vues (conditions, boucles, ...). Dans cet exemple, une boucle foreach permettra d'afficher l'attribut name des objets issus d'un tableau "userList".

Il ne reste plus qu'à demander à notre UserController d'utiliser Twig dans sa méthode list(). Nous allons la ré-écrire ainsi :

    public function list()
    {
      $userMapper = spot()->mapper('Models\Users');
      $userMapper->migrate();
      $userList = $userMapper->all();

      echo $this->twig->render('list.html',
        [
          "userList" => $userList,
          "quantity" => count($userList)
        ]
      );
    }

Maintenant, observons le résultat :

Nous pouvons donc désormais travailler avec des vues ! Notre architecture MVC est complète, la logique est faite dans les contrôleurs, qui récupèrent ou envoient des données à des modèles et génère une vue HTML où il est facile d'introduire des valeurs issues de variables ou des listes...

Il est bien entendu possible de faire beaucoup mieux (et heureusement) sans plus de complexité. Il est tout à fait possible, à partir de là, d'intégrer Bootstrap, du Material design ou n'importe quel framework frontend et d'utiliser du javascript dans des fichiers séparés. N'oubliez pas que, pour votre navigateur, vous êtes toujours sur index.php. Il est donc tout à fait envisageable de créer un dossier "resources" dans lequel mettre les fichiers javascripts et CSS dont vous avez besoin afin de réaliser un site très professionnel par-dessus ce code PHP en backend.

Pour aller plus loin

Nous avons réussi à faire fonctionner un véritable projet suivant le pattern MVC en PHP, sans être dépendant d'un framework. Nous pouvons ainsi travailler sur notre projet sans se soucier du risque que le framework soit abandonné ou de devoir suivre ses mises à jours. De plus, nous connaissons maintenant bien mieux ce qui se déroule en arrière plan au sein des frameworks. Nous savons maintenant faire un code modulable, en choisissant nous-même l'ORM et le moteur de template.

Il est possible d'améliorer notre base avec les idées suivantes :

  • Isoler index.php dans un sous-dossier et empêcher l'utilisateur de remonter dans les dossiers parents, ceci permettant une meilleure sécurité en empêchant à un utilisateur malveillant de pouvoir approcher des autres dossiers.

  • Mettre en place un routeur de façon à gérer les différentes routes vers nos contrôleurs.

  • Rendre les URLs plus "friendly" à l'aide d'URL Rewriting et/ou de PHP.

Je vous encourage également à approcher d'autres ORMs, tels que Eloquent et d'autres moteurs de templates, tels que Smarty.

A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Toeic, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels