Zend Framework Tutorial Teil 7: MVC Design Pattern für weitere Controller

Im sechsten Teil vom Zend Framework Tutorial hhaben wir mit Hilfe der Zend_View Hilfsmethoden die Formularverarbeitung für unser TravelloBlog erstellt. Wir haben ein Formulartemplate erstellt, die Validierung eingerichtet und die Daten in der Datenbank gespeichert. Zudem haben wir Funktionen für das Ändern und Löschen bestehender Artikel eingerichtet. Wenn du die ersten sechs Teile noch nicht gelesen hast, hole dies bitte schnell nach.

In diesem siebten Teil des Tutorials werden wir nun das MVC Design Pattern für einige weitere Controller umsetzen. Neben den Artikeln hast du in den Übungen auch schon einige Funktionalitäten für die Anzeige und Pflege von Kategorien und Tags programmiert. Heute kümmern wir uns darum, die Navigation unseres TravelloBlogs ein wenig umzustrukturieren und die vorhandenen Funktionen mehr miteinander zu verknüpfen. Außerdem werden wir ein wenig intensiver in den Zend_Controller_RewriteRouter einsteigen, bestehende Zend Framework Klassen erweitern und unsere erste Zend_View Hilfsklasse schreiben.

Wenn du über neue Tutorial Teile informiert werden möchtest, abonniere am besten den Feed dieses Blogs. Dann verpasst du garantiert keinen Teil des Tutorials.

Wichtig: Dieses Tutorial setzt die Zend Framework Preview Version 0.2.0 voraus und kann bei der Verwendung einer aktuelleren Version aus dem SVN unter Umständen nicht zu 100% funktionieren.

Inhaltsverzeichnis

Zend_Controller_RewriteRouter reloaded

Bereits im dritten Teil dieses Tutorials haben wir bereits die Zend_Controller_RewriteRouter Komponente kennen gelernt. Wir schauen uns dafür einmal einen Ausschnitt aus der aktuellen Bootstrap Datei an, die sich im Verzeichnis "/travelloblog/public/" befindet:

PHP:
  1. $route1 = new Zend_Controller_Router_Route(':controller/:action/:id', array('action' => 'index'));
  2.  
  3. $router = new Zend_Controller_RewriteRouter();
  4. $router->addRoute('myroute', $route1);
  5. $router->setRewriteBase('')// change to '/travelloblog/public' for subdirectory
  6. Zend::register('router', $router);
  7.  
  8. [...]
  9.  
  10. $controller = Zend_Controller_Front::getInstance();
  11. $controller->setRouter($router);
  12. $controller->setControllerDirectory($dir);
  13. $controller->dispatch();

Der Router wird also bisher initialisiert, es wird eine Route hinzugefügt und dann wird der Router im Objektspeicher registriert. Später wird der Router dann dem Controller hinzugefügt. Doch was macht die Methode addRoute() nun eigentlich genau?

Dazu ein kurzer Blick in die recht kurze, aber schon vorhandene Geschichte des Zend Frameworks. Zu Beginn mit Version 0.1.1 konnte der vorhandene Router nur Adressen im folgendem Format verarbeiten und auf Basis dessen den gewünschten Controller und die gewünschte Aktionsmethode aufrufen.

/article/show/key1/value1/

Die genannte URL würde die Methode showAction im ArticleController aufrufen und zusätzlich den Parameter "key1" mit dem Wert "value1" belegen. Dieses rudimentäre Routing reicht für einfache Websites aus, aber für ein komplexeres Routing wurde ein flexiblere Router benötigt. Die Idee vom Zend_Controller_RewriteRouter war geboren.

An dieser Stelle empfehle ich noch einmal die Lektüre des entsprechenden Kapitels im Manual. Dort wird die grundsätzliche Funktionsweise und die Möglichkeiten gut erläutert.

Zurück zu unserer bereits vorhandenen Route:

PHP:
  1. $route1 = new Zend_Controller_Router_Route(':controller/:action/:id', array('action' => 'index'));
  2. [...]
  3. $router->addRoute('myroute', $route1);

Jede Route benötigt einen Namen, in unserem Fall haben wir "myroute" verwendet. Die eigentliche Route lautet ":controller/:action/:id". Der Doppelpunkt markiert hier eine Variable, die in einer Aktionsmethode durch die Methode Zend_Controller_Action::_getParam() zugänglich ist. Dabei wird ":controller" immer als Angabe des gewünschten Controllers verwenden und ":action" als Angabe der aufzurufenden Aktionsmethode. Diese beiden Variablen sind also quasi vorbelegt. Zusätzlich können aber beliebig viele weitere Variablen eingeführt werden.

Unsere obige Route passt somit auf alle URLs, die aus drei Teilen bestehen:

  • http://travelloblog/article/show/3
  • http://travelloblog/article/change/2
  • http://travelloblog/category/show/this_and_that

Der erste Teil gibt den Controller an, der zweite die Aktionsmethode und der dritte landet in der Variablen "id". Diese Id wird z.B. in ArticleController::showAction(), ArticleController::changeAction() oder CategoryController::showAction() verwendet.

Mit dem dritten Parameter für addRoute() kannst du Variablenwerte vorgeben. Durch die Angabe array('action' => 'index') wird also per Default die indexAction aufgerufen. Im folgenden werden wir immer mal wieder einige neue Routen einführen.

Startseite des TravelloBlogs

Beim Aufruf der Startseite des TravelloBlogs erscheint immer noch die eher langweilige Ausgabe "IndexController::indexAction()". Damit per Default die Artikelliste angezeigt wird, müssen wir nur in unserer vorhandenen Route eine kleine Änderung einfügen. Ändere bitte in der Bootstrap Datei die Angabe der Route wie folgt ab:

PHP:
  1. $route1 = new Zend_Controller_Router_Route(':controller/:action/:id', array('controller' => 'article', 'action' => 'index'));
  2.  
  3. $router = new Zend_Controller_RewriteRouter();
  4. $router->addRoute('actionroute', $route1);

Jetzt soll per Default nicht der IndexController, sondern der ArticleController verwendet werden. Ein Aufruf der Adresse "http://travelloblog/" bestätigt dies.

Um redundante URLs zu vermeiden, bearbeite nun bitte die drei Templates "delete.php", "form.php" und "show.php" im Verzeichnis "/application/view/article" und ändere dort den Link zur Artikelübersicht wie folgt ab:

HTML:
  1. <a href="/">back to list</a>

Außerdem musst du nun noch die Datei "ArticleController.php" im Verzeichnis "/application/controller" bearbeiten und dort in den Methoden validateAction und doDeleteAction die Redirects auf die Artikelliste anpassen:

PHP:
  1. $url = $this->getRewriteBase() . '/';

Natürlich ist die Artikelliste auch weiterhin über die Adresse "http://travelloblog/article/" erreichbar, wir verwenden diese Adresse in unserer Applikation aber nun nicht mehr selber.

Kategorien in Navigation verwenden

Als nächstes möchten wir die Navigation ein wenig überarbeiten. Bisher sind in der Navigationsspalte auf der linken Seiten nur Links zu den Listen für Artikel, Tags und Kategorien sowie zum Neuanlegen. Schöne wäre es, wenn wir dort stattdessen die Kategorien verwenden würden.

Als erstes öffnen wir wieder die Datei "ArticleController.php" im Verzeichnis "/application/controllers" und fügen dort eine neue Methode zum Lesen der Kategorien hinzu:

PHP:
  1. <?php
  2.     private function getNavigation()
  3.     {
  4.         $category = new CategoryModel();
  5.        
  6.         $order  = 'cat_name';
  7.         $rowset = $category->fetchAll(null, $order);
  8.        
  9.         $data = $rowset->toArray();
  10.        
  11.         return $data;
  12.     }
  13. ?>

Diese müssen wir dann in unserer indexAction Methode aufrufen und die Daten an das Template übergeben. Passe also die indexAction Methode wie folgt an:

PHP:
  1. <?php
  2.         $view->assign('title', 'TravelloBlog - List of articles');
  3.         $view->assign('articles', $data);
  4.         $view->assign('navigation', $this->getNavigation());
  5.         $view->assign('subtemplate', 'article/index.php');
  6. ?>

Öffne nun bitte die Datei "main.php" im Verzeichnis "/application/views". Wir verschieben die Links zum Neuanlegen in Box 3 und entfernen die Links zu den Tag- und Kategorielisten. Stattdessen geben wir die Navigation aus. Die Datei main.php sollte nun wie folgt aussehen:

PHP:
  1. [...]
  2. <div id="navi">
  3. <ul>
  4. <li><a href="/">Homepage</a></li>
  5. <?php foreach($this->navigation as $category) : ?>
  6. <li><a href="category/show/<?php echo $this->escape($category['cat_path']); ?>"><?php echo $this->escape($category['cat_name']); ?></a></li>
  7. <?php endforeach; ?>
  8. </ul>
  9. </div>
  10. <div id="extra">
  11. <div class="extrabox">Box 1</div>
  12. <div class="extrabox">Box 2</div>
  13. <div class="extrabox">
  14. <ul>
  15. <li><a href="article/create/">create new article</a></li>
  16. <li><a href="tag/create/">create new tag</a></li>
  17. <li><a href="category/create/">create new category</a></li>
  18. </ul>
  19. </div>
  20. </div>
  21. [...]

Wenn du nun die Adresse "http://travelloblog/" aufrufst, sollte die Startseite des Blogs wie folgt aussehen:

Screen 1: Navigation

Doch nun haben wir ein kleines Problem. In jeder Aktionsmethode, welche unser Haupttemplate verarbeitet und ausgeben soll, müssten wir nun die Liste der Kategorien ermitteln und an das Template übergeben. Diese Änderung müssten wir jetzt schon in 11 Aktionsmethoden nachpflegen, später kommen sicher weitere Aktionsmethoden hinzu. Außerdem müssten wir die Methode getNavigation() auch in jedem unserer Controller hinein kopieren. Ich denke, da hat niemand große List darauf.

Das Problem ließe sich einfach lösen, wenn wir die Zend_Controller_Action Klasse anpassen und mit unseren gewünschten Funktionen erweitern könnten. Doch geht das überhaupt? Natürlich geht das und sogar recht einfach, da das Zend Framework darauf ausgelegt ist, dass du es einfach erweitern kannst.

Zend Framework Klassen erweitern

Wenn du eine bestehende Klasse aus dem Zend Framework erweitern möchtest, bietet es sich an, für deine erweiterte Klasse das gleiche Schema für die Benennung der Klasse und die Lage der Datei zu verwenden. So liegt die Klasse Zend_Controller_Action z.B. im Verzeichnis "Zend/Controller/" und heißt "Action.php". Wenn wir das gleiche Schema verwenden, können wir das früher eingeführte Autoloading auch für unsere erweiterten Klassen verwenden.

Lege bitte im Verzeichnis "/library/zf" das Unterverzeichnis "Travello" und darunter das Verzeichnis "Controller", so dass du nun ein Verzeichnis "/library/zf/Travello/Controller" hast. Dort legst du die Datei "Action.php" mit folgendem Code an:

PHP:
  1. <?php
  2. abstract class Travello_Controller_Action extends Zend_Controller_Action
  3. {
  4.     protected $_view = null;
  5.  
  6.     protected $_template = 'main.php';
  7.    
  8.     protected $_vars = array();
  9.    
  10.     public function __construct()
  11.     {
  12.         $this->_view = Zend::registry('view');
  13.     }
  14.    
  15.     public function __destruct()
  16.     {
  17.         $this->_view->assign('navigation', $this->getNavigation());
  18.        
  19.         echo $this->_view->render($this->_template);    }
  20. }
  21. ?>

Wir führen zuerst einige geschützte Eigenschaften ein. "$_view" wird für das Speichern der Zend_View Instanz verwendet, in "$_template" legen wir den Namen des Haupttemplates ab und in "$_vars" werden alle Templatevariablen abgelegt.

Im Konstruktor holen wir uns die Instanz von Zend_View und legen sie in unserer Eigenschaft "$_view" ab. Im Destruktor werden die Daten für die Navigation geholt und das Template verarbeitet und ausgegeben.

Im nächsten Schritt öffne bitte alle Controller Klassen im Verzeichnis "/application/controllers" und ändere die Deklaration so ab, dass die Klassen ab sofort nicht mehr Zend_Controller_Action sondern Travello_Controller_Action erweitern. Jetzt schließe bitte alle Controller Klassen mit Ausnahme von ArticleController, CategoryController und TagController.

Jetzt kannst du alle Zeilen entfernen, in denen die Instanz von Zend_View aus dem Objektspeicher geholt wird, sowie alle Zeilen, in denen das Template verarbeitet und ausgeben wird. Danach musst du noch alle Aufrufe von "$view->assign" in "$this->_view->assign" umwandeln sowie den Aufruf von getNavigation() aus der indexAction Methode im ArticleController entfernen. Als Beispiel zeige ich einmal die indexAction Methode vom ArticleController:

PHP:
  1. <?php
  2.     public function indexAction()
  3.     {
  4.         $article = new ArticleModel();
  5.        
  6.         $order  = 'art_cdate DESC';
  7.         $rowset = $article->fetchAll(null, $order);
  8.        
  9.         $data = $rowset->toArray();
  10.        
  11.         $this->_view->assign('title', 'TravelloBlog - List of articles');
  12.         $this->_view->assign('articles', $data);
  13.         $this->_view->assign('subtemplate', 'article/index.php');
  14.     }
  15. ?>

Diese Änderungen musst du in den Methoden buildForm, indexAction, showAction und deleteAction in den Controllern ArticleController, CategoryController und TagController durchführen.

Zum Schluss müssen wir nun nur noch die Methode getNavigation aus dem ArticleController in die neue Klasse Travello_Controller_Action verschieben. Wenn du nun das TravelloBlog wieder im Browser aufrufst, sollte alles noch wie vorher funktionieren und zudem hast du auf jeder Seite die Navigation mit den Kategorien eingebunden.

Kategorie anzeigen

Wenn du auf eine der Kategorien in der Navigation klickst, wird momentan nur die Kategorie selber, aber nicht die zugeordneten Artikel ausgegeben. Das ist natürlich eher suboptimal. Also ändern wir als erstes die showAction Methode im CategoryController wie folgt ab:

PHP:
  1. <?php
  2.     public function showAction()
  3.     {
  4.         $path = $this->_getParam('id');
  5.        
  6.         $category = new CategoryModel();
  7.         $db = $category->getAdapter();
  8.         $where = $db->quoteInto('cat_path = ?', $path);
  9.         $row = $category->fetchRow($where);
  10.        
  11.         $data = $row->toArray();
  12.        
  13.         $select = $db->select();
  14.         $select->from('article', '*');
  15.         $select->join('art2cat', 'art_id = art2cat_art_id');
  16.         $select->where('art2cat_cat_id = ?', $data['cat_id']);
  17.         $select->order('art_cdate DESC');
  18.        
  19.         $articles = $db->fetchAll($select);
  20.        
  21.         $this->_view->assign('title', 'TravelloBlog - ' . $data['cat_name']);
  22.         $this->_view->assign('articles', $articles);
  23.         $this->_view->assign('category', $data);
  24.         $this->_view->assign('subtemplate', 'category/show.php');
  25.     }
  26. ?>

Da Zend_Db_Table bisher noch keine Joins von Tabellen unterstützt, verwenden wir der Einfachheit halber die Zend_Db_Select Komponente, die ich bisher noch nicht ausführlich vorgestellt habe. Ich denke aber, dass du mit Hilfe des Manuals schnell herausfinden kannst, was hier im einzelnen passiert. Die gelesenen Artikel werden dann ebenfalls an das Template übergeben.

Als nächstes musst du noch das Template "show.php" im Verzeichnis "/application/views/category" öffnen und entsprechend wie folgt ändern:

PHP:
  1. <?php foreach($this->articles as $article) : ?>
  2. <h2><a href="article/show/<?php echo $this->escape($article['art_id']); ?>"><?php echo $this->escape($article['art_title']); ?></a></h2>
  3. <p><em><?php echo $this->escape($article['art_cdate']); ?></em></p>
  4. <p><?php echo $this->escape($article['art_teaser']); ?></p>
  5. <hr>
  6. <?php endforeach; ?>
  7. <a href="category/">back to list</a> |
  8. <a href="category/change/<?php echo $this->escape($this->category['cat_path']); ?>">change category</a> |
  9. <a href="category/delete/<?php echo $this->escape($this->category['cat_path']); ?>">delete category</a>

Wenn du dir nun eine Kategorie im Browser anzeigen lässt, wird eine Liste aller dieser Kategorie zugeordneten Artikel ausgegeben:

Screen 2: Liste aller Artikel einer Kategorien

Wir basteln uns eine Tagwolke

Als nächstes möchten wir uns eine Tagwolke basteln, die gut in – sagen wir – die noch freie Box 1 auf der rechten Seite platzieren können. Wir haben in den vorherigen Abschnitten bereits gesehen, wie man Zend_Controller_Action Klasse für die Liste der Kategorien erweitern kann. Dies führen wir nun auch für unsere Tagwolke durch.

Rufe also wieder die Datei "Action.php" im Verzeichnis "/library/zf/Travello/Controller" auf und füge dort die neue private Methode getTagcloud ein und übergeben im Destruktor die Daten an die Templatevariable "tagcloud":

PHP:
  1. <?php
  2.     [...]
  3.  
  4.     private function getTagcloud()
  5.     {
  6.         $tag = new TagModel();
  7.         $db = $tag->getAdapter();
  8.        
  9.         $select = $db->select();
  10.         $select->from('tag', 'tag.*, COUNT(tag_id) AS tag_count');
  11.         $select->join('art2tag', 'tag_id = art2tag_tag_id');
  12.         $select->order('tag_name');
  13.         $select->group('tag_id');
  14.        
  15.         $tags = $db->fetchAll($select);
  16.        
  17.         return $tags;
  18.     }
  19.    
  20.     public function __destruct()
  21.     {
  22.         $this->_view->assign('navigation', $this->getNavigation());
  23.         $this->_view->assign('tagcloud', $this->getTagcloud());
  24.        
  25.         echo $this->_view->render($this->_template);
  26.     }
  27.  
  28.     [...]
  29. ?>

Öffne nun bitte wieder die Datei "main.php" im Verzeichnis "/application/views" und gebe unsere Tagwolke in der Box 1 auf der rechten Seite aus:

PHP:
  1. [...]
  2. <div id="extra">
  3. <div class="extrabox">
  4. <h3>Tagcloud</h3>
  5. <ul>
  6. <?php foreach($this->tagcloud as $tag) : ?>
  7. <li><a href="tag/show/<?php echo $this->escape($tag['tag_path']); ?>"><?php echo $this->escape($tag['tag_name']); ?></a> (<?php echo $this->escape($tag['tag_count']); ?>)</li>
  8. <?php endforeach; ?>
  9. </ul>
  10. </div>
  11. <div class="extrabox">Box 2</div>
  12. [...]

Die Tagwolke ist nun zwar schon vorhanden, sieht aber noch nicht so besonders schön aus. Die Ausgabe sollte in etwa wie folgt aussehen:

Screen 3: Tagwolke 1

Um die Ausgabe der Tagwolke ein wenig schicker zu machen, legen wir dafür nun eine eigene Zend_View Hilfsklasse an. Da ich diese nicht im Zend Framework Verzeichnis, sondern in dem Baum mit unseren abgeleiteten Klassen platzieren möchte, müssen wir Zend_View noch mitteilen, ein weiteres Verzeichnis mit Zend_View Hilfsklassen zu beachten. Öffne dafür die Bootstrap Datei index.php im Verzeichnis "/public" und ergänzte die Konfiguration von Zend_View mit einer Zeile:

PHP:
  1. [...]
  2.  
  3. $view = new Zend_View();
  4. $view->setScriptPath('../application/views');
  5. $view->addHelperPath('e:\travelloblog\library\zf\Travello\View\Helper');
  6.  
  7. [...]

Natürlich musst du das Verzeichnis bei dir entsprechend anpassen. Und natürlich kümmern wir uns auch bald um die überall herum schwirrenden Konfigurationsdaten. Das aber erst in einem der nächsten Teile des Tutorials.

Lege nun das Verzeichnis "/library/zf/Travello/View/Helper" an und erstelle dort die Datei "Tagcloud.php" mit folgendem Inhalt:

PHP:
  1. <?php
  2. class Zend_View_Helper_Tagcloud
  3. {
  4.     public function tagcloud($data)
  5.     {
  6.         $sorted = array();
  7.        
  8.         foreach($data as $tag)
  9.         {
  10.             $tagKey   = $tag['tag_id'   ];
  11.             $tagCount = (integer) $tag['tag_count'];
  12.            
  13.             $sorted[$tagCount][] = $tagKey;
  14.         }
  15.        
  16.         krsort($sorted);
  17.         $biggest  = key($sorted);
  18.         end($sorted);
  19.         $smallest = key($sorted);
  20.        
  21.         $bigSize   = 13;
  22.         $smallSize = 7;
  23.         $diffSize  = $bigSize - $smallSize;
  24.        
  25.         $html = '<ul>';
  26.        
  27.         foreach($data as $key => $tag)
  28.         {
  29.             $tagKey   = $tag['tag_id'];
  30.             $tagCount = (integer) $tag['tag_count'];
  31.            
  32.             if ($tagCount == $biggest)
  33.             {
  34.                 $tag['tag_size'] = ($bigSize * 10);
  35.             }
  36.             elseif ($tagCount == $smallest)
  37.             {
  38.                 $tag['tag_size'] = ($smallSize * 10);
  39.             }
  40.             else
  41.             {
  42.                 $value1 = $tagCount - $smallest;
  43.                 $value2 = $value1 / $biggest;
  44.                 $value3 = $diffSize * $value2;
  45.                 $value4 = $smallSize + $value3;
  46.                
  47.                 $tag['tag_size'] = (integer) round($value4) * 10;
  48.             }
  49.            
  50.             $html.= '<li style="font-size:' . $tag['tag_size'] . '%"><a href="tag/show/' . $tag['tag_path'] . '">' . $tag['tag_name'] . '</a></li>';
  51.            
  52.         }
  53.        
  54.         $html.='</ul>';
  55.        
  56.         return $html;
  57.        
  58.     }
  59. }
  60. ?>

Auf den Algorithmus für das Erstellen den Tagwolke möchte ich an dieser Stelle nicht zu sehr ins Details gehen. In diesem Abschnitt ging es eher darum, eine erste eigene Hilfsklasse für Zend_View zu programmieren. Nur soviel: der Algorithmus gibt zwar die Tags in verschiedenen Größen aus, funktioniert aber nicht wirklich zu 100% - Stichwort: logarithmische Verteilung. Vielleicht hat jemand Lust, die Methode zu verbessern, und meldet sich dann einfach unten in den Kommentaren.

Aber für unsere Zwecke reicht die Tagwolke allemal aus, so dass wir im letzten Schritt noch schnell die Hilfsklasse auch im Template verwenden wollen:

PHP:
  1. [...]
  2. <div id="extra">
  3. <div class="extrabox">
  4. <div class="tagcloud">
  5. <h3>Tagcloud</h3>
  6. <?php echo $this->tagcloud($this->tagcloud); ?>
  7. </div>
  8. </div>
  9. <div class="extrabox">Box 2</div>
  10. [...]

Außerdem lassen wir unserer kleinen Tagwolke noch ein wenig CSS angedeihen. Öffne dafür die Datei "styles.css" im Verzeichnis "/public/files/css" und füge folgendes ans Ende der Datei ein.

CSS:
  1. .tagcloud h3 {
  2.     font-size: 1.1em; margin: 0em; padding: 0.2em;
  3.     background-color: rgb(223, 237, 215); text-align: center;
  4. }
  5.  
  6. .tagcloud ul {
  7.     margin: 0; padding: 0.2em;
  8. }
  9.  
  10. .tagcloud li {
  11.     list-style: none; display: inline; padding: 0.2em;
  12. }

Das Ergebnis sieht doch schon viel besser aus:

Screen 4: Tagwolke 2

Tag anzeigen

Jetzt müssen wir nur noch die Änderungen für die Anzeige einer Kategorie aus einem der vorherigen Kapitel für die Tags nachziehen. Also zuerst im TagController die Methode showAction wie folgt ändern:

PHP:
  1. <?php
  2.     public function showAction()
  3.     {
  4.         $path = $this->_getParam('id');
  5.        
  6.         $tag = new TagModel();
  7.         $db = $tag->getAdapter();
  8.         $where = $db->quoteInto('tag_path = ?', $path);
  9.         $row = $tag->fetchRow($where);
  10.        
  11.         $data = $row->toArray();
  12.        
  13.         $select = $db->select();
  14.         $select->from('article', '*');
  15.         $select->join('art2tag', 'art_id = art2tag_art_id');
  16.         $select->where('art2tag_tag_id = ?', $data['tag_id']);
  17.         $select->order('art_cdate DESC');
  18.        
  19.         $articles = $db->fetchAll($select);
  20.        
  21.         $this->_view->assign('title', 'TravelloBlog - Tag ' . $data['tag_name']);
  22.         $this->_view->assign('articles', $articles);
  23.         $this->_view->assign('tag', $data);
  24.         $this->_view->assign('subtemplate', 'tag/show.php');
  25.     }
  26. ?>

Und dann noch das Template "show.php" im Verzeichnis "/application/views/tag" öffnen und entsprechend wie folgt ändern:

PHP:
  1. <?php foreach($this->articles as $article) : ?>
  2. <h2><a href="article/show/<?php echo $this->escape($article['art_id']); ?>"><?php echo $this->escape($article['art_title']); ?></a></h2>
  3. <p><em><?php echo $this->escape($article['art_cdate']); ?></em></p>
  4. <p><?php echo $this->escape($article['art_teaser']); ?></p>
  5. <hr>
  6. <?php endforeach; ?>
  7. <a href="tag/">back to list</a> |
  8. <a href="tag/change/<?php echo $this->escape($this->tag['tag_path']); ?>">change tag</a> |
  9. <a href="tag/delete/<?php echo $this->escape($this->tag['tag_path']); ?>">delete tag</a>

Kategorien zuordnen

Die neue Kategorienliste macht natürlich erst so richtig Spaß, wenn wir Artikel auch beliebig viele Kategorien zuordnen können. Wie du dich vielleicht erinnerst, haben wir in Teil 4 dieses Tutorials sogar eigens dafür die Tabelle "art2cat" angelegt. Wir haben aber für diese - ich nenne sie mal Verknüpfungstabelle – keine von Zend_DB abgeleitete Model Klassen im Verzeichnis "/application/models" angelegt.

Das liegt daran, dass Zend_Db zwingend nur einen einspaltigen Primärschlüssel in einer Tabelle erwartet. Die Verknüpfungstabelle "art2cat" besteht jedoch im Prinzip nur als zwei Fremdschlüsseln. Leider stellt das Zend Framework derzeit noch keine einfache Möglichkeit für die Verarbeitung von Datenbanktabellen bereit, die von der durch Zend_Db vorausgesetzten Norm abweichen.

Also müssen wir unsere Tabelle "art2cat" anpassen und um einen Primärschlüssel erweitern. Der Einfachheit halber hier der der MySQL Dump mit den Änderungen. Du findest dieses SQL Skript auch im neuen Download im Verzeichnis "/stuff".

SQL:
  1. DROP TABLE IF EXISTS `art2cat`;
  2. CREATE TABLE IF NOT EXISTS `art2cat` (
  3.   `art2cat_id` mediumint(8) UNSIGNED NOT NULL AUTO_INCREMENT,
  4.   `art2cat_art_id` mediumint(8) UNSIGNED NOT NULL DEFAULT '0',
  5.   `art2cat_cat_id` tinyint(3) UNSIGNED NOT NULL DEFAULT '0',
  6.   PRIMARY KEY  (`art2cat_id`),
  7.   KEY `fk_art2cat_art_id` (`art2cat_art_id`),
  8.   KEY `fk_art2cat_cat_id` (`art2cat_cat_id`)
  9. ) TYPE=MyISAM AUTO_INCREMENT=3 ;
  10.  
  11. INSERT INTO `art2cat` VALUES (1, 1, 2);
  12. INSERT INTO `art2cat` VALUES (2, 2, 1);

Jetzt können wir die Model Klasse "Art2catModel" im Verzeichnis "/application/models" anlegen.

PHP:
  1. <?php
  2. class Art2catModel extends Zend_Db_Table
  3. {
  4.     protected $_name    = 'art2cat';
  5.     protected $_primary = 'art2cat_id';
  6. }
  7. ?>

Als nächstes müssen wir unser Formular für die Pflege der Artikel um die Liste der Kategorien erweitert. Öffne dafür die Datei "form.php" im Verzeichnis "/application/views/article" und füge die Ausgabe der Auswahlliste mit den Kategorien ans Ende des Formulars an.

PHP:
  1. [...]
  2. <tr>
  3. <td><label for="art2cat_cat_id">Categories</label></td>
  4. <td>
  5. <?php if (isset($this->errors['art2cat_cat_id'])) : ?><div class="error"><?php endif; ?>
  6. <?php echo $this->formSelect('art2cat_cat_id', $this->art2cat_cat_id, array('id' => 'art2cat_cat_id', 'size' => 3, 'multiple' => 'multiple'), $this->categories); ?>
  7. <?php if (isset($this->errors['art2cat_cat_id'])) : ?><br/><?php echo $this->escape($this->errors['art2cat_cat_id']); ?></div><?php endif; ?>
  8. </td>
  9. </tr>
  10. [...]

Als nächstes müssen wir den ArticleController anpassen. Öffne also die Datei "ArticleController.php" im Verzeichnis "/application/controllers" und erweitere als erstes die Methode buildForm, damit auch die Liste aller vorhandenen Kategorien an Zend_View übergeben werden kann.

PHP:
  1. <?php
  2.     private function buildForm($params)
  3.     {
  4.         $category = new CategoryModel();
  5.         $rowset = $category->fetchAll(null, 'cat_name');
  6.        
  7.         $categories = $rowset->toArray();
  8.        
  9.         foreach($categories as $values)
  10.         {
  11.             $key = $values['cat_id'];
  12.            
  13.             $params['categories'][$key] = $values['cat_name'];
  14.         }
  15.        
  16.         [...]
  17.     }
  18. ?>

Wenn du nun einmal einen Artikel zum Bearbeiten öffnest oder einen neuen Artikel anlegen möchtest, sollte die Auswahlliste mit allen Kategorien bereits entsprechend befüllt sein.

ArticleController überarbeiten

Jetzt müssen wir noch die Kategorien vorbelegen, die für einen Artikel bereits ausgewählt worden sind. Ändere dafür die Methode changeAction im ArticleController wie folgt ab:

PHP:
  1. <?php
  2.     public function changeAction()
  3.     {
  4.         [...]
  5.        
  6.         $params['title'] = 'TravelloBlog - Update article';
  7.        
  8.         $art2cat = new Art2catModel();
  9.         $db = $art2cat->getAdapter();
  10.         $where = $db->quoteInto('art2cat_art_id = ?', $art_id);
  11.         $rowset = $art2cat->fetchAll($where);
  12.         $data = $rowset->toArray();
  13.        
  14.         foreach($data as $values)
  15.         {
  16.             $params['art2cat_cat_id'][] = $values['art2cat_cat_id'];
  17.         }
  18.  
  19.         $this->buildForm($params);
  20.     }
  21. ?>

Jetzt werden alle Primärschlüssel der zugeordneten Kategorien ermittelt und ebenfalls an Zend_View übergeben, damit der Zend_View Helfer alle Daten hat, um die die Selectliste erstellen zu können. Das Ganze sollte dann in etwa wie folgt aussehen:

Screen 5: Kategorien zuordnen

Jetzt fehlt uns nur noch die Möglichkeit, die zugeordneten Kategorien auch zu validieren und speichern. Ändere als erstes die Methode validateAction, um die Eingaben zu prüfen. Wir möchten sicher stellen, dass jeder Artikel mindestens einer Kategorie zugeordnet ist.

PHP:
  1. <?php
  2.     public function validateAction()
  3.     {
  4.         [...]
  5.         $data['art_text'  ] = $_POST['art_text'  ];
  6.        
  7.         $data['art2cat_cat_id'] = isset($_POST['art2cat_cat_id'])
  8.                                 ? $_POST['art2cat_cat_id']
  9.                                 : null;
  10.        
  11.         [...]
  12.        
  13.         if (is_null($data['art2cat_cat_id']))
  14.         {
  15.             $errors['art2cat_cat_id'] = 'Please select at least one category!';
  16.         }
  17.        
  18.         [...]
  19.     }
  20. ?>

Die Fehlermeldung erscheint nun jedes Mal, wenn du einen Artikel ohne zugeordnete Kategorie abspeichern möchtest. Die ausgewählten Daten werden nun an die Methode saveData übergeben, um die wir uns als letztes kümmern müssen.

Doch vorher nur noch eine Kleinigkeit. In der changeAction Methode haben wir eben eine Abfrage aller einem Artikel zugeordneten Kategorien vorgenommen. Da wir diese gleich noch einmal benötigen, möchte ich sie gerne in einer eigene private Methode auslagern. Lege also als erstes die Methode getCategories im ArticleController an:

PHP:
  1. <?php
  2.     private function getCategories($art_id)
  3.     {
  4.         $art2cat = new Art2catModel();
  5.         $db = $art2cat->getAdapter();
  6.         $where = $db->quoteInto('art2cat_art_id = ?', $art_id);
  7.         $rowset = $art2cat->fetchAll($where);
  8.         $data = $rowset->toArray();
  9.        
  10.         foreach($data as $values)
  11.         {
  12.             $categories[] = $values['art2cat_cat_id'];
  13.         }
  14.        
  15.         return $categories;
  16.     }
  17. ?>

Und ändere dann erneut die changeAction Methode, indem du jetzt die getCategories Methode verwendest:

PHP:
  1. <?php
  2.     public function changeAction()
  3.     {
  4.         [...]
  5.  
  6.         $params['title'] = 'TravelloBlog - Update article';
  7.        
  8.         $params['art2cat_cat_id'] = $this->getCategories($art_id);
  9.        
  10.         $this->buildForm($params);
  11.     }
  12. ?>

Methode saveData überarbeiten

Jetzt können wir endlich zu der Überarbeitung der saveData Methode kommen. Hier sind zwei Dinge zu erledigen. Zum einen müssen wir den Abschnitt für das Neuanlegen von Artikeln ein wenig überarbeiten.

PHP:
  1. <?php
  2.     private function saveData($data)
  3.     {
  4.         [...]
  5.  
  6.         else
  7.         {
  8.             $insertData = array();
  9.             $insertData['art_title'  ] = $data['art_title' ];
  10.             $insertData['art_teaser' ] = $data['art_teaser'];
  11.             $insertData['art_text'   ] = $data['art_text'  ];
  12.             $insertData['art_user_id'] = '1';
  13.             $insertData['art_cdate'  ] = $newDate;
  14.             $insertData['art_udate'  ] = $newDate;
  15.            
  16.             $art_id = $article->insert($insertData);
  17.         }
  18.  
  19.         [...]
  20.     }
  21. ?>

Wir bauen also das Array $insertData mit den zu speichernden Daten komplett neu auf, anstatt das an die Methode saveData übergebene Array $data zu verwenden. Ansonsten würden wir eine Fehlermeldung erhalten, weil Zend_Db versuchen würde, die Spalte "art2cat_cat_id" in der Tabelle "article" zu finden.

Direkt im Anschluss folgt dann das Speichern der zugeordneten Kategorien. Wir ermitteln hierbei zuerst die Differenz zwischen den bereits gespeicherten Zuordnungen in $savedCategories und den vom Formular übergebenen Zuordnungen in $passedCategories. Mit Hilfe von array_diff() können wir nun ermitteln, welche Zuordnungen wie löschen können und welche wir neu speichern müssen.

PHP:
  1. <?php
  2.     private function saveData($data)
  3.     {
  4.         [...]
  5.  
  6.         $savedCategories = $this->getCategories($art_id);
  7.        
  8.         $passedCategories = $data['art2cat_cat_id'];
  9.        
  10.         $forDeletion = array_diff($savedCategories, $passedCategories);
  11.         $forSaving   = array_diff($passedCategories, $savedCategories);
  12.        
  13.         $art2cat = new Art2catModel();
  14.        
  15.         foreach($forDeletion as $cat_id)
  16.         {
  17.             $db = $art2cat->getAdapter();
  18.            
  19.             $where = $db->quoteInto('art2cat_art_id = ?', $art_id);
  20.             $where.= ' AND ' . $db->quoteInto('art2cat_cat_id = ?', $cat_id);
  21.            
  22.             $art2cat->delete($where);
  23.         }
  24.        
  25.         foreach($forSaving as $cat_id)
  26.         {
  27.             $insertData = array();
  28.             $insertData['art2cat_art_id'] = $art_id;
  29.             $insertData['art2cat_cat_id'] = $cat_id;
  30.            
  31.             $art2cat->insert($insertData);
  32.         }
  33.  
  34.         [...]
  35.     }
  36. ?>

Artikelanzeige verbessern

Jetzt haben wir diesen heutigen sehr ausführlichen Abschnitt aber bald überstanden. Ich möchte nur noch die Anzeige eines Artikels ein wenig verbessern. Es sollen alle zugeordneten Kategorien und Tags sowie das letzte Datum der Änderung ausgegeben werden.

Als erstes müssen wir die showAction Methode im ArticleController anpassen:

PHP:
  1. <?php
  2.     public function showAction()
  3.     {
  4.         $art_id = $this->_getParam('id');
  5.        
  6.         $article = new ArticleModel();
  7.         $row = $article->find($art_id);
  8.        
  9.         $data = $row->toArray();
  10.        
  11.         $this->_view->assign('title', 'TravelloBlog - ' . $data['art_title']);
  12.         $this->_view->assign('article', $data);
  13.         $this->_view->assign('subtemplate', 'article/show.php');
  14.        
  15.         $db = $article->getAdapter();
  16.        
  17.         $select = $db->select();
  18.         $select->from('category', '*');
  19.         $select->join('art2cat', 'cat_id = art2cat_cat_id');
  20.         $select->where('art2cat_art_id = ?', $art_id);
  21.         $select->order('cat_name');
  22.         $categories = $db->fetchAll($select);
  23.        
  24.         $this->_view->assign('categories', $categories);
  25.        
  26.         $select = $db->select();
  27.         $select->from('tag', '*');
  28.         $select->join('art2tag', 'tag_id = art2tag_tag_id');
  29.         $select->where('art2tag_art_id = ?', $art_id);
  30.         $select->order('tag_name');
  31.         $tags = $db->fetchAll($select);
  32.        
  33.         $this->_view->assign('tags', $tags);
  34.     }
  35. ?>

Und dann noch das Template "show.php" im Verzeichnis "/application/views/article" wie folgt anpassen:

PHP:
  1. <h2><?php echo $this->escape($this->article['art_title']); ?></h2>
  2. <p>Categories:
  3. <?php foreach($this->categories as $category) : ?>
  4. <a href="category/show/<?php echo $this->escape($category['cat_path']); ?>"><?php echo $this->escape($category['cat_name']); ?></a>
  5. <?php endforeach; ?>
  6. </p>
  7. <p><?php echo $this->escape($this->article['art_text']); ?></p>
  8. <p><em><small>(Written <?php echo $this->escape($this->article['art_cdate']); ?>, updated <?php echo $this->escape($this->article['art_udate']); ?>)</small></em></p>
  9. <p><small>Tags:
  10. <?php foreach($this->tags as $tag) : ?>
  11. <a href="tag/show/<?php echo $this->escape($tag['tag_path']); ?>"><?php echo $this->escape($tag['tag_name']); ?></a>
  12. <?php endforeach; ?>
  13. </small></p>
  14. <hr>
  15. <a href="/">back to list</a> |
  16. <a href="article/change/<?php echo $this->escape($this->article['art_id']); ?>">change article</a> |
  17. <a href="article/delete/<?php echo $this->escape($this->article['art_id']); ?>">delete article</a>

Jetzt werden auch Kategorien und Tags bei jedem Beitrag angezeigt und verlinkt. Es würde sich auch anbieten, für die Ausgabe der Kategorien und Tags auch eine eigene Zend_View Hilfsklasse einzurichten.

Das soll es nun aber endlich gewesen sein. Mittlerweile sieht unser TravelloBlog einem richtigen Blog gar nicht mehr so unähnlich. Es gibt aber immer noch eine Menge zu tun.

Übung: Tags zuordnen

Wie eben im Abschnitt für die Kategorien beschrieben, solltest du nun auch die Zuordnung der Tags zu einem Artikel programmieren. Wie das Ganze abläuft, hast du ja eben gesehen, du musst dies nun nur auf die Tags adaptieren.

Zur Kontrolle lade dir den aktuellen Stand des Tutorials runter. Dann kannst du deine Ergebnisse damit vergleichen.

Download

Der aktuelle Stand des Tutorials nach diesem sechsten Teil kann herunter geladen werden. Hier sind auch alle Templates enthalten. Die Textdateien "empty.txt" in einigen Verzeichnissen habe ich nur angelegt, weil mein Winzip keine leeren Verzeichnisse im Zip Archiv anlegt:

Die Zip Datei enthält nicht die aktuelle Version des Zend Frameworks. Dies musst du bitte selber in das entsprechende Verzeichnis kopieren.

Zusammenfassung

In diesem siebten Teil des Zend Framework Tutorials haben wir unsere Applikation ein wenig aufgeräumt und besser navigierbar gemacht. Die Kategorien bilden nun die Navigation auf der linken Seite und die Tags werden in einer kleinen Tagwolke dargestellt. Zudem können wir nun jedem Artikel auch beliebig viele Kategorien und Tags zuordnen. Wir haben uns ein wenig intensiver mit Zend_Controller_RewriteRouter beschäftigt, bestehende Zend Framework Klassen erweitert und eine erste Hilfsklasse für Zend_View programmiert.

Auf vielfachem Wunsch wird sich der nächste Teil des Tutorials nicht wie ursprünglich geplant mit Smarty beschäftigen. Stattdessen ziehe ich das Kapitel über Zend_Config vor. Dabei werden wir im nächsten Teil sicher auch wieder noch ein wenig mehr aufräumen.

Change Log

An dieser Stelle werden Änderungen an diesem Tutorial Teil zusammengefasst, die nach dem Ersterscheinen (02.10.2006) notwendig waren, z.B. durch neuere Versionen des Zend Frameworks oder Änderungen am Konzept des Tutorials oder den Anforderungen an unser TravelloBlog.

  • 05.11.2006: Tutorial für Release 0.2.0 aktualisiert

Navigation

32 Antworten für “Zend Framework Tutorial Teil 7: MVC Design Pattern für weitere Controller”

  1. Ralfs PHP Blog » Zend Framework Tutorial Teil 6: MVC Design Pattern und Formularverarbeitung sagt:

    [...] Tutorial, View, Zend Framework, Zend Framework Tutorial, Zend View Bookmark: del.icio.us yigg.de mister-wong.de  [...]

  2. Ralfs PHP Blog » Zend Framework Tutorial Teil 1: Einführung und Anforderungen sagt:

    [...] MVC Design Pattern für weitere Controller [...]

  3. Tobias sagt:

    Hallo Ralf,
    in deinem Zip Archiv steckt ein Fehler in der Datei /application/view/main.php - es handelt sich um die Tagwolke.

  4. Ralf Eggert sagt:

    Hallo Tobias,

    was genau meinst du denn? Wo soll da der Fehler enthalten sein?

    Gruß,

    Ralf

  5. Ralfs PHP Blog » Zend Framework Tutorial Teil 8: Konfiguration mit Zend_Config sagt:

    [...] Im siebten Teil vom Zend Framework Tutorial haben wir uns ein wenig intensiver mit Zend_Controller, Zend_Db und Zend_View auseinander gesetzt und unsere Anwendung navigierbarer und mit Hilfe von CSS auch ein wenige hübscher gemacht. Wenn du die ersten sieben Teile noch nicht gelesen hast, hole dies bitte schnell nach. [...]

  6. Michael sagt:

    Hallo Ralf,

    ich habe mir den aktuellen Stand des Tutorials (nach dem 6. Teil) heruntergeladen,
    entpackt und in das Travello-Projekt integriert!
    Beim Aufruf bekomme ich die folgende Fehlermeldung.

    Fatal error: Uncaught exception 'Zend_View_Exception' with message 'helper 'tagcloud' not found in path.' in C:\xampplite\htdocs\travelloblog\library\zf\Zend\View\Abstract.php:491 Stack trace: #0 C:\xampplite\htdocs\travelloblog\library\zf\Zend\View\Abstract.php(186): Zend_View_Abstract->_loadClass('helper', 'tagcloud') #1 [internal function]: Zend_View_Abstract->__call('tagcloud', Array) #2 C:\xampplite\htdocs\travelloblog\application\views\main.php(37): Zend_View->tagcloud(Array) #3 C:\xampplite\htdocs\travelloblog\library\zf\Zend\View.php(46): include('C:\xampplite\ht...') #4 C:\xampplite\htdocs\travelloblog\library\zf\Zend\View\Abstract.php(361): Zend_View->_run('../application/...') #5 C:\xampplite\htdocs\travelloblog\library\zf\Travello\Controller\Action.php(77): Zend_View_Abstract->render('main.php') #6 C:\xampplite\htdocs\travelloblog\library\zf\Zend\Controller\Dispatcher.php(188): Travello_Controller_Action->__destruct() #7 C:\xampplite\htdocs\travelloblog\library\zf\Zend\Controller\Dispatcher.php(136): Zend_Cont in C:\xampplite\htdocs\travelloblog\library\zf\Zend\View\Abstract.php on line 491

    was liegt im Argen?

    Gruß Michael

  7. blablabla sagt:

    Also ich habe da mal ein paar Fragen zu deinem klasse Tutorial:

    - Wieso lädst du die Klassen ständig mit Zend::loadClass(...)? Ich mein dafür gibts doch __autoload ??!

    - Der RewriteRouter ist für mich total unverständlich. Im Zend Manual ist es ganz anders beschrieben als in der API Dokumentation oder hier bei dir. z.B. hier:

    // Standardroute für Root URL
    $this->addRoute('default', '', array('controller' => 'index', 'action' => 'index'));

    das steht im Kapitel Zend_Controller_RewriteRouter. Ich benutze folgenden Code:

    $route1 = new Zend_Controller_Router_Route(':controller/:action/:id', array('controller' => 'entry', 'action' => 'index'));

    $router = new Zend_Controller_RewriteRouter();
    $router->addRoute('actionroute', $route1);
    $router->setRewriteBase(''); // change to '/travelloblog/public' for subdirectory
    Zend::register('router', $router);

    $controller = Zend_Controller_Front::getInstance();
    $controller->setRouter($router);
    $controller->setControllerDirectory($config->getSetting("framework", "controller_dir"));
    $controller->dispatch();

    also fast 1:1 aus deinem Code übernommen (hab version 07 oder so runtergeladen). Aber trotzdem sucht er den IndexController: "IndexController.php was not found" kommt als Fehlermeldung.

    - Warum nennst du eine Route "myroute" und ein paar Zeilen darunter "actionroute" ?! haben die Namen also etwas zu bedeuten?

  8. J.Wachtel sagt:

    Nun stehe ich schonwieder vor einem Problem :(
    Wie blablabla
    Funktioniert bei mir das umleiten nicht. Ich werde immer auf den IndexController geroutet.

    $route1 = new Zend_Controller_Router_Route(':controller/:action/:id', array('controller' => 'article', 'action' => 'index'));
    $router = new Zend_Controller_RewriteRouter();
    $router->addRoute('actionroute', $route1);

    Funktioniert nicht, im Zend Manuell stoß ich auf folgendes und habe so umgeändert:
    $router->addRoute('actionroute', ':controller/:action/:id', array('controller' => 'article', 'action' => 'index'));
    Dann bekomme ich aber ein
    "Argument 2 passed to Zend_Controller_RewriteRouter::addRoute() must be an object of class Zend_Controller_Router_Route_Interface"

    Hätte noch jemand eine Idee warum er nicht dahinroutet? Ich bin ehrlich gesagt als Anfänger recht ratlos :(

  9. Ralf Eggert sagt:

    Hallo blablabla,

    die Frage nach dem __autoload() verstehe ich nicht. Seit dem Tutorial Teil 4 wird doch __autoload() verwendet.

    Das Problem beim RewriteRouter ist, dass die Doku teilweise vom aktuellen Stand abwich/abweicht.

    Die Namen für die Routen haben nichts direkt zu bedeuten. Ich habe den Namen an der einen Stelle nur geändert, weil myroute irgendwie doof klang ;-)

    Gruß,

    Ralf

  10. Ralf Eggert sagt:

    Hallo jens,

    also das Zend Manual ist auf der Website leider nicht mehr aktuell. So, wie es im Tutorial angegeben ist, ist es eigentlich korrekt. Frage: läuft dein Blog im Hauptverzeichnis oder in einem Unterverzeichnis?

    Gruß,

    Ralf

  11. J.Wachtel sagt:

    Hallo Ralf,

    also im DocumentRoot ist es nicht.
    Es ist schon ein Unterverzeichnis, ich habe das ganze vom DocumentRoot so Gegliedert:
    z.b. /zendtutorial/travelloblog/public
    Habe aber wie in deinem Tut beschrieben die setRewriteBase('/zendtutorial/travelloblog/public') gesetzt. Es funktioniert ja auch sonst alles.

    Danke schonmal

    gruß Jens

  12. Peter sagt:

    Hallo Ralf,

    zunächst ein großes Kompliment für dieses Tutorial, das mir den Einstieg in das Zend Framework sehr erleichterte.

    Da ich in einem eigenen Projekt viele der hier beschriebenen Methodiken übernommen habe, bin ich auf einen Fehler gestoßen, der das Exception-Handling verletzt.
    Ursache ist das zwar bequeme, aber gefährliche Platzieren von Code im Destruktor des abgeleiteten Controllers (siehe id="lphp-9").

    Angenommen, in einer Methode des Controllers wird beim Erzeugen eines Objektes eine Exception geworfen. Dies führt zum automatischen Aufruf des Destruktors des Controllers und somit zur Ausführung! des hier stehenden Codes, sprich Rendern der View. Wird nun in der View auf dieses Objekt zugegriffen, entsteht ein Fatal Error mit Abbruch des Skriptes.
    Somit wird der catch-Block für die Exception in der index.php niemals erreicht.

    Der Code zum Rendern der View sollte also besser in eine separate Methode verlagert werden, die dann natürlich in jeder Action-Methode explizit aufgerufen werden muss.

    Gruß Peter

  13. Ralf Eggert sagt:

    Hallo Peter,

    genau auf das Problem bin ich vor einige Zeit auch gestossen. Mittlerweile habe ich für das automatische Rendern des Views ein Plugin geschrieben, was nur ausgeführt wird, wenn die Dispatcher Schleife wirklich abgearbeitet worden ist. Das funktioniert prima.

    Da aber an den MVC Komponenten in den letzten Wochen viel gearbeitet worden ist (erst heute wurden die Änderungen in den Core übernommen), habe ich das Tutorial bisher so belassen. Wenn das Release 0.6.0 erschienen ist (geplant ist Mitte Demzember), muss ich diese Bereiche des Tutorials noch einmal überarbeiten. Bis dahin liegt das Tutorial auch erst einmal auf Eis.

    Gruß,

    Ralf

  14. smeier sagt:

    Hallo Ralf,
    Ich weiss nicht, ob es dir hilft, aber ich möchte dich hier kurz auf ein Problem aufmerksam machen, auf welches ich beim Wechsel von ZF 0.2 auf 0.6 gestossen bin.

    Deine Travello_Controller_Action Klasse funktionierte bei der Übergabe von Requests nicht richtig, da sie die benötigten Parameter nicht übergab. Ich habe dies nun folgendermassen gelöst:

    in Travello_Controller_Action:

    public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
    {
    $this->setRequest($request)
    ->setResponse($response)
    ->_setInvokeArgs($invokeArgs)
    ->init();

    $this->_view = Zend::registry('view');

    }

    Gruss

    Stephan Meier

  15. MackAttack sagt:

    Moin!!

    Ich wollte mich an dieser Stelle auch mal bei Dir für dieses tolle Tutorial danken, man merkt und sieht, wie viel Arbeit darin steckt.

    Ich stecke noch in den Kinderschuhen, was das Framework angeht, aber mit diesem Tutorial in Verbindung mit dem Manual hab ich einen schnellen und guten Einstieg gefunden.

    Leider stehe ich gerade vor einem Problem. Ich arbeite dein Tutorial nicht eins zu eins durch, soll heissen, dass ich ein anderes Projekt zum Testen entwickle. Eben aus diesem Grund habe ich an einigen Stellen auch andere Bezeichner etc., was die ganze Sache ein wenig interessanter macht.
    Es geht um folgendes:
    Bei dem Schritt der Erweiterung des Zend_Controller_Action Objekts kriege ich nun einen Fehler "Call to a member function getParam() on a non-object", wenn ich aus meiner Action den Parameter 'id' abrufen will. Komischer sind die Klassen-Methoden aber vorhanden, wenn ich an der Stelle mit get_class_methods($this); die Methoden ausgeben lasse, werden alle Methoden des Zend_Controller_Action Objektes gelistet. Irgendwie scheint mir beim Erweitern der Klasse das Request-Objekt flöten gegangen zu sein. Hast Du oder sonst irgendwer evtl. einen Ansatz für mich, wie ich das lösen kann, bzw. wo der Fehler sitzen könnte?

    Ich sag schonmal besten Dank im Vorraus - und weiter so :-)

    Gruß,
    MackAttack

  16. MackAttack sagt:

    Hallo nochmal...

    ich denke, ich habe den Fehler gefunden...
    Durch den Konstruktor, den ich in meinem neuen Controller_Action Objekt nutze, überschreibe ich den Konstruktor des Zend_Controller_Action Objekts. Nachdem ich den Konstruktor aus dem Zend_Controller_Action in mein neues Objekt kopiert hatte, kam diese Meldung nicht mehr.

    Mich stimmt das aber dennoch etwas misstrauisch, denn scheinbar hat hier sonst keiner solche Probleme gehabt und nach Deinem Tutorial "dürfte" das alles gar nicht funktionieren. Es wäre toll, wenn man mich ein wenig ins Licht rücken könnte, ich habe nach wie vor das ungute Gefühl, irgendwas falsch zu machen (auch wenns jetzt zu funktionieren scheint)

    Gruß,
    MackAttack

  17. thomas sagt:

    Hallo MackAttack, du bist nicht der einzige mit dem Problem, bei mir kommt genau die selbe Fehlermeldung. Würde mich freuen wenn du deine Lösung für das Problem mal etwas genauer hier beschreiben könntest.

    Gruß Thomas

  18. thomas sagt:

    Mist, wer lesen kann ist klar im Vorteil :) -> einfach den orginal _constructor nehmen und den neuen überschreiben. Sorry für den Sinnlosen Post hiervor :(.

  19. MackAttack sagt:

    Moin Thomas,

    du kannst natürlich in deinem Konstruktor auch einfach parent::__construct aufrufen, wenn mich nicht alles täuscht. Sollte auch funktionieren.

    Gruß,
    MackAttack

  20. thomas sagt:

    Japp, so habe ich es gemacht und so schaut es aus

    public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
    {
    //$this->setRequest($request)
    // ->setResponse($response)
    // ->_setInvokeArgs($invokeArgs)
    // ->init();
    parent::__construct($request, $response, $invokeArgs);
    $this->_view = Zend::registry('view');
    }

  21. Diiimo sagt:

    Hallo zusammen also,

    thomas meint:
    public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
    {
    //$this->setRequest($request)
    // ->setResponse($response)
    // ->_setInvokeArgs($invokeArgs)
    // ->init();
    parent::__construct($request, $response, $invokeArgs);
    $this->_view = Zend::registry('view');
    }

    Das ist soweit richtig und funktioniert, allerdings meldet der Zend Debuger einen Fehler.
    Das Problem ist der Name der Action Klasse, er lädt an der Stelle an der Zend_Controller_Action geladen werden soll den anderen Controller geht aber davon aus das Zend_Controller_Action geladen wurde, bei einer Sicherheitsabfrage stellt er fest das Action.php geladen ist aber nicht die Klasse Zend_Controller_Action und bricht mit Exception ab. Allerdings nur im Debugmodus, lässt man das ganze auf dem Browser laufen, läuft es anscheinend normal. Keine Ahnung muss ein Seiteneffekt sein.

    Die Lösung ist die eigene Action Klasse anders zu nennen.
    Bsp. FoAction.php Klasse: EigeneLib_Controller_FoAction

    So läuft der Debuger ohne Fehler durch.

  22. Christoph sagt:

    Hallo Ralf!

    Erstmal danke für das tolle Tutorial. Ich arbeite mich gerade durch und es hilft mir sehr bei der Einarbeitung ins Zend Framework!

    Ich bin noch auf einen kleinen Fehler gestoßen der bei mir zumindest mit der Version 0.8 auftritt:

    $route1 = new Zend_Controller_Router_Route(':controller/:action/:id', array('controller' => 'article', 'action' => 'index'));

    funktioniert bei mir nicht, Ich werde weiter auf den Index Controller geroutet.

    Wenn ich noch 'id' => null dranhänge funktionierts:
    $route1 = new Zend_Controller_Router_Route(':controller/:action/:id', array('controller' => 'article', 'action' => 'index', 'id' => null));

    llg
    Christoph

  23. Bernhard sagt:

    Ich habe nun die Zend_Controller_Action-Klasse wie beschrieben erweitert und bekomme jetzt nur die Fehlermeldung: Fatal error: Call to a member function isDispatched() on a non-object in [..]\zf\Zend\Controller\Action.php on line 486. Kann es sein, das vergessen wurde, im Constructor der Ausgangsklasse aufzurufen? Es scheint, das die Instanz Request nicht existiert.

    Bernhard

  24. Bernhard sagt:

    Ich habe es selbst gelöst indem ich den Inhalt des Contructors in die Methode Init verschoben habe, die vom Vaterconstruktor aufgerufen wird. Jetzt läufts.

  25. Ole sagt:

    Hallo Bernhard,

    Du hast scheinbar 2 Wochen Vorlauf vor mir ;-)
    Hänge nun auch an dem von Dir oben beschriebenen Problem. Versteh aber nicht ganz in welche Methode init Du den Inhalt des Construktors verschoben hast. Kannst Du kurz näher erläutern? Danke!

  26. Ole sagt:

    Hallo Bernhard,

    kurz überlegt und begriffen ;-) Du hast noch die alte Construktor Methode gelöscht und nun in der init() den Inhalt des Constructors.
    Gab aber das Problem nun, dass getNavigation() private ist. Wenn die public ist funzt es.

  27. Jazeps sagt:

    I could not addHelperPath until I did var_dump(exception) for the exception thrown after ZF could not load my helper.
    It turned out that you have to add extra parameter to addHelperPath to load helpers properly: check out ZF API documentation.

    if your helper class is called Vais_View_Helper_MyHelper, then you have to use this addHelperPath line:
    $view->addHelperPath('./application/views/helpers','Vais_View_Helper');

    This is as of ZF 0.9.2.

  28. Maik sagt:

    Ich hab nun noch ein ganz anderes Problem:

    Im Destructor der abstrahierten Zend_Controller_Action Klasse (meine heißt Custom_Controller_Action) springt mein PHP-Parser anscheinend auf einmal in das SERVER_ROOT, hier C:\Programme\Apache2. Das ist echt seltsam.

    Da ich meine eigene Template-Engine gebaut habe, die Custom_View_Smarty heißt und von Zend_View abstrahiert wurde, verwende ich als Template-Datei-Erweiterung .html. Mein Destructor in Custom_Controller_Action sieht so aus:

    public function __destruct()
    {
    echo getcwd();
    echo $this->_view->render($this->_template);
    }

    Hier der Konstruktor:

    public function __construct(Zend_Controller_Request_Abstract $request,
    Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
    {
    parent::__construct($request, $response, $invokeArgs);

    $this->_view = Zend_Registry::get('view');
    $this->_view->navigation = $this->getNavigation();
    }

    Die Ausgabe im Browser zeigt das:

    C:\Programme\Apache2
    Warning: Smarty error: unable to read resource: "main.html" in C:\wwwroot\htdocs\blog\library\smarty\Smarty.class.php on line 1095

    Ich zweifel langsam an meinem Verstand.... Hat das Phänomen noch einer beobachten können?

  29. Maik sagt:

    Ich habe einen Workarround für das von mir eben beschriebene Problem geschrieben:

    public function __destruct()
    {
    chdir($_SERVER['DOCUMENT_ROOT']);
    echo $this->_view->render($this->_template);
    }

    Das erklärt aber nicht, warum ein Script in den Server-Root springt. Normal ist das nicht!

  30. Maik sagt:

    Noch mal ich, sorry, das ich hier schon rum nerve ;-)

    Mein Problem scheint nicht nur mich zu betreffen, wie das PHP-Manual und ein Bug-Report belegt:

    http://www.php.net/manual/en/language.oop5.decon.php#70928
    http://bugs.php.net/bug.php?id=34206

    Also war meine Lösung genau richtig für dieses Problem.

  31. Jürgen Mutwalek sagt:

    Also um die Standard-Route zu ändern, bönitigt man in der aktuellen Version folgenden Code:

    $route = new Zend_Controller_Router_Route_Static(
    '',
    array(

    'controller' => 'user',
    'action' => 'profil'
    )
    );

    $router->addRoute('default', $route);

    Man muss den Default-Router überschreiben.

    Gruß Jürgen

  32. Carsten sagt:

    Vielen Dank für diesen Beitrag, der hat mir sehr weitergeholfen (und die Umstehenden Dinge vor Wutausbrüchen gerettet ;-))

Hinterlasse eine Antwort