Mis à jour le 08/01/2018
  • 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Développement de la bibliothèque

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Le plus gros a été fait : nous savons comment fonctionnera notre application, nous savons où nous voulons aller. Maintenant, il ne reste plus qu'à nous servir de tout cela pour construire les diagrammes UML qui vont lier toutes nos classes, nous permettant ainsi de les écrire plus facilement.

Accrochez-vous à vos claviers, ça ne va pas être de tout repos !

L'application

L'application

Commençons par construire notre classe Application. Souvenez-vous : nous avions dit que cette classe possédait une fonctionnalité, celle de s'exécuter. Or je ne vous ai pas encore parlé des caractéristiques de l'application. La première ne vous vient peut-être pas à l'esprit, mais je l'ai pourtant déjà évoqué : il s'agit du nom de l'application.

Il en existe deux autres. Nous en avons brièvement parlé, et il est fort probable que vous les ayez oubliées : il s'agit de la requête ainsi que la réponse envoyée au client. Ainsi, avant de créer notre classe Application, nous allons nous intéresser à ces deux entités.

La requête du client

Schématisons

Comme vous vous en doutez, nous allons représenter la requête du client au travers d'une instance de classe. Comme pour toute classe, intéressons-nous aux fonctionnalités attendues. Qu'est-ce qui nous intéresse dans la requête du client ? Quelles fonctionnalités seraient intéressantes ? À partir de cette instance, il serait pratique de pouvoir :

  • Obtenir une variable POST.

  • Obtenir une variable GET.

  • Obtenir un cookie.

  • Obtenir la méthode employée pour envoyer la requête (méthode GET ou POST).

  • Obtenir l'URL entrée (utile pour que le routeur connaisse la page souhaitée).

Et pour la route, voici un petit diagramme (j'en ai profité pour ajouter des méthodes permettant de vérifier l'existence de tel cookie, de telle variable GET et de telle variable POST) - voir la figure suivante.

Modélisation de la classe HTTPRequest
Modélisation de la classe HTTPRequest
Codons

Le contenu est assez simple, les méthodes effectuent des opérations basiques. Cette classe, comme toute classe de notre framework, est à écrire dans un fichier situé dans le dossier /lib/OCFram (donc, dans ce cas-là, vous devez créer le fichier /lib/OCFram/HTTPRequest.php). Voici donc le résultat auquel vous étiez censé arriver :

<?php
namespace OCFram;

class HTTPRequest
{
  public function cookieData($key)
  {
    return isset($_COOKIE[$key]) ? $_COOKIE[$key] : null;
  }

  public function cookieExists($key)
  {
    return isset($_COOKIE[$key]);
  }

  public function getData($key)
  {
    return isset($_GET[$key]) ? $_GET[$key] : null;
  }

  public function getExists($key)
  {
    return isset($_GET[$key]);
  }

  public function method()
  {
    return $_SERVER['REQUEST_METHOD'];
  }

  public function postData($key)
  {
    return isset($_POST[$key]) ? $_POST[$key] : null;
  }

  public function postExists($key)
  {
    return isset($_POST[$key]);
  }

  public function requestURI()
  {
    return $_SERVER['REQUEST_URI'];
  }
}

Un petit mot sur la toute première ligne, celle qui contient la déclaration du namespace. J'en avais déjà parlé lors de l'écriture de l'autoload, mais je me permets de faire une petite piqûre de rappel. Toutes les classes de notre projet sont déclarées dans des namespaces. Cela permet d'une part de structurer son projet et, d'autre part, d'écrire un autoload simple qui sait directement, grâce au namespace contenant la classe, le chemin du fichier contenant ladite classe.

Par exemple, si j'ai un contrôleur du module news. Celui-ci sera placé dans le dossier /App/Frontend/Modules/News (si vous avez oublié ce chemin, ne vous inquiétez pas, nous y reviendrons : je l'utilise juste pour l'exemple). La classe représentant ce contrôleur (NewsController) sera donc dans le namespace App\Frontend\Modules\News !

La réponse envoyée au client

Schématisons

Là aussi, nous allons représenter la réponse envoyée au client au travers d'une entité. Cette entité, vous l'aurez compris, n'est autre qu'une instance d'une classe. Quelles fonctionnalités attendons-nous de cette classe ? Que voulons-nous envoyer au visiteur ? La réponse la plus évidente est la page. Nous voulons pouvoir assigner une page à la réponse. Cependant, il est bien beau d'assigner une page, encore faudrait-il pouvoir l'envoyer ! Voici une deuxième fonctionnalité : celle d'envoyer la réponse en générant la page.

Il existe de nombreuses autres fonctionnalités « accessoires ». Pour ma part, je trouvais intéressant de pouvoir rediriger le visiteur vers une erreur 404, lui écrire un cookie et d'ajouter un header spécifique. Pour résumer, notre classe nous permettra :

  • D'assigner une page à la réponse.

  • D'envoyer la réponse en générant la page.

  • De rediriger l'utilisateur.

  • De le rediriger vers une erreur 404.

  • D'ajouter un cookie.

  • D'ajouter un header spécifique.

Et à la figure suivante, le schéma tant attendu !

Modélisation de la classe HTTPResponse
Modélisation de la classe HTTPResponse

Notez la valeur par défaut du dernier paramètre de la méthode setCookie() : elle est à true, alors qu'elle est à false sur la fonction setcookie() de la bibliothèque standard de PHP. Il s'agit d'une sécurité qu'il est toujours préférable d'activer.

Concernant la redirection vers la page 404, laissez-là vide pour l'instant, nous nous chargerons de son implémentation par la suite.

Codons

Voici le code que vous devriez avoir obtenu :

<?php
namespace OCFram;

class HTTPResponse
{
  protected $page;

  public function addHeader($header)
  {
    header($header);
  }

  public function redirect($location)
  {
    header('Location: '.$location);
    exit;
  }

  public function redirect404()
  {
    
  }
  
  public function send()
  {
    // Actuellement, cette ligne a peu de sens dans votre esprit.
    // Promis, vous saurez vraiment ce qu'elle fait d'ici la fin du chapitre
    // (bien que je suis sûr que les noms choisis sont assez explicites !).
    exit($this->page->getGeneratedPage());
  }

  public function setPage(Page $page)
  {
    $this->page = $page;
  }

  // Changement par rapport à la fonction setcookie() : le dernier argument est par défaut à true
  public function setCookie($name, $value = '', $expire = 0, $path = null, $domain = null, $secure = false, $httpOnly = true)
  {
    setcookie($name, $value, $expire, $path, $domain, $secure, $httpOnly);
  }
}

Retour sur notre application

Schématisons

Maintenant que nous avons vu comment sont représentées la requête du client et la réponse que nous allons lui envoyer, nous pouvons réfléchir pleinement à ce qui compose notre classe. Elle possède une fonctionnalité (celle de s'exécuter) et trois caractéristiques : son nom, la requête du client et la réponse que nous allons lui envoyer.

Je vous rappelle que nous construisons une classe Application dont héritera chaque classe représentant une application. Par conséquent, cela n'a aucun sens d'instancier cette classe. Vous savez ce que cela signifie ? Oui, notre classe Application est abstraite ! La représentation graphique de notre classe sera donc telle que vous pouvez la voir sur la figure suivante.

Modélisation de la classe Application
Modélisation de la classe Application
Codons

Le code est très basique. Le constructeur se charge uniquement d'instancier les classes HTTPRequest et HTTPResponse. Quant aux autres méthodes, ce sont des accesseurs, nous avons donc vite fait le tour.

<?php
namespace OCFram;

abstract class Application
{
  protected $httpRequest;
  protected $httpResponse;
  protected $name;
  
  public function __construct()
  {
    $this->httpRequest = new HTTPRequest;
    $this->httpResponse = new HTTPResponse;
    $this->name = '';
  }
  
  abstract public function run();
  
  public function httpRequest()
  {
    return $this->httpRequest;
  }
  
  public function httpResponse()
  {
    return $this->httpResponse;
  }
  
  public function name()
  {
    return $this->name;
  }
}

Dans le constructeur, vous voyez qu'on assigne une valeur nulle à l'attribut name. En fait, chaque application (qui héritera donc de cette classe) sera chargée de spécifier son nom en initialisant cet attribut (par exemple, l'application frontend assignera la valeur Frontend à cet attribut).

Les composants de l'application

Les deux premières classes (comme la plupart des classes que nous allons créer) sont des composants de l'application. Toutes ces classes ont donc une nature en commun et doivent hériter d'une même classe représentant cette nature : j'ai nommé ApplicationComponent.

Que permet de faire cette classe ?

D'obtenir l'application à laquelle l'objet appartient. C'est tout !
Cette classe se chargera juste de stocker, pendant la construction de l'objet, l'instance de l'application exécutée. Nous avons donc une simple classe ressemblant à celle-ci (voir la figure suivante).

Modélisation de la classe ApplicationComponent
Modélisation de la classe ApplicationComponent

Niveau code, je pense qu'on peut difficilement faire plus simple :

<?php
namespace OCFram;

abstract class ApplicationComponent
{
  protected $app;
  
  public function __construct(Application $app)
  {
    $this->app = $app;
  }
  
  public function app()
  {
    return $this->app;
  }
}

Le routeur

Réfléchissons, schématisons

Comme nous l'avons vu, le routeur est l'objet qui va nous permettre de savoir quelle page nous devons exécuter. Pour en être capable, le routeur aura à sa disposition des routes pointant chacune vers un module et une action.

La question que l'on se pose alors est : où seront écrites les routes ? Certains d'entre vous seraient tentés de les écrire directement à l'intérieur de la classe et faire une sorte de switch / case sur les routes pour trouver laquelle correspond à l'URL. Cette façon de faire présente un énorme inconvénient : votre classe représentant le routeur sera dépendante du projet que vous développez. Par conséquent, vous ne pourrez plus l'utiliser sur un autre site ! Il va donc falloir externaliser ces définitions de routes.

Comment pourrions-nous faire alors ? Puisque je vous ai dit que nous n'allons pas toucher à la classe pour chaque nouvelle route à ajouter, nous allons placer ces routes dans un autre fichier. Ce fichier doit être placé dans le dossier de l'application concernée, et puisque ça touche à la configuration de celle-ci, nous le placerons dans un sous-dossier Config. Il y a aussi un détail à régler : dans quel format allons-nous écrire le fichier ? Je vous propose le format XML car ce langage est intuitif et simple à parser, notamment grâce à la bibliothèque native DOMDocument de PHP. Si ce format ne vous plaît pas, vous êtes libres d'en choisir un autre, cela n'a pas d'importance, le but étant que vous ayez un fichier que vous arrivez à parser. Le chemin complet vers ce fichier devient donc /App/Nomdelapplication/Config/routes.xml.

Comme pour tout fichier XML qui se respecte, celui-ci doit suivre une structure précise. Essayez de deviner la fonctionnalité de la ligne 3 :

<?xml version="1.0" encoding="utf-8" ?>
<routes>
  <route url="/news.html" module="News" action="index" ></route>
</routes>

Alors, avez-vous une idée du rôle de la troisième ligne ? Lorsque nous allons aller sur la page news.html, le routeur dira donc à l'application : « le client veut accéder au module News et exécuter l'action index ».

Un autre problème se pose. Par exemple, si je veux afficher une news spécifique en fonction de son identifiant, comment faire ? Ou, plus généralement, comment passer des variables GET ? L'idéal serait d'utiliser des expressions régulières (ou regex) en guise d'URL. Chaque paire de parenthèses représentera une variable GET. Nous spécifierons leur nom dans un quatrième attribut vars.

Comme un exemple vaut mieux qu'un long discours, voyons donc un cas concret :

<route url="/news-(.+)-([0-9]+)\.html" module="News" action="show" vars="slug,id" />

Ainsi, toute URL vérifiant cette expression pointera vers le module News et exécutera l'action show. Les variables $_GET['slug'] et $_GET['id'] seront créées et auront pour valeur le contenu des parenthèses capturantes.

On sait désormais que notre routeur a besoin de routes pour nous renvoyer celle qui correspond à l'URL. Cependant, s'il a besoin de routes, il va falloir les lui donner !

Pourquoi ne peut-il pas aller chercher lui-même les routes ?

S'il allait les chercher lui-même, notre classe serait dépendante de l'architecture de l'application. Si vous voulez utiliser votre classe dans un projet complètement différent, vous ne pourrez pas, car le fichier contenant les routes (/App/Nomdelapplication/Config/routes.xml) n'existera tout simplement pas. De plus, dans ce projet, les routes ne seront peut-être pas stockées dans un fichier XML, donc le parsage ne se fera pas de la même façon. Or, je vous l'avais déjà dit dans les premiers chapitres, mais l'un des points forts de la POO est son caractère réutilisable. Ainsi, votre classe représentant le routeur ne dépendra ni d'une architecture, ni du format du fichier stockant les routes.

De cette façon, notre classe présente déjà deux fonctionnalités :

  • Celle d'ajouter une route à sa liste de routes.

  • Celle de renvoyer la route correspondant à l'URL.

Avec, bien entendu, une caractéristique : la liste des routes attachée au routeur. Cependant, une autre question se pose : nous disons qu'on « passe une route » au routeur. Mais comment est matérialisée une route ? Puisque vous pensez orienté objet, vous devriez automatiquement me dire « une route, c'est un objet ! ».

Intéressons-nous donc maintenant à notre objet représentant une route. Cet objet, qu'est-ce qui le caractérise ?

  • Une URL.

  • Un module.

  • Une action.

  • Un tableau comportant les noms des variables.

  • Un tableau clé/valeur comportant les noms/valeurs des variables.

Quelle différence entre les deux dernières caractéristiques ?

En fait, lorsque nous créerons les routes, nous allons assigner les quatre premières caractéristiques (souvenez-vous du fichier XML : nous définissons une URL, un module, une action, et la liste des noms des variables). C'est donc cette dernière liste de variables que nous allons assigner à notre route. Ensuite, notre routeur ira parcourir ces routes et c'est lui qui assignera les valeurs des variables. C'est donc à ce moment-là que le tableau comportant les noms/valeurs des variables sera créé et assigné à l'attribut correspondant.

Nous pouvons maintenant dresser la liste des fonctionnalités de notre objet représentant une route :

  • Celle de savoir si la route correspond à l'URL.

  • Celle de savoir si la route possède des variables (utile, nous le verrons, dans notre routeur).

Pour résumer, voici le diagramme UML représentant nos classes (voir la figure suivante).

Modélisation des classes Router et Route
Modélisation des classes Router et Route

Codons

Commençons par nos deux classes Router et Route :

<?php
namespace OCFram;

class Router
{
  protected $routes = [];

  const NO_ROUTE = 1;

  public function addRoute(Route $route)
  {
    if (!in_array($route, $this->routes))
    {
      $this->routes[] = $route;
    }
  }

  public function getRoute($url)
  {
    foreach ($this->routes as $route)
    {
      // Si la route correspond à l'URL
      if (($varsValues = $route->match($url)) !== false)
      {
        // Si elle a des variables
        if ($route->hasVars())
        {
          $varsNames = $route->varsNames();
          $listVars = [];

          // On crée un nouveau tableau clé/valeur
          // (clé = nom de la variable, valeur = sa valeur)
          foreach ($varsValues as $key => $match)
          {
            // La première valeur contient entièrement la chaine capturée (voir la doc sur preg_match)
            if ($key !== 0)
            {
              $listVars[$varsNames[$key - 1]] = $match;
            }
          }

          // On assigne ce tableau de variables � la route
          $route->setVars($listVars);
        }

        return $route;
      }
    }

    throw new \RuntimeException('Aucune route ne correspond à l\'URL', self::NO_ROUTE);
  }
}
<?php
namespace OCFram;

class Route
{
  protected $action;
  protected $module;
  protected $url;
  protected $varsNames;
  protected $vars = [];

  public function __construct($url, $module, $action, array $varsNames)
  {
    $this->setUrl($url);
    $this->setModule($module);
    $this->setAction($action);
    $this->setVarsNames($varsNames);
  }

  public function hasVars()
  {
    return !empty($this->varsNames);
  }

  public function match($url)
  {
    if (preg_match('`^'.$this->url.'$`', $url, $matches))
    {
      return $matches;
    }
    else
    {
      return false;
    }
  }

  public function setAction($action)
  {
    if (is_string($action))
    {
      $this->action = $action;
    }
  }

  public function setModule($module)
  {
    if (is_string($module))
    {
      $this->module = $module;
    }
  }

  public function setUrl($url)
  {
    if (is_string($url))
    {
      $this->url = $url;
    }
  }

  public function setVarsNames(array $varsNames)
  {
    $this->varsNames = $varsNames;
  }

  public function setVars(array $vars)
  {
    $this->vars = $vars;
  }

  public function action()
  {
    return $this->action;
  }

  public function module()
  {
    return $this->module;
  }

  public function vars()
  {
    return $this->vars;
  }

  public function varsNames()
  {
    return $this->varsNames;
  }
}

Tout cela est bien beau, mais il serait tout de même intéressant d'exploiter notre routeur afin de l'intégrer dans notre application. Pour cela, nous allons implémenter une méthode dans notre classe Application qui sera chargée de nous donner le contrôleur correspondant à l'URL. Pour cela, cette méthode va parcourir le fichier XML pour ajouter les routes au routeur. Ensuite, elle va récupérer la route correspondante à l'URL (si une exception a été levée, on lèvera une erreur 404). Enfin, la méthode instanciera le contrôleur correspondant à la route et le renverra (il est possible que vous ne sachiez pas comment faire car nous n'avons pas encore vu le contrôleur en détail, donc laissez ça de côté et regardez la correction, vous comprendrez plus tard).

Voici notre nouvelle classe Application :

<?php
namespace OCFram;

abstract class Application
{
  protected $httpRequest;
  protected $httpResponse;
  protected $name;

  public function __construct()
  {
    $this->httpRequest = new HTTPRequest($this);
    $this->httpResponse = new HTTPResponse($this);

    $this->name = '';
  }

  public function getController()
  {
    $router = new Router;

    $xml = new \DOMDocument;
    $xml->load(__DIR__.'/../../App/'.$this->name.'/Config/routes.xml');

    $routes = $xml->getElementsByTagName('route');

    // On parcourt les routes du fichier XML.
    foreach ($routes as $route)
    {
      $vars = [];

      // On regarde si des variables sont présentes dans l'URL.
      if ($route->hasAttribute('vars'))
      {
        $vars = explode(',', $route->getAttribute('vars'));
      }

      // On ajoute la route au routeur.
      $router->addRoute(new Route($route->getAttribute('url'), $route->getAttribute('module'), $route->getAttribute('action'), $vars));
    }

    try
    {
      // On récupère la route correspondante à l'URL.
      $matchedRoute = $router->getRoute($this->httpRequest->requestURI());
    }
    catch (\RuntimeException $e)
    {
      if ($e->getCode() == Router::NO_ROUTE)
      {
        // Si aucune route ne correspond, c'est que la page demandée n'existe pas.
        $this->httpResponse->redirect404();
      }
    }

    // On ajoute les variables de l'URL au tableau $_GET.
    $_GET = array_merge($_GET, $matchedRoute->vars());

    // On instancie le contrôleur.
    $controllerClass = 'App\\'.$this->name.'\\Modules\\'.$matchedRoute->module().'\\'.$matchedRoute->module().'Controller';
    return new $controllerClass($this, $matchedRoute->module(), $matchedRoute->action());
  }

  abstract public function run();

  public function httpRequest()
  {
    return $this->httpRequest;
  }

  public function httpResponse()
  {
    return $this->httpResponse;
  }

  public function name()
  {
    return $this->name;
  }
}

Le back controller

Nous venons de construire notre routeur qui donne à l'application le contrôleur associé. Afin de suivre la logique du déroulement de l'application, construisons maintenant notre back controller de base.

Réfléchissons, schématisons

Remémorons-nous ce que permet de faire un objet BackController. Nous avions vu qu'un tel objet n'offrait qu'une seule fonctionnalité : celle d'exécuter une action. Comme pour l'objet Application, je ne vous avais pas parlé des caractéristiques d'un back controller. Qu'est-ce qui caractérise un back controller ? Si vous connaissez l'architecture MVC, vous savez qu'une vue est associée au back controller : c'est donc l'une de ses caractéristiques.

Maintenant, pensons à la nature d'un back controller. Celui-ci est propre à un module, et si on l'a instancié c'est que nous voulons qu'il exécute une action. Cela fait donc deux autres caractéristiques : le module et l'action.

Enfin, il y en a une dernière que vous ne pouvez pas deviner : la page associée au contrôleur. Comme nous le verrons bientôt, c'est à travers cette instance représentant la page envoyée par la suite au visiteur que le contrôleur transmettra des données à la vue. Pour l'instant, mémorisez juste l'idée que le contrôleur est associé à une page stockée en tant qu'instance dans un attribut de la classe BackController.

Une instance de BackController nous permettra donc :

  • D'exécuter une action (donc une méthode).

  • D'obtenir la page associée au contrôleur.

  • De modifier le module, l'action et la vue associés au contrôleur.

Cette classe est une classe de base dont héritera chaque contrôleur. Par conséquent, elle se doit d'être abstraite. Aussi, il s'agit d'un composant de l'application, donc un lien de parenté avec ApplicationComponent est à créer. Nous arrivons donc à une classe ressemblant à ça (voir la figure suivante).

Modélisation de la classe Backcontroller
Modélisation de la classe BackController

Notre constructeur se chargera dans un premier temps d'appeler le constructeur de son parent. Dans un second temps, il créera une instance de la classe Page qu'il stockera dans l'attribut correspondant. Enfin, il assignera les valeurs au module, à l'action et à la vue (par défaut la vue a la même valeur que l'action).

Concernant la méthode execute(), comment fonctionnera-t-elle ? Son rôle est d'invoquer la méthode correspondant à l'action assignée à notre objet. Le nom de la méthode suit une logique qui est de se nommer executeNomdelaction(). Par exemple, si nous avons une action show sur notre module, nous devrons implémenter la méthode executeShow() dans notre contrôleur. Aussi, pour une question de simplicité, nous passerons la requête du client à la méthode. En effet, dans la plupart des cas, les méthodes auront besoin de la requête du client pour obtenir une donnée (que ce soit une variable GET, POST, ou un cookie).

Codons

Voici le résultat qui était à obtenir :

<?php
namespace OCFram;

abstract class BackController extends ApplicationComponent
{
  protected $action = '';
  protected $module = '';
  protected $page = null;
  protected $view = '';

  public function __construct(Application $app, $module, $action)
  {
    parent::__construct($app);

    $this->page = new Page($app);

    $this->setModule($module);
    $this->setAction($action);
    $this->setView($action);
  }

  public function execute()
  {
    $method = 'execute'.ucfirst($this->action);

    if (!is_callable([$this, $method]))
    {
      throw new \RuntimeException('L\'action "'.$this->action.'" n\'est pas définie sur ce module');
    }

    $this->$method($this->app->httpRequest());
  }

  public function page()
  {
    return $this->page;
  }

  public function setModule($module)
  {
    if (!is_string($module) || empty($module))
    {
      throw new \InvalidArgumentException('Le module doit être une chaine de caractères valide');
    }

    $this->module = $module;
  }

  public function setAction($action)
  {
    if (!is_string($action) || empty($action))
    {
      throw new \InvalidArgumentException('L\'action doit être une chaine de caractères valide');
    }

    $this->action = $action;
  }

  public function setView($view)
  {
    if (!is_string($view) || empty($view))
    {
      throw new \InvalidArgumentException('La vue doit être une chaine de caractères valide');
    }

    $this->view = $view;
  }
}

Accéder aux managers depuis le contrôleur

Un petit souci se pose : comment le contrôleur accèdera aux managers ? On pourrait les instancier directement dans la méthode, mais les managers exigent le DAO lors de la construction de l'objet et ce DAO n'est pas accessible depuis le contrôleur. Nous allons donc créer une classe qui gérera les managers : j'ai nommé Managers. Nous instancierons donc cette classe au sein de notre contrôleur en lui passant le DAO. Les méthodes filles auront accès à cet objet et pourront accéder aux managers facilement.

Petit rappel sur la structure d'un manager

Je vais faire un bref rappel concernant la structure des managers. Un manager, comme nous l'avons vu durant le TP des news, est divisé en deux parties. La première partie est une classe abstraite listant toutes les méthodes que le manager doit implémenter. La seconde partie est constituée des classes qui vont implémenter ces méthodes, spécifiques à chaque DAO. Pour reprendre l'exemple des news, la première partie était constituée de la classe abstraite NewsManager et la seconde partie de NewsManagerPDO et NewsManagerMySQLi.

En plus du DAO, il faudra donc spécifier à notre classe gérant ces managers l'API que l'on souhaite utiliser. Suivant ce qu'on lui demande, notre classe nous retournera une instance de NewsManagerPDO ou NewsManagerMySQLi par exemple.

La classe Managers

Schématiquement, voici à quoi ressemble la classe Managers (voir la figure suivante).

Modélisation de la classe Managers
Modélisation de la classe Managers

Cette instance de Managers sera stockée dans un attribut de l'objet BackController comme $managers par exemple. L'attribution d'une instance de Managers à cet attribut se fait dans le constructeur de la manière suivante :

<?php
namespace OCFram;

abstract class BackController extends ApplicationComponent
{
  // ...
  protected $managers = null;
  
  public function __construct(Application $app, $module, $action)
  {
    parent::__construct($app);
    
    $this->managers = new Managers('PDO', PDOFactory::getMysqlConnexion());
    $this->page = new Page($app);
    
    $this->setModule($module);
    $this->setAction($action);
    $this->setView($action);
  }
  
  // ...
}

Niveau code, voici à quoi ressemble la classe Managers :

<?php
namespace OCFram;

class Managers
{
  protected $api = null;
  protected $dao = null;
  protected $managers = [];

  public function __construct($api, $dao)
  {
    $this->api = $api;
    $this->dao = $dao;
  }

  public function getManagerOf($module)
  {
    if (!is_string($module) || empty($module))
    {
      throw new \InvalidArgumentException('Le module spécifié est invalide');
    }

    if (!isset($this->managers[$module]))
    {
      $manager = '\\Model\\'.$module.'Manager'.$this->api;

      $this->managers[$module] = new $manager($this->dao);
    }

    return $this->managers[$module];
  }
}

Et maintenant, comment je passe l'instance de PDO au constructeur de Managers ? Je l'instancie directement ?

Non car cela vous obligerait à modifier la classe BackController à chaque modification, ce qui n'est pas très flexible. Je vous conseille plutôt d'utiliser le pattern factory que nous avons vu durant la précédente partie avec la classe PDOFactory :

<?php
namespace OCFram;

class PDOFactory
{
  public static function getMysqlConnexion()
  {
    $db = new \PDO('mysql:host=localhost;dbname=news', 'root', '');
    $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    
    return $db;
  }
}

À propos des managers

Nous trouvons ici des natures en commun : les managers sont tous des managers ! Et les entités, et bien ce sont toutes des entités !

Cela peut vous sembler bête (oui, ça l'est), mais si je vous dis ceci c'est pour mettre en évidence le lien de parenté qui saute forcément aux yeux après cette phrase. Tous les managers devront donc hériter de Manager et chaque entité de Entity. La classe Manager se chargera d'implémenter un constructeur qui demandera le DAO par le biais d'un paramètre, comme ceci :

<?php
namespace OCFram;

abstract class Manager
{
  protected $dao;
  
  public function __construct($dao)
  {
    $this->dao = $dao;
  }
}

Par contre, la classe Entity est légèrement plus complexe. En effet, celle-ci offre quelques fonctionnalités :

  • Implémentation d'un constructeur qui hydratera l'objet si un tableau de valeurs lui est fourni.

  • Implémentation d'une méthode qui permet de vérifier si l'enregistrement est nouveau ou pas. Pour cela, on vérifie si l'attribut $id est vide ou non (ce qui inclut le fait que toutes les tables devront posséder un champ nommé id).

  • Implémentation des getters / setters.

  • Implémentation de l'interface ArrayAccess (ce n'est pas obligatoire, c'est juste que je préfère utiliser l'objet comme un tableau dans les vues).

Le code obtenu devrait s'apparenter à celui-ci :

<?php
namespace OCFram;

abstract class Entity implements \ArrayAccess
{
  protected $erreurs = [],
            $id;

  public function __construct(array $donnees = [])
  {
    if (!empty($donnees))
    {
      $this->hydrate($donnees);
    }
  }

  public function isNew()
  {
    return empty($this->id);
  }

  public function erreurs()
  {
    return $this->erreurs;
  }

  public function id()
  {
    return $this->id;
  }

  public function setId($id)
  {
    $this->id = (int) $id;
  }

  public function hydrate(array $donnees)
  {
    foreach ($donnees as $attribut => $valeur)
    {
      $methode = 'set'.ucfirst($attribut);

      if (is_callable([$this, $methode]))
      {
        $this->$methode($valeur);
      }
    }
  }

  public function offsetGet($var)
  {
    if (isset($this->$var) && is_callable([$this, $var]))
    {
      return $this->$var();
    }
  }

  public function offsetSet($var, $value)
  {
    $method = 'set'.ucfirst($var);

    if (isset($this->$var) && is_callable([$this, $method]))
    {
      $this->$method($value);
    }
  }

  public function offsetExists($var)
  {
    return isset($this->$var) && is_callable([$this, $var]);
  }

  public function offsetUnset($var)
  {
    throw new \Exception('Impossible de supprimer une quelconque valeur');
  }
}

La page

Toujours dans la continuité du déroulement de l'application, nous allons nous intéresser maintenant à la page qui, nous venons de le voir, était attachée à notre contrôleur.

Réfléchissons, schématisons

Commençons par nous intéresser aux fonctionnalités de notre classe Page. Vous le savez, une page est composée de la vue et du layout afin de générer le tout. Ainsi, nous avons une première fonctionnalité : celle de générer une page. De plus, le contrôleur doit pouvoir transmettre des variables à la vue, stockées dans cette page : ajouter une variable à la page est donc une autre fonctionnalité. Enfin, la page doit savoir quelle vue elle doit générer pour l'ajouter ensuite au layout : il est donc possible d'assigner une vue à la page (voir la figure suivante).

Composition d'une page
Composition d'une page

Pour résumer, une instance de notre classe Page nous permet :

  • D'ajouter une variable à la page (le contrôleur aura besoin de passer des données à la vue).

  • D'assigner une vue à la page.

  • De générer la page avec le layout de l'application.

Avant de commencer à coder cette classe, voici le diagramme la représentant (voir la figure suivante).

Modélisation de la classe Page
Modélisation de la classe Page

Codons

La classe est, me semble-t-il, plutôt facile à écrire. Cependant, il se peut que vous vous demandiez comment écrire la méthode getGeneratedPage(). En fait, il faut inclure les pages pour générer leur contenu, et stocker ce contenu dans une variable grâce aux fonctions de tamporisation de sortie pour pouvoir s'en servir plus tard. Pour la transformation du tableau stocké dans l'attribut $vars en variables, regardez du côté de la fonction extract.

<?php
namespace OCFram;

class Page extends ApplicationComponent
{
  protected $contentFile;
  protected $vars = [];

  public function addVar($var, $value)
  {
    if (!is_string($var) || is_numeric($var) || empty($var))
    {
      throw new \InvalidArgumentException('Le nom de la variable doit être une chaine de caractères non nulle');
    }

    $this->vars[$var] = $value;
  }

  public function getGeneratedPage()
  {
    if (!file_exists($this->contentFile))
    {
      throw new \RuntimeException('La vue spécifiée n\'existe pas');
    }

    extract($this->vars);

    ob_start();
      require $this->contentFile;
    $content = ob_get_clean();

    ob_start();
      require __DIR__.'/../../App/'.$this->app->name().'/Templates/layout.php';
    return ob_get_clean();
  }

  public function setContentFile($contentFile)
  {
    if (!is_string($contentFile) || empty($contentFile))
    {
      throw new \InvalidArgumentException('La vue spécifiée est invalide');
    }

    $this->contentFile = $contentFile;
  }
}

Retour sur la classe BackController

Maintenant que nous avons écrit notre classe Page, je peux vous faire écrire une instruction dans la classe BackController et, plus particulièrement, la méthode setView($view). En effet, lorsque l'on change de vue, il faut en informer la page concernée grâce à la méthode setContentFile() de notre classe Page :

<?php
namespace OCFram;

abstract class BackController extends ApplicationComponent
{
  // ...
  
  public function setView($view)
  {
    if (!is_string($view) || empty($view))
    {
      throw new \InvalidArgumentException('La vue doit être une chaine de caractères valide');
    }
    
    $this->view = $view;
    
    $this->page->setContentFile(__DIR__.'/../../App/'.$this->app->name().'/Modules/'.$this->module.'/Views/'.$this->view.'.php');
  }
}

Retour sur la méthode HTTPResponse::redirect404()

Étant donné que vous avez compris comment fonctionne un objet Page, vous êtes capables d'écrire cette méthode laissée vide jusqu'à présent. Comment procéder ?

  • On commence d'abord par créer une instance de la classe Page que l'on stocke dans l'attribut correspondant.

  • On assigne ensuite à la page le fichier qui fait office de vue à générer. Ce fichier contient le message d'erreur formaté. Vous pouvez placer tous ces fichiers dans le dossier /Errors par exemple, sous le nom code.html. Le chemin menant au fichier contenant l'erreur 404 sera donc /Errors/404.html.

  • On ajoute un header disant que le document est non trouvé (HTTP/1.0 404 Not Found).

  • On envoie la réponse.

Et voici ce que l'on obtient :

<?php
namespace OCFram;

class HTTPResponse extends ApplicationComponent
{
  // ...
  
  public function redirect404()
  {
    $this->page = new Page($this->app);
    $this->page->setContentFile(__DIR__.'/../../Errors/404.html');
    
    $this->addHeader('HTTP/1.0 404 Not Found');
    
    $this->send();
  }
  
  // ...
}

Bonus : l'utilisateur

Cette classe est un « bonus », c'est-à-dire qu'elle n'est pas indispensable à l'application. Cependant, nous allons nous en servir plus tard donc ne sautez pas cette partie ! Mais rassurez-vous, nous aurons vite fait de la créer.

Réfléchissons, schématisons

L'utilisateur, qu'est-ce que c'est ? L'utilisateur est celui qui visite votre site. Comme tout site web qui se respecte, nous avons besoin d'enregistrer temporairement l'utilisateur dans la mémoire du serveur afin de stocker des informations le concernant. Nous créons donc une session pour l'utilisateur. Vous connaissez sans doute ce système de sessions avec le tableau $_SESSION et les fonctions à ce sujet que propose l'API. Notre classe, que nous nommerons User, devra nous permettre de gérer facilement la session de l'utilisateur. Nous pourrons donc, par le biais d'un objet User :

  • Assigner un attribut à l'utilisateur.

  • Obtenir la valeur d'un attribut.

  • Authentifier l'utilisateur (cela nous sera utile lorsque nous ferons un formulaire de connexion pour l'espace d'administration).

  • Savoir si l'utilisateur est authentifié.

  • Assigner un message informatif à l'utilisateur que l'on affichera sur la page.

  • Savoir si l'utilisateur a un tel message.

  • Et enfin, récupérer ce message.

Cela donne naissance à une classe de ce genre (voir la figure suivante).

Modélisation de la classe User
Modélisation de la classe User

Codons

Avant de commencer à coder la classe, il faut que vous ajoutiez l'instruction invoquant session_start() au début du fichier, en dehors de la classe. Ainsi, dès l'inclusion du fichier par l'autoload, la session démarrera et l'objet créé sera fonctionnel.

Ceci étant, voici le code que je vous propose :

<?php
namespace OCFram;

session_start();

class User
{
  public function getAttribute($attr)
  {
    return isset($_SESSION[$attr]) ? $_SESSION[$attr] : null;
  }

  public function getFlash()
  {
    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);

    return $flash;
  }

  public function hasFlash()
  {
    return isset($_SESSION['flash']);
  }

  public function isAuthenticated()
  {
    return isset($_SESSION['auth']) && $_SESSION['auth'] === true;
  }

  public function setAttribute($attr, $value)
  {
    $_SESSION[$attr] = $value;
  }

  public function setAuthenticated($authenticated = true)
  {
    if (!is_bool($authenticated))
    {
      throw new \InvalidArgumentException('La valeur spécifiée à la méthode User::setAuthenticated() doit être un boolean');
    }

    $_SESSION['auth'] = $authenticated;
  }

  public function setFlash($value)
  {
    $_SESSION['flash'] = $value;
  }
}

Comme promis, ce fut court, et tout ce qui compose cette classe est, il me semble, facilement compréhensible.

Bonus 2 : la configuration

Cette classe est également un bonus dans la mesure où elle n'est pas essentielle pour que l'application fonctionne. Je vous encourage à vous entraîner à créer cette classe : tout comme la classe User, celle-ci n'est pas compliquée (si tant est que vous sachiez parser du XML avec une bibliothèque telle que DOMDocument).

Réfléchissons, schématisons

Tout site web bien conçu se doit d'être configurable à souhait. Par conséquent, il faut que chaque application possède un fichier de configuration déclarant des paramètres propres à ladite application. Par exemple, si nous voulons afficher un nombre de news précis sur l'accueil, il serait préférable de spécifier un paramètre nombre_de_news à l'application que nous mettrons par exemple à 5 plutôt que d'insérer ce nombre en dur dans le code. De cette façon, nous aurons à modifier uniquement ce nombre dans le fichier de configuration pour faire varier le nombre de news sur la page d'accueil.

Un format pour le fichier

Le format du fichier sera le même que le fichier contenant les routes, à savoir le format XML. La base du fichier sera celle-ci :

<?xml version="1.0" encoding="utf-8" ?>
<definitions>
</definitions>

Chaque paramètre se déclarera avec une balise define comme ceci :

<define var="nombre_news" value="5" />
Emplacement du fichier

Le fichier de configuration est propre à chaque application. Par conséquent, il devra être placé aux côtés du fichier routes.xml sous le doux nom de app.xml. Son chemin complet sera donc /App/Nomdelapplication/Config/app.xml.

Fonctionnement de la classe

Nous aurons donc une classe s'occupant de gérer la configuration. Pour faire simple, nous n'allons lui implémenter qu'une seule fonctionnalité : celle de récupérer un paramètre. Il faut également garder à l'esprit qu'il s'agit d'un composant de l'application, donc il faut un lien de parenté avec... Je suis sûr que vous savez !

La méthode get($var) (qui sera chargée de récupérer la valeur d'un paramètre) ne devra pas parcourir à chaque fois le fichier de configuration, cela serait bien trop lourd. S'il s'agit du premier appel de la méthode, il faudra ouvrir le fichier XML en instanciant la classe DOMDocument et stocker tous les paramètres dans un attribut (admettons $vars). Ainsi, à chaque fois que la méthode get() sera invoquée, nous n'aurons qu'à retourner le paramètre précédemment enregistré.

Notre classe, plutôt simple, ressemble donc à ceci (voir la figure suivante).

Modélisation de la classe Config
Modélisation de la classe Config

Codons

Voici le résultat qui vous deviez obtenir :

<?php
namespace OCFram;

class Config extends ApplicationComponent
{
  protected $vars = [];

  public function get($var)
  {
    if (!$this->vars)
    {
      $xml = new \DOMDocument;
      $xml->load(__DIR__.'/../../App/'.$this->app->name().'/Config/app.xml');

      $elements = $xml->getElementsByTagName('define');

      foreach ($elements as $element)
      {
        $this->vars[$element->getAttribute('var')] = $element->getAttribute('value');
      }
    }

    if (isset($this->vars[$var]))
    {
      return $this->vars[$var];
    }

    return null;
  }
}

Notre bibliothèque est écrite, voilà une bonne chose de faite ! Il ne reste plus qu'à écrire les applications et à développer les modules.

Cependant, avant de continuer, je vais m'assurer que vous me suiviez toujours. Voici l'arborescence de l'architecture, avec des explications sur chaque dossier (voir la figure suivante).

Résumé de l'arborescence du projet
Résumé de l'arborescence du projet

Il est possible que des dossiers vous paraissent sortir de nulle part. Cependant, je vous assure que j'en ai parlé au moins une fois lors de la création de certaines classes. N'hésitez surtout pas à relire le chapitre en vous appuyant sur cette arborescence, vous comprendrez sans doute mieux. ;)

Exemple de certificat de réussite
Exemple de certificat de réussite