Pages

Date 11 mai 2013

Jobeet ZF2 - Jour 7 - Jouons avec la page Catégorie

De retour, pour le jour 7!
Nous allons amélioré aujourd'hui nos différentes pages. Pour cela, nous allons commencer par améliorer nos routes, pour avoir des urls plus propre et nous compléterons l'affichage de la page d'accueil pour indiquer le nombre d'offres n'apparaissant pas dans le tableau (à cause de la limite du nombre d'offre à afficher que nous avons fixée précédemment).

La route de la catégorie

Nous allons commencer par créer une nouvelle route pour avoir des URL plus propre: cela vous permettra de rendre vos URLs plus lisibles (pour l'internaute) et améliorer le référencement de vos pages (pour les moteurs de recherche).
Modifier le fichier module.config.php pour ajouter la route suivante:
return array(
    [...]
        'router' => array(
        [...]
        'list_category_page' => array(
            'type'    => 'Segment',
            'options' => array(
                'route'    => '/category/:slug[/page/[:page]]',
                'constraints' => array(
                    'page'   => '[0-9]+',
                    'slug'   => '[a-z]+'
                ),
                'defaults' => array(
                    'module'     => 'Front',
                    'controller' => 'Front\Controller\Category',
                    'action'     => 'list',
                    'page'       => 1
                ),
            ),
        ),
    ),
    [...]
);
Nous utilisons ici une route de type Segment. Nous aurons les URLs suivante:

  • /category/design
  • /category/design/page/2
  • /category/design/page/3
Le paramètre slug correspondra au nom de la catégorie, que nous aurons au préalable "nettoyer" (remplacement de tous les caractères non-alphabétiques ou numériques par un tiret, texte en minuscule).
Pour cela, ajoutons une classe Jobeet dans le répertoire Model de notre module:
// Jobeet.php
    namespace Front\Model;

    class Jobeet
    {
        static public function slugify($text)
        {
            $text = preg_replace('/\W+/', '-', $text);
            $text = strtolower(trim($text, '-'));
            return $text;
        }
    }

J'ai aussi modifier la table Category pour ajouter une colonne "slug", cela nous servira plus loin:
ALTER TABLE category ADD COLUMN slug VARCHAR(100) NULL;

    // Mettez à jour les enregistrements de la table Category pour ajouter une valeur à la colonne slug
    // Je mets un exemple d'INSERT, mais vous pouvez le faire via la commande UPDATE
    INSERT INTO category VALUES (1, 'Design', 'design');
    INSERT INTO category VALUES (2, 'Programmation', 'programmation');
    INSERT INTO category VALUES (3, 'Administrator', 'administrator');
    INSERT INTO category VALUES (4, 'Manager', 'manager');


Modifions le modele Category, pour prendre en compte le slug:
<?php
namespace Front\Model;

class Category
{
    public $idCategory;
    public $name;
    public $slug;

    public function exchangeArray($data)
    {
        $this->idCategory = (isset($data['id_category'])) ? $data['id_category'] : null;
        $this->name = (isset($data['name'])) ? $data['name'] : null;
        $this->slug = (isset($data['slug'])) ? $data['slug'] : Jobeet::slugify($this->name);
    }
    
    public function getArrayCopy()
    {
     return get_object_vars($this);
    }
}

Le lien de la page Catégorie

Modifions la vue index.phtml du contrôleur Index pour utiliser notre nouvelle route:
<?php
    use Front\Model\Jobeet;
    $this->headLink()->appendStylesheet('/css/jobs.css');
?>
<?php foreach ($this->results as $result): ?>
    <div class="category">
        <div class="feed">
          <a href="">Feed</a>
        </div>
        <h1>
            <a href="<?php echo $this->url('list_category_page', array('slug' => $result['category']->slug))?>">
                <?php echo $result['category']->name; ?>
            </a>
        </h1>
    </div>
    <table class="jobs">
    [...]
    </table>
<?php endforeach;?>
Allez voir la page d'accueil, survoler une catégorie avec la souris, si je n'ai rien oublier, vous devriez obtenir quelque chose comme ça:
Ex d'url pour la catégorie Programmation
Si vous cliquez maintenant sur la catégorie pour voir les emplois associés à celle-ci, vous obtiendrez une erreur. En effet la route précédente (que nous avons remplacée) attendait un id category, que nous récupérions dans le contrôleur Category pour avoir la liste des emplois. Maintenant, nous n'avons que le  paramètre "slug" (que nous avons ajouté aussi dans la table Category). Modifions le contrôleur Category (et son action List)
class IndexController extends JobeetController
{
    public function listAction()
    {
        // On récupère le paramètre "slug" que l'on a défini dans la route
        $slug = $this->params()->fromRoute('slug', null);
        $currentPage = $this->params()->fromRoute('page', null);
        
        $adapter = $this->jobTable->getAdapter();
        
        if (!is_null($slug)) {
            // On va chercher la categorie correspondant au slug
            $category = $this->categoryTable->getCategoryBySlug($slug);
            
            $select = $this->jobTable->getActiveJobsForPagination($category->idCategory, $this->config['job_nb_valid_days']);
            $paginator = new Paginator(new \Zend\Paginator\Adapter\DbSelect($select, $adapter));
            $paginator->setCurrentPageNumber($currentPage);
            $paginator->setDefaultItemCountPerPage($this->config['nb_job_pagination']);
            
            $jobs = $paginator->getCurrentItems();
        
            return new ViewModel(
                array(
                    'category' => $category,
                    'jobs'     => $jobs,
                    'paginator' => $paginator,
                    'id' => $category->idCategory,
                 'slug' => $slug
                )
            );
        } else {
            $this->getResponse()->setStatusCode(404);
            return;
        }
    }
    [...]
}
Ajoutons aussi la méthode getCategoryBySlug() dans la table Category:
// module/Front/src/Front/Model/CategoryTable.php
class CategoryTable
{
    [...]
    public function getCategoryBySlug($slug)
    {
        $rowset = $this->tableGateway->select(array('slug' => $slug));
        $row = $rowset->current();

        if (!$row) {
            throw new \Exception("Could not find row $slug");
        }

        return $row;
    }
    [...]
}

Voilà, nous avons modifié le code du contrôleur et du modèle. Tout devrait maintenant fonctionner...ou pas!
En affichant la page d'une catégorie, un message d'erreur nous indique qu'il manque un paramètre "slug" pour notre route. Il s'agit en fait d'une erreur causée par la vue partielle gérant la pagination (pagination.phtml): les urls construites dans cette vue prenne en paramètre le nom de la route, mais nous n'avons pas le paramètre slug, mais impossible de l'ajouter sans casser la pagination ailleurs...(si l'on voulait se resservir de la vue, cela pourrait poser problème).

J'ai cependant trouver une solution sur stackoverflow. L'aide de vue Url accepte 4 paramètres ($name, $urlParams, $routeOptions, $reuseMatchedParams): nous utilisons $name (le nom de la route), et $urlParams (les paramètres de l'url), mais la documentation officielle ne parle pas des 2 derniers paramètres ($routeOptions, $reuseMatchedParams). D'après ce que j'ai compris, le paramètre $reuseMatchedParams = true permet de réutiliser les paramètres de la route actuelle comme valeur par défaut.
Modifier la vue pagination comme ci-dessous:
<?php if ($this->pageCount): ?>
    <div class="pagination">
        <!-- First page link -->
        <a href="<?php echo $this->url($this->route, array('page' => $this->first), false, true); ?>">
            <img src="/images/first.png" alt="First page" width="14" height="14" />
        </a>
    
        <!-- Previous page link -->
        <a href="<?php echo $this->url($this->route, array('page' => $this->previous), false, true); ?>">
            <img src="/images/previous.png" alt="Previous page" />
        </a>

        <!-- Numbered page links -->
        <?php foreach ($this->pagesInRange as $page): ?>
            <?php if ($page != $this->current): ?>
                <a href="<?php echo $this->url($this->route, array('page' => $page), false, true); ?>"><?php echo $page; ?></a>
            <?php else: ?>
                <?php echo $page; ?>
            <?php endif; ?>
        <?php endforeach; ?>

        <!-- Next page link -->
        <?php if (isset($this->next)): ?>
            <a href="<?php echo $this->url($this->route, array('page' => $this->next), false, true); ?>">
                <img src="/images/next.png" alt="Next page" />
            </a>
        <?php else: ?>
            <a href="#" class="disabled"><img src="/images/next.png" alt="Next page" /></a>
        <?php endif; ?>
        
        <!-- Last Page -->
        <a href="<?php echo $this->url($this->route, array('page' => $this->last), false, true); ?>">
            <img src="/images/last.png" alt="Last page" />
        </a>
    </div>
<?php endif; ?>

<div class="pagination_desc">
  <strong><?php echo $this->totalItemCount; ?></strong> annonces dans cette catégorie - page <strong><?php echo $this->current;?> / <?php echo $this->last;?></strong>
</div>
La route fonctionne maintenant correctement, avec la pagination


La route Job

Comme pour la page Category, nous allons améliorer l'URL du détail d'un emploi.
Modifier le fichier module.config.php pour ajouter la route suivante:
return array(
    [...]
    'router' => array(
        [...]
        'list_category_page' => array(
            [...]  
        ),
        'get_job' => array(
            'type'    => 'Segment',
            'options' => array(
                'route'    => '/job/:company/:location/:id/:position',
                'constraints' => array(
                    'company' => '[a-z0-9-]*',
                    'position' => '[a-z0-9-]*',
                    'location' => '[a-z0-9-]*',
                    'id'     => '[0-9]+',
                ),
                'defaults' => array(
                    'module'     => 'Front',
                    'controller' => 'Front\Controller\Job',
                    'action'     => 'get',
                ),
            ),
        ),
    ),
    [...]
);
La route prendra 4 paramètres:

  • company : chiffres, lettres en minuscules et tiret
  • location : chiffres, lettres en minuscules et tiret
  • id: chiffres
  • position : chiffres, lettres en minuscules et tiret

Pour utiliser ces URLs, nous allons modifier les 2 vues permettant d’accéder à la fiche d'un emploi: la vue index.phtml du contrôleur Index et la vue list.phtml du contrôleur Category.
// module/Front/view/front/index/index.phtml
<?php
    use Front\Model\Jobeet;
    $this->headLink()->appendStylesheet('/css/jobs.css');
?>
<?php foreach ($this->results as $result): ?>
    <div class="category">
        <div class="feed">
          <a href="">Feed</a>
        </div>
        <h1>
            <a href="<?php echo $this->url('list_category_page', array('slug' => $result['category']->slug))?>">
                <?php echo $result['category']->name; ?>
            </a>
        </h1>
    </div>
    <table class="jobs">
        <tbody>
            <?php foreach($result['job'] as $job): ?>
                <tr class="<?php echo $this->cycle(array("even", "odd"))->next();?>">
                    <td class="location"><?php echo $this->escapeHtml($job->location); ?></td>
                    <td class="position">
                        <a href="<?php echo $this->url('get_job',
                         array(
                             'company' => Jobeet::slugify($job->company),
                             'position' => Jobeet::slugify($job->position),
                             'location' => Jobeet::slugify($job->location),
                             'id'     => $job->idJob
                         )
                        );?>">
                            <?php echo $this->escapeHtml($job->position); ?>
                        </a>
                    </td>
                    <td class="company">
                        <?php echo $this->escapeHtml($job->company); ?>
                    </td>
                </tr>
            <?php endforeach;?>
        </tbody>
    </table>
    <?php if (($count = (int)$result['activeJobs'] - $this->nbJobByCategory) > 0): ?>
        <div class="more_jobs">
          et <a href="<?php echo $this->url('list_category_page', array('slug' => $result['category']->slug))?>">
                <?php echo $count;?>
            </a> autres...
        </div>
    <?php endif; ?>
<?php endforeach;?>

// module/Front/view/front/category/list.phtml
<?php
    use Front\Model\Jobeet;
    $this->headLink()->appendStylesheet('/css/jobs.css');
?>
<h1><?php echo $this->escapeHtml($this->category->name); ?></h1>
<table class="jobs">
    <tbody>
        <?php foreach($this->jobs as $job): ?>
        <tr class="<?php echo $this->cycle(array("even", "odd"))->next();?>">
            <td class="location"><?php echo $this->escapeHtml($job->location); ?></td>
            <td class="position">
                <a href="<?php echo $this->url('get_job',
                    array(
                        'company' => Jobeet::slugify($job->company),
                        'position' => Jobeet::slugify($job->position),
                        'location' => Jobeet::slugify($job->location),
                        'id'     => $job->id_job
                    ), false, false);?>">
                    <?php echo $this->escapeHtml($job->position); ?>
                </a>
            </td>
            <td class="company">
                <?php echo $this->escapeHtml($job->company); ?>
            </td>
        </tr>
        <?php endforeach;?>
    </tbody>
</table>
<?php echo $this->paginationControl($this->paginator, 'Sliding', 'pagination.phtml', (
        array(
            'id' => $this->id,
            'slug' => $this->category->slug
        )
    )
);
?>
La page d'un job devrait maintenant être accessible:

Vues partielles

Vous aurez sans doute remarqué que nous avons copié-collé le tableau des emplois pour la page d'accueil et la page d'une catégorie. C'est n'est pas génial, surtout si les 2 tableaux sont identiques. Nous allons voir comment remédier à cela.

Lorsque vous devez utiliser la même portion d'un template, vous devez utiliser l'aide de vue partial().
Il s'agit d'un extrait de template que l'on pourra réutiliser dans plusieurs templates.

Créons notre vue partielle:
// module/Front/view/partials/job_list.phtml
<?php 
    use FrontModelJobeet;
?>
<table class="jobs">
    <tbody>
        <?php foreach($this->jobs as $test => $job): ?>
         <tr class="<?php echo $this->cycle(array("even", "odd"))->next();?>">
             <td class="location"><?php echo $this->escapeHtml($job->location); ?></td>
             <td class="position">
                 <a href="<?php echo $this->url('get_job',
                     array(
                         'company' => Jobeet::slugify($job->company),
                         'position' => Jobeet::slugify($job->position),
                         'location' => Jobeet::slugify($job->location),
                         'id'     => $job->id_job
                     ), false, false);?>">
                     <?php echo $this->escapeHtml($job->position); ?>
                 </a>
             </td>
             <td class="company">
                 <?php echo $this->escapeHtml($job->company); ?>
             </td>
         </tr>
        <?php endforeach;?>
    </tbody>
</table>

Supprimer le code HTML des tableaux des jobs des vues index.phtml du controlleur Index et de list.phtml du controlleur Category:
// Dans module/Front/view/front/index/index.phtml
<?php
    $this->headLink()->appendStylesheet('/css/jobs.css');
?>
<?php foreach ($this->results as $result): ?>
    <div class="category">
        <div class="feed">
          <a href="">Feed</a>
        </div>
        <h1>
            <a href="<?php echo $this->url('list_category_page', array('slug' => $result['category']->slug))?>">
                <?php echo $result['category']->name; ?>
            </a>
        </h1>
    </div>
    
    <?php
        // On appelle ici notre vue Partiel, on lui passe les jobs en paramètre
        echo $this->partial('/partials/job_list', array('jobs' => $result['job']));
    ?>

    <?php if (($count = (int)$result['activeJobs'] - $this->nbJobByCategory) > 0): ?>
        <div class="more_jobs">
          et <a href="<?php echo $this->url('list_category_page', array('slug' => $result['category']->slug))?>">
                <?php echo $count;?>
            </a> autres...
        </div>
    <?php endif; ?>
<?php endforeach;?>


// Dans module/Front/view/front/category/list.phtml
<?php
    $this->headLink()->appendStylesheet('/css/jobs.css');
?>
<h1><?php echo $this->escapeHtml($this->category->name); ?></h1>
<?php echo $this->partial('/partials/job_list.phtml', array('jobs' => $this->jobs));?>
<?php echo $this->paginationControl($this->paginator, 'Sliding', 'pagination.phtml', (
        array(
            'id' => $this->id,
            'slug' => $this->category->slug
        )
    )
); ?>

Une fois arrivée à ce stade, j'ai eu un soucis concernant le type des données. La requête que l'on fait dans le contrôleur Index retourne un resultSet de Job, alors que la pagination dans l'action List du contrôleur Category nous retourne un tableau d'objet Job (si je me souviens bien).
Pour que le fonctionnement de la vue partielle soit identique dans les vues de ces deux contrôleurs, j'ai effectué un petit refactoring du contrôleur Index, pour utiliser le composant Paginator, plutôt que d'effectuer une requête "custom".

Voilà le code de l'action Index du contrôleur Index:
namespace Front\Controller;

use Zend\View\Model\ViewModel;
use Zend\Paginator\Paginator;

class IndexController extends JobeetController
{
    public function indexAction()
    {
        $categories = $this->categoryTable->fetchAll();
        $results = array();
        $adapter = $this->jobTable->getAdapter();

        foreach ($categories as $category) {
            $select = $this->jobTable->getActiveJobsForPagination($category->idCategory, $this->config['job_nb_valid_days']);
            $paginator = new Paginator(new \Zend\Paginator\Adapter\DbSelect($select, $adapter));

            // On ne veut afficher que la 1ere page (la pagination est en place sur la page de la catégorie
            $paginator->setCurrentPageNumber(0);
            $paginator->setDefaultItemCountPerPage($this->config['nb_job_by_category']);

            // On récupère les 10 jobs de la page en cours
            $jobs = $paginator->getCurrentItems();

            // et le nombre de jobs total
            $activeJobs = $paginator->getTotalItemCount();
            
            $results[] = array(
                'category' => $category,
                'job' => $jobs,
                'activeJobs' => $activeJobs
            );
        }

        $nbJobByCategory = $this->config['nb_job_by_category'];
        
        return new ViewModel(
            array(
                'results' => $results,
                'nbJobByCategory'=> $nbJobByCategory,
            )
        );
    }
}
Voilà, les deux pages utilisent maintenant la même vue partielle pour afficher le tableau d'emplois.


C'est maintenant terminé pour ce jour 7.
Nous avons vu le fonctionnement des routes et nous avons ainsi pu amélioré nos URLs, nous avons utiliser l'aide de vue Partial, pour réutiliser une partie de code HTML et nous avons fait un peu de refactoring (ce qui simplifie le code du contrôleur Index).
Nous verrons dans le jour 8 comment remplir la base de données grâce au formulaire. Vous trouverez le code sur mon compte Github
1 commentaire:
  1. Ne pas oublier d'ajouter cette ligne à la fonction exchangeArray() de Front\Model\Categorie dès que le champ slug a été ajouté à la table category :

    $this->slug = (isset($data['slug'])) ? $data['slug'] : Jobeet::slugify($this->name);

    Marc

    RépondreSupprimer