MVC - activité 3
Introduction
Section titled “Introduction”Si vous souhaitez repartir d’un projet “propre” et à jour, vous pouvez créer le projet à nouveau en utilisant la branche mvc2-fin du github :
$ git clone -b mvc2-fin git@github.com:nn-teach/PHP-MVC.git mvc-activite3Ou
$ git clone https://github.com/nn-teach/PHP-MVC.git mvc-activite3$ cd mvc-activite3$ git checkout mvc2-finPuis placez vous dans le nouveau répertoire mvc-activite3 et relancez le serveur PHP.
Travail guidé
Section titled “Travail guidé”Exercice 1 - Autoloader & Namespaces
Section titled “Exercice 1 - Autoloader & Namespaces”Au fur et à mesure que nous développons le projet, le nombre de fichiers et de classes PHP augmentent.
Jusqu’à maintenant, à chaque nouvelle classe que nous créons, nous devons ajouter des require pour les rendre disponibles dans les autres fichiers. Le système de chargement automatique des classes, l’autoloader, va nous aider à faciliter ce processus.
Par ailleurs, 2 classes ne peuvent pas avoir le même nom. Jusqu’à maintenant, nous avons pris soin d’utiliser des noms différents pour les classes, mais cette solution n’est pas viable.
Les “espaces de noms”, Namespaces, permettent “d’encapsuler” des fichiers et de régler ce problème.
Exercice 1.1 - Autoloader
Section titled “Exercice 1.1 - Autoloader”La fonction spl_autoload_register() est disponible par défaut dans PHP. Elle permet de définir une fonction, qui sera appelée lors de l’appel à une classe PHP qui n’est pas trouvée.
En pratique, lorsqu’on effectue un new NomClass(); PHP retourne une erreur si le fichier de la classe n’a pas été inclus dans le projet.
Créer un nouveau fichier app/autoload.php et coller le code suivant à l’intérieur :
function autoload($class){ //Liste des répertoires à explorer pour essayer de trouver la classe $folders = ['app/', 'app/Controllers/', 'app/Models/'];
//Nettoyage du nom de la classe //Le nom de la class appellée avec le namespace est du type "App\Models\Article" pour une classe Article //Avec les fonctions substr et strrpos, on récupère la dernière partie du nom, après le \, soit par exemple Article pour "App\Models\Article" $class = substr($class, strrpos($class, '\\') + 1);
foreach ($folders as $folder) { //on boucle sur la liste de répertoires if (is_file($folder . $class . '.php')) { //si le fichier existe dans ce répertoire require_once $folder . $class . '.php'; //on l'inclut } }}spl_autoload_register('autoload'); //inscrire notre fonction autoload() dans le chargement automatique des classes de PHPDans le fichier app/bootstrap.php, ajouter la ligne suivante pour inclure le fichier créé précédemment :
include_once 'autoload.php';Il suffira maintenant d’ajouter les répertoires dans lesquels on veut chercher les classes dans la fonction autoload(), pour qu’elles soient automatiquement chargées !
Exercice 1.2 - Namespaces
Section titled “Exercice 1.2 - Namespaces”Admettons que nous souhaitions créer une classe controller Newsletter et une classe model Newsletter (exactement le même nom). Dans notre version actuelle, PHP déclenchera une erreur car 2 classes ne peuvent avoir le même nom, PHP doit pouvoir différencier toutes les classes :
-
Renommer le fichier
app/Controllers/Newsletters.phpenapp/Controllers/Newsletter.php(sans le s) -
Renommer la classe de ce fichier en Newsletter (sans le s)
-
Modifier les routes en conséquence
-
Rafraichir le navigateur, qui devrait maintenant produire l’erreur :
Fatal error: Cannot declare class Newsletter, because the name is already in use in PHP-MVC/app/Models/Newsletter.php on line 2
-
En haut du fichier , ajouter la ligne suivante :
namespace App\Controllers;Désormais, notre classe controller Newsletter s’appelle
\App\Controllers\Newsletteret plus simplement Newsletter. Pour l’utiliser, il faudra spécifier ce nom complet.
Lorsque l’on déclare un namespace dans un fichier, tous les new utilisés pour créer des objets qui ne spécifient pas eux-mêmes de namespace dans ce fichier, chercheront la classe dans le même namespace.
De ce fait, dans notre classe controller Newsletter, les objets des classes View View et Model Newsletter ne seront plus trouvés et PHP va renvoyer des erreurs.
-
Dans la classe
app/View.php, ajouter le namespacenamespace App; -
Dans la classe
app/Models/Newsletter.php, ajouter le namespacenamespace App\Models;Pour les modèles, il faudra aussi apporter des modification supplémentaires :
- Ajouter
use Exception;juste en dessous du namespace, avant la déclaration de la classe - Remplacer les appels à
PDOpar\PDO
- Ajouter
-
Dans le fichier
app/Controllers/Newsletter.php, préfixer tous les appels à la class View avec\App\ -
Dans le fichier
app/Controllers/Newsletter.php, préfixer tous les appels à la class Model Newsletter avec\App\Models\
La partie newsletter du projet devrait fonctionner à nouveau !
Exercice 1.3 - Finalisation de la transition du projet vers les namespace et l’autoloader
Section titled “Exercice 1.3 - Finalisation de la transition du projet vers les namespace et l’autoloader”Reproduire les étapes précédentes avec les controllers Home et Articles et le modèle Article.
Enfin, ajouter le namespace namespace App; au fichier app/Route.php, puis dans le fichier routes.php retirer toutes les lignes d’entête et remplacer Route::get par \App\Route::get
Conclusion MVC
Section titled “Conclusion MVC”Ça y est ! Le projet est entièrement fonctionnel, respecte le design pattern MVC, utilise l’autoloader et les namespaces et la programmation objet.
Si on souhaite l’étendre, il suffira de dupliquer les fichiers des MVC actuels et de mettre à jour les routes. Cette structure, éprouvée depuis de nombreuses années, permet de développer des applications complexes.
Les frameworks comme Laravel, Symfony, Angular, Codeigniter, Django, etc. utilisent des structures similaires. \ Bien sûr, ces frameworks, plus évolués, utilisent aussi d’autres principes de développement et offrent plus de fonctionnalités que notre petit framework maison, mais il devrait vous permettre d’aborder les autres frameworks plus aisément.
Vous pouvez retrouver le code complet du projet sur la branche mvc3 du github
Exercice 2 - Héritage, abstraction et ORM
Section titled “Exercice 2 - Héritage, abstraction et ORM”En programmation orientée objet, on utilise souvent l’héritage et l’abstraction pour simplifier le code redondant entre des classes similaires.
L’ORM (Object Relational Mapping) est une technique de programmation qui facilite l’interaction entre le programme applicatif et une source de données.
Nous allons mettre ces concepts en place dans les Models du projet pour illustrer leur utilisation.
Exercice 2.1 - Héritage, abstraction
Section titled “Exercice 2.1 - Héritage, abstraction”-
Exercice 2.1.1 - Création de la classe abstraite Model
Si on compare les deux modèles Article et Newsletter, on peut observer que :
- Ils ont exactement les même noms de fonctions
- Le code du constructeur est exactement le même
- Le code des fonctions list(), get($id) et delete($id) est très similaire, hormis pour le nom de la table de la base de données correspondante et le nom de la clé primaire de la table
- Les fonctions add() et update() sont aussi très similaires, seuls les champs correspondants aux colonnes de la base de données sont différents.
En programmation, dès lors que du code est répété plus d’une fois, on devrait essayer de le regrouper et utiliser l’héritage, les interfaces ou les Traits pour simplifier les applications. Dans ce cas nous allons utiliser l’héritage :
-
Créer un nouveau fichier
app/Model.phpet copier le code deapp/Models/Article.phpà l’intérieur. Renommer le nom de la classe avec le mot Model. Modifier le Namespace:App\ModelsdevientApp -
Ajouter les 2 attributs suivants à la classe Model :
protected $tablename = 'db_table_name';protected $pk_name = 'db_pk_name'; -
Remplacer toutes les références à la table SQL articles avec
$this->tablename(il faudra surement le concaténer dans les string avec'.$this->tablename.') l’attribut que l’on vient d’ajouter. -
Idem, remplacer toutes les références vers la clé primaire, qui était
article_idpour la table articles, avec$this->pk_name, l’autre attribut que l’on a ajouté à la classe. -
Remplacer, dans les messages d’erreur, le mot “Article” par
get_class($this).La fonctionget_class()récupère le nom de la classe en cours d’utilisation. Elle affichera donc “Article” quand on utilisera la classe Article, “Newsletter” quand on utilisera la classe Newsletter, etc. -
Pour le moment, on va supprimer les fonctions add et update, nous reviendrons dessus par la suite.
-
Enfin, placer le mot abstract devant class Model
Déclarer une classe comme abstraite signifie que l’on ne pourra pas l’instancier -> on ne pourra pas faire de
new Model(). C’est logique. Les classes Article et Newsletter vont hériter de la classe Model, mais la classe Model n’aura jamais besoin et ne devrait jamais être utilisée directement.Si vous avez suivi les étapes correctement, le code de votre classe Model devrait être comme celui-ci.
-
Exercice 2.1.1 - Modification des classes Article et Newsletter
Dans les fichiers
app/Models/Article.phpetapp/Models/Newsletter.php:-
Modifier le nom de la classe pour étendre la classe de
app/Model.phpet ajouter les 2 attribut $tablename et$pk_nameavec les bonnes valeurs. Pour Article, voilà à quoi ça doit ressembler :class Article extends \App\Model {protected $tablename = 'articles';protected $pk_name = 'article_id'; -
Supprimer toutes les fonctions sauf add() et update(), que nous allons traiter dans l’exercice suivant.
Et voilà ! L’application doit fonctionner exactement comme auparavant, mais maintenant Article et Newsletter utilisent une classe mère commune.
-
Exercice 2.2 - Finaliser l’ORM
Section titled “Exercice 2.2 - Finaliser l’ORM”À ce stade, il reste les fonctions add() et update() qui sont très similaires dans les classes Article et Newsletter, mais qui varient tout de même au niveau du nom des colonnes et du tableau des données qui est passé comme argument.
Il y a plusieurs méthodes pour aborder ce problème. Nous allons en implémenter une version assez simple. L’idée ici n’est pas de vous présenter la meilleure solution possible, mais plutôt de vous faire comprendre comment fonctionnent les ORM.
- Dans la classe Model, ajouter l’attribut
protected $fields = []; - Dans la classe Article, supprimer les fonctions add() et update(), et ajouter l’attribut
protected $fields = ['article_title', 'article_content'];
-
Exercice 2.2.1 - La fonction add()
Voici le code de la fonction add(), à ajouter à la classe Model :
function add($data){try {//Variable pour construire la requête$at_least_one_value = false;$sql_query_start = 'INSERT INTO ' . $this->tablename . ' (';$sql_query_end = 'VALUES (';$pdo_execute = [];foreach ($this->fields as $field) { //On boucle sur ls noms des colonnesif (isset($data[$field])) { //Si on trouve la colonne dans le tableau $data$at_least_one_value = true; //Au moins une valeur trouvée, on executera la requête$sql_query_start .= '"' . $field . '",'; //on ajoute le nom de la colonne en cours$sql_query_end .= '?,'; //we add the ? for the pdo execute$pdo_execute[] = $data[$field]; //we add the value}}if ($at_least_one_value) {$sql_query = substr($sql_query_start, 0, -1) . ') ' . substr($sql_query_end, 0, -1) . ')'; //on retire les dernières virgules ajoutées au strigns et on ferme les parenthèse$query = $this->pdo->prepare($sql_query);$query->execute($pdo_execute);return true;}return false;} catch (Exception $e) {print "Erreur fonction add($data) dans le modèle " . get_class($this) . " : " . $e->getMessage() . "<br/>";die();}}Le code est commenté et vous pouvez l’étudier, mais ce n’est pas obligatoire.
Dans les grandes lignes, la fonction reconstruit la requête SQL :
- Elle compare les données reçues dans l’argument $data avec les données de l’attribut $fields
- Lorsque un nom dans $data correspond à un nom dans $fields, alors le champ et la sa valeur sont ajoutés dans la requête SQL
- Si au moins un champ a été trouvé, la requête est exécutée
-
Exercice 2.2.2 - La fonction update()
Voici le code de la fonction update(), à ajouter à la classe Model :
function update($data){try {//On vérifie que $data contient bien une valeur pour la clé primaireif(!isset($data[$this->pk_name])) return false; //sinon on annule$id = $data[$this->pk_name]; //on stocke la valeur de la clé primaireunset($data[$this->pk_name]); //on supprime cette entrée dans $data, pour ne pas avoir de problème lors de la construction de la requête SQL//Variable pour construire la requête$at_least_one_value = false;$sql_query = 'UPDATE ' . $this->tablename . ' SET ';$pdo_execute = [];foreach ($this->fields as $field) { //On boucle sur ls noms des colonnesif (isset($data[$field])) { //Si on trouve la colonne dans le tableau $data$at_least_one_value = true; //Au moins une valeur trouvée, on executera la requête$sql_query .= '"' . $field . '" = ?,'; //on ajoute le nom de la colonne en cours$pdo_execute[] = $data[$field]; //we add the value}}$pdo_execute[] = $id; //On ajoute la valeur de la clé primaire à la fin du tableau pour l'execution PDOif ($at_least_one_value) {$sql_query = substr($sql_query, 0, -1) . ' WHERE '.$this->pk_name.' = ?'; //on retire les dernières virgules ajoutées au strigns et on ferme les parenthèse$query = $this->pdo->prepare($sql_query);$query->execute($pdo_execute);return true;}return false;} catch (Exception $e) {print "Erreur fonction add($data) dans le modèle Article : " . $e->getMessage() . "<br/>";die();}}Le code est commenté et vous pouvez l’étudier, mais ce n’est pas obligatoire.
Dans les grandes lignes, la fonction reconstruit la requête SQL :
- Elle compare les données reçues dans l’argument $data avec les données de l’attribut $fields
- Lorsque un nom dans $data correspond à un nom dans $fields, alors le champ et la sa valeur sont ajoutés dans la requête SQL
Si au moins un champ a été trouvé, la requête est exécutée
-
Exercice 2.2.3 - Conclusion
Notre ORM basique est fonctionnel ! Voici le code final du Model Article :
<?phpnamespace App\Models;class Article extends \App\Model{protected $tablename = 'articles';protected $pk_name = 'article_id';protected $fields = ['article_title', 'article_content'];}On peut effectuer la même modification sur le Model Newsletter et il fonctionnera directement.
Désormais, on peut donc très simplement ajouter de nouveaux Model à notre application ajoutant très peu de code !
On peut noter que :
- Si on souhaite un comportement spécifique des fonctions, qui dérogerait au fonctionnement basique de la classe Model, il suffit de les re-déclarer dans les Models enfants et de les modifier.
- Cet ORM implique d’utiliser exactement les mêmes noms pour les champs des formulaires HTML, les champs du Model dans l’attribut $fields et le nom des colonnes dans la base de données. L’ORM de Laravel (Eloquent), fonctionnera de manière légèrement différente.
Travail pour vous exercer
Section titled “Travail pour vous exercer”Exercice 3 - Héritage et abstraction pour les Controllers
Section titled “Exercice 3 - Héritage et abstraction pour les Controllers”Le travail que nous venons d’effectuer sur les Models peut aussi s’appliquer sur les Controllers.
En effet, les Controllers Articles et Newsletters sont très similaires eux aussi :
-
Les fonctions sont similaires
-
Les noms des templates utilisées sont très similaires
-
Ils utilisent tous les deux un Model
Essayez d’appliquer l’héritage et l’abstraction aux Controllers :
- Créer une classe abstraite Controller dans un fichier
app/Controller.php - Copier / coller le code du controller Article à l’intérieur
- Modifier le code en conséquence :
- Ajout d’un attribut pour le nom du modèle (nom de la classe avec le namespace)
- Ajout d’un attribut pour le nom du modèle au singulier, par exemple “article”. Attribut utilisé pour le formulaire d’édition
- Ajout d’un attribut pour le nom du dossier des templates
- Modification du code des fonctions en conséquence
- Modifier les Controllers Articles et Newsletter pour qu’ils héritent du Controller abstrait Controller
- Le Controller Home, n’ayant pas besoin des fonctions du CRUD et faisant appel à plusieurs Models, n’a pas besoin d’hériter du Controller Controller
Vous pouvez retrouver le code complet de cet exercice et le précédent sur cette branche du github.
Cela dit, vous devriez vraiment essayer de le faire par vous-même si vous voulez apprendre à bien maitriser !