Controllers & Views - Zend Framework 1.12 partie 3

Tutorial Application Facebook PHP/JS : Controllers & Views

Publié le 02/10/2013

Liste des tâches :

  • - Organisation du dossier public
  • - Création du module default
  • - Création d'un layout
  • - Création d'une route sur un module spécifique
  • - Détails d’un controller
  • - Création de vues et vues partielles
  • - Insertion des helpers dans le layout
  • - Création d'une requête dans un model et récupération des données dans la vue
  • - Gestion d'un pagination simple "prev/next"
  • - Gestion de l'exposé dans la navigation avec les placeholders
  • - Création d’un Error controller pour la gestion des 404
  • - Gestion d’un système de colonnage

Cette section aborde la mise en place d’un layout pour les views, des contrôleurs et des vues partielles.

Nous aborderons aussi les helpers ou aide de vues.

1 - Organisation du dossier public

Créer les dossiers suivants dans le dossier /Applications/MAMP/htdocs/next/public/ :

  • - images
  • - css
  • - js

Nous avons, dans la section précedente, déclaré la constante PUBLIC_PATH dans le fichier index.php du dossier "APPLICATION_PATH"/public/ que nous appellerons par la suite afin de simplifier les chemins avec la constante PUBLIC_PATH :


defined('PUBLIC_PATH')
    || define('PUBLIC_PATH', realpath(dirname(__FILE__)));

2 - Création du module default

Se positionner dans le dossier "APPLICATION_PATH"/application/ et créer un dossier modules dans lequel on va créer un dossier default, dans lequel on va déplacer : views et controllers situés à la base du dossier "APPLICATION_PATH"/application/. On peut alors supprimer le dossier models du dossier application.

- Déclarer le module dans la section production du fichier /configs/application.ini :


autoloaderNamespaces[] = "Next_"
resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"
resources.modules = ""

3 - Création d'un layout

Rôle du fichier layout et son contenu :

Le fichier layout va servir de «template» html dans lequel on va injecter nos vues, il contiendra donc du html et du php pour l’appel des données.

Ensuite on va enregistrer le dossier où les layouts seront créés, dans le fichier /application/configs/application.ini en la section [production] :


resources.layout.layoutPath = APPLICATION_PATH "/layouts"
resources.layout.layout = master

Dans "APPLICATION_PATH"/application/ on va créer un dossier layouts, dans ce dernier, on va créer un fichier de layout : master.phtml, qui sera le template qu'on associera aux vues publiques du sites. On appellera le contenu dans une vue avec :

<?php echo $this->layout()->content; ?> 

Le choix du layout se fera dans un controller, dans la méthode init() :

$this->_helper->layout->setLayout('master'); 

5 - Détails d’un controller

Détail et structure du fichier "APPLICATION_PATH"/application/controllers/IndexController.php :

init() [facultative] : La méthode init() rassemble les propriétés et méthodes communes à tout le contrôleur et qui seront exécutées avant toute action. indexAction() : Cette méthode s'exécute au chargement de la page. "name"Action() : Ces méthodes définirons les actions en fonction des GET et POST.

4 - Création d'un route sur un module spécifique

Ici on souhaite rediriger l'utilisateur sur la home si il tape "home" après la FQDN (Fully Qualified Domain Name) : http:/next/home. On va définir la route et le module dans la section production du fichier application.ini qui se trouve dans"APPLICATION_PATH"/application/configs/ :


; route - home
resources.router.routes.home.route = /
resources.router.routes.home.defaults.module = default
resources.router.routes.home.defaults.controller = index
resources.router.routes.home.defaults.action = index

Une route Zend sera gérée de la façon suivante :

/module/controller/action/nomparamètre1/valeurparamètre1/nomparamètre2/valeurparamètre2 

Remarque : Un controller dont le nom sera "loginSuccessAction" se notera : login-success dans les routes du fichiers application.ini

5 - Détails d’un controller

Détail et structure du fichier "APPLICATION_PATH"/application/controllers/IndexController.php :

init() [facultative] : La méthode init() rassemble les propriétés et méthodes communes à tout le contrôleur et qui seront exécutées avant toute action. indexAction() : Cette méthode s'exécute au chargement de la page. "name"Action() : Ces méthodes définirons les actions en fonction des GET et POST.

6 - Création de vues et vues partielles

Création de la vue Index :

Déplacer le dossier /application/views dans le dossier "APPLICATION_PATH"/application/modules/default/

La vue index.phtml dans "APPLICATION_PATH"/application/modules/default/views/scripts/index/ existe déjà car elle à été crée lors de la création du projet avec la commande de CLI Zend : zf create.

Création d'un dossier common dans le dossier "APPLICATION_PATH"/application/modules/default/views/scripts/ :

Créer le fichier de vue partielle footer.phtml

Appel de ce dernier dans le fichier de layout "APPLICATION_PATH"/application/layouts/master.phtml avec le code suivant :

<?php echo $this->partial('common/footer.phtml'); ?> 

7 - Insertion des helpers dans le layout

Les helpers se place dans led’un fichier de layout.

headTitle :

echo $this->headTitle('NEXT | Agence de communication interactive'); 

headMeta :


echo $this->headMeta()->appendName('description', 'Agence de communication interactive indépendante depuis 2000, NEXT s´appuie sur son expertise en Rich Media et l´avant-gardisme de ses créations pour faire émerger les marques de ses clients sur le web.')
->appendName('keywords', 'next, agence, studio, communication, interactive, paris, france')
->appendName('author', 'next')
->appendName('revisit-after', '7 days');

headLink :

echo $this->headLink()->appendStylesheet('/css/reset5.css') ->appendStylesheet('/css/common.css') ->appendStylesheet('/css/titles.css') ->appendStylesheet('/css/layouts.css') ->appendStylesheet('/css/nav.css'); 

headScript :


echo $this->headLink()->appendStylesheet('/css/reset5.css')
->appendStylesheet('/css/common.css')
->appendStylesheet('/css/titles.css')
->appendStylesheet('/css/layouts.css')
->appendStylesheet('/css/nav.css');

Remarques : Attention ! certaines aides du vues ne sont pas valides W3C.

Faire aussi attention de chainer les méthodes, sinon le contenu généré risque d'apparaitre deux fois :


echo $this->headMeta()->appendName('description', 'Agence de communication interactive indépendante depuis 2000, NEXT s´appuie sur son expertise en Rich Media et l´avant-gardisme de ses créations pour faire émerger les marques de ses clients sur le web.')
->appendName('keywords', 'next, agence, studio, communication, interactive, paris, france');

Nous pouvons créer une aide de vue personnalisée affin de la rappeler dans n'importe quel vue, layout ou controller d'un même module.

Nous allons créer dans le module "default" une helper personnalisé qui va se trouver dans le dossier "APPLICATION_PATH"/application/modules/default/views/helpers/, un fichier Head.php qui contiendra :


<?php class Zend_View_Helper_Head extends Zend_View_Helper_Abstract
{     
    function head()
    {
        $this->view->headTitle('Pages Engagement');
        return $this->view->headTitle();
    }
}

On appelera cette aide de vue personnalisée dans le fichier de layout ou la vue de cette façon :

echo $this->Head(); 

8 - Création d'une requête dans un model et récupération des données dans la vue

Nous allons créer la requête récupérant dans la vue home les réalisations publiées. Pour cela nous allons créer une méthode static dans la class model Realisations "APPLICATION_PATH"/library/Next/Model/Realisations.php :


static function getPublishedRealisation()
{
    $q = Doctrine_Query::create()
        ->from('Next_Model_Realisations')
        ->where('status = 1')
        ->orderBy('position DESC');
    return $q->fetchArray();
}

Ensuite nous allons récupérer le résultat de la requête dans le controller "APPLICATION_PATH"/application/modules/default/controllers/IndexController.php dans la méthode IndexAction(), l’affecter à la vue.


// action to display list of realisations items
public function indexAction()
{
    // Get published realisations to view
    $result = Next_Model_Realisations::getPublishedRealisation();
    $this->view->records = $result;
}

Dans la vue "APPLICATION_PATH"/application/modules/default/views/scripts/index/index.phtml, on récupérera le résultat dans la vue grâce à $this que l’on fera tourner dans un foreach pour récupérer l’ensemble des résultats retournés par la requête :


<?php foreach($this->records as $record): ?>

<article class="bloc_content_04">
    <a href="<?php echo $this->url(array('id' => $record['id']), 'portfolio-article'); ?>">
        <img alt="<?php echo $record['title']; ?>" src="<?php echo 'img/portfolio/'.$record['thumbnail']; ?>" />
    </a>
    <hgroup>
        <h3></h3>
        <h4></h4>
    </hgroup>
</article>
;<?php endforeach; ?>

Le lien vers l'article sera obtenu grâce $this-> url : <?php echo $this->url(array('id' => $record['id']), 'portfolio-article'); ?>

'portfolio-article' sera la vue correspondante à l'article.

On prendra soin de rajouter une route dynamique pour l'article du portfolio, dans le fichier "APPLICATION_PATH"/application/configs/application.ini :


resources.router.routes.portfolio-article.route = /:id
resources.router.routes.portfolio-article.defaults.module = default
resources.router.routes.portfolio-article.defaults.controller = portfolio
resources.router.routes.portfolio-article.defaults.action = index

9 - Gestion d'un pagination simple "prev/next"

Attention : Cette exemple nécessite la mise en place des routes propres.

Exemple du PortfolioController.php gérant les articles :


<?php class PortfolioController extends Zend_Controller_Action
{
    public function init()
    {
        $this->view->placeholder('rubrique')->set('portfolio');
    }

    // action to display a realisation item
    public function indexAction()
    {
        /* Navigation pagination system */

        // set filters and validators for GET input
        $filters = array(
            'id_propre' => array('HtmlEntities', 'StripTags', 'StringTrim')
        );
        $validators = array(
            'id_propre' => array('NotEmpty'),
        );
        $input = new Zend_Filter_Input($filters, $validators);
        $input->setData($this->getRequest()->getParams());

        // test if input is valid
        // retrieve requested record
        // find next and prev item for pagination
        // attach to view
        if ($input->isValid())
        {
            // get selected realisation and linked document
            $result = Next_Model_Realisations::getRealisationPortfolioByIdPropre($input->id_propre);

            // test if result from request is not empty
            if (!empty($result))
            {
                $this->view->records = $result[0];

                // find the id of the current item and send it to view
                $this->view->current = $this->view->records['position'];

                // send id current item to var
                $position_current = $this->view->records['position'];

                // NEXT POSITION
                // query for finding next position item
                $resultNext = Next_Model_Realisations::findNextRealisation($position_current);

                // test if it's the the item is the higher position
                // query for the position from the last realisation
                // send the value to the view
                if($resultNext == null)
                {
                    // query for last realisation position
                    $resultNextLast = Next_Model_Realisations::lastRealisationPosition();
                    
                    // send the value to the view
                    $this->view->next = $resultNextLast[0];
                }
                else
                {
                    $this->view->next = $resultNext[0];
                }

                // PREVIOUS REALISATION
                // query for the previous item
                $resultPrevious = Next_Model_Realisations::findPreviousRealisation($position_current);

                // test if it's the last item
                if($resultPrevious == null)
                {
                    // query first realisation position
                    $resultPreviousLast = Next_Model_Realisations::firstRealisationPosition();
                    
                    $this->view->previous = $resultPreviousLast[0];
                }
                else
                {
                    $this->view->previous = $resultPrevious[0];
                }

            }
            else
            {
                throw new Zend_Controller_Action_Exception('Page not found', 404);
            }
        }
    }
}

La première partie protège la base de données des injections SQL grâce aux validateurs et aux filtres.

La seconde partie, à partir du "if ($input->isValid())", repère la position de l'article en cours et calcul le suivant et le précédent. S'il s'agit du premier ou du dernier article, elle renvoie l’id_propre de ces derniers.

On récupère les valeur dans la vue avec $this->url :

Next :


<?php echo $this->url(array('id_propre' => $this->next['id_propre']), 'portfolio-article'); ?>" title="<?php echo $this->next['title']; ?>

Previous :


<?php echo $this->url(array('id_propre' => $this->previous['id_propre']), 'portfolio-article'); ?>" title="<?php echo $this->previous['title']; ?>

10 - Gestion de l'exposé dans la navigation avec les placeholders

Les placeholders sont disponibles dans l'objet view et peuvent contenir des variables un peu comme $GLOBALS que l'on pourra récupérer à différents endroits du site.

On placera la déclaration et le set-up de cette dernière dans la méthode init() d'un controller :


public function init()
{
    $this->view->placeholder('rubrique')->set('portfolio');
}

Dans la vue ou vue partielle, on pourra donc tester la valeur de cette dernière et effectuer l'action désirée. Dans notre cas il s'agit de donner une valeur à une classe css :


<a class="<?php echo (($this->placeholder('rubrique') == 'portfolio') ? 'on' : ''); ?>" href="/">Portfolio_</a>

Remarque : On utilise une syntaxe d'un if simplifié avec l'opérateur ternaire pour ne pas rendre la lecture du html pénible. On test alors si la rubrique est bien celle désirée et ensuite place la classe voulue dans la balise html.

Ecrire un if avec l'opérateur ternaire :

if($toto == null) {echo 'toto';} else {echo 'titi';} 

S'écrit de la façon suivante :

echo ($toto == null) ? 'rien' : 'quelque chose'; 

11 - Création d’un Error controller pour la gestion des 404

Nous allons créer dans le dossier "APPLICATION_PATH"/application/modules/default/controllers/, le fichier ErrorController.php qui prendra en charge la gestion des erreurs 404 en fonction de l’environnement décrit dans le fichier .

htaccess du répertoire "APPLICATION_PATH"/public :


<?php class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors = $this->_getParam('error_handler');

        switch ($errors->type) {
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:

            // 404 error -- controller or action not found
            $this->getResponse()->setHttpResponseCode(404);
            $this->view->message = 'Page not found';
            break;

            default:
            // application error
            $this->getResponse()->setHttpResponseCode(500);
            $this->view->message = 'Application error';
            break;
        }

        $this->view->exception = $errors->exception;
        $this->view->request = $errors->request;
    }
}

12 - Gestion d’un système de colonnage

Nous allons mettre en place un système de colonnage qui va nous servir pour la rubrique de client. Ce système sera décomposé entre : le model, le controller et la vue.

Dans le Model "APPLICATION_PATH"/application/library/Next/Model/Customers.php :


<?php class Next_Model_Customers extends Next_Model_BaseCustomers
{
    /* CRUD */
    // get selected customer item
    static function getCustomer($id)
    {
        $q = Doctrine_Query::create()->from('Next_Model_Customers c')
                                     ->where('c.id = ?', $id);
        return $q->fetchArray();
    }

    // delete selected customer item
    static function deleteCustomer($id)
    {
        $q = Doctrine_Query::create()
            ->delete('Next_Model_Customers c')
            ->whereIn('c.id', $id);
        return $q->execute();
    }

    /* front */

    // get all sectors, customers and projects
    static function getSectorsCustomersProjects()
    {
        $q = Doctrine_Query::create()
            ->from('Next_Model_Sectors s')
            ->leftJoin('s.Next_Model_Customers c')
            ->leftJoin('c.Next_Model_Projects p')
            ->where('c.id_sector = s.id')
            ->andWhere('p.id_customer = c.id');
        return $q->fetchArray();
    }

    // strip accents
    static function remove_accents($str, $charset='utf-8')
    {
        $str = htmlentities($str, ENT_NOQUOTES, $charset);
        
        $str = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $str);
        $str = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $str);
        $str = preg_replace('#&[^;]+;#', '', $str);
        
        return $str;
    }

    /* backoffice */
    
    // get all customer items
    static function getCustomers()
    {
        $q = Doctrine_Query::create()
        ->from('Next_Model_Customers')
        ->orderBy('id DESC');
        return $q->fetchArray();
    }

    // retrieve record from children customer records
    // SQL = SELECT * FROM customers c, projects p WHERE c.id = p.id_customer AND c.id = $this;
    static function getCustomerChildren($id)
    {
        $qSector = Doctrine_Query::create()
            ->from('Next_Model_Customers c')
            ->leftJoin('c.Next_Model_Projects p')
            ->Where('c.id = ?', $id)
            ->andWhere('c.id = p.id_customer');
            return $qSector->fetchArray();
    }
}

Ici, on s'intéressera à la méthode getSectorsCustomersProjects() qui va se charger de rapatrier tous les secteurs incluant : les clients, les projets et les réalisations.

Controller "APPLICATION_PATH"/application/modules/default/controller/ClientsController.php :


<?php class ClientsController extends Zend_Controller_Action
{
    public function init()
    {
        $this->view->placeholder('rubrique')->set('clients');
    }

    public function indexAction()
    {
        // get all customers by sectors and send to view
        $records = Next_Model_Customers::getSectorsCustomersProjects();

        /* sort by cutomer name */

        // count nb sectors and init nb_project
        $nb_sectors = count($records);
        $nb_projects = 0;

        // increment nb_project count for each entry
        foreach ($records as $r)
        {
            $nb_projects += count($r["Next_Model_Customers"]);
        }

        /* sort by alphabetic order */

        // creation of an empty array where we will stored customer's project name return by the next foreach
        $alphabetical_records = array();

        foreach ($records as $r)
        {
            foreach ($r["Next_Model_Customers"] as $p)
            {
                // retrieve first letter from customer name
                $first_letter = strtoupper(substr($p["name"],0,1));

                if (array_key_exists($first_letter, $alphabetical_records))
                {
                    // if index already created for this letter
                    $alphabetical_records[$first_letter][] = $p;
                }
                else
                {
                    // else we create the index in array
                    $alphabetical_records[$first_letter] = array($p);
                }
            }
        }

        // sort alphabetical_records array by alphabetic order
        ksort($alphabetical_records);
        $this->view->alphabetical_records = $alphabetical_records;
        $this->view->nb_letters = count($alphabetical_records);
        $this->view->nb_sectors = $nb_sectors;
        $this->view->nb_projects = $nb_projects;
        $this->view->records = $records;

    }
}

Le controller va se charger de compter les éléments project name et de les affecter dans un tableau en fonction des deux cas présents : par nom clients et par ordre alphabétique.

La vue, elle se chargera de répartir le contenu de ce tableau dans les colonnes en fonction du nombre de colonnes définies et de la hauteur de ces dernières.

Vues "APPLICATION_PATH"/application/modules/default/views/scripts/clients/index.phtml :


<code>
<!-- DEBUT - bloc content 10 - head article nav -->
<section class="bloc_content_10 clearfix">

    <!-- DEBUT - bloc content 10 column 01 -->
    <div class="bloc_content_10_column_01">

        <!-- DEBUT - bloc title 11 -->
        <h2 class="bloc_title_11">Clients</h2>
        <!-- FIN - bloc title 11 -->

    </div>
    <!-- FIN - bloc content 10 column 01
    DEBUT - bloc content 10 column 02 -->
    <div class="bloc_content_10_column_02">

        <!-- DEBUT - customers nav -->
        <nav id="customers_nav">
            <ul>

                <li>
                    <a href="#">Par ordre alphabétique</a>
                    <span> | </span>
                </li>

                <li>
                    <a href="#" class="on">Par secteur </a>

                </li>

            </ul>
        </nav>
        <!-- FIN - customers nav -->

    </div>
    <!-- FIN - bloc content 10 column 02 -->

</section>
<!-- FIN - bloc content 10 - head article nav
DEBUT - bloc content 17 - customers section -->
<section class="bloc_content_17">

    <!-- DEBUT - bloc content 17 - customers column -->
    <div class="bloc_content_17_column">

        <!-- DEBUT - loop sectors -->
        <?php 
        $height_sector = 4;
        $height_letter = 4;
        $nb_columns = 4;
        $lines_per_column = floor(($this->nb_projects + $this->nb_sectors * $height_sector) / $nb_columns);

        $lines = 0;
        foreach($this->records as $record)
        {

            if ($lines > $lines_per_column)
            {
                $lines = 0;
                echo '</div>';
                echo '<div class="bloc_content_17_column">';
            }
            ?>
            <!-- DEBUT - bloc content 18 - sector -->
            <article class="bloc_content_18" id="<?php echo Next_Model_Customers::remove_accents(strtolower($record['name'])); ?>">

                <!-- DEBUT - bloc title 08 -->
                <h3 class="bloc_title_08"><?php echo $record['name']; ?></h3>
                <!-- FIN - bloc title 08 -->

                <ul class="main_list">

                    <!-- DEBUT - loop customers -->
                    <?php foreach($record['Next_Model_Customers'] as $customer): ?>
                        <li>
                            <?php echo $customer['name']; ?>
                            <ul class="bloc_content_19">
                                <li class="top"></li>
                                <ul class="center">

                                    <!-- DEBUT - loop projects -->
                                    <?php foreach($customer['Next_Model_Projects'] as $project): ?>
                                        <li>
                                            <a href="<?php echo $project['url']; ?>" target="_blank"><?php echo $project['name']; ?></a>
                                        </li>
                                    <?php endforeach; ?>
                                    <!-- FIN - loop projects -->

                                </ul>
                                <li class="bottom"></li>
                            </ul>
                        </li>
                    <?php
                        $lines++;
                    endforeach; 
                    ?>
                    <!-- FIN - loop customers -->

                </ul>
            </article>
            <!-- FIN - bloc content 18 - sectors -->

        <?php 
            $lines += $height_sector;
        } ?>
        <!-- FIN - loop sectors -->

    </div>
    <!-- FIN - bloc content 17 - customers column -->

</section>
<!-- FIN - bloc content 17 - customers section -->

<!-- FIN - bloc content 10 - head article nav
DEBUT - bloc content 17 - customers section -->
<section class="bloc_content_17" style="clear: both;">

    <!-- DEBUT - bloc content 17 - customers column -->
    <div class="bloc_content_17_column">

        <!-- DEBUT - loop sectors -->
        <?php 
        $lines_per_column = floor( ($this->nb_projects + $this->nb_letters * $height_letter) / $nb_columns);

        $lines = 0;
        foreach($this->alphabetical_records as $letter => $record)
        {

            if ($lines > $lines_per_column)
            {
                $lines = 0;
                echo '</div>';
                echo '<div class="bloc_content_17_column">';
            }
            ?>
            <!-- DEBUT - bloc content 18 - sector -->
            <article class="bloc_content_18" id="<?php echo Next_Model_Customers::remove_accents(strtolower($record['name'])); ?>">

                <!-- DEBUT - bloc title 08 -->
                <h3 class="bloc_title_08"><?php echo $letter; ?></h3>
                <!-- FIN - bloc title 08 -->

                <ul class="main_list">

                    <!-- DEBUT - loop customers -->
                    <?php foreach($record as $customer): ?>
                        <li>
                            <?php echo $customer['name']; ?>
                            <ul class="bloc_content_19">
                                <li class="top"></li>
                                <ul class="center">

                                    <!-- DEBUT - loop projects -->
                                    <?php foreach($customer['Next_Model_Projects'] as $project): ?>
                                        <li>
                                            <a href="<?php echo $project['url']; ?>" target="_blank"><?php echo $project['name']; ?></a>
                                        </li>
                                    <?php endforeach; ?>
                                    <!-- FIN - loop projects -->

                                </ul>
                                <li class="bottom"></li>
                            </ul>
                        </li>
                    <?php
                        $lines++;
                    endforeach; 
                    ?>
                    <!-- FIN - loop customers -->

                </ul>
            </article>
            <!-- FIN - bloc content 18 - sectors -->

        <?php 
            $lines += $height_sector;
        } ?>
        <!-- FIN - loop sectors -->

    </div>
    <!-- FIN - bloc content 17 - customers column -->

</section>
<!-- FIN - bloc content 17 - customers section -->