IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

rawsrc

[Actualité] PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous

Note : 5 votes pour une moyenne de 4,20.
par , 11/10/2019 à 18h42 (4392 Affichages)
Salut les développeurs, après deux billets de blog assez ardus (je dois le reconnaître), aujourd'hui on va aller dans la simplicité et surtout dans une extrême utilité : je vais vous présenter un petit moteur de rendu PHP de ma conception qui est bâti que sur une seule et unique classe : la programmation orientée objet réduite à sa plus simple expression.

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 :

Nom : 2019-10-10_224159.jpg
Affichages : 1358
Taille : 275,4 Ko

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 - Visualiser dans une fenêtre à part
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'    
];
N'oubliez pas que ce tableau se répétera autant de fois que de messages à afficher (tableau de tableaux).

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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
// AVEC ACCÈS PUBLIC
class Foo
{
    public $nom;
}
$foo = new Foo();
$foo->nom = 'rawsrc';
Quand on code en objet, il est rare que l'on laisse les attributs publics. Généralement, il est important de bien savoir ce qui est manipulé et l'utilisation des mutateurs permet d'effectuer des contrôles sur ce qui rentre dans l'instance de la classe.
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 - Visualiser dans une fenêtre à part
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 = ''
Maintenant, si on avait laissé l'attribut public, on aurait l'obligation de faire la vérification sur la contrainte de longueur partout avant d'utiliser le nom :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
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
}
C'est tout simplement ingérable.


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 - Visualiser dans une fenêtre à part
1
2
3
4
5
6
class Foo
{
    private $vars = '';
}
$foo        = new Foo();
$foo['nom'] = 'rawsrc'; // voyez la notation tableau
PHP vous en donne la possibilité à l'unique condition que votre classe implémente l'interface système ArrayAccess, c'est une interface définie dans la documentation PHP qui vous oblige à implémenter 4 fonctions spéciales qui une fois insérées dans le code de votre classe, vont vous permettre de disposer de ce comportement. Génial, non ?
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
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
Maintenant, vous voulez compter le nombre d'éléments définis dans votre instance de classe comme si s'était un tableau, pas de problème, l'interface Countable est là pour ça :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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;
Comme l'orienté objet existe depuis belle lurette au sein de PHP, il a été prévu depuis fort longtemps d'avoir la possibilité d'adapter automatiquement le comportement d'une instance de classe à son contexte d'exécution.
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 - Visualiser dans une fenêtre à part
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
Le moteur de PHP va vérifier si la classe définit la méthode magique __toString() et si oui, va l'appeler automatiquement, ainsi aucune erreur ne sera générée.
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
La classe du moteur de rendu fait massivement appel à cette méthode magique.

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 - Visualiser dans une fenêtre à part
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);
Sans cette fonction magique, notre code aurait produit une erreur fatale :
Fatal error: Uncaught Error: Function name must be a string
Sans vous en rendre compte mais à chaque fois que vous faites new MaClasse, vous utilisez la méthode magique public function __construct(). C'est dire combien vous appréciez son utilité quotidiennement
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 - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
class Foo
{
    private function __construct()
    {
    }
}
$foo = new Foo();
On récupère une belle erreur fatale :
Fatal Error: Call to private Foo::__construct() from invalid context
J'espère que vous commencez à apercevoir l'étendu du monde de l'orienté objet, c'est très vaste.
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 - Visualiser dans une fenêtre à part
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);
La version compatible PHP5.3+ est disponible à la fin de ce billet.


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 - Visualiser dans une fenêtre à part
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; &lt; &gt;'  // notation fonction : valeur échappée, inoffensive, pour le &_quote; c'est sans le _ bien sûr (c'est juste pour l'affichage)
Donc dans votre code HTML, préférez la notation fonction qui sécurise automatiquement les valeurs et ne gardez la notation tableau quand vous êtes absolument certain que les données ont été échappées correctement.
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 - Visualiser dans une fenêtre à part
$z = $engine('hsc', 'une valeur quelconque à échapper');
Le mot clé 'hsc' est réservé et s'il est suivi d'une valeur, alors la classe reconnait ce contexte d'appel et adapte son fonctionnement en appelant la fonction htmlspecialchars().
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
Un fichier index.php à la racine du site :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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>
Dans ce code notez l'utilisation de $this, comme on fait des inclusions, $this représente une instance de la classe PhpEcho, du coup on a à disposition les fonctionnalités offertes par celle-ci (regardez le code source de la fonction __toString() pour bien comprendre l'origine de $this).

NB : Pour les inclusions de fichiers (include, include_once, require, require_once), la doc PHP stipule :
Citation Envoyé par PHP Manuel
Lorsqu'un fichier est inclus, le code le composant hérite de la portée des variables de la ligne où l'inclusion apparaît. Toutes les variables disponibles à cette ligne dans le fichier appelant seront disponibles dans le fichier appelé, à partir de ce point.
Quand on est dans le code d'une classe, l'instance de cette dernière est toujours représentée par la variable $this, donc au moment de la génération du rendu par inclusion de fichier externe et en vertu du fonctionnement interne de PHP, le fichier inclus a à sa disposition un accès à la variable présente avant son inclusion : $this.
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 - Visualiser dans une fenêtre à part
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>
Voyez ce qui est attendu par le formulaire, même raisonnement que précédemment.

Enfin un dernier script Login.php enregistré dans /src qui lui pilote la fonctionnalité : affichage du formulaire de connexion.
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
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;
Ou en version courte Login.php :
Code php : Sélectionner tout - Visualiser dans une fenêtre à part
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'
    ])
]);
Concis, lisible, compréhensible et assez élégant, c'est pas tip-top ?


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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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>
Voyez comment il est possible d'utiliser à bon escient le contexte créé par l'identifiant de chaque instance de PhpEcho. Le fait de définir un id de bloc <div id="<?= $id ?>"> vous met immédiatement à disposition un contexte qui offre des tas de possibilités pratiques.
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 - Visualiser dans une fenêtre à part
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
    );
Si vous poussez le concept jusqu'au bout, il est tout à fait possible avec ce système d'avoir des blocs vue totalement autonomes (contenu, formatage, comportement dynamique).
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 - Visualiser dans une fenêtre à part
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

Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog Viadeo Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog Twitter Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog Google Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog Facebook Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog Digg Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog Delicious Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog MySpace Envoyer le billet « PhpEcho : moteur de rendu PHP ⇒ une classe pour les gouverner tous » dans le blog Yahoo

Mis à jour 15/04/2020 à 09h04 par rawsrc

Catégories
HTML / CSS , PHP , Développement Web

Commentaires

  1. Avatar de Dendrite
    • |
    • permalink
    Bonjour Raw et merci pour le partage.
    Je vais surtout te remercier pour ce bel effort de simplicité. Je comprends 85% de l'article à la première lecture, c'est que c'est gagné.
    Tu as eu entièrement raison de séparer totalement tous les pré-requis puis de nous proposer ton code de classe non truffé de commentaires.
    C'est plus agréable.
    Je découvre grâce à tes préambules que l'on peut implémenter des librairies natives de PHP.
    Je vais tenter d'adopter ta classe pour mon site perso justement, my-memo... Un mini-twig en quelque sorte... Je suis trop contente.
    Et je reviens vers toi dans quelques temps, pour te faire le feedback. Ne t'inquiète pas si c'est long.
  2. Avatar de rawsrc
    • |
    • permalink
    Salut Dendrite

    85% de compris à la première lecture ! Ouf (pour moi)
    En tout cas, ça fait plaisir à lire car pour en arriver à ce score, j'ai mis plus d'une journée de rédaction et de mise en forme.
    C'est pas évident de se mettre à la place d'un débutant derrière son clavier surtout quand tu ne sais pas où les difficultés se situent.
    Je ne vois pas encore les têtes des lecteurs derrière leur écran à la lecture de tel ou tel paragraphe

    C'est parfait si tu l'utilises pour un petit projet perso.

    De mon côté et pour la petite histoire, c'est une version très simplifiée d'un moteur de rendu que j'ai créé en 2009 et qui s'appelle Rerender. Il offre des fonctionnalités aussi simples que celles de PhpEcho à côté d'autres beaucoup plus avancées comme : la gestion de widgets, de contexte d'exécution, l'injection dynamique et la création de helpers, dérivation de template... Comme pour PhpEcho, aucune nouvelle syntaxe à apprendre, juste du PHP basique (tout orienté objet, il va de soi) et le tout tient en 10 fichiers .php
  3. Avatar de moimp
    • |
    • permalink
    Bonjour à tous,

    Tout d'abord merci pour ce travail.

    Malheureusement, on sent que ce billet a été écrit "à la va vite". En effet, tu ne définis pas ce qu'est un moteur de rendu. Tu ne fais pas allusion à la structure MVC et il faut entrer dans le détail pour savoir que cet article peut nous intéresser. J'ai également vu au passage des imprécisions et des fautes de Français.
    A ceci près, je ne suis pas encore aller au bout mais ce que j'ai vu m'intéresse.
    A l'occasion, j'approfondirai et te ferai une critique plus précise et plus constructive. Faut-il te les envoyer en commentaires ou en messages privés?
    Mis à jour 12/10/2019 à 19h25 par moimp
  4. Avatar de rawsrc
    • |
    • permalink
    Salut moimp

    je trouve ta critique sur le MVC infondée, tu peux tout à fait utiliser PhpEcho en dehors du l'approche MVC. Dans notre exemple on isole la vue du reste, mais ce n'est nullement obligatoire, tu peux très bien tout mélanger. Ce n'est pas conseillé, je te l'accorde mais cela n'est pas interdit. Personnellement, je sépare naturellement les éléments quand je code, c'est sans réfléchir comme un réflexe. Alors je n'ai pas trop prêté attention. C'est une très bonne approche pour survivre dans la programmation : Diviser pour mieux régner.

    Pour les autres critiques, poste tout ici même.

    Et enfin pour ce qui est du MVC, je t'invite à visiter mon blog DVP, tu y trouveras du grain à moudre
  5. Avatar de laurentSc
    • |
    • permalink
    Bonsoir à tous,

    merci pour ton boulot rawsrc.

    d'abord, amusant la remarque de moimp sur les fautes de français car quand il écrit "je ne suis pas encore aller au bout", y a aussi une faute ! ("allé", pas "aller")...

    Bon, je viens de terminer ma première lecture. Je compte en faire une 2e demain, puis d'essayer d'utiliser PhpEcho. Mais merci pour ce billet clair car mon niveau de compréhension est à peu près équivalent à celui de Dendrite. Et même remarque : je découvre l'implémentation de classes natives ; très instructif.
    Mis à jour 15/10/2019 à 11h19 par laurentSc
  6. Avatar de moimp
    • |
    • permalink
    Pour laurentSc, tu as raison, je ne suis pas exempt, non plus. Compte tenu de tes remarques et des erreurs déjà corrigées, mes suggestions deviennent en grande partie du pinaillage. Je les donne quand même:

    Salut les développeurs, après deux billets de blog assez ardus (je dois le reconnaître), aujourd'hui on va aller dans la simplicité et surtout dans une extrême utilité : je vais vous présenter un petit moteur de rendu PHP (j'aurais précisé ce que c’est ?) de ma conception qui est bâti que sur une seule et unique classe : la programmation orientée objet réduite à sa plus simple expression.



    Pour suivre notre exemple, il est tout à fait sensé à ce que la vue en charge de rendre fournir un message attende un tableau de valeurs de ce genre :



    ne tolère aucun oubli dans les échappements : J'aurais précisé htmlspecialchars.



    La contrepartie c'est quoi qu'on en dise, En contrepartie, quoi qu’on en dise,



    il est rare que l'on qu'on laisse les attributs publics.



    Maintenant, vous voulez compter le nombre d'éléments définis dans votre instance de classe comme si s'était un tableau, pas de problème, l'interface Countable est là pour ça :



    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



    Dans ce code notez l'utilisation de $this, comme on fait des inclusions, $this représente une instance de la classe PhpEcho, du coup on a à disposition les fonctionnalités offertes par celle-ci (regardez le code source de la fonction __toString() pour bien comprendre l'origine de $this).
    Là, malgré tes explications, je ne suis pas sûr de bien comprendre en quoi le fait de faire une inclusion, permet de récupérer le texte de l’instance. Si tu peux, il serait peut-être nécessaire de préciser davantage.



    $body = new PhpEcho([DIR_ROOT, 'view LoginForm.php']
    Il y a une erreur (espace entre view et LoginForm.php)


    De toute façon, merci encore pour cet excellent boulot. Je pense que je vais utiliser ton travail.
  7. Avatar de rawsrc
    • |
    • permalink
    Salut moimp,

    Citation Envoyé par moimp
    Salut les développeurs, après deux billets de blog assez ardus (je dois le reconnaître), aujourd'hui on va aller dans la simplicité et surtout dans une extrême utilité : je vais vous présenter un petit moteur de rendu PHP (j'aurais précisé ce que c’est ?) de ma conception qui est bâti que sur une seule et unique classe : la programmation orientée objet réduite à sa plus simple expression.
    Comme c'est une introduction, je n'allais pas partir dans des explications techniques qui sont abordées de toute façon juste après.

    Citation Envoyé par moimp
    ne tolère aucun oubli dans les échappements : J'aurais précisé htmlspecialchars.
    htmlspecialschars() ou htmlentities ou urlencode ou rawurlencode... des échappements il en existe un paquet.

    Citation Envoyé par moimp
    il est rare que l'on qu'on laisse les attributs publics.
    Euphonie.

    Citation Envoyé par moimp
    Dans ce code notez l'utilisation de $this, comme on fait des inclusions, $this représente une instance de la classe PhpEcho, du coup on a à disposition les fonctionnalités offertes par celle-ci (regardez le code source de la fonction __toString() pour bien comprendre l'origine de $this).
    Là, malgré tes explications, je ne suis pas sûr de bien comprendre en quoi le fait de faire une inclusion, permet de récupérer le texte de l’instance. Si tu peux, il serait peut-être nécessaire de préciser davantage.
    Ok, je vais expliquer un peu plus en détail.

    Citation Envoyé par moimp
    $body = new PhpEcho([DIR_ROOT, 'view LoginForm.php']
    Il y a une erreur (espace entre view et LoginForm.php)
    Non c'est totalement voulu, regarde le code de la méthode setFile(), les espaces sont automatiquement transformés en DIRECTORY_SEPARATOR. Lis aussi la tartine PhpDoc juste au-dessus.

    J'ai corrigé les fautes et autres étourderies. Merci.

    Citation Envoyé par moimp
    De toute façon, merci encore pour cet excellent boulot. Je pense que je vais utiliser ton travail.
    Bon courage donc
  8. Avatar de rawsrc
    • |
    • permalink
    Salut,

    J'ai rajouté une version de la classe compatible avec l'ancienne branche PHP 5.3+ pour les nostalgiques
  9. Avatar de rawsrc
    • |
    • permalink
    Bonjour,

    j'ai publié le code source de PhpEcho sur mon espace Github,

    Vous noterez que j'ai légèrement amélioré la version en y ajoutant la possibilité de renvoyer directement du code html sans passer par l'inclusion d'un fichier.
    Je mettrai à jour ce billet avec cette nouvelle fonctionnalité demain.

    rawsrc
    Mis à jour 21/10/2019 à 09h46 par rawsrc
  10. Avatar de rawsrc
    • |
    • permalink
    Bonjour,

    Mise à jour du billet terminée : code source mis à jour et pour les explications, le paragraphe 6.1 - CODE DE RENDU SANS L'INCLUSION DE FICHIER EXTERNE a été ajouté.

    rawsrc
  11. Avatar de rawsrc
    • |
    • permalink
    Bonjour,

    Code source de PhpEcho mis à jour avec la nouvelle fonctionnalité : auto-génération et/ou définition d'un id d'exécution unique pour chaque instance de bloc.
    Pour les explications et un exemple concret d'utilisation, veuillez vous reporter au paragraphe 6.2.

    Bon code à tous
  12. Avatar de rawsrc
    • |
    • permalink
    Bonjour,

    j'ai procédé à la mise à jour du code source de PHPEcho, j'y ai rajouté quelques fonctionnalités : en particulier la possibilité de manipuler et d'échapper nativement les tableaux récursivement (clés et valeurs) ainsi que la possibilité d'échapper et d'afficher toute instance de classe implémentant la méthode magique __toString().

    Concrètement cela vous ouvre la possibilité de faire ce genre de chose :
    - Les tableaux :
    Code php : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    $body = new PhpEcho('', [
        'url_submit' => '/index.php?page=loginsubmit',
        'login'      => 'rawsrc',
        'data'       => ['"', '<', '>', "'"]   // ici on stocke un tableau
    ]);
    puis on l'affiche de manière sécurisée :
    Code php : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    foreach ($body('data') as $k => $v) {   // l'appel à $body('data') renvoie un tableau avec tout le contenu échappé récursivement (clés et valeurs)
        echo "<p>{$v}</p>";   // ici $v est bien échappé
    }

    - les classes implémentant __toString() :
    Code php : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php
     
    class Foo
    {
        public function __toString()
        {
            return 'renvoi de données dangereuses : " < >';
        }
    }
     
    $body = new PhpEcho('', [
        'cls' => new Foo()   // ici on stocke un objet implémentant __toString()
    ]);
     
    echo $body('cls'); // affiche : "renvoi de données dangereuses : &_quot; &lt; &gt;"
    Mis à jour 19/03/2020 à 13h05 par rawsrc