j'espère que vous avez bien assimilé la théorie du développement selon l'approche MVC Modèle - Vue - Contrôleur de mon précédent billet parce qu'aujourd'hui on va se lancer dans le grand bain avec la mise en application de ce concept.
Pour ceux qui auraient besoin de se rafraîchir la mémoire, c'est par là que ça se passe.
Pour corser le tout, je vais privilégier une approche objet de ce paradigme.
Je vais essayer de vous exposer simplement les fondements de la Programmation Orientée Objet (POO pour les intimes) et qui sait, en convertir un ou deux
Enfin, il faut aussi avoir assimilé au préalable la théorie et le fonctionnement des espaces de nom (namespace) et de l'autochargement des classes (autoloading), vous trouverez tout ce qu'il faut sur cet autre billet rédigé aussi par mes soins.
INTRODUCTION AUX BASES DE LA PROGRAMMATION ORIENTÉE OBJET
On va prendre un cas hyper simple qui va parler à tout développeur web : un échange entre un navigateur et un serveur web.
L’utilité principale de la programmation objet réside dans la possibilité de représenter des éléments tangibles sous forme de concepts abstraits (équivalent à une représentation purement informatique).
Dans notre cas de figure, le serveur web devra envoyer une réponse (à ce stade on se pas encore laquelle, mais il doit envoyer une réponse), donc pour faciliter le traitement du côté du serveur web, on va modéliser une représentation abstraite de la réponse. C’est cette représentation qui va être manipulée par le programme. Autrement dit on va créer une class Response qui va être l’alter ego abstrait de la réponse physique du serveur web.
Ainsi quand le développeur manipulera une instance de Response, il saura immédiatement qu’il manipule la réponse finale du serveur qui va être envoyée au navigateur à la toute fin du traitement.
Il va de soi que c’est exactement pareil pour une requête. Pour faciliter sa manipulation, elle aura une représentation abstraite dans le monde informatique qui sera l’alter ego de la requête physique reçue par le serveur. Nous aurons donc une autre classe, class Request en charge de tout ce qui se rapporte à une requête web.
Plus généralement, il faut bien comprendre que le passage au monde objet correspond dans un premier temps à une modélisation d'une problématique (ou d'une réalité) sous forme de concepts abstraits.
Cette abstraction va permettre à un développeur de savoir précisément ce qu'il manipule dans son monde dématérialisé. Généralement, le code devient plus parlant au premier coup d’œil.
La POO offre énormément en terme de fonctionnalités. Pour vous en convaincre, prenez n'importe quel livre consacré à la théorie de ce paradigme et vous verrez que d'une part il est généralement gros et d'autre part que vous allez y consacrer un certain temps d'apprentissage pour vous familiariser avec le contenu. Dans un second temps, l'expérience finira par vous convaincre de que c'est, somme toute, "évident" .
Pour couvrir la POO, il faudrait bien plus que ce billet, je vais rester succinct, juste ce qu'il faut pour vous faire saliver.
Il faut reconnaître qu'il faut un peu de doigté pour modéliser correctement en POO. Le travers c'est qu'un débutant à tendance à créer des classes pour tout et n'importe quoi et cela finit immanquablement par une jolie noyade.
Revenons à nos moutons ; comme la réalité diffère, il va de soi que les classes dans le monde informatique vont avoir leurs spécificités.
Par exemple, le traitement d'une réponse est totalement différent d'une requête.
Pour une réponse, on va avoir besoin au minimum de :
- connaître à l'avance les en-têtes à envoyer au navigateur en fonction du type de données à transmettre
- avoir des données à envoyer
Pour une requête, on va avoir besoin au minimum de :
- d'avoir l'URL d'appel morcelée en composants selon la norme en vigueur RFC3986
Donc, selon cette analyse, on va pouvoir créer 2 classes qui vont se charger de répondre aux besoins :
Classe Response :
Code php : | Sélectionner tout |
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | <?php declare(strict_types=1); namespace rawsrc; /** * TUTORIAL DVP SUR LE CONCEPT MVC : MODÈLE-VUE-CONTRÔLEUR * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7786/developpement-selon-l-approche-mvc-modele-vue-controleur-retour-theorie/ * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7804/developpement-selon-lapproche-mvc-modele-vue-controleur-cas-pratique/ * * Classe en charge de la gestion d'une réponse générique * Pour une réponse valide, le serveur doit envoyer d'abord les en-têtes et ensuite la réponse proprement dite * * Cette classe est instanciable, car il est tout à fait possible de créer une réponse à la volée selon ses besoins * par exemple : renvoyer du xml, pdf... Il faudra juste adapter les en-têtes ($headers) et le corps de la réponse ($data) */ class Response { /** * @var array */ public $headers = []; /** * Stocke les données relatives à la réponse * * @var mixed */ public $data = null; /** * @param mixed $data * @param array $headers Array of headers to send first */ public function __construct($data = null, array $headers = []) { $this->data = $data; $this->headers = $headers; } /** * Envoi des données de la réponse */ public function send() { // envoi des en-têtes (le type des données qui vont suivre) foreach ($this->headers as $h) { header($h); } // envoi des données (le navigateur sait à quoi s'attendre) if ($this->data !== null) { echo $this->data; } } } |
Si vous prenez le temps de lire la norme RFC, vous verrez que la classe ne fait que reprendre d'une manière simplifiée tous les composants d'une URL
Code php : | Sélectionner tout |
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | <?php declare(strict_types=1); namespace rawsrc; /** * TUTORIAL DVP SUR LE CONCEPT MVC : MODÈLE-VUE-CONTRÔLEUR * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7786/developpement-selon-l-approche-mvc-modele-vue-controleur-retour-theorie/ * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7804/developpement-selon-lapproche-mvc-modele-vue-controleur-cas-pratique/ * * Classe en charge de la gestion d'une requête générique * Anlalyse complète de l'URL */ class Request { // composants d'une URL /** * @var string */ private $scheme = ''; /** * @var string */ private $user = ''; /** * @var string */ private $pwd = ''; /** * @var string */ private $host = ''; /** * @var string */ private $port = ''; /** * @var array */ private $path = []; /** * @var array */ private $query = []; /** * @var string */ private $fragment = ''; /** * @var bool */ private $is_ajax = false; /** * @var bool */ private $is_valid = false; /** * @param string $url */ public function __construct(string $url) { $this->parse($url); } /** * @return string */ public function scheme(): string { return $this->scheme; } /** * @return string */ public function user(): string { return $this->user; } /** * @return string */ public function host(): string { return $this->host; } /** * @return array */ public function path(): array { return $this->path; } /** * @return string */ public function port(): string { return $this->port; } /** * @return array */ public function query(): array { return $this->query; } /** * @return string */ public function fragment(): string { return $this->fragment; } /** * @return bool */ public function isAjax(): bool { return $this->is_ajax; } /** * @return bool */ public function isValid(): bool { return $this->is_valid; } /** * PARSEUR d'url * Sépare l'URL en composants selon la norme RFC3986 */ private function parse(string $url) { // ici la fonction décompose l'url en composants // quand vous manipulez une instance de la classe Request // vous ne savez pas comment ce travail est fait => on va parler d'IMPLÉMENTATION // pour vous c'est transparent : vous utilisez simplement la classe qui ENCAPSULE cette implémentation $parts = parse_url($url); if ($parts === false) { return; } if (isset($parts['scheme'])) { $this->scheme = $parts['scheme']; } if (isset($parts['host'])) { $this->host = $parts['host']; } if (isset($parts['port'])) { $this->port = $parts['port']; } if (isset($parts['user'])) { $this->user = $parts['user']; $this->pwd = $parts['pass'] ?? ''; } if (isset($parts['path'])) { $this->path = explode('/', trim($parts['path'], '/')); } if (isset($parts['query'])) { $this->query = parse_str($parts['query']); } if (isset($parts['fragment'])) { $this->fragment = $parts['fragment']; } if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { $this->is_ajax = (strtoupper($_SERVER['HTTP_X_REQUESTED_WITH']) === 'XMLHTTPREQUEST'); } $this->is_valid = true; } } |
Pour manipuler ces 2 concepts, rien de plus simple :
Code php : | Sélectionner tout |
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 41 | <?php declare(strict_types=1); // ici on déclare les dépendances du code ci-dessous // cela permet au mécanisme d'autoloading de localiser les fichiers contenant la définition des classes use rawsrc\Request; use rawsrc\Response; // vous n'avez plus à vous préoccuper de comment ça fonctionne en interne // les classes encapsulent tout le code, vous ne manipulez que des concepts abstraits // par exemple, on veut avoir des détails sur une requête $request = new Request('https://www.developpez.net/forums/blogs/32058-rawsrc/b7804/developpement-selon-lapproche-mvc-modele-vue-controleur-cas-pratique/') // notez que le constructeur de la classe attend en paramètre : string $url // et il déclenche l'analyse automatiquement dès l'instanciation avec : $this->parse($url); // ici comme vous pouvez le constater vous ne vous préoccupez pas de savoir comment // le parsage de l'url a été fait, vous accédez directement au résultat : la classe Request // encapsule les traitements et devient pour ainsi dire une boite noire echo $request->scheme(), '<br>'; // https echo $request->host(), '<br>'; // www.developpez.net echo $request->path()[0], '<br>'; // forums echo $request->path()[1], '<br>'; // blogs echo $request->path()[2], '<br>'; // 32058-rawsrc echo $request->path()[3], '<br>'; // b7804 echo $request->path()[4], '<br>'; // developpement-selon-lapproche-mvc-modele-vue-controleur-cas-pratique // maintenant voyons une réponse $response = new Response(); // je souhaite envoyer des données json // je paramètre la réponse : $response->headers[] = 'content-type: application/json'; $response->data = json_encode(['a', 'b', 'c']); $response->send(); // le navigateur recevra du json // il aurait aussi été possible de faire tout en une seule ligne // le constructeur de la classe Response le permet (new Response(json_encode(['a', 'b', 'c']), ['content-type: application/json']))->send(); |
UNE PINCÉE D'HÉRITAGE
Il va falloir aborder ce concept assez succinctement, car il va être nécessaire dans notre cas pratique MVC.
Comme les classes ne sont qu'une représentation théorique, elles peuvent être très générales et très abstraites. Comme la POO a été conçue pour répondre à des besoins réels, la spécialisation d'une classe très générale est tout à fait possible via un mécanisme appelé l'héritage. Une classe fille va hériter de sa classe mère, elle va spécialiser la classe mère.
Par exemple, prenons le cas de la classe générique Response, le code de cette classe est très ouvert, vous pouvez à la volée paramétrer une réponse avec 2 lignes de code.
Si par exemple, vous devez envoyer très souvent des réponses au format JSON, cela va s'avérer très vite fastidieux de toujours recopier le paramétrage de la réponse, sans compter la redondance de code et le risque d'erreur qui va avec.
Donc pour palier à cet état de fait, on va spécialiser la classe mère et figer le paramétrage des en-têtes dans la classe fille de manière à n'avoir plus que des données à passer :
Code php : | Sélectionner tout |
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 | <?php declare(strict_types=1); namespace rawsrc; use rawsrc\Response; /** * TUTORIAL DVP SUR LE CONCEPT MVC : MODÈLE-VUE-CONTRÔLEUR * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7786/developpement-selon-l-approche-mvc-modele-vue-controleur-retour-theorie/ * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7804/developpement-selon-lapproche-mvc-modele-vue-controleur-cas-pratique/ * * Réponse : au format JSON */ class Json extends Response { // redéfinition de la méthode send() de la classe parente public function send() { // ici on force les en-têtes, on ne tient pas compte des en-têtes qui auraient pu être définis dans $headers // comparez ce code avec celui de la classe parente // on a la certitude d'envoyer le bon type au navigateur // LA CLASSE MÈRE A ÉTÉ SPÉCIALISÉE // il est tout à fait possible au développeur de mettre n'importe quoi dans $data // généralement on considère le développeur comme intelligent // après le navigateur fera ce qu'il pourra avec ce qu'il reçoit si le mime-type ne correspond pas header('content-type: application/json'); echo $this->data; } } |
Un exemple :
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 | <?php declare(strict_types=1); use rawsrc\Json; $response = new Json(json_encode(['a', 'b', 'c']); $response->send(); // quoi qu'il arrive les bons en-têtes seront envoyés au client, la classe Json s'en charge |
On en a vu assez pour se lancer dans notre cas pratique : attachez votre ceinture et mettez vos bretelles
MISE EN APPLICATION DU MVC : CAS PRATIQUE
Le support à la démonstration sera une page d'authentification basique : identifiant/mot de passe avec formulaire et accès à la base de données.
ANALYSE DE LA SITUATION
2 problèmes seront traités simultanément :
- modélisation sous forme objet du MVC
- modélisation sous forme objet de la problématique d'exemple
Dites-vous bien que les choix que j'opère ne sont pas l'unique solution possible, ils sont simplifiés de manière à pouvoir appréhender le concept plus facilement.
MODÉLISATION SOUS FORME OBJET DU MVC
Le serveur traitera chaque requête reçue comme une tâche (Task). Chaque tâche sera donc composée d'une requête et d'une réponse, comme ceci :
La tâche devra être capable de prendre en charge absolument TOUTES LES REQUÊTES qui se présenteront selon un processus standardisé. Ceci va permettre de rajouter des fonctionnalités au site sans avoir à chaque fois à se poser des tas de questions sur le comment vais-je bien donc pouvoir faire.
La class Task devra dispatcher la requête, trouver le modèle qui sera en mesure de la traiter, passer le flux de traitement au contrôleur approprié et au final collecter la réponse à envoyer.
Code source de la class Task
Code php : | Sélectionner tout |
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | <?php declare(strict_types=1); namespace rawsrc; use rawsrc\{ Controller, Request, Response }; /** * TUTORIAL DVP SUR LE CONCEPT MVC : MODÈLE-VUE-CONTRÔLEUR * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7786/developpement-selon-l-approche-mvc-modele-vue-controleur-retour-theorie/ * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7804/developpement-selon-lapproche-mvc-modele-vue-controleur-cas-pratique/ * * Représentation d'une tâche serveur générique * * Est considéré comme tâche serveur, une requête envoyée au serveur à laquelle une réponse devra être fournie * (réponse au sens générique : succès ou erreur), à ce stade on en sait encore rien * * Le traitement correspond à : * - l'analyse de la requête (étape du PARSAGE de l'URL => séparation de l'url en ses constituants) * - recherche d'une action correspondant à la requête (étape du ROUTAGE => analyse des constituants de l'URL) * - si action trouvée : transfert du flux de traitement au contrôleur rattaché à l'action * - sinon : génération d'une réponse de type Error avec le code (400 Bad Request, 404 Not Found, etc.) * * Le contrôleur doit par principe fournir une réponse quelle qu'elle soit, c'est-à-dire mouvementer la valeur de $response de la présente classe */ class Task { /** * @var Request */ private $request = null; /** * @var Response */ private $response = null; /** * @var Action */ private $action = null; /** * @return Url */ public function request(): ?Request { return $this->request; } /** * @param Response $p */ public function setResponse(Response $p) { $this->response = $p; } /** * @return Response */ public function response(): ?Response { return $this->response; } /** * @return Action */ public function action(): ?Action { return $this->action; } /** * Fonction en charge d'apporter une Réponse à la requête * Pour y parvenir, elle va essayer d'abord de trouver l'action correspondante à la requête ; * si la recherche est fructueuse alors elle va automatiquement appeler le Contrôleur rattaché à * l'action qui se chargera de fournir une réponse à la tâche */ public function dispatch() { // reconstruction de l'uri complète $scheme = 'http'.(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 's' : ''); $uri = $scheme.'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; // le parsage de l'url est automatique à l'instanciation (voir le code Request::__construct()) $this->request = new Request($uri); if ($this->request->isValid() === false) { // BAD REQUEST : création de la réponse à la volée $this->response = new Response('Invalid request', '400 Bad Request'); return; // fin de traitement } /*********** * ROUTAGE * ***********/ /** @param string $full_class_name Nom pleinement qualifié de la classe en charge du concept */ $search_action_controller = function(string $full_class_name) { $obj = new $full_class_name; foreach ($obj->actions() as $action) { /** @var Action */ $action->setTask($this); if ($action->handleRequest($this->request)) { // l'action est en mesure de prendre en charge la requête // on renvoie le nom pleinement qualifié du contrôleur qui va s'en charger return $action->controller(); } } return false; }; // on extrait la valeur du premier segment du path // par principe on a défini que cela correspondait à // l'identifiant du concept dans l'application $concept = $this->request->path()[0] ?? ''; // on recherche une correspondance dans la table des concepts $class = CONCEPTS[$concept] ?? ''; // on prépare un controller générique qui va prendre en charge la requête $controller = false; // si aucune correspondance directe trouvée if ($class === '') { // on parcourt les concepts de l'application à la recherche // de l'action capable de prendre en charge la requête foreach (CONCEPTS as $cls) { $controller = $search_action_controller($cls); if ($controller !== false) { break; } } } else { $controller = $search_action_controller($class); } if ($controller === false) { $this->response = new Response('Unable to manage the request', '400 Bad Request'); return; } // on passe le flux d'exécution au contrôleur qui // lui doit obligatoirement fournir une réponse /** @var Controller */ $controller = new $controller(); $controller->setTask($this); $controller->invoke(); // on s'assure que la réponse a bien été fournie par le contrôleur // sinon on en fournit une remontant l'erreur if ( ! ($this->response instanceof Response)) { $this->response = new Response('Invalid response format', '500 Internal Server Error'); } // à la fin de cette fonction : on est ABSOLUMENT certain // que le programme a fourni une réponse à la requête } } |
MODÉLISATION SOUS FORME OBJET DE LA PROBLÉMATIQUE D'EXEMPLE
Pour modéliser plus facilement une problématique, il faut diviser la totalité en morceaux plus petits et plus facilement gérables.
Personnellement, j'ai l'habitude de tout diviser en concepts et chaque concept est le plus autonome possible : il va contenir ses contrôleurs, ses vues, son modèle... (c'est comme si c'était un mini MVC)
Par exemple, la page d'accueil va correspondre au concept Home, la gestion d'un utilisateur au concept Utilisateur, etc.
De cette conception va découler naturellement une organisation des fichiers très simple :
N'oubliez pas que chaque fichier ne comporte qu'une seule et unique classe, ainsi on est capable de très rapidement savoir qui fait quoi. C'est beaucoup plus lisible, non ?
Comme nous l'avons vu précédemment, tout ce qui se passe sur le serveur correspond à une Action.
Et pour chaque Action devra correspondre une route et un contrôleur :
Ce qui nous donne :
Code php : | Sélectionner tout |
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | <?php declare(strict_types=1); namespace rawsrc; use rawsrc\{ Controller, Request, Response }; use BadFunctionCallException; /** * TUTORIAL DVP SUR LE CONCEPT MVC : MODÈLE-VUE-CONTRÔLEUR * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7786/developpement-selon-l-approche-mvc-modele-vue-controleur-retour-theorie/ * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b7804/developpement-selon-lapproche-mvc-modele-vue-controleur-cas-pratique/ * * Représentation d'une action générique * * Une action va correspondre à un traitement sur le serveur web * Elle fournira tout ce qui est nécessaire à l'application pour être pilotée de manière la plus autonome possible * ainsi on pourra empiler des actions sans avoir pour autant à toucher au code de traitement d'une requête et d'une réponse * * Comme il été vu dans le tuto : * À CHAQUE ACTION DISPONIBLE DANS LE SITE WEB DEVRA CORRESPONDRE UNE SEULE ET UNIQUE URL/ROUTE/CONTRÔLEUR * QUI SERA DE FAIT L'UNIQUE POINT D'ENTRÉE DU SITE POUR CE TRAITEMENT */ class Action { /** * Si l'action correspond à celle demandée par la requête alors une instance * de la tâche serveur en cours sera dynamiquement injectée au cas où l'action * aurait besoin de connaître son environnement d'exécution pour s'exécuter * * @var Task */ private $task = null; /** * @var Route */ private $route = null; /** * @var Controller */ private $controller = null; /** * @var Response */ private $response = null; /** * @param array $p [route, controller, response] */ public function __construct(array $p) { $this->set($p); } /** * @param Task $p * @return self */ public function setTask(Task $p): self { $this->task = $p; if ($this->controller instanceof Controller) { $this->controller->setTask($p); } return $this; } /** * @return Task */ public function task(): ?Task { return $this->task; } /** * @param mixed $p closure that return a strict boolean value | null * @return self */ public function setRoute($p): self { if (is_callable($p) || ($p === null)) { $this->route = $p; } return $this; } /** * @return mixed Closure|null */ public function route() { return $this->route; } /** * @param mixed $p Controller | closure that return a Controller object | null | string * @return slef */ public function setController($p): self { if (is_string($p) || ($p instanceof Controller) || is_callable($p) || ($p === null)) { $this->controller = $p; } return $this; } /** * @return mixed Controller|\Closure|null|string */ public function controller() { return $this->controller; } /** * @param mixed $p * @return self Response | closure that return a Response object | null | string */ public function setResponse($p): self { if (is_string($p) || ($p instanceof Response) || is_callable($p) || ($p === null)) { $this->response = $p; } return $this; } /** * @return mixed Response|closure|null|string */ public function response() { return $this->response; } /** * Set many at once * * @param array $p [acl, route, controller, response] */ public function set(array $p) { if (isset($p['route'])) { $this->setRoute($p['route']); } if (isset($p['controller'])) { $this->setController($p['controller']); } if (isset($p['response'])) { $this->setResponse($p['response']); } } /** * @param Request $p * @return bool * * @throws BadFunctionCallException */ public function handleRequest(Request $p): bool { if (is_callable($this->route)) { $func = $this->route; $match = $func($p); if (is_bool($match)) { return $match; } else { throw new BadFunctionCallException('Callable does not return a strict boolean'); } } return false; } } |
CONCEPTS MÉTIER
Tout est concept dans l'organisation , attardons nous sur le concept Utilisateur, comme tout concept il est générique c'est-à-dire qu'il est capable de gérer n'importe quel utilisateur sans exception. Tous les cas de figure doivent être prévus, c'est le boulot d'un développeur aguerri.
Code php : | Sélectionner tout |
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | <?php declare(strict_types=1); namespace src\Utilisateur; use rawsrc\Action; use rawsrc\Request; use src\Utilisateur\Controller\{ Login, Connect, Dashboard }; use src\Utilisateur\Model\Data; /** * Classe représentant le concept Utilisateur */ class Utilisateur { /** * Liste des actions possibles pour un utilisateur * * @return array [Action] */ public function actions(): array { // pour bien comprendre les concepts, je vais aller au plus simple // il est tout à fait possible d'écrire son code autrement afin de gagner en souplesse et possibilités return [ // action 1 = /utilisateur/login pour afficher le formulaire de connexion new Action([ // cette fonction (route) ne fait que vérifier si les composants de la requête en paramètre lui correspondent // si la correspondance est totale alors cette action sera retenue et considérée comme l'action en cours // donc c'est le controller rattaché à cette action qui héritera du flux de traitement // route = /utilisateur/login 'route' => function(Request $p) { return $p->path()[1] === 'login'; }, 'controller' => Login::class ]), // route = /utilisateur/connect pour la soumission du formulaire de connexion new Action([ 'route' => function(Request $p) { return $p->path()[1] === 'connect'; }, 'controller' => Connect::class ]), // route = /utilisateur/dashboard (tableau de bord) new Action([ 'route' => function(Request $p) { return $p->path()[1] === 'dashboard'; }, 'controller' => Dashboard::class ]), ]; } /** * Pointeur vers la classe en charge de la gestion des données -> LE MODELE * @return Data */ public function data() { return Model\Data::class; } } |
Vu que l'organisation est générique et l'approche du code standardisée, il va être dorénavant beaucoup plus simple d'étendre les fonctionnalités du site sans se noyer. Il fat toujours s'efforcer de préserver la même architecture tout au long du développement. Le MVC vous permet assez facilement de le faire.
LES VUES
Les vues reposent sur un petit moteur de rendu de ma conception : PhpEcho. Ce moteur reposant sur une seule et unique classe est disponible et détaillé sur un de mes autres articles de blog DVP.
Par exemple le formulaire de connexion d'un utilisateur est codé ainsi :
Code html : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | <p> Blog de <strong>rawsrc</strong> sur DVP </p> <p>Veuillez vous identifier</p> <form method=post action="<?= $this['url_submit'] ?>"> <label>Identifiant</label> <input type="text" name="login" value="<?= $this('login') ?>"><br> <label>Mot de passe</label> <input type="password" name="pwd" value=""><br> <input type="submit" name="submit" value="SE CONNECTER"> </form> <br> <p style="display:<?= $this['error'] ? 'block' : 'none' ?>"><strong><?= $this('error') ?></strong></p> |
Le contrôleur s'occupe de paramétrer l'affichage du formulaire de connexion comme ceci :
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php declare(strict_types=1); namespace src\Utilisateur\Controller; use rawsrc\{ Controller, Response }; use rawsrc\PhpEcho\PhpEcho; class Login extends Controller { public function invoke() { // corps de la vue $body = new PhpEcho([__DIR__, '.. View Login.php'], ['url_submit' => '/utilisateur/connect']); // layout de la page principale $page = new PhpEcho([DIR_ROOT, 'src View Layout.php'], ['body' => $body]); $this->setResponse(new Response($page)); } } |
Il vous tout à fait possible d'opter pour n'importe quel autre moteur de génération de rendu. Le concept MVC s'accommode très facilement de toute extension ou nouvel ajout.
SCHÉMA GLOBAL DU PRINCIPE MVC
Compte tenu des problèmes de compréhension de certaines étapes, j'ai décidé de vous proposer un schéma global de l'approche MVC mettant à plat toute l'articulation de la logique sous-jacente.
Voici globalement le travail accompli par ces 4 lignes de code situées dans index.php:
Code php : | Sélectionner tout |
1 2 3 4 5 | use rawsrc\Task; $task = new Task(); $task->dispatch(); $task->response()->send(); |
NOTION DE CONCEPT
FICHIERS DU PROJET ET MISE EN ROUTE
Pour que vous puissiez tester le projet en réel, il vous faudra une petite base de données MySQL avec qu'une seule table :
Code sql : | Sélectionner tout |
1 2 3 4 5 6 7 8 | CREATE TABLE `t_user` ( `user_id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, `user_login` VARCHAR(255) COLLATE utf8_general_ci NOT NULL, `user_pwd_hash` VARCHAR(255) COLLATE utf8_general_ci NOT NULL, PRIMARY KEY USING BTREE (`user_id`), UNIQUE KEY `user_login` USING BTREE (`user_login`) ) ENGINE=InnoDB AUTO_INCREMENT=1 ROW_FORMAT=DYNAMIC CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'; |
et juste une seule ligne de données :
Code sql : | Sélectionner tout |
1 2 | INSERT INTO `t_user` (`user_login`, `user_pwd_hash`) VALUES ('rawsrc', '$argon2i$v=19$m=1024,t=2,p=2$Uzdldm5iWTJMOGp3cXdsZQ$4ZE9jZ8YyH4SGXsjT/ED/XYQpf0oVJe4TRWQXXS04eQ'); |
REDIRECTION VERS index.php
Comme nous l'avons vu, un seul et unique point d'entrée de toutes les requêtes est nécessaire au bon fonctionnement du projet, pour cela il faudra faire une redirection au niveau du serveur web comme ceci pour apache :
RewriteEngine on
RewriteCond %{REQUEST_URI} /(index)(\..{3,4})?$ [NC]
RewriteRule ^.* http://dev.mvc.fr [R=301,L]
RewriteCond %{REQUEST_FILENAME} !^/css/.*$
RewriteCond %{REQUEST_FILENAME} !^/js/.*$
RewriteCond %{REQUEST_FILENAME} !^/img/.*$
RewriteRule . /index.php [QSA,L]
Il y a quelques constantes à adapter à votre environnement dans le fichier index.php
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | // Gestion des constantes define('URL_HOME', 'http://dev.mvc.fr'); // Base de données define('DB_SCHEME', 'mysql'); define('DB_HOST', 'localhost'); define('DB_NAME', 'db_mvc'); define('DB_PORT', '3306'); define('DB_USER', 'root'); define('DB_PWD', ''); |
Et enfin, le ficher ZIP qui contient tous les fichiers du projet (fichiers mis à jour avec PhpEcho)
[ATTACH]509914d1/a/a/a" />
CONCLUSION
Nous sommes arrivés au terme de ce tutoriel qui j'espère vous aura été très utile.
Dites-vous bien que le passage à un codage MVC va nécessiter une nouvelle manière de penser (beaucoup mieux découplée) ainsi qu'un certain travail de reprise de vos codes existants, mais c'est pour du mieux alors !!!
Bon code à tous, bienvenue aux p'tits nouveaux dans le vaste monde de la POO.
EDIT : Reprise complète des vues en utilisant PhpEcho, disponible à cette adresse.
rawsrc