Pas d'autoloading, pas de code ni de concepts compliqués, juste de simples include, comme à l'ancienne et avec une facilité de travail proche de la maternelle.
Ce moteur s'occupe de tout : gestion des inclusions, des échappements, du passage de variables et aussi de la récupération du code généré
Il est également disponible sur mon espace Github.
ATTENTION : Ce projet est mis à jour régulièrement, pensez plutôt à consulter le dépôt sur GitHub.
1 - CONSTRUCTION D'UNE PAGE WEB
Généralement, la construction d'une page web nécessite une bonne organisation et un découpage fin des différents éléments visuels qui une fois agencés correctement produiront le résultat escompté.
C'est un principe phare en informatique en général : Diviser pour mieux régner.
On ne va pas déroger à cette règle et on va l'appliquer totalement.
Pour illustrer le propos, on va prendre un tout petit bout de votre site préféré : developpez.net, forum PHP :
Comme vous pouvez le constater, la présentation des messages du forum est totalement standardisée. Il va donc être possible de générer le rendu de manière uniforme à partir de simples informations textuelles qui auront été au préalable extraites et parfaitement identifiées. J'insiste sur le ce dernier point : chaque information que vous manipulez doit être identifiée de manière unique. Il faut toujours faire très attention à ce que les identifiants (clés des tableaux dans la plupart des cas), quand ils s'empilent, ne s'écrasent pas les uns les autres.
Pour suivre notre exemple, il est tout à fait sensé que la vue en charge de rendre un message attende un tableau de valeurs de ce genre :
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | $vars = [ 'titre_message' => '[POO] classe non trouvée malgré autoloader ET include du fichier contenant la classe', 'auteur' => 'laurentSc', 'horodatage' => '2019-10-09 21:26', 'nb_reponses' => '10', 'nb_affichages' => '87', 'forum' => 'Débuter', 'dernier_message' => '2019-10-10 22:17', 'auteur_dernier_message' => 'laurentSc' ]; |
Comme la page va être divisée en plein de blocs, il va falloir bien identifier les éléments nécessaires au bon fonctionnement de chaque bloc vue (en particuliers les valeurs attendues). De même, il est possible de diviser à l'infini les blocs vue en d'autres sous-blocs vue et ainsi de suite. C'est au développeur qu'il appartient d'organiser le découpage.
Position de la vue dans la chaîne de traitement
Nous arrivons à un point fondamental dans la compréhension de la construction d'un site, comme l'exemple vous le démontre, une vue n'est rien d'autre qu'un afficheur de données.
La vue est ce que l'on appelle une terminaison dans le traitement : elle ne fait que recevoir des données à afficher mais ne se préoccupe pas de savoir comment ces données ont été extraites, comment elles ont été travaillées ou même à quoi elles servent... Attention : Une vue n'a pas à extraire des données.
La vue ne fait que de la mise en forme.
Attention ! Comme souvent : le terme de mise en forme est volontairement générique.
Une mise en forme peut être :
- une page html
- un fichier .pdf
- un fichier .zip
- etc.
2 - GÉNÉRATION D'UNE PAGE WEB EN PHP
N'oublions pas que PHP est déjà à lui seul un moteur de rendu. Il sait parfaitement injecter des valeurs dans des chaînes de caractères, inclure des fichiers entiers les uns dans les autres, sécuriser les valeurs renvoyées au navigateur, être très souple dans sa manipulation pour permettre une mise en forme aisée selon ses besoins. Bref, il faut se rendre à l'évidence, à priori, il ne lui manque rien quand on le connait un peu.
Le hic, c'est que, quand on le connait un peu (et c'est mon cas ), la gestion des rendus manque de souplesse, amène rapidement un code redondant et ne tolère aucun oubli dans les échappements. Bref, faire un rendu à coup d'include devient vite une vraie galère (je compatis les gars, je compatis). C'est partant de ce constat, que des développeurs ont eu l'idée d'améliorer tout ça et ont créé une tonne de moteurs de rendu (template engine) : Smarty, Twig, Blade, Volt...
La contrepartie quoi qu'on en dise, ils rajoutent tous une couche non négligeable de traitements et d'autres contraintes. Sans compter qu'il faut prendre le temps de les découvrir, apprendre leur syntaxe, règles internes et enfin pour la plupart d'entre-eux avoir l'obligation de mettre en place un moteur de cache performant.
3 - ATTENTES D'UN MOTEUR DE RENDU EN PHP
- Gestion des inclusions de fichiers (ou blocs)
- Aide à la construction des chemins des fichiers à inclure
- Transmission aisée de variables aux vues sous forme de tableau [clé ⇒ valeur]
- Échappement automatique des valeurs
- Échappement sur demande des valeurs
- Légèreté dans son fonctionnement
- Ne pas avoir à apprendre une nouvelle syntaxe
La version minimale visée est PHP 7+ mais vu la simplicité du truc, cela fonctionnera aussi sur les versions antérieures PHP 5.3+.
Contrainte unique : Il est admis qu'aucun espace n'est inséré dans le nommage des éléments tels que répertoires et noms de fichier. C'est déjà le cas dans 100% des développements (je m'avance un peu, là ), malgré tout si vous avez l'habitude d'en insérer, perdez la vite car cela crée plus de problèmes qu'autre chose. Il est possible de remplacer avantageusement l'espace par le tiret bas : _
Règle d'or : EN DÉVELOPPEMENT INFORMATIQUE, NE JAMAIS INSÉRER D'ESPACE DANS QUOI QUE CE SOIT. ÇA PEUT VOUS SAUVER LA VIE
4 - CONCEPTS RELATIFS À LA PROGRAMMATION ORIENTÉE OBJET
Dans ce passage, on va passer en revue les concepts qu'il faut maîtriser pour bien comprendre le fonctionnement de l'outil.
4.1 - CLASSES : DÉFINITION DES DONNÉES - ACCESSEURS ET MUTATEURS
Si vous prenez une classe standard, pour lui modifier une valeur interne, plusieurs méthodes sont possibles : il faut soit lui définir des mutateurs (les fameuses fonctions commençant généralement par set comme setNom($nom)), soit rendre ses attributs publics.
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | // AVEC MUTATEUR class Foo { private $nom; public function setNom(string $nom) { $this->nom = $nom; } } $foo = new Foo(); $foo->setNom('rawsrc'); |
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 | // AVEC ACCÈS PUBLIC class Foo { public $nom; } $foo = new Foo(); $foo->nom = 'rawsrc'; |
Par exemple, on veut s'assurer que seuls les noms ayant au minimum 5 caractères soient acceptés, cela donnera ce code :
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 | // AVEC MUTATEUR class Foo { private $nom = ''; public function setNom(string $nom) { // SI ET SEULEMENT SI LA LONGUEUR DU NOM EST >= 5 // SINON L'ATTRIBUT $nom N'EST PAS DÉFINI if (mb_strlen($nom) >= 5) { $this->nom = $nom; } } public function getNom(): string { return $this->nom; } } $foo = new Foo(); $foo->setNom('rawsrc'); // 6 caractères, ça passe $nom = $foo->getNom(); // $nom = rawsrc $foo = new Foo(); $foo->setNom('luc'); // 3 caractères, le nom ne sera pas défini dans l'instance $foo $nom = $foo->getNom(); // $nom = '' |
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | class Foo { public $nom; } $foo = new Foo(); $foo->nom = 'luc'; // aucun contrôle, c'est la fête au village if (mb_strlen($foo->nom) >= 5) { // traitement quand le nom est valide } |
4.2 - CLASSES : INTERFACES SYSTÈMES ET IMPLÉMENTATION
Après avoir vu ce préalable, revenons à nos moutons.
Quand vous développez en programmation orientée objet, il est possible de conférer certains comportements à vos classes pour peu que vous implémentiez certaines interfaces systèmes.
Je m'explique : si vous souhaitez avoir la possibilité de manipuler un objet (instance de classe) comme un tableau, c'est-à-dire être capable de coder ainsi :
Code php : | Sélectionner tout |
1 2 3 4 5 6 | class Foo { private $vars = ''; } $foo = new Foo(); $foo['nom'] = 'rawsrc'; // voyez la notation tableau |
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 | class Foo implements \ArrayAccess { private $vars = []; // tableau qui va servir à stocker toutes les valeurs /** * Interface ArrayAccess * @param mixed $offset * @return bool */ public function offsetExists($offset) { return isset($this->vars[$offset]); // on vérifie que la clé est définie et différente de null } /** * Interface ArrayAccess * @param mixed $offset * @return mixed|null */ public function offsetGet($offset) { return $this->vars[$offset] ?? null; } /** * Interface ArrayAccess * @param mixed $offset * @param mixed $value */ public function offsetSet($offset, $value) { $this->vars[$offset] = $value; } /** * Interface ArrayAccess * @param mixed $offset */ public function offsetUnset($offset) { unset($this->vars[$offset]); } } // ET LA TECHNIQUE OPÈRE $foo = new Foo(); // ici vous manipulez une instance de classe $foo['nom'] = 'rawsrc'; // qui se comporte comme un tableau |
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 | class Foo implements \ArrayAccess, \Countable { private $vars = []; // tableau qui va servir à stocker toutes les valeurs /** * Interface ArrayAccess * @param mixed $offset * @return bool */ public function offsetExists($offset) { return isset($this->vars[$offset]); // on vérifie que la clé est définie et différente de null } /** * Interface ArrayAccess * @param mixed $offset * @return mixed|null */ public function offsetGet($offset) { return $this->vars[$offset] ?? null; } /** * Interface ArrayAccess * @param mixed $offset * @param mixed $value */ public function offsetSet($offset, $value) { $this->vars[$offset] = $value; } /** * Interface ArrayAccess * @param mixed $offset */ public function offsetUnset($offset) { unset($this->vars[$offset]); } /** * Interface Countable * @return int */ public function count() { return count($this->vars); } } // ET LA TECHNIQUE OPÈRE $foo = new Foo(); $foo['nom'] = 'rawsrc'; $nb_elem = count($foo); // $nb_elem = 1 |
Une dernière pour la route, tout en gardant le comportement tableau, on veut que si la clé est 'nom' alors ne sont acceptées que les valeurs dont la longueur est supérieure ou égale à 5.
On corrige légèrement la fonction en charge de la définition des valeurs en y intégrant le contrôle adéquate et hop le tour est joué :
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * Interface ArrayAccess * @param mixed $offset * @param mixed $value */ public function offsetSet($offset, $value) { if ($offset === 'nom') { if (is_scalar($value) && (mb_strlen($value) >= 5)) { $this->vars[$offset] = $value; } } else { $this->vars[$offset] = $value; } } |
CONCLUSION : tout ça pour vous dire qu'il existe des tas de manières pour rendre votre code objet très souple sans perdre pour autant les éléments de contrôle et de vérification nécessaires à tout bon code robuste. Un développeur doit TOUJOURS savoir ce qu'il manipule, même si le langage est dynamique et permissif sur le typage.
4.3 - CLASSES : MÉTHODES MAGIQUES
PHP offre une autre fonctionnalité très puissante : les méthodes magiques. Ces méthodes permettent d'adapter le fonctionnement d'une classe à certains contextes spécifiques. Oui je sais, lu comme ça, ça pique un peu.
Un exemple, va vite vous éclairer.
Quand nous faisons un simple echo, ce qui est attendu après ce mot clé est une chaîne de caractères (string). Ici, on peut dire que echo détermine un contexte fermé avec une contrainte. Pareil, si on écrit au sein d'un bloc heredoc, le contexte est clairement défini et il faut produire du texte :
Code php : | Sélectionner tout |
1 2 3 4 5 | $str = <<<str Ici je suis dans un contexte qui attend du texte pour fonctionner correctement Là aussi Et jusqu'à que ce contexte soit fermé. str; |
Dans notre cas, la méthode magique en charge de renvoyer automatiquement du texte quand c'est nécessaire à partir d'une instance est public function __toString(): string.
Regardez bien :
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | class Foo { public function __toString() { return "je suis du texte en provenance d'une instance de la classe Foo"; } } $foo = new Foo(); // instance de la classe Foo // voyez ici, on renvoie directement l'instance de la classe comme si c'était du texte echo $foo; // ce qui est affiché : je suis du texte en provenance d'une instance de la classe Foo |
Si notre classe n'avait pas cette méthode magique, on aurait récupéré une belle erreur fatale :
Catchable fatal error: Object of class Foo could not be converted to string
Il y a plein de méthode magiques pour répondre à des tas de besoins.
Il y a une autre méthode magique qui va nous intéresser : public function __invoke().
Cette méthode nous offre la possibilité d'utiliser une instance de classe comme une fonction !
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 | class Foo { public function add(int $x, int $y): int { return $x + $y; } public function substract(int $x, int $y): int { return $x - $y; } public function __invoke(string $op, int $x, int $y) { if ($op === 'add') { return $this->add($x, $y); } elseif ($op === 'substract') { return $this->substract($x, $y); } } } $foo = new Foo(); $sum = $foo->add(10, 25); // 35 $sub = $foo->substract(25, 10); // 15 // il est possible d'utiliser l'instance comme une fonction pour arriver au même résultat : grâce à la méthode magique __invoke() $sum = $foo('add', 10, 20); // ici nous appelons une fonction $foo() alors que $foo est une instance de classe... $sub = $foo('substract', 25, 10); |
Fatal error: Uncaught Error: Function name must be a string
Enfin juste pour finir, il est possible de bloquer le contexte défini par new, si par exemple vous ne souhaitez pas que la classe soit instanciée (cas du Singleton) en rendant la méthode magique privée :
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 | class Foo { private function __construct() { } } $foo = new Foo(); |
Fatal Error: Call to private Foo::__construct() from invalid context
Cela offre des possibilités inouïes en terme de modélisation.
Allez, assez bavardé, on passe aux choses sérieuses.
5 - MOTEUR DE RENDU - PHP 7+
Ci-après, vous trouverez le code commenté du moteur de rendu. Le code fait appel massivement aux concepts abordés et détaillés précédemment.
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 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | <?php namespace rawsrc\PhpEcho; /** * PhpEcho : PHP Template engine : One class to rule them all ;-) * * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b8215/phpecho-moteur-rendu-php-classe-gouverner/ * @author rawsrc - https://www.developpez.net/forums/u32058/rawsrc/ * @copyright MIT License * * Copyright (c) 2020 rawsrc * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ class PhpEcho implements \ArrayAccess { /** * @var string */ private $id = ''; /** * @var array */ private $vars = []; /** * Full resolved filepath to the external view file * @var string */ private $file = ''; /** * @var string */ private $code = ''; /** * @param mixed $file see setFile() below * @param array $vars * @param string $id if empty then auto-generated */ public function __construct($file = '', array $vars = [], string $id = '') { if ($file !== '') { $this->setFile($file); } if ($id === '') { $this->generateId(); } else { $this->id = $id; } $this->vars = $vars; } /** * @param string $id */ public function setId(string $id) { $this->id = $id; } /** * @return string */ public function id(): string { return $this->id; } /** * Generate an unique execution id based on random_bytes() * Always start with a letter */ public function generateId() { $this->id = chr(mt_rand(97, 122)).bin2hex(random_bytes(4)); } /** * Interface ArrayAccess * @param mixed $offset * @return bool */ public function offsetExists($offset) { return array_key_exists($offset, $this->vars); } /** * Interface ArrayAccess * @param mixed $offset * @return mixed|null */ public function offsetGet($offset) { return $this->vars[$offset] ?? null; } /** * Interface ArrayAccess * @param mixed $offset * @param mixed $value */ public function offsetSet($offset, $value) { $this->vars[$offset] = $value; } /** * Interface ArrayAccess * @param mixed $offset */ public function offsetUnset($offset) { unset($this->vars[$offset]); } /** * Define the filepath to the external view file to include * * Rule R001 : Any space inside a name will be automatically converted to DIRECTORY_SEPARATOR * * For strings : $parts = 'www user view login.php'; * - become "www/user/view/login.php" if DIRECTORY_SEPARATOR = '/' * - become "www\user\view\login.php" if DIRECTORY_SEPARATOR = '\' * * For arrays, same rule (R001) for all values inside : $parts = ['www/user', 'view login.php']; * - become "www/user/view/login.php" if DIRECTORY_SEPARATOR = '/' * - become "www/user\view\login.php" if DIRECTORY_SEPARATOR = '\' * * File inclusion remove the inline code * * @param mixed $parts string|array */ public function setFile($parts) { $file = []; $parts = is_string($parts) ? explode(' ', $parts) : $parts; foreach ($parts as $p) { $file[] = str_replace(' ', DIRECTORY_SEPARATOR, $p); } $this->file = str_replace(DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, $file)); $this->code = ''; } /** * Instead on including an external file, use inline code for the view * * CAREFUL : when you use inline code with dynamic values from the array $vars, you must * be absolutely sure that the values are already defined before, otherwise you will only have empty strings * * Inline code remove the included file * * @param string $code */ public function setCode(string $code) { $this->code = $code; $this->file = ''; } /** * This function return always escaped value with htmlspecialchars() from the array $vars * * You escape on demand anywhere in your code by calling this class like this : * $this('hsc', 'any scalar value you would like to escape'); * * NOTE : a scalar value is a value that return true on PHP is_scalar() function * or an instance of class that implements the magic function __toString() * * @param array $args * @return mixed */ public function __invoke(...$args) { $nb = count($args); if (empty($args) || ($nb > 2)) { return ''; } /** * @param $p * @return bool */ $is_scalar = function($p): bool { return is_scalar($p) || (is_object($p) && method_exists($p, '__toString')); }; /** * @param $p * @return string */ $hsc = function($p): string { return htmlspecialchars((string)$p, ENT_QUOTES, 'utf-8'); }; /** * Return an array of escaped values with htmlspecialchars(ENT_QUOTES, 'utf-8') for both keys and values * Works for scalar and array type and transform any object having __toString() function implemented to a escaped string * Otherwise, keep the object as it * * @param array $part * @return array */ $hsc_array = function(array $part) use (&$hsc_array, $hsc, $is_scalar): array { $data = []; foreach ($part as $k => $v) { $sk = $hsc($k); if (is_array($v)) { $data[$sk] = $hsc_array($v); } elseif ($is_scalar($v)) { $data[$sk] = $hsc($v); } else { $data[$sk] = $v; } } return $data; }; $value = null; if (($nb === 1) && isset($this->vars[$args[0]])) { $value = $this->vars[$args[0]]; } elseif ($args[0] === 'hsc') { $value = $args[1]; } if ($is_scalar($value)) { return $hsc($value); } elseif (is_array($value)) { return $hsc_array($value); } else { return ''; } } /** * Magic method that returns a string instead of current instance of the class in a string context */ public function __toString() { if (($this->file !== '') && is_file($this->file)) { ob_start(); include $this->file; return ob_get_clean(); } else { return $this->code; } } } // make the class directly available on the global namespace class_alias('rawsrc\PhpEcho\PhpEcho', 'PhpEcho', false); |
Quelques explications :
Avec ce code, si on utilise la notation tableau, on va récupérer la valeur rattachée à la clé telle qu'elle a été définie.
Si on utilise la notation fonction, on va récupérer la valeur rattachée à la clé telle qu'elle a été définie mais échappée avec htmlspecialchars().
Code php : | Sélectionner tout |
1 2 3 4 5 | $engine = new PhpEcho(); $engine['abc'] = 'abc " < >'; // on stocke une paire clé-valeur dans notre classe (utilisation de l'interface ArrayAccess offsetSet()) // maintenant si on fait : $x = $engine['abc']; // $x = 'abc " < >' // notation tableau, valeur brut renvoyée $y = $engine('abc'); // $y = 'abc &_quot; < >' // notation fonction : valeur échappée, inoffensive, pour le &_quote; c'est sans le _ bien sûr (c'est juste pour l'affichage) |
Où que vous soyez, dans le code, si vous avez besoin d'échapper une valeur, il est possible de faire appel à la fonction htmlspecialchars() nativement :
Code php : | Sélectionner tout |
$z = $engine('hsc', 'une valeur quelconque à échapper');
Regardez bien le code la fonction __invoke(), tout se passe dedans.
Pour fonctionner, ce moteur attend que les différents éléments de la vue produisent un rendu soit avec echo, soit en utilisant l'output buffering, c'est-à-dire l'écriture de code HTML en dehors des balises <?php ... ?>.
Enfin, il est important de bien comprendre comment fonctionnent les inclusions en php avec le mot clé include, je vous invite à lire directement la documentation sur le site officiel de PHP.
Voilà, nous en avons fait le tour, il n'y a plus qu'à le tester en situation réelle
6 - CAS PRATIQUE POUR LE MOTEUR DE RENDU PhpEcho
On va faire un simple formulaire de connexion avec PhpEcho.
Pour cela on va avoir besoin de 4 fichiers !
Arborescence :
www
|---index.php <- Point d'entrée du site, démarrage de l'environnement
|---src
| |---Login.php <- Gestion du traitement pour afficher le formulaire de connexion
|---view
| |---Layout.php <- Gabarit HTML de page par défaut
| |---LoginForm.php <- Formulaire HTML de connexion
|---vendor
| |---PhpEcho
| |---PhpEcho.php <- Classe du moteur de rendu
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | <?php // quelques constantes utiles define('DIR_ROOT', __DIR__.DIRECTORY_SEPARATOR); define('URL_HOME', 'http://dev.dvp.fr'); // sur mon serveur j'utilise cette adresse // ici on va chercher notre classe de moteur de rendu include DIR_ROOT.'vendor'.DIRECTORY_SEPARATOR.'PhpEcho'.DIRECTORY_SEPARATOR.'PhpEcho.php'; // démarrage de l'application : formulaire de connexion include DIR_ROOT.'src'.DIRECTORY_SEPARATOR.'Login.php'; |
Une page Layout.php enregistrée dans /view
Code html : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <?= implode('', $this['meta'] ?? []) ?> <title><?= $this('title') ?></title> </head> <body> <?= $this['body'] ?> </body> </html> |
NB : Pour les inclusions de fichiers (include, include_once, require, require_once), la doc PHP stipule :
Envoyé par PHP Manuel
N'oubliez pas que l'inclusion est faite à l'intérieur de la classe PhpEcho public function __toString(), d'où l'existence de $this dans le fichier inclus.
Dans notre layout, on offre la possibilité
- d'avoir un tableau de balises <meta> qui, s'il est défini, sera transformé en texte : notez la notation tableau : $this['meta'], les données dedans ne seront pas échappées.
- de personnaliser un titre qui sera échappé à l'affichage : $this('title'), notez la notation fonction
- et un corps de page qui lui est déjà échappé dans la mesure où il est assemblé par bouts qui sont tous théoriquement déjà échappés ⇒ notation tableau : $this['body'].
Une page LoginForm.php enregistrée dans /view :
Code html : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | <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['show_error'] ?? 'none' ?>"><strong><?= $this('err_msg') ?></strong></p> |
Enfin un dernier script Login.php enregistré dans /src qui lui pilote la fonctionnalité : affichage du formulaire de connexion.
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 | <?php // pour tous les blocs, on utilise PhpEcho // on charge notre Layout $page = new PhpEcho([DIR_ROOT, 'view Layout.php']); $page['title'] = 'Connexion au site'; // définition d'un paramètre attendu par notre Layout // construction du corps de la page : variable 'body' // le corps de la page sera notre formulaire de connexion // on lui passe ce qui est attendu pour son fonctionnement $body = new PhpEcho([DIR_ROOT, 'view LoginForm.php'], [ 'url_submit' => '/index.php?page=loginsubmit', 'login' => 'rawsrc' ]); // on rattache le corps de page au layout // notez que la valeur de la clé 'body' est directement une instance de la classe PhpEcho $page['body'] = $body; /** * dans le layout, voici comment cette valeur est traitée : <?= $this['body'] ?> * l'instance est directement transformée en string avec echo * aucun problème car on sait que PhpEcho implémente la méthode magique __toString() * dans ce contexte la commande echo dans sa forme abrégée <?= recevra bien du texte */ // on renvoie au navigateur la page assemblée echo $page; |
Code php : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | <?php echo new PhpEcho([DIR_ROOT, 'view Layout.php'], [ 'title' => 'Connexion au site', 'body' => new PhpEcho([DIR_ROOT, 'view LoginForm.php'], [ 'url_submit' => '/index.php?page=loginsubmit', 'login' => 'rawsrc' ]) ]); |
6.1 - CODE DE RENDU SANS L'INCLUSION DE FICHIER EXTERNE
Il est possible de construire un code de rendu sans passer par le mécanisme d'inclusion de fichier.
On va reprendre notre exemple et on va omettre le fichier LoginForm.php, on va inclure directement son code source dans le fichier construisant la vue : Login.php.
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 | <?php // pour tous les blocs, on utilise PhpEcho // on charge notre Layout $page = new PhpEcho([DIR_ROOT, 'view Layout.php']); $page['title'] = 'Connexion au site'; // définition d'un paramètre attendu par notre Layout // construction du corps de la page : variable 'body' // le corps de la page sera le code source de notre formulaire de connexion // on lui passe ce qui est attendu pour son fonctionnement $body = new PhpEcho('', [ 'url_submit' => '/index.php?page=loginsubmit', 'login' => 'rawsrc' ]); // ICI on définit directement le code de rendu sans passer par l'inclusion de fichier // ATTENTION : dans ce cas d'utilisation, $this est remplacé par $body // vous avez toujours à disposition les notations tableau et fonction $body->setCode(<<<html <p>Veuillez vous identifier</p> <form method=post action="{$body['url_submit']}>"> <label>Identifiant</label> <input type="text" name="login" value="{$body('login')}"><br> <label>Mot de passe</label> <input type="password" name="pwd" value=""><br> <input type="submit" name="submit" value="SE CONNECTER"> </form> html ); // on rattache le corps de page au layout // notez que la valeur de la clé body est une instance de la classe PhpEcho $page['body'] = $body; // on renvoie au navigateur la page assemblée echo $page; |
6.2 - UTILISATION DE L'ID D'EXÉCUTION UNIQUE
Avec la dernière mise à jour, j'y ai inclus la génération d'un id d'exécution unique pour chaque instance de la classe PhpEcho.
Cet id va permettre de définir facilement un contexte d'exécution fermé propre à l'instance courante, l'utilité est grande dans la mesure où le HTML gère cela parfaitement avec l'attribut id disponible pour chaque tag.
Comment s'en servir et pour quels usages :
On va reprendre notre fichier LoginForm.php et par exemple on veut faire des essais de mise en forme de ce petit bloc sans altérer le rendu des autres blocs vue du site.
Par exemple, on veut refaire un peu la mise en page et revoir certains aspects esthétiques de notre formulaire de connexion.
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 $id = $this->id() ?> <style> #<?= $id ?> label { color: blue; float: left; font-weight: bold; width: 30%; } #<?= $id ?> input { float: right; } </style> <div id="<?= $id ?>"> <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['show_error'] ?? 'none' ?>"><strong><?= $this('err_msg') ?></strong></p> </div> |
Et cerise sur le gâteau : cela fonctionne aussi pour du javascript personnalisé au bloc !
Même code sans l'utilisation de l'inclusion de fichier (tiré du paragraphe précédent) :
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 | <?php // code précédent le bloc $id = $body->id(); $body->setCode(<<<html <style> #{$id} > p { font-weight: bold; } #{$id} label { color: blue; float: left; font-weight: bold; width: 30%; } #{$id} input { float: right; } </style> <div id="{$id}"> <p>Veuillez vous identifier</p> <form method=post action="{$body['url_submit']}>"> <label>Identifiant</label> <input type="text" name="login" value="{$body('login')}"><br> <label>Mot de passe</label> <input type="password" name="pwd" value=""><br> <input type="submit" name="submit" value="SE CONNECTER"> </form> </div> html ); |
C'est une approche équivalente au concept plus global de Widgets.
7 - CONCLUSION
Nous sommes arrivés au terme de cet article de blog, et encore une fois je vous ai mis une de ces tartine ! Code, explications, concepts... Désolé
Ce système élégant de rendu n'est possible que parce qu'on a fait appel aux concepts et fonctionnalités de la programmation orientée objet. Avec une approche fonctionnelle, cela doit être faisable mais à quel prix...
J'espère que cela vous donnera envie de plonger dans le monde la programmation orientée objet et d'aller explorer plus en avant tout le monde des possibles.
Vous voilà libre maintenant d'utiliser ce petit moteur de rendu à votre guise en fonction de vos projets. J'espère que PhpEcho vous rendra des tas de services et qu'il vous aidera à produire du beau code.
Il est évident que PhpEcho est améliorable. Si vous le faites et que vous en avez envie, n'hésitez pas à poster vos upgrades et j'essaierais de vous donner mon avis.
Essayez de privilégier la légèreté dans votre code : cela n'enlève rien à l'aspect fonctionnel. Après en avoir fait le tour, vous viendrez peut-être à vous intéresser à des poids "lourds" du rendu PHP comme ceux cités au début de cet article, cela vous donnera sûrement des pistes d'amélioration.
EDIT 2019-10-21:
Pour résumer la technique : cette classe ne fait ni plus ni moins qu'encapsuler le code de rendu dans un écrin (la classe PhpEcho). Écrin (appelé aussi objet) qui lui apporte des fonctionnalités nouvelles : lecture des valeurs transmises, échappement des caractères, renvoi de texte quand c'est nécessaire...
EDIT 2019-11-25 :
Rajout de la gestion d'un id d'exécution unique pour chaque instance de PhpEcho.
EDIT 2020-03-18 :
Rajout de la possibilité de manipuler et d'échapper :
- les tableaux récursivement, sont échappées les clés et les valeurs.
- les instances de classe implémentant la fonction magique __toString()
PhpEcho pour PHP 5.3+
Pour ceux qui utiliseraient encore une ancienne branche de PHP, voici le code fonctionnel sous PHP 5.3+ :
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 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | <?php namespace rawsrc\PhpEcho; /** * PhpEcho : PHP Template engine : One class to rule them all ;-) * * @link https://www.developpez.net/forums/blogs/32058-rawsrc/b8215/phpecho-moteur-rendu-php-classe-gouverner/ * @author rawsrc - https://www.developpez.net/forums/u32058/rawsrc/ * @copyright MIT License * * Copyright (c) 2020 rawsrc * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ class PhpEcho implements \ArrayAccess { /** * @var string */ private $id = ''; /** * @var array */ private $vars = []; /** * Full resolved filepath to the external view file * @var string */ private $file = ''; /** * @var string */ private $code = ''; /** * @param mixed $file see setFile() below * @param array $vars * @param string $id if empty then auto-generated */ public function __construct($file = '', array $vars = array(), $id = '') { if ($file !== '') { $this->setFile($file); } if ($id === '') { $this->generateId(); } else { $this->id = $id; } $this->vars = $vars; } /** * @param string $id */ public function setId($id) { $this->id = $id; } /** * @return string */ public function id() { return $this->id; } /** * Generate an unique execution id * Always start with a letter */ public function generateId() { $alnum = 'abcdefghiklmnopqrstuvwxyzABCDEFGHIKLMNOPQRSTUVWXYZ0123456789'; $this->id = chr(mt_rand(97, 122)).substr(str_shuffle($alnum), 8); } /** * Interface ArrayAccess * @param mixed $offset * @return bool */ public function offsetExists($offset) { return array_key_exists($offset, $this->vars); } /** * Interface ArrayAccess * @param mixed $offset * @return mixed|null */ public function offsetGet($offset) { return isset($this->vars[$offset]) ? $this->vars[$offset] : null; } /** * Interface ArrayAccess * @param mixed $offset * @param mixed $value */ public function offsetSet($offset, $value) { $this->vars[$offset] = $value; } /** * Interface ArrayAccess * @param mixed $offset */ public function offsetUnset($offset) { unset($this->vars[$offset]); } /** * Define the filepath to the external view file to include * * Rule R001 : Any space inside a name will be automatically converted to DIRECTORY_SEPARATOR * * For strings : $parts = 'www user view login.php'; * - become "www/user/view/login.php" if DIRECTORY_SEPARATOR = '/' * - become "www\user\view\login.php" if DIRECTORY_SEPARATOR = '\' * * For arrays, same rule (R001) for all values inside : $parts = ['www/user', 'view login.php']; * - become "www/user/view/login.php" if DIRECTORY_SEPARATOR = '/' * - become "www/user\view\login.php" if DIRECTORY_SEPARATOR = '\' * * File inclusion remove the inline code * * @param mixed $parts string|array */ public function setFile($parts) { $file = []; $parts = is_string($parts) ? explode(' ', $parts) : $parts; foreach ($parts as $p) { $file[] = str_replace(' ', DIRECTORY_SEPARATOR, $p); } $this->file = str_replace(DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, implode(DIRECTORY_SEPARATOR, $file)); $this->code = ''; } /** * Instead on including an external file, use inline code for the view * * CAREFUL : when you use inline code with dynamic values from the array $vars, you must * be absolutely sure that the values are already defined before, otherwise you will only have empty strings * * Inline code remove the included file * * @param string $code */ public function setCode($code) { $this->code = $code; $this->file = ''; } /** * This function return always escaped value with htmlspecialchars() from the array $vars * * You escape on demand anywhere in your code by calling this class like this : * $this('hsc', 'any value you would like to escape'); * * The key 'hsc' is reserved and if a second value is passed, then the function adapt itself * to that context and return the second value escaped * * @param string $key * @param $value * @return mixed */ public function __invoke($key, $value = null) { $nb = count($args); if (empty($args) || ($nb > 2)) { return ''; } /** * @param $p * @return bool */ $is_scalar = function($p) { return is_scalar($p) || (is_object($p) && method_exists($p, '__toString')); }; /** * @param $p * @return string */ $hsc = function($p) { return htmlspecialchars((string)$p, ENT_QUOTES, 'utf-8'); }; /** * Return an array of escaped values with htmlspecialchars(ENT_QUOTES, 'utf-8') for both keys and values * Works for scalar and array type and transform any object having __toString() function implemented to a escaped string * Otherwise, keep the object as it * * @param array $part * @return array */ $hsc_array = function(array $part) use (&$hsc_array, $hsc, $is_scalar) { $data = []; foreach ($part as $k => $v) { $sk = $hsc($k); if (is_array($v)) { $data[$sk] = $hsc_array($v); } elseif ($is_scalar($v)) { $data[$sk] = $hsc($v); } else { $data[$sk] = $v; } } return $data; }; $value = null; if (($nb === 1) && isset($this->vars[$args[0]])) { $value = $this->vars[$args[0]]; } elseif ($args[0] === 'hsc') { $value = $args[1]; } if ($is_scalar($value)) { return $hsc($value); } elseif (is_array($value)) { return $hsc_array($value); } else { return ''; } } /** * Magic method that returns a string instead of current instance of the class in a string context */ public function __toString() { if (($this->file !== '') && is_file($this->file)) { ob_start(); include $this->file; return ob_get_clean(); } else { return $this->code; } } } // make the class directly available on the global namespace class_alias('rawsrc\PhpEcho\PhpEcho', 'PhpEcho', false); |
Bon code à tous
rawsrc