Pages

Date 19 mai 2013

Jobeet ZF2 - Jour 8 - Les formulaires

De retour pour le jour 8.
Nous avons vu précédemment comment consulter / afficher les données présentes dans la base de données. Il est temps de voir à présent comment ajouter (ou modifier) des données dans cette base de données. Nous allons donc passer un moment ensemble avec les formulaires de Zend Framework 2

Les formulaires

Beaucoup de développeurs trouvaient que les formulaires du Zend Framework 1 étaient difficiles à prendre en main (notamment les décorateurs...).
Dans cette version 2 du Zend Framework, l'organisation et la structure de composant Formulaire ont complètement été revu par l'équipe de développement.

Introduction à Zend\Form

L'élément de base du formulaire est le \Zend\Form\Element. Cet objet sera composé d'un nom et d'attributs (le nom était un des attribut).

Un autre élément à connaitre, le Zend\Form\Fieldset (hérite de \Zend_Form\Element). Cet objet est un ensemble d'éléments \Zend\Form\Element et il pourra contenir d'autres éléments  Zend\Form\Fieldset).
Nous avons accès à plusieurs méthodes telles que:
  • add : pour ajouter un élément,
  • get : pour récupérer un élément,
  • has : savoir si le fieldset contient un élément donnée,
  • remove : retirer un élément.
La classe de formulaire hérite de la \Zend\Form\Fieldset (un formulaire est capable de contenir de nombreux éléments), et indirectement de \Zend\Form\Element: il sera possible de définir ses attributs (méthode de passage des paramètres, l'action, etc).

Nous allons maintenant voir comment mettre en place un formulaire très simple: le formulaire Category.

Le formulaire Category

Pour le moment, nous n'allons pas mettre en place d'administration, ni d'ACL: la page création/modification d'un formulaire sera accessible à tous le monde (pour le moment). Nous allons juste voir comment créer / afficher un formulaire (et enregistrer les informations dans la base de données évidemment).

Actuellement, nous avons les informations suivantes dans le modèle Category:
  1. un id
  2. un nom: obligatoire, 100 caractères maximum
  3. un "slug": qui sera "calculé".
Notre formulaire Category sera donc très simple (je vous l'avais annoncé): nous n'aurons que 3 éléments:
  1. l'id : qui sera un champs caché.
  2. le nom: un champs de type text
  3. un bouton submit, pour soumettre le formulaire et enregistrer les données dans la base de données.
Créez un nouveau répertoire Form (dans module/Front/src/Front): nous allons y mettre notre formulaire Category. Appelez-le CategoryForm.php:
// module/Front/src/Front/CategoryForm.php
<?php
    namespace Front\Form;
    use Zend\Form\Form;
    
    
    // Notre class CategoryForm étend l'élément \Zend\Form\Form; 
    class CategoryForm extends Form
    {
        public function __construct($name = null)
        {
            // On ne veut pas tenir compte du parametre $name,
            // On va le surcharger via le contructeur du parent
            parent::__construct('Category');
            
            // On définit la méthode d'envoie du formulaire en POST 
            $this->setAttribute('method', 'post');
            
            // Le champs caché id_category
            $this->add(array(
                'name' => 'id_category', // Nom du champ
                'type' => 'Hidden',      // Type du champ
            ));
            
            // Le champs name, de type Text
            $this->add(array(
                'name' => 'name',       // Nom du champ
                'type' => 'Text',       // Type du champ
                'attributes' => array(
                    'id'    => 'name'   // Id du champ
                ),
                'options' => array(
                    'label' => 'Nom',   // Label à l'élément
                ),
            ));
            
            // Le bouton Submit
            $this->add(array(
                'name' => 'submit',        // Nom du champ
                'type' => 'Submit',        // Type du champ
                'attributes' => array(     // On va définir quelques attributs
                    'value' => 'Ajouter',  // comme la valeur
                    'id' => 'submit',      // et l'id
                ),
            ));
        }
    }
Vous aurez remarqué que nous n'avons indiqué nul aucun champ obligatoire, aucun validateur. Nous verrons cela dans quelques instants.
Pour le moment, nous allons uniquement afficher le formulaire: les données se seront pas sauvegardées.

Nous allons maintenant modifier le contrôleur Category. Ajoutez le code suivant dans la méthode AddAction() du contrôleur:
// module/Front/src/Front/Controller/CategoryController.php
namespace Front\Controller;

use Zend\Paginator\Paginator;
use Zend\View\Model\ViewModel;
use Front\Form\CategoryForm;

class CategoryController extends JobeetController
{
    [...]
    public function addAction()
    {
        // On instancie notre formulaire Category
        $formCategory = new CategoryForm();
        
        // Et on le passe en paramètre à la vue (add.phtml)
        return new ViewModel(
            array(
                'form' => $formCategory
            )
        );
    }
}

Nous allons devoir modifier la vue add.phtml pour afficher les champs du formulaire. Jusqu'à maintenant, c'était très simple. Rassurez-vous: on va continuer dans la simplicité.
La refonte du composant \Zend\Form dans ZF2 a rendu l'affichage des formulaires beaucoup plus simple que dans la version 1. Le principal point de blocage des développeurs apprenant ZF1 était les décorateurs qui permettaient de "personnaliser" (avec quelques difficultés) le formulaire. Les décorateurs ont heureusement disparu, et l'affichage du formulaire peut se faire rapidement:

<?php
$form = $this->form;
$form->setAttribute('action', '');
$form->prepare();

echo $this->form()->openTag($form);                 // Ouverture de la balise form
echo $this->formHidden($form->get('id_category'));  // Champ caché id_category
echo $this->formRow($form->get('name'));            // Champ nom
echo $this->formSubmit($form->get('submit'));       // Bouton submit
echo $this->form()->closeTag();                     // Fermeture de la balise form

Zend Framework 2 fournit de nombreuses aides de vue pour afficher un formulaire plus simplement. L'aide de vue form() a deux méthodes openTag() et closeTag() qui sont utiliser pour ouvrir et fermer le formulaire. Ensuite, pour chaque élément avec un label (nous n'en avons qu'un ici...), nous utilisons l'aide de vue formRow().
Enfin, pour les éléments caché et submit, nous utilisons les aides de vue formHidden() et formSubmit().

Mais il existe encore plein d'aides de vue dédiées à l'affichage des éléments du formulaire: je vous invite à regarder directement dans le code de ZF2 pour découvrir ces aides de vue (dans vendor/zendframework/zendframework/library/Zend/Form/View/Helper).

Pour ma part, j'ai préféré ne pas utiliser l'aide de vue formRow(), mais d'autres aides de vue: c'est un peu plus fastidieux car on doit écrire un peu plus de code mais est, je pense, plus personnalisable. Je vous donne le code complet de ma vue add.phtml:
// module/Front/view/front/category/add.phtml
<?php
    $this->headLink()->appendStylesheet('/css/main.css');
    $title = 'Ajouter une catégorie';
    $this->headTitle($title);
?>

<h1><?php echo $this->escapeHtml($title); ?></h1>

<?php
    $form = $this->form;
    $form->setAttribute('action', $this->url('add_category'));
    $form->prepare();

    echo $this->form()->openTag($form);
    echo $this->formHidden($form->get('id_category'));
?>
    <table id="category_form">
        <tfoot>
            <tr>
                <td colspan="2">
                    <?php echo $this->formSubmit($form->get('submit'));?>
                </td>
              </tr>
        </tfoot>
        <tbody>
            <tr>
                <th><?php echo $this->formLabel($form->get('name'));?></th>
                <td>
                    <p>
                        <?php echo $this->formInput($form->get('name'));?>
                    </p>
                    <?php echo $this->formElementErrors($form->get('name'), array('class' => 'error_list'));?>
                </td>
            </tr>
        </tbody>
    </table>
<?php
    echo $this->form()->closeTag();
?>
J'ai remplacé l'utilisation de l'aide de vue formRow() par l'utilisation des aides de vue formLabel(), formInput() et formElementErrors(). Cela me permet de rendre le formulaire comme je le veux (ici, pour réutiliser le style de jobeet.org, le formulaire est affiché sous forme de tableau).
  • formLabel(): va afficher le label du champ
  • formInput(): affiche le champ lui-même
  • formElementErrors(): affichera les erreurs, après validation du formulaire
Pour voir le résultat, il faut maintenant ajouter une route pour accéder à notre action add:
return array(
    [...]
    'router' => array(
        'routes' => array(
            'add_category' => array(
                'type'    => 'Segment',
                'options' => array(
                    'route'    => '/add/category[/]',
                    'defaults' => array(
                        'module'     => 'Front',
                        'controller' => 'Front\Controller\Category',
                        'action'     => 'add',
                    ),
                ),
            ),
    [...]
);

Dans votre navigateur (/add/category), vous devriez obtenir ça:
Si vous cliquez maintenant sur le bouton "Ajouter", vous n'aurez aucune validation. Nous allons remédier à ça maintenant.

La validation du formulaire

Avec ZF2, la validation des formulaire (filtres + validateurs) se fait avec un filtre d'entrée (InputFilter, je ne suis pas sûr de la traduction française...) qui peut être soit autonome (une classe dédiée) soit contenue dans une classe qui implémente l'interface InputFilterAwareInterface (une classe du modèle par exemple). Cette interface InputFilterAwareInterface définie deux méthodes: setInputFilter() et getInputFilter(). C'est cette seconde méthode que je vais vous présenter.

Nous allons modifier le modèle Category, pour implémenter l'interface InputFilterAwareInterface.
Dans la méthode getInputFilter(), nous allons instancier un objet InputFilter et nous ajouterons une entrée pour chaque propriété que nous voulons filtrer ou valider. Pour la categorie, cela restera court, nous ne prenons en compte que le nom:
// module/Front/src/Front/Model/Category.php
namespace Front\Model;

use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilter;

class Category implements InputFilterAwareInterface
{
    [...]
    // La méthode setInputFilter ne sera pas utilisé ici...
    public function setInputFilter(\Zend\InputFilter\InputFilterInterface $inputFilter)
    {
        $this->inputFilter = $inputFilter;
    }
    
    // La méthode qui nous intéresse
    public function getInputFilter()
    {
        if (!$this->inputFilter) {
            $inputFilter = new InputFilter();

            $inputFilter->add(
                array(
                    'name'     => 'name',               // Le nom du champ / de la propriété
                    'required' => true,                 // Champ requis
                    'filters'  => array(                // Différents filtres:
                        array('name' => 'StripTags'),   // Pour retirer les tags HTML
                        array('name' => 'StringTrim'),  // Pour supprimer les espaces avant et apres le nom
                    ),
                    'validators' => array(              // Des validateurs
                        array(
                            'name'    => 'StringLength',// Pour vérifier la longueur du nom
                            'options' => array(
                                'encoding' => 'UTF-8',  // La chaine devra être en UTF-8
                                'min'      => 1,        // et une longueur entre 1 et 100
                                'max'      => 100,
                            ),
                        ),
                    ),
                )
            );
    
            $this->inputFilter = $inputFilter;
        }
    
        return $this->inputFilter;
    }
}
Comme je l'ai dit, c'est très court. Pour la catégorie, nous définissons des filtres et des validateurs que pour le nom de celle-ci. Pour les filtres:

  • StripTags: pour retirer les tags HTML
  • StripTrim: pour retirer les espaces blancs non désirés avant et après le nom saisi
Pour les validateurs, je ne vois l'utilité que du validateur StringLength pour notre propriété nom: on va s'assurer que la chaine saisie a une longueur comprise entre 1 et 100 caractères.

Ajoutons le code nécessaire à la soumission / validation du formulaire dans notre contrôleur:
namespace Front\Controller;

use Zend\Paginator\Paginator;
use Zend\View\Model\ViewModel;
use Front\Form\CategoryForm;
use Front\Model\Category;

class CategoryController extends JobeetController
{
    [...]
    public function addAction()
    {
        $formCategory = new CategoryForm();

        // On récupère l'objet Request
        $request = $this->getRequest();
        
        // On vérifie si le formulaire a été posté
        if ($request->isPost()) {
            // On instancie notre modèle Category
            $category= new Category();

            // Et on passe l'InputFilter de Category au formulaire
            $formCategory->setInputFilter($category->getInputFilter());
            $formCategory->setData($request->getPost());
        
            // Si le formulaire est valide
            if ($formCategory->isValid()) {
                // On prend les données du formulaire qui sont converti pour correspondre à notre modèle Category
                $category->exchangeArray($formCategory->getData());

                // On enregistre ces données dans la table Category
                $this->categoryTable->saveCategory($category);

                // Puis on redirige sur la page d'accueil.
                return $this->redirect()->toRoute('home');
            }

            // Si le formulaire n'est pas valide, on reste sur la page et les erreurs apparaissent
        }
        
        return new ViewModel(
            array(
                'form' => $formCategory
            )
        );
    }
}
Le code de la méthode addAction() est très classique, et vous serez sans doute amener à reproduire le même schéma:

  • Création du formulaire / Affichage du formulaire
  • Ajout des filtres / validateurs
  • Vérifier si on vient d'une requête POST
  • Vérifier que les données saisies dans le formulaire sont valides
  • Enregistrer les données dans la base
Voila un exemple d'erreur à la validation

Le formulaire Job

Voyons maintenant un formulaire un peu plus complexe: le formulaire Job.
Celui-ci devrait être beaucoup plus intéressant car il contient plus de champs, de différents types (selectbox, upload, text, textarea, checkbox. etc).

Commençons par ajouter une nouvelle route pour accéder à notre formulaire:
// module/Front/config/module.config.php
return array(
    'router' => array(
        'routes' => array(
            [...]
            'add_job' => array(
                'type'    => 'Segment',
                'options' => array(
                    'route'    => '/add/job[/]',
                    'defaults' => array(
                        'module'     => 'Front',
                        'controller' => 'Front\Controller\Job',
                        'action'     => 'add',
                    ),
                ),
            ),
            [...]
        )
    )
)
Créons le formulaire JobForm:
// module/Front/src/Front/Form/JobForm.php
<?php
    namespace Front\Form;
    use Zend\Form\Form;
    use Front\Model\CategoryTable;

    class JobForm extends Form
    {
        protected $categoryTable = null;
        
        public function __construct(CategoryTable $table)
        {
            parent::__construct('Job');
            $this->setAttribute('method', 'post');
            $this->setAttribute('enctype', 'multipart/form-data');
            
            $this->categoryTable = $table;
            
            $this->add(
                array(
                    'name' => 'id_job',
                    'type' => 'Hidden',
                )
            );
            
            $this->add(
                array(
                    'name' => 'csrf',
                    'type' => 'Csrf',
                    'options' => array(
                        'csrf_options' => array(
                            'timeout' => 600
                        )
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'id_category',
                    'type' => 'Select',
                    'attributes' => array(
                        'id'    => 'id_category'
                    ),
                    'options' => array(
                        'label' => 'Catégory',
                        'value_options' => $this->getCategoryOptions(),
                        'empty_option'  => '--- Sélectionnez une categorie---'
                    ),
                )
            );

            $this->add(
                array(
                    'name' => 'type',
                    'type' => 'Radio',
                    'attributes' => array(
                        'id'    => 'type'
                    ),
                    'options' => array(
                        'label' => 'Type',
                        'value_options' => array(
                            'Plein-temps' => 'Plein-temps',
                            'Mi-temps' => 'Mi-temps',
                            'Freelance' => 'Freelance'
                        )
                    )
                )
            );

            $this->add(
                array(
                    'name' => 'company',
                    'type' => 'Text',
                    'attributes' => array(
                        'id'    => 'company'
                    ),
                    'options' => array(
                        'label' => 'Company'
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'logo',
                    'type' => 'File',
                    'attributes' => array(
                        'id'    => 'logo'
                    ),
                    'options' => array(
                        'label' => 'Company logo'
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'url',
                    'type' => 'Url',
                    'attributes' => array(
                        'id'    => 'url'
                    ),
                    'options' => array(
                        'label' => 'Url'
                    )
                )
            );

            $this->add(
                array(
                    'name' => 'position',
                    'type' => 'Text',
                    'attributes' => array(
                        'id'    => 'position'
                    ),
                    'options' => array(
                        'label' => 'Position'
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'location',
                    'type' => 'Text',
                    'attributes' => array(
                        'id'    => 'location'
                    ),
                    'options' => array(
                        'label' => 'Location'
                    )
                )
            );

            $this->add(
                array(
                    'name' => 'description',
                    'type' => 'Textarea',
                    'attributes' => array(
                        'id'    => 'description',
                        'rows' => 4,
                        'cols' => 30,
                    ),
                    'options' => array(
                        'label' => 'Description'
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'how_to_play',
                    'type' => 'Textarea',
                    'attributes' => array(
                        'id'    => 'how_to_play',
                        'rows' => 4,
                        'cols' => 30,
                    ),
                    'options' => array(
                        'label' => 'How to apply'
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'is_activated',
                    'type' => 'Radio',
                    'attributes' => array(
                        'id'    => 'is_activated'
                    ),
                    'options' => array(
                        'label' => 'Activated ?',
                        'value_options' => array(
                            '1' => 'Oui',
                            '0' => 'Non',
                        )
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'is_public',
                    'type' => 'MultiCheckbox',
                    'attributes' => array(
                        'id'    => 'is_public'
                    ),
                    'options' => array(
                        'label' => 'Public ?',
                        'value_options' => array(
                            '1' => 'Whether the job can also be published on affiliate websites or not.'
                        )
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'email',
                    'type' => 'Text',
                    'attributes' => array(
                        'id'    => 'email'
                    ),
                    'options' => array(
                        'label' => 'Email'
                    )
                )
            );
            
            $this->add(
                array(
                    'name' => 'submit',
                    'type' => 'Submit',
                    'attributes' => array(
                        'value' => 'Save your job',
                        'id' => 'submit',
                    ),
                )
            );
        }
        
        public function getCategoryOptions()
        {
            $data  = $this->categoryTable->fetchAll()->toArray();
            $selectData = array();
            
            foreach ($data as $key => $selectOption) {
                $selectData[$selectOption["idCategory"]] = $selectOption["name"];
            }

            return $selectData;
        }
    }
Comme vous le voyez, le formulaire contient des éléments de différents types:

  • Select
  • Radio
  • Text
  • Textarea
  • Submit
Le champs Crsf va permettre de protéger le formulaire contre les attaques CSRF (Cross-Site Request Forgery): ce sera un champs de type Hidden, qui ajoutera un jeton de validité (dont on définit la durée de validité via l'option timeout). Je pense que ce champ CSRF devrait être présent sur tous les formulaires que nous serions amené à developper (aussi bien sur ce projet que sur vos futurs projets)

Un autre champ va être intéressant, le champ id_category de type Select (selecbox), car nous allons lui fournir les catégories de façon dynamique grâce à la méthode _getCategoryOptions().
Cela se passe dans le tableau options:
$this->add(
    array(
        'name' => 'id_category',
        'type' => 'Select',
        'attributes' => array(
            'id'    => 'id_category'
        ),
        'options' => array(
            'label' => 'Catégory',
            'value_options' => $this->_getCategoryOptions(),
            'empty_option'  => '--- Sélectionnez une categorie---'
        ),
    )
);
La clé 'empty_option' ajoute une option vide à notre selectbox (option value="").
La clé 'value_options' ajoute une ou plusieurs options grâce à un tableau (défini "en dur" ou dynamique, comme ici).

Le formulaire est prêt (sans validateurs et sans filtre). Ajoutons maintenant la vue add.phtml
//module/Front/view/front/job/add.phtml
<?php
    $this->headLink()->appendStylesheet('/css/main.css');
    $this->headLink()->appendStylesheet('/css/job.css');
    $title = 'New job';
    $this->headTitle($title);
?>

<h1><?php echo $this->escapeHtml($title); ?></h1>

<?php
    $form = $this->form;
    $form->setAttribute('action', $this->url('add_job'));
    $form->prepare();

    echo $this->form()->openTag($form);
    echo $this->formHidden($form->get('id_job'));
    echo $this->formHidden($form->get('csrf'));
?>
    <table id="category_form">
        <tfoot>
            <tr>
                <td colspan="2">
                    <?php echo $this->formSubmit($form->get('submit'));?>
                </td>
              </tr>
        </tfoot>
        <tbody>
            <tr>
                <th><?php echo $this->formLabel($form->get('id_category'));?></th>
                <td>
                    <?php echo $this->formSelect($form->get('id_category'));?>
                    <?php echo $this->formElementErrors($form->get('id_category'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('type'));?></th>
                <td>
                    <?php echo $this->formRadio($form->get('type'));?>
                    <?php echo $this->formElementErrors($form->get('type'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('company'));?></th>
                <td>
                    <?php echo $this->formInput($form->get('company'));?>
                    <?php echo $this->formElementErrors($form->get('company'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('logo'));?></th>
                <td>
                    <?php echo $this->formFile($form->get('logo'));?>
                    <?php echo $this->formElementErrors($form->get('logo'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('url'));?></th>
                <td>
                    <?php echo $this->formUrl($form->get('url'));?>
                    <?php echo $this->formElementErrors($form->get('url'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('position'));?></th>
                <td>
                    <?php echo $this->formInput($form->get('position'));?>
                    <?php echo $this->formElementErrors($form->get('position'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('location'));?></th>
                <td>
                    <?php echo $this->formInput($form->get('location'));?>
                    <?php echo $this->formElementErrors($form->get('location'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('description'));?></th>
                <td>
                    <?php echo $this->formTextarea($form->get('description'));?>
                    <?php echo $this->formElementErrors($form->get('description'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('how_to_play'));?></th>
                <td>
                    <?php echo $this->formTextarea($form->get('how_to_play'));?>
                    <?php echo $this->formElementErrors($form->get('how_to_play'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('is_public'));?></th>
                <td>
                    <?php echo $this->formMultiCheckbox($form->get('is_public'));?>
                    <?php echo $this->formElementErrors($form->get('is_public'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('is_activated'));?></th>
                <td>
                    <?php echo $this->formRadio($form->get('is_activated'));?>
                    <?php echo $this->formElementErrors($form->get('is_activated'), array('class' => 'error_list'));?>
                </td>
            </tr>
            <tr>
                <th><?php echo $this->formLabel($form->get('email'));?></th>
                <td>
                    <?php echo $this->formInput($form->get('email'));?>
                    <?php echo $this->formElementErrors($form->get('email'), array('class' => 'error_list'));?>
                </td>
            </tr>
        </tbody>
    </table>
<?php
    echo $this->form()->closeTag();
?>
Rien de bien nouveau dans cette vue: nous affichons les champs du formulaire, comme pour le form Category. Il y a simplement plus de champs.

Pour les validateurs et les filtres, nous allons implémenter l'interface InputFilterAwareInterface dans notre modèle Job (implémentation des méthodes setInputFilter() et getInputFilter()):
<?php
namespace Front\Model;

use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilter;

class Job implements InputFilterAwareInterface
{
    public $idJob;
    public $idCategory;
    public $type;
    public $company;
    public $logo;
    public $url;
    public $position;
    public $location;
    public $description;
    public $howToPlay;
    public $isPublic;
    public $isActivated;
    public $email;
    public $createdAt;
    public $updatedAt;
    protected $inputFilter;

    public function exchangeArray($data)
    {
        $this->idJob = (isset($data['id_job'])) ? $data['id_job'] : null;
        $this->idCategory = (isset($data['id_category'])) ? $data['id_category'] : null;
        $this->type = (isset($data['type'])) ? $data['type'] : null;
        $this->company = (isset($data['company'])) ? $data['company'] : null;
        $this->logo = (isset($data['logo'])) ? $data['logo'] : null;
        $this->url = (isset($data['url'])) ? $data['url'] : null;
        $this->position = (isset($data['position'])) ? $data['position'] : null;
        $this->location = (isset($data['location'])) ? $data['location'] : null;
        $this->description = (isset($data['description'])) ? $data['description'] : null;
        $this->howToPlay = (isset($data['how_to_play'])) ? $data['how_to_play'] : null;
        $this->isPublic = (isset($data['is_public'])) ? $data['is_public'] : 0;
        $this->isActivated = (isset($data['is_activated'])) ? $data['is_activated'] : 1;
        $this->email = (isset($data['email'])) ? $data['email'] : null;
        $this->createdAt = (isset($data['created_at'])) ? $data['created_at'] : null;
        $this->updatedAt = (isset($data['updated_at'])) ? $data['updated_at'] : null;
    }
    
    public function getArrayCopy()
    {
        return get_object_vars($this);
    }

    public function setInputFilter(\Zend\InputFilter\InputFilterInterface $inputFilter)
    {
        $this->inputFilter = $inputFilter;
    }

    public function getInputFilter()
    {
        if (!$this->inputFilter) {
            $inputFilter = new InputFilter();
        
            $inputFilter->add(
                array(
                    'name'     => 'id_category',
                    'required' => true,
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'type',
                    'required' => true,
                )
            );

            $inputFilter->add(
                array(
                    'name' => 'company',
                    'required' => true,
                    'filters'  => array(
                        array('name' => 'StripTags'),
                        array('name' => 'StringTrim'),
                    ),
                    'validators' => array(
                        array(
                            'name'    => 'StringLength',
                            'options' => array(
                                'encoding' => 'UTF-8',
                                'min'      => 1,
                                'max'      => 255,
                            )
                        )
                    )
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'logo',
                    'required' => true,
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'url',
                    'required' => false,
                    'filters'  => array(
                        array('name' => 'StripTags'),
                        array('name' => 'StringTrim'),
                    ),
                    'validators' => array(
                        array(
                            'name'    => 'StringLength',
                            'options' => array(
                                'encoding' => 'UTF-8',
                                'min' => 1,
                                'max' => 255,
                            )
                        ),
                    )
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'position',
                    'required' => true,
                    'filters'  => array(
                        array('name' => 'StripTags'),
                        array('name' => 'StringTrim'),
                    ),
                    'validators' => array(
                        array(
                            'name'    => 'StringLength',
                            'options' => array(
                                'encoding' => 'UTF-8',
                                'min' => 1,
                                'max' => 255,
                            )
                        )
                    )
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'location',
                    'required' => true,
                    'filters'  => array(
                        array('name' => 'StripTags'),
                        array('name' => 'StringTrim'),
                    ),
                    'validators' => array(
                        array(
                            'name'    => 'StringLength',
                            'options' => array(
                                'encoding' => 'UTF-8',
                                'min' => 1,
                                'max' => 255,
                            )
                        )
                    )
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'description',
                    'required' => true,
                    'filters'  => array(
                        array('name' => 'StripTags'),
                        array('name' => 'StringTrim'),
                    ),
                    'validators' => array(
                        array(
                            'name'    => 'StringLength',
                            'options' => array(
                                'encoding' => 'UTF-8',
                                'min' => 1,
                                'max' => 255,
                            )
                        )
                    )
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'how_to_play',
                    'required' => true,
                    'filters'  => array(
                        array('name' => 'StripTags'),
                        array('name' => 'StringTrim'),
                    ),
                    'validators' => array(
                        array(
                            'name'    => 'StringLength',
                            'options' => array(
                                'encoding' => 'UTF-8',
                                'min' => 1,
                                'max' => 255,
                            )
                        )
                    )
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'email',
                    'required' => true,
                    'filters'  => array(
                        array('name' => 'StripTags'),
                        array('name' => 'StringTrim'),
                    ),
                    'validators' => array(
                        array(
                            'name'    => 'StringLength',
                            'options' => array(
                                'encoding' => 'UTF-8',
                                'min' => 1,
                                'max' => 255,
                            )
                        ),
                        array(
                            'name' => 'EmailAddress'
                        )
                    )
                )
            );
            
            $inputFilter->add(
                array(
                    'name' => 'is_public',
                    'required' => false,
                )
            );

            $inputFilter->add(
                array(
                    'name' => 'is_activated',
                    'required' => true,
                )
            );

            $this->inputFilter = $inputFilter;
        }
        
        return $this->inputFilter;
    }
}
Un peu fastidieux et assez répétitif, mais rien de compliqué.

L'upload de fichier

La partie intéressante arrive maintenant!
Nous avons, dans notre formulaire Job, un champs de type File (pour uploader un logo sur le serveur).
Nous allons voir comment gérer ce formulaire dans notre contrôler, et gérer le fichier à uploader.
namespace Front\Controller;

use Zend\View\Model\ViewModel;
use Front\Form\JobForm;
use Front\Model\Job;
use Zend\Validator\File\Size;

class JobController extends JobeetController
{
    [...]
    public function addAction()
    {
        $formJob = new JobForm($this->categoryTable);
        $request = $this->getRequest();
        
        if ($request->isPost()) {
            $job = new Job();
            $formJob->setInputFilter($job->getInputFilter());
            
            $nonFiles = $this->getRequest()->getPost()->toArray();
            $files = $this->getRequest()->getFiles()->toArray();
            
            // Pour ZF 2.2.x uniquement
            $data = array_merge_recursive(
                $nonFiles,
                $files
            );
            
            $formJob->setData($data);
        
            if ($formJob->isValid()) {
                $size = new Size(array('max' => 716800));
                $adapter = new \Zend\File\Transfer\Adapter\Http();
                $adapter->setValidators(array($size), $files['logo']);

                if (!$adapter->isValid()){
                    $dataError = $adapter->getMessages();
                    $error = array();
                    foreach($dataError as $key => $row) {
                        $error[] = $row;
                    }
                    $formJob->setMessages(array('logo' => $error ));
                } else {
                    $adapter->setDestination('./public/resources');
                 
                    if ($adapter->receive($files['logo']['name'])) {
                        $job->exchangeArray($formJob->getData());
                        $job->logo = $files['logo']['name'];

                        $this->jobTable->saveJob($job);
                        return $this->redirect()->toRoute('home');
                    }
                }
            }
        }
        
        return new ViewModel(
            array(
                'form' => $formJob
            )
        );
    }
}
Sur le principe, nous faisons la même chose que pour le contrôleur Category:
  • Initialisation du formulaire
  • Ajout du InputFilter
  • Vérification si la requête a été posté
  • Vérification que les données saisies dans le formulaire sont valides
  • Passage du formulaire à la vue
La principale différence provient de la gestion du fichier uploadé.
Il faut récupérer les données postées en deux fois:
  • les données standards (zone de texte, radio, select, etc): $nonFiles = $this->getRequest()->getPost()->toArray();
  • le ou les fichiers uploadés: $files = $this->getRequest()->getFiles()->toArray();
Pour récupérer le fichier, il faut instancier un objet \Zend\File\Transfer\Adapter\Http().
Ensuite, nous pouvons lui passer un ou plusieurs validateurs (ici, nous ajoutons un validateur pour autoriser une taille maximum au fichier uploadé)
La validation sur le fichier se fait comme pour le formlulaire, avec la méthode isValid().

Enfin, la réception du fichier se faire avec la méthode receive(). Le fichier sera déplacé dans le répertoire ./public/resources (avec la méthode setDestination()).

Voilà, vous avez maintenant deux formulaires fonctionnels.


Pour le moment, ils ne vous permettent que d'insérer des catégories (ou des jobs), mais vous ne pourrez pas les éditer. Nous verrons comment le faire plus tard, quand nous aborderons les utilisateurs et l'interface d'administration.


Nous en avons terminé pour le moment. J'espère que vous aurez bien compris comment utiliser les formulaires avec ZF2 et que le tutoriel aura été assez clair sur ce point. Comme toujours, les sources sont disponibles sur mon compte Github

25 commentaires:
  1. Bravo et merci pout cet excellent tuto.

    RépondreSupprimer
  2. Bonjour,
    je tenais à vous féliciter et à vous dire merci, car je commence à étudier Zend framework 2 pour un projet et grace à vous je comprend mieux le fonctionnement.
    Encore merci de cet excellent travail et j'espère que vous aurez le temps de le finir le plus rapidement possible.
    Cordialement, bonne continuation

    RépondreSupprimer
  3. Le premier tuto de qualité que je trouve sur le ZF2 et les forms. Merci beaucoup.

    RépondreSupprimer
  4. Merci beaucoup pour cet excellent tuto , impatient pour le reste et j’espère que vous aurez assez de temps pour le finir rapidement .
    Mille merci

    RépondreSupprimer
    Réponses
    1. J'ai attaqué le prochain tuto, j'espère le mettre en ligne rapidement

      Supprimer
  5. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  6. une petite remarque il faut juste ajouter le Jobeet::slugify apres exchangeArray

    $category->exchangeArray($formCategory->getData());

    $category->slug = Jobeet::slugify($category->name);

    RépondreSupprimer
    Réponses
    1. J'ai corrigé les sources (je suis en train de tout basculer sur Github)
      Cet ajout n'est plus nécessaire => Le slug est géré dans le modèle Category

      Supprimer
  7. Bonjour

    j'ai cette erreur

    Rows as part of this DataSource, with type object cannot be cast to an array
    #0 /var/www/inmorocco.local/app/module/Front/src/Front/Form/JobForm.php(208): Zend\Db\ResultSet\AbstractResultSet->toArray()
    #1 /var/www/inmorocco.local/app/module/Front/src/Front/Form/JobForm.php(46): Front\Form\JobForm->_getCategoryOptions()
    #2 /var/www/inmorocco.local/app/module/Front/src/Front/Controller/JobController.php(43): Front\Form\JobForm->__construct(Object(Front\Model\CategoryTable))

    la solution c que j'ai changer le comportement de la fonction _getCategoryOptions voila le code:

    protected function _getCategoryOptions()
    {
    $data = $this->categoryTable->fetchAll();
    $selectData = array();

    foreach ($data as $selectOption) {
    $selectData[$selectOption->idCategory] = $selectOption->name;
    }

    return $selectData;
    }

    PS: j'utilise Zend 2.2.2

    Merci

    RépondreSupprimer
    Réponses
    1. Bonjour,

      J'ai eu la même erreur, par contre quand avec cette solution, il me semble que l'Array construit n'est pas du type '1' => 'Catétorie1','2' => 'Catétorie2', et donc à l'insertion dans la base, je me retrouve avec un id = 0 quelque soit la valeur que je sélectionne.
      Quelqu'un a-t'il une solution ?

      Supprimer
  8. Bravo pour ces excellents tuto. C'est vraiment clair et didactique.
    Ça change de la doc officielle !

    RépondreSupprimer
  9. J'ai du ajouter ceci dans JobForm.php car le champ is_activated était requis par ma table job

    $this->add(
    array(
    'name' => 'is_activated',
    'type' => 'Hidden',
    'attributes' => array(
    'value' => 1,
    ),
    )
    );

    Marc

    RépondreSupprimer
    Réponses
    1. Bonjour, merci pour ce remarque tuto.

      Pour le problème de la colonne is_activated obligatoire dans la base mais non présent dans le formulaire, Romain a proposé une autre solution en initialisant la valeur de is_activated à 1 dans la méthode exchangeArray de Job.php.
      Quelle est à votre avis la meilleure solution ?

      Supprimer
  10. Bonjour, super (série !) d'article . Vraiment intéressant, je me met au ZF2 après avoir il y'a quelques temps pas mal bossé avec le ZF1.

    Et du coup je me posais une petit question voir si tu as un avis là dessus etc..

    Pour fournir la liste des catégories à JobForm tu passes CategoryTable $table en paramètre du constructeur et crée une méthode qui va utiliser ton catégorie table pour récupérer la liste et passer ça à ton Select. Jusque là tout va bien.

    De mon côté je fais ça uniquement au niveau du Controller, je m'explique :

    //CandidatForm
    $this->add(array(
    'name' => 'autre_mandat',
    'type' => 'Select',
    'options' => array(
    'label' => 'Autre mandat',
    'empty_option' => '-- Choisir un mandat --',
    ),
    ));

    //CandidatController
    public function addAction() {
    $candidat = new Candidat();
    $form = new CandidatForm();

    // En l'occurence là mes données sont dans un array propre à mon Model je ne passe pas par un stockage DB.
    $form->get('autre_mandat')->setValueOptions($candidat->aMandats);
    [etc..]
    }

    Je pratique comme ça dans l'idée de toujours bien séparer les couches, où le controller fait toujours le pont entre Model/View et Form en l'occurance.

    Que penses tu de cette façon de faire ?
    C'est toujours bon d'avoir un autre avis si tu as un retour là dessus.

    RépondreSupprimer
    Réponses
    1. Pour moi, la façon dont tu procèdes est la bonne méthode :-)

      Pour les tutos, je me permet beaucoup de liberté, pour montrer comment fonctionne certains principes du framework => et pour gagner un peu de temps, je l'avoue. Je ne voulais pas faire des tutos pour en plus montrer les bons principes de la POO et la notion SOLID :-)

      Avec le recul, je me dit que j'aurais du :-)

      Supprimer
  11. D'accord :)
    Oui justement c'est bien aussi de voir les différentes façons possibles, c'est l'occasion aussi avec les commentaires.
    Super boulot de ta part ! ^*^

    RépondreSupprimer
  12. Bonjour,

    Tout d'abord, merci pour tes supers tutos, j'apprend plein de trucs depuis que j'ai trouvé ton blog :) (vivement la suite, j'ai vu qu'on n'en est qu'à la moitié du projet :) ) Je m'estime encore débutant avec zend... mais grâce à toi (et à Stoyan Cheresharof sur youtube) je progresse vite :)

    J'ai un soucis avec les formulaires sur l'input csrf.. or Il faut absolument que je sécurise mon projet.
    Si le token est bien généré et inclus dans l'input... par contre, il passe les filtres...
    => j'ai testé en le modifiant manuellement par firebug, et le formulaire est malgré tout validé...

    Il faudrait faire la comparaison avec le token enregistré en session manuellement dans le controller ?

    Merci de ton aide :)
    Bruno.

    RépondreSupprimer
    Réponses
    1. Bonjour Bruno

      Il me semble qu'il y a un validateur CSRF à ajouter sur le champs dans les InputFilter

      Supprimer
  13. Merci Romain pour ta réponse :)

    J'ai cherché ! mais j'ai pas trouvé comment se paramètre le validateur csrf dans les InputFilter...

    En attendant, j'ai contourné le pb, en faisant la validation dans le controller (c'est moins propre que de passer par les validateurs, mais au moins ça fonctionne...)

    Je le fais jsute après avoir identifié qu'on est en méthode post => si attaque CSRF, on dégage de suite, sans aller plus loin dans la validation du form... j'ai fait dans le bourrin !

    Voici le code :

    if($request->isPost()){
    //vérification attaque CSRF
    $data=$request->getPost()->toArray();
    if (isset($data['csrf'])){
    $token = $data['csrf'];
    $csrf= new CsrfValidator;
    if (!$csrf->isvalid($token)){
    echo 'CSRF ATTACK';die;
    }
    } else {
    // input csrf supprimé par utilisateur
    echo 'CSRF ATTACK';die;
    }
    [...]

    Je continue de chercher pour faire cette fichue validation par l'inputFilter... si je trouve je te dis comment ^^ (si tu trouves avant ou que tu as déjà cherché, je suis preneur hein :D)

    Cordialement,
    Bruno.

    RépondreSupprimer
    Réponses
    1. Si j'ai un peu de temps, j'essayerais de faire fonctionner le validateur ce soir.

      C'est bien celui-là : https://github.com/zendframework/zf2/blob/master/library/Zend/Validator/Csrf.php

      Supprimer
    2. ben c'était tout con en fait (sauf que personne ne l'explique simplement nulle part... c'est encore trop mal documenté zf2 :'( )

      Donc pour le validateur csrf par inputFilter faut juste le déclarer comme ça et ça marche !
      J'ai pas trouvé s'il y avait des options applicables au validateur... mais en même temps le timeout est déjà défini à la création du form... donc il ne reste pas des masses d'options pour un csrf :D

      Mais je m'égare ! Le code, le code !... ;) :

      $inputFilter->add(array(
      'name' => 'csrf',
      'required' => true,
      'validators' => array(
      array(
      'name' => 'Zend\Validator\Csrf',
      )
      ),
      ));


      Et tant qu'à faire on peut ajouter dans la vue le message d'erreur :

      echo $this->formElementErrors($form->get('csrf'), array('class' => 'error_list'));

      sinon, l'imput étant hidden, rien ne s'affiche...


      Valaaa, c'était ma petite pierre ;)

      ++
      Bruno.

      Supprimer
    3. Merci Bruno :-)

      Je n'ai pas eu le temps de regarder du coup. Je voulais faire ça demain ^^.
      En regardant dans le constructeur du validateur Csrf, tu pourras voir les options:
      - name
      - salt
      - session
      - timeout

      Supprimer
  14. oups, j'ai oublié de préciser le use... indispensable pour que le controller trouve la class ^^ (d'autant que j'ai fait un alias :D)

    Pour que ça fonctionne avec le code que je viens de poster, il faut donc ajouter :
    use Zend\Validator\Csrf as CsrfValidator;

    RépondreSupprimer
  15. j'ai créer un formulaire contenant Select j'ai réussi à afficher une liste à partir de ma base mais je n'ai pas rassir à insérer la valeur sélectionnée dans la base base. voila les parties des codes s'il vous plais m'aidez à résoudre se problème :
    public class RegisterForm{
    //
    protected $profiles;
    protected $modules;
    public function __construct(array $profiles, array $modules)
    {
    $this->profiles=$profiles;
    $this->modules=$modules;
    $this->add(array(
    'name' => 'type_utilisateur',
    'type' => 'Select',
    'attributes' => array(
    'id' => 'type_utilisateur'
    ),
    'options' => array(
    'label' => 'Type ',
    'value_options' => $this->getTypeUtlisateurOptions($options),//récupération des types d'utilisateur à partir de la base
    'empty_option' => '--- Sélectionnez un type---'
    ),
    )
    );
    //
    public function getTypeUtlisateurOptions(array $profiles)
    {
    $selectData = array();
    foreach ($this->profiles as $selectOption) {
    $selectData[$selectOption->id_type_utilisateur] = $selectOption->nom_type_utilisateur];
    }

    return $selectData;
    }
    //

    }

    user.php
    class User
    {
    //
    function exchangeArray($data)
    {
    $this->nom_utilisateur = (isset($data['nom_utilisateur'])) ?
    $data['nom_utilisateur'] : null;
    $this->visibilite_utilisateur = (isset($data['visibilite_utilisateur'])) ?
    $data['visibilite_utilisateur'] : null;
    $this->id_type_utilisateur_utilisateur = (isset($data['id_type_utilisateur_utilisateur'])) ?
    $data['id_type_utilisateur_utilisateur'] : null;
    //
    }
    }

    RépondreSupprimer