1. DateTime
Il est quasiment impossible dans un projet PHP un peu conséquent de ne pas avoir un jour à manipuler des dates.
La difficulté d'apprentissage peut être croissante. Au début, on souhaite uniquement afficher la date du jour au format jj/mm/AAAA ; puis, on cherche à estimer le nombre de jours entre deux dates ; enfin, on cherche à parser des dates.
Et on se rend vite compte que la fonction PHP date() ne suffit pas réellement à faire ce que l'on souhaite.
En général, la classe native \DateTime suffit à résoudre les problèmes les plus communs, je vous invite à consulter
cette page-ci où je donne quelques exemples d'utilisation de celle-ci.
1.1. Limites de la classe DateTime
Le problème de la classe native \DateTime est que certains méthodes basiques ont l'air de manquer.
Par exemple, des choses pratiques comme getYear, setDay ou encore des alias du type isoFormat (équivalent au format('Y-m-d')).
Deux solutions s'offrent donc à nous :
* la première est de surcharger la classe DateTime avec de nouvelles méthodes.
* la seconde serait d'utiliser un vendor comme Chronos/Carbon/Moment qui peut répondre à des besoins plus poussés.
1.1.1 Les vendor DateTime
Beaucoup de composants PHP populaires se retrouvent sur la page
Github Awesome PHP(https://github.com/ziadoz/awesome-php).
Rien ne permet d'être convaincu que ces vendors sont meilleurs que d'autres, mais leur popularité fait qu'en cas de problème, plus de monde pourra aider.
Voici la liste des vendors orientés manipulation de Date à la mi-2021 :
* CalendR - A calendar management library.
* Carbon - A simple DateTime API extension.
* Chronos - A DateTime API extension supporting both mutable and immutable date/time.
* Moment.php - Moment.js inspired PHP DateTime handler with i18n support.
* Yasumi - An library to help you calculate the dates and names of holidays.
Chronos utilise un trait nommé MagicPropertyTrait qui permet d'avoir des méthodes comme getYear / getMonth / getDay. Il utilise également le trait ModifierTrait pour ajouter des méthodes comme hour/year/month/addYear/addYears...
La méthode toDateString existe également.
Moment.php fournit également des méthodes getYear / getMonth / getDay et également des méthodes comme addDays, subtractDays. On peut également faire un ->format(Moment::NO_TIME).
Ces deux vendors sont au coude à coude concernant la popularité, à la différence que Chronos est maintenu par l'équipe derrière CakePHP, un framework assez connu, on imagine donc que les développeurs ont un peu de bouteille.
Moment est compatible dès PHP5 alors que Chronos nécessite au minimum PHP 7.2.
🧙♂️️Chronos l'emporte selon moi.
1.1.2 La classe surchargeant DateTime
💡️Ne pas réinventer la roue.
Il est possible de créer une classe surchargeant la classe \DateTime et de l'utiliser à travers son projet.
Il y a deux avantages certains à faire sa propre classe :
* On ne dépend pas d'un vendor et donc de ces mises à jour ;
* Le projet n'embarque pas du code inutile, on utilise assez rarement toutes les méthodes d'un vendor ;
* Le code correspond parfaitement au besoin, et fait le nécessaire ;
Il y a par contre au moins trois inconvénients :
* On perd du temps à développer et maintenir du code technique (vs code fonctionnel/métier/business) ;
* Si le projet n'est pas open source, on ne profite pas des revues de code de la communauté ;
* Il faut que les développeurs de l'équipe apprennent à utiliser la "couche maison", c'est simple quand il s'agit dun getDay, mais cela peut être plus compliqué avec une méthode qui serait nommée "processBuffstream()" : à moins d'être omniscient, il faut forcément aller voir la mécanique interne ou faire des copiers/collers de ce qui existe déjà, ou encore (dans un monde idéal) : lire la documentation faite par le développeur.
2. Twig Vs Smarty 2
Comparer SmartTPL, Smarty 2 et Twig, c'est comme comparer CVS, SVN et GIT.
On passe pour ainsi dire de l'âge de pierre à l'âge de fer. Nous ne sommes surement pas encore à l'âge d'or.
La grande plus-value du Twig par rapport à Smarty 2 est surement l'héritage entre les templates, évitant non seulement du copier coller entre les différentes pages mais surtout une souplesse bien agréable. On peut éventuellement imaginer que les feuilles de style sont importées dans un fichier dédié et éventuellement overrideraient ce fichier dans une page spéciale du site (ex: une page spéciale Halloween où la page aurait des tons oranges/noirs).
2.1. Twig vs Smarty 3
Le fossé entre Twig et Smarty 3 est moins large qu'entre Twig et Smarty 2. On peut faire globalement la même chose mais la syntaxe de Twig reste plus agréable (même si elle peut rentrer en conflit avec des frameworks JS qui utilisent les doubles moustaches).
Smarty 3 a introduit la fonctionnalité d'héritage, rattrapant son retard. Mais il ne font guère mieux que l'original en copiant jusqu'à la syntaxe :
{block name=title}My Page Title{/block}
Prestashop est actuellement le projet le plus ambitieux utilisant Smarty, à voir dans les années qui viennent s'ils décident ou non de migrer chez la concurrence, ce qui n'est pas à exclure s'ils cherchent un jour à utiliser plus que quelques composants de Symfony 3.4.
3. PhpStorm
3.1. Empecher l'insertion automatique des guillemets simples ou doubles
Editor > Smart Keys
Décocher "Insert pair quote"
📖️️https://www.jetbrains.com/help/phpstorm/completing-punctuation.html
4. Résumé de "Clean Code" par Robert C. Martin
📖️️Traduction de https://gist.github.com/cedrickchee/55ecfbaac643bf0c24da6874bf4feb08(https://gist.github.com/cedrickchee/55ecfbaac643bf0c24da6874bf4feb08)
Un résumé des principales idées du livre "Clean Code : A Handbook of Agile Software Craftsmanship" de Robert C. Martin (alias Oncle Bob).
Le code est propre s'il peut être compris facilement - par tous les membres de l'équipe. Un code propre peut être lu et amélioré par un développeur autre que son auteur original. Avec la compréhensibilité viennent la lisibilité, la modifiabilité, l'extensibilité et la maintenabilité.
4.1. Règles générales
4.1.1 Suivez les conventions
* Restez simple et stupide. Le plus simple est toujours le mieux. Réduisez la complexité autant que possible.
* Règle des scouts. Laissez le terrain de camping plus propre que vous ne l'avez trouvé.
* Toujours trouver la cause profonde. Cherchez toujours la cause profonde d'un problème.
* Suivez le principe de la moindre surprise.
* Ne vous répétez pas (DRY).
* Ne pas passer outre les sécurités.
4.2. Règles de conception
* Gardez les données configurables (ex. : constantes) à un niveau élevé. Elles doivent être faciles à modifier.
* Préférez le polymorphisme à if/else ou switch/case.
* Séparer le code multithreading.
* Empêcher la sur-configurabilité.
* Utiliser l'injection de dépendances.
* Suivre la loi de Déméter. Une classe ne doit connaître que ses dépendances directes.
4.3. Conseils de compréhensibilité
* Soyez cohérent (consistent). Si vous faites quelque chose d'une certaine manière, faites toutes les choses similaires de la même manière.
* Utilisez des noms de variables explicites.
* Encapsulez les conditions limites. Il est difficile de garder une trace des conditions limites. Mettez le traitement de ces conditions à un seul endroit.
* Préférez les objets aux types primitifs.
* Évitez les dépendances logiques. N'écrivez pas de méthodes qui fonctionnent correctement en fonction de quelque chose d'autre dans la même classe.
* Évitez les conditions négatives.
4.4. Règles relatives aux noms
* Choisissez des noms descriptifs et non ambigus.
* Faites des distinctions significatives.
* Utilisez des noms prononçables.
* Utilisez des noms faciles à chercher (évitez les noms très courts, très récurrents).
* Remplacer les nombres magiques par des constantes nommées.
* Évitez les codages. Ne pas ajouter de préfixes ou d'informations de type.
4.5. Règles de fonctions
* Petites.
* Elles ne font qu'une chose et doivent la faire bien.
* Utilisez des noms descriptifs.
* Préférez moins d'arguments. Pas plus de 2 arguments si possible.
* N'ont pas d'effets de bord (ie. se limite à ce qu'elles sont censées faire).
* Ne pas utiliser d'arguments de type "
flag(https://martinfowler.com/bliki/FlagArgument.html)". Diviser la méthode en plusieurs méthodes indépendantes qui peuvent être appelées depuis le client sans ce type d'argument.
4.6. Règles de commentaires
Essayez toujours de vous expliquer dans le code. Si ce n'est pas possible, prenez votre temps pour écrire un bon commentaire.
* Ne soyez pas redondant (par exemple : i++ ; // incrémente i).
* N'ajoutez pas de commentaires évidents. (par exemple: ->save() // sauvegarde la donnée).
* N'utilisez pas d'accolades fermantes (par exemple : } // fin de fonction).
* Ne commentez pas le code mort. Supprimez-le plutôt.
* Utiliser les commentaires pour expliquer l'intention.
* Utiliser les commentaires pour clarifier le code.
* Utiliser les commentaires pour avertir des conséquences.
4.7. Structure du code source
* "Séparez les concepts verticalement."
* "Le code connexe doit apparaître verticalement dense."
* Déclarez les variables à proximité de leur utilisation.
* Les fonctions dépendantes doivent être proches.
* Les fonctions similaires doivent être proches.
* Placez les fonctions dans le sens de la descente.
* Gardez les lignes courtes.
* N'utilisez pas l'alignement horizontal.
* Utilisez les espaces blancs pour associer des éléments connexes et dissocier des éléments faiblement connexes.
* Ne rompez pas l'indentation.
4.8. Objets et structures de données
* Cachez la structure interne.
* Préférez les structures de données.
* Évitez les structures hybrides (moitié objet et moitié données).
* Doivent être petites.
* Ne font qu'une seule chose.
* Un petit nombre de variables d'instance. Si votre classe a trop de variables d'instance, alors elle fait probablement plus d'une chose.
* La classe de base ne doit rien savoir de ses dérivés.
* Mieux vaut avoir plusieurs fonctions que de passer du code dans une fonction pour sélectionner un comportement.
* Préférez les méthodes non statiques aux méthodes statiques.
4.9. Tests
* Une assertion par test.
* Rapide.
* Indépendant.
* Répétable.
* Auto-validation.
* Rapide.
* Lisible.
* Facile à exécuter.
* Utilisez un outil de couverture.
4.10. Senteurs du code
* Rigidité. Le logiciel est difficile à modifier. Une petite modification entraîne une cascade de modifications ultérieures.
* Fragilité. Le logiciel se casse en de nombreux endroits à cause d'une seule modification.
* Immobilité. Vous ne pouvez pas réutiliser des parties du code dans d'autres projets en raison des risques encourus et de l'effort élevé.
* Complexité inutile.
* Répétition inutile.
* Opacité. Le code est difficile à comprendre.
4.11. Gestion des erreurs
Ne mélangez pas la gestion des erreurs et le code.
Utilisez des exceptions au lieu de renvoyer des codes d'erreur.
Ne retournez pas null, ne passez pas null non plus.
Lancez les exceptions avec le contexte.
5. Code Smell
📖️️Lire ceci directement : https://fr.wikipedia.org/wiki/Code_smell
6. Les codes à éviter
Lorsqu'on débute en PHP, on commence par écrire du code en suivant les mauvaises pratiques.
La plupart du temps, ce code est un héritage de celui qu'on pouvait écrire en 2005. A cette époque, le PHP balbutiait encore. En 2020, ce code ne devrait plus exister, car il est toujours possible de suivre les bonnes pratiques même avec du PHP procédural.
6.1. Utiliser le or die
On rencontre l'utilisation du 'or die' lorsque le développeur copie/colle un exemple d'utilisation du mysql_connect. On retrouve alors ce genre de ligne :
$bd = mysql_connect($nomserveur, $login, $pass) or die("Connexion échouée");
6.1.1 Pourquoi il ne faut pas utiliser le or die
Il n'est pas conseillé de forcer l'arrêt de l'interpréteur PHP.
Dans le cas d'une communication entre un serveur et un client, le client, lorsqu'il fait une requête HTTP s'attend à avoir une réponse du serveur. Ici, il reçoit la chaine de caractère "Connexion échouée". Non seulement le client ne devrait pas connaitre ce genre d'information, mais en plus il ne peut pas savoir ce qu'il doit en faire.
6.1.2 Ce qu'il faut faire
Il faut :
1. arrêter d'utiliser le die (sauf éventuellement pour débugguer) ;
2. utiliser le retour du mysql_connect pour gérer l'erreur de façon plus censée :
2.1 mettre à jour le journal d'évenement (log) en indiquant l'erreur (date/ip/contexte) ;
2.2 retourner une erreur au client dans un format structuré (ex: json) associée à un code d'erreur HTTP, ici cela peut être le code 500 puisque l'erreur vient manifestement du serveur WEB.
6.2. Utiliser un die/exit dans une fonction
Ce type de code introduit un danger :
function connect_bd(nomserveur, $login, $pass)
{
$db = mysql_connect($nomserveur, $login, $pass);
if (!$db) {
die("Connexion échouée");
}
return $db;
}
6.2.1 Pourquoi ?
Utiliser un exit ou un die dans une fonction rend complexe la gestion des erreurs.
6.2.2 Alternative
On peut lancer une exception au niveau de la fonction afin de récupérer plus proprement l'erreur.
Voici un exemple :
function connect_bd(nomserveur, $login, $pass)
{
$db = mysql_connect($nomserveur, $login, $pass);
if (!$db) {
throw new \Exception("Connexion échouée");
}
return $db;
}
Note : \Exception peut ici être remplacée par une classe plus spécifique (ex: "MySQLException").
6.3. Utiliser le tag ?>
Lorsque le tag PHP est fermé, tout ce qui le suit est tout de même retourné par le serveur WEB.
C'est-à-dire que des caractères blancs supplémentaires peuvent être retournés et interprétés par le navigateur comme du HTML.
Or, le HTML, c'est avant tout une syntaxe à base de balises à respecter, et quand on utilise le ?>, on ne contrôle plus ce qui est retourné par le serveur WEB. Dans la grande majorité des cas, cela est sans conséquence ; le résultat est interprété comme du HTML invalide et la page s'affiche tout de même. Dans la minorité des cas, cela a de graves conséquences ; cela peut entrainer des problèmes d'affichage (dans le cas où un <pre/> a été ouvert par exemple) ou même des erreurs JS du côté du client (le JS pense recevoir un objet JSON et il reçoit des caractères supplémentaires).
6.3.1 Ce qu'il faut faire
Il ne faut pas utiliser le tag ?>. Le serveur doit interpréter les espaces et les retours à la ligne comme du code PHP.
Il faut respecter les deux règles suivantes :
1. Ne jamais mélanger du HTML et du PHP dans le même fichier ;
2. N'utiliser qu'une et une seule fois le tag <?php par fichier .php ;
6.4. Ne pas espacer les symboles +,*,-,/,=
C'est surtout une question de confort de lecture et de norme de codage.
6.5. Mélanger du HTML et du PHP dans le même fichier
Le net regorge d'exemples où dans un fichier .PHP, on ouvre et on ferme des balises PHP pour écrire du HTML.
Si l'on souhaite construire plusieurs modèles de page selon le site web, autant isoler dès le départ les morceaux de templates dans des fichiers séparés.
6.6. Utiliser le double égal '=='
Alors, le double égal ne prend pas en compte le typage fort et ça c'est un problème.
Car toutes ces choses là retourne TRUE :
<?php
0 == false;
1 == 1.0;
true == 'chataigne';
!null == true;
Ceci peut génèrer des anomalies. En pratique, on compare une donnée qui vient de la base de données, d'un système de cache ou d'un formulaire avec une valeur en dur dans le code, et c'est souvent un drame car le code peut avoir un comportement non prévu par la suite si le test retourne soit disant TRUE.
6.6.1 Ce qu'il faut faire
Utiliser le triple égal '==='.
6.7. Utiliser un God Object
Copie de la page Wikipedia :
God object(https://fr.wikipedia.org/wiki/God_object)
Un God object est, dans le domaine de la programmation orientée objet, un objet qui reconnaît trop de choses ou fait trop de choses. Le god object est un exemple d'antipattern (ou anti-patron).
Le principe général de la programmation structurée est de s'attaquer à un problème important en le divisant en plus petits problèmes à résoudre (stratégie de diviser pour régner). Une fois chacun des petits problèmes résolus, le problème général est automatiquement réglé. Ainsi, il n'y a qu'un objet auquel il doit être connu ou renseigné : lui-même. De la même façon, il n'y a qu'un seul ensemble de problèmes auquel un objet doit se confronter : l'ensemble des siens propres.
La programmation « god object » ne suit pas cette approche. Au lieu de cela, la plus grande partie du programme consiste en un seul bloc qui est renseigné sur tout et maintient constamment à jour données ou informations sur le programme, et fournit la plupart des fonctions et des algorithmes qui utilisent ces données. Du fait que cet objet supporte et organise tellement d'informations à lui seul, il joue un rôle identique à celui d'un dieu. Au lieu de blocs de programme communicant indépendamment entre eux et sans intermédiaire, les autres parties du programme sont dépendants du god object pour communiquer et prendre leurs informations. Comme le god object est référencé par tout le reste de la programmation, la maintenance de celle-ci devient très difficile, y compris dans les plus ordonnés des programmes.
Un god object est la version « orientée-objet » de l'incapacité à concevoir correctement les sous-programmes dans un langage de programmation procédural, ou d'utiliser trop de variables globales pour y stocker des informations sur l'état du programme à un moment donné (comme les drapeaux).
Bien que la création d'un god object soit considérée comme une mauvaise pratique de programmation, cette technique est à l'occasion utilisée dans les environnements critiques de programmation (comme les microcontrôleurs), où le gain dans la vitesse d'exécution et la centralisation du contrôle sont des facteurs plus importants que la facilité de maintenance et l'élégance de la programmation.
6.8. Dupliquer du code
Copie de la page Wikipedia :
Duplication de code(https://fr.wikipedia.org/wiki/Duplication_de_code)
La duplication de code en programmation informatique est une erreur courante de conception de logiciels où une suite d'instructions similaires (voire identiques) existe en plusieurs endroits du code source d'un logiciel.
La duplication de code arrive à la suite de la programmation par copier-coller. C'est une erreur classique de débutants en programmation informatique ; cependant, cette erreur touche également les développeurs confirmés.
Le code dupliqué pose des problèmes de maintenance dont l'importance augmente avec la quantité de code dupliqué. Plus le code est dupliqué, plus il y a de code à maintenir. Par exemple :
si le code copié a une erreur de programmation, cette erreur se répète à chaque copier-coller, et corriger cette erreur nécessite de modifier tous les endroits où le code est dupliqué ;
si le code dupliqué doit évoluer, il faut alors modifier tous les endroits où le code est dupliqué pour y parvenir ;
un code dupliqué peut masquer des différences minimes, mais essentielles, qui existent avec une autre portion de code similaire.
L'antipattern correspondant est l'erreur de copier/coller : Il s'agit d'un copier-coller de code, où le code collé n'a pas été adapté à la portion de code environnante ce qui entraîne des incohérences. Cela arrive généralement à cause d'un défaut de vérification de la part du programmeur. La meilleure solution étant de factoriser les parties communes au lieu de les dupliquer.
La technique permettant d'éviter la duplication de code est la factorisation du code et celle permettant de s'en débarrasser est le réusinage du code.
Test Driven Development est un processus de développement permettant d'éviter le code dupliqué tout en disposant d'un code bien testé.
DRY (Don't repeat yourself! en anglais, ou Ne vous répétez pas) est un principe de programmation encourageant à éviter la duplication de code (dans laquelle le programmeur se répète).
Des logiciels tels PMD, permettent d'identifier le code dupliqué dans une base de code.
6.8.1 Utiliser la règle de trois
Copie de la page Wikipedia :
Règle de trois(https://fr.wikipedia.org/wiki/R%C3%A8gle_de_trois_(programmation_informatique))
La règle de trois est une règle empirique de refactorisation de code pour décider quand des morceaux de code similaires doivent être refactorisés pour éviter la duplication de code. Cette règle indique que deux instances de code similaire ne nécessitent pas de refactorisation, mais lorsqu'un code similaire est utilisé trois fois, il doit être extrait dans une nouvelle procédure. La règle a été popularisée par Martin Fowler dans Refactoring 1 et attribuée à Don Roberts.
La duplication est considérée comme une mauvaise pratique en programmation car elle rend le code plus difficile à maintenir. Lorsque la règle codée dans un morceau de code répliqué change, celui qui gère le code devra le changer correctement à tous les endroits.
Cependant, le choix d'une conception appropriée pour éviter la duplication pourrait bénéficier de plus d'exemples pour voir les modèles. Tenter une refactorisation prématurée risque de sélectionner une mauvaise abstraction, ce qui peut entraîner un code pire lorsque de nouvelles exigences émergent 2 et devront éventuellement être à nouveau refactorisées.
La règle implique que le coût de la maintenance l'emporte certainement sur le coût de la refactorisation et une mauvaise conception potentielle lorsqu'il y a trois copies, et peut-être ou non s'il n'y a que deux copies.
7. Les conventions de nommage
C'est le genre de sujet qui fait couler beaucoup de sang entre pas mal de développeurs, c'est un peu dommage car ce serait plus intéressant qu'ils débattent plutôt à propos des RFC, histoire que la controverse s'élève au dessus des préférences personnelles.
Le PHP est surement encore décrié car le langage lui même n'a pas respecté à ses débuts de convention de nommage, notamment au niveau de ses fonctions.
On retrouve ainsi des noms de fonctions comme strtolower (flatcase), str_pos (snake_case) et d'autres curiosités comme bin2hex (le 2 voulant dire to) et cal_to_jd
Concernant le nommage des classes (PascalCase) et de leurs méthodes (camelCase), rien à signaler.
7.1. camelCase
Il est souvent confondu avec le PascalCase alors que la première lettre du premier mot doit être en majuscule.
Le camelCase est utilisé en PHP pour le nom des méthodes de classe.
Tous les mots sont attachés, la première lettre de chaque mot dès le deuxième mot est en majuscule.
Exemple: doSomething
7.2. PascalCase
Le PascalCase ou StudlyCaps est actuellement la convention de nommage à utiliser pour le nom des classes en PHP.
Tous les mots sont attachés, la première lettre de chaque mot est en majuscule.
Exemple : ChickenIterator : nom de classe composé des deux mots chicken et iterator.
7.3. snake_case
Le snake_case est utilisé dans certains noms de fonctions natives de PHP.
Tous les mots sont séparés par un underscore, les lettres de chaque mot sont en minuscule.
Exemple : chicken_iterator
7.4. kebab-case
Le kebab-case n'est pas utilisé en PHP, mais est utilisé en HTML pour le nommage des classes ou dans le nom des URLs.
Tous les mots sont séparés par un tiret, les lettres de chaque mot sont en minuscule.
Exemple : chicken-iterator
7.5. flatcase
Tous les mots sont attachés, les lettres qui les composent sont en minuscule.
Exemple: strtolower
# UPPER_CASE
L'UPPER_CASE est utilisé en PHP pour les noms des constantes.
Les mots sont séparés par des underscore, les lettres qui les composent sont en majuscule.
Pendant un moment, c'était aussi conventionnel de nommer les colonnes de ses bases de données en UPPER_CASE. Cela dérive actuellement plutôt vers du snake_case.
Exemple : STR_PAD_RIGHT
7.6. WikiCase
Rarement entendu autour d'une table, c'est du PascalCase où il est imposé que chaque lettre majuscule ne soit jamais suivie par une autre lettre majuscule.
Ex: CreateAJob est interdit et doit être remplacé par CreateJob
8. Les normes
Voici les normes que le projet doit suivre, exemples à l'appui.
On adaptera bien sûr les règles aux contraintes de la version de PHP utilisée sur les serveurs de production.
8.1. Exemple d'une classe
<?php
namespace App\Feature;
use App\Service;
class MyClass implements MyInterface
{
public function myMethod(): bool
{
if () {
}
while() {
}
do {
} while ();
return true;
}
}
Plusieurs règles sont à respecter :
* l'emplacement des accolades
** à la ligne pour la déclaration de la classe et de ses méthodes
** même ligne pour les conditions
* l'emplacement des espaces :
** entre le if/do/while/for/foreach/switch et les parenthèses
* l'emplacement des retours à la ligne :
** retour à la ligne avant le return/if/while/do/switch
* le nom des classes : en StudlyCase (PSR-1)
* le nom des méthodes : en camelCase (PSR-1)
* la visibilité la moins permissive possible est toujours indiquée, que ce soit pour les méthodes/constantes/attributs
* le type d'attribut/argument est toujours indiqué, le type mixed est interdit
* le type de retour des méthodes est toujours indiqué, même le void, le type mixed est interdit
* utiliser le mot clef static partout où cela est possible
* toute classe doit implémenter une interface directement ou par héritage
* toujours utiliser le cas default dans un switch/match
8.2. Plus de code procédural
Mis à part celui de l'autoload, le projet doit être codé en full POO :
* Plus de fonctions seules : on n'utilise que des méthodes de classes.
* Ne pas utiliser de variable globales mise à part les superglobales
8.3. Utilisation des superglobales
Les superglobales ($GLOBALS, $_SERVER, $_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, $_REQUEST, $_ENV) ne doivent être utilisées qu'au plus une et une seule fois dans le projet.
Il est strictement interdit d'en modifier directement la valeur.
8.4. Templating
Ne pas écrire de HTML dans un fichier PHP, utiliser un moteur de template ou créer sa propre solution.
Ne pas utiliser de fonctions natives de sortie standard plus d'une fois dans l'ensemble du projet.
8.5. Fonctions natives PHP
Autant que faire ce peut, ne pas utiliser plus d'une fois les fonctions natives de PHP dont le rôle est très précis (ex: htmlentities, json_decode...) ou dont le nombre d'arguments facultatifs est important.
Les encapsuler si besoin en définissant des classes à responsabilité unique (ex: class Templater, méthode escape($htmlContent)).
Bien sûr, le but n'est pas de faire une classe par fonction PHP...
9. Acronymes de bonnes pratiques
9.1. SOLID
* Single responsibility principle (Responsabilité unique)
* Open/closed principle (Ouvert/fermé )
* Liskov substitution principle (Substitution de Liskov )
* Interface segregation principle (Ségrégation des interfaces)
* Dependency inversion principle (Inversion des dépendances)
9.2. KISS
Keep It Simple, Stupid (garde ça simple, idiot)
🧙♂️️Cette phrase peut être détournée et utilisée pour des motifs non valables.
Le KISS concerne de mon point de vue uniquement la facilité d'utilisation du logiciel, et non sa conception technique.
9.3. DRY
Don't Repeat Yourself (Ne vous répétez pas)
📖️️Wikipédia : https://fr.wikipedia.org/wiki/Ne_vous_répétez_pas(https://fr.wikipedia.org/wiki/Ne_vous_r%C3%A9p%C3%A9tez_pas)
9.4. MoSCoW
* Must have this (doit être fait)
* Should have this if at all possible (devrait être fait dans la mesure du possible)
* Could have this if it does not affect anything else (pourrait être fait dans la mesure où cela n'a pas d'impact sur les autres tâches)
* Won't have this time but would like in the future (ne sera pas fait cette fois mais sera fait plus tard)
9.5. YAGNI
You Ain't Gonna Need It (vous n'en aurez pas besoin)
📖️️Wikipédia : https://fr.wikipedia.org/wiki/YAGNI(https://fr.wikipedia.org/wiki/YAGNI)
9.6. GRASP
General Responsibility Assignment Software Patterns (lignes directrices pour l'attribution de la responsabilité des classes et des objets en conception orientée objet)
# Loi de Demeter
La Loi de Déméter pour les fonctions requiert que toute méthode M d'un objet O peut simplement invoquer les méthodes des types suivants d'objets :
* O lui-même
* les paramètres de M
* les objets que M crée/instancie
* les objets membres de O
📖️️Wikipédia : https://fr.wikipedia.org/wiki/Loi_de_Déméter(https://fr.wikipedia.org/wiki/Loi_de_D%C3%A9m%C3%A9ter)
# Loi de Leblanc
Later Equals Never
Utilisé en particulier pour cibler les @TODO qui font partie du code pendant des années. Il est en effet assez rare que le @TODO soit traité par un autre développeur que celui d'origine.
10. PSR-1: Basic Coding Standard
10.1. Introduction
Cette toute première PSR toujours valide, la bien nommée
PSR-1(https://www.php-fig.org/psr/psr-1/) pose le décor : il est vivement recommandé lorsqu'on fait du PHP de faire du POO, cela ne semble par contre pas obligatoire (voir la
conclusion).
10.2. Résumé des règles
* Les fichiers PHP doivent uniquement utiliser la balise <?php
* Les fichiers doivent uniquement être encodés en UTF-8 sans entête BOM
* Les fichiers doivent soit être déclaratifs (constances, classes, fonctions), soit être à effet de bord (modifie des fichiers, appels des services), jamais les deux en même temps
* Les namespaces et classes doivent respecter la convention de nommage de la PSR-4
* Les noms des classes sont en StudlyCaps
* Les noms des constantes de classes sont toujours en majuscule et espacé si besoin par des underscores.
* Les noms des méthodes doivent être déclarés en camelCase.
10.3. Les règles une par une
10.3.1 Les balises PHP
10.3.1.1 Une et une seule balise
Bien qu'il existe d'autres balises, une seule balise doit être utilisée, la balise <?php
La balise ?> ne doit pas être utilisée:
* Le code HTML est de toute manière toujours séparé du code PHP
* cela évite de laisser des caractères en fin de fichier interprété comme des caractères HTML
10.3.1.2 Exemple d'utilisation
Tous les fichiers .php de votre projet doivent commencer par cette ligne suivie d'un retour à la ligne :
<?php
10.3.2 UTF-8 sans BOM
D'où la nécessite d'écrire du code avec des outils dédiés (exit donc le wordpad de Windows par exemple).
Le BOM est une entête de fichier pratiquement invisible lorsque l'on code. Au moment de l'appel au serveur, ce caractère est le premier que le navigateur voit lorsqu'il pense récupérer le contenu de la page (avant donc le <!DOCTYPE ou <html>).
Comme ce caractère est en dehors de toute balise meta qui indiquerait l'encodage de la page, le navigateur va faire comme il souhaite puisqu'il considère que la page html n'est pas valide, tous les caractères sont alors affichés en non UTF-8.
10.3.2.1 Exemple d'IDE
Voici plusieurs outils qui ont la notion d'encodage et de BOM:
Sûrs :
* PHPStorm 2020.3
* Geany 1.36
Probables :
* NodePad++
* Visual Studio
* SublimeText
* Atom
* Eclipse
Peu probables :
* Wordpad
* Gedit
* Kate
* Mousepad
* Leafpad
* Pluma
* KWrite et Kedit
* Nedit/TEA ?
Certains outils de traitement de texte en ligne de commande n'injectent pas d'entête BOM :
* emacs (et XEdit)
* vim (et Gvim)
* nano
10.3.3 Fichiers purement déclaratifs ou à effet de bord (side-effect)
10.3.3.1 Fichier PHP avec effet de bords == PHP procédural
Si un fichier PHP, lors de son execution provoque ceci,on considère qu'il a des effets de bord :
* génèrer une sortie (avec echo, print_r et autres)
* lire, créer ou modifier un fichier (file_put_contents, fwrite et autres)
* modifier la configuration de PHP (set_ini)
* utiliser un require/include, interdit de toute manière en dehors des choses comme l'autoloader
* faire appel à un service externe (HTTP, FTP, n'importe quoi)
* génèrer des erreurs
* modifier des variables globales ou statiques
Ce qui est globalement très souvent le cas lorsque l'on fait du procédural, à moins de faire du code qui ne fait rien.
10.3.3.2 Fichier PHP sans effet de bords == PHP POO
En bref, un fichier déclaratif ne doit rien faire sauf déclarer, c'est à dire :
* tous les fichiers de classes/interfaces sont déclaratifs
* tous les fichiers PHP qui déclarent uniquement des fonctions (utilisation du mot clef function), des constantes (utilisation du mot clef define)
Cependant, il y a certaines ambiguités, aujourd'hui nous avons tendance à créer des classes de type Controller ou même Command.
Ces deux là remplacent dans le concept les fichiers de type command.php et index.php que l'on manipulait en non POO.
En d'autres termes, la PSR-1 est clairement du côté de l'usage de la POO. Les fichiers PHP de type procédurale sont à oublier dans le code source de votre application.
10.3.4 Une execution dépend forcément d'un fichier écrit en PHP procédural
Il ne faut pas penser qu'une application en PHP ne peut tourner qu'avec du POO vu que ce n'est que du déclaratif, car il y a forcément au moins dans votre projet un fichier quelque part écrit en procédural pour executer tout ce beau monde.
Ex: via composer 1.10, le fichier autoload.php est celui qui se charge d'executer tout ce petit monde.
Donc, pour faire simple et respecter cette règle :
* tous les fichiers PHP de votre projet sont en POO use/class/interface/namespace et sont donc déclaratif
* le seul et unique fichier de type side-effect est l'autoload, bien souvent géré par Composer ou par un autre Framework
/!\ ce qui est appelé effet de bord est le fait que la ligne s'execute, pas qu'elle soit utilisée dans une définition.
Par exemple, on peut très bien définir une méthode dans une classe qui ferait un appel à un service externe.
Mais le fait de charger cette classe ne doit effectivement pas executer la ligne en question, sauf si c'est demandé par un fichier executif.
10.3.4.1 Un fichier PHP == Une seule classe ou interface
Bien que dans sa cuisine interne d'optimisation, le framework que vous utilisez fait une fusion de toutes les déclarations. Lors du développement, il faut créer autant de fichier que de classe/interface.
10.3.5 L'autoloading
Fini le temps où les fichiers PHP sont importés dans son code via des require ou des include. Chaque fichier PHP doit déclarer en entête son namespace (cf la PSR-4 pour le nommage) et importer les autres fichiers via des use.
Il y a des exceptions : l'autoloader est le seul à pouvoir faire des includes, idem pour un système personnalisé de mise en cache du code PHP.
10.3.6 Nommage des classes
Le nom des classes et des fichiers de classe doivent être en StudlyCaps, c'est à dire que tous les mots sont reliés ensemble sans espace et commencent chacun par une majuscule.
Ex: SecurityController pour le nom de la classe et SecurityController.php pour le nom du fichier.
10.3.7 Nommage des constantes
Toutes les constantes doivent être définies dans des classes ou des interfaces.
En static car il n'est pas être nécessaire d'instancier la classe.
Pendant un temps la visibilité unique des constantes était public, on peut désormais changer cette visibilité.
10.3.7.1 Exemple de déclaration de constante
Voilà comment on peut définir des constantes :
<?php
namespace App;
class MyClass
{
public static const MY_STRING_CONST = 'valeur';
protected static const MY_BOOL_CONST = true;
private static const MY_INT_CONST = 8;
public static const MY_FLOAT_CONST = 8.08;
}
10.3.7.2 Exemple d'utilisation d'une constante dans un namespace commun
Voilà comment on peut appeler une constante public (ne pas oublier le use en entête) :
<?php
namespace App;
echo MyClass::MY_STRING_CONST;
10.3.7.3 Exemple d'utilisation d'une constante dans un namespace différent
Si la classe est dans un autre namespace, par exemple elle est déclarée dans la classe App\MyClass et est appelée en procédural dans le namespace Walt, il faut l'indiquer dans un use.
<?php
namespace Walt;
use App\MyClass;
echo MyClass::MY_STRING_CONST;
10.4. Conclusion : POO obligatoire ou non ?
Alors, est il possible de faire du PHP sans faire du POO en respectant la PSR-1 ? Je pense que la réponse est Oui.
Imaginons deux fichiers :
* index.php : uniquement du procédural (pas de définition), fait un require_once('lib.php');
* lib.php : uniquement des définitions de fonctions.
index.php :
<?php
require_once('lib.php');
myFunction();
lib.php :
<?php
function myFunction()
{
// Peu importe, du code.
}
A priori, ce n'est pas de la POO mais cela respecte les différentes règles de la PSR-1.
On peut même aller plus loin en créant autant de librairies que l'on souhaite avec des noms plus judicieux et les injecter avec d'autres require.
La PSR-1 ne rend donc pas obligatoire la POO mais l'incite fortement vu que l'accent est fortement mis sur l'application de la PSR-4.
11. PSR-4: Autoloader
11.1. Pourquoi la PSR-0 a été remplacée par la PSR-4 ?
La réponse est toute simple, la PSR-0 introduisait la notion de namespace en PHP mais cela ne concernait que les vendors du projet et non pas le projet lui même.
Ce qui est en soi assez dommage de ne pas pouvoir utiliser l'élégance des namespaces au sein du code de son application.
12. Que faire des vieilles applications ?
12.1. Le problème
12.1.1 Que veut dire le terme Legacy ?
Legacy est un mot anglais qui veut uniquement dire héritage. En informatique, on appelle en général Legacy le code source original du projet, bien souvent avant l'implémentation d'un framework.
Mais cela pourrait très bien faire référence au code de la veille, il faut que celui-ci soit considéré comme obsolète et qu'il soit toujours executé en production.
12.1.2 Traits typiques d'une application Legacy
Alors, la vieille application que tout le monde connait, c'est celle-ci :
* du PHP procédural ;
* une page web générée par des echo ou des ouvertures et fermetures de tags php ;
* des ouvertures / fermetures d'accès à la base de données dans chaque fichier ;
* des variables globales qui portent des noms évocateurs comme mysql_password default_admin_name ;
* du copier/coller de fonctions mélangeant des noms de variables comme $a, $b, $tartiflette... ;
* des appels par référence utilisés sans justification ;
* de la mise en cache gérée un peu partout pour tout et n'importe quoi ;
* des "scripts cron" qui réparent les erreurs des "scripts web" ;
* des librairies en version 0.4.0-modifie-par-luc-le-dev-parti-depuis-3-ans compatible avec une version particulière de PHP ;
* du franglais car les développeurs ont trouvé un compromis : chacun fait comme il veut ;
* les tests sont faits manuellement avec le navigateur web.
12.1.3 Les problèmes qu'engendre une application Legacy
Tout le monde peut les lister :
* fuite des développeurs ;
* charge de maintenance technique (des bugs, des rattrapages) ;
* clients mécontents, ambiance morose au travail ;
* évolutions fonctionnelles très difficiles à implémenter ;
* fonctions métiers floues.
12.1.4 Est-ce si grave ?
La vrai question à se poser est celle-ci :
Combien cette application rapporte à l'entreprise VS combien cette application coûte à l'entreprise ?
Même si l'entreprise dépense énormement d'argent pour maintenir tout ce beau monde en vie, est-ce qu'elle gagne tout de même de l'argent ? La réponse est souvent affirmative :tant que les clients sont là et que l'application répond dans les grandes lignes à leurs demandes, cela suffit pour vendre.
12.2. Comportement 1 : tout refaire et tout de suite
12.2.1 Pourquoi la refonte totale n'est pas forcément judicieuse
Avant d'envisager de tout casser pour reconstruire, il faut d'abord répondre à toutes ces questions :
* Combien ça va prendre de temps de faire un code iso-fonctionnalité et donc combien d'argent l'entreprise doit dépenser pour ça ?
* Y a-t-il vraiment un gain financier derrière (on fait rarement une refonte au sein d'une société pour la gloire) ?
** Cela peut-il réduire le temps de chargement des pages ?
** Cela peut+il augmenter le nombre de clients et donc de ventes ?
** Cela va-t-il réduire la charge en maintenance technique et du SAV auprès du client ?
** Cela va-t-il réduire la charge du SAV auprès du client ?
Enfin pour résumer :
* Est-ce que l'entreprise va gagner plus d'argent après cette refonte ? Combien ? Quand ?
Si vous ne pouvez pas répondre à cette dernière question, chiffres à l'appui, est-ce que vous pensez réellement que l'entreprise va prendre aveuglément le risque de dépenser l'argent que lui rapporte la vente de ses produits via l'application actuelle ?
12.2.2 Avec ou sans refonte, il faut toujours penser à l'après
Dans un monde utopique et merveilleux où vous refaites tout gratuitement, avez-vous penser à la suite ?
* qui gérera les futurs bugs et combien cela coutera ?
* qui modifiera le futur code et a-t-on la garantie qu'il ne va pas réduire la qualité de la refonte ?
* qui mettra à jour la version du framework/des librairies/du langage ?
* est ce que l'application sera toujours utilisée ?
12.2.3 Remplaçons le framework maison par un framework populaire : la petite histoire
Voici un exemple concret : en 2011, une entreprise spécialiste dans la vente en ligne de DVD a fait le choix d'utiliser symfony 1.4 pour refaire son application mère.
Tout porte à croire que le framework va faire des miracles, après tout il est dans le top 5 des frameworks français.
Bilan plusieurs années plus tard :
* le framework a mal été mis en place
** les développeurs n'avaient pas été formés
** certaines pratiques controversées de code ont été maintenues
* la maintenance de l'application est terrible
** la documentation sur cette version du framework n'existe quasiment plus
** la communauté s'est tournée vers des solutions plus récentes
** aucun développeur du marché ne veut travailler sur le code source
* l'entreprise pleure :
** elle a perdu de l'argent (temps de conception/développement/test...)
** un à plusieurs développeurs sont partis, aussi bien ceux à l'ère pré-framework que ceux à l'ère post-framework
Et à ceux qui dise "il fallait le mettre à jour en version 2.0 au bon moment", mettez-vous une seconde à la place de l'entreprise : "je ne comprends pas, je viens de payer ce projet qui a pris 1 an plus de 100 000€ et maintenant tu me dis qu'il faut encore 6 mois pour le mettre à jour ? Et demain ? ".
Quelle est la véritable erreur ? Concevoir son application en mélangeant le code métier ET le code technique.
Cette petite histoire nous apprends deux choses :
* il peut être dangereux d'utiliser un framework sans avoir à l'esprit que le code du projet devrait pouvoir être indépendant de celui-ci
* un projet legacy 2020 n'est pas forcément la même chose qu'un projet legacy 2010
12.2.4 On ne peut pas tout changer du jour au lendemain
Quand un développeur arrive sur un projet, son avis se résume souvent à ceci :
* le modèle de données doit être refait
* les applications doivent être refaites
Il faut alors prendre de la hauteur sur tout ceci. Si tout *marchouille* à son arrivée, c'est au prix de plusieurs années de travail, il est réellement illusoire de penser que le nouveau développeur va tout changer du jour au lendemain, car il lui faudra déjà plusieurs semaines pour assimiler toutes les règles métiers : sachant que la plupart du temps ces règles sont écrites et modifiées sur des documents ou des emails dissiminés sur plusieurs comptes utilisateurs, le développeur les découvrira bien souvent dans le code des anciennes applications.
Idem pour le modèle de données, il est intrinséquement lié au code source, chaque colonne a sa raison d'exister ou non et répond parfois à des règles énigmatiques.
12.2.5 Garder sa vieille application VS la remplacer
Quid de la TMA et du SAV ?
Malheureusement, un mauvais projet, c'est de la maintenance en plus. Là il faut aussi prendre deux minutes pour poser les choses :
- quelles sont les anomalies remontées ? Leurs fréquences ? Leurs coûts ?
* S'il suffit à l'entreprise de payer une personne 10€ pour débloquer une fois sur cent un client qui en rapporte 2000€ et que cela arrive une fois par jour sur l'application principale de la boite, quel est concrètement l'interet financier de payer un développeur 100000€/an (si on compte l'urssaf, la mutuelle, les chèques cadeaux et tout le tintoin) pour tout refaire ? Et bien en supposant que le coût de la TMA et du SAV est de 0€ (idéalement utopique), voilà deux calculs :
** cas de figure 1 : (CA - coût de SAV/TMA) = 2000*100 - 10*365 = 196 350€ de marge
-> au bout de 10 ans, en supposant une progression de 0% : 1 963 500€
** cas de figure 2 : (CA - coût du nouveau projet - coût de SAV/TMA) = 2000*100 - 100000 - 0 = 100 000€ de marge
-> au bout de 10 ans, en supposant une progression de 0% : (marge de la 1iere année + marge des 9 années suivantes) = 100 000 + 200 000*9 = 1 900 000€ de marge sur 10 ans
Conclusion ? on perd plus d'argent dans le cas de figure 2, en refaisant tout, que dans le cas 1, en payant de façon -certes parfois ingrates- quelqu'un pour maintenir le code source d'origine.
Bien sûr, si on change les chiffres de cette histoire, le cas de figure 2 peut être plus rentable. Et c'est là que c'est compliqué pour tout le monde : alors que le développeur pense ça serait forcément mieux d'avoir un projet de qualité, l'entreprise va surtout demander avant toute chose le prix que ça coutera pour le gain que ça rapportera (moins les coûts de maintenance qui ne seront jamais nulle pour autant).
Tout est donc qu'une question de mesure encore une fois, il n'y a pas de réponse magique et ferme car chaque cas est unique mais les questions qu'il faut se poser sont par contre toujours les mêmes :
* combien ça coute aujourd'hui à l'entreprise de garder cette solution technique et combien elle lui rapporte ?
* combien ça couterait de changer la solution technique et combien ça rapporterait sur le court/moyen/long terme ?
En supposant bien sûr que les clients soient toujours là, mais ça c'est encore un autre problème. (ex: le site d'ecommerce spécialiste des DVDs a bien fait de payer une nouvelle version de son site web au début des années 2010, la vente de DVDs en france décline de plus de 10% chaque année...).
12.3. Comportement 2 : améliorer l'existant au fur et à mesure, développer de nouvelles applications
Par existant, je nomme l'ensemble du code source de la société, applications Legacy ou non.
12.3.1 Faire avec l'ancien code
L'ancien code, vous pouvez toujours l'améliorer au fil de l'eau pour qu'il soit plus simple à comprendre/maintenir/modifier pour les générations futures, même dans une ancienne version du langage ou du framework.
Vous pouvez en outre être stratégique et n'améliorer que les bouts de code qui génèrent le plus de problèmes (bugs/lenteurs)
Par améliorations, voici ce que je préconise :
* réduiser le nombre de lignes de code
** en identifiant les endroits dupliqués
** en identifiant le code mort
** en créant des fonctions de refactorisation : attention à ne pas faire des choses trop complexes par ailleurs
* appliquer autant que possible les normes de code de la dernière version du langage/framework
* améliorer les performances des pages les plus critiques pour le chiffre d'affaire (ex: paiement du panier)
12.3.1.1 Qu'est ce que des choses trop complexes ?
Par complexe, j'entends que si vous remplacez quelques lignes de codes HTML facilement maintenables par n'importe qui par du code qui va génerer ces lignes avec des règles de plus en plus bizarres pour prendre en compte tous les cas, votre nouveau code, même des dizaines de fois moins long que le précédent sera plus difficile à comprendre, donc plus couteux à terme par l'entreprise.
Notamment :
* un code qui génère des formulaires HTMLs
* un code qui génère des bouts de codes PHP
* un code qui met en cache la terre entière pour optimiser sur le moment une lenteur
12.3.2 Produire du code utile
Donc que faut-il faire ? La réponse est simple : se concentrer sur du code de qualité qui *paye*.
* Faire du code de qualité : toujours garder un projet dont le code respecte les bonnes pratiques. Faire un code métier autonome du framework.
* Faire du code qui paye : travailler sur les projets qui rapporte de l'argent à l'entreprise car c'est une partie de celui-ci qui est reversé aux salariés (même dans le cas où il y a des associés vénaux).
En outre, si un développeur produit du code de qualité, il sera surement plus heureux.
12.3.3 Que faire si le métier demande une ou plusieurs nouvelles fonctionnalités
Si la nouvelle fonctionnalité demandée par le métier rentre dans le cadre d'une ancienne application, il faut savoir choisir entre deux solutions :
* faire avec les "normes" de l'application pour ajouter son bout de code
* faire un code de qualité en dehors de l'application (ex: via un web service) et l'appeler via l'application comme on peut
Si le métier demande beaucoup de nouvelles fonctionnalités, il peut s'avérer pertinent de proposer une nouvelle application qui adoptera de meilleurs principes.
Dans les deux cas, la question est toujours la même : combien cela va couter à l'entreprise sur le court/moyen/long terme ?
13. Utilisation des exceptions en PHP
Voici une interprétation de la page
15 Best Practices for Exception Handling(http://codebuild.blogspot.com/2012/01/15-best-practices-about-exception.html). Certaines best practices se complètent, il ne me semble pas nécessaire d'en citer 15.
13.1. Ne pas utiliser les exceptions pour gérer des erreurs métiers
Toutes les erreurs métiers doivent être prévues et gérées via des conditions classiques, on évitera donc d'écrire quelque chose comme catch (FunctionalException $e).
13.2. Eviter l'usage du \Exception
Le nom de l'exception doit être clair, on évite d'utiliser la classe \Exception, trop générale. Donc on évite le catch (\Exception $e) même dans le cas où on utiliserait une librairie/composant qui retournerait beaucoup d'exception (ex: Doctrine).
Evidemment, si la librairie retourne une instance d'\Exception, il n'y a pas le choix que de la catcher.cher
13.2.1 Exemples de noms clairs
* MysqlConnectionException
* NullEntityException
* EmptyFileException
* DeniedAccessException
* HttpException (Eventuellement, on peut définir des exceptions pour chaque code d'erreur HTTP)
En plus d'avoir un nom clair, chaque exception doit être correctement documentée.
13.3. Ne pas utiliser autre chose que des exceptions en cas d'erreurs techniques
On évite de retourner des nombres négatifs par exemple selon les erreurs. Mieux vaut privilégier des exceptions claires et non ambigues.
13.3.1 Exemples de mapping pour une lecture de fichier
* -1 -> FileNotFoundException
* -2 -> FileNotReadableException
* -3 -> UnsupportedFileException
* -4 -> TooBigFileException (dans le cas d'une limite à l'écriture par exemple)
13.4. Eviter de faire uniquement un throw dans un catch
Alors, ce genre de code est inutile :
try {
// Peu importe
} catch (AnyException $e) {
throw $e;
}
Soit le catch sert à quelque chose (on logguerait l'erreur par exemple), soit s'il ne sert à rien, il n'a pas de raison d'être.
De la même façon, ce n'est pas pertinent non plus de transformer le type d'exception en un autre type d'exception, autant laisser passer l'exception d'origine.
Eventuellement, il peut être par contre intéressant de compléter le message d'erreur de l'exception afin d'aider à la résolution du problème.
13.4.1 Eviter de ne rien faire dans le catch
Alors, ignorer l'exception, c'est souvent une fausse bonne idée. On évitera donc ce code :
try {
// Peu importe
} catch (AnyException $e) {
// Sifflote
}
13.5. Faire de l'héritage autant que faire ce peut
Plutôt que de faire hériter toutes ses exceptions avec \Exception, cela peut être intéressant de les réunir au sein de leurs domaines respectifs (ex: FileNotFoundException extends FileException, NotFoundHttpException extends HttpException, UnauthorizedUserException extends UserException...).
13.6. Logger les exceptions selon leur gravité
Selon les exceptions catchés, il peut être intéressant d'inscrire dans les logs les erreurs en fonction de leur gravité (DEBUG/NOTICE/FATAL).
On évite par contre de logger plusieurs fois la même erreur.
13.6.1 Exemples de niveau
DEBUG : Fichier non initialisé, Utilisateur n'existant pas encore
NOTICE : Time out sur la récupération d'une donnée non essentielle en cache par exemple
FATAL : erreur de connexion à la BDD, moteur de recherche, file d'attente
13.7. Utiliser le finally
Rarement utilisé, il peut être parfois être pratique pour libérer des ressources par exemple.
13.8. Eviter les try/catch dans des boucles
On peut catcher 1 fois mille erreurs plutôt que catcher mille fois 1 erreur.
13.9. Faire plusieurs try/catch plutôt qu'un seul
Bon cela se défend car personnellement je préfère fois un seul try/catch que plusieurs d'affilée.
L'idée est qu'il y ait peu d'instructions dans le try afin d'évaluer plus facilement d'où proviennent les erreurs.
13.10. User de code d'erreurs
Dans l'idéal, chaque exception est accompagnée d'un code d'erreur qui simplifiera la maintenance de l'application, ne serait-ce que dans l'étude statistique des problèmes courants.
13.11. Enfin, ne pas en abuser
Si pour afficher une seule page web, on risque de tomber sur l'une des 200 exceptions possibles, c'est que manifestement, la page est peut-être trop complexe.
Au-delà de 10 exceptions possibles pour un seul appel HTTP, je pense que cela devient ingérable.
14. Anti pattern Flag parameter
Le flag parameter peut être considéré comme un anti-pattern.
Le principe est de passer un argument supplémentaire (le plus souvent de type boolean) à une méthode afin que celle-ci se comporte différemment selon ce paramètre.
Par exemple, supposons cette fonction :
function render(array $data, bool $flag)
{
if ($flag) {
// Premier comportement.
} else {
// Second comportement.
}
}
// Code appelant
$data = getData();
$flag = getFlag();
render($data, $flag);
14.1. Solution 1 : découper la fonction en deux.
La fonction render doit être découpée en deux et c'est à l'appelant de ces fonctions de faire le if/else qui convient.
function firstRender(array $data)
{
// Premier comportement.
}
function secondRender(array $data)
{
// Second comportement.
}
// Code appelant
$data = getData();
$flag = getFlag();
if ($flag) {
firstRender($data);
} else {
secondRender($data);
}
14.2. Solution 2 : fusionner les deux paramètres
Si la fonction n'est pas simple à découper (plusieurs if/else interne). Même s'il conviendrait de la refactoriser, il peut être pertinent dans certaines conditions de faire porter la valeur du flag soit dans un objet que l'on transfère à la fonction, soit dans la classe qui contient la méthode.
14.2.1 Cas 1 : via un objet
class Renderer
{
public function render(object $object)
{
$flag = $object->getFlag();
// Suite des différentes conditions.
}
}
14.2.2 Cas 2 : via un attribut de la classe
class Renderer
{
protected bool $flag;
function setFlag(bool $flag)
{
$this->flag = $flag;
}
function getFlag()
{
return $this->flag;
}
function render(object $object)
{
$flag = $this->getFlag();
// Suite des différentes conditions.
}
}
14.3. Solution 3 : créer plusieurs classes et une factory ou équivalent
Chaque classe possède un comportement propre. Ce comportement contient la logique selon le cas flag=true|false.
Le rôle de la Factory sera alors de fournir la bonne instance en fonction du paramètre flag.
// Code appelant
$rendererFactory = new RendererFactory();
$renderer = $rendererFactory->create($flag);
$renderer->render($data);
🧙♂️️Si la fonction existe déjà, il peut être long de tout refaire, il faut à ce moment là prendre un peu de temps de réflexion quant à l'intérêt de tout refaire pour un gain immédiat souvent faible.
Privilégier donc la réfacto des morceaux de code importants (à vous de définir cette importance en fonction du contexte).
14.4. Solution type Troll
🧙♂️️Cette solution n'est évidemment pas conseillée.
L'idée est de créer une instance Flag et de la passer à la fonction ; ainsi nous ne passons plus un boolean et donc nous sortons de l'anti-pattern.
<?php
class Flag
{
protected bool $value;
public function __construct(bool $value)
{
$this->value = $value;
}
public function isFalse(): bool
{
return !$this->isTrue();
}
public function isTrue(): bool
{
return true === $value;
}
}
// Code appelant
$data = getData();
$flag = new Flag(true);
render($data, $flag);
15. Liste des anti patterns
Voici la liste des anti patterns les plus populaires.
15.1. 20 antipatterns généraux
* Accidental complexity
* Action at a distance
* Boat anchor
* Busy waiting
* Caching failure
* Cargo cult programming
* Coding by exception
* Design pattern
* Error hiding
*
Flag parameter
* Hard code
* Lasagna code
* Lava flow
*
Loop-switch sequence
* Magic numbers
* Magic strings
* Repeating yourself
* Shooting the messenger
* Shotgun surgery
* Soft code
* Spaghetti code
15.2. 12 antipatterns POO
* Anemic domain model
* Call super
* Circle–ellipse problem
* Circular dependency
* Constant interface
* God object
* Object cesspool
* Object orgy
* Poltergeists
* Sequential coupling
* Singleton Pattern
* Yo-yo problem
15.2.1 9 antipatterns méthodologiques
* Copy and paste programming
* Golden hammer
* Invented here
* Not invented here (NIH) syndrome
* Premature optimization
* Programming by permutation
* Reinventing the square wheel
* Silver bullet
* Tester-driven development
16. Anti Pattern : LoopSwitch
16.1. Exemple
Voici un exemple d'implémentation de cet anti pattern.
<?php
namespace App\Core;
/**
* Class LoopSwitch
*/
class LoopSwitch
{
protected const FIRST_STEP = 'first_step';
protected const SECOND_STEP = 'second_step';
/**
* @var string[]
*/
protected array $steps = [
self::FIRST_STEP,
self::SECOND_STEP,
];
/**
* Executes All the steps.
*/
public function executeAllSteps()
{
foreach($this->steps as $step) {
$this->executeStep($step) ;
}
}
/**
* @param string $step
*/
public function executeStep(string $step)
{
switch ($step) {
case self::FIRST_STEP:
echo self::FIRST_STEP;
break;
case self::SECOND_STEP:
echo self::SECOND_STEP;
break;
}
}
}
17. Prototype
17.1. Introduction
Le Prototype est un design pattern très simplement accessible en PHP puisque l'usage de la méthode __clone est une application de celui-ci.
Le Prototype est la capacité d'un Object de se cloner en un nouvel Object qui hérite de l'état de l'Object cloné.
A noter que le __clone crée une nouvelle référence pour l'Object copié.
Dans le cas où un Object fait référence à d'autres Objects (par exemple un Attribute "Color $color" ), cette référence est copiée, ce qui peut être problématique si l'on modifie la valeur de l'Object référencé.
17.2. Le problème
Supposons qu'on a une Class Pencil qui aurait un Attribute Color :
Pencil.php
<?php
class Pencil implements PencilInterface
{
protected ColorInterface $color;
public function __construct(ColorInterface $color)
{
$this->color = $color;
}
public function getColor(): ColorInterface
{
return $this->color;
}
}
PencilInterface.php
<?php
interface PencilInterface
{
public function getColor(): ColorInterface;
}
ColorInterface.php
<?php
interface ColorInterface
{
public function getValue(): string;
}
Color.php
<?php
class Color implements ColorInterface
{
public function __construct($value)
{
$this->value = $value;
}
public function setValue(string $value)
{
$this->value = $value;
}
public function getValue(): string
{
return $this->value;
}
}
Voici un comportement problématique :
$color = new Color('red');
$firstPencil = new Pencil($color);
$clonedPencil = clone $firstPencil;
$color->setValue('green');
echo $clonedPencil->getColor()->getValue(); // -> 'green'
echo $firstPencil->getColor()->getValue(); // -> 'green'
Ici, les deux instances de Pencil se retrouvent avec une couleur ayant la valeur verte. Ce que l'on ne souhaitait pas forcément : on souhaite avoir un Pencil ayant une Color 'green' et l'autre ayant une Color 'red'.
La seule façon de résoudre ce problème est de surcharger la méthode __clone afin de cloner également l'instance de Color (et donc obtenir une nouvelle référence différente de la première).
17.3. Solution
Il faut réécrire la méthode __clone de cette façon :
Pencil.php
<?php
class Pencil implements PencilInterface
{
protected ColorInterface $color;
public function __construct(ColorInterface $color)
{
$this->color = $color;
}
public function getColor(): ColorInterface
{
return $this->color;
}
public function __clone()
{
$this->color = clone $this->color;
}
}
Se faisant, si l'on réexecute le bout de code précédent, on obtient ceci
$color = new Color('red');
$firstPencil = new Pencil($color);
$clonedPencil = clone $firstPencil;
$color->setValue('green');
echo $clonedPencil->getColor()->getValue(); // -> 'red'
echo $firstPencil->getColor()->getValue(); // -> 'green'
Le premier crayon a bien hérité d'une couleur verte, tandis que le second a bien une couleur qui a été modifiée en rouge.
17.4. Les ValueObjects
Le ValueObject est un Object immutable, c'est à dire que ces propriétés sont initialisés qu'à un seul moment, la grande majorité du temps dans le constructeur ; il est aussi possible de faire des setters qui ne settent qu'une seule fois la valeur, ce qui peut potentiellement produire un effet de bord selon le test effectué.
17.5. Setter d'un immutable
Voici à quoi pourrait ressembler un tel setter. Reprenons la Class Pencil :
<?php
class Pencil implements PencilInterface
{
protected ?ColorInterface $color = null;
public function __construct(ColorInterface $color)
{
$this->color = $color;
}
public function setColor(?ColorInterface $color = null): PencilInterface
{
if (\is_null($this->color)) {
$this->color = $color;
}
return $this;
}
public function getColor(): ColorInterface
{
return $this->color;
}
}
Ici, on sette la couleur une seule et unique fois, mais étant donné que l'Attribute color peut également être null, il est tout à fait possible de passer une première fois dans la méthode, setter $color à null, puis repasser une autre fois et setter $color avec une instance implémentant ColorInterface.
Le principe d'un Object immutable est de ne plus être modifié après sa création, ce qui potentiellement peut toujours être le cas ici.
Eventuellement, on pourrait rajouter un Attribute indiquant si l'état est fixe ou non, mais cela deviendrait compliqué à gérer s'il fallait identifier pour chaque Attribute si son setter a été ou non appelé. En outre, il faudrait également garantir que cet Attribute n'est setté qu'une seule fois.
Donc la solution de passer par des setters plutôt que par le constructeur n'est pas une bonne idée.
17.6. Comment cloner un Object possédant un Attribute ValueObject ?
Il est difficile de lier les concepts de Prototype et de ValueObject car il faudrait créer une méthode clone ayant des paramètres similaire à ceux du __construct.
Dans le cas de l'Attribute Color, si l'on considère que c'est un ValueObject, il ne serait plus possible de modifier la Color du Pencil cloné, ce qui serait déroutant.
Donc si l'on souhaite cloner un Object possédant un Attribute ValueObject, il faut accepter que cet Attribute sera immutable d'un clone à l'autre.
17.7. Factory+Pool
Pour créer un Pencil avec une Color immutable, on peut passer par une Factory, voire mélanger le pattern design Pool et Factory afin de conserver les différents Pencil créés.
On peut envisager de n'avoir qu'une seule Factory nommé PencilFactory (on pourrait éventuellement avoir une deuxième Factory nommée ColorFactory qui stockerait les différents instances de Color).
Ici, la PencilPoolFactory garde en mémoire les différentes instance de Color créées puis fabrique et retourne des instances de Pencil. Etant donné qu'il change d'état, il n'est donc pas possible de passer par une méthode statique.
<?php
class PencilPoolFactory
{
/**
* ColorInterface[]
*/
protected array $colors = array();
public function has(string $key)
{
return isset($this->colors[$key]);
}
public function get(string $colorValue): ColorInterface
{
return $this->colors[$colorValue];
}
public function add(ColorInterface $color, string $colorValue)
{
$this->colors[$colorValue] = $color;
}
public function create(string $colorValue)
{
if (!$this->has($colorValue)) {
$this->add(new Color($colorValue), $colorValue);
}
return new Pencil($this->get($colorValue));
}
}
Voici comment l'utiliser :
$pencilPoolFactory = new PencilPoolFactory();
$redPencil = $pencilPoolFactory->create('red');
$greenPencil = $pencilPoolFactory->create('green');
echo $redPencil->getColor()->getValue(); // 'red'
echo $greenPencil->getColor()->getValue(); // 'red'
🧙♂️️Le PencilPoolFactory mélange deux design patterns mais cela produit un mauvais pattern (le mot antipattern est un peu trop populaire et désigne aujourd'hui des choses eux-mêmes populaires).
D'une première raison : ce PencilPoolFactory fabrique des instances de Color ET des instances de Pencil, il a donc deux responsabilités, ce n'est pas la faute de la combinaison des deux design patterns.
D'une deuxième raison : le PencilPoolFactory garde en mémoire les instances de Color, ce qui lui donne une nouvelle responsabilité car l'utilisateur sera tenté de faire également un $pencilPoolFactory->get('color').
17.7.1 Résolution du problème
Il faut découper le PencilPoolFactory en trois : PencilFactory, ColorFactory et PencilPool. Chacune de ces Classs aura une seule responsabilité.
17.7.1.1 PencilPool avec un Attribute $colorFactory
* Le PencilFactory aura un Attribute Singleton : $pencilPool.
* Le PencilPool aura un Attribute Singleton : $colorFactory.
PencilFactory.php
<?php
class PencilFactory
{
PencilPoolInterface $pencilPool;
public function __construct(
PencilPoolInterface $pencilPool
)
{
$this->pencilPool = $pencilPool
}
public function create(string $colorValue): PencilInterface
{
$color = $this->pencilPool->get($colorValue);
return new Pencil($color);
}
protected function getColor(string $colorValue): ColorInterface
{
return $this->pencilPool->get($colorValue);
}
}
Cette solution est dérangeante. Le PencilPool dispose d'un Attribute autre que celui pour lequel il est dédié, ce qui est déroutant car il stocke des références de ColorInterface ET de PencilFactoryInterface.
La solution inverse semble plus pertinente : le PencilFactory possède un Attribute $pencilPool qui ferait office de stockage des nouvelles instances.
17.7.1.2 ColorFactory avec un Attribute $pencilPool
Le PencilPool se réduit ici à son strict nécessaire : il stocke des instances implémentant ColorInterface
PencilPool.php
<?php
class PencilPool implements PencilPoolInterface
{
/**
* ColorInterface[]
*/
protected array $colors = array();
public function has(string $key)
{
return isset($this->colors[$key]);
}
public function get(string $colorValue): ColorInterface
{
return $this->colors[$colorValue];
}
public function add(ColorInterface $color, string $colorValue)
{
$this->colors[$colorValue] = $color;
}
}
Et le PencilFactory possède un Attribute $pencilPool.
PencilFactory.php
<?php
class PencilFactory implements PencilFactoryInterface
{
protected PencilPool $pencilPool;
public function create(string $colorValue)
{
if (!($pencilPool->has($colorValue)) {
$color = new Color($colorValue);
$pencilPool->set($color, $colorValue);
} else {
$color = $pencilPool->get($colorValue);
}
return $color;
}
}
La PencilFactory demande à sa $pencilPool si la Color existe, auquel cas elle le retourne. Si ce n'est pas le cas, elle la crée et le transmet au pencilPool pour que ce dernier le stocke.
Eventuellement, on pourrait imaginer une nouvelle Class plus complexe, avec un nom un peu ambigue comme PencilManager.
Cette Class ferait le lien entre le PencilFactory, le ColorFactory et le PencilPool.
Cela garantirait que les trois Class seraient simples et immutables (on ne les modifierait plus une fois conçues).
Sauf qu'on imagine bien que le PencilManager finirait par avoir plusieurs responsabilités, même si elles sont déléguées à d'autres Class, ce qui n'est en soi pas un problème si on fait en sorte que ce Manager ne représente que la fusion des trois autres.
Ce ne serait qu'une Facade, qui ne devrait faire qu'une chose : retourner un Pencil en fonction d'une valeur de couleur.
18. Le design pattern Singleton et l'implémentation des services
Avec les frameworks modernes, on passe notre temps à manipuler des Singletons car tout ce qu'on nomme service en est en fait un.
Un singleton n'est rien d'autre qu'une classe que l'on ne peut instancier qu'une et une seule fois par exécution. Si l'on désire créer une nouvelle instance et qu'il en existe déjà une, c'est celle-ci qui sera utilisée, magique donc !
18.1. La classe Singleton
18.1.1 Schema DML
18.1.2 Exemple de la classe Singleton
Voici une façon d'écrire une classe qui implémente le design pattern Singleton :
<?php
class Singleton
{
private static $mySelf = null;
private function __construct(){} // Empêche toute création d'instance.
private function __clone(){} // Empêche toute copie d'instance.
public static function getInstance()
{
if (self::$mySelf === null) {
self::$mySelf = new Singleton();
}
return self::$mySelf;
}
}
18.1.3 Exemple d'utilisation (attention, ça se complique !)
A l'usage, lorsque l'on cherche à créer deux instances, voici ce qu'il se passe:
<?php
$firstSingleton = Singleton::getInstance();
$secondSingleton = Singleton::getInstance();
18.1.4 Pourquoi l'usage de self et du static ?
Puisque que la classe n'est pas instanciée au moment de l'appel à getInstance, $this n'existe pas encore. Et comme $this n'existe pas, le seul moyen de faire appel à l'attribut mySelf est de passer par du static ::.
18.2. La classe SingletonMaker
18.2.1 Introduction
C'est tout de même très étrange comme façon de créer une instance, c'est un peu trop fantastique ce qu'il se passe, non ? On dirait que l'on hack la façon dont on doit utiliser les classes, notamment, il me semble vraiment étrange pour une classe de s'autocréer.
On pourrait éventuellement passer par un créateur de Singleton pour gérer tout ceci de façon plus claire, je vous propose donc de créer le SingletonMaker.
18.2.2 Exemple de la classe SingletonMaker
A noter que la classe aurait pu s'appeler SingletonFactory, mais elle s'appelle SingletonMaker pour éviter l'ambiguité avec le design pattern Factory, dont le code ci dessous n'est pas représentatif.
class Singleton
{
}
class SingletonMaker
{
private static $singleton = null;
public static function getSingleton()
{
if (self::$singleton === null) {
self::$singleton = new Singleton();
}
return self::$singleton;
}
}
C'est déjà un peu plus normal ce qu'il se passe ici, on passe par une classe qui nous retourne le Singleton en le créant si besoin.
18.2.3 Exemple d'utilisation
Rien de très différent de la fois d'avant.
<?php
$firstSingleton = SingletonMaker::getSingleton();
$secondSingleton = SingletonMaker::getSingleton();
18.3. Notre gestionnaire de services - la classe ServiceManager
Quelque part, nous venons de créer un gestionnaire de services, si on renomme un peu les choses, voici ce qu'on pourrait obtenir :
18.3.1 Proposition d'implémentation du ServiceManager
En renommant Singleton en MyFirstService, en créant une seconde classe MySecondService et en renommant le SingletonMaker par ServiceManager, on pourrait écrire ceci :
class MyFirstService
{
}
class MySecondService
{
}
class ServiceManager
{
private static $services = [];
public static function get(string $serviceClassName)
{
if (!isset(self::$services[$serviceClassName])) {
self::$services[$serviceClassName] = new $serviceClassName();
}
return self::$services[serviceClassName];
}
}
A l'usage, voici comment on récupérerait nos services :
$myFirstService = ServiceManager::get('MyFirstService');
$mySecondService = ServiceManager::get('MySecondService');
18.3.2 Appelation de services plus abstraites
Et eventuellement faire une appelation de services différente que l'on recalculerait à la volée.
Ex : 'myFirstService' comme nom du service et new ucfirst("myFirstService")() pour créer l'instance (pour rappel le ucfirst met en majuscule la première lettre d'une chaine de caractères).
class MyFirstService
{
}
class ServiceManager
{
private static $services = [];
public static function get(string $serviceName)
{
$serviceClassName = ucfirst($serviceName);
if (!isset(self::$services[$serviceName])) {
self::$services[$serviceName] = new $serviceClassName();
}
return self::$services[$serviceName];
}
}
A l'usage, on pourrait l'appeler comme ceci :
$myService = ServiceManager::get('myFirstService');
18.3.2.1 Apparté
Derrière, on peut appeler ça un containeur de services, puis déclarer dans un fichier de configuration yml, xml ou ce que l'on souhaite le nom des services publics (haha).
19. Static Factory
19.1. Introduction
FactoryMethod/AbstractFactory/StaticFactory/SimpleFactory : peu importe le nom qu'on donne à ces designs patterns, il faut surtout comprendre que le but d'une classe suffixée Factory est celui-ci : construire et retourner des objets.
Que cette classe Factory étende une AbstractFactory, qu'elle contienne une ou plusieurs méthodes de fabrication, qu'elle soit elle même générée par une autre Factory, l'essentiel est qu'elle crée des instances.
En outre, si le Factory a ce rôle de constructeur, il paraitrait logique que tous les 'new' ne soient présents que dans de telles classes dans tout le code source du projet.
Les méthodes de création des classes suffixées Factory sont justement nommées FactoryMethod. "(create(...)/factory(...)/make(...)")
19.2. Intention
Fournir une interface permettant de créer des familles d'objets liés ou dépendants sans spécifier leurs classes concrètes. A la différence de l'AbstractFactory qui définit des Factory ayant plusieurs méthodes de fabrication, la StaticFactory définit des Factory qui n'ont qu'une seule méthode statique de fabrication pour un ensemble d'objets.
🧙♂️️Ce que je comprends plutôt de ce qu'est le StaticFactory :
A la différence de l'AbstractFactory, on ne définit ici qu'une seule classe Factory ; étant donné qu'il n'y a qu'une seule classe Factory, il n'est donc pas nécessaire de mettre du code dans une classe abstraite (donc le fichier 'AbstractSomethingFactory.php' n'existerait pas). Le design pattern StaticFactory est un cas particulier de l'AbstractFactory.
19.3. Exemple concret
Supposons que l'on souhaite afficher à l'écran le nom du repas à prendre en fonction du moment de la journée (par exemple pour une personne qui n'aurait plus toute sa tête). Pour simplifier, on ne considère que trois types de repas : le petit déjeuner, le déjeuner et le diner.
19.3.1 Code procédural
En procédural, voici ce que l'on pourrait écrire :
index.php
<?php
$currentHour = (int) date('H');
if ($currentHour < 12) {
echo 'breakfeast';
} elseif ($currentHour < 16) {
echo 'lunch';
} else {
echo 'dinner';
}
Ici, il y a un problème fondamental : la logique heure/type de repas n'est pas récupérable en l'état dans les autres fichiers de l'application, il faudra alors dupliquer ce code. Si un nouveau repas est introduit (comme le petit déjeuner), il faudra chercher dans l'ensemble du code cette logique pour la changer.
Une fonction aurait tout sa place ici.
Par exemple :
<?php
function getMealName($currentHour): string
{
if ($currentHour < 12) {
$mealName = 'breakfeast';
} elseif ($currentHour < 16) {
$mealName = 'lunch';
} else {
$mealName = 'dinner';
}
return $mealName;
}
echo getMealName((int) date('H'));
Cette fonction résout le problème principal : la logique heure/type de repas est à un seul endroit.
Là où cela se complique, c'est si l'on souhaite retourner autre chose que le nom du repas, par exemple les horaires de celui-ci, on pourrait éventuellement faire une deuxième fonction comme celle-ci :
function getMealTime($currentHour): string
{
if ($currentHour < 12) {
$mealTime = '0h - 12h';
} elseif ($currentHour < 16) {
$mealTime = '12h - 16h';
} else {
$mealTime = '> 16h';
}
return $mealTime;
}
echo getMealTime((int) date('H'));
Mais cette fonction génère à nouveau un problème de duplication.
Non seulement on duplique à nouveau la logique heure/type de repas mais s'il faut rajouter un type de repas, il faudra modifier les deux fonctions.
La solution ici est de renvoyer une donnée plus complexe qui contiendrait plusieurs informations. Cette donnée pourrait contenir à la fois le nom du repas mais également les horaires de celui-ci. Il existe des solutions pour cela : on peut retourner un tableau, une structure particulière (ex: json/xml/yaml) ou bien un Objet. Le tableau ou la structure pose une difficulté : le client de la fonction doit connaitre le nom des clefs d'accès aux différentes informations. Dans le cas d'un Objet, le client a accès aux méthodes publiques de celui-ci pour accéder à une représentation des différentes informations.
19.3.2 En POO
19.3.3 Création des modèles Meal
En POO, on pourrait représenter chaque type de repas de la façon suivante :
MealInterface.php
<?php
namespace App\Model\Meal;
interface MealInterface
{
public function getName();
}
AbstractMeal.php
<?php
namespace App\Model\Meal;
abstract class AbstractMeal implements MealInterface
{
protected string $name;
public function getName(): string
{
return $this->name;
}
}
Breakfeast.php
<?php
namespace App\Model\Meal;
class Breakfast extends AbstractMeal implements MealInterface
{
public function __construct()
{
$this->name = 'breakfast';
}
}
Lunch.php
<?php
namespace App\Model\Meal;
class Lunch extends AbstractMeal implements MealInterface
{
public function __construct()
{
$this->name = 'lunch';
}
}
Dinner.php
<?php
namespace App\Model\Meal;
class Dinner extends AbstractMeal implements MealInterface
{
public function __construct()
{
$this->name = 'dinner';
}
}
19.3.4 Mise en application sans Factory
Maintenant que les différentes classes sont prêtes, on peut les utiliser :
MealController.php
<?php
namespace App\Meal\Controller;
use App\Meal\Model\Breakfast;
use App\Meal\Model\Lunch;
use App\Meal\Model\Dinner;
class MealController
{
public function index()
{
$currentHour = (int) date('H');
if ($currentHour < 12) {
$meal = new Breakfast();
} elseif ($currentHour < 16) {
$meal = new Lunch();
} else {
$meal = new Dinner();
}
echo $meal->getName();
}
}
Ici, le Controller doit avoir connaissance des différentes classes pour faire son affichage. Ce qui pose un problème dans le cas d'une modification des types de repas : il faut modifier l'ensemble des fichiers qui manipulent les classes concrètes (là où il y a des 'use').
Le but de la Factory est d'éviter cela afin que le Controller n'ait plus à gérer les différents modèles. Tout ce qu'il doit savoir est qu'il manipule une instance qui implémente l'interface MealInterface. Cette interface a une méthode getName(), commune à toutes les classes qui l'implémentent.
19.3.5 Avec l'Abstract Factory
Voici une implémentation possible de la classe MealFactory, elle reprend la logique du MealController :
MealFactory.php
<?php
namespace App\Meal\Factory;
use App\Meal\Model\MealInterface;
use App\Meal\Model\Breakfast;
use App\Meal\Model\Lunch;
use App\Meal\Model\Dinner;
class MealFactory implements MealFactoryInterface
{
public static function create(int $hour): MealInterface
{
if ($hour < 12) {
$meal = new Breakfast();
} elseif ($hour < 16) {
$meal = new Lunch();
} else {
$meal = new Dinner();
}
return $meal;
}
}
Elle implémente l'interface MealFactoryInterface. Cette interface permettra dans le cas où l'on souhaite créer une deuxième MealFactory (par exemple pour un pays où les repas seraient différents) de mettre en place le mécanisme d'AbstractFactory (ie. plusieurs classes de type MealFactory (FrenchMealFactory/AfricanMealFactory)).
MealFactory.php
<?php
namespace App\Meal\Factory;
use App\Meal\Model\MealInterface;
interface MealFactoryInterface
{
public function create(int $hour): MealInterface;
}
Voici désormais ce que fera le MealController :
MealController.php
<?php
namespace App\Meal\Controller;
use App\Meal\Factory\MealFactory;
class MealController
{
public function index()
{
$mealFactory = MealFactory::create((int) date('H'));
echo $meal->getName();
}
}
🧙♂️️L'usage des méthodes static est assez déconseillée car le Controller a ici une dépendance avec la classe concrète de la Factory. Si on souhaite utiliser une autre MealFactory, il faudra modifier dans l'ensemble du code cet usage. Mieux vaut que MealFactory implémente l'interface MealFactoryInterface. On injectera ensuite la Factory dans le constructeur des classes qui en dépendent.
19.4. Sources
📖️️https://designpatternsphp.readthedocs.io/en/latest/Creational/StaticFactory/README.html
📖️️https://waytolearnx.com/2020/02/design-patterns-static-factory-en-php.html
20. Le design pattern Decorator
20.1. Schema DML
21. Le design pattern Observer
21.1. Schema DML
21.2. Implementation
Ce DP introduit deux notions : l'Observer et l'Observable.
L'Observable doit envoyer une notification aux Observers qui lui sont rattachés pour leur indiquer que l'une de ces données a été changée.
La classe Observable doit donc gérer à la fois son modèle et la liste des Observers (qui sont des attributs de la classe).
Le sujet observé doit donc être au courant que les observateurs existent.
Voici une implémentation classique :
class Subject
{
protected Observer[] $observers;
public function __construct(Observer[] $observers) {
$this->observers = $observers;
}
public function doSomething()
{
$this->observer->notify($this);
}
}
abstract class Observer
{
public function notify(Subject $subject)
{
// Fais quelque chose
}
}
class MyObserver extends Observer
{
}
Une façon différente de procéder serait d'utiliser des EventsListeners : on crée une classe qui met à jour le modèle et qui notifie une liste de Listeners en fonction d'une typologie d'évènement (par exemple des changements d'état).
22. Immutable MVC en PHP version 2019
Ceci est une proposition de traduction de la page Immutable MVC: MVC In PHP 2019 Edition (https://r.je/immutable-mvc-in-php) de Tom Butler, que j'estime être très doué.
A noter que je n'ai pas trouvé quelle était la licence sur le contenu de son article, il est bien entendu que cette page sera supprimée si la copie de l'article original est interdite.
-- Début de la traduction --
22.1. MVC Revisité
Les articles les plus populaires de ce site Web concernent le MVC, dont le plus ancien a maintenant près de dix ans. Beaucoup de choses ont changé au cours de cette période, et ma propre expérience s'est également enrichie. Ma précédente série d'articles sur les "accidents vasculaires cérébraux" s'est en quelque sorte éteinte parce que je voulais revenir en arrière et peaufiner les exemples des articles précédents.
Le PHP a beaucoup changé depuis que j'ai écrit ces articles. Je vais prendre un nouveau départ et donner des exemples de code plus propres, plus complets et plus modernes.
Neuf ans se sont écoulés depuis que j'ai écrit le premier article sur les MVC ici et j'ai quelques observations à faire à ce sujet en regardant en arrière :
L'écosystème du PHP n'a pas changé. Les gens confondent toujours l'approche "contrôleur au lieu de médiateur" et MVC. Si la situation a empiré parce que les cadres populaires sont maintenant si bien intégrés, essayer de faire quelque chose de différent semble tout simplement mal.
Les points que j'ai soulevés sont toujours valables. Donner à la vue l'accès au modèle présente des avantages significatifs que je ne répéterai pas ici. Jetez un coup d'œil à mon précédent article si vous êtes intéressé.
Les exemples de code que j'ai donnés ne sont pas assez clairs ou complets. J'ai passé plus de temps à me concentrer sur les concepts qu'à donner des exemples de code. Je vais rectifier cela ici.
En plus d'écrire des exemples plus complets, il est temps de faire une mise à jour avec les nouvelles fonctionnalités de PHP et les tendances de programmation comme l'immutabilité. Je vais vous montrer comment écrire une structure MVC complètement immuable. Ensuite, je vous montrerai des contrôleurs réutilisables et un système de routage.
Outre l'utilisation de nouvelles fonctionnalités de PHP telles que l'indication du type de retour (ndlr, PHP 7.0), les deux principaux changements que j'ai apportés à mon style de codage sont l'immutabilité et le fait de privilégier les arguments par rapport aux constructeurs ayant des propriétés connexes.
22.2. Hello World
Voici une démonstration du trinome MVC en immutable à travers le fichier mvc.php dont voici le contenu :
class Model
{
public $text;
public function __construct()
{
$this->text = 'Hello world!';
}
}
class View
{
private $model;
public function __construct(Model $model)
{
$this->model = $model;
}
public function output()
{
return '<a href="mvc.php?action=textclicked">' . $this->model->text . '</a>';
}
}
class Controller
{
private $model;
public function __construct(Model $model)
{
$this->model = $model;
}
public function textClicked()
{
$this->model->text = 'Text Updated';
}
}
$model = new Model();
// It is important that the controller and the view share the model.
$controller = new Controller($model);
$view = new View($model);
if (isset($_GET['action'])) $controller->{$_GET['action']}();
echo $view->output();
22.3. Le modèle
Dans cet exemple, le modèle est mutable mais les autres composants ne le sont pas. Tout d'abord, rendons le modèle immuable :
class Model
{
private $text;
public function __construct($text = 'Hello World')
{
$this->text = $text;
}
public function getText()
{
return $this->text;
}
public function setText($text)
{
return new Model($text);
}
}
Il n'est plus possible de modifier l'état du modèle une fois qu'il a été instancié. L'appel de la méthode setText crée une nouvelle instance. Tout endroit où l'instance originale est référencée ne verra pas de changement dans l'état du modèle. Le contrôleur et la vue devront être mis à jour pour utiliser cette nouvelle classe de modèle.
Auparavant, le modèle et le contrôleur partageaient une instance de modèle. Le contrôleur modifiait l'état du modèle et la vue lisait l'état de l'instance de modèle. Maintenant que le modèle est immuable, nous ne pouvons plus compter sur le fait que la vue et le contrôleur partagent la même instance de modèle. Au lieu de cela, l'action du contrôleur retournera une instance de modèle qui sera ensuite transmise à la vue.
22.4. Le contrôleur
Au lieu de mettre à jour l'état d'une instance de modèle existante et mutable, le contrôleur renvoie une nouvelle instance de modèle après avoir effectué ses modifications :
class Controller
{
public function textClicked(Model $model): Model
{
return $model->setText('Text Clicked');
}
}
Plutôt que de m'appuyer sur les constructeurs et les propriétés, j'ai choisi d'utiliser des arguments pour faire passer le modèle qui va être mis à jour. L'action du contrôleur reçoit maintenant une instance de modèle immuable et renvoie une nouvelle instance avec les modifications nécessaires.
Il est à noter que cela ne fait pas partie de MVC, l'utilisation d'arguments de constructeur avec des propriétés liées ou d'arguments à l'action de contrôleur ont le même effet. Mais en évitant les constructeurs, il n'est plus nécessaire de conserver le modèle comme une propriété et le contrôleur est considérablement simplifié. Moins de code pour faire le même travail est toujours une bonne chose.
22.5. La vue
La vue nécessite peu de modification pour arriver au même résultat (plus besoin de l'attribut publique).
class View
{
public function output(Model $model)
{
return '<a href="mvc.php?action=textClicked">' . $model->getText() . '</a>';
}
}
L'avantage de faire passer le modèle en argument est que une seule instance de View permet d'afficher autant de modèles que l'on souhaite.
22.6. Mise en place du trinome
Il ne reste plus qu'à instancier tout ce petit monde, plus besoin de passer le modèle aux constructeurs de la View et du Controller.
<?php
$model = new Model();
$controller = new Controller();
$view = new View();
if (isset($_GET['action']))
{
$model = $controller->{$_GET['action']}($model);
}
echo $view->output($model);
Dans cette version immuable, le modèle est passé au contrôleur qui construit une nouvelle instance du modèle qui est ensuite passé à la vue.
22.7. Conclusion
Cette implémentation immuable de MVC présente certains avantages par rapport à la version mutable :
L'état est mieux géré de sorte que l'application ne souffre pas d'une action à distance où le changement d'un objet à un endroit (dans le contrôleur) provoque ensuite des changements dans un composant apparemment sans rapport (la vue).
Il y a moins d'État dans l'ensemble. Il n'y a plus de références aux différents objets en plusieurs endroits. Le contrôleur et la vue n'ont plus de référence au modèle, on leur donne une instance avec laquelle travailler au moment où ils en ont besoin, pas avant.
-- Fin de la traduction --
Bien évidemment, ce code ne doit pas se retrouver de cette façon dans votre application car le paramètre HTTP action doit être vérifié (ie : on contrôle que l'utilisateur a le droit ou non de faire l'action).
22.8. Vue d'artiste
Attention : le schéma suivant ne représente pas vraiment la beauté conceptuelle derrière le MVC.
23. DML 0.1 : Dot Modeling Language
Le DML est une représentation simplifiée du diagramme de classe UML à base de petits ronds.
Chaque petit rond représente un type d'élément de la POO (interface, classe, trait, classe abstraite).
Cette représentation a pour objectif de détecter les problèmes de dépendances entre les classes de notre projet afin d'éviter des incohérences de conception et de produire du code spaghetti.
A la différence du diagramme de classe, les types de liens (extends, implements) ne sont pas représentés car ces liens sont induits par les éléments liés.
Une interface liée par une classe l'est forcément pas un lien d'implementation (implements). De même qu'une classe liée par une autre classe l'est forcément par un lien d'héritage (extends), ceci étant vrai pour les classes abstraites.
Le DML s'occupe uniquement de représenter les use entre les classes.
23.1. Légende du DML
L'idée du DML est d'offrir aux développeurs un outil simple pour prendre des décisions de conception. Cette représentation est plus rapide à mettre en oeuvre sur un tableau noir ou blanc, qu'il soit numérique ou non.
* les interfaces sont représentées par des ronds vides ;
* les classes abstraites sont représentées par des ronds marqués par un point central ou par des hachures ;
* les classes sont représentées par des ronds pleins ;
* les traits sont représentés par des ronds contenant un trait ou des rayures ;
* la dépendance entre les classes est marquée par un trait simple dont la longueur peut être nulle si le lien entre deux éléments est évident.
23.2. Implémentation en DML
En DML, une classe qui implémente une interface peut être représentée de la façon suivante.
Le lien est implicitement un implements.
Dans le cas où l'implémentation est classique (MyClass extends MyInterface), on peut coller les deux ronds ensemble.
23.3. Héritage en DML
En DML, une classe qui hérite d'une classe abstraite peut être représentée de la façon suivante.
Le lien est implicitement un extends.
23.4. Trait en DML
En DML, une classe qui utilise un trait peut être représentée de la façon suivante.
Le lien est un use.
23.5. Représentation générale.
Voici une réprésentation générale de tous les éléments. Il est tout à fait possible de mélanger les représentations courtes et longues des dépendances entre les classes.
Voici la même représentation avec cette fois-ci des liens de longueur nulle.
Le nom des éléments peut disparaitre, seuls les liens entre ceux-ci existent.
* Il est envisageable de changer la couleur de chaque élément pour savoir ce que représente chaque rond. (Par exemple, une couleur par composant/vendor/bundle)
* Il est possible aussi de changer la taille des ronds selon l'importance de la dépendance lors de la conception (par exemple, il n'est pas nécessaire d'afficher la dépendance avec la classe Request dans une classe Controller, au contraire de la dépendance avec un Service (Repository/WsClient/Manager/Provider/Transformer...), si on souhaite le faire, le rond peut être à peine visible).
23.6. DML centrée
Avec le DML, il est possible de représenter une seule classe afin de voir le nombre de dépendance qu'elle a. Plus le nombre de ronds qui gravitent autour du rond principal est grand, plus la classe est difficile à maintenir.
24. L'héritage
Une classe SubClass peut hériter d'une autre classe MainClass.
Elle hérite ainsi de toutes les méthodes et de tous les attributs.
Ceci évite de dupliquer du code et permet de faire de l'override en redéfinissant ci-besoin les attributs/méthodes.
24.1. Code PHP
interface MainInterface
{
public function method();
}
class MainClass implements MainInterface
{
public function method()
{
}
}
class FirstSubClass extends MainClass
{
public function method()
{
// Another implementation of the method.
}
}
class SecondSubClass extends MainClass
{
public function method()
{
// Another implementation of the method.
}
}
24.2. Schéma DML
25. Immutable MVC en PHP version 2019
Ceci est une proposition de traduction de la page Immutable MVC: MVC In PHP 2019 Edition (https://r.je/immutable-mvc-in-php) de Tom Butler, que j'estime être très doué.
A noter que je n'ai pas trouvé quelle était la licence sur le contenu de son article, il est bien entendu que cette page sera supprimée si la copie de l'article original est interdite.
-- Début de la traduction --
25.1. MVC Revisité
Les articles les plus populaires de ce site Web concernent le MVC, dont le plus ancien a maintenant près de dix ans. Beaucoup de choses ont changé au cours de cette période, et ma propre expérience s'est également enrichie. Ma précédente série d'articles sur les "accidents vasculaires cérébraux" s'est en quelque sorte éteinte parce que je voulais revenir en arrière et peaufiner les exemples des articles précédents.
Le PHP a beaucoup changé depuis que j'ai écrit ces articles. Je vais prendre un nouveau départ et donner des exemples de code plus propres, plus complets et plus modernes.
Neuf ans se sont écoulés depuis que j'ai écrit le premier article sur les MVC ici et j'ai quelques observations à faire à ce sujet en regardant en arrière :
L'écosystème du PHP n'a pas changé. Les gens confondent toujours l'approche "contrôleur au lieu de médiateur" et MVC. Si la situation a empiré parce que les cadres populaires sont maintenant si bien intégrés, essayer de faire quelque chose de différent semble tout simplement mal.
Les points que j'ai soulevés sont toujours valables. Donner à la vue l'accès au modèle présente des avantages significatifs que je ne répéterai pas ici. Jetez un coup d'œil à mon précédent article si vous êtes intéressé.
Les exemples de code que j'ai donnés ne sont pas assez clairs ou complets. J'ai passé plus de temps à me concentrer sur les concepts qu'à donner des exemples de code. Je vais rectifier cela ici.
En plus d'écrire des exemples plus complets, il est temps de faire une mise à jour avec les nouvelles fonctionnalités de PHP et les tendances de programmation comme l'immutabilité. Je vais vous montrer comment écrire une structure MVC complètement immuable. Ensuite, je vous montrerai des contrôleurs réutilisables et un système de routage.
Outre l'utilisation de nouvelles fonctionnalités de PHP telles que l'indication du type de retour (ndlr, PHP 7.0), les deux principaux changements que j'ai apportés à mon style de codage sont l'immutabilité et le fait de privilégier les arguments par rapport aux constructeurs ayant des propriétés connexes.
25.2. Hello World
Voici une démonstration du trinome MVC en immutable à travers le fichier mvc.php dont voici le contenu :
class Model
{
public $text;
public function __construct()
{
$this->text = 'Hello world!';
}
}
class View
{
private $model;
public function __construct(Model $model)
{
$this->model = $model;
}
public function output()
{
return '<a href="mvc.php?action=textclicked">' . $this->model->text . '</a>';
}
}
class Controller
{
private $model;
public function __construct(Model $model)
{
$this->model = $model;
}
public function textClicked()
{
$this->model->text = 'Text Updated';
}
}
$model = new Model();
// It is important that the controller and the view share the model.
$controller = new Controller($model);
$view = new View($model);
if (isset($_GET['action'])) $controller->{$_GET['action']}();
echo $view->output();
25.3. Le modèle
Dans cet exemple, le modèle est mutable mais les autres composants ne le sont pas. Tout d'abord, rendons le modèle immuable :
class Model
{
private $text;
public function __construct($text = 'Hello World')
{
$this->text = $text;
}
public function getText()
{
return $this->text;
}
public function setText($text)
{
return new Model($text);
}
}
Il n'est plus possible de modifier l'état du modèle une fois qu'il a été instancié. L'appel de la méthode setText crée une nouvelle instance. Tout endroit où l'instance originale est référencée ne verra pas de changement dans l'état du modèle. Le contrôleur et la vue devront être mis à jour pour utiliser cette nouvelle classe de modèle.
Auparavant, le modèle et le contrôleur partageaient une instance de modèle. Le contrôleur modifiait l'état du modèle et la vue lisait l'état de l'instance de modèle. Maintenant que le modèle est immuable, nous ne pouvons plus compter sur le fait que la vue et le contrôleur partagent la même instance de modèle. Au lieu de cela, l'action du contrôleur retournera une instance de modèle qui sera ensuite transmise à la vue.
25.4. Le contrôleur
Au lieu de mettre à jour l'état d'une instance de modèle existante et mutable, le contrôleur renvoie une nouvelle instance de modèle après avoir effectué ses modifications :
class Controller
{
public function textClicked(Model $model): Model
{
return $model->setText('Text Clicked');
}
}
Plutôt que de m'appuyer sur les constructeurs et les propriétés, j'ai choisi d'utiliser des arguments pour faire passer le modèle qui va être mis à jour. L'action du contrôleur reçoit maintenant une instance de modèle immuable et renvoie une nouvelle instance avec les modifications nécessaires.
Il est à noter que cela ne fait pas partie de MVC, l'utilisation d'arguments de constructeur avec des propriétés liées ou d'arguments à l'action de contrôleur ont le même effet. Mais en évitant les constructeurs, il n'est plus nécessaire de conserver le modèle comme une propriété et le contrôleur est considérablement simplifié. Moins de code pour faire le même travail est toujours une bonne chose.
25.5. La vue
La vue nécessite peu de modification pour arriver au même résultat (plus besoin de l'attribut publique).
class View
{
public function output(Model $model)
{
return '<a href="mvc.php?action=textClicked">' . $model->getText() . '</a>';
}
}
L'avantage de faire passer le modèle en argument est que une seule instance de View permet d'afficher autant de modèles que l'on souhaite.
25.6. Mise en place du trinome
Il ne reste plus qu'à instancier tout ce petit monde, plus besoin de passer le modèle aux constructeurs de la View et du Controller.
<?php
$model = new Model();
$controller = new Controller();
$view = new View();
if (isset($_GET['action']))
{
$model = $controller->{$_GET['action']}($model);
}
echo $view->output($model);
Dans cette version immuable, le modèle est passé au contrôleur qui construit une nouvelle instance du modèle qui est ensuite passé à la vue.
25.7. Conclusion
Cette implémentation immuable de MVC présente certains avantages par rapport à la version mutable :
L'état est mieux géré de sorte que l'application ne souffre pas d'une action à distance où le changement d'un objet à un endroit (dans le contrôleur) provoque ensuite des changements dans un composant apparemment sans rapport (la vue).
Il y a moins d'État dans l'ensemble. Il n'y a plus de références aux différents objets en plusieurs endroits. Le contrôleur et la vue n'ont plus de référence au modèle, on leur donne une instance avec laquelle travailler au moment où ils en ont besoin, pas avant.
-- Fin de la traduction --
Bien évidemment, ce code ne doit pas se retrouver de cette façon dans votre application car le paramètre HTTP action doit être vérifié (ie : on contrôle que l'utilisateur a le droit ou non de faire l'action).
25.8. Vue d'artiste
Attention : le schéma suivant ne représente pas vraiment la beauté conceptuelle derrière le MVC.
26. Architecture des fichiers du projet
26.1. Architectures des dossiers en Symfony 4/5
Voilà un topic sur le sujet :
https://stackoverflow.com/questions/47594542/symfony-4-how-to-organize-folder-structure-namely-your-business-logic
Deux solutions sortent du lot avec des arguments différents.
26.1.1 Solution 1
-src
- Controller
- Core
- Todos
- ...
- Entity
- Core
- Todos
- ...
- Repository
- Core
- Todos
- Validator (or other Symfony oriented components)
- Core
- Todos
- Others (depend on project)
- Core
- Todos
L'avantage d'une telle arborescence est de retrouver toutes les implémentations d'une certaine catégorie dans un dossier principal.
Le problème est par contre le suivant : si on souhaite supprimer une fonctionnalité, il faut supprimer dans chaque dossier les fichiers qui concernent la fonctionnalité, on ne distingue pas un découpage simple des différents domaines de l'application.
26.1.2 Solution 2
├─ assets/
├─ bin/
│ └─ console
├─ config/
│ ├─ doctrine/
│ │ ├─ core/
│ │ └─ sample/
│ ├─ packages/
│ ├─ routes/
│ └─ validator/
│ │ ├─ core/
│ │ └─ sample/
├─ public/
│ └─ index.php
├─ src/
│ ├─ Core/
│ │ ├─ Controller/
│ │ ├─ Entity/
│ │ ├─ Repository/
│ │ └─ ...
│ ├─ Sample/
│ └─ ...
├─ templates/
│ ├─ core/
│ └─ sample/
├─ tests/
├─ translations/
├─ var/
│ ├─ cache/
│ ├─ log/
│ └─ ...
└─ vendor/
L'avantage d'une telle solution est qu'il est plus simple de retrouver tous les fichiers qui concernent une fonctionnalité. Par contre la configuration peut être plus compliquée ; dans le cas de doctrine, il ne suffit plus de dire que toutes les classes d'entités sont dans src/Entity/*(/*), mais plutôt dans src/*/Entity/*(/*).
🧙♂️️Je préfère cette solution-ci qui permet d'avoir un lien simple entre une url /domain et un dossier src/Domain.
26.2. Projet monolithique
Attention : ici, je distingue un projet monolithique et une application monolithique.
Le projet est pour moi l'ensemble des fichiers que les développeurs manipulent pour construire toutes les applications.
Chaque application ne doit être déployée qu'avec son code source.
Qu'est ce qu'il est plus simple : gérer dans son équipe un projet par application ou un repository git (qui peut être un composite de plusieurs .git) pour toutes les applications ?
La réponse semble évidente, et pourtant on a tendance à se dire que chaque application doit avoir son propre repository.
Alors, éventuellement, il est possible de configurer son IDE pour intégrer plusieurs repository .git
Voilà à quoi pourrait ressembler un projet contenant plusieurs applications :
application_[n]
/src
/config
/routes
/templates
/var
/cache
/log
/vendor
composer.json
core
/config
/domains
/Common
/Infrastructure
/Doctrine
/RabbitMq
/Elasticsearch
/Service
/Utility
/Domain_[n]
/Enumeration
/Helper
/Infrastructure
/Model
/Service
/Utility
/vendor
/composer.json
A noter que,
* l'utilisation du framework est liée à une application, cette application peut être native PHP ou non ;
* tout ce qui se trouve dans le dossier 'core' est frameworkless, c'est à dire que le code ne devrait pas dépendre d'un framework ou d'un composant tier (vendor) ; ce qui est potentiellement difficile à respecter.
Ce qui veut dire qu'il est possible, sous réserve évidemment que le framework/CMS tourne sous la même version de PHP que le code, de travailler sur plusieurs applications en gardant un espace propre à son métier (dossier core).
Il est possible d'avoir un .git au niveau de chaque application et d'avoir un gestionnaire de version de cette forme :
-company/application_1
-company/application_2
-company/application_3
-company/core
Bien sûr, si vos applications mélangent plusieurs technologies (ex: PHP/NodeJs/J2E), il faudra peut-être éventuellement avoir autant d'espaces de travail distincts.
27. Créer une extension PHP
💣️N'étant pas du tout un expert sur le sujet, tout ce qui suit est à prendre avec des pincettes.
Voici un petit tutoriel pour créer une extension PHP (sous Debian).
27.1. Les ressources
Le site officiel de Zend(https://www.zend.com/resources/writing-php-extensions)
La page Github d'un squelette d'extension PHP(https://github.com/improved-php-library/skeleton-php-ext)
27.2. Exemple : création de la fonction PHP triple
Voici un exemple d'implémentation d'une nouvelle fonction, appelée `triple` qui retourne un entier multiplié par trois.
Ce code est à placer dans le fichier skeleton.c du projet skeleton-php-ext.
ZEND_BEGIN_ARG_INFO_EX(arginfo_triple, 0, 1, 0)
ZEND_ARG_TYPE_INFO(0, str, IS_LONG, 1)
ZEND_END_ARG_INFO()
static const zend_function_entry functions[] = {
PHP_FE(triple, arginfo_triple)
PHP_FE_END
};
PHP_FUNCTION(triple)
{
zend_long integer;
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
Z_PARAM_LONG(integer)
ZEND_PARSE_PARAMETERS_END();
RETVAL_LONG(integer*3);
}
Il faut également modifier le fichier php_skeleton.h pour ajouter le nom de la nouvelle fonction :
static PHP_FUNCTION(triple);
27.3. Exemple : création de la fonction PHP addition
Cela se complique un peu car nous allons créer maintenant une fonction avec deux arguments.
static const zend_function_entry functions[] = {
PHP_FE(addition, arginfo_addition)
PHP_FE_END
};
ZEND_BEGIN_ARG_INFO_EX(arginfo_addition, 0, 1, 0)
ZEND_ARG_TYPE_INFO(0, a, IS_LONG, 1)
ZEND_ARG_TYPE_INFO(0, b, IS_LONG, 1)
ZEND_END_ARG_INFO()
PHP_FUNCTION(addition)
{
zend_long a;
zend_long b;
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2)
Z_PARAM_LONG(a)
Z_PARAM_LONG(b)
ZEND_PARSE_PARAMETERS_END();
RETVAL_LONG(a+b);
}
Il faut également modifier le fichier php_skeleton.h pour ajouter le nom de la nouvelle fonction :
static PHP_FUNCTION(addition);
27.4. Exemple : création de la fonction PHP guessThenumber
La règle : l'utilisateur indique un nombre, si le nombre est plus grand que le nombre à deviner, la fonction retourne -1, si le nombre est le même, la fonction retourne 0, et si le nombre est plus petit, la fonction retourne 1.
📖️️L'idée de ce petit jeu provient de ce TP en C sur le site d'OpenClassRooms(https://openclassrooms.com/fr/courses/19980-apprenez-a-programmer-en-c/14828-tp-plus-ou-moins-votre-premier-jeu).
Pour cet exemple, on peut utiliser une variable globale qui sera initialisée qu'une seule fois par la fonction guessTheNumber.
ZEND_BEGIN_ARG_INFO_EX(arginfo_guessTheNumber, 0, 1, 0)
ZEND_END_ARG_INFO()
static const zend_function_entry functions[] = {
PHP_FE(guessTheNumber, arginfo_guessTheNumber)
PHP_FE_END
};
zend_long THE_NUMBER_TO_FIND;
PHP_FUNCTION(guessTheNumber)
{
zend_long guess_long;
if (!THE_NUMBER_TO_FIND) {
THE_NUMBER_TO_FIND = rand() % 101;
}
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
Z_PARAM_LONG(guess_long)
ZEND_PARSE_PARAMETERS_END();
zend_long guessed_status;
if (THE_NUMBER_TO_FIND > guess_long) {
guessed_status = 1;
} else if (THE_NUMBER_TO_FIND < guess_long) {
guessed_status = -1;
} else {
guessed_status = 0;
}
RETVAL_LONG(guessed_status);
}
#endif
Il faut également modifier le fichier php_skeleton.h pour ajouter le nom de la nouvelle fonction :
static PHP_FUNCTION(guessTheNumber);
En pratique voilà ce que ça donnerait :
# php -a
php > var_dump(guessTheNumber(40));
int(-1)
php > var_dump(guessTheNumber(30));
int(-1)
php > var_dump(guessTheNumber(28));
int(0)
php > var_dump(guessTheNumber(28));
int(0)
On peut éventuellement faire un reset du nombre à trouver dès qu'il est trouvé pour que l'amusement perdure indéfiniment.
27.4.0.1 Création de la fonction C resetTheNumber
zend_long THE_NUMBER_TO_FIND;
void resetTheNumber()
{
THE_NUMBER_TO_FIND = rand() % 101;
}
PHP_FUNCTION(resetTheNumber)
{
resetTheNumber();
}
PHP_FUNCTION(guessTheNumber)
{
zend_long guess_long;
if (!THE_NUMBER_TO_FIND) {
resetTheNumber();
}
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
Z_PARAM_LONG(guess_long)
ZEND_PARSE_PARAMETERS_END();
zend_long guessed_status;
if (THE_NUMBER_TO_FIND > guess_long) {
guessed_status = 1;
} else if (THE_NUMBER_TO_FIND < guess_long) {
guessed_status = -1;
} else {
guessed_status = 0;
}
RETVAL_LONG(guessed_status);
}
27.5. Signatures des fonctions ZEND
Voici la signature de la fonction ZEND_PARSE_PARAMETERS_START_EX
define ZEND_PARSE_PARAMETERS_START_EX(flags, min_num_args, max_num_args)
28. Encodage
28.1. Les 3 petites règles pratiques
Voici 3 petites règles qui vont vous permettre de comprendre le problème d'affichage des caractères spéciaux et accentués.
Si dans votre page web vos accents ont la forme de "�", c'est que le texte est encodé en ISO et que le navigateur l'affiche en UTF-8.
Si dans votre page web vos accents ont la forme de "é", "î" ou "Ã", c'est que le texte est encodé en UTF-8 et que votre navigateur l'affiche en ISO.
Si dans votre page web vos accents ont la forme de "", c'est que généralement le texte est encodé en WINDOWS-1252 et que le navigateur l'affiche en UTF-8.
📖️️https://outils-javascript.aliasdmc.fr/generateur-caracteres-speciaux-accentues-html-css-js.php?#formConversion
29. Faire un projet de qualité sans framework
29.1. Contraintes
C'est possible à la condition de respecter strictement plusieurs choses :
* toutes les PSR doivent être appliquées à la lettre ;
* le langage est toujours à la dernière version ;
* les librairies sont toujours à jour (cela ne doit pas empêcher pas d'être prudent).
29.2. Démarrer un projet
Nous avons vu que la PSR-1 nous imposait si on faisait de la POO d'utiliser les namespaces.
Même en PHP 8.0, il ne semble pas y avoir d'autoloader par défaut qui puisse nous permettre d'utiliser les différents use.
29.2.1 Créer un autoload via spl_autoload_register
29.2.1.1 Créer le fichier autoload.php
On peut créer un fichier autoload.php et y écrire ceci :
<?php
spl_autoload_register(function ($className) {
$className = str_replace('\\', DIRECTORY_SEPARATOR, $className);
$filename = __DIR__ . DIRECTORY_SEPARATOR . $className . '.php';
if (is_readable($filename)) {
require_once($filename);
}
});
29.2.1.2 Importer l'autoload dans l'index.php
Puis importer ce fichier dans un index.php de la façon suivante :
<?php
require_once('autoload.php');
Après quoi il sera possible dans le fichier index.php d'utiliser des use, eux-mêmes utilisant des use et des namespace.
29.2.1.3 Exemple d'arborescence
App\
Service\
MyService.php
index.php
autoload.php
Avec MyService.php :
<?php
namespace App\Service;
class MyService
{
public static function myMethod(): string
{
return 'ok';
}
}
et index.php :
<?php
require_once('autoload.php');
use App\Service\MyService;
echo MyService::myMethod();
Attention à bien respecter la PSR-4 : le respect de la casse est indispensable.
29.2.1.4 Défaut d'un autoload personnel
L'autoload doit être mis à jour dès qu'il y a des nouveautés en terme de PSR et de montée de version du langage.
Par exemple, en PHP 7.0 est introduit la possibilité de faire des use multiples, ce que ne prend pas en compte l'autoload du dessus.
29.2.2 Utiliser l'autoloader de Composer
Quoi de plus simple que d'utiliser l'autoloader certainement le plus populaire et le plus utilisé du moment ?
29.2.2.1 On commence par créer le composer.json à la racine de son projet
{
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"App\\": "App/"
}
}
}
Puis on crée un projet via la commande suivante :
composer create-project
Cette commande va générer le dossier vendor et surtout le fichier autoload.php que l'on peut inclure dans son index.php
On peut alors utiliser les uses multiples.
<?php
require ('autoload.php');
use App\Service\{MyClass, MyOther};
MyClass::myMethod();
MyOther::myMethod();
29.2.2.2 Matcher le namespace App et le dossier src
Si vous souhaitez que toutes les classes définies dans le dossier src de votre projet correspondent au namespace App, il vous faut pour cela modifier la propriété autoload du composer.jon
{
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Puis faire une mise à jour de l'autoload via :
composer update
Il est alors possible d'utiliser l'arborescence suivante :
src\
Service\
MyService.php
index.php
autoload.php
En gardant le namespace App.
Cela semble assez antinomique que la PS4 autorise ce type de mapping mais c'est une solution plus souple que de contraindre les namespaces aux noms de dossiers.
29.2.3 Que faire sans un moteur de template ?
Sur un projet qui n'aurait pas la chance d'avoir un moteur de template évolué (twig/smarty pour les plus connus), on peut éventuellement s'en sortir en faisait quelque chose comme la solution que je présente dans la page sur "les différentes façons de construire une page HTML".
30. Installer rapidement un serveur PHP en local avec nginx
Le couple nginx+php-fpm est une bonne alternative à apache2.
# Fichier de configuration nginx
30.1. Ajouter le nouveau domaine dans le fichier hosts
vim /etc/hosts
127.0.0.1 new_domain.local
30.2. Créer le fichier de configuration nginx
Voici un exemple du fichier de configuration nginx à créer pour le nouveau domaine :
vim /etc/nginx/sites-enabled/new_domain
server {
server_name new_domain.local;
root /var/www/html/new_domain;
location / {
# try to serve file directly, fallback to index.php
try_files $uri /index.php$is_args$args;
}
location ~ ^/(.*)\.php(/|$) {
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
}
error_log /var/log/nginx/new_domain_error.log;
access_log /var/log/nginx/new_domain_access.log;
}
30.3. Redémarrer nginx
On peut utiliser reload au lieu de restart :
sudo service nginx reload
30.3.1 Acceder au site :
Ouvrir le navigateur web et taper :
http://new_domain.local/
31. Les différentes façons de construire une page HTML.
Alors que le PHP est certainement le langage le plus populaire pour faire du web, la façon dont le contenu des pages est généré est, plus de 25 ans après sa création, encore très archaïque et disons-le franchement, pas très pro ! C'est pour dire : d'un côté on a les 23 design patterns écrits par 4 types il y a trois siècles dont les règles sont toujours aussi pertinentes, et de l'autre il y a l'espèce de chose visqueuse dont on se sert pour faire quelques balises HTML qui bave de tous les côtés. Cet article fait un peu le tour des façons les plus courantes de créer un contenu HTML à partir du PHP.
31.1. La méthode old school
Il est presque inutile de la présenter car tout développeur l'a rencontrée. Je veux bien sûr parler de cette façon-ci de faire : on dispose d'un fichier PHP et on passe notre temps à ouvrir et à fermer les balises <?php?> pour y intégrer notre code HTML.
De cette façon exactement :
<?php
$myArray = ['chaussure', 'chocolat'];
foreach ($myArray as $item) {
?>
<li><?php echo $item;?></li>
<?php
}
Il va sans dire que si l'on commence en plus à y intégrer du code JS, cela devient vite un cauchemar à gérer (même si c'est à première vue malin de créer du code JS dynamique).
L'indentation est difficilement respectable (on mélange forcément l'indentation du PHP et l'indentation du HTML) ;
minimiser le contenu de la page HTML est compliqué : il faudrait récupérer ce que génère le PHP et le convertir, ce qui demande de configurer son serveur web pour le faire via des modules spécifiques alors que cela pourrait être fait en PHP directement ;
l'injection de code malveillant est plus probable avec ce genre de code car si l'on commence à injecter des valeurs provenant des utilisateurs (ex: la liste de vos plats préférés, plat que l'utilisateur peut librement écrire, TODO: écrire un article sur l'injection XSS), celui-ci pourra modifier le DOM de notre page sans trop de difficulté. Il faudrait éventuellement utiliser des fonctions de sécurité un peu partout, ce qui serait assez chronophage, inesthétique et source d'oubli ;
la duplication de code est facilitée. Dès lors, quid de l'ajout d'attributs HTML sur nos éléments ? Faut-il revenir sur tous nos fichiers PHP en faisant une recherche globale de chaque élément ?
31.2. La méthode via echo et autres
Je ne saurais vraiment dire si c'est pire que la version oldschool, peut-être que l'avantage est de toujours pouvoir encapsuler la génération du HTML dans des fonctions.
On ferait comme ceci :
<?php
$myArray = ['chaussure', 'chocolat'];
foreach ($myArray as $item) {
echo sprintf('<li>%s</li>', $item);
}
Ici c'est toujours très mauvais, cependant on peut évacuer plusieurs problèmes induits par la solution oldchool, notamment celle de l'injection HTML par l'utilisateur. Il est en effet possible d'adapter notre code de cette façon-ci :
<?php
$myArray = ['chaussure', 'chocolat'];
foreach ($myArray as $item) {
echo makeHTMLLine($item);
}
function makeHTMLLine(string $item): string
{
return sprintf('<li>%s</li>', htmlentities($item));
}
On peut imaginer construire tout un tas de fonctions pour créer nos éléments HTML qui appelleraient une fonction d’échappement (ici il s'agit de htmlentities, il vaut mieux ne l'écrire qu'une seule et unique fois dans tout son code pour potentiellement protéger l'ensemble du site).
Au passage, il ne serait pas élégant d'intégrer le echo dans la fonction, mieux vaut lui aussi ne l'écrire qu'une et unique fois dans son code et ne l'appeler qu'à la fin de l’exécution.
31.3. Empile moi du HTML
Troisième solution donc, qui est d'une certaine façon une amélioration de la seconde : on construit le contenu de son HTML que l'on stocke dans une variable $htmlContent puis on l'affiche à la fin de l’exécution du programme.
Ce n'est certes toujours pas l'idéal mais on peut éventuellement imaginer que cette variable contient dans son initialisation l'ensemble du squelette de la page HTML que l'on souhaite construire (ie. toutes les balises de type meta, style, jusqu'au body).
31.4. Mon premier template HTML
On peut même imaginer que ce squelette est dans un fichier squelette.html, chargé via une méthode de type file_get_contents puis modifié à coup de str_replace.
31.4.1 Exemple d'un tel fichier squelette.html
<html>
<body>{{body}}</body>
</html>
31.4.2 On ferait ensuite ce genre de code PHP :
<?php
$htmlContent = file_get_contents('squelette.html');
$htmlContent = str_replace('{{body}}', '<div>mon contenu</div>', $htmlContent);
echo $htmlContent;
31.4.3 Pour avoir ce résultat en sortie de l’exécution :
<html>
<body><div>mon contenu</div></body>
</html>
Les symboles moustaches moustaches {{}} peuvent bien sûr être remplacés par n'importe quoi d'autre, sous réserve que ce ne soient pas des caractères fréquents dans les mots syntaxiques du HTML. Vous pouvez essayer "<<>>" mais ce ne serait pas forcément judicieux, rien que pour la lisibilité.
En outre, on peut tout à fait imaginer que certains morceaux de squelette soient enregistrés dans des fichiers secondaires afin d'être réutilisés plus tard, par exemple le contenu du pied de page, le menu, l'entête, ou même des blocs indépendants (carrousel d'images, boutons etc...).
Rien qu'avec ces quelques bouts de code, on peut dire qu'on a conçu un moteur de template ! Avec ici une règle assez simple : remplacer le pattern {{body}} par du contenu dynamique.
Mais, cela se complique fortement dès que l'on souhaite ajouter des conditions sur notre contenu, faire des boucles ou encore de l'héritage entre les pages. Ecrire un tel moteur s'avère alors plus difficile. Mais certains l'ont fait.
31.5. Système de template avec la force de PHP
L'implémentation d'un moteur qui ne ferait que de l'injection de valeurs trouve vite ses limites. Il n'est en effet non possible de personnaliser son HTML en fonction des variables PHP (comme faire des listes, afficher un message différent en fonction d'une condition etc...).
Pour pallier à ceci, on peut créer un fichier de template qui ressemblerait à un template HTML mais qui serait en fait une fonction PHP.
On peut éventuellement s'en sortir de cette façon-ci via un Templater maison :
class Templater
{
public static function render($tplName, $parameters)
{
$tplFilePath = __DIR__.'/template/'.$tplName.'.php';
if (!is_file($tplFilePath)) {
throw new \Exception('Template '.$tplFilePath.' is not found.');
}
$rendering = require('template/'.$tplName.'.php');
return $rendering($parameters);
}
}
Et créer un fichier de template comme 'page.php' par exemple :
<?php $page = function($p) { ?>
<html>
<head></head>
<body><?php echo $p['name'];?></body>
</html>
<?php } ?>
On l'appelerait ensuite de cette façon-ci :
require_once('Templater.php');
echo Templater::render('page', array(
'name' => 'dmeloni',
));
Ce n'est pas ce qu'il y a de plus classe mais l'avantage reste tout de même d'utiliser la puissance de PHP dans un template (donc de pouvoir profiter des switch (ça n'existe toujours pas en twig), du caractère triple égal (===), du foreach et du for classique (plutôt qu'un loop.index pour savoir où on est dans le tableau) et des constances de classes MyClass::CONSTANCE_NAME plutôt que d'utiliser la chose un peu barbare qui est const('App\\Domain\\...\\MyClass::CONSTANCE_NAME')).
Par contre, on perd la grande force de l'héritage des templates...donc dès que notre application dépasse une seule page HTML, la solution full PHP deviendra vite limitante (sauf à réécrire soi-même un moteur de template plus évolué...) afin d'éviter le copier coller des balises HTML communes entre les pages.
31.6. Moteurs de templates populaires
Pour n'en citer que deux, Twig et Smarty.
Alors autant le dire franchement, c'est certainement mieux que toutes les autres solutions dont on vient de parler, mais cela reste d'assez mauvaises solutions.
Car avec ce genre de moteur, on en revient à écrire des choses qui finalement sont extrêmement similaires avec notre solution old school.
Pour preuve, comparez ce qui suit.
31.6.1 Solution old school
<?php
$myArray = ['chaussure', 'chocolat'];
foreach ($myArray as $item) {
?>
<li><?php echo $item;?></li>
<?php
}
31.6.2 Solution new school
En Twig 2 et 3
{% foreach item in myArray %}
<li>{{item}}</li>
{% endfor %}
En Smarty 3 (je ne sais plus pour la 2, surement que cela marche aussi)
{foreach $myArray as $item}
<li>{$item}</li>
{/foreach}
Autrement dit, on fait exactement la même erreur avec ces moteurs de templates qu'avec du PHP de base : on mélange la logique et le squelette de la page, et ça c'est mal car l'intégration du HTML n'en sera que plus compliqué puisque l'on va mélanger des conditions, des boucles et pire parfois des instances de classes PHP que l'on injecterait dans le template comme ci de rien n'était avec de l'affichage de listes, de tableaux, d'images etc..
Donc quid dès lors que l'on revient sur la logique (ie, les règles métiers) ?: Il faut revenir sur tous nos templates et prendre le risque de faire du HTML invalide ou même des templates qui vont faire planter la page puisque telle ou telle variable renverra un null pointer exception ou quoique cela puisse être du même style, sérieusement une exception lancée au moment de la génération de la page ?! Est-ce vraiment sérieux ?
Donc, dans l'idéal, il faut séparer le template du code métier. Cela permettra dans le cadre d'une entreprise par exemple de permettre à l'intégrateur HTML de faire le design de ses pages sans se poser de question sur comment sont faits les objets manipulés par l'application.
Pour faire un autre parallèle qui ne divise quasiment plus les développeurs web : on ne mélange pas non plus le CSS et le HTML.
31.6.3 Les moteurs de templates bientôt populaires
Pour faire bref : ce sont ceux qui permettent d'injecter des valeurs dans notre template via des pointeurs au niveau du DOM (ex: Xpath ou règles CSS)
Exemple, voici le squelette de la page :
<html>
<ul><li></li</ul>
</html>
Et une proposition de syntaxe :
newPopularTemplateEngine->do('repeat item in items to the li element', $items, $templateContent);
Contrairement aux moteurs classiques où l'on injecte la plupart du temps un tableau de variables que l'on réutilise dans le contenu du template, ici le squelette de la page doit pouvoir être ouvert via le navigateur, avec un contenu Lorem Ipsum pourquoi pas puisque celui-ci serait effacé par le moteur au moment de l'injection du contenu.
Le TAL est un langage qui est une réponse à cette problématique, il est implémenté dans plusieurs langages de programmation. Son approche est par contre un peu austère car les pointeurs sont écrits en XML avec des tags propres.
Le Haml est également une solution via des pointeurs proches du CSS.
Transphporm est surement pour l'heure la solution la plus porteuse d'espoir, il est fort probable que son nom apparaisse un de ces jours dans la liste du Awesome PHP
A noter que le moteur nommé Plates fait pale figure comparé à ces deux-là car il est très proche de la philosophie derrière les JSP.
32. Les types
Les types les plus courants en PHP sont les suivants :
* bool
* float (aussi appelé double)
* int
* string
* array
* object (qui représente l'abstraction de toutes les instances de classe)
Il existe aussi le non type null.
32.1. Le null
Le null est par définition ce qui est différent du non null. Ce qui est particulier avec le null, c'est qu'il y a une ambiguité car une variable à laquelle on associe une valeur nulle a également un type null.
Dès lors, si on définit une valeur et donc le type d'une variable, le null est une valeur qui ne devrait plus être permise.
32.1.1 Eviter le null en signature de méthode ?
Idéalement, moins il y a de types différents en retour de méthode, et plus simple sera l'utilisation de celle-ci par les utilisateurs, car les cas seront moins nombreux à gérer : il vaudrait donc mieux éviter d'utiliser le null dans son code car cela peut poser pas mal de problèmes lorsque la valeur nulle d'une variable se répand à travers les différentes méthodes. En pratique, cela ne simplifie pas pour autant le code et donc tout le reste (maintenabilité, temps d'exécution...).
Plutôt que d'écrire ceci :
function getMyArray()
{
if (/* condition pouvant valoir false */) {
return ['key' => 'value'];
}
return null;
}
On pourrait écrire cela :
function getMyArray()
{
$myArray = [];
if (/* condition pouvant valoir false */) {
$myArray = ['key' => 'value'];
}
return $myArray;
}
Ici, on garantit à l'utilisateur de la méthode getMyArray que le retour sera toujours un tableau, il pourra alors utiliser des fonctions d'itérations sur le retour de getMyArray sans avoir besoin de faire une vérification du retour comme is_array.
Evidemment, au niveau de l'exécution, la valeur affectée à myArray sera un tableau, donc cela consommera de fait plus de mémoire vive, mais ce point est clairement négligeable comparé au reste de la plupart des projets.
Là où cela devient moins pertinent de garantir un seul type de sortie, c'est dans le cas par exemple où la méthode doit chercher une entité stockée en base de données, il serait peut-être plus pertinent d'utiliser une méthode supplémentaire pour compter par exemple le nombre de cette entité en base. Cette dernière méthode pourrait en outre stocker dans un cache les données de l'entité pour épargner une requête supplémentaire à la base.
Voici une proposition, admettons qu'on ait cette méthode :
(findMyRessource($id) : myRessource);
Si on code avec l'usage du null, on aura plutôt ce genre de signature ci :
(findMyResource($id) : myResource|null);
et donc l'utilisateur sera obligé de tester que le retour ne vaut pas null afin d'utiliser cette ressource (par exemple : afficher une représentation de celle-ci à l'écran).
Il pourra faire ça de cette façon-là :
// Bout de code d'un controlleur Symfony qui récupère les infos de la ressource avant de la passer au template géré par Twig.
return $this->render('xxx.twig.html', [
'myResource' => findMyResource($id)
]);
{# Code Twig #}
{% if myResource is not null %}
<span>{{myResource.content}}</span>
{% else %}
<span>No content</span>
{% endif %}
Ici, il doit gérer le fait que la ressource puisse avoir deux types différents (le null et object (myInstance)).
Pour éviter l'usage du null, on pourrait créer une autre méthode afin que la méthode findMyResource n'ait qu'un seul type de retour :
(myResourceExists($id) : bool);
(findMyResource($id) : myResource);
La première méthode retournera true ou false selon l'existence de la ressource, et la seconde méthode retournera la ressource en elle-même. Cependant, cela oblige de mettre des gardes fous dans les deux méthodes afin de retourner des Exceptions dans le cas où quelque chose se passe mal (récupération dans le cache par exemple qui ne fonctionnerait plus).
Et donc on aurait deux façons d'utiliser cela, soit directement dans le template :
$myResourceExists = myResourceExists($id);
if ($myResourceExists) {
$myResource = findMyRessource($id);
}
return $this->render('xxx.twig.html', [
'id' => $id,
]);
{# Code Twig #}
{% if myResourceExists(id) #}
<span>{{findMyRessource($id).content}}</span>
{% else #}
<span>No content</span>
{% endif #}
Cela demande au passage de créer deux fonctions Twigs.
Soit dans le controlleur :
$myResource = null;
$myResourceExists = myResourceExists($id);
if ($myResourceExists) {
$myResource = findMyRessource($id);
}
return $this->render('xxx.twig.html', [
'myResource' => $myResource,
]);
{# Code Twig #}
{% if myResource is not null #}
<span>{{myResource.content}}</span>
{% else #}
<span>No content</span>
{% endif #}
On voit ici les limites de la logique derrière l'idée de ne pas utiliser le null : on augmente le nombre de lignes de code, ainsi que leur complexité, en augmentant les risques de plantage
32.1.2 Comparer deux null entre eux
Ce genre de code par ailleurs n'a pas vraiment de sens :
$variableA = null;
$variableB = null;
$variableA === $variableB; // ceci vaut le bool true.
Car un null ne représentant rien, il est impossible de comparer deux nulls.
Exemple : Un non nombre de pommes dans un non sac.
$appleNumbers = null;
$bag = null;
$bag === $appleNumbers;
Ici, du point de vue de l'interpréteur, la dernière ligne vaudra true, mais on comprend assez facilement que fonctionnellement parlant, cela n'a aucun sens.
Par contre, si on déclare une variable nommée $null qui sert d'étalon (pour un test unitaire par exemple), ça aurait éventuellement un sens (en PhpUnit, on utiliserait le AssertNull voire un AssertTrue(\is_null($var))).
33. Migrer une application de Symfony 3.4 à 5.4
Si votre application tourne sous Symfony 3.4 et que vous avez un peu de temps devant vous, je vous conseille de passer directement le code afin qu'il soit directement compatible avec Symfony 5.4 (actuellement la version stable de Symfony).
Voici quelques pièges à éviter pour que la migration soit la plus efficace possible :
33.1. Le fichier bundles.php
Le fichier bundles.php est le fichier où sont chargés tous les vendors spécifiques à Symfony (que l'on nomme toujours Bundle).
L'ordre des bundles définie la priorité de chargement.
C'est à dire que si l'un des vendors a une dépendance avec un autre, il est important de vérifier que le vendor nécessaire est chargé avant.
Ainsi, je vous conseille de commencer par toujours charger les composants Symfony avant les composants tiers.
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
My\Extra\MyExtraBundle\MyExtraBundle::class => ['all' => true],
];
Ici, le bundle MyExtraBundle est chargé après ses éventuelles dépendances.
Pour connaitre la liste des dépendances d'un Bundle, il suffit de regarder le composer.json du Bundle tier.
33.2. Les fichiers de config .yaml
Contrairement à Symfony 3.4 où la configuration se trouve dans le dossier app/config, en SF5.4, tout se trouve dans le dossier config.
Si vous avez besoin que les deux versions de l'application co-existent pendant un temps (ie. conserver les fichiers de configuration dans le dossier app/config), il est tout à fait possible d'importer les .yml de la version 3.4 dans les .yaml de la version 5.4.
Egalement, il faut faire attention à l'ordre de priorité des fichiers de configuration.
Par exemple, le fichier package/dev/special_config.yaml sera moins prioritaire que le fichier package/special_config.yaml et sera ignoré même si vous chargez le projet en mode dev.
Il faut alors :
- soit supprimer le fichier package/special_config.yaml et le recréer pour chaque environnement package/{environnement}/special_config.yaml
- soit supprimer le fichier package/dev/special_config.yaml et créer le fichier package/special_config_dev.yaml
33.3. Remplacement d'assetic par Webpack Encore
Pour installer Webpack Encore, il suffit de lire la doc officielle (
https://symfony.com/doc/current/frontend/encore/installation.html).
33.3.1 Piège : les fonctions de vos .js ne seront plus globales par défaut
Le gros piège avec cette migration est que le code de vos fichiers .js ne sera plus global. C'est à dire que si vous utilisez certaines fonctions directement dans votre html (soit avec la balise <script> soit dans les attributs HTML), elles ne pourront pas fonctionner si elles ne sont pas déclarées globlales.
Pour déclarer une fonction globale, il y a au moins trois façons de faire :
- soit vous utilisez le mot clef "global." qui est convertie par Webpack en fonction du contexte pour que la fonction soit disponible ;
- soit vous utilisez le mot clef "window." qui ne nécessite pas d'être convertie par Webpack et qui fonctionnera également dans la version Assetic du projet ;
- soit vous déclarez 'inline' le script directement dans le fichier de template twig.
33.3.1.1 Version compatible application sous Assetic ET application sous Webpack
Avant :
function toto(arg) {
// ...
}
Après :
window.toto = function(arg) {
// ...
}
33.3.1.2 Version compatible uniquement via Webpack
Avant :
function toto(arg) {
// ...
}
Après :
global.toto = function(arg) {
// ...
}
33.3.2 Piège : la transpilation sass interdit les extends dans les balises @media
Note : ce qui suit est à prendre avec des pincettes.
Alors qu'avec Assetic (et son transpileur sass), il est possible de faire des extends à l'intérieur de tag @media. Via webpack, cela n'est plus possible, il faut alors :
- soit sortir le extends du @media et faire un extends d'un pointeur déjà lié à un @media
@extends .justify-content-sm-center;
- soit mettre l'équivalence css directement dans le tag @media (eg: "justify-content:center;")
@media
{
justify-content:center;
}
33.3.3 Piège : jquery ou $ ne fonctionne plus
Egalement, en chargeant jquery via Webpack, l'alias $ ne sera plus disponible pour les autres scripts.
Il faut indiquer à Webpack de rendre disponible jQuery de façon globale.
Fichier app.js
import "jquery";
window.$ = window.JQuery = jQuery;
Note : il est possible aussi d'utiliser le mot clef "global" à la place de "window".
33.3.4 Piège : priorité des librairies
Egalement, l'ordre dans lequel sont déclarés les fichiers .js dans le app.js est important, il faut commencer par les librairies fondamentales.
(ex: jquery > jqueryui > bootstrap > bootstrap-plugin1... > librairie-js-quelconque)
33.4. Chargement des templates
Sous SF5.4, les fichiers de templates twig doivent normalement se trouver dans les dossiers "templates".
Il est possible de conserver les dossiers "Resources" le temps de la bascule de la nouvelle version afin de simplifier la maintenance entre les deux versions de l'application.
Pour déclarer le dossier Resources/views comme un dossier de templates, il suffit de modifier la configuration de twig de cette façon :
# config/packages/twig.yaml
twig:
paths:
'OldBundle/Resources/views': 'Old'
Les fichiers de templates seront alors toujours accessibles via "@Old".
33.4.1 Piège : l'utilisation du double points (ou colon) ':' pour charger les templates
A priori (je n'ai pas trouvé d'historique sur le sujet), il n'est plus possible d'utiliser le double-points pour indiquer le chemin d'un template, il est possible d'utiliser le slash, qui marchera quelque soit la version de Symfony :
'@Old/Domain:index.html.twig' -> ne fonctionne plus sous Symfony 5.4
'@Old/Domain/index.html.twig' -> fonctionnera sous Symfony 3.4 et Symfony 5.4
Cela vaut pour tous les chargements :
Dans les fichiers *.twig :
{% use ''
{% extends ''
{% embed ''
{% form_theme form ''
Dans les fichiers *.php :
->render('')
33.5. L'usage de block twig non déclarés
Si le block n'existe pas, cela déclenchera une erreur.
{# Erreur lors du render #}
{{ block('block_inexistant') }}
33.6. Le routing des Controllers
33.6.1 Dans les fichiers routing.yml
Il vaut mieux oublier la syntaxe avec les deux-points dans les fichiers de type routing.yml :
Avant :
nom_route:
path: /
defaults : {_controller : "AppBundle:Domain:index"}
Après :
nom_route:
path: /
defaults : {_controller : AppBundle\Controller\DomainController::indexAction}
33.6.2 Dans les fichiers de templates .twig
Même chose dans les templates, il vaut mieux éviter d'utiliser les deux points comme séparateur.
Avant :
{{ render(controller('AppBundle:Domain:index')) }}
Après :
{{ render(controller('AppBundle\\Controller\\DomainController:indexAction')) }}
33.7. L'utilisation du ->get dans les Controller
En Symfony 5.4, le ->get('nom_service') devient obsolète.
Il est toujours utilisable pour les services classiques de Symfony (router / twig / session) mais ne l'est plus pour les services personnalisés de votre projet.
Il devient alors nécessaire de déclarer les services dans les constructeurs de vos controlleurs, de les charger via l'autowiring (ou sans en déclarant manuellement les noms des services dans les fichiers services.yaml/xml).
Avant :
public function indexAction()
{
$this->get('mon_service')->do();
// ...
}
Après (ici le code en PHP 7.4):
private MonService $monService;
public function __construct(MonService $monService)
{
$this->monService = $monService;
}
public function indexAction(): Response
{
$this->monService->do();
// ...
}
Note : le suffixe "Action" est conservé toujours dans l'optique où l'ancienne version du code sous SF3.4 est toujours en fonctionnement.
33.8. L'utilisation du getParameter
De la même façon, il n'est plus possible de récupérer un paramètre de configuration via la méthode getParameter du Controller.
Il y a alors au moins deux façons de faire :
- soit injecter dans le construct le service ParameterBag :
private ParameterBagInterface $monService;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->parameterBag = $parameterBag;
}
public function indexAction(): Response
{
$this->parameterBag->get('nom_parametre'); // Valeur du paramètre
// ...
}
💡️On peut même créer une méthode protected function getParameter(string $name) qui retournerait $this->parameterBag->get('nom_parametre')
- soit injecter directement le paramètre dans le construct et utiliser l'autoconfigure dans le services.yaml (ou le déclarer manuellement également).
private string $parameter;
public function __construct(string $parameter)
{
$this->parameter = $parameter;
}
public function indexAction(): Response
{
$this->parameter; // Valeur du paramètre
// ...
}
33.9. Piège : utiliser des choses supprimées
33.9.0.1 $form->isValid()
Il n'est plus possible d'utiliser isValid() directement sans vérifier au préalable que le formulaire est soumis.
L'utilisation de la méthode isValid doit toujours être précédée par la méthode isSubmitted.
Il faut donc faire ceci :
Avant :
$form->isValid();
Après :
$form->isSubmitted() && $form->isValid();
33.9.0.2 choices_as_values
Le paramètre choices_as_values dans les FormType n'existe plus. Il faut le supprimer en veillant à tester que les formulaires fonctionnent toujours.
33.9.0.3 new Email(['strict' => true])
Le validator Email (Symfony/Component/Validator/Constraints/Email) a changé, il ne faut plus utiliser le paramètre 'strict', il faut le remplacer par le paramètre 'mode'.
Avant :
new Email(['strict' => true]);
Après :
new Email(['mode' => Email::VALIDATION_MODE_STRICT]);
33.9.0.4 transchoice
La méthode 'transchoice' n'existe plus, il faut toujours passer par la méthode 'trans' et utiliser le paramètre %count% pour indiquer une quantité.
📖️️Documentation officielle : https://symfony.com/doc/current/translation.html#message-format
33.9.0.5 $eventDispatcher->dispatch('nom.event', $event)
La méthode dispatch ne prend plus qu'un seul argument, à savoir l'instance de la classe Event :
Avant :
$eventDispatcher->dispatch('nom.event', $event)
Après :
$eventDispatcher->dispatch($event)
33.9.0.6 Balises twig javascripts
Evidemment, vu qu'Assetic est aux oubliettes, les tags twig {% javascripts '...' %}{% endjavascripts %} et {% stylesheets '...' %}{% endstylesheets %} ne marcheront plus.
Ils sont remplacés par leurs homologues {{ encore_entry_script_tags('...') }} et {{ encore_entry_link_tags('...') }}
33.9.0.7 Injecter un paramètre sans l'autoconfigure et sans le Configuration.php
Il est possible d'injecter un paramètre en argument d'un service en le déclarant dans le services.yaml :
Avant (en passant par la méthode getConfigTreeBuilder de la classe DependencyInjection/Configuration.php du AppBundle):
nom_service:
class: App\MyClass
arguments:
- "%key.index%"
Après (en injectant directement le paramètre dans le service):
nom_service:
class: App\MyClass
arguments:
- "@parameter('key')['index']"
34. Préparer un entretien technique
Si vous avez la chance de passer un entretien technique avec moi, voici éventuellement les questions que je pourrais vous poser.
Bien sur, cela dépend de l'offre pourvue et du niveau d'expérience demandé par celle-ci.
Mais certaines questions seront les mêmes que vous soyez étudiant ou expérimenté.
34.1. Questions de cultures générales
34.1.1 Questions autour du web
* Comment fonctionne Internet ?
* Qu'est-ce qu'une application dite serveur web ?
* Existe-t-il d'autres types de serveur ?
* Comment communique concrètement un ordinateur-client avec un serveur-client ?
34.1.2 Questions autour de PHP
* Pouvez-vous m'écrire une classe qui hérite d'une classe abstraite qui implémente une interface ?
* Pouvez-vous me dire les nouveautés entre chaque nouvelle version de PHP ?
* Quelles sont les techniques courantes pour qu'une page web s'affiche très rapidement ?
* Combien connaissez vous de design pattern ? Quels sont ceux que vous avez déjà utiliser ?
* Que pensez vous de la dernière RFC en reflexion/validée/refusée ?
* Comment fonctionne un ORM ?
* Pouvez-vous m'écrire un algorithme qui filtre un tableau avec et sans regex ?
* Pouvez-vous m'écrire un algorithme qui ferait du multithread (le traitement en lui même ne sera qu'un sleep(rand(0, 20))) ?
* Quelles sont les différentes assertions sous PHPUnit ?
34.1.2.1 Questions autour du framework
Si vous postulez pour une mission sur un framework particulier :
* Quels sont les composants/sous namespaces les plus courants ?
* Qu'apporte le nouveau composant/bundle natif versus le composant/bundle apporté par quelqu'un d'autre ?
* Quels sont les différences notables entre chaque version majeure ?
* Où sont situés les fichiers de configuration sur la dernière version du framework ? Et la première ?
34.1.3 Questions autour du JS
* Pouvez-vous m'écrire un algorithme qui ferait un POST sans Jquery ?
* Quelles sont les solutions pour générer des bouts de JS dont une partie du contenu provient d'une base de données ?
* Comment vous controlez qu'une valeur existe dans un tableau ?
* Comment vous testez du code JS ?
* Quelle architecture employez-vous pour classer vos fichiers JS ?
34.1.4 Questions autour de HTML
* Pouvez-vous écrire le squelette classique d'une page en HTML 5 ?
* Savez-vous définir un tableau HTML ?
* A quoi servent les balises meta ?
34.1.5 Questions autour de CSS
* Pouvez-vous me définir une animation dont le texte subira une rotation à l'endroit puis à l'envers pendant 5 secondes ?
34.1.6 Questions autour de SCSS/LESS
* Pouvez-vous me dire comment définit-on un héritage entre les feuilles de style ?
* Pouvez-vous me faire varier une couleur sur un texte de façon aléatoire entre 1 et 5 secondes ?
34.2. Questions techniques
Et éventuellement, si après 1 heure d'entretien on a fait le tour des questions de culture G, on abordera en anglais des questions plus techniques :
* Théorie des algorithmes
* Structures Stack, Queue, LinkedList, HashMap, PriorityQueue, Binary Tree
* Algorithmes de tri (quicksort, merge sort, radix-sort, bucket sort…),
* Algorithme de recherche (binary search, fulltext search),
* Parcours des arbres (largeur/profondeur)
* Graphes (BFS, DFS …)
* Récursivité
* Gestion de la concurrence
* Manipulation de bits
34.3. Ne prenez pas peur !
Et enfin, pour vous rassurer : même si vous ne savez pas grand chose de tout cela, l'essentiel est de montrer sur peu de temps votre potentiel : n'hésitez donc pas à prendre les devants et imposer vos sujets techniques.
Après il est sûr que si vous postulez à un poste qui demande 5 ans d'expérience sur du JS et que vous n'avez jamais testé ni React, ni Angular, ni Vue entre 2017 et 2020, on pourra se poser quelques questions sur votre curiosité !
Idéalement, le code sera écrit au tableau blanc/noir.
35. Schématisation
35.1. Avant l'UML
35.1.1 La méthode OMT (Object Modeling Technique)
James Rumbaugh(https://fr.wikipedia.org/wiki/James_Rumbaugh), Michael Blaha, William Lorensen, Frederick Eddy et William Premerlani conçoivent cette méthode en 1991.
35.1.2 La méthode Booch
Grady Booch(https://fr.wikipedia.org/wiki/Grady_Booch) publie une première version de sa méthode, aussi appelée OOD (probablement "Object Oriented Design") en 1992.
35.1.3 La méthode OOSE (Object Oriented Software Engineering)
Ivar Jacobson(https://fr.wikipedia.org/wiki/Ivar_Jacobson) fonde cette méthode en 1992.