Pages

Date 16 juin 2013

Jobeet ZF2 - Jour 9 - L'Administration

Avec le travail effectué jusqu'à maintenant, l'application frontend est déjà bien avancée. Nous ne gérons pas encore les utilisateurs, les formulaires d'ajout de catégories et de jobs ne sont accessibles qu'en saisissant les urls correspondantes, et n'importe qui peut poster une annonce. Nous réglerons ces problème dans le jour 10.

Aujourd'hui, nous allons nous attaquer à l'administration de notre application Jobeet. Ne paniquez pas: le présent tutoriel est relativement long, mais ne devrait pas vous poser de problèmes.

La partie Administration nous permettra de consulter la liste des catégories, d'en ajouter, d'en modifier ou même d'en supprimer: idem pour les jobs. Dans cette partie du tutoriel, nous ne gérerons pas les droits d'accès: l'administration sera accessible à tout le monde (pour le moment). Nous gérerons les utilisateurs et les droits d'accès dans la prochaine partie (jour 10 - Les utilisateurs).

Zend Framework 2 ne permet pas de générer une administration de façon automatique. Nous allons devoir créer nous-même. Il existe cependant un module pour mettre en place une interface admin minimale: ZfcAdmin. Nous allons l'utiliser dès maintenant.

Un peu de réorganisation du code existant

J'ai profité de la création du module d'administration pour revoir un peu le code du module Front, et créer un nouveau module "Jobeet" pour regrouper le code commun aux module Front et Administration. Voici les fichiers que j'ai déplacé dans le module Jobeet:

Pour gagner un peu de temps, vous pouvez télécharger le module Jobeet ici. Installez ce nouveau module dans le répertoire module du projet et déclarez-le dans config/application.config.php

Nous avions précédemment une classe utilitaire "Jobeet.php", contenant la méthode statique slugify: je vous invite à effacer cette classe. J'ai transformé la méthode en filtre, que vous trouverez dans le module Jobeet (Filter/Slugify.php). En voici le code:
<?php
    namespace Jobeet\Filter;
    use Zend\Filter\FilterInterface;

    class Slugify implements FilterInterface
    {
        public function filter($value) {
            $value = htmlentities($value, ENT_NOQUOTES, 'utf-8');
            $value = preg_replace('#\&([A-za-z])(?:acute|cedil|circ|grave|ring|tilde|uml)\;#', '\1', $value);
            $value = preg_replace('#\&([A-za-z]{2})(?:lig)\;#', '\1', $value);
            $value = preg_replace('#\&[^;]+\;#', '', $value);
            $value = preg_replace('/\s/', '', $value);
            $value = preg_replace('/\W+/', '-', $value);
            $value = strtolower(trim($value, '-'));
            
            return $value;
        }
    }

Pour créer un filtre, il suffit de créer une nouvelle classe, implémentant l'interface FilterInterface. Cette interface ne définit qu'une seule méthode filter(). Le code précédent de notre méthode Jobeet::slugify() a été améliorée au passage, beaucoup de caractères n'étaient pas pris en compte (accents,ç, ~, etc).
Je vous laisse aller voir les modifications du modèle Category, pour voir comment utiliser ce filtre.

Installation du module ZfcAdmin

L'installation d'un module ZF2 est très simple. Soit on récupère une archive que l'on décompresse dans /vendor, soit on utilise composer. C'est cette dernière méthode que nous allons utiliser.

Commencez par modifier le fichier composer.json, pour ajouter une reference a ZfcAdmin
{
    "name" : "zendframework/skeleton-application",
    "description" : "Skeleton Application for ZF2",
    "keywords" : [
        "framework",
        "zf2"
    ],
    "homepage" : "http://framework.zend.com/",
    "license" : [
        "BSD-3-Clause"
    ],
    "require" : {
        "php" : ">=5.3.3",
        "zendframework/zendframework" : "2.*",
        "zendframework/zend-developer-tools": "dev-master",
        "bjyoungblood/bjy-profiler": "dev-master",
        "zf-commons/zfc-admin": "dev-master"
    },
    "minimum-stability" : "beta"
}

Puis lancez une mise à jour de composer, en ligne de commande:
php composer.phar self-update
php composer.phar update

Si l'installation du module se passe bien, vous devriez obtenir la réponse suivante
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing zf-commons/zfc-admin (dev-master v0.1.1)
    Cloning v0.1.1

zf-commons/zfc-admin suggests installing bjyoungblood/bjy-authorize (Access control to protect ZfcAdmin against unauthorized users)
Writing lock file
Generating autoload files

Le nouveau module s'est installé dans /vendor/zf-commons/zfc-admin.

Activons ce nouveau module. Pour cela, il faut le déclarer dans notre application.config.php
return array(
    'modules' => array(
        'Front',
        'ZendDeveloperTools',
        'BjyProfiler',
        'ZfcAdmin',
    ),
    'module_listener_options' => array(
        'config_glob_paths'    => array(
            'config/autoload/{,*.}{global,local}.php',
        ),
        'module_paths' => array(
            './module',
            './vendor',
        ),
    ),
);

Vous devriez avoir accès à l'administration maintenant. Allez voir http://votreprojet/admin pour voir la page d'admin par défaut:


Nous allons maintenant personnaliser cette page d'accueil, et créer notre module d'administration.

Notre module Admin

Commencer par créer un nouveau répertoire Admin dans le répertoire module. Je vous ai préparer une archive contenant le squelette du module Admin. Décompressez ce module dans le répertoire module de l'application.
Puis activez ce module dans application.config.php:
return array(
    'modules' => array(
        'Front',
        'ZendDeveloperTools',
        'BjyProfiler',
        'ZfcAdmin',
        'Admin',
    ),
    'module_listener_options' => array(
        'config_glob_paths'    => array(
            'config/autoload/{,*.}{global,local}.php',
        ),
        'module_paths' => array(
            './module',
            './vendor',
        ),
    ),
);

Surcharger la vue index.phtml du module ZfcAdmin

Nous pouvons maintenant modifier la page d'accueil du module ZfcAdmin. Pour cela, pas besoin de modifier le code directement dans le module ZfcAdmin. Il suffit simplement de surcharger la vue index.phtml de ce module. Cela se fait très simplement. Le fichier module.config.php de notre nouveau module Admin indique où trouver les vues:
return array(
    'view_manager' => array(
        'template_path_stack' => array(
            __DIR__ . '/../view',
        ),
    ),
);

Si vous regarder dans le module ZfcAdmin, il contient une vue index.phtml (dans vendor/zf-commons/zfc-admin/view/zfc-admin/admin). Pour surcharger cette vue, créez le répertoire zfc-admin (contenant un répertoire admin) dans le répertoire view du module Admin.
Ajoutez un fichier index.phtml
// module/Admin/view/zfc-admin/admin/index.phtml
<?php
    $this->headLink()->appendStylesheet('/css/fam-icons.css');
?>
<div class="container">
    <h2>Accueil</h2>
    <div class="row">
        <div class="span3">
            <a href="#"><i class="fam-cog"></i> Catégories</a>
        </div>
        <div class="span9">
            Gestion des catégories
        </div>
    </div>
    <hr />
    <div class="row">
        <div class="span3">
            <a href="#"><i class="fam-page"></i> Jobs</a>
        </div>
        <div class="span9">
            Gestion des emplois
        </div>
    </div>
    <hr />
    <div class="row">
        <div class="span3">
            <a href="#"><i class="fam-user"></i> Utilisateurs</a>
        </div>
        <div class="span9">
            Gestion des utilisateurs
        </div>
    </div>
</div>
Pour les icônes, je n'ai pas utilisé celles fournies avec Twitter Bootstrap, mais les icônes FamFam (pour Twitter Bootstrap): télécharger l'archive et copier le fichier CSS dans /public/css et l'image dans /public/img

Notre page d'accueil personnalisé devrait être maintenant visible:


Gestion des catégories

Nous avons vu précédemment comment gérer les formulaires avec Zend Framework 2, et nous avons créer un formulaire Category pour ajouter rapidement des catégories dans le module Front. Nous allons améliorer notre formulaire, et la validation de celui-ci pour pouvoir gérer correctement les catégories dans notre module d'administration.

Comme d'habitude, nous allons définir les routes pour la gestion des catégories. Nous devons:
  • pouvoir accéder à la liste des catégories (avec une pagination): /admin/category/[page/X]
  • pouvoir ajouter une catégorie: /admin/category/add
  • pouvoir modifier ou supprimer une catégorie existante
    • /admin/category/edit/X
    • /admin/category/delete/X
J'ai donc créer la route suivante:
// Dans module/Admin/config/module.config.php
'router' => array(
    'routes' => array(
        'zfcadmin' => array(
            'child_routes' => array(
                'category' => array(
                    'type' => 'segment',
                    'options' => array(
                        'route' => '/category[/page/:page]',
                        'constraints' => array(
                            'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                            'id' => '[0-9]+',
                            'page' => '[0-9]+'
                        ),
                        'defaults' => array(
                            'controller' => 'Admin\Controller\Category',
                            'action' => 'index',
                            'page' => 1
                        ),
                    ),
                    'may_terminate' => true,
                    'child_routes' => array(
                        'action' => array(
                            'type' => 'segment',
                            'options' => array(
                                'route' => '/category/:action[/:id]',
                                'constraints' => array(
                                    'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                                    'id' => '[0-9]+',
                                ),
                                'defaults' => array(
                                    'controller' => 'Admin\Controller\Category',
                                    'action' => 'index',
                                )
                            ),
                            'may_terminate' => true
                        )
                    )
                )
            )
        )
    )
)

Créons maintenant le controller Category dans le module Admin.
<?php
    namespace Admin\Controller;
    
    use Jobeet\Controller\JobeetController;
    use Jobeet\Form\CategoryForm;
    use Zend\Paginator\Paginator;
    use Zend\View\Model\ViewModel;
    
    class CategoryController extends JobeetController
    {
        public function indexAction()
        {
            $currentPage = $this->params()->fromRoute('page', null);
            $adapter = $this->categoryTable->getAdapter();
            $select = $this->categoryTable->getAll();
    
            $paginator = new Paginator(new \Zend\Paginator\Adapter\DbSelect($select, $adapter));
            $paginator->setCurrentPageNumber($currentPage);
            $paginator->setDefaultItemCountPerPage($this->config['admin_element_pagination']);
            
            $categories = $paginator->getCurrentItems();
            
            return new ViewModel(
                array(
                    'categories' => $categories,
                    'paginator' => $paginator
                )
            );
        }
    }
Je n'expliquerais pas le code ci-dessus: nous avons déjà vu l'utilisation du composant Paginator dans le jour 8.

Voici la page index correspondant à l'action index de notre controller Category:
<div class="container">
    <a href="<?php echo $this->url('zfcadmin/category/action', array('action' => 'add'), null, false);?>"><i class="fam-add"></i>Nouvelle catégorie</a>
    <table class="table table-hover">
        <thead>
            <tr>
                <th>#</th>
                <th>Catégorie</th>
                <th>Slug</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($this->categories as $category):?>
                <tr>
                    <td><?php echo $category->id_category; ?></td>
                    <td><?php echo $category->name; ?></td>
                    <td><?php echo $category->slug; ?></td>
                    <td>
                        <?php 
                            $urlEdit = $this->url('zfcadmin/category/action',array('action' => 'edit', 'id' => $category->id_category), null, false);
                            $urlDelete = $this->url('zfcadmin/category/action', array('action' => 'delete', 'id' => $category->id_category), null, false);
                        ?>
                        <a href="<?php echo $urlEdit;?>"><i class="fam-pencil"></i></a>
                        <a href="<?php echo $urlDelete;?>"><i class="fam-delete"></i></a>
                    </td>
                </tr>
            <?php endforeach;?>
        </tbody>
    </table>
    <?php echo $this->paginationControl($this->paginator, 'Sliding', 'paginator'); ?>
</div>

Rien de compliqué de notre vue. Regardez comment les liens sont générés, grâce à l'aide de vue url(): on définit le nom de la route et éventuellement un tableau de paramètres (correspondant aux paramètres définits dans la route).

La pagination est gérée grâce à l'aide de vue paginationControl (on lui passe en paramètre l'objet paginator, le type de pagination 'Sliding' et une vue partielle 'paginator' (module/Admin/view/layout/adminPagination.phtml) que l'on va "mapper" dans module/Admin/config/module.config.php, avec la clé "template_map":
'view_manager' => array(
        'template_path_stack' => array(
            __DIR__ . '/../view',
        ),
        'template_map' => array(
            'paginator' => __DIR__ . '/../view/layout/adminPagination.phtml', // Notre vue partielle pour la pagination
        ),
    ),

Le code de la vue partielle:
<?php if ($this->pageCount): ?>
    <div class="pagination pagination-centered">
        <ul>
            <li <?php echo !isset($this->previous) ? 'class="disabled"' : ''; ?>>
                <a href="<?php echo $this->url($this->route, array('page' => $this->first), false, true); ?>">&laquo;</a>
            </li>
            <li <?php echo!isset($this->previous) ? 'class="disabled"' : ''; ?>>
                <a href="<?php echo $this->url($this->route, array('page' => $this->previous), false, true); ?>">&lsaquo;</a>
            </li>
            <?php foreach ($this->pagesInRange as $page): ?>
                <li <?php echo $page == $this->current ? 'class="active"' : ''; ?>>
                    <a href="<?php echo $this->url($this->route, array('page' => $page), false, true); ?>"><?php echo $page; ?></a>
                </li>
            <?php endforeach; ?>
            <li <?php echo!isset($this->next) ? 'class="disabled"' : ''; ?>>
                <a href="<?php echo $this->url($this->route, array('page' => $this->next), false, true); ?>">&rsaquo;</a>
            </li>
            <li <?php echo!isset($this->next) ? 'class="disabled"' : ''; ?>>
                <a href="<?php echo $this->url($this->route, array('page' => $this->last), false, true); ?>">&raquo;</a>
            </li>
        </ul>
    </div>
<?php endif; ?>

Rendez-vous sur la page /admin/category pour admirer le résultat:


Ajoutons les actions manquantes dans le controller Category:
public function addAction()
{
    $formCategory = new CategoryForm();
    $request = $this->getRequest();
            
    if ($request->isPost()) {
        $category = $this->getServiceLocator()->get('Jobeet/Model/Category');
                
        $inputFilter = $category->getInputFilter();
        $formCategory->setInputFilter($inputFilter);
        $formCategory->setData($request->getPost());
    
        $formCategory->getInputFilter()->get('name')->getValidatorChain()->attachByName(
            'Db\NoRecordExists',
            array(
                'adapter' => $category->getDbAdapter(),
                'table' => 'category',
                'field' => 'name'
            )
        );
    
        $formCategory->getInputFilter()->get('slug')->getValidatorChain()->attachByName(
            'Db\NoRecordExists',
            array(
                'adapter' => $category->getDbAdapter(),
                'table' => 'category',
                'field' => 'slug'
            )
        );
                
        if ($formCategory->isValid()) {
            $category->exchangeArray($formCategory ->getData());
            $this->categoryTable->saveCategory($category);
                    
            $this->flashMessenger()->addMessage(array('success' => "Category '{$category->name}' was added successfully"));
            return $this->redirect()->toRoute('zfcadmin/category');
        }
    }
            
    return new ViewModel(
        array(
            'form' => $formCategory
        )
    );
}

Cela ressemble fortement au code que nous avions dans le jour 8 (sur les formulaires), mais avec 3 petites nouveautés:
  • Gestion du validateur Db/NoRecordExists: celui-ci va nous permettre de vérifier que le nom (et le slug) de la catégorie n'existe pas déjà dans la base de données
  • Utilisation du FlashMessenger pour afficher des messages (succès, erreur, information, ...) dans le layout
  • Utilisation du ServiceLocator pour récuperer l'objet Category: j'injecte le DbAdapter dans le modèle Category grâce à une methode setDbAdapter() que j'ai ajouté (nous en avons besoin pour le validateur Db/NoRecordExiste): regarder le fichier /module/Jobeet/config/module.config.php. Mais j'aurais très bien pu passer l'adapter au validateur dans le controleur.

Remarques:
J'ai défini le validateur au niveau du controller (et pas dans l'InputFilter). Ce validateur m'a posé problème pour l'édition d'une catégorie existante: à la validation, un message d'erreur m'indiquait que le nom de la catégorie existait déjà (le nom existe, puisque je suis en train de modifier une catégorie...). Nous verrons au moment de l'édition comment exclure l'enregistrement que nous éditons.
Pour le composant FlashMessenger, je n'ai pas réussi à l'utiliser correctement. J'ai trouvé, en cherchant sur différents forums, une méthode qui a bien fonctionné.
Il suffit de modifier le fichier Module.php du module Admin, dans la méthode onBootstrap:
public function onBootstrap(MvcEvent $e)
{
    $e->getApplication()->getServiceManager()->get('translator');
    $eventManager = $e->getApplication()->getEventManager();
    $moduleRouteListener = new ModuleRouteListener();
    $eventManager->attach(MvcEvent::EVENT_RENDER, function($e) {
        $flashMessenger = new FlashMessenger();
                
        $messages = array_merge(
            $flashMessenger->getSuccessMessages(),
            $flashMessenger->getInfoMessages(),
            $flashMessenger->getErrorMessages(),
            $flashMessenger->getMessages()
        );
                
        if ($flashMessenger->hasMessages()) {
            $e->getViewModel()->setVariable('flashMessages', $messages);
        }
    });
            
    $moduleRouteListener->attach($eventManager);
}
Et dans le layout admin.phtml:
<div class="container">
    <?php
    echo $this->navigation('admin_navigation')->breadcrumbs()->setMinDepth(0)->setPartial(array('breadcrumb', 'Admin'));
    ?>
    <?php if (count($this->flashMessages) > 0):?>
        <?php foreach ($this->flashMessages as $messages):?>
            <?php foreach ($messages as $key => $message):?>
                <div class="alert alert-<?php echo $key;?>">
                    <button type="button" class="close" data-dismiss="alert">�</button>
                    <?php echo $message;?>
                </div>
            <?php endforeach;?>
        <?php endforeach;?>
    <?php endif; ?>
    <?php echo $this->content; ?>
</div>


La vue add.phtml (correspondant à l'affichage de notre formulaire):
<?php
    $title = 'Add a category';
    $this->headTitle($title);
?>

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

<?php
    $form = $this->form;
    $form->setAttribute('action', $this->url('zfcadmin/category/action', array('action' => 'add')));
    $form->setAttribute('class', 'form-horizontal');
    $form->prepare();

    echo $this->form()->openTag($form);
    echo $this->formHidden($form->get('id_category'));
    echo $this->formHidden($form->get('csrf'));
?>

    <div class="control-group">
        <?php echo $this->formLabel($form->get('name')->setLabelAttributes(array('class' => 'control-label')));?>
        <div class="controls">
            <?php echo $this->formInput($form->get('name'));?>
            <?php echo $this->formElementErrors($form->get('name'), array('class' => 'error_list'));?>
        </div>
    </div>
    
    <div class="control-group">
        <?php echo $this->formLabel($form->get('slug')->setLabelAttributes(array('class' => 'control-label')));?>
        <div class="controls">
            <?php echo $this->formInput($form->get('slug'));?>
            <?php echo $this->formElementErrors($form->get('slug'), array('class' => 'error_list'));?>
        </div>
    </div>
    
    <div class="control-group">
        <div class="controls">
            <?php echo $this->formSubmit($form->get('submit'), array('class' => 'btn'));?>
        </div>
    </div>
<?php
    echo $this->form()->closeTag();
?>

Cela ressemble encore une fois à ce que nous avons fait précédemment, sauf que le formulaire n'est plus afficher à l'aide d'un tableau, mais à l'aide de div (avec les classes CSS de Twitter Bootstrap).

Un exemple de message retourné par le composant FlashMessenger (avec le style CSS de Twitter Bootstrap) lors de l'ajout d'une catégorie:

Je ne détaillerais pas le code pour les actions edit et delete du controller Category. Essayez de les implémenter vous-même, ainsi que le controller Job (complètement): cela sera un bon exercice!

En cas de problèmes, n'hésitez pas a chercher des informations sur internet et à consulter le code du framework, il n'y a rien de tel pour s'améliorer!

Pour vous aider, je vous donne la route que j'ai créer pour le controller Job:
'router' => array(
    'routes' => array(
        'zfcadmin' => array(
         'child_routes' => array(
                // Nos routes 'category'
                [...]
             'job' => array(
                 'type' => 'segment',
                 'options' => array(
                     'route' => '/job[/page/:page]',
                     'constraints' => array(
                         'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                         'id' => '[0-9]+',
                         'page' => '[0-9]+'
                     ),
                     'defaults' => array(
                         'controller' => 'Admin\Controller\Job',
                         'action' => 'index',
                         'page' => 1
                     ),
                 ),
                 'may_terminate' => true,
                 'child_routes' => array(
                     'action' => array(
                         'type' => 'segment',
                         'options' => array(
                             'route' => '/job/:action[/:id]',
                             'constraints' => array(
                                 'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                                 'id' => '[0-9]+',
                                 'page' => '[0-9]+'
                             ),
                             'defaults' => array(
                                 'controller' => 'Admin\Controller\Job',
                                 'action' => 'index',
                             ),
                         ),
                         'may_terminate' => true,
                     ),
                 ),
             ),
         ),
        ),
    ),
),


Et si vous bloquez, vous trouverez (comme d'habitude), le code complet dans l'archive disponible à la fin de l'article (et je répondrais évidemment à vos questions dans les commentaires).

Voyons maintenant comment ajouter un menu et un fil d'ariane!

La navigation

Dans les modules d'administration, il n'est pas rare d'avoir besoin d'une navigation accessible sur toutes les pages (avec une barre de menu), et de nous situer dans l'administration (fil d'ariane). Nous allons voir comment intégrer ces deux éléments.
Zend Framework 2 nous fournis des aides de vue pour afficher des élements de navigation: nous allons en voir deux:

  • menu
  • breadcrumbs

Le menu

L'aide vue navigation()->menu() va nous permettre d'ajouter facilement (et rapidement) un menu dans notre module Admin.
Dans un premier temps, nous allons modifier le layout module/Admin/view/layout/admin.phtml:
<body>
    <div class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <a class="brand" href="<?php echo $this->url('zfcadmin') ?>">Jobeet :: Administration</a>
                <div class="nav-collapse">
                    <?php echo $this->navigation('admin_navigation')->menu()
                                                                    ->setUlClass('nav')
                                                                    ->setMinDepth(1)
                                                                    ->setMaxDepth(1)
                                                                    ->setRenderInvisible(false)
                                                                    ->render();?>
                </div>
            </div>
        </div>
    </div>
[...]
</body>
Nous utilisons l'aide de vue navigation() et nous configurons le rendu que nous voulons avoir:

  • l'élément UL aura la class 'nav'
  • nous ne voulons afficher que les éléments de pronfondeur 1 (min et max)
  • nous ne voulons pas les éléments invisibles
Pour les deux derniers points (profondeur 1 et éléments invisible), il nous faut définir les éléments qui composeront le menu. Nous allons ajouter ces éléments dans module/Admin/config/module.config.php:
<?php
return array(
    'router' => array(
        [...]
    ),
    'navigation' => array(
        'admin' => array(
            // Profondeur 0
            'index' => array(
                'label' => 'Admin',
                'route' => 'zfcadmin',
                'pages' => array(
                    // Profondeur 1
                    'category' => array(
                        'label' => 'Category',
                        'route' => 'zfcadmin/category',
                        'pages' => array(
                            'new_category' => array(
                                'label' => 'New category',
                                'route' => 'zfcadmin/category/action',
                                'params' => array('action' => 'add')
                            ),
                            'edit_category' => array(
                                'label' => 'Edit category',
                                'route' => 'zfcadmin/category/action',
                                'params' => array('action' => 'edit')
                            )
                        )
                    ),
                    // Profondeur 1
                    'job' => array(
                        'label' => 'Job',
                        'route' => 'zfcadmin/job',
                        'pages' => array(
                            'new_job' => array(
                                'label' => 'New job',
                                'route' => 'zfcadmin/job/action',
                                'params' => array('action' => 'add')
                            ),
                            'edit_job' => array(
                                'label' => 'Edit job',
                                'route' => 'zfcadmin/job/action',
                                'params' => array('action' => 'edit')
                            )
                        )
                    ),
                )
            )
        )
    ),
    'service_manager' => array(
        'factories' => array(
            'admin_navigation' => 'ZfcAdmin\Navigation\Service\AdminNavigationFactory',
        ),
    ),
);

La clé 'navigation' va nous permettre de décrire les différentes pages (définit par un libellé, une route et éventuellement des pages filles). La page 'index' aura la profondeur 0, ses pages filles la profondeur 1 (nous ne voulons afficher que les élément du menu de profondeur1)

Nous définissons aussi le service de navigation à utiliser (clé 'service_manager'): le module ZfcAdmin fournit un service AdminNavigationFactory que nous allons utiliser (une fabrique). Sinon, il est aussi possible d'utiliser Zend\Navigation\Service\DefaultNavigationFactory: dans ce cas, il faudra modifier la clé 'admin' par 'default', et dans la layout, appelé $this->navigation('navigation')->menu() au lieu de $this->navigation('admin_navigation')->menu()

Si tout fonctionne bien (et que je n'ai pas oublié d'étape...), vous devriez maintenant voir votre menu:
Le menu actif sera sélectionné en fonction des pages où vous irez. Cliquez sur le menu Category (ou sur le lien Categories):

Pour le fil d'ariane, cela va être aussi simple.

Le fil d'ariane (breadcrumbs)

Pour mettre en place le fil d'ariane, il n'y a rien de plus simple. Nous avons déjà fait une partie du travail quand nous avons ajouté le menu: nous avons déjà configuré les pages (et leurs filles).

Pour ajouter le fil d'ariane sur notre layout admin.phtml, nous allons utiliser l'aide de vue navigation()->breadcrumbs():
<div class="container">
    <?php
        echo $this->navigation('admin_navigation')->breadcrumbs()->setMinDepth(0)->setPartial(array('breadcrumb', 'Admin'));
    ?>
    [...]
    <?php echo $this->content; ?>
    [...]
</div>

Pour avoir un fil d'ariane type Twitter Bootstrap, j'ai crée une vue partielle ('breadcrumb'). Comme pour la pagination, il faut "mapper" le template "breadcrumb" avec le nom du fichier (dans module.config.php du module Admin):
'view_manager' => array(
        'template_path_stack' => array(
            __DIR__ . '/../view',
        ),
        'template_map' => array(
            'paginator' => __DIR__ . '/../view/layout/adminPagination.phtml', // Le paginator
            'breadcrumb' => __DIR__ . '/../view/layout/adminBreadcrumb.phtml', // Notre fil d'ariane
        ),
    ),

Le template pour le fil d'ariane style Boostrap est disponible ci-dessous:
// module/Admin/view/layout/adminBreadcrumb.phtml
<?php
    $container = $this->navigation()->breadcrumbs();
?>
    <ul class="breadcrumb">
        <li>Vous êtes ici :</li>
        <?php foreach($this->pages as $page): ?>
            <?php if( ! $page->isActive()): ?>
                <li>
                    <a href="<?php echo $page->getHref() ?>"><?php echo $page->getLabel() ?></a> 
                    <span class="divider"> &gt; </span>
                </li>
            <?php else: ?>
                <li class="active">
                    <?php if($container->getLinkLast()): ?>
                        <a href="<?php echo $page->getHref() ?>">
                    <?php endif ?>
                    <?php echo $page->getLabel() ?>
                    <?php if($container->getLinkLast()): ?>
                        </a>
                    <?php endif ?>
                </li>
            <?php endif ?>
        <?php endforeach ?>
    </ul>

Remarque:
Vous n'êtes pas obligés de créer de vue partielle, ZF2 génère un fil d'ariane avec un style par défaut (il suffit de ne pas appeler la méthode setPartial() dans le layout admin.phtml.

Comme pour le menu, le fil d'ariane se mettra à jour en fonction des pages visitées. Allez voir le résultat:

En en visitant une page:


Nous en avons enfin terminé avec le tutoriel Administration. Nous avons vu pas mal de choses au cours de celui-ci:

  • utilisation du module ZfcAdmin et personnalisation des layouts / vues
  • utlisation du validateur Db/NoRecordExist
  • utilisation des aides de vue navigation()->menu() et navigation()->breadcrumbs()
Prenez bien le temps pour digérer ce gros tutoriel, n'hésitez pas à poser des questions (dans les commentaires, ou sur la communauté Zend Framework 2 France sur Google Plus)

Vous pouvez récupérer le code complet de ce jour 9 sur mon compte Github
(édit du 16:06/2013 J'ai mis a jour l'archive, une petite erreur s'etait glissé dans l'edition d'un job, dans le modele)

Dans le prochain tutoriel, nous verrons la gestion des utilisateurs et les ACL: encore du boulot en perspective! Reposez vous bien en attendant.

12 commentaires:
  1. Merci pour le tuto! A quand la partie authentification de l'admin ?

    Tu fais du bon boulot mais comme tes tutos ne sont pas régulier c'est pas facile de progresser.

    RépondreSupprimer
    Réponses
    1. Bonjour et merci pour le message :-)

      Comme tu l'indique, les tutos ne sont pas réguliers, et j'en suis désolé : c'est un point qui me frustre aussi un peu ^^

      Pour progresser, n'hésite pas à mettre les mains dans le cambouis: je vais te donner quelques pistes pour que tu puisse essayer prendre de l'avance sur le prochain tuto:
      - ajoute l'action pour poster un job (action sur le bouton "Post a job") => le formulaire existe deja (dans le tuto précédent), il faut ajouter le lien sur le bouton
      - installe les modules ZfcUser et BjyAuthorize
      - configure BjyAuthorize pour avoir 3 roles (guest, user, admin) => un utilisateur non authentifier aura le role "guest"
      - configure BjyAuthorize pour que le front soit accessible a tout le monde, sauf pour la page "Post a job" => qui ne devra être accessible qu'avec le role User / Admin
      - configure BjyAuthorize pour que l'admin ne soit accessible qu'au role Admin
      - essaye d'avoir 2 fenêtres de login différentes (une pour le front, pour pouvoir poster un job, et un pour l'admin)
      - si tu accede à l'admin => avoir la fenetre de login Admin et etre redigiré sur l'accueil de l'admin
      - si tu accède a l'action "Post a Job" sur le front => afficher la fenetre de login Front et redirigé sur la page Post a job une fois authentifié

      Quand je posterais le tuto, je serais ravi d'avoir un retour d'experience de ta part :-)
      - qu'est-ce qui t'as posé des problèmes ?
      - comment as-tu contourné ces problèmes ?
      - la documentation des 2 modules a-t'elle été suffisante pour avancer ? (ces remarques seront bénéfiques pour les développeurs de ces modules, qu'ils puissent améliorer la documentation)

      En tout cas, je suis en mode Best Effort pour avancer sur le prochain tuto (je suis encore bloqué sur un point dans le code, mais des que cela sera résolu, le tuto devrait être dispo assez rapidement)

      Supprimer
    2. Merci pour tes indications, je vais les suivre et te ferais un retour lorsque tu posteras le tuto!

      Bon courage ;)

      Supprimer
    3. J'ai réussi sans trop de difficulté en utilisant les deux modules indiqués (plus Roleuserbridge, mais que j'ai finalement enleve car il ne fonctionne pas avec la derniere version).

      J'ai juste bloqué au niveau des rôles en base donnée pour lesquels il n'y aucune indication sur comment on doit faire. Mais après quelques essais, c'est tout bon!

      Supprimer
  2. Effectivement encore merci pour le boulot accompli, quand à la régularité des tutos vous faites de votre mieux selon votre IRL.
    Encore félicitation et à très vite pour de nouvelles aventures ZF2.

    RépondreSupprimer
  3. Merci a vous deux pour vos commentaires et les encouragements:-)

    Effectivement, j'ai encore du mal a sortir les tutos régulièrement. Suivant les périodes, j'ai pas mal de boulot coté pro, et j'ai envie (aussi) de prendre un peu de temps coté perso ^^

    Mais surtout, je suis dans le même cas que les personnes découvrant le framework: il m'arrive de bloquer sur certains points et de ne pas trouver de solutions (car peu de ressources disponibles, que ce soit en français ou en anglais) ou alors d'avoir une solution peu élégante: je préfère dans ce cas prendre un peu de temps de mon coté pour chercher une solution correcte / acceptable, plutôt qu'écrire de grosses âneries et vous induire en erreur en utilisant un composant (ou un module) de la mauvaise façon.

    RépondreSupprimer
  4. Bonjour,

    super blog et bons tutos. Je pense que je vais pouvoir apprendre pas mal de choses. J'ai déjà fait quelqueschoses sous zf2 mais c'est dur d'être rigoureux et respecter les bonnes conduites, j'espère que tes tutos m'aideront dans ce sens. ;)

    RépondreSupprimer
  5. Bonjour,

    Tout d'abord je tiens à vous féliciter pour vos travaux sur les tutos car je sais que c'est du gros travail !!
    J'apprends ZF2 en faisant mes propres exercices et grâce à votre tuto je peux mettre en place un système d'administration. Seulement une étape coince et concerne la pagination.
    Lorsque je veux ajouter une "catégorie", "New category" dans le fil Ariane n'apparaît pas... Je pense que le souci vient de la configuration du fichier "module.config.php" où je dois renseigner : pages => array('new_category' => [...]).
    Je ne comprend pas d'où vient 'new_category' est-ce un nom attribué au hasard ou existe t-il déjà ?

    Merci pour vos réponses. :-)

    RépondreSupprimer
    Réponses
    1. Bonjour,

      Merci pour le commentaire :-)
      Petite correction, il s'agit de "navigation" et non de "pagination" :-)

      Concernant la clé 'new_category', c'est absolument au hasard et optionnel. Il est possible de ne pas préciser de clé, mais simplement de faire :

      [...]
      'pages' => array(
      array(
      'label' => 'Category',
      'route' => 'zfcadmin/category',
      'pages' => array(
      array(
      'label' => 'New category',
      'route' => 'zfcadmin/category/action',
      'params' => array(
      'action' => 'add'
      )
      ),
      array(
      'label' => 'Edit category',
      'route' => 'zfcadmin/category/action',
      'params' => array(
      'action' => 'edit'
      )
      )
      )
      ),
      [...]

      Supprimer
  6. Merci pour ce tuto, c'est génial :)

    RépondreSupprimer
  7. comment gérer les accents dans les formulaires? comment aussi gérer l'internationalisation des formulaires avec $this->translate() ..

    RépondreSupprimer
  8. Bonjour Romain ,

    Super tuto.
    en effet moi j'utilise le ZFAdmin pour la gestion de ma partie admin , et j'ai creer mon propre système de connexion du user , je n'utilise pas zfUser .
    Mon souci en ce moment , j'ai du mal a poser les permissions sur mes différentes routes , j'ai vraiment besoin d'un coup de main.
    Merci

    RépondreSupprimer