Manipulation des données sur un site web

Licence Informatique, semestre 6

Alexandre Niveau — Valentin Lemière

Enseignement des technologies du Web

 

Manipulation des données sur un site web

Notes de cours

Travail personnel

Objectifs

Les deux premiers exercices de ce TP sont indépendants et complémentaires : le premier porte purement sur PDO, sans souci architectural, tandis que le second porte uniquement sur la manipulation des données dans l'architecture présentée en cours, sans utiliser de base de données. Le dernier fait la synthèse des deux, en montrant comment utiliser une base de données dans l'architecture — ce qui est assez simple et direct puisque les composants sont bien séparés.

Exercice 1 — Galerie d’images

Un corrigé de ce TP est disponible (résultat, archive du code). Attention, le déploiement nécessite d’importer la table SQL ci-dessous dans votre BD, et de créer un fichier de configuration dans votre private (explications dans le code source).

Cet exercice vise à découvrir le fonctionnement de PDO en affichant une galerie d'images. On ne cherchera pas à appliquer l'architecture vue en cours (en tout cas pas dès le début) : l'objectif est de manipuler PDO.

Les informations sur les images à afficher sont dans une table images en base de données. Les images elles-mêmes sont hébergées par Flickr. La description de la table est la suivante :

mysql> desc images;
+----------------+--------------+------+-----+---------+----------------+
| Field          | Type         | Null | Key | Default | Extra          |
+----------------+--------------+------+-----+---------+----------------+
| id             | int(11)      | NO   | PRI | NULL    | auto_increment |
| title          | varchar(255) | YES  |     | NULL    |                |
| creation_date  | datetime     | YES  |     | NULL    |                |
| thumb_url      | varchar(255) | YES  |     | NULL    |                |
| image_page_url | varchar(255) | YES  |     | NULL    |                |
| author_name    | varchar(255) | YES  |     | NULL    |                |
| author_url     | varchar(255) | YES  |     | NULL    |                |
+----------------+--------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)

Il y a trois URL, celle de la miniature (thumbnail) de l'image, celle de la page de l'image, et celle de la page de l'auteur. Le fichier images.sql contient le code SQL permettant de créer et remplir cette table.

  1. Utiliser le fichier fourni pour créer la table images dans une de vos bases de données MySQL. Voir cette page de la FAQ du département pour savoir comment y accéder. À noter en particulier, le mot de passe initial de votre BD est dans le répertoire Protected de votre répertoire par défaut (/home/etudiants/NUMETU/Protected/mysql.txt).
  2. Dans un script galerie.php, créer une instance de PDO qui se connecte à la base.
  3. Afficher une liste des titres de toutes les images de la base.
  4. Modifier le code pour qu'il affiche la miniature de l'image à côté de chaque titre.
  5. Ajouter un peu de CSS pour que la liste ressemble plus à une galerie.
  6. Ajouter à chaque image un lien vers image.php?id=34 (avec à la place de 34 l'identifiant de l'image).
  7. Créer le script image.php, qui affiche toutes les infos sur l'image dont l'identifiant a été passé en paramètre : titre, miniature, lien vers la page Flickr de l'image, date, et nom de l'auteur avec un lien vers sa galerie Flickr.
  8. Que se passe-t-il si vous allez sur la page d'URL « image.php?id='; DROP TABLE images; -- » ? Si ce n'est déjà fait, protégez votre BD contre les injections SQL en utilisant uniquement des requêtes préparées.

Exercice 2 — Manipulation des données dans MVCR

Une proposition de correction incrémentale de ce TP est disponible ici sous forme d'un dépôt Git. Cet exercice commence au commit « Persistance: utilisation d’un fichier » et va jusqu'au commit « Suppression d’un animal ». Une archive du résultat final (comprenant le dépôt) est disponible ici.

On va continuer le site sur les animaux fait au TP précédent, en permettant aux internautes d'ajouter leurs propres animaux à la base.

Une nouvelle fois, l'énoncé est long mais c'est parce que l'exercice est très guidé ; il n'y a pas de difficulté particulière. Si vous avez des doutes, jetez un œil à l'exemple des couleurs qui a été en partie présenté en cours (résultat final, archive du code). Ne faites pas de copier-coller, réécrire est beaucoup plus efficace pour l'apprentissage. De plus, l'exercice est (comme celui du TP précédent) très incrémental : j'essaie de justifier chaque ajout de complexité dans l'architecture. On ne prendra pas le chemin le plus direct vers la version finale, ne soyez pas surpris si ça ne ressemble pas immédiatement à l'exemple présenté en cours.

Préliminaires : persistance des données et affichage de debug

Avant de commencer cet exercice, il faut avoir terminé l'exercice d'introduction à l'architecture du TP précédent, au moins jusqu'à la partie « Stockage » incluse. Si vous avez fait (certains morceaux de) la partie « Compléments », il faudra adapter un peu certaines questions, mais vous devriez vous y retrouver.

La pseudo-base (statique) qu'on a utilisée ne suffit plus pour cet exercice, puisqu'on veut pouvoir y rajouter des animaux : il faut que les modifications soient persistantes. Pour cela, on pourrait stocker nos animaux dans une BD MySQL, mais pour gagner du temps on va choisir une solution plus simple (au moins au début), celle d'enregistrer notre tableau dans un fichier après chaque requête.

  1. Créer une classe AnimalStorageFile dans src/model, avec des attributs $db (qui va contenir un tableau d'instances de Animal), $nextId (le prochain identifiant qui va être utilisé), et $file (le nom du fichier où la « base » est enregistrée). Copier dans cette classe le constructeur et le destructeur suivants (qui font le même genre de travail que ColorStorageFile dans l'exemple des couleurs) :
    /** Construit une nouvelle instance, qui utilise le fichier donné en paramètre. */
    public function __construct($file) {
    	$this->file = $file;
    	if (file_exists($this->file)) {
    		/* on a sérialisé un tableau contenant la base à l'indice db
    		 * et le prochain identifiant à l'indice nextId */
    		$storedData = unserialize(base64_decode(file_get_contents($this->file)));
    		$this->db = $storedData['db'];
    		$this->nextId = $storedData['nextId'];
    	} else {
    		/* Le fichier n'existe pas, on crée une base vide */
    		$this->db = array();
    		$this->nextId = 1;
    	}
    }
    /** Sérialise et stocke la base avant de détruire l'instance. */
    public function __destruct() {
    	$dataToBeStored = array('db' => $this->db, 'nextId' => $this->nextId);
    	file_put_contents($this->file, base64_encode(serialize($dataToBeStored)));
    }
  2. Ajouter une méthode reinit() à AnimalStorageFile, qui remet la base dans l'état « initial », c'est-à-dire avec les animaux du TP précédent écrits en dur (Médor, Félix et Denver). Contrairement au TP précédent, les identifiants utilisés ici sont numériques, mais ça ne doit rien changer.
  3. AnimalStorageFile doit implémenter l'interface AnimalStorage. Copier les méthodes read et readAll de la classe AnimalStorageStub, et les adapter si besoin.
  4. Modifier le routeur (ou le fichier de configuration) pour que le contrôleur utilise un AnimalStorageFile à la place de l'AnimalStorageStub. Attention, le serveur du département n'autorise pas PHP à écrire n'importe où. Le plus simple est de mettre le fichier de sauvegarde dans le répertoire /users/NUMETU/tmp (récupérable dans la variable $_SERVER['TMPDIR']). Bien sûr, sur un vrai site, on mettrait le fichier à un endroit plus sûr… Appeler reinit() sur la base après l'avoir créée.
  5. Tester : tout doit marcher comme avant ! Enlever l'appel à reinit() : tout doit toujours marcher.
  6. Ajouter la méthode suivante à la vue :
    public function makeDebugPage($variable) {
    	$this->title = 'Debug';
    	$this->content = '<pre>'.var_export($variable, true).'</pre>';
    }
    Elle va faciliter le debug en nous permettant d'afficher le contenu d'une variable. La tester depuis le routeur en lui faisant afficher divers objets.

Création d'un nouvel animal

  1. La page avec le formulaire de création d'un animal sera (par exemple) à l'URL animaux.php?action=nouveau, et la page destinée à recevoir les données sera à l'URL animaux.php?action=sauverNouveau. Ajouter au routeur des méthodes getAnimalCreationURL() et getAnimalSaveURL() qui renvoient ces URLs.
  2. Ajouter une méthode makeAnimalCreationPage() à la vue. Elle doit afficher un formulaire de création d'un animal, avec trois champs texte : nom, espece et age (et d'autres, si vous voulez, en fonction de votre classe Animal). Le formulaire doit envoyer les données en POST à l'URL donnée par le getAnimalSaveURL() du routeur.
  3. Ajouter une méthode saveNewAnimal(array $data) au contrôleur, qui se contente d'afficher son argument grâce à makeDebugPage.
  4. Modifier le routeur pour qu'il analyse le paramètre action de l'URL, et qu'il appelle makeAnimalCreationPage() de la vue si le paramètre est nouveau, et qu'il appelle saveNewAnimal($_POST) du contrôleur si le paramètre est sauverNouveau. Cela permet de tester que tout fonctionne pour le moment.
  5. Dans la méthode saveNewAnimal, au lieu d'afficher le tableau passé en argument, l'utiliser pour créer une instance de Animal et l'afficher avec makeAnimalPage. Ça devrait fonctionner, mais l'animal n'est pas ajouté à la base (on peut le constater en affichant la liste des animaux).
  6. Ajouter une méthode create(Animal $a) à l'interface AnimalStorage, et l'implémenter dans AnimalStorageFile. Cette méthode doit ajouter à la base l'animal donné en argument, et retourner l'identifiant de l'animal ainsi créé.
  7. Faire en sorte que saveNewAnimal ajoute le nouvel animal à la base avant de le passer à la vue.
  8. Vérifier que tout marche bien : on doit pouvoir créer des animaux qui s'ajoutent à la liste.

Validation

Les internautes peuvent maintenant ajouter leurs animaux favoris à votre site. Cependant, ils peuvent aussi envoyer des données incorrectes, par exemple en laissant les champs vides. On va commencer par faire une validation de base, et on gérera les données incomplètes dans la section suivante.

  1. Modifier saveNewAnimal() pour qu'il ne soit pas possible d'ajouter un animal dont le nom ou l'espèce soient vides ou dont l'âge ne soit pas un nombre positif. On pourra afficher une page d'erreur par exemple.
  2. Cette solution n'est pas idéale : en cas d'erreur, l'internaute perd ce qu'il ou elle a entré. Il est bien plus ergonomique de lui redonner la main sur le formulaire avec les champs tels qu'ils ont été remplis. Pour cela, en cas d'erreur, le contrôleur va redonner le tableau qu'il a reçu à la méthode makeAnimalCreationPage de la vue, pour qu'elle remplisse les champs du formulaire avec. Faire les modifications nécessaires. (Attention notamment à ce que tous les appels à makeAnimalCreationPage soient corrects.)
  3. C'est mieux, mais maintenant il est moins clair pour l'internaute qu'une erreur est survenue. D'autre part, la raison pour laquelle les données étaient invalides n'est pas forcément évidente : il est nécessaire d'en informer l'internaute. Faire en sorte que le contrôleur passe une chaîne $error à makeAnimalCreationPage qui se chargera de l'afficher. La chaîne sera null s'il n'y avait pas d'erreur, et contiendra sinon une explication sur l'erreur.
  4. Vérifier que tout fonctionne, c'est-à-dire le cas normal et tous les cas d'erreur.
  5. Essayer d'ajouter un animal qui aurait pour espèce « <script>alert('coucou')</script> ». Que se passe-t-il ? Que faut-il faire pour empêcher ça ?

Gestion des données incomplètes

Il y a deux problèmes avec notre manipulation des données. D'abord, c'est le contrôleur qui décide quelles données sont valides (alors que ça concerne le modèle). D'autre part, il y a duplication du nom des champs de formulaire, dans la vue et dans le contrôleur. C'est moins gênant que pour les paramètres d'URL, car ils sont moins visibles, mais ils font cependant partie de l'interface « externe » du site (pas comme les noms des variables PHP, par exemple), et à ce titre ils se doivent d'être relativement faciles à changer.

On va gérer ces deux problèmes à la fois en ajoutant une nouvelle classe au modèle, AnimalBuilder, qui représente un animal en cours de manipulation (création ou modification) dans l'application, et qui permet de construire l'instance de Animal correspondante.

  1. Créer cette classe et lui donner un attribut $data, passé en argument à son constructeur, et un attribut $error, initialisé à null. Ajouter un accesseur pour chacun de ces attributs.
  2. Modifier la méthode saveNewAnimal du contrôleur pour qu'elle crée une instance de AnimalBuilder avec le tableau $data qu'elle a elle-même reçu du routeur.
  3. Ajouter une méthode createAnimal() dans AnimalBuilder, qui crée une nouvelle instance de Animal en utilisant l'attribut $data (déplacer le code depuis le contrôleur).
  4. Ajouter une méthode isValid() dans AnimalBuilder, qui vérifie que les données de son attribut $data sont correctes (déplacer à nouveau le code depuis le contrôleur). Si elles ne le sont pas, placer la chaîne expliquant l'erreur dans l'attribut $error.
  5. Modifier la méthode makeAnimalCreationPage pour qu'elle ne prenne comme argument qu'une instance de AnimalBuilder, et l'utilise pour pré-remplir les champs et pour afficher l'erreur éventuelle.
  6. Modifier saveNewAnimal pour utiliser l'instance de AnimalBuilder. Vérifier que tout marche toujours (normalement non : il faut modifier l'appel initial à makeAnimalCreationPage, qui doit récupérer un AnimalBuilder vide ; qui doit s'occuper de le construire ?)
  7. Il ne reste plus qu'à « cacher » les noms des champs. Ajouter à AnimalBuilder des constantes NAME_REF, SPECIES_REF et AGE_REF, qui contiennent respectivement nom, espece et age. Utiliser ces constantes dans la classe en question, et dans makeAnimalCreationPage à la place des noms de champ codés « en dur ».
  8. Vérifier que tout marche toujours, puis changer les noms des champs (les passer en majuscules par exemple). Normalement, vous ne devriez avoir à modifier que les constantes de AnimalBuilder, et tout doit toujours marcher (et les noms des champs modifiés se verront dans le HTML).

Suppression d'un animal (optionnel)

La suppression est beaucoup plus simple que la création (en particulier, pas besoin de AnimalBuilder). Les questions suivantes sont un peu moins guidées. Penser à tester vos ajouts au fur et à mesure, par exemple en utilisant l'affichage de debug !

  1. Ajouter au routeur des méthodes getAnimalAskDeletionURL($id) (page demandant à l'internaute de confirmer son souhait de supprimer l'animal) et getAnimalDeletionURL($id) (page supprimant effectivement l'animal). Faire en sorte qu'accéder à ces URL affiche quelque chose (en utilisant la méthode de debug, par exemple).
  2. Implémenter la méthode makeAnimalDeletionPage($id) de la vue, qui doit afficher un bouton de confirmation envoyant une requête POST à l'URL de suppression effective.
  3. Implémenter la méthode askAnimalDeletion($id) du contrôleur, qui vérifie que l'animal existe avant d'appeler makeAnimalDeletionPage($id) (un message d'erreur doit être affiché dans le cas contraire).
  4. Ajouter une méthode delete($id) à AnimalStorage et l'implémenter dans AnimalStorageFile.
  5. Implémenter la méthode deleteAnimal($id) du contrôleur, qui supprime effectivement l'animal.

Compléments (optionnel)

Assurez-vous de maîtriser ce qui précède avant de vous attaquer à ces questions moins guidées.

  1. Ajouter la possibilité de modifier un animal : les principes sont les mêmes que pour la création, mais il va falloir ajouter des méthodes à AnimalBuilder.
  2. Faire la section « Compléments » de l'exercice du TP précédent, et faire en sorte que les opérations CRUD fonctionnent toujours. Pour les URL plus propres, on pourra prendre par exemple animaux.php/nouveau, animaux.php/2328/supprimer, et animaux.php/2328/modifier, la distinction entre page de formulaire et page de « confirmation » pouvant se faire par le choix de la méthode HTTP utilisée (récupérable en PHP par $_SERVER['REQUEST_METHOD']).
  3. Au lieu d'afficher une seule chaîne avec les erreurs, faire en sorte que chaque erreur s'affiche à côté du champ concerné.

Exercice 3 (optionnel) — Utilisation d’une base de données dans MVCR

Une proposition de correction incrémentale de ce TP est disponible ici sous forme d'un dépôt Git. Cet exercice commence au commit « Utilisation d’une BD MySQL » et va jusqu'au commit « StorageMySQL: readAll, create, delete, init ». Une archive du résultat final (comprenant le dépôt) est disponible ici.

Dans cet exercice, on implémente simplement l'interface AnimalStorage en utilisant une vraie base de données MySQL. J'en profite pour préciser quelques détails annexes, mais sur le fond il n'y a pas de surprise, c'est assez direct.

  1. Pour commencer, si ce n'est déjà fait, faire en sorte que l'instance de AnimalStorage manipulée par le contrôleur soit créée par animaux.php et passée comme argument au main du routeur, qui l'utilisera pour construire le contrôleur. L'idée est que la décision du type de stockage utilisé est prise par animaux.php, c'est-à-dire le véritable point d'entrée de l'application.
  2. Créer une table dans une vos bases de données MySQL qui contiendra les animaux. Vous pouvez utiliser ce fichier, qui contient le code SQL permettant de créer une table animals avec les trois animaux de départ.
  3. Créer une classe AnimalStorageMySQL dans src/model qui implémente AnimalStorage. Pour commencer, on se contentera d'implémenter la méthode read($id) (les autres méthodes enverront une exception), et on construira l'instance de PDO dans le constructeur.
  4. Remplacer l'instance de AnimalStorageFile que manipule le contrôleur par une instance de AnimalStorageMySQL ; normalement, il doit suffire de modifier animaux.php.
  5. Vérifier que les pages animaux.php?id=1, animaux.php?id=2 et animaux.php?id=3 (ou équivalentes) fonctionnent toujours.
  6. Il n'est pas propre de construire l'instance de PDO dans AnimalStorageMySQL : si on a plusieurs classes de ce type, on va construire plusieurs instances de PDO et donc ouvrir plusieurs connexions à la BD, ce qui est inefficace. Modifier le code pour que l'instance de PDO soit créée dans animaux.php et passée en argument au constructeur de AnimalStorageMySQL.
  7. Pour l'instant, on a mis les paramètres de connexion à la BD dans animaux.php, notamment le mot de passe. C'est embêtant si on veut montrer notre code ou le mettre sur un dépôt. Créer un fichier mysql_config.php dans le répertoire private qui se trouve à la racine de votre compte web (à côté de www-prod et www-dev). Y déclarer des constantes MYSQL_HOST, MYSQL_PORT, MYSQL_DB, MYSQL_USER et MYSQL_PASSWORD.
  8. Dans animaux.php, ajouter require_once('/users/NUMETU/private/mysql_config.php'); et utiliser les constantes pour construire l'instance de PDO. L'intérêt est que le fichier mysql_config.php est inaccessible depuis le web, ce qui garantit une bonne sécurité pour vos identifiants. Vos scripts PHP y ont accès car ils sont sur la même machine.
  9. Vérifier que les pages de base fonctionnent toujours, puis implémenter les autres méthodes de AnimalStorageMySQL, en prenant soin d'utiliser des requêtes préparées.