Tutoriel premier en MVC avec Zend Framework

Olivier Capuozzo

16 novembre 2011

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 et Modèle avec données sont abordés en premier, puis la navigation dans le Modèle et Formulaire

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

  • Approfondir l'utilisation des objects techniques présentés.

  • 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, Zend_Cache, ...)

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 des principaux concepts objet - PHP5/Java
2. Une typologie des applications
3. Architecture Bazar
4. MVC
5. Zend Framework
6. Installation des composants
7. Structures des répertoires de l'application
8. Contrôle de connaissances
9. Architecture MVC : Modèle et contrat : Vues - Contrôleur
10. Création d'un projet
11. Le Modèle
11.1. Installation de world
11.2. Les tables
11.3. Paramètres de configuration à la base de données
11.4.
11.5. Utilisation de Zend_Db_Table, Zend_Db_Row
12. Mise en place d'un contrôleur
12.1. Méthode d'Action
12.2. Vue d'action
12.3. Résumé
13. Contrôleur et Vues
13.1. Variables passées à la vue
13.2. La vue exploite les données
13.3. Résumé
14. Passage d'arguments via l'url
14.1. Conception de la vue
14.2. Passage d'arguments
15. Exercices
16. Opérations CRUD
16.1. Sélectionner une ligne, une objet métier
16.2. Modifier un objet métier, une ligne
16.3. Création d'un objet métier, une ligne
16.4. Suppression d'un objet métier, une ligne
16.5. Transaction
16.6. Quelques liens utiles
17. Introduction à la navigation dans le modèle
17.1. Déclaration des relations dans le modèle
17.2. Relation Un-à-Un (One To One)
17.3. Relation Un-à-Plusieurs (One to Many)
17.3.1. Vue de la liste des villes d'un pays
17.3.2. Méthode d'action du contrôleur liant modèle et vue
17.3.3. Méthode de liaison entre modèle
17.4. Relation Plusieurs-à-Plusieurs (Many-To-Many)
17.5. Exercice
17.6. Méthodes magiques
18. Formulaire et Validateurs
18.1. Qu'est-ce qu'un filtre ?
18.2. Qu'est-ce qu'un validateur ?
18.3. Exemple de code d'exploitation du formulaire
19. Quelques modules (guide, tutoriel) à explorer, dans la continuité de ce tutoriel.
19.1. Gestion des droits utilisateur à rapprocher avec la notion UML d'acteur/cas d'utilisation
19.2. Gestion du suivi de sessions
19.3. Structuration de blocs de présentation
20. Conclusion

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

Figure 1. Concepts Objet avec PHP5

Concepts Objet avec PHP5

Un version PDF ici (une contribution de Boris Vacher) : tabPooPhpJava.pdf

2. 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.

3. 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 2. 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.

4. MVC

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

Figure 3. 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

5. 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. Voici quelques modules du Zend Framework

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


6. Installation des composants

7. 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 :

 
     NomDuProjet
     |-- application
     |   |-- configs
     |   |-- controllers
     |   |-- forms
     |   |-- layouts 
     |   |-- models 
     |   `-- views 
     |-- library 
     |   `-- Zend (racine - ou lien vers - des librairies de ZF) 
     `-- public
     |   |-- css
     `-- scripts 
         |-- (des scripts sql par exemple)
             
 

La zone publique contiendra des ressources directement accessibles. On veillera à interdire l'accès aux autres répertoires.

8. Contrôle de connaissances

POO

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

8.1.

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

Oui

8.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.

8.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

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

8.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.

8.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.

9. Architecture MVC : Modèle et contrat : Vues - Contrôleur

On entend par modèle la couche ayant en charge les données. Cette couche de service interagit avec le système de persistance, les contrôleurs et les vues.

Un contrôleur est le point d'entrée d'un cas d'utilisation.

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 (voir contrôleur).

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

Contrôleurs et Vues 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

Comme on le verra bientôt, les contrôleurs sont implémentés sous forme d'objet (classe spécialisée) et leurs actions sous forme de méthodes d'instance, dites méthode d'action.

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 4. Interaction type d'un système MVC avec les données

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

On trouvera d'autres représentations ici.

L'étape suivante vise à installer ces notions, indépendamment du contexte du quickstart, l'idée étant de nous « accrocher » à une base de données, avec une technique d'impléméntation du modèle plus rudimentaire que celle proposée par l'application quickstart (qui fait référence au modèle de conception Data Mapper de Martin Fowler ).

10. Création d'un projet

Certains détails de la procédure décrite ici concerne essentiellement ceux qui ont un compte sur un serveur unix ou GNU/Linux, en non root, avec le mode userdir d'apache activé avec les paramètres par défaut - bref ceux qui ont leur racine de publication nommée public_html dans leur home directory.

Nous proposons dans ce tutoriel une approche progressive des concepts et techniques mis en oeuvre par le framework, à destination des débutants.

en ligne de commande

# dans le dossier bin de ZF
 bin$ ./zf.sh create project zftuto
 
# on deplace le projet à la racine de publication de Zend Server 
  mv zftuto/ /chemin/vers/repoetoire/qui/va/bien
  
 

Voila, l'installation du squelette du projet est terminée. Vous remarquerez que le fichier index.php 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.

Ajouter un hôte virtuel à votre configuration apache selon les consignes du Guide du programmeur : http://framework.zend.com/manual/fr/

A titre d'exemple, pour des raisons de contraintes techniques locales, l'exemple ci-dessous tient compte de l'utilisation de Zend Serveur CE sur une machine virtuelle, avec distinction des projets par numéro de port.

 
/usr/local/zend/apache2/conf.d# cat mesvhosts.conf 

Lisyen IP:leBonPort
NameVirtualHost IP:leBonPort

[...]

<VirtualHost IP:leBonPort>
  DocumentRoot /chemin/vers/.../zftuto/public
  SetEnv APPLICATION_ENV "development"
  <Directory /chemin/vers/.../zftuto/public/>
              DirectoryIndex index.php
              AllowOverride All
              Order allow,deny
              Allow from all
  </Directory>
</VirtualHost>

 

Faire un test : http://IP:leBonPort

Parallèlement à la création du projet, nous installons la base de données « world » sur un serveur MySQL.

11. Le Modèle

« world » est une base de données statistiques géo-politiques sur le monde datant de 2006, d'après Official Statistics of Finland.

La base de données que nous installons est issues de MySql : world - légèrement modifiée pour l'occasion (vous trouverez ici les grandes lignes de la modifcation) .

11.1. Installation de world

Instructions rapides : télécharger le script world2.zip et le décompresser.

 mysql -u root < world2-schema.sql
 mysql -u root < world2-data.sql 
 

11.2. Les tables

Figure 5. tables

tables

Voici une description SQL de la structure de la table « Country » (on aurait pu en choisir une autre)

  
mysql> describe Country;
+----------------+--------------------------------------+------+-----+---------+----------------+
| Field          | Type                                 | Null | Key | Default | Extra          |
+----------------+--------------------------------------+------+-----+---------+----------------+
| id             | int(11)                              | NO   | PRI | NULL    | auto_increment | 
| Code           | char(3)                              | YES  |     | NULL    |                | 
| Name           | char(52)                             | NO   |     |         |                | 
| Continent      | enum('Asia','Europe',                |      |     |         |                | 
|                | 'North America', 'Africa','Oceania', |      |     |         |                | 
|                | 'Antarctica','South America')        | NO   |     | Asia    |                | 
| Region         | char(26)                             | NO   |     |         |                | 
| SurfaceArea    | float(10,2)                          | NO   |     | 0.00    |                | 
| IndepYear      | smallint(6)                          | YES  |     | NULL    |                | 
| Population     | int(11)                              | NO   |     | 0       |                | 
| LifeExpectancy | float(3,1)                           | YES  |     | NULL    |                | 
| GNP            | float(10,2)                          | YES  |     | NULL    |                | 
| GNPOld         | float(10,2)                          | YES  |     | NULL    |                | 
| LocalName      | char(45)                             | NO   |     |         |                | 
| GovernmentForm | char(45)                             | NO   |     |         |                | 
| HeadOfState    | char(60)                             | YES  |     | NULL    |                | 
| Capital        | int(11)                              | YES  |     | NULL    |                | 
| Code2          | char(2)                              | NO   |     |         |                | 
+----------------+--------------------------------------+------+-----+---------+----------------+

Par simple commodité ici, nous utiliserons l'utilisateur 'root' alors que nous devrions créer d'un utilisateur ou plusieurs utilisateurs spécifiques pour cette base (par exemple via l'entrée "User Administration" de MySQL Administrator.

Combien de lignes contient cette table ?

$ mysql -u root world2

mysql> select count(*) as `nombre de pays` from Country;
+----------------+
| nombre de pays |
+----------------+
|            239 | 
+----------------+

Il est temps maintenant de configurer la connexion à la base de données.

11.3. Paramètres de configuration à la base de données

Nous ajoutons quelques lignes à application/configs/application.ini (ZF utilise PDO pour l'accès aux données)

[production]
...
 
resources.db.adapter = PDO_MYSQL
resources.db.params.charset = UTF8
resources.db.params.host = 127.0.0.1
resources.db.params.username = ausername
resources.db.params.password = secret
resources.db.params.dbname = world2

 

Puis nous exploitons ces informations au démarrage (dans index.php)


-- dans index.php
// il n'y a rien à faire ! En effet le système exploite ces informations
// à  l'instanciation de la classe Zend_Application (avant dernière instruction
// de l'index.php) 

 

Modifier les chemins (utilisés pour les inclusions) dans index.php, afin de prendre en comptes les classes du modèles que vous allez déclarer.

 # ajout le chemin des modeles
 set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APPLICATION_PATH . '/../library'),
    realpath(APPLICATION_PATH . '/models'), 
    get_include_path(),
)));
 

Nous allons voir maintenant comment lier une classe à une table.

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).

Il est commode alors 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.

11.5. 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-classes Zend_Db_Table_Abstract et Zend_Db_Table_Row_Abstract

Concevons une classe modèle représentant une instance d'un pays : CountryRow (les raisons de ce nommage seront justifiées ultérieurement) :

Remarque : les classes du modèle seront placées dans le dossier « models »

     zftuto
     |-- application
     |   |-- controllers               
     |   |-- models      
     |   |    |--CountryRow.php
     |   `-- views 
     |-- library 
     |   `-- Zend (racine des librairies de ZF) 
     `-- public
         |-- styles 
         |-- images 
         `-- js 
    
 
 
 <?php

// CountryRow.php

class CountryRow extends Zend_Db_Table_Row_Abstract {

 // c'est juste pour montrer que l'on peut définir
 // ici ses propres méthodes      
  public function getNbCities(){
    // TODO
    return -1;
  }

}

  
 

Nous concevons en plus la classe Country que nous lions à CountryRow :

// Country.php
 
class Country extends Zend_Db_Table_Abstract {    
  protected $_name = 'Country';
  protected $_primary = 'id';
  protected $_rowClass = 'CountryRow'; 
}
 

L'attribut $_name a pour valeur le nom de la table en question, $_primary la colonne (ou les) déclarée(s) comme clé primaire et $_rowClass désigne la classe responsable de la représentation d'UNE ligne de la table.

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

[Note]Avertissement

Vous devez concevoir toutes les classes du modèle AVANT de poursuivre ce tutoriel.

A savoir : Country, CountryRow, Language, LanguageRow, City, CityRow, CountryLanguage

12. Mise en place d'un contrôleur

Nous allons concevoir la Consultation de l'ensemble des pays.

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

Ce contrôleur sera accessible par l'url : http://zftuto/country

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 Country) 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 :

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

Voici le contrôleur en question :

  
 <?php
class CountryController 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 CountryController :

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/country/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.

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.

 
     zftuto
     |-- application
     |   |-- controllers
     |   |    |--CountryController.php
     |   |               
     |   |-- models 
     |   `-- views 
     |   |    |--scripts
     |   |    |    |--country
     |   |    |    |    |--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/country/index.phtml :


<h1>Les pays</h1>

Sollicitons de nouveau notre contrôleur CountryController :

Figure 7. Interaction avec le contrôleur : http://zftuto/country

Interaction avec le contrôleur : http://zftuto/country

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

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

13. Contrôleur et Vues

Contrôleurs et Vues sont liés par contrat. Le contrôleur passe des données à la vue, cette dernière doit en connaître la nature, c'est à dire le « nom » et le « type associé ».

13.1. Variables passées à la vue

Le contrôleur passera un objet de type "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 coder le contrôleur :

require_once('Country.php');
class CountryController extends Zend_Controller_Action
{

  public function init()
  {   
        
  }

  public function indexAction()
  {
    // nous transmettons sous le nom 'countries' à la vue l'ensemble
    // des lignes de la table Country soit un objet Zend_Db_Table_Rowset 
    //    (colection d'objets CountryRow - des Zend_Db_Table_Row) 
    // Liaison automatique via la connexion par défaut 
   $ct = new Country();    

    // donnons à la vue un attribut 'countries' que nous valorisons    
    $this->view->countries=$ct->fetchAll();
    
    // passe la main à la vue         
    $this->render();
  }
[...]

13.2. La vue exploite les données

Le script de la vue associé à l'action est : views/country/index.phtml.

En effet, les attributs automatiques de notre modèle sont directement accessibles (non privés).

 
<table>
<?php foreach ($this->countries as $country) : ?>
   <tr>
      <td> <?php echo $this->escape($country->id) ?>   </td>
      <td> <?php echo $this->escape($country->Name) ?>   </td>
      <td> <?php echo $this->escape($country->Continent) ?>   </td>
      <td> <?php echo $this->escape($country->LocalName) ?>   </td>
  </tr>
<?php endforeach; ?>
</table> 

Ce script utilise la méthode « escape » pour échapper les caractères (Par défaut, la méthode escape() utilise la fonction PHP htmlspecialchar() pour l'échappement).

A noter la syntaxe à la « endif », voir doc syntaxe PHP qui favorise la lecture (les accolades ouvrantes et fermantes sont parfois difficiles à repérer dans la vue).

Figure 8. country/index.phtml

country/index.phtml

Ce script de vue exploite une collection d'objets de type Country.

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 countries, une collection d'objets de type Country.

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

13.3. 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).

14. Passage d'arguments via l'url

Nous souhaitons ne pouvoir consulter qu'un seul pays à la fois (et non une 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 Country.

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

14.1. Conception de la vue

Par convention, les noms capitalisés sont associés à des noms où les caractères majuscule sont remplacés par le caractère minuscule correspondant précédé d'un tiret (voir doc ici).

  :action: MixedCase and camelCasedWords are separated by dashes; 
  non-alphanumeric characters are translated to dashes, 
  and the entire string cast to lower case. 
  Examples: "fooBar" becomes "foo-bar"; "foo-barBaz" becomes "foo-bar-baz". 
  

Fichier voir-pays.phtml

    
<style type="text/css">
<!--
.pays {
 margin-left: 40px; 
}
-->
</style>
<div class='pays'>
<h2>Pays</h2>
Pays : <?= $this->pays->Name ?> <br>
Continent : <?= $this->pays->Continent ?> <br>
Nombre de villes inscrites : <?= $this->pays->getNbCities() ?> <br>
</div>  
  
  

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

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

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

  
     
  public function voirPaysAction()
  {
    if ($this->_hasParam('id'))
       $index = $this->_getParam('id');
    else
       $index = 1;
    $ct = new Country();  
    $pays = $ct->find($index)->current();
    $this->view->pays=$pays;
    $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 pays (index=1), sinon, elle utilise directement la valeur reçue index (très dangereux, à ne pas reproduire !)

Nous obtenons :

Figure 9. http://zftuto/country/voir-pays

http://zftuto/country/voir-pays

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);

14.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://zftuto/country/voir-pays/id/1
  

Équivalent à :

  http://zftuto/country/voir-pays?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 un autre pays (id=3)

Figure 10. http://zftuto/country/voir-pays/id/3

http://zftuto/country/voir-pays/id/3

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

Figure 11. http://zftuto/country/voir-pays/id/3

http://zftuto/country/voir-pays/id/3

15. 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 de consulter les caractéristiques d'UN pays.

    Prévoir une nouvelle action :

      http://zftuto/country/voir-details
      
  2. Permettre à un utilisateur, lorsqu'il consulte les caractéristiques d'UN pays, de naviguer vers le prochain ou le précédent.

    Indication : la vue dispose d'une méthode, nommée url, prenant en argument un tableau associatif spécifiant des composantes ZF-MVC de l'url. Exemple :

    // dans un script de vue .phtml
       ...  
       <a href='<?=$this->url(array('controller'=> 'index'
                  , 'action'=> 'lister'
                  , 'param1' => 'valParam1'
                  , 'param2' => 'valParam2'));?>'>un lien</a> 
       ...
       
       // produit la sortie : 
       // <a href='http://.../index/lister/param1/valParam1/param2/valParam2'>un lien<a>
      

    La difficulté consiste à trouver le moyen de sélectionner le prochain (et le précédent) pays correspondant au pays courant, car il peut y avoir des "trous" dans les valeurs d'id (en cas de suppression par exemple). Ci-dessous, quelques éléments qui pourrait bien vous donner des idées de solution :

      ...
       $ct = new Country();
       $this->index = $this->_getParam('id', 1);   
       $pays = $ct->find($this->index)->current();
          
       // on retrouve notre connexion (attention, c'est du PDO)
       $db = $ct->getAdapter();       
       //Recuperation de l'id max (le système utilise PDO)
       $max = $db->fetchOne('SELECT MAX(id) as id FROM Country');          
     

    Vous trouverez ici : le rôle de fetchOne .

  3. Reprendre l'exercice précédent mais ne pas proposer à l'utilisateur la possibilité de naviguer avant le premier et après le dernier pays.

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

16. 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') » : Zend_Db_Table_Row_Abstract.

Ces méthodes de 'niveau table' ou 'tuple' sont consultables ici Zend_Db_Table *

En voici quelques exemples :

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

On utilise la méthode find (de niveau table) :

Figure 12. API méthode find de Zend_Db_Table_Abstract

API méthode find de Zend_Db_Table_Abstract

Recherche le pays d'id = 1

// Fonction de niveau 'table' 
 $dt = new Country();  
 $pays = $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 sous la forme d'un objet Row (ou NULL s'il n'existe pas. Dans notre cas c'est le premier et unique CountryRow.

16.2. Modifier un objet métier, une ligne

Modification de l'attribut 'HeadOfState' du pays d'id = 73

// Fonction de niveau 'tuple' 
 $dt = new Country();  
 $rowPays = $dt->find(73)->current();
 // en 2006, $rowPays->HeadOfState == 'Jacques Chirac'
 $rowPays->HeadOfState = "Nicolas Sarkozy";
 $rowPays->save();
 

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

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

création d'une ville

public function testAddAction(){
 require_once 'City.php';
// Fonction de niveau 'table' et 'tuple'
  $ct = new City(); 
  // les données sont représentées par un dictionnaire
  $data = array(  
    'Name' => 'Test',
    'LocalName' => 'local test',
     // pays concerné, car il en faut un.
    'idCountry' => 2,
    'District' => 'regionTest',
    'Population' => 1  // pas grand monde  
  ); 
  $cityRow=$ct->createRow($data);  
//  Zend_Debug::dump($cityRow);
  if ($cityRow) {
    $id=$cityRow->save();    
    // Ok, c'est fait ! 
    
    // en extra, on zappe de méthode d'action, afin de visualiser le résultat 
    // passage en argument de l'id du pays concerné par ce test
    $this->_setParam('id',$id);
    // sous-traitance de service (function villesAction de ce
    // contrôleur, présentée un peu plus loin dans ce doc)
    $this->_forward('villes');    
    
    // MAIS, dans un cas normal, ce n'est pas une bonne idée du tout !
    // une instruction d'écriture devrait toujours être suivie
    // d'une redirection client. Voir le modèle de conception PRG ici
    // @see http://en.wikipedia.org/wiki/Post/Redirect/Get
    // return $this->_helper->redirector('index', 'country');       
  }    
}    
 

La méthode createRow crée un CityRow en mémoire.

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

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

suppression d'une (ou des) ville(s) portant le nom 'Test'

 // Opération de niveau 'table'
 
public function testDeleteAction(){
 require_once 'City.php';
 $ct = new City();
 $where = $ct->getAdapter()->quoteInto('Name = ?', 'Test');
 // suppression dans la table
 $ct->delete($where);
 
 // en extra, on zappe de méthode d'action, afin de visualiser le résultat 
 // passage en argument de l'id du pays concerné par ce test (voir testAddAction)   
 $this->_setParam('id','2');
 $this->_forward('villes');    
}     
 

La méthode delete supprime, de la table City, le ou les tuples filtrés par la clause 'where'.

16.5. Transaction

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

 $db = ... // getAdapter()
 $db->beginTransaction();
 try {
    // insert pays
    [...]    
    
    // insert pays avec une clé primaire erronée 
    $data = array(
     'id' => 1,  
     'Name' => 'France',
     ...
    );   
    $ct = new Country();
    $ct->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('/');
 }

16.6. Quelques liens utiles

17. 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.

17.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'

 
//City.php
<?php

//Country.php
<?php
class Country extends Zend_Db_Table_Abstract {    
  protected $_name = 'Country';
  protected $_primary = 'id';
  protected $_rowClass = 'CountryRow';

  protected $_referenceMap    = array(
        'Capitale' => array(
            'columns'   	 => 'Capital',
            'refTableClass'  => 'City',
            'refColumns'     => 'id'
        ));
}


//City.php
<?php
class City extends Zend_Db_Table_Abstract {    
  protected $_name = 'City';
  protected $_primary = 'id';
  protected $_rowClass = 'CityRow'; 
  
  protected $_referenceMap    = array(
        'MonPays' => array(
            'columns'           => 'idCountry',
            'refTableClass'     => 'Country',
            'refColumns'        => 'id'
        ));
} 

//CountryLanguage.php 
<?php
class CountryLanguage extends Zend_Db_Table_Abstract {    
  protected $_name = 'CountryLanguage';
  protected $_primary = array('idCountry', 'idLanguage');
  
  protected $_referenceMap    = array(
        'Langue' => array(
            'columns'           => array('idLanguage'),
            'refTableClass'     => 'Language',
            'refColumns'        => array('id')
  ),
        'Pays' => array(
            'columns'           => array('idCountry'),
            'refTableClass'     => 'Country',
            'refColumns'        => array('id')
  )
  );
  
}
// la notation 'tableau' montre comment opérer en cas de clés multi-attributs ('id', ou 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 'Pays'

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

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

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

17.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 pays, nous souhaiterions voir apparaître, non pas l'identifiant de la capitale, mais son nom.

Actuellement le code de la vue est :

 
<table>
<?php foreach ($this->countries as $country) : ?>
   <tr>
      <td> <?php echo $this->escape($country->id) ?>   </td>
      <td> <?php echo $this->escape($country->Name) ?>   </td>
      <td> <?php echo $this->escape($country->Continent) ?>   </td>
      <td> <?php echo $this->escape($country->LocalName) ?>   </td>
      <td> <?php echo $this->escape($country->Capital) ?>   </td>
  </tr>
<?php endforeach; ?>
</table> 

Figure 13. vue id capital

vue id capital

Nous souhaitons afficher le nom de la capitale, à la place de son identifiant.

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 pays qui connaît sa capitale », nous nous adresserons donc à ce dernier.

<?php
 <?php foreach ($this->countries as $country) : ?>
   <tr>
      <td> <?php echo $this->escape($country->id) ?> </td>
      <td> <?php echo $this->escape($country->Name) ?>  </td>
      <td> <?php echo $this->escape($country->Continent) ?>  </td>
      <td> <?php echo $this->escape($country->LocalName) ?>  </td>
      <td> <?php echo $this->escape($country->getCapitale()->Name) ?>   &bnsp; </td>
      <td> <a href=''> les villes </a> </td>            
  </tr>
<?php endforeach; ?> 
 }

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 14. findParentRow

findParentRow

  
  <?php
class CountryRow extends Zend_Db_Table_Row_Abstract {

 public function getCapitale() {
   $city = $this->findParentRow('City', 'Capitale');
   // les arguments de findParentRow : 
   //  'City' -> c'est la table
   //  'Capitale' -> c'est le rôle défini dans $_referenceMap de County.php 
   return $city;
 }
  
  public function getNbCities(){
    // TODO
    return -1;
  }
}

?>    
  

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

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 15. vue nom capitale

vue nom capitale

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) .

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

Je souhaite connaître, pour UN pays donné, l'ensensemble de ses villes référencées dans la base de données.

Plusieurs alternatives s'offrent à nous :

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

     $idPays = ...
     $ct = new Country();
     $db  = $ct->getAdapter(); 
     $sql = "SELECT v.Name, v.District, v.Population FROM City v WHERE v.idCountry = ?";
     $sql = $db->quoteInto($sql, $idPays);
     $lesVilles = $db->fetchAll($sql);
     ...   
      
  2. Reléguer cette responsabilité à CountryRow

    $ct = new Country();
    $pays =  $ct->find($idPays)->current();
    $lesVilles = $pays->getVilles();
    ...     
      

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

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

17.3.1. Vue de la liste des villes d'un pays

Fichier : views/script/country/villes.phtml


<style>
<!--
table,th,td {
 border-style: solid;
 border-width: 1px;
}

table,caption {
 margin-left: auto;
 margin-right: auto;
 width: auto;
}
-->
</style>

<table>
 <caption>Les villes du pays <?php echo $this->nomPays; ?></caption>
 <thead>
  <tr>
   <th>Ville</th>
   <th>Région</th>  
   <th>Population</th>
  </tr>
 </thead>
 <tbody>
 <?php foreach ($this->lesVilles as $ville) : ?>
   <tr><td> <?php echo $ville->Name; ?> </td>
       <td> <?php echo $ville->District    ?> &nbsp;</td>
       <td> <?php echo $ville->Population  ?> &nbsp; </td>   
   </tr>
 <?php endforeach; ?>
 </tbody>
</table>

Cette vue part du principe qu'elle dispose, dans son contexte d'exécution, de deux attributs : $nomPays et une liste de villes $lesVilles

Voyons comment ces informations lui sont transmises.

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

La vue s'appelant villes.phtml, notre méthode action sera villesAction :

 
 //CountryController.php
  public function villesAction()
    {
      $idPays=$this->_getParam('id', 1);
      $ct = new Country();
      $paysRow = $ct->find($idPays)->current();
      $villes=$paysRow->getVilles();
      $this->view->lesVilles=$villes;
      $this->view->nomPays=$paysRow->Name;
      $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 (pays=1)

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

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

  4. Nous obtenons, à partir d'un pays, l'ensemble des villes

  5. ... et le nom du pays

  6. Le tout est transmis à la vue

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

17.3.3. Méthode de liaison entre modèle

Sur cette base nous pouvons implémenter la méthode getVilles de la classe CountryRow.

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

Cette méthode permet d'obtenir l'ensemble des tuples d'une table référencant le tuple courant.

Figure 16. findDependentRowset

findDependentRowset

Soit :

  
  
<?php  
class CountryRow extends Zend_Db_Table_Row_Abstract {

   public function getVilles() {
    return  $this->findDependentRowset('City');
  }  

    
  public function getVillesBis() {
    // "SELECT * FROM City WHERE idCountry = ?", array($this->id)
    $db  = $this->getTable()->getAdapter(); 
  $sql = "SELECT v.Name, v.District, v.Population FROM City v WHERE v.idCountry = ?";
  return $db->fetchAll($sql, array($this->id), PDO::FETCH_CLASS);	
  } 

  
  
  public function getCapitale() {
 	$city = $this->findParentRow('City', 'Capitale');
 	 // les arguments de findParentRow : 
 	 //  'City' -> c'est la table
 	 //  'Capitale' -> c'est le rôle défini dans $_referenceMap de County.php 
    return $city;
  }
  
  public function getCapitaleBis() {
  	$db  = $this->getTable()->getAdapter();
  	$res = $db->query("SELECT * FROM City WHERE id = ?", array($this->Capital));  	  	  	
  	require_once('CityRow.php');
  	$capitale = new CityRow();
  	$res->setFetchMode(PDO::FETCH_INTO, $capitale);
  	$capitale = $res->fetchObject();
    return $capitale;
  }
  
  public function getNbCities(){
    // 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 CityRow (relatifs aux lignes) lignes de la table City 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 - voir City.php)

 
  return  $this->findDependentRowset('City', 'MonPays');

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

Cas d'une table de liaison NON porteuse de propriétés

Une table liaison, connue également sous l'appellation table intersection, est une table dont la principale raison d'être est de mettre en relation deux tuples (d'une même table ou de tables différentes). La clé étrangère d'une des deux tables est systématiquement "déréférencées".

Zend Framework fournit une façon directe d'exploiter des tables relation.

Figure 17. Many To Many

Many To Many

Prenons un exemple, nous souhaitons connaître les pays où la langue française est parlée. C'est à dire que nous souhaitons obtenir la listes des pays (objet CountryRow) via CountryLanguageRow :


// LanguageRow.php

  // obtenir tous les pays parlant cette langue
  public function getPaysParlantCetteLangue() {
    return $this->findManyToManyRowset('Country', 'CountryLanguage');
  }

Nous utilisons ici la table lisaison « CountryLanguage » côté « Country ».

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 Country) et en deuxième argument, ce que la documentation zend.db.table.relationships.html nomme table intersection (connu aussi sous l'appelation 'table de liaison'). La méthode reçoit optionnellement d'autres arguments comme le rôle dans $_referenceMap (sinon c'est le premier rôle qui est retenu).

Voici un exemple d'utilisation : L'utilisateur demande la liste des pays où la langue française est représentée :

Figure 18. http://zftuto/country/pays-langue/idlangue/23

http://zftuto/country/pays-langue/idlangue/23

Une action du controleur :

   

   public function paysLangueAction()     {
      $idLangue=$this->_getParam('idlangue', 1);      
      //Zend_Debug::dump($this->getRequest()->getParams());
      $lt = new Language();
      $languageRow = $lt->find($idLangue)->current();         
      $lesPays=$languageRow->getPaysParlantCetteLangue();
      $this->view->lesPays=$lesPays;
      $this->view->nomLangue=$languageRow->Name;
      $this->render();
   } 

Exemple d'une vue :

//pays-langue.phtml

<table>
 <caption>Pays ayant comme langue parlée: <?php echo $this->nomLangue ?></caption>
 <thead>
  <tr>
   <th>Pays</th>
  </tr>
 </thead>
 <tbody>
 <?php foreach ($this->lesPays as $pays) : ?>
   <tr>
   <td> <?php echo $pays->Name; ?> </td>          
   </tr>
 <?php endforeach; ?>
 </tbody>
</table>

17.5. Exercice

  1. Vous avez certainement remarqué que l'exploitation de la relation many-to-many vue précédemment ne permet pas d'explpoiter les données portées par la table intersection.

    Pour exploiter une table instersection porteuse de propriétés il suffit de considérer cette dernière dans une relation de type un à plusieurs (One To Many).

    Dans cette optique, il faudra donc créer la classe CountryLanguageRow.

    A faire : Concevoir le nécessaire permettant d'obtenir le détail suivant (l'utilisateur demande la liste des pays où la langue française est représentée) :

    Figure 19. http://zftuto/country/pays-langue/idlangue/23

    http://zftuto/country/pays-langue/idlangue/23

  2. L'utilisateur souhaite maintenant connaître toutes les langues référencées pour un pays donné :

    Figure 20. http://zftuto/country/langues/id/44

    http://zftuto/country/langues/id/44

    //langues.phtml
    
    table>
     <caption>Les langues parlées du pays <?php echo $this->nomPays ?></caption>
     <thead>
      <tr>
       <th>Langue</th>
       <th>Officielle</th>  
       <th>Pourcentage</th>
       <th>autres pays</th>
      </tr>
     </thead>
     <tbody>
     <?php foreach ($this->langues as $langue) : ?>
       <tr>
          <td> <?php echo $langue->Name; ?> </td>          
          <td> <?php echo $langue->IsOfficial; ?> </td>
          <td> <?php echo $langue->Percentage; ?> </td>      
          <td> <a href='<?php 
             echo $this->url(array('controller'=>'country',
                                   'action'=>'pays-langue', 
                                   'idlangue'=> $langue->idLanguage), null, true) ?>'>voir</a> </td>
       </tr>
     <?php endforeach; ?>
     </tbody>
    </table>
    

17.6. 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

// CountryRow.php
...

public function getVilles() {
  //return  $this->findDependentRowset('City', 'MonPays');
  return $this->findCity();
}

...   

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

Il en va de même avec les méthodes de type « findTableViaTableRelation » quie seront traduites avec findManyToManyRowset : Exemple

// CountryRow.php

public function getLanguesParlees() {
  //return $this->findManyToManyRowset('Language', 'CountryLanguage');
  return  $this->findLanguageViaCountryLanguage();		
}
...    

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

18. 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 modification d'une donnée associée à Country, à savoir l'attribut « HeadOfState ».

Nous mettons en place un lien de modification dans la vue en liste.

Figure 21. http://zftuto/country

http://zftuto/country

Le lien 'modifier' pointe sur country/edit. Nous créons donc une méthode d'action associée.

 
public function editAction() {       
    $this->view->title = "Modifier pays";
    $form = new PaysForm();
    
    // ...    
                    
    $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 pouvant entraîner 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 PaysForm hérite de Zend_Form. Remarque : Cette classe de base prend en compte bien d'autres aspects, voir ici Zend_Form .

Le formulaire PaysForm.php est à placer dans /application/forms :

 <?php
class PaysForm extends Zend_Form 
{ 
    public function __construct($options = null) 
    { 
        parent::__construct($options);
        $this->setName('modifierpays');        
                
        $headstate = new Zend_Form_Element_Text('HeadOfState');
        $headstate->setLabel('HeadOfState')
                  ->setRequired(true)
                  ->addValidator('NotEmpty');
                      
        $headstate->addValidator(new Zend_Validate_StringLength(3));
         
        $id = new Zend_Form_Element_Hidden('id');              
                      
        $submit = new Zend_Form_Element_Submit('submit');
        $submit->setLabel('Modifier');
        
        $this->addElements(array($headstate, $id, $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é.

18.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 :

    $headstate = new Zend_Form_Element_Text('HeadOfState');
    $headstate->setLabel('HeadOfState');
    $headstate->setRequired(true);
    $filter = new Zend_Filter_StripTags();
    $headstate->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.

18.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 :

    $headstate = new Zend_Form_Element_Text('HeadOfState');
    $headstate->setLabel('HeadOfState');
    $headstate->setRequired(true);
 
    $headstate->addValidator(new Zend_Validate_NotEmpty());                
 

Les validateurs peuvent être chainés :

    ...       
    $headstate->addValidator(new Zend_Validate_StringLength(2));          
    $headstate->addValidator(new Zend_Validate_Alnum());
    // ou $headstate->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).

18.3. Exemple de code d'exploitation du formulaire

 $form = new PaysForm(); // instanciation du formulaire
 if ($this->_request->isGet()) {
   $id = $this->_getParam('id', -1);
   if ($id == -1)
     // rien à faire ici
     return $this->_helper->redirector('index', 'country');
   $ct=new Country();
   $rowPays=$ct->find($id)->current();
   if ($rowPays) {
     // initialisation du formulaire
     // Zend_Debug::dump($rowPays);
     $form->getElement('HeadOfState')->setValue($rowPays->HeadOfState);
     $form->getElement('id')->setValue($rowPays->id);
   }                            
 } 
 // reception de donnees ?       
 else if ($this->_request->isPost()) {
   $formData = $this->_request->getPost();
   // nous les affectons au formulaire 
   $form->populate($formData);            
   // qui applique les filtres
   $formData=$form->getValues();   
   $ct=new Country();
   $rowPays=$ct->find($formData['id'])->current();                       
   // activation des validateurs
   if ($rowPays && $form->isValid($formData)) {
      // ok, nous pouvons opérer              
      $rowPays->HeadOfState = $formData['HeadOfState'];
      // sauvegarde dans la BD
      $id=$rowPays->save();
      // make a "redirect after post"
      // @see http://en.wikipedia.org/wiki/Post/Redirect/Get
      return $this->_helper->redirector('index', 'country');                                  
   }
 }                
 // présentation du formulaire
 if ($rowPays) {
     $this->view->action = "Modifier le nom du chef d'état";
     $this->view->pays = $rowPays->Name;        
     $this->view->form = $form;
     $this->render();
 }
 else {
    //sinon, rien à faire ici !
   return $this->_helper->redirector('index', 'country');
 }
}  

 

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 22. http://zftuto/country/edit/id/73

http://zftuto/country/edit/id/73

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

19.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.

19.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

19.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

20. 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.

Vous pouvez maintenant vous intéresser au quickstart en ligne, vous y découvrirez une utilisation plus avancée du modèle, et une construction plus structurée des vues car ces dernières (dans ce tuto) ce ne sont que des blocs de doc HTML.

Vous trouverez ici une implémentation simple de ce tuto, avec un début d'intégration d'un layout : tuto.zip ALL IN ONE (5.5Mo) - la base de données est à installer puis les valeurs database.params.xxx de application.ini. devront être renseignées en conséquence.

Le quick start de ZF, chez Zend..

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

Site officiel : zend france