Tutoriel premier en MVC avec Zend Framework

Olivier Capuozzo

18 janvier 2008

Résumé

Support de cours à destination d'étudiants en formation professionnelle (BAC +2), BTS Informatique de Gestion option Développeur d'Applications.

Prérequis :

  • Concepts de base de l'objet, première expérience dans un langage (Java)

  • HTML et SQL dans les grandes lignes

Objectifs :

  • Découvrir par la pratique le potentiel structurant d'une architecture MVC et de la programmation objet (PHP >=5)

  • Approche progressive : Vue et Contrôleur sont abordés en premier, puis Modèle et Formulaire

A la fin de cet apprentissage, l'étudiant détient les clés pour :

  • Exploiter la documentation officielle du framework : API et Manuel

  • Étendre sa recherche vers d'autres modules du framework (Zend_Auth , Zend_Acl, Zend_Layout, Zend_Pdf, ...)

Document au format docbook , mis en page avec le processeur xsltproc et les feuilles de styles de Norman Walsh.

Ce document est placé sous la Licence GNU Free Documentation License, Version 1.1 ou ultérieure publiée par la Free Software Foundation.


Table des matières

1. Présentation du contexte support
2. Outils utilisés
3. Présentation des principaux concepts objet - PHP5/Java
4. Une typologie des applications
5. Architecture Bazar
6. MVC
7. Zend Framework
8. Installation des composants
9. Structures des répertoires de l'application
10. contrôleur d'application
11. Contrôle de connaissances
12. Mise en place d'un contrôleur
12.1. Méthode d'Action
12.2. Vue d'action
12.3. Résumé
13. Exercices
14. Gestion du contrat : contrôleur -> Vues
14.1. Le modèle simple de ce tuto
14.2. Configuration du bootstrap : spécifier le chemin des modèles
14.3. La Vue exploite les informations dont elle a la charge
14.4. Le Contrôleur transmet des informations à la Vue
14.5. Résumé
15. Passage d'arguments via l'url
15.1. Conception de la vue
15.2. Passage d'arguments
16. Contrôle de connaissances
17. Exercices
18. Introduction au Modèle
18.1. Rôles du modèle
18.2. Définition du modèle
18.3. Utilisation de Zend_Db_Table, Zend_Db_Row
18.4. Opérations CRUD
18.4.1. Sélectionner une ligne, une objet métier
18.4.2. Modifier un objet métier, une ligne
18.4.3. Suppression d'un objet métier, une ligne
18.4.4. Création d'un objet métier, une ligne
18.4.5. Transaction
18.4.6. Quelques liens utiles
19. Introduction à la navigation dans le modèle
19.1. Déclaration des relations dans le modèle
19.2. Relation Un-à-Un (One To One)
19.3. Relation Un-à-Plusieurs (One to Many)
19.3.1. Vue de la liste des étudiants inscrits à un diplôme
19.3.2. Méthode d'action du contrôleur liant modèle et vue
19.3.3. Méthode de liaison entre modèle
19.3.4. Résultats
19.4. Relation Plusieurs-à-Plusieurs (Many-To-Many)
19.5. Méthodes magiques
20. Formulaire et Validateurs
20.1. Qu'est-ce qu'un filtre ?
20.2. Qu'est-ce qu'un validateur ?
20.3. Exemple de code d'exploitation du formulaire
20.4. Vue résultante
21. Quelques modules (guide, tutoriel) à explorer, dans la continuité de ce tutoriel.
21.1. Gestion des droits utilisateur à rapprocher avec la notion UML d'acteur/cas d'utilisation
21.2. Gestion du suivi de sessions
21.3. Structuration de blocs de présentation
22. Conclusion

1. Présentation du contexte support

On souhaite réaliser une application Web permettant à un organisme de formation de gérer la note obtenue par ses étudiants à un examen.

Un étudiant est caractérisé par son nom, prénom, éventuellement une note d'examen, nombre de semaines de stage, et résultat (reçu ou non).

Un étudiant est reçu à l'examen si sa note d'examen est au moins égale à 10 (sur 20) et s'il a réalisé au moins 12 semaines de stage.

2. Outils utilisés

3. Présentation des principaux concepts objet - PHP5/Java

Figure 1. Concepts Objet avec PHP5 -1

Concepts Objet avec PHP5 -1

Figure 2. Concepts Objet avec PHP5 -2

Concepts Objet avec PHP5 -2

Figure 3. Concepts Objet avec PHP5 -3

Concepts Objet avec PHP5 -3

4. Une typologie des applications

D'un point de vue "physique", les architectures d'applications sont traditionnellement réparties en trois catégories :

  • Autonome

    C'est le cas d'applications qui ne dépendent pas de systèmes tiers, autres que ceux généralement offerts par un système d'exploitation.

  • Client/Serveur

    Modèle phare des années 80, où le système d'information est centré sur les données. La logique métier est répartie, plus ou moins, entre le client (qui détient/exécute les formulaires) et le serveur (SGBDR).

  • Architecture 3 tiers (et plus)

    Tiers veut dire parties. Alors que les applications C/S sont de types 2 parties (le client IHM et le SGBDR), les architectures n tiers (pour n > 2) font intervenir un middleware applicatif responsable de la logique applicative. Le terme middleware est à prendre dans le sens intermédiaire de communication entre 2 niveaux.

Cette vue par "parties" (tiers) correspond à « un changement de niveau dès qu'un module logiciel doit passer par un intermédiaire de communication (middleware) pour en invoquer un autre » - ref : www.octo.com.

Le modèle n tiers le plus répandu des années 2000 est le modèle 3 tiers Web, typiquement représenté par :


 Navigateur    <---->  Serveur d'applications  <-----------> SGBDR
présentation    http       pages dynamiques     middleware 
coordination             composants logiciels     SGBD

                                                    

C'est ce modèle que nous vous proposons d'exploiter, en environnement Php.

5. Architecture Bazar

Le succès de PHP vient de sa faciliter à combiner, dans un même document, à la fois du HTML, CSS, instructions PHP et requêtes SQL. Si le développeur ne met pas un peu d'ordre il en résulte un vrai bazar fragile, difficilement extensible, peu modulable et très difficile à maintenir.

Figure 4. Organisation bazar (http://www.manning.com/allen/)

Organisation bazar (http://www.manning.com/allen/)

Oui, mais comment mettre de l'ordre ?

une réponse courante à ce problème consiste à séparer physiquement les différentes logiques. Typiquement on distingue :

  • La logique de présentation : partie de l'application en interaction avec l'utilisateur

  • La logique de traitement : partie de l'application qui réagit aux événements

  • La logique de persistance : concerne la logique de stockage des données (via un SGBDR par exemple)

il est alors facile d'identifier ces parties avec les langages mis en oeuvre dans des applications Web... sauf que là aussi, plusieurs modèles sont possibles.

  • Client léger (ou pauvre) : la partie IHM est en (X)HTML + CSS

  • Client riche : utilise un maximum de ressources du client Web (JavaScript, Ajax...) avec un minimum de problématique de déploiement (respect des standards du W3C). Exemples : Netvibes, GMail.

Nous nous concentrerons sur le client léger.

Remarque : le modèle client lourd concerne le client qui exécute de la logique de traitement en s'appuyant sur des composants dédiés et déployés pour l'occasion (le navigateur ne suffit pas). Exemple application Swing.

6. MVC

Le moèdel MVC (Model View Controller) adapté au Web est une réponse à une division des responsabilités.

Figure 5. Organisation MVC (http://www.manning.com/allen/)

Organisation MVC (http://www.manning.com/allen/)

Dans ce contexte :

  • la partie Vue est prise en charge par un script (PHP ou autre langage) générant du HTML (sans autre logique de traitement)

  • la partie contrôleur est représentée par un script PHP qui déclenche des traitements liés aux services (Use Case) auquel il est attaché (Un contrôleur par Use Case)

  • la partie Modèle est représentée par des scripts PHP gérant les accès au SGBDR. Cela peut être par exemple des classes Métier qui implémentent des fonctions CRUD vers un SGBDR.

Remarquez que les framework applicatifs (CMS, Forum, ...) sont généralement basés sur une architecture MVC. D'autres exemples ici : http://fr.wikipedia.org/wiki/Liste_de_frameworks_PHP

7. Zend Framework

Pourquoi utiliser un framework ?

En informatique, un framework peut être vu comme un outil de travail adaptable. C'est un ensemble de bibliothèques, d'outils et de conventions permettant le développement rapide d'applications. Un framework est composé de briques logicielles organisées pour être utilisées en interaction les unes avec les autres. L'utilisation d'un framework permet le partage d'un savoir-faire technologique et impose suffisamment de rigueur pour pouvoir produire une application aboutie et facile à maintenir ( voir Framework sur Wikipedia).

Nous utiliserons Zend Framework. ZF est un framework Open Source de la société Zend qui se veut modulaire et modulable.

Tableau 1. Voisi quelques modules du Zend Framework (2007)

Core:

Zend_Controller

Zend_View

Zend_Db

Zend_Config

Zend_Filter Zend_Valdiate

Zend_Registry

Authentication and Access:

Zend_Acl

Zend_Auth

Zend_SessionZend_Controller

Internationalization:

Zend_Date

Zend_Locale

Zend_Measure

Http:

Zend_Http_Client

Zend_Http_Server

Zend_UriZend_Controller

Inter-application communication:

Zend_Json

Zend_XmlRpc

Zend_Soap

Zend_Rest

Web Services:

Zend_Feed

Zend_Gdata

Zend_Service_Amazon

Zend_Service_Flickr

Zend_Service_Yahoo

Advanced:

Zend_Cache

Zend_Search

Zend_Pdf

Zend_Mail/Zend_Mime

Misc!

Zend_Measure


8. Installation des composants

Voir également le screencast d'installation installing ZF.

9. Structures des répertoires de l'application

L'objectif est d'isoler physiquement les différentes logiques (vues, contrôleurs, modèles, zone publique, librairies)

Pour ce faire, il est nécessaire d'établir une arborescence de répertoires. Nous utiliserons celle-ci :

 
     MyApp
     |-- application
     |   |-- controllers 
     |   |-- models 
     |   `-- views 
     |-- library 
     |   `-- Zend (racine des librairies de ZF) 
     `-- public
         |-- styles 
         |-- images 
         `-- js 
    
 

La zone publique contiendra des ressources directement accessibles. Par exemple via l'url http://monOrganisation.org/MyApp/public/myImage.png. On veillera à interdire l'accès aux autres répertoires.

10. contrôleur d'application

Pour que le framework Zend Framework puisse prendre la main, les requêtes devront être dirigées vers un même fichier : index.php (le bootstrap).

nous devons donc configurer Apache pour qu'il redirige les requêtes vers ce fichier.

Le fichier de configuration .htaccess contiendra, par exemple :

RewriteEngine on
RewriteBase /chemin/de/MyApp
RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php
 

Traduction : toute demande d'une ressource autre qu'un script, image etc. sera aiguillée vers index.php.

Ce fichier .htaccess sera placé à la racine de l'application, à côté de index.php.


MyApp
     |-- .htaccess
     |-- index.php
     

index.php, prenant la main, c'est lui qui est responsable de l'instanciation du framework.


<?php

// index.php 

// activer le reporting des erreurs 
error_reporting(E_ALL|E_STRICT);
ini_set('display_errors', 'on');

// localisation de la date 
date_default_timezone_set('Europe/Paris');

// modifie le chemin d'inclusion pour inclure le chemin de library
set_include_path('.' . PATH_SEPARATOR . './library'
  . PATH_SEPARATOR . get_include_path());

// inclure une classe spécialisée dans l'inclusion de classe du framework
include "Zend/Loader.php";

// chargement de la classe Front du dossier library/Zend/Controller/
Zend_Loader::loadClass('Zend_Controller_Front');

[...]

Cet extrait montre les premières étapes du contrôleur principal (Front Controller). Après avoir activé les options de report d'alerte (pour une bonne gestion des variables :), nous fournissons à PHP le chemin des librairies de ZF et autres.

Nous pouvons alors charger la classe Zend/Loader.php, dont la principale fonction sera de simplifier le chargement des autres classes du framework, selon nos besoins.

Ce script doit maintenant instancier le Front Controller, le configurer et le lancer.


<?php

// suite et fin de index.php

[...]

Zend_Loader::loadClass('Zend_Controller_Front');

// obtenir une instance du contrôleur 
$frontController = Zend_Controller_Front::getInstance();

// la configurer
$frontController->setControllerDirectory('./application/controllers');

// en phase de developpement, le client voit le retour des erreurs
// (cette information devrait être placée dans un fichier de configuration)
$frontController->throwExceptions(true);

// run!
$frontController->dispatch();

Après avoir obtenu une instance, nous spécifions le répertoire où seront logés les contrôleurs de l'application, puis nous demandons à ce même contrôleur de prendre en charge les erreurs, qui seront retournées à l'appelant.

Passer false à la méthode throwExceptions nous obligerait à gérer nous même le rendu des erreurs, donc à concevoir un contrôleur et une vue associée, ce que vous réaliserez sans trop tarder.

Voila, la première version de notre index.php est terminée. Vous remarquerez qu'elle ne comporte aucune logique de l'application ; en effet son rôle est principalement technique. Le terme technique est, dans ce contexte, opposé au terme métier, ce dernier terme faisant référence à la logique de l'application régie par les règles de gestion liées au problème à résoudre.

11. Contrôle de connaissances

POO

11.1. Le modèle objet de Php implémente une logique par référence ?
11.2. En POO Php, this et self font-ils référence à l'instance courante ?
11.3. Quel est le rôle de la méthode toString ?

11.1.

Le modèle objet de Php implémente une logique par référence ?

Oui

11.2.

En POO Php, this et self font-ils référence à l'instance courante ?

FAUX. this a le même rôle qu'en Java, et self fait référence à l'« instance » de la classe chargée en mémoire.

11.3.

Quel est le rôle de la méthode toString ?

Représenter textuellement l'état de l'instance concernée par l'appel.

ZF

11.1. L'"architecture bazar" permet un développement rapide d'application
11.2. Avec ZF/MVC seules certaines ressources sont directement accessibles par l'internaute.

11.1.

L'"architecture bazar" permet un développement rapide d'application

En apparence seulement ! cette approche du développement génère une complexité (tout est mélangé) qui tend au blocage total, proportionnellement à l'accroissement de la couverture fonctionnelle de l'application.

11.2.

Avec ZF/MVC seules certaines ressources sont directement accessibles par l'internaute.

VRAI, moyennant quelques précautions côté configuration du serveur HTTP (.htaccess par exemple). Dans notre exemple, elles sont situées dans l'arborescence ayant public comme racine.

12. Mise en place d'un contrôleur

Nous allons concevoir la Consultation des résultats potentiels de l'ensemble des étudiants.

C'est un cas d'utilisation du sytème. Nous allons donc concevoir un contrôleur de cas d'utilisation.

Ce contrôleur sera accessible par l'url : http://host/myapp/resultats

Par défaut, ZF fait correspondre à cette ressource un script PHP bien particulier. Hormis le fait que le nom de ce script est basé sur le nom de la ressource (ici Resultats) suivi du mot Controller (on remarquera la capitalisation des termes), ce script, qui est en fait une classe héritant de Zend_Controller_Action, sera placé dans la branche controllers :

 
     MyApp
     |-- application
     |   |-- controllers
     |   |    |--ResultatsController.php
     |   |               
     |   |-- models 
     |   `-- views 
     |-- library 
     |   `-- Zend (racine des librairies de ZF) 
     `-- public
         |-- styles 
         |-- images 
         `-- js 
    
 

Voici le contrôleur en question :

  
 <?php
class ResultatsController extends Zend_Controller_Action
{	
 public function init()
  {
  	
  }

  public function preDispatch()
  {

  }
  
  public function indexAction()
  {
    $this->render();
  }

  public function postDispatch()
  {

  }
  
  
}

 

Présentons rapidement ce script PHP

  • C'est une classe qui hérite de Zend_Controller_Action, une classe de framework.

  • init: appelé une seule fois, à l'instanciation de la classe (à l'image d'un constructeur)

  • preDispatch: appelé avant un appel automatique à une méthode d'action

  • xxAction : des méthodes d'action

  • postDispatch: appelé après un appel automatique à une méthode d'action

Il est important de comprendre que vous ne ferez quasiment jamais appel explicitement à ces méthodes ! elles font partie de la logique du framework, et c'est ce dernier qui prend en charge les appels.

Votre rôle, en tant que développeur, consiste à définir (concevoir, déclarer) les bonnes classes, les bonnes méthodes.

12.1. Méthode d'Action

Une Méthode d'Action est une méthode d'une sous-classe de Zend_Controller_Action identifiée par un nom se terminant par Action. Par exemple : ajouterAction, supprimerAction.

Qu'est-ce qu'une « Action » ? C'est le « service » associé à une ressource. Par exemple, dans la requête suivante :

http://serveur/racineApplication/document/obtenir

la ressource demandée est document et le service sollicité est obtenir. Dans un contexte ZF de base, le contrôleur DocumentController.php (qui sera automatiquement chargé, suivi d'une instanciation) devra contenir la méthode obtenirAction, car c'est cette dernière qui sera appelée par le framework.

L'action par défaut : En abscence de nom de service l'action « indexAction » sera invoquée. Par exemple, dans la requête suivante :

http://serveur/racineApplication/document

la méthode indexAction de l'objet, instance de DocumentController (ou IndexController par défaut), sera appelée.

Il en va de même avec la ressource. En abscence de nom de ressource, c'est la ressource « index » qui sera sollicitée. Par exemple, dans la requête suivante :

http://serveur/racineApplication/

sera interprété comme:

http://serveur/racineApplication/index/index

Cela suppose donc que le contrôleur IndexController et sa méthode indexAction existent !

Sollicitons notre contrôleur ResultatsController :

Figure 6. Interaction avec le contrôleur

Interaction avec le contrôleur

Nous avons droit à une erreur, c'est tout à fait NORMAL ! Explications :

En analysant la première ligne (voir encadré) du rapport d'erreur (pas d'affolement, l'analyse de la première ligne suffit dans la majorité des cas), nous constatons que le système recherche le fichier ./application/views/scripts/resulats/index.phtml.

C'est en fait le script responsable de la VUE (le V de MVC), le contrôleur de cas d'utilisation (le C de MVC) ne s'occupant que de la logique de type conversationnel et traitement. Remarque : la partie modèle (le M de MVC) sera introduite dans un autre chapitre.

En effet, il ne devrait pas y avoir d'instruction echo ou print dans un contrôleur (mais parfois des Zend_Debug::dump() ;-).

12.2. Vue d'action

Comme on a pu le constater, ZF, par défaut, s'attend à ce que le développeur ait conçu des vues pour chaque méthode d'action.

 
     MyApp
     |-- application
     |   |-- controllers
     |   |    |--ResultatsController.php
     |   |               
     |   |-- models 
     |   `-- views 
     |   |    |--scripts
     |   |    |    |--resultats
     |   |    |    |    |--index.phtml
     |-- library 
     |   `-- Zend (racine des librairies de ZF) 
     `-- public
         |-- styles 
         |-- images 
         `-- js 
    
 

Les vues sont stockées dans la branche racine/application/views/scripts/xxx/yyy, ou xxx est le nom de la ressource sollicitée (en référence au contrôleur), et yyy le nom de la vue en relation, de la forme nomDuService.phtml . Le nom du service étant la partie gauche du nom de la méthode d'action. Par exemple :

http://serveur/racineApplication/document/ajouter

la métode ajouterAction de l'objet, instance de DocumentController, sera appelée, puis la vue application/views/scripts/document/ajouter.phtml sera traitée et retournée au client web.

Bien entendu, toute cette logique de traitement est entièrement prise en charge par le framework.

Voici un exemple de vue ./application/views/scripts/resultats/index.phtml :


<h1>Les résultats</h1>

Sollicitons de nouveau notre contrôleur ResultatsController :

Figure 7. Interaction avec le contrôleur : http://.../zfmvc-tuto/resultats/index

Interaction avec le contrôleur : http://.../zfmvc-tuto/resultats/index

12.3. Résumé

Nous avons vu :

  • Comment concevoir, par héritage, un contrôleur de cas d'utilisation.

  • Comment solliciter, par l'URL, une action d'un contrôleur de cas d'utilisation.

  • Comment associer une vue à une action du contrôleur

13. Exercices

La documentation que vous avez téléchargée ( documentation/manual/core/fr/zend.controller.html) nous informe que le framework est configuré pour fonctionner avec un contrôleur d'erreurs. Par défaut, il s'agit de ErrorController dans le module par défaut avec une méthode errorAction.

  1. Concevoir le contrôleur et sa méthode

  2. Concevoir la vue associée

  3. Tester la gestion des erreurs. Pour cela il est nécessaire d'intervenir sur le script de démarrage (bootstrap), afin de spécifier que l'application prend en main la gestion des exceptions :

       $frontController->throwExceptions(false);
       

    Exemple de résultat attendu : si vous sollicitez une ressource non prévue, par exemple : http://.../zfmvc-tuto/etudiants :

    Figure 8. Interception d'une erreur

    Interception d'une erreur

  4. On souhaite personnaliser les messages d'erreur. La méthode d'action peut demander au gestionnaire d'erreur de ZF (en fait c'est un plugin installé par défaut) le type d'erreur :

     
      function errorAction()
      {
        $errors = $this->_getParam('error_handler');
        switch ($errors->type) {
          case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
          case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
            // page not found error (404)
            $this->render('404');
            break;
          default:
            // Application error (500)
            $this->render('500');
            break;
        }
      }
      
     

    Comme vous pouvez le constater, un argument est passé à la méthode render.

    Bien entendu, les scripts de vue 404.phtml et 500.phtml sont attendus...

14. Gestion du contrat : contrôleur -> Vues

Les responsabilités d'un contrôleur de cas d'utilisation sont :

  • Prendre en charge la logique du service demandé (les traitements)

  • Interagir pour cela avec les données de l'application, représentées par le modèle (le M de MVC)

  • Transmettre à la vue les informations dont elle a besoin pour une réponse personnalisée en direction du demandeur (client web)

Les responsabilités d'une vue sont :

  • Présenter les informations dont elle dispose.

  • Gérer la présentation de l'absence possible de certaines informations (liste vide par exemple)

contrôleur et Vue sont donc liés par un contrat :

  • La vue prend en charge la présentation des informations

  • Le contrôleur, après traitement, est tenu de transmettre à la vue les informations dont elle a la charge

Cas particuliers

  • Le contrôleur, après traitement, n'a pas de donnée à transmettre à la vue :

    => il peut « passer la main » à une autre méthode d'action :

      ... 
      public function supprimerAction() {
       ...
       $this->_forward('index'); // soustraite (renvoi) à une autre action de ce contrôleur
      } 
     
  • La vue n'attend rien du contrôleur. Pas de problème, c'est ce que nous avons fait jusqu'à présent.

  • Sélection d'une autre vue (liée au même contrôleur) que celle basée sur le nom de l'action : $this->render('autrevue'); - plus d'infos ici zend.controller.action.html

Nous nous intéressons maintenant à la façon dont le contrôleur transmet des informations à la vue, et comment la vue les exploite.

Les informations peuvent provenir d'une base de données, et/ou de données calculées, préparées par la méthode d'action. Le schéma général est :

Figure 9. Interaction type d'un système MVC avec les données

Interaction type d'un système MVC avec les données

14.1. Le modèle simple de ce tuto

Afin de ne pas rentrer ici dans la logique ZF du Modèle, nous nous contenterons d'un Modèle simple représenté par un tableau d'objets Etudiant en mémoire.

Voici donc les classes de notre modèle (voir Intro) :

Figure 10. Classes du modèle

Classes du modèle

Une représentation objet (Etudiant.php) :


<?php

class Etudiant{
  private $id;
  private $nom;
  private $prenom;

  public function __construct($id, $prenom, $nom){
    $this->nom=$nom;
    $this->prenom=$prenom;
    $this->id=$id;
  }

  public function __toString(){
    return $this->prenom .' '.$this->nom;
  }

  public function setNom($nom) {
    $this->nom=$nom;
  }
  
  public function setPrenom($prenom) {
    $this->prenom=$prenom;
  }

  public function getNom() {
    return $this->nom;
  }
  
  public function getPrenom() {
    return $this->prenom;
  }
}
?>

Enseignant.php :


<?php

class Enseignant{
  private $id;
  private $nom;
  private $prenom;

  public function __construct($id, $prenom, $nom){
    $this->nom=$nom;
    $this->prenom=$prenom;
    $this->id=$id;
  }

  public function __toString(){
    return $this->prenom .' '.$this->nom;
  }

  public function setNom($nom) {
    $this->nom=$nom;
  }
  
  public function setPrenom($prenom) {
    $this->prenom=$prenom;
  }

  public function getNom() {
    return $this->nom;
  }
  
  public function getPrenom() {
    return $this->prenom;
  }
}
?>

Diplome.php :


<?php

class Diplome {
  private $id;
  private $libelle;
  private $nbMinSemainesStage;
  private $idResponsable;

  public function __construct($id,  $idResponsable, $libelle, $minSemStage,){
    $this->id=$id;
    $this->libelle=$libelle;
    $this->nbMinSemainesStage= $minSemStage;  
    $this->idResponsable=$idResponsable;  
  }
  
  
  public function getId() {
    return $this->id;
  }
  
  public function setLibelle($lib) {
    $this->libelle=$lib;
  }

  public function getLibelle() {
    return $this->libelle;
  }
  
  public function setNbMinSemainesStage($n) {
    $this->nbMinSemainesStage=$n;
  }

  public function getNbMinSemainesStage() {
    return $this->nbMinSemainesStage;
  }
  
  public function getNbInscrits(){
    // TODO
    return -1;
  }

}
?>

Et Inscription.php :



<?php

class Inscription{
  private $idEtudiant;
  private $idDiplome;
  private $noteExamen;
  private $nbSemStage;

  public function __construct($idEtudiant, $idDiplome){
    
    $this->idEtudiant=$idEtudiant;
    $this->idDiplome=$idDiplome;
  }

  public function __toString(){
    return $this->idEtudiant .' '.$this->idDiplome;
  }

  public function getIdEtudiant() {
    return $this->idEtudiant;
  }
  
  public function getIdDiplome() {
    return $this->idDiplome;
  }
  
  public function setNoteExamen($note) {
    $this->noteExamen=$note;
  }

  public function getNoteExamen() {
    return $this->noteExamen;
  }

  public function setNbSemStage($n) {
    $this->nbSemStage=$n;
  }

  public function getNbSemStage() {
    return $this->nbSemStage;
  }

  public function estRecu() {
    // TODO
    return false; 
  }

}
?>

L'arborescence s'enrichit :

     MyApp
     |-- application
     |   |-- controllers
     |   |    |--ResultatsController.php
     |   |               
     |   |-- models 
     |   |    |--Etudiant.php
     |   |    |--Enseignant.php
     |   |    |--Diplome.php
     |   |    |--Inscription.php     
     |   `-- views 
     |   |    |--scripts
     |   |    |    |--resultats
     |   |    |    |    |--index.phtml
     |-- library 
     |   `-- Zend (racine des librairies de ZF) 
     `-- public
         |-- styles 
         |-- images 
         `-- js     
 

14.2. Configuration du bootstrap : spécifier le chemin des modèles

Pour utiliser des objets de notre modèle, l'application a besoin de connaître leur localisation (application/models). Ceci se réalise dans le bootstrap :

 
<?php
// index.php
error_reporting(E_ALL|E_STRICT);

date_default_timezone_set('Europe/Paris');

set_include_path('.' . PATH_SEPARATOR . './library'
  . PATH_SEPARATOR . './application/models/'
  . PATH_SEPARATOR . get_include_path());

[...]  

  

14.3. La Vue exploite les informations dont elle a la charge

Nous considérons que la vue dispose d'une collection 'diplomes' (en fait des formations).

 

<style type="text/css">
<!--
table,th,td {
 border-style: solid;
 border-width: 1px;
}

table,caption {
 margin-left: auto;
 margin-right: auto;
 width: auto;
}
-->
</style>
<table>
 <caption>Les diplômes</caption>
 <thead>
  <tr>
   <th>id</th>
   <th>Libellé</th>  
   <th>Nb Inscrits</th>
   <th>Nb Reçus</th>
  </tr>
 </thead>
 <tbody>
 <?php
 foreach ($this->diplomes as $diplome) {
   echo '<tr><td>' . $diplome->getId(). '</td>';
   echo ' <td>' . $diplome->getLibelle() .'</td>';
   echo '<td> &nbsp; </td>';
   echo " <td> &nbsp; </td></tr>\n";
 }
 ?>
 </tbody>
</table>


Ce script de vue exploite un tableau d'objets de type Diplome.

Le contrat qui lie la vue et le contrôleur est simple : La vue considère qu'un contrôleur lui a mis à disposition, sous l'appellation diplomes, une collection d'objets de type Diplome.

Remarque : Dans cette configuration, Contrôleur et Vue sont dépendants du Modèle.

Voici ce que nous obtenons :

Figure 11. http://localhost/.../zfmvc-tuto/resultats

http://localhost/.../zfmvc-tuto/resultats

Une erreur attendue ! En effet, le contrôleur n'a pas encore réalisé sa part du contrat. L'erreur vient du fait que la variable $diplomes n'a encore pas d'existence dans le contexte de la vue.

La vue a-t-elle le droit d'y aller franco ?, de ne prendre aucune précaution su l'existence ou non de variables dans son contexte ? la réponse est OUI ! En effet, le cycle de vie des variables de contexte n'est pas de sa responsabilité. Revoir le schéma MVC schéma général.

Le travail sur la vue étant terminé, nous pouvons passer à la logique du contrôleur.

14.4. Le Contrôleur transmet des informations à la Vue

Pour les besoins du test, les données seront provisoirement stockées en mémoire dans un tableau. le contrôleur se chargera de cette tâche, mais ce n'est que PROVISOIRE bien entendu.

Création d'une collection d'objets Diplome :


<?php
class ResultatsController extends Zend_Controller_Action
{
  public function init()
  {
    Zend_Loader::loadClass('Zend_Debug');

  // chargement de la classe Diplome
    Zend_Loader::loadClass('Diplome');

    // creation d'une champ de type collection
    $this->diplomes=array();

    // création des éléments
    for ($i=0; $i<10; $i++) {
      $d = new Diplome($i, 'lib_'.$i, ($i+1)*2);
      $this->diplomes[]=$d;
    }
  }
[...]


 

Nous placerons ces instructions dans le corps de la méthode init de ResultatsController.

La méthode d'action indexAction du contrôleur ResultatsController se charge de pousser les données sur la vue :


  public function indexAction()
  {
    //Zend_Debug::dump("Passe par indexAction de ResultatsController");
    $this->view->diplomes=$this->diplomes;
    $this->render();
  }

Et le tour est joué !

Voici ce que nous obtenons :

Figure 12. http://localhost/.../zfmvc-tuto/resultats

http://localhost/.../zfmvc-tuto/resultats

14.5. Résumé

On a vu :

  • Les devoirs du contrôleur, de la vue.

  • Les droits de la vue (ceux du contrôleur étant liés aux données reçues ainsi qu'au modèle)

  • La façon dont le contrôleur passe des données à la vue.

  • La façon dont la vue exploite des données.

  • ... et pour les plus observateurs, la façon d'envoyer des données de débogage (Zend_Debug::dump).

15. Passage d'arguments via l'url

Nous souhaitons ne pouvoir consulter qu'un seul diplome à la fois (et non un liste comme précédemment) .

[Note]Autre service -> autre UC

Nous considérons cette demande comme un nouveau cas d'utilisation du système spécifique au contrôleur Resultat.

Nous concevons alors une nouvelle méthode d'action : voirdiplome.

15.1. Conception de la vue

Fichier voirdiplome.phtml

    
<style type="text/css">
<!--
.diplome {
 margin-left: 40px; 
}
-->
</style>
<div class='diplome'>
<h2>Diplôme</h2>
Diplome : <?= $this->diplome->getLibelle() ?> <br>
Nombre de semaines de stage : <?= $this->diplome->getNbMinSemainesStage() ?> <br>
Nombre d'inscrits : <?= $this->diplome->getNbInscrits() ?> <br>
</div>  
  
  

Le problème se pose côté contrôleur : Quel diplome transmettre à la vue ?

Le service devra donc être paramétré. Le contrôleur attend donc une donnée lui permettant de déterminer quel diplome transmettre à la vue. Par défaut le contrôleur transmettre le premier de la liste (index=0).

Voici la méthode d'action en charge de ce cas d'utilisation du contrôleur ResultatsController :

  
     
  public function voirdiplomeAction()
  {
    if ($this->_hasParam('id'))
       $index = $this->_getParam('id');
    else
       $index = 0;

    $this->view->diplome=$this->diplomes[$index];
    $this->render();
  }
  
  

La méthode vérifie qu'elle dispose bien d'un argument ('id'). Si ce n'est pas le cas, elle choisira le premier diplôme (index=0), sinon, elle utilise directement la valeur reçue comme index de collection (très dangereux, à ne pas reproduire !)

Nous obtenons :

Figure 13. http://localhost/.../zfmvc-tuto/resultats/voirdiplome

http://localhost/.../zfmvc-tuto/resultats/voirdiplome

Noter que la doc API (method _getParam) nous indique que la méthode _getParam admet comme second argument optionnel un valeur par défaut. Ainsi aurions-nous pu écrire :

 $index = $this->_getParam('id', 0);

15.2. Passage d'arguments

Nous passerons les arguments par l'URL. Le mode rewrite étant activé sur le serveur HTTP, les arguments peuvent être insrits dans une expression de chemin :

  http:// ... /zfmvc-tuto/resultats/voirdiplome/id/1
  

Équivalent à :

  http:// ... /zfmvc-tuto/resultats/voirdiplome?id=1
  

Sauf que l'expression de chemin est d'un apparence plus stable, donc plus facile à gérer pour les moteurs de recherche, entre autres.

La syntaxe générale de passage d'arguments : .../nom1/val1/nom2/val2/.../nomN/valN

Nous testons le contrôleur en demandant de consulter le deuxième diplôme (id=1)

Figure 14. http://localhost/.../zfmvc-tuto/resultats/voirdiplome/id/1

http://localhost/.../zfmvc-tuto/resultats/voirdiplome/id/1

Enfin, voici les liens logiques liant les éléments de la requête et le contrôleur.

Figure 15. http://localhost/.../zfmvc-tuto/resultats/voirdiplome/id/1

http://localhost/.../zfmvc-tuto/resultats/voirdiplome/id/1

16. Contrôle de connaissances

(M)VC

16.1. Pourquoi est-il déconseillé à la vue de *systématiquement* vérifier la présence de variables de contexte ?

16.1.

Pourquoi est-il déconseillé à la vue de *systématiquement* vérifier la présence de variables de contexte ?

Afin de responsabiliser les contrôleurs.

17. Exercices

Avant d'interagir avec le modèle, et maintenant que vous avez compris l'essentiel (rôle d'une vue, d'un contrôleur, passage d'arguments), vous réaliserez les cas d'utilisation suivant:

  1. Permettre à un utilisateur, lorsqu'il consulte UN diplôme, de naviguer vers le prochain ou le précédent.

    Prévoir un nouveau contrôleur :

      http:// ... /zfmvc-tuto/resultats/voirdiplomenavigation
      
  2. Reprendre l'exercice précédent mais ne pas proposer à l'utilisateur la possibilité de naviguer avant le premier et après le diplôme affiché.

    Indication : Conformément au paradigme MVC, on veillera à ne pas charger la vue en responsabilité (métier/technique).

18. Introduction au Modèle

18.1. Rôles du modèle

Dans une application de gestion typique (interface avec un SGBDR), il n'est pas rare d'avoir un modèle qui colle aux tables concernées par les responsabilités de l'application.

Dans ce contexte, il parait alors logique de charger le modèle des responsabilités d'accès aux données : create, retreive, update, delete (CRUD).

Pour cela, et pour ne pas avoir à réinventer la roue, il est commode de faire hériter les classes du modèle par une classe prenant en charge les interactions typiques avec un SGBDR. ZF propose la classe Zend_Db_Table_Abstract comme parent de base des classes du modèle.

18.2. Définition du modèle

Voici une description SQL de la structure de la base, fidèle à l'analyse du domaine :

  
  CREATE TABLE  `tutoMvc`.`enseignant` (
  `id` int(11) NOT NULL auto_increment,
  `nom` varchar(50) NOT NULL,
  `prenom` varchar(50) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;

  CREATE TABLE  `tutoMvc`.`diplome` (
  `id` int(11) NOT NULL auto_increment,
  `libelle` varchar(100) NOT NULL,
  `nbMinSemainesStage` int(11) NOT NULL default '0',
  `idResponsable` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `resp_diplome_fk_constraint` (`idResponsable`),
  CONSTRAINT `resp_diplome_fk_constraint` FOREIGN KEY (`idResponsable`)
  REFERENCES `enseignant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1;
  
  CREATE TABLE  `tutoMvc`.`etudiant` (
  `id` int(11) NOT NULL auto_increment,
  `nom` varchar(50) NOT NULL,
  `prenom` varchar(50) NOT NULL,
   PRIMARY KEY  (`id`)
  ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
  
 CREATE TABLE  `tutoMvc`.`inscription` (
  `idEtudiant` int(11) NOT NULL,
  `idDiplome` int(11) NOT NULL,
  `nbSemainesStage` int(11) NOT NULL default '0',
  `note` DECIMAL(2,0) default NULL,
  PRIMARY KEY  (`idEtudiant`,`idDiplome`),
  KEY `res_diplome_fk_constraint` (`idDiplome`),
  CONSTRAINT `res_etudiant_fk_constraint` FOREIGN KEY (`idEtudiant`) 
  REFERENCES `etudiant` (`id`),
  CONSTRAINT `res_diplome_fk_constraint` FOREIGN KEY (`idDiplome`) 
  REFERENCES `diplome` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;  
  

Figure 16. Exemple de définition des tables avec l'outil MySQL Administrator

Exemple de définition des tables avec l'outil MySQL Administrator

On remarquera que la table inscription comporte une valeur de champ pouvant ne pas être renseignée.

mysql> describe inscription;
+-----------------+--------------+------+-----+---------+-------+
| Field           | Type         | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+-------+
| idEtudiant      | int(11)      | NO   | PRI |         |       | 
| idDiplome       | int(11)      | NO   | PRI |         |       | 
| nbSemainesStage | int(11)      | NO   |     | 0       |       | 
| note            | decimal(4,2) | YES  |     | NULL    |       | 
+-----------------+--------------+------+-----+---------+-------+

De plus, la base dispose d'un utilisateur non root (login : userWeb, mote de passe : userWeb).

 show grants for userWeb;
+-----------------------------------------------------------------------+
| Grants for userWeb@%                                                  |
+-----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'userWeb'@'%'                                   |
| IDENTIFIED BY PASSWORD '*5BE427F69545FEA5458A874193160978A8909C15'    | 
| GRANT SELECT, INSERT, UPDATE, DELETE ON `tutoMvc`.* TO 'userWeb'@'%'  | 
+-----------------------------------------------------------------------+

Remarque : cet utilisateur peut être crée via l'entrée "User Administration" de MySQL Administrator.

Voici, pour les tests, quelques instructions d'insertion :

INSERT INTO `tutoMvc`.`enseignant` VALUES  (1,'nomEns1','prenomEns1');
INSERT INTO `tutoMvc`.`enseignant` VALUES  (2,'nomEns2','prenomEns2');

INSERT INTO `tutoMvc`.`diplome` VALUES  (1,'BTS Informatique de Gestion',12,1);
INSERT INTO `tutoMvc`.`diplome` VALUES  (2,'BTS Biotechnologies',14,1);
INSERT INTO `tutoMvc`.`diplome` VALUES  (3,'BTS Design de Produits',4,2);
INSERT INTO `tutoMvc`.`diplome` VALUES  (4,'B2i - Brevet informatique et internet',0,2);

INSERT INTO `tutoMvc`.`etudiant` VALUES  (1,'nomA','prenomA');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (2,'nomB','prenomB');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (3,'nomC','prenomC');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (4,'nomD','prenomD');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (5,'nomE','prenomE');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (6,'nomF','prenomF');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (7,'nomG','prenomG');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (8,'nomH','prenomH');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (9,'nomI','prenomI');
INSERT INTO `tutoMvc`.`etudiant` VALUES  (10,'nomJ','prenomJ');

INSERT INTO `tutoMvc`.`inscription` VALUES  (1,1, 14,13.4);
INSERT INTO `tutoMvc`.`inscription` VALUES  (2,1, 12,9);
INSERT INTO `tutoMvc`.`inscription` VALUES  (3,1, 12, null);
INSERT INTO `tutoMvc`.`inscription` VALUES  (1,4, 18, 0);
INSERT INTO `tutoMvc`.`inscription` VALUES  (2,4, 19, 0);
INSERT INTO `tutoMvc`.`inscription` VALUES  (3,4, 9, 0);

Un backup complet : tutoMvcDataBase.sql .

18.3. Utilisation de Zend_Db_Table, Zend_Db_Row

Nous avons avec la base de données (le SGBDR), deux types de structure à gérer : La table et ses lignes

ZF met à notre disposition deux super-classe Zend_Db_Table_Abstract et Zend_Db_Table_Row_Abstract

Récrivons notre classe modèle Diplome que nous rebaptiserons DiplomeRow (les raisons de ce renommage seront justifiées ultérieurement) :

 
 <?php

// DiplomeRow.php

class DiplomeRow extends Zend_Db_Table_Row_Abstract {
    
  public function getNbInscrits(){
    // TODO
    return -1;
  }

}

  
 

Ainsi que la classe Diplome que nous lions à DiplomeRow :

// Diplome.php
 
class Diplome extends Zend_Db_Table_Abstract {    
  protected $_name = 'diplome';
  protected $_primary = 'id';
  protected $_rowClass = 'DiplomeRow'; 
}


 

C'est tout ? Pour l'instant OUI. Les attributs de la classe DiplomeRow sont auto-générés à partir du nom des attributs de la table (les colonnes).

En relançant http://localhost/.../zfmvc-tuto/resultats/voirdiplome/id/1 , nous obtenons

Fatal error: Class 'Zend_Db_Table_Row_Abstract' 
not found in /.../zfmvc-tuto/application/models/DiplomeRow.php on line 3

Nous devons charger en mémoire les classes Zend_Db_Table_Abstract et Zend_Db_Table_Row_Abstract. Afin d'éviter de répéter l'ordre de chargement dans tous nos modèles, nous placerons dans index.php, le chargement de cette classe :

// chargement de la classe Front du dossier library/Zend/Controller/
Zend_Loader::loadClass('Zend_Controller_Front');
Zend_Loader::loadClass('Zend_Db_Table_Row_Abstract');

[...]
 

Nous obtenons alors, dans le navigateur, une toute autre erreur :

Fatal error: Uncaught exception 'Zend_Db_Table_Exception' with message
 'Argument must be of type Zend_Db_Adapter_Abstract, or a Registry key 
 where a Zend_Db_Adapter_Abstract object is stored'

Et oui ! Nous avons oublié une étape essentielle, celle consistant à spécifier le type de SGBDR et le paramètres de connexion !

Zend Framework met à notre disposition un module de gestion de fichier de configuration Zend_Config_Ini

Nous plaçons alors les données de connexion dans un fichier de configuration à l'abri des regards (./application/config.ini) :

[general]
db.adapter = PDO_MYSQL
db.host = localhost
db.username = userWeb
db.password = userWeb
db.dbname = tutoMvc

Retour donc dans index.php.

Nous définissons un objet Zend_Db_Adapter (voir Manuel en ligne).

[...]
include "Zend/Loader.php";
Zend_Loader::loadClass('Zend_Controller_Front');

Zend_Loader::loadClass('Zend_Db');
Zend_Loader::loadClass('Zend_Db_Table_Abstract');


// Chargement automatique de Zend_Db_Adapter_Pdo_Mysql, et instanciation.
$config = new Zend_Config_Ini('./application/config.ini', 'general');

$db = Zend_Db::factory($config->db->adapter,array(
   'host'  => $config->db->host, 
  'username'  => $config->db->username,
  'password'  => $config->db->password,
  'dbname'  => $config->db->dbname,
    )
);

// placons la connexion dans un registre global à l'application
Zend_Loader::loadClass('Zend_Registry');
$registry = Zend_Registry::getInstance();
$registry->set('db', $db);


[...]

On remarquera la souplesse d'utilisation de Zend_Config_Ini : les clés du fichier de configuration sont traduites en propriétés public, ce qui nous permet de consulter une valeur associée à une clé par : instance_Zend_Confi_Ini->clé

Nous devons repenser le contrôleur et la vue. En effet, comme nous le verrons, nous n'avons plus à faire à des objets dans un tableau créés par le contrôleur, mais à une collection "RowSet" (ens. de lignes) d'objets de type "Row" (ligne).

Les objets de type Zend_Db_Table_Row, sont automatiquement spécialisés par ZF en fonction du modèle.

Il est temps maintenant de modifier notre contrôleur :

class ResultatsController extends Zend_Controller_Action
{

  public function init()
  {
    Zend_Loader::loadClass('Zend_Debug');

    // chargement de la classe Diplome 
    Zend_Loader::loadClass('Diplome');    
  }

  public function indexAction()
  {
    // on retrouve le registre global  
    $registry = Zend_Registry::getInstance();
    // et notre connexion
    $db = $registry->get('db');
    
    // que l'on passe au constructeur (hérité) de notre modèle
    $d = new Diplome($db);
    
    // obtenons un objet Zend_Db_Select (capable d'appliquer des restrictions)
    $select = $d->select();
    
    // aucune clause where
    
    // nous transmettons à la vue l'ensemble des lignes de la table diplome
    // soit un objet Zend_Db_Table_Rowset 
    //    (colection d'objets DiplomeRow - des Zend_Db_Table_Row) 
    $this->view->diplomes=$d->fetchAll($select);    

    $this->render();
  }
[...]
[Note]Connexion par défaut

Si l'application n'accède qu'à une seule base de données, on peut, par exemple dans le bootstrap (index.php), spécifier une connexion par défaut.

  
  Zend_Db_Table::setDefaultAdapter($db);
  Zend_Registry::set('dbAdapter', $db); 
 

Ainsi, dans le contrôleur, le code se trouve encore réduit : :

// SANS connexion par défaut
   // on retrouve le registre global  
   $registry = Zend_Registry::getInstance();
   // et notre connexion
   $db = $registry->get('db');
   
   // que l'on passe au constructeur (hérité) de notre modèle
   $d = new Diplome($db);

//AVEC connexion par défaut (une seule instruction !)
   // Aucun argument à la construction. 
   // Liaison automatique via la connexion par défaut 
   $d = new Diplome();    
 

Le script de vue views/resultats/index.phtml doit légèrement être impacté.

En effet, les attributs automatiques de notre modèle sont directement accessibles (non privés). Donc l'instruction $diplome->getId() sera transformée en $diplome->id, tout simplement. Voici les changements à apporter :

 
<?php
 foreach ($this->diplomes as $diplome) {
   echo '<tr><td>' . $diplome->id  . '</td>';
   echo ' <td>' . $diplome->libelle .'</td>';
   echo ' <td>' . $diplome->idResponsable .'</td>';
   echo '<td> &nbsp; </td>';
   echo " <td> &nbsp; </td></tr>\n";
 }

Figure 17. resultats/index.phtml

resultats/index.phtml

18.4. Opérations CRUD

Les opérations de base SQL sur les données (interrogation, création, modification, suppression) sont assistées par des méthodes héritées de Zend_Db_Table_Abstract. En effet, ces responsabilités sont conceptuellement soit de niveau 'table', soit de niveau 'objet métier' ('tuple'). En voici quelques exemples :

18.4.1. Sélectionner une ligne, une objet métier

Recherche le diplôme d'id = 1

// Fontion de niveau 'table' 
 $dt = new Diplome();  
 $diplome = $dt->find(1)->current();  
 

La méthode find prend comme argument un identifiant (un ou plusieurs arguments selon le cas), et retourne un Rowset.

La méthode current d'un Rowset retourne l'élément courant (le premier et unique dans notre cas).

18.4.2. Modifier un objet métier, une ligne

Modification du libellé du diplôme d'id = 1

// Fontion de niveau 'tuple' 
 $dt = new Diplome();  
 $diplome = $dt->find(1)->current();
 $diplome->libelle = "new libelle";
 $diplome->save();
 

La méthode save fait une mise à jour, dans la table diplome, du tuple associé à notre objet.

18.4.3. Suppression d'un objet métier, une ligne

suppression du diplôme B2i, d'id = 4

// Fontion de niveau 'tuple' 
 $dt = new Diplome();  
 $diplome = $dt->find(4)->current();
 $diplome.delete();
 

La méthode delete supprime, de la table Diplome, le tuple associé à notre objet.

18.4.4. Création d'un objet métier, une ligne

création du diplôme , d'id = 4

// Fontion de niveau 'table' et 'tuple'
  $dt = new Diplome(); 
  // les données sont représentées par un dictionnaire
  $data = array(  
    'libelle' => 'BTS IRIS',
    'nbMinSemainesStage' => 10,
    'idResponsable' => 1
  ); 
  $diplome=$dt->createRow($data);  
  if ($diplome ) {
    $id=$diplome->save();
    $this->view->diplome=$diplome;
  }    
 

La méthode createRow crée un 'tuple' en mémoire.

La méthode save tente d'insérer le nouveau tuple dans la table diplome.

18.4.5. Transaction

On se réfère à la connexion pour gérer une transaction :

 $db = Zend_Registry::get('dbAdapter');
 $db->beginTransaction();
 try {
    // insert etudiant
    [...]    
    
    // insert diplome avec une clé primaire erronée 
    $data = array(
     'id' => 1,  
     'libelle' => 'BTS IRIS',
     'nbMinSemainesStage' => 10,
     'idResponsable' => 2
    );   
    $d = new Diplome();
    $d->insert($data);
    
    // demande d'exécution du lot des opérations    
    $db->commit();    
    $this->render();
 } catch (Exception $e) {
    // annule toutes les opérations du lot
    $db->rollBack();
    $this->_redirect('/');
 }

18.4.6. Quelques liens utiles

19. Introduction à la navigation dans le modèle

Nous nous intéressons ici aux relations entre les entités du modèle, entre les objets de type Table_Row.

La phase préliminaire consiste à déclarer les relations dans les définitions des Classes de niveau Table.

19.1. Déclaration des relations dans le modèle

En référence à notre schéma relationnel ('voir la définition des tables), nous intervenons dans la définition des classes de type 'Table'

 
 <?php

class Diplome extends Zend_Db_Table_Abstract {
    
  protected $_name = 'diplome';
  protected $_primary = 'id';
  protected $_rowClass = 'DiplomeRow';
      
  protected $_referenceMap    = array(
        'Responsable' => array(
            'columns'           => 'idResponsable',
            'refTableClass'     => 'Enseignant',
            'refColumns'        => 'id'
        ));
}
?>
 
 <?php
class Inscription extends Zend_Db_Table_Abstract {

  protected $_name = 'inscription';
  protected $_rowClass = 'InscriptionRow';

  protected $_referenceMap    = array(
        'Diplome' => array(
            'columns'           => array('idDiplome'),
            'refTableClass'     => 'Diplome',
            'refColumns'        => array('id')
  ),
        'Etudiant' => array(
            'columns'           => array('idEtudiant'),
            'refTableClass'     => 'Etudiant',
            'refColumns'        => array('id')
  )
  );
}
?> 
 

$_referenceMap est une propriété héritée, qui permet de définir le rôle de chacune des clés étrangères de la table concernée. Chaque rôle est identifié par un :

  • Un nom : par exemple 'Diplome'

  • La ou les colonnes de la clé (éventuellement composée) : par exemple array('idDiplome')

  • La classe correspondant à la table référencée : par exemple 'Diplome"

  • La ou les colonnes correspondant dans la table référencée : par exemple array('id')

19.2. Relation Un-à-Un (One To One)

Une cas typique consiste à exploiter le tuple associé à une clé étrangère. Par exemple, dans le tableau qui liste les diplômes, nous souhaiterions voir apparaître, non pas l'identifiant du responsable, mais son nom.

Actuellement le code de la vue est :

 
<?php
 foreach ($this->diplomes as $diplome) {
   echo '<tr><td>' . $diplome->id . '</td>';
   echo ' <td>' . $diplome->libelle .'</td>';
   echo ' <td>' . $diplome->idResponsable .'</td>';
   echo '<td> &nbsp; </td>';
   echo " <td> &nbsp; </td></tr>\n";
 }

Fidèle à nos principes (architecture MVC, patterns GRASP), ce n'est pas de la responsabilité de la vue de contenir le code d'extraction du nom du responsable dans le modèle : C'est le diplome qui connaît son responsable, nous nous adresserons donc à ce dernier.

<?php
 foreach ($this->diplomes as $diplome) {
   echo '<tr><td>' . $diplome->id . '</td>';
   echo ' <td>' . $diplome->libelle .'</td>';
   echo ' <td>' . $diplome->getResponsable()->nom .'</td>';
   echo '<td> &nbsp; </td>';
   echo " <td> &nbsp; </td></tr>\n";
   echo " <td> <a href='voiretudiants/diplome/$diplome->id'>voir</a> </td></tr>\n";
 }

Nous intervenons donc au niveau de la classe des objets de type Row, afin de "déréférencer" une clé étrangère.

Pour cela nous utiliserons la méthode findParentRow héritée de la classe Zend_Db_Table_Row_Abstract :

Figure 18. findParentRow

findParentRow

  
  <?php
class DiplomeRow extends Zend_Db_Table_Row_Abstract {
  
  public function getResponsable() {
    return $this->findParentRow('Enseignant');
  }
  
  public function getNbInscrits(){
    // TODO
    return -1;
  }
}
?>    
  

La méthode findParentRow exploite les données de $_referenceMap pour sélectionner la ligne (Row) de la table enseignant liée à l'instance courante (relative à une ligne de la table diplome).

La méthode findParentRow attend deux arguments : Le premier est le nom d'une 'TableClass' et le deuxième (optionnel) est une clé de $_referenceMap, soit le nom du rôle de la relation.

Nous pouvons alors tester le résultat :

Figure 19. resultats/index.phtml

resultats/index.phtml

Nous venons de voir comment utiliser Zend Framework pour réaliser une jointure retournant au plus une ligne.

Nous nous intéressons maintenant à la relation Un-à-Plusieurs (One To Many) .

19.3. Relation Un-à-Plusieurs (One to Many)

Je souhaite connaître l'ensensemble des étudiants inscrits à un diplome (à une formation diplomante).

Plusieurs alternatives s'offrent à nous :

  1. Concevoir, dans le contrôleur, une requête du genre :

     $db  = Zend_Registry::get('dbAdapter');
     $sql = "SELECT * FROM etudiant e, inscription i WHERE e.id = i.idEtudiant AND i.idDiplome = ?";
     $sql = $db->quoteInto($sql, $idDplome);
     $inscrits = $db->fetchAll($sql);
     ...   
      
  2. Reléguer cette responsabilité à Diplome

    $dt = new Diplome();
    $diplome =  $dt->find($idDplome)->current();
    $inscrits = $diplome->getInscriptions();     
      

Dans le respect du pattern Expert en Information, nous choisirons la deuxième solution.

Comme vous l'avez certainement constaté , le lien nommé 'voir' : <a href='voiretudiants/diplome/$diplome->id'>voir</a> fait référence à une méthode d'action du contrôleur courant, que nous devons concevoir. Mais intéressons-nous en premier à sa vue :

19.3.1. Vue de la liste des étudiants inscrits à un diplôme

Fichier : views/script/resultats/voiretudiants.php


<style type="text/css">
<!--
table,th,td {
 border-style: solid;
 border-width: 1px;
}

table,caption {
 margin-left: auto;
 margin-right: auto;
 width: auto;
}
-->
</style>
<table>
 <caption>Les étudiants inscrits au diplôme <?=$this->diplome ?></caption>
 <thead>
  <tr>
   <th>Etudiant</th>
   <th>Stage <br>Nombre de <br>semaines réalisé</th>  
   <th>note</th>
   <th>reçu</th>
  </tr>
 </thead>
 <tbody>
 <?php
 foreach ($this->inscriptions as $inscription) {
   echo '<tr><td>' . $inscription->getNomEtudiant() . '</td>';
   echo ' <td>' . $inscription->nbSemainesStage .'</td>';
   echo '<td>'. $inscription->note .'&nbsp; </td>';
   echo '<td>'.$inscription->estRecu() ."&nbsp;</td></tr>\n";
 }
 ?>
 </tbody>
</table>

Cette vue part du principe qu'elle dispose, dans son contexte d'exécution, de deux attributs : $diplome et une liste d'étudiants nommée $inscriptions

Voyons comment ces informations lui sont transmises.

19.3.2. Méthode d'action du contrôleur liant modèle et vue

La vue s'appelant voiretudiants.php, notre méthode action sera voiretudiantsAction :

 
 //ResultatsController.php
  public function voiretudiantsAction()
    {
      $idDplome=$this->_getParam('diplome', 1);

      $dt = new Diplome();
      $diplome = $dt->find($idDplome)->current();
      $inscriptions=$diplome->getInscriptions();
      $this->view->inscriptions=$inscriptions;
      $this->view->diplome=$diplome->libelle;
      $this->render();
    }   

Les différentes étapes

  1. La méthode commence par obtenir la valeur du paramètre de la requête, avec une valeur par défaut (diplome=1)

  2. Un objet de type Diplome est crée.

  3. ... à partir duquel une requête est lancée, afin d'obtenir une instance de DiplomeRow.

  4. Nous obtenons, à partir d'un diplôme, l'ensemble des inscrits

  5. ... et le libellé du diplôme

  6. Le tout est transmis à la vue

  7. ... qui prend le relai (render)

19.3.3. Méthode de liaison entre modèle

Sur cette base nous pouvons implémenter la méthode getInscriptions de la classe DiplomeRow.

Pour cela nous utiliserons la méthode findDependanteRowset héritée de la classe Zend_Db_Table_Row_Abstract :

Figure 20. findDependanteRowset

findDependanteRowset

Soit :

  
<?php

class DiplomeRow extends Zend_Db_Table_Row_Abstract {

  public function getInscriptions(){
    return  $this->findDependentRowset('Inscription');
  }
  
  public function getResponsable() {
    return $this->findParentRow('Enseignant');
  }
  
  public function getNbInscrits(){
    // TODO
    return -1;
  }

}
?> 

La méthode findDependentRowset retourne un Rowset (et non pas un Row comme précédemment, qui est l'ensemble des objets InscriptionRow (relatifs aux lignes) lignes de la table inscription qui sont en relation avec this.

[Note]Le rôle des relations

La méthode findDependentRowsetpeut prendre un deuxième argument, dont la valeur est un des noms de rôle (clé de $_referenceMap de la classe en premier argument)

 
  return  $this->findDependentRowset('Inscription', 'Diplome');

Avec la logique One-To-One (à une ligne de la table inscription correspond un étudiant, un diplome), nous pouvons coder la classe InscriptionRow :

  
  <?php

class InscriptionRow extends Zend_Db_Table_Row_Abstract {
   
  //  private $idEtudiant;
  //  private $idDiplome;
  //  private $noteExamen;
  //  private $nbSemStage;

  public function estRecu() {
    // TODO
    return false;
  }

  public function getNomEtudiant() {
    return $this->findParentRow('Etudiant')->nom;
  }

  public function getLibDiplome() {
    return $this->findParentRow('Diplome')->libelle;
  }

}
?>  

19.3.4. Résultats

Index des diplômes, avec lien vers les étudiants inscrits

Figure 21. views/sccripts/resultats/index.phtml

views/sccripts/resultats/index.phtml

Figure 22. Etudiants inscrits à un diplôme

Etudiants inscrits à un diplôme

19.4. Relation Plusieurs-à-Plusieurs (Many-To-Many)

Cas d'une table relation non porteuse de propriétés

Une table relation, connue également sous l'appellation table intersection, est une table dont la seule raison d'être est de mettre en relation deux tuples (d'une même table ou de tables différentes). Lorsque ces tables ne portent pas d'autres informations (cas d'une association N-N non porteuse de propriété dans le schéma conceptuel), à l'utilisation, leurs clés étrangères sont très souvent systématiquement "déréférencées".

Zend Framework fournit une façon directe d'exploiter des association non porteuse.

Figure 23. Many To Many

Many To Many

Prenons un exemple, (bien que nous n'ayons pas de table relation on porteuse, mais qui peut le plus peu le moins), si nous avions voulu simplement prendre connaissance des identités des étudiants inscrits à un diplôme, sans en connaître les détails (note, durée effective des stages...). C'est à dire que nous souhaitons obtenir la listes des étudiants inscrits (objet EtudiantRow) et non la liste des inscrits (objet InscriptionRow) :


// DiplomeRow.php

  // obtenir tous les étudiants inscrits à ce diplome
  public function getEtudiants() {
    return $this->findManyToManyRowset('Etudiant', 'Inscription');
  }
  

La méthode findManyToManyRowset prend en argument le nom de (ou un objet de) la classe correspondant au Rowset à retourner (ici se sera des objets Etudiant) et en deuxième argument, ce que la documentation zend.db.table.relationships.html nomme table intersection (connu aussi sous l'appelation 'table de relation'). La méthode reçoit optionnellement d'autres arguments comme le rôle dans $_referenceMap (sinon c'est le premier rôle qui est élu).

19.5. Méthodes magiques

Si vous avez respecté les conventions de nommage utilisées dans ce support, pour pouvez alors utiliser les fonctions "magiques" de navigation dans le modèle.

Une méthode magique est une méthodes non déclarée par le développeur, mais respectant des conventions de nommage de sorte que Zend_Db_Table_Row_Abstract puisse en déduire les bons appels. Par exemple

// DiplomeRow.php
...
public function getInscriptions(){
    return  $this->findInscription();
}
...   

sera interprété : $this->findDependentRowset('Inscription')

Il en va de même avec EtudiantViaInscription

// DiplomeRow.php
...

public function getEtudiants() {
   return $this->findEtudiantViaInscription();
   //return $this->findManyToManyRowset('Etudiant', 'Inscription');
}
...    

Plus d'information avec le guide du développeur et l'API

20. Formulaire et Validateurs

Comme tout framework qui se repecte, ZF propose des composants dédiées aux formulaires des applications Web.

ZF propose l'API Zend_Form, associé à Zend_Validate, Zend_Filter pour la création et la gestion des formulaires (présentation, validation, retour au client).

Nous allons permettre la création d'un étudiant dans la base de données.

Nous mettons en place un simple index, présentant deux liens : l'un pointant vers le travail déjà réalisé (liste des formations diplômantes) et un autre vers la création d'un étudiant.

Figure 24. http://localhost/~kpu/wsPhp/zfmvc-tuto/

http://localhost/~kpu/wsPhp/zfmvc-tuto/

Le lien 'Ajouter un étudiant' pointe sur index/addetudiant. Nous créons donc une méthode d'action associée.

 
 function addetudiantAction()
  {       
        $this->view->title = "Ajouter un étudiant";
        $form = new EtudiantForm();
    
        // ...    
                    
        $this->view->form = $form;
  }
  
 

Un formulaire est un peu plus qu'une vue, il permet à l'utilisateur de transmettre de multiples informations issues de listes déroulantes, cases à cocher, champs d'édition de texte...

De plus, les données d'un formulaire doivent être filtrées (pour se prémunir d'actions dangereuses) et validées (pour assurer la cohérence des données), ceci entraînant des aller/retour entre le serveur et le client.

Ce type de comportement est pré-programmé par ZF, et pour en hériter, nous devons faire ... hériter nos formulaires de Zend_Form.

La classe EtudiantForm hérite de Zend_Form. Reamrque : Cette classe de base prend en compte bien d'autres aspects, voir ici Zend_Form .

Remarque : nous avons ajouté le champ 'email', absent de la table Etudiant, afin d'illustrer la fonction de validation.

 
 <?php
class EtudiantForm extends Zend_Form 
{ 
    public function __construct($options = null) 
    { 
        parent::__construct($options);
        $this->setName('creationetudiant');        
                
        $prenom = new Zend_Form_Element_Text('prenom');
        $prenom->setLabel('Prénom')
                  ->setRequired(true)
                  ->addValidator('NotEmpty');

        $nom = new Zend_Form_Element_Text('nom');
        $nom->setLabel('nom')
                 ->setRequired(true)
                 ->addValidator('NotEmpty');
             
        $email = new Zend_Form_Element_Text('email');
        $email->setLabel('Email address')
              ->addFilter('StringToLower')
              ->setRequired(true)
              ->addValidator('NotEmpty', true)
              ->addValidator('EmailAddress'); 
                      
        $submit = new Zend_Form_Element_Submit('submit');
        $submit->setLabel('Ajouter cet étudiant');
        
        $this->addElements(array($prenom, 
            $nom, $email, $submit));        
    } 
}
?>
 
 

Quelques explications :

  • Le constructeur se charge de la structure du formulaire, élément par élément.

  • Des contrôles HTML de type input='text' sont créés via Zend_Form_Element_Text

  • Les champs de saisis requis sont spécifiés (required)

  • A chacun des contrôles d'entrée sont associés filtres, validateurs et vue

A titre d'indication, on trouvera ici une liste des éléments standards définis par ZF. Exemples : Button, Checkbox, Hidde, Image, MultiCheckbox, Multiselect, Password, Radio, Reset, Select, Submit, Text, Textarea et Zend_Form_Element_Hash pour augmenter la sécurité.

20.1. Qu'est-ce qu'un filtre ?

Un filtre est une fonction de transformation d'une donnée d'entrée, soumise par un client, en une donnée conforme pour le système : simplification, ajout de code d'échappement...

Il existe de nombreux filtres prêts à l'emploi. Un filtre est un objet, instance d'une classe qui implémente Zend_Filter_Interface.

Par exemple :

    $prenom = new Zend_Form_Element_Text('prenom');
    $prenom->setLabel('Prénom');
    $prenom->setRequired(true);
            
    $filter = new Zend_Filter_StripTags();
    $prenom->addFilter($filter);                
 

Le filtre StripTags supprimera toute balise présente dans prénom (afin de se prémunir d'injection de code HTML).

Les filtres peuvent être chaînés :

    ...       
    $filter = new Zend_Filter_StripTags();
    $nom->addFilter($filter);          
    $nom->addFilter(new Zend_Filter_StringToUpper());
    ...      
 

Dans ce cas, les filtres sont appliqués par ordre d'insertion dans le composant. Ici, les balises seront supprimées avant la passage en majuscule.

Plus loin avec les filtres : Zend_Filter.

20.2. Qu'est-ce qu'un validateur ?

Un validateur est une fonction booléenne qui reçoit une donnée d'entrée, et retourne vrai si son argument est conforme aux règles de gestion prises en charge par le validateur, retourne faux sinon.

De plus, le validateur peut fournir des informations sur les règles non respectées, comme une chaîne vide, ou trop courte ou ne respectant pas une certaine syntaxe, etc.

Il existe de nombreux validateurs prêts à l'emploi. Un validateur est un objet, instance d'une classe qui implémente Zend_Validate_Interface.

Par exemple :

    $prenom = new Zend_Form_Element_Text('prenom');
    $prenom->setLabel('Prénom');
    $prenom->setRequired(true);
    $prenom->addValidator(new Zend_Validate_NotEmpty());                
 

Les validateurs peuvent être chainés :

    ...       
    $nom->addValidator(new Zend_Validate_StringLength(2));          
    $nom->addValidator(new Zend_Validate_Alnum());
    // ou $nom->addValidator('Alnum'); // le framework se chargeant de la correspondance
    ...      
 

Dans ce cas, les validations sont appliqués par ordre d'insertion dans le composant. Ici, le test de la longueur minimale est réalisé en premier, puis, quelqu'en soit le résultat le seconde test sera lui aussi évalué (caractères alphanumériques uniquement).

Exemple de validateurs standards : EmailAddress, Date, Barcode, Between, Float, Hex, Regex... Voir ici : Zend_Validate

20.3. Exemple de code d'exploitation du formulaire

 
 function addetudiantAction()
  {       
        $this->view->title = "Ajouter un étudiant";
        $form = new EtudiantForm();
        
        // reception de donnees ?       
        if ($this->_request->isPost()) {
            $formData = $this->_request->getPost();
//          Zend_Debug::dump($formData);

            // nous les affectons au formulaire 
            $form->populate($formData);
            
            // qui applique les filtres
            $formData=$form->getValues();
            
            //Zend_Debug::dump($formData);
            // activation des validateurs
            if ($form->isValid($formData)) {
                // ok, nous pouvons opérer              
                $et=new Etudiant();
                $etudiant=$et->createRow($formData);  
                if ($etudiant ) {
                  // sauvegarde dans la BD
                  $id=$etudiant->save();
                  // transmission des données à la vue 'voiretudiant.phtml'
                  $this->view->etudiant=$etudiant;                  
                  $this->render('voiretudiant');
                  return;           
                } 
            }
        }                
        // présentation du formulaire
        $this->view->form = $form;
        $this->render();
  }
  
 
 

Comme on peut le constater, en cas de non validation ou de requête non POST, le formulaire est retourné au client. Ci-dessous un exemple de retour avec informations transmises par les différents validateurs.

Figure 25. http://localhost/~kpu/wsPhp/zfmvc-tuto/index/addetudiant

http://localhost/~kpu/wsPhp/zfmvc-tuto/index/addetudiant

20.4. Vue résultante

Rien de nouveau pour cette vue associée voiretudiant.phtml :

 
<style type="text/css">
<!--
table,th,td {
 border-style: solid;
 border-width: 1px;
}

table,caption {
 margin-left: auto;
 margin-right: auto;
 width: auto;
}
-->
</style>
<table>
 <caption>Etudiant </caption>
 <thead>
  <tr>
   <th>Id</th>
   <th>Nom</th>  
   <th>Prénom</th>
  </tr>
 </thead>
 <tbody>
 <?php
   echo '<tr><td>' . $this->etudiant->id. '</td>';
   echo '<td>'. $this->etudiant->nom .'&nbsp; </td>';
   echo '<td>'.$this->etudiant->prenom ."&nbsp;</td></tr>\n";
 ?>
 </tbody>
</table>
 
 

Exemple d'un ajout réussi :

Figure 26. http://localhost/~kpu/wsPhp/zfmvc-tuto/index/addetudiant

http://localhost/~kpu/wsPhp/zfmvc-tuto/index/addetudiant

21. Quelques modules (guide, tutoriel) à explorer, dans la continuité de ce tutoriel.

21.1. Gestion des droits utilisateur à rapprocher avec la notion UML d'acteur/cas d'utilisation

Zend Framework est livré, entre autres, avec les modules permettant le gestion des authentifications (Zend_Auth) et des droits (Zend_Acl).

Vous trouverez ici un tutoriel Rob Allen sur la question, traduit en français.

21.2. Gestion du suivi de sessions

Le module Zend_Session propose une alternative à l'utilisation brute du tableau $_SESSION. En effet, Zend_Session intègre la notion d'"espace de noms" dans la zone de stockage des données de session, limitant ainsi des conflits de noms. Voir Guide du programmeur : Zend_Session

21.3. Structuration de blocs de présentation

Le module Zend_Layout permet d'encapsuler le contenu d'une vue dans une autre, bien que pouvant être utilisé sans MVC, le module est conçu pour s'intégrer avec ce dernier, utilise Zend_View. Voir Guide du programmeur : Zend_Layout

22. Conclusion

Espérons que ce document vous ait permis d'apprécier le fort potentiel d'un framework en application Web : productivité, robustesse, extensibilité, maintenabilité, sécurité, ... Même si ces qualités n'ont été qu'effleurées.

Le guide du développeur, de très bonne qualité et mis à jour régulièrement.

Site officiel : zend france


http://www.reseaucerta.org --- dernière mise à jour 20 juin 2008 ---