Les décorateurs Zend_Form

Un des éléments classiques dans un projet PHP et web en général est le formulaire. Le framework Zend les gère très bien et avec une grande simplicité, cela se complique lorsque l’on veut personnaliser un peu leur apparence. Ce tutoriel va tenter d’éclaircir un peu ce point en utilisant les Décorateurs Zend_Form_Decorator associés aux formulaires Zend_Form.

Lorsque l’on parle de personnaliser un formulaire, il ne s’agit pas de style (CSS), mais plus de la structure visuelle des éléments du formulaire. En effet, le formulaire type n’existe pas, les éléments le composant peuvent plus ou moins être standardisés, mais leur positionnement reste libre. On peut très bien avoir un champ texte avec son libellé le précédent, ou l’inverse. On peut également pour diverses raisons ne pas vouloir de libellé du tout. C’est ce genre de choses que Zend_Form_Decorator fait en utilisant le motif de conception décorateur (decorator) qui est – il faut le dire – un peu difficile à saisir lorsque l’on débute avec ZF.

Mise en place du tutoriel

Pour le tutoriel, nous allons créer et personnaliser un simple formulaire de login. On suppose qu’un nouveau projet ZF avec son arborescence standard vient d’être crée dans un répertoire tuto-zf-form-decorator, manuellement ou avec la commande zf create project tuto-zf-form-decorator, ce qui donne ceci :

|-- application
|   |-- Bootstrap.php
|   |-- configs
|   |   `-- application.ini
|   |-- controllers
|   |   |-- ErrorController.php
|   |   `-- IndexController.php
|   |-- models
|   `-- views
|       |-- helpers
|       `-- scripts
|           |-- error
|           |   `-- error.phtml
|           `-- index
|               `-- index.phtml
|-- docs
|   `-- README.txt
|-- library
|-- public
|   `-- index.php
`-- tests
    |-- application
    |   `-- controllers
    |       `-- IndexControllerTest.php
    |-- bootstrap.php
    |-- library
    `-- phpunit.xml

Afin de nous simplifier la tache, nous allons utiliser Zend_Application_Module_Autoloader afin de charger automatiquement nos classes. Ceci n’est pas expliqué en détail et sort du sujet de cet article, pour plus d’informations se référer à la documentation officielle.

On ajoute donc la fonction _initAppAutoload au Bootstrap (application/Bootstrap.php) qui doit ressembler à ceci :

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initAppAutoload()
    {
        $moduleLoad = new Zend_Application_Module_Autoloader(array(
		'namespace' => '',
		'basePath'	=> APPLICATION_PATH
	)); 
    }
}

Le formulaire

Le formulaire est tout ce qu’il y a de plus simple, un champ nom d’utilisateur, un champ pour le mot de passe et un bouton submit. Le formulaire sera crée dans la méthode init() de la classe Form_Login étendant Zend_Form, voici le squelette de cette classe que l’on place sous application/forms/Login.php :

class Form_Login extends Zend_Form
{
    public function init()
    {
	}
}

Maintenant il faut construire le formulaire. On commence par déclarer la méthode de soumission en POST, puis on ajoute les éléments, ce qui donne ceci :

class Form_Login extends Zend_Form
{
    public function init()
    {
	$this->setMethod('post');
		
	$username = new Zend_Form_Element_Text('username');
	$username->setLabel('Nom d\'utilisateur :')->setRequired(true);
		
	$password = new Zend_Form_Element_Password('password');
	$password->setLabel('Mot de passe :')->setRequired(true);
		
	$submit = new Zend_Form_Element_Submit('submit');
	$submit->setLabel('Je me connecte');
		
	$this->addElements(array($username, $password, $submit));
	}
}

Le contrôlleur

Nous avons notre classe Form_Login, il faut maintenant l’utiliser dans un contrôlleur pour voir apparaitre notre formulaire. Pour ce tutoriel très simple, nous placerons le formulaire dans la page d’index, en utilisant donc le contrôlleur de cette page se trouvant ici application/controllers/IndexController.php. Nous devons instancier notre classe dans la méthode indexAction() et placer cette instance dans une variable destinée à la vue. Voici le contrôlleur :

class IndexController extends Zend_Controller_Action
{
    public function init()
    {
    }

    public function indexAction()
    {
        $this->view->form = new Form_Login();
    }
}

Petite précision à propos de _initAppAutoload du Bootstrap vu en début de tutoriel. Ce petit bout de code sert à auto-charger notre classe suivant la syntaxe ZF. Comme notre classe se nomme Form_Login, ZF va automatiquement la chercher sous application/forms/Login.php. Sans ce morceau de code dans le Bootstrap, il aurait fallu inclure la classe à la mano.

La vue

Le contrôlleur a fait son travail, il faut maintenant afficher le formulaire, on ouvre donc le fichier de vue associé à IndexController.php qui se situe sous application/views/scripts/index/index.phtml et on demande à afficher la variable créee dans le contrôlleur :

echo $this->form;

Maintenant le formulaire s’affiche lorsque l’on visite la page d’index. Suivant votre installation de ZF, elle peut être à différents endroits, chez moi sur ma machine de développement c’est http://localhost/tuto-zf-form-decorator/public/.

Si l’on jète un oeil aux sources de la page, on voit que le formulaire est construit avec des listes de définition. Il s’agit en quelque sorte du décorateur par défaut.

<form enctype="application/x-www-form-urlencoded" method="post" action="">
<dl class="zend_form">
    <dt id="username-label">
	    <label for="username" class="required">Nom d'utilisateur :</label>
    </dt>
    <dd id="username-element">
    	<input type="text" name="username" id="username" value="">
    </dd>
    <dt id="password-label">
    	<label for="password" class="required">Mot de passe :</label>
    </dt>
    <dd id="password-element">
    	<input type="password" name="password" id="password" value="">
    </dd>
    <dt id="submit-label">&#160;</dt>
    <dd id="submit-element">
    	<input type="submit" name="submit" id="submit" value="Je me connecte">
    </dd>
</dl>
</form>

C’est très bien comme cela et peut convenir pour la plupart des projets. Mais comment changer cette structure? Que dois-je faire si je veux par exemple remplacer les dl, dt, dd par des div ou par un tableau (ce n’est pas beau, mais c’est pour l’exemple)? C’est ce que nous allons faire grace à Zend_Form_Decorator.

Allons décorer notre formulaire

Dans un premier temps nous allons supprimer les listes de définition afin d’obtenir un formulaire nu. Dans notre classe Form_Login, on ajoute ce qui suit en fin de méthode init().

class Form_Login extends Zend_Form
{
    public function init()
    {
		$this->setMethod('post');
			
		$username = new Zend_Form_Element_Text('username');
		$username->setLabel('Nom d\'utilisateur :')->setRequired(true);
			
		$password = new Zend_Form_Element_Password('password');
		$password->setLabel('Mot de passe :')->setRequired(true);
			
		$submit = new Zend_Form_Element_Submit('submit');
		$submit->setLabel('Je me connecte');
			
		$this->addElements(array($username, $password, $submit));
		
		$this->setElementDecorators(array(
				'ViewHelper',
				'Label'
		));
	}
}

On vérifie le code la page :

<form enctype="application/x-www-form-urlencoded" method="post" action="">
    <dl class="zend_form">
    <label for="username" class="required">Nom d'utilisateur :</label>
    <input type="text" name="username" id="username" value="">
    
    <label for="password" class="required">Mot de passe :</label>
    <input type="password" name="password" id="password" value="">
    
    <label for="submit" class="optional">Je me connecte</label>
    <input type="submit" name="submit" id="submit" value="Je me connecte">
    </dl>
</form>

On voit que les éléments du formulaire ne sont plus contenus dans une liste. Seul le dl du formulaire lui-même est encore présent, pour le supprimer il faut ajouter ces lignes :

class Form_Login extends Zend_Form
{
    public function init()
    {
		$this->setMethod('post');
			
		$username = new Zend_Form_Element_Text('username');
		$username->setLabel('Nom d\'utilisateur :')->setRequired(true);
			
		$password = new Zend_Form_Element_Password('password');
		$password->setLabel('Mot de passe :')->setRequired(true);
			
		$submit = new Zend_Form_Element_Submit('submit');
		$submit->setLabel('Je me connecte');
			
		$this->addElements(array($username, $password, $submit));
		
		$this->setElementDecorators(array(
				'ViewHelper',
				'Label'
		));
		
		$this->setDecorators(array(
				'FormElements',
				'Form'
		));
	}
}

On vérifie :

<form enctype="application/x-www-form-urlencoded" method="post" action="">
<label for="username" class="required">Nom d'utilisateur :</label>
<input type="text" name="username" id="username" value="">

<label for="password" class="required">Mot de passe :</label>
<input type="password" name="password" id="password" value="">

<label for="submit" class="optional">Je me connecte</label>
<input type="submit" name="submit" id="submit" value="Je me connecte"></form>

Le formulaire est tout propre. Alors comment ça marche?

Pour comprendre ce mécanisme, il faut savoir que le décorateur pour Zend_Form est composé d’un tableau contenant FormElements (les éléments du formulaire), HtmlTag (une liste de définition par défaut), et Form (la balise form), qui correspond au code suivant :

$form->setDecorators(array(
    'FormElements',
    'HtmlTag',
    'Form'
));

On voit bien que dans notre code pour supprimer la balise de liste du formulaire et avoir un code nu, on a juste supprimé la deuxième valeur du tableau (HtmlTag) pour ne garder que FormElements et Form. De la même manière pour avoir des éléments de formulaire (input, select, etc) nus, on ne va renseigner que les parties du décorateur pour les éléments que l’on veut :

$this->setElementDecorators(array (
	'ViewHelper',
    'Errors',
    'HtmlTag',
    'Label'
)); 

On peut déjà s’aperçevoir du fonctionnement en pelures d’oignon de l’intérieur vers l’extérieur des décorateurs, en effet le tableau passé à setElementDecorators (et à setDecorators) va décorer couche par couche nos éléments. On commence avec ViewHelper qui dessine l’élément, puis on a la gestion des erreurs (Errors), ensuite autour de tout cela on ajoute un élément HTML avec HtmlTag, puis on y ajoute le label avec Label.
En plus de ce système en pelures d’oignon, il y a un système de placement des éléments, par exemple le libellé qui arrive en dernier dans le tableau passé à setElementDecorators peut se placer avant (PREPEND) ou après (APPEND) ce qui a déjà été rendu, et celui-ci a un placement par défaut (pour le libellé c’est PREPEND).
Il est très important de comprendre ce mécanisme et c’est là que la confusion peut venir ^.^ On voit dans un exemple le PREPEND/APPEND par la suite.

Maintenant nous pouvons essayer d’englober le formulaire dans un bloc ayant la classe css zend_form, et remplacer les listes par des blocs. On revient dans notre classe Form_Login et on ajoute ce qui suit en fin de méthode.

class Form_Login extends Zend_Form
{
    public function init()
    {
		$this->setMethod('post');
			
		$username = new Zend_Form_Element_Text('username');
		$username->setLabel('Nom d\'utilisateur :')->setRequired(true);
			
		$password = new Zend_Form_Element_Password('password');
		$password->setLabel('Mot de passe :')->setRequired(true);
			
		$submit = new Zend_Form_Element_Submit('submit');
		$submit->setLabel('Je me connecte');
			
		$this->addElements(array($username, $password, $submit));
		
		$this->setDecorators(array(
				'FormElements',
				array('HtmlTag', array('tag' => 'div', 'class' => 'zend_form')),
				'Form'
		));
        
        $this->setElementDecorators(array(
            'ViewHelper',
            array(array('formElement' => 'HtmlTag'), array('tag' => 'div', 'class' => 'formElement')),
            'Errors',            
            array('Label', array('tag' => 'div', 'class' => 'formElementLabel', 'placement' => 'PREPEND'))
		));
	}
}

On vérifie le code la page pour découvrir que les dl sont remplacés par des div, de même que le formulaire.

<form enctype="application/x-www-form-urlencoded" method="post" action="">
<div class="zend_form">
    <div id="username-label">
    	<label for="username" class="formElementLabel required">Nom d'utilisateur :</label>
    </div>
    <div class="formElement">
    	<input type="text" name="username" id="username" value="">
	</div>
    <div id="password-label">
    	<label for="password" class="formElementLabel required">Mot de passe :</label>
	</div>
    <div class="formElement">
    	<input type="password" name="password" id="password" value="">
    </div>
    <div id="submit-label">
    	<label for="submit" class="formElementLabel optional">Je me connecte</label>
	</div>
    <div class="formElement">
    	<input type="submit" name="submit" id="submit" value="Je me connecte">
    </div>
</div>
</form>

On retrouve un formulaire un peu plus habillé. Plus de listes de définition mais des balises div. On remarque que pour la re-décoration du libellé, on a ajouté 'placement' => 'PREPEND', c’est inutile car c’est le positionnement par défaut, c’était juste pour l’exemple. Essayons de placer le libellé après le champ avec APPEND et vérifions le code HTML.

<form enctype="application/x-www-form-urlencoded" method="post" action="">
<div class="zend_form">
    <div class="formElement">
    	<input type="text" name="username" id="username" value="">
    </div>
    <div id="username-label">
    	<label for="username" class="formElementLabel required">Nom d'utilisateur :</label>
    </div>
    <div class="formElement">
    	<input type="password" name="password" id="password" value="">
    </div>
    <div id="password-label">
    	<label for="password" class="formElementLabel required">Mot de passe :</label>
    </div>
    <div class="formElement">
    	<input type="submit" name="submit" id="submit" value="Je me connecte">
    </div>
    <div id="submit-label">
    	<label for="submit" class="formElementLabel optional">Je me connecte</label>
	</div>
</div>
</form>

Il reste un soucis, le bouton submit possède lui aussi un libellé, ce n’est pas très élégant. On va le supprimer avec un décorateur ciblant son élément uniquement.

$submit->setDecorators(array(
	'ViewHelper',
	array(array('formElement' => 'HtmlTag'), array('tag' => 'div', 'class' => 'formElementSubmit'))
));

Dans l’ordre, j’affiche l’élément brut (ViewHelper), puis j’ajoute (autour) une balise div avec la class CSS formElementSubmit. Et c’est tout.

<form enctype="application/x-www-form-urlencoded" method="post" action="">
<div class="zend_form">
    <div id="username-label">
    	<label for="username" class="formElementLabel required">Nom d'utilisateur :</label>
	</div>
    <div class="formElement">
    	<input type="text" name="username" id="username" value="">
    </div>
    <div id="password-label">
    	<label for="password" class="formElementLabel required">Mot de passe :</label>
    </div>
    <div class="formElement">
    	<input type="password" name="password" id="password" value="">
    </div>
    <div class="formElementSubmit">
    	<input type="submit" name="submit" id="submit" value="Je me connecte">
	</div>
</div>
</form>

Ajouter un élément

Prenons l’hypothèse que notre formulaire a besoin d’un bloc description, comment l’ajouter? Il faut déjà savoir où le faire apparaitre et ensuite on ajoute simplement un élément HTML avec HtmlTag.

$this->setElementDecorators(array(
	'ViewHelper',
	array(
    	array('description' => 'HtmlTag'), 
        array(
        	'tag' => 'div',
            'class' => 'desc',
            'placement' => 'APPEND'
        )
    ),
	array(
    	array('formElement' => 'HtmlTag'),
        array(
        	'tag' => 'div',
            'class' => 'formElement'
        )
    ),
	'Errors',            
	array(
    	'Label',
        array(
        	'tag' => 'div',
            'class' => 'formElementLabel'
        )
    )
));

Ce qui donne :

<form enctype="application/x-www-form-urlencoded" method="post" action="">
<div class="zend_form">
    <div id="username-label">
    	<label for="username" class="formElementLabel required">Nom d'utilisateur :</label>
    </div>
    <div class="formElement">
    	<input type="text" name="username" id="username" value="">
    	<div class="desc"></div>
    </div>
    <div id="password-label">
    	<label for="password" class="formElementLabel required">Mot de passe :</label>
    </div>
    <div class="formElement">
    	<input type="password" name="password" id="password" value="">
        <div class="desc"></div>
    </div>
    <div class="formElementSubmit">
    	<input type="submit" name="submit" id="submit" value="Je me connecte">
    </div>
</div>
</form>

Formulaire dans un tableau

Juste pour pousser un peu plus le concept des décorateurs et le système de l’oignon, voici le code pour placer notre petit formulaire dans un tableau.

class Form_Login extends Zend_Form
{
    public function init()
    {
	$this->setMethod('post');
		
	$username = new Zend_Form_Element_Text('username');
	$username->setLabel('Nom d\'utilisateur :')->setRequired(true);
		
	$password = new Zend_Form_Element_Password('password');
	$password->setLabel('Mot de passe :')->setRequired(true);
		
	$submit = new Zend_Form_Element_Submit('submit');
	$submit->setLabel('Je me connecte');
		
	$this->addElements(array($username, $password, $submit));
	
	$this->setDecorators(array(
            'FormElements',
            array(
            	'HtmlTag',
                array(
                	'tag' => 'table',
                    'class' => 'zend_form'
                )
            ),
            'Form'
	));
        
    $this->setElementDecorators(array(
            'ViewHelper',
			array(
            	array('formElement' => 'HtmlTag'),
                array(
                	'tag' => 'td',
                    'class' => 'formElement'
                )
            ),
            'Errors',            
            array(
            	'Label',
                array(
                	'tag' => 'td',
                    'class' => 'formElementLabel'
                )
            ),
			array(
            	array('elementTr' => 'HtmlTag'),
                array('tag' => 'tr')
            )
	));
    
    $submit->setDecorators(array(
            'ViewHelper',
            array(
            	array('formElement' => 'HtmlTag'),
                array(
                	'tag' => 'td',
                    'class' => 'formElementSubmit'
                )
            ),
            array(
            	array('emptyLabel' => 'HtmlTag'),
                array(
                	'tag' => 'td',
                    'class' => 'emptyLabel',
                    'placement' => 'PREPEND'
                )
            ),
			array(
            	array('elementTr' => 'HtmlTag'),
                array('tag' => 'tr')
            )
	));
		
    }
}

Le code HTML de la page :

<form enctype="application/x-www-form-urlencoded" method="post" action="">
<table class="zend_form">
    <tr>
    	<td id="username-label">
        <label for="username" class="formElementLabel required">Nom d'utilisateur :</label>
        </td>
    	<td class="formElement">
    	<input type="text" name="username" id="username" value="">
        </td>
    </tr>
    <tr>
    	<td id="password-label">
        <label for="password" class="formElementLabel required">Mot de passe :</label>
        </td>
    	<td class="formElement">
    	<input type="password" name="password" id="password" value="">
        </td>
    </tr>
    <tr>
    	<td class="emptyLabel"></td>
        <td class="formElementSubmit">
    	<input type="submit" name="submit" id="submit" value="Je me connecte">
        </td>
	</tr>
</table>
</form>

Conclusion

Les décorateurs de Zend_Form ne sont pas si évidents que ça à comprendre, il faut essayer, batailler un peu pour bien saisir les concepts de ce patron de conception. Cela peut être rebutant et frustrant aux premiers abords, mais en expérimentant, en lisant un peu à droite à gauche et en mettant les mains dans le cambouis, ça rentre.

Sources

Retrouvez les sources de ce tutoriel en suivant ce lien : tuto-zf-form-decorator

Crédits

Les sites m’ayant aidés pour comprendre et progresser avec Zend_Form_Decorator : http://devzone.zend.com/1240/decorators-with-zend_form/, http://framework.zend.com/manual/1.12/en/learning.form.decorators.html

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *