Hier geben wir Einblicke in das Unternehmen, Gedanken, How-Tos, Wissenswertes und News, die sich aus der Programmierung und Projekten ergeben. Auch unsere Open Source Aktivitäten begleiten wir hier.

Corporate Blog der prooph software GmbH

Zend Framework 2 Routing und Navigation - Tutorial 2

Zend Framework 2 Routing und Navigation - Tutorial 2

Dies ist der zweite Teil meiner Tutorialreihe zur Zend Framework 2 Navigation. Nachdem wir uns im ersten Teil des Tutorials angeschaut haben, wie wir eine Zend Framework 2 Navigation per Konfiguration erstellen können, geht es nun weiter ins Detail. Im Fokus steht die Verknüpfung der Navigation-Pages mit dem Routing der Anwendung. Neben statischen und parametrisierten Routen erläutert der Artikel auch die Verknüpfung von Child-Routes innerhalb einer Zend Framework 2 Navigation und beschreibt einen Workaround für einen Fallstrick beim Arbeiten mit Child-Routes.

Statische Routen

Beginnen wir mit dem einfachsten Fall: Wir haben eine feste Route und können unsere Navigation-Page direkt mit dieser verknüpfen.

//router config
'router' => array(
    'routes' => array(
	//route to homepage
        'home' => array(
            'type' => 'Zend\Mvc\Router\Http\Literal',
            'options' => array(
                'route'    => '/',
                'defaults' => array(
                    'controller' => 'Application\Controller\Index',
                    'action'     => 'index',
                ),
            ),
        ),
[...]

//navigation config
'navigation' => array(
	//application navigation
    'application' => array(
        //Link to homepage
        'home' => array(
            'label' => 'home',
            'route' => 'home',
        ),
[...]

Bei dieser Variante sind keine weiteren Angaben in der Konfiguration nötig. Alle Parameter zur Erzeugung der URI sind fester Bestandteil der Route.

Parametrisierte Routen

Spannender wird es, wenn wir eine dynamische Route haben, die - in Abhängigkeit von definierten Parametern - ein jeweils anderes Ziel in unserer Zend Framework 2 Anwendung anspricht.

//router config
'router' => array(
    'routes' => array(
        [...]
        'freelancer' => array(
                'type' => 'Segment',
                'options' => array(
                    'route'    => '/freelancer[/:action]',
                    'constraints' => array(
                        'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                    ),
                    'defaults' => array(
                        'controller' => 'Application\Controller\Freelancer',
                        'action' => 'index',
                    ),
                ),
            ),

Die freelancer-Route ist vom Typ Segment und gilt für alle Controller-Actions des Freelancer-Controllers. Der Freelancer-Controller beinhaltet neben der indexAction auch eine aboutmeAction und eine skillsAction. Diese beiden Actions wollen wir nun über unsere ZF2 Navigation erreichbar machen. Dafür legen wir unterhalb unserer application-Navigation-Konfiguration zwei neue Einträge an:

'navigation' => array(
    'application' => array(
       [...]
        'about_me' => array(
            'label' => 'Über mich',
            'route' => 'freelancer',
            'action' => 'aboutme',
        ),
        'freelancer_skills' => array(
            'label' => 'Skills',
            'route' => 'freelancer',
            'action' => 'skills',
        ),
[...]

Beide Navigation-Pages beziehen sich auf dieselbe Route freelancer, adressieren jedoch unterschiedliche Actions. Die Angabe des Controllers ist nicht erforderlich, da diese Zuweisung fest in der Route freelancer hinterlegt ist. Als Ergebnis erhalten wir im HTML-Markup folgende li-Elemente:

<li>
    <a href="/freelancer/aboutme">Über mich</a>
</li>
<li>
    <a href="/freelancer/skills">Skills</a>
</li>

Nicht-MVC Parameter, die in einer Zend Framework 2 Controller-Action verarbeitet werden, können die Konfiguration erweitern. Als Beispiel dazu nehmen wir einen Blog-Controller. Dieser hat eine Artikel-Action, die je nach übergebener Artikel-ID einen anderen Blog-Artikel aus der Datenbank lädt und anzeigt. Dafür ergänzen wir folgende Route in unserer Konfiguration:

//router config
'router' => array(
    'routes' => array(
        [...]
        'blog_article' => array(
            'type' => 'Segment',
            'options' => array(
                'route'    => '/blog/article[/:articleid]',
                'constraints' => array(
                    'articleid'     => '[0-9]+',
                ),
                'defaults' => array(
                    'controller' => 'Application\Controller\Blog',
                    'action' => 'article',
                    'articleid' => '0',
                ),
            ),
        ),

Der 1. Artikel soll direkt über die Navigation erreichbar sein. Dies erreichen wir mit folgendem Eintrag in unserer ZF2 Navigation Konfiguration.

'navigation' => array(
    'application' => array(
        [...]
        'blog_article_1' => array(
            'label' => '1. Blog Artikel',
            'route' => 'blog_article',
            'params' => array(
                'articleid' => '1'
            ),
        ),

Die MVC-Parameter controller und action sind in der Route fest hinterlegt und müssen in der Page-Konfiguration nicht angegeben werden. Die Artikel-ID können wir über den Schlüssel params mit der Seite verknüpfen. Im HTML-Markup erscheint nun dieses neue li-Element in unserer Navigation-Ul:

<li>
    <a href="/blog/article/1">1. Blog Artikel</a>
</li>

Aus SEO Sicht empfiehlt es sich an dieser Stelle, den Artikel nicht über seine ID zu verlinken sondern z.B. über einen Parameter link. Dies aber nur am Rande. Die konkrete Umsetzung überlasse ich euch.

Child-Routes

Im Zend Framework 2 gibt es die Möglichkeit sogenannte Child-Routes zu definieren. Dabei handelt es sich um Teil-Routen, die einer Haupt-Route untergeordnet sind. Vorteil des Ganzen ist, dass man den Linkteil der Haupt-Route ändern kann und sich die Änderung automatisch auf alle Teil-Routen auswirkt. Wenn ein externes Zend Framework 2 Modul eigene Routen definiert, die unter einer Modul-Route erreichbar sind, kann man die Modul-Route individuell anpassen, um das Modul besser in die eigene Anwendung zu integrieren. Nehmen wir als Beispiel nochmal unseren Blog und machen daraus ein eigenes Modul. An dieser Stelle betrachten wir ausschließlich die Konfiguration der Routen und Navigation. Ein ZF 2 Modul erstellen ist ein Thema für sich und würde den Rahmen des Tutorials sprengen.

//router config
'router' => array(
    'routes' => array(
    [...]
    'blog' => array(
        'type'    => 'Literal',
        'options' => array(
	//main route for blog module
        'route'    => '/blog',
        'defaults' => array(
            '__NAMESPACE__' => 'Blog\Controller',
            'controller'    => 'Index',
            'action'        => 'index',
        ),
    ),
    'may_terminate' => true,
    'child_routes' => array(
	//article route maps to blog-module, index-controller, article-action
        'article' => array(
            'type'    => 'Segment',
            'options' => array(
                'route'    => '/article[/:articleid]',
                'constraints' => array(
                    'articleid'     => '[0-9]+',
                ),
                'defaults' => array(
                    'action' => 'article',
                    'articleid' => '0'
                ),
            ),
        ),
    ),
),

Das Routing für unser Blog-Modul steht. Der Link auf den 1. Artikel löst sich wieder in folgende URI auf: /blog/article/1. In einer anderen Anwendung wollen wir jedoch die Funktionalität des Blog als Wiki verwenden. Dies soll sich auch in der URI widerspiegeln. Wir binden also unser Blog-Modul in die Anwendung ein und überschreiben die Haupt-Route:

//router config
'router' => array(
    'routes' => array(
        [...]
        'blog' => array(
            'type'    => 'Literal',
            'options' => array(
                //main route to blog module used as wiki
                'route'    => '/wiki',

Die Child-Route article wird nicht überschrieben. Der Link auf den 1. Artikel sieht nun so aus: /wiki/article/1. Um eine Child-Route in der ZF2 Navigation zu verlinken, muss man eine spezielle Schreibweise in der Page-Konfiguration verwenden:

'navigation' => array(
    'application' => array(
        [...]
        'wiki_article_1' => array(
            'label' => '1. Wiki Artikel',
            'route' => 'blog/article',
            'params' => array(
                'articleid' => '1'
            ),
        ),

Unter dem Schlüssel route gibt man den Namen der Haupt-Route und den Namen der Child-Route an, getrennt durch einen Slash. Auch wenn wir unser Blog als Wiki verwenden, ist die Haupt-Route weiterhin dem Namen blog zugeordnet. Dies darf nicht mit der Angabe in der Route selbst verwechselt werden, die als Link-Segment /wiki definiert. Unser HTML-Markup weist nun dieses neue li-Element aus:

<li>
    <a href="/wiki/article/1">1. Wiki Artikel</a>
</li>

Stolperfalle Controller-Parameter

Die Zend Framework 2 SkeletonApplication definiert für das Application-Modul eine Standard-Route bestehend aus einer Haupt-Route:

'application' => array(
    'type'    => 'Literal',
    'options' => array(
        'route'    => '/application',
        'defaults' => array(
            '__NAMESPACE__' => 'Application\Controller',
            'controller'    => 'Index',
            'action'        => 'index',
        ),
    ),
    'may_terminate' => true,

und einer Child-Route:

'child_routes' => array(
        'default' => array(
            'type'    => 'Segment',
            'options' => array(
                'route'    => '/[:controller[/:action]]',
                'constraints' => array(
                    'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                    'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                ),
                'defaults' => array(
                ),
            ),
        ),
    );

Dadurch haben wir die Möglichkeit, in dem Application-Modul Controller und Actions zu definieren, die unter dem - aus dem Zend Framework 1 bekannten - Schema /module/controller/action erreichbar sind, ohne weitere Routen beschreiben zu müssen.
Liegt unser Freelancer-Controller aus dem zweiten Beispiel also im Application-Modul, können wir auch mit der URI /application/freelancer/aboutme auf die Über mich Seite zugreifen.
Eine Verlinkung über unsere ZF2 Navigation erreichen wir mit diesem Eintrag in der Konfiguration:

'navigation' => array(
    'application' => array(
        [...]
        'about_me' => array(
            'label' => 'Über mich',
            'route' => 'application/default',
            'controller' => 'freelancer',
            'action' => 'aboutme',
        ),

Interessant ist der Schlüssel controller. Hier reicht die Angabe freelancer, um auf den Freelancer-Controller zu verweisen, obwohl der Alias für den Controller im ControllerLoader (ServiceManager für Controller-Klassen) mit vollem Namespace angegeben wird:

'controllers' => array(
    'invokables' => array(
        'Application\Controller\Index' 
            => 'Application\Controller\IndexController',
        'Application\Controller\Freelancer' 
            => 'Application\Controller\FreelancerController', 
    ),
),

Möglich wird dies durch die Definition des Namespace innerhalb der Haupt-Route:

'application' => array(
    'type'    => 'Literal',
    'options' => array(
        'route'    => '/application',
        'defaults' => array(
            '__NAMESPACE__' => 'Application\Controller',
            'controller'    => 'Index',
            'action'        => 'index',
        ),
    ),
    'may_terminate' => true,
    [...]

So weit, so gut. Über mich ist über die ZF2 Navigation verlinkt und kann aufgerufen werden - aber ein Detail passt nicht. Wenn wir uns auf der Seite Über mich befinden, ist der Eintrag in der Navigation nicht als active markiert, wie eigentlich in Teil 1 meines Tutorials beschrieben. Ein Blick in Zend\Navigation\Page\Mvc - der automatisch ermittelten Klasse für die Page-Konfiguration - verrät uns warum:

/**
* Returns whether page should be considered active or not
*
* This method will compare the page properties against the route matches
* composed in the object.
*
* @param  bool $recursive  [optional] whether page should be considered
*                          active if any child pages are active. Default is
*                          false.
* @return bool             whether page should be considered active or not
*/
public function isActive($recursive = false)
{
   if (!$this->active) {
       $reqParams = array();
       if ($this->routeMatch instanceof RouteMatch) {
           $reqParams  = $this->routeMatch->getParams();

           $myParams   = $this->params;
           if (null !== $this->controller) {
               $myParams['controller'] = $this->controller;
           }
           [...]
           if (count(array_intersect_assoc($reqParams, $myParams)) 
                == count($myParams)) {
                $this->active = true;
                return true;
           }

Die aktive Seite in der Navigation wird ermittelt, indem die Parameter aus dem aktuellen RouteMatch Objekt verglichen werden mit den Page-Parametern, die in der Konfiguration angegeben wurden. Im Falle der Controller-Angabe stimmen diese beiden Werte jedoch nicht überein. Der Bug wurde bereits gemeldet. Den Status könnt ihr auf github einsehen.

Ein möglicher Workaround dafür wäre, für jeden Controller eine eigene Route zu definieren, wie ich es in meinem zweiten Beispiel zu parametrisierten Routen gezeigt habe. Es gibt aber auch die Möglichkeit, den Fehler mit einer eigenen Page-Klasse zu umschiffen, die von Zend\Navigation\Page\Mvc erbt und die isActive()-Methode überschreibt mit einer angepassten Lösung:

<?php
/**
 * DefaultRoute Page
 *  
 * @author Alexander Miertsch < contact@prooph.de>
 * @copyright (c) 2012, Alexander Miertsch
 */
namespace Application\Mvc\Navigation\Page;

use Zend\Navigation\Page\Mvc;
use Zend\Mvc\Router\RouteMatch;

class DefaultRoute extends Mvc
{
    /**
     * Returns whether page should be considered active or not
     *
     * This method will compare the page properties against the route matches
     * composed in the object.
     *
     * @param  bool $recursive  [optional] whether page should be considered
     *                          active if any child pages are active. Default is
     *                          false.
     * @return bool             whether page should be considered active or not
     */
    public function isActive($recursive = false)
    {        
        if (!$this->active) {
            $reqParams = array();
            
            if ($this->routeMatch instanceof RouteMatch) {
                $reqParams  = $this->routeMatch->getParams();
                
                //cut off the namespace of controller name
                $reqParams['controller'] = strtolower(
                        str_replace(
                            $reqParams['__NAMESPACE__'] . '\\', 
                            '', 
                            $reqParams['controller']
                        )
                );
               
                $myParams   = $this->params;
                if (null !== $this->controller) {
                    //lowercase controller name for comparision
                    $myParams['controller'] = strtolower($this->controller);
                }
                if (null !== $this->action) {
                    $myParams['action'] = $this->action;
                }
                                
                if (null !== $this->getRoute()
                    && $this->routeMatch->getMatchedRouteName() === $this->getRoute()
                    && (count(array_intersect_assoc($reqParams, $myParams)) 
                        == count($myParams))
                ) {
                    $this->active = true;
                    return true;
                }
            }

            $myParams = $this->params;

            if (null !== $this->controller) {
                $myParams['controller'] = $this->controller;
            } else {
                /**
                 * @todo In ZF1, this was configurable 
                 * and pulled from the front controller
                 */
                $myParams['controller'] = 'index';
            }

            if (null !== $this->action) {
                $myParams['action'] = $this->action;
            } else {
                /**
                 * @todo In ZF1, this was configurable 
                 * and pulled from the front controller
                 */
                $myParams['action'] = 'index';
            }

            if (count(array_intersect_assoc($reqParams, $myParams)) 
                == count($myParams)) {
                $this->active = true;
                return true;
            }
        }

        return parent::isActive($recursive);
    }
} 
?>

Zend\Mvc\Router\RouteMatch liefert den Parameter __NAMESPACE__ mit. Diesen Namespace entfernen wir einfach vom Controller-Parameter und wenden die PHP Funktion strtolower() auf den Controller-Parameter des RouteMatch-Objekts und des Page-Objekts an.

$reqParams['controller'] = strtolower(
    str_replace($reqParams['__NAMESPACE__'] . '\\', '', $reqParams['controller'])
);
[...]
if (null !== $this->controller) {
    //lowercase controller name for comparision
    $myParams['controller'] = strtolower($this->controller);
}

Durch diese Modifikation passt die Prüfung wieder und die isActive()-Methode gibt true zurück.
Unsere neue Klasse können wir der Page über die Navigations-Konfiguration zuweisen.

'navigation' => array(
    'application' => array(
        [...]      
        'about_me' => array(
            'label' => 'Über mich',
            'route' => 'application/default',
            'controller' => 'freelancer',
            'action' => 'aboutme',
            'type' => 'Application\Mvc\Navigation\Page\DefaultRoute'
        ),                        
    ),

Et voilà, beim Aufruf von Über mich wird die CSS-Klasse active wieder wie gewünscht gesetzt.

<li class="active">
    <a href="/application/freelancer/aboutme">Über mich</a>
</li>

Sie suchen einen Software Dienstleister für Ihr Projekt: Projektanfrage stellen

Blog Artikel mit ähnlichen Themen