RAD im Zend Framework mit dem zf-Tool

Nachdem ich in den vorherigen Beiträgen gezeigt habe, wie man eine Webapplikation erstellt und sie dann konfiguriert, will ich nun das zf Kommandozeilen-Tool näher beleuchten. Die Dokumentation dazu ist bisher sehr dünn bis nicht existent.

Voraussetzungen für die Nutzung

Nachdem man das Zend Framework Paket heruntergeladen hat muss man es entpacken, inklusive des bin-Ordners. Diesen muss man dann in der Pfad-Variable PATH (unter Windows) bekannt machen, damit man überall Zugriff auf das Kommando zf hat. Dazu muss der library-Ordner in der Struktur wie im Paket zu finden sein.

Als Probe muss man in der Eingabeaufforderung folgendes Kommando eingeben:

$> zf show version

Wenn kein Fehler kommt, dann ist das zf-Tool korrekt verfügbar.

A. Anlegen eines Projektes

Damit wird eine komplette Projektstruktur erstellt vom Wurzelverzeichnis angefangen. Siehe meinem ersten Beitrag.
Grundlegende Aufrufstruktur:

$> zf create project {ProjectName}

Konkreter Beispielaufruf

$> zf create project zf-app

Dieser Befehl erzeugt eine leere Projektverzeichnisstruktur mit diversen Grunddaten. Für die weitere Bearbeitung der Struktur mit dem zf-cli-Tool sollte man in das Applikations-/Projektverzeichnis wechseln.

B. Anlegen eines Moduls

Für die sinnvolle Teilung von anwendungsfallbezogenen Bereichen gibt es Module im Zend Framework. Ein Modul ist verzeichnisbezogen ein separates Verzeichnis unterhalb von application und dessen Unterordner modules. Es beinhaltet wiederum Unterordner für Controller, Views, Models und Formularen. Es ist quasi eine Applikation in der Applikation etwas überspitzt gesagt.

Grundlegende Aufrufstruktur:

$> zf create module {ModuleName}

Konkreter Beispielaufruf:

$> zf create module Modulname

Als Antwort erhält man folgendes:

Creating the following module and artifacts:
C:\workspace\zf-app/application/modules/Modulename/controllers
C:\workspace\zf-app/application/modules/Modulename/models
C:\workspace\zf-app/application/modules/Modulename/views
C:\workspace\zf-app/application/modules/Modulename/views/scripts
C:\workspace\zf-app/application/modules/Modulename/views/helpers
C:\workspace\zf-app/application/modules/Modulename/views/filters
Updating project profile 'C:\workspace\zf-app/.zfproject.xml'

Dieser Aufruf erzeugt folgende Verzeichnisstruktur unterhalb von application:

modules
└───Modulename
	├───controllers
	├───models
	└───views
		├───filters
		├───helpers
		└───scripts

Um nun auch dieses neue Modul nutzen zu können muss die application.ini erweitert werden um folgenden Eintrag:

resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"

Erst nachdem dieser Parameter gesetzt ist, wird auch das Verhalten des Zend Frameworks dahingehend geändert, dass die Applikation modulbasiert funktioniert.
D.h. ein Aufruf der URL zf-app.loc/Modulname ohne den Konfigurationsparameter führt zu folgender Fehlermeldung:

An error occurred
Page not found
Exception information:

Message: Invalid controller specified (Modulname)
Stack trace:

#0 C:\workspace\zf-app\library\Zend\Controller\Front.php(954): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http))
#1 C:\workspace\zf-app\library\Zend\Application\Bootstrap\Bootstrap.php(97): Zend_Controller_Front->dispatch()
#2 C:\workspace\zf-app\library\Zend\Application.php(366): Zend_Application_Bootstrap_Bootstrap->run()
#3 C:\workspace\zf-app\public\index.php(26): Zend_Application->run()
#4 {main}  

Request Parameters:

array (
  'controller' => 'Modulname',
  'action' => 'index',
  'module' => 'default',
)

Sobald der Parameter gesetzt ist kommt folgende Fehlermeldung:

An error occurred
Page not found
Exception information:

Message: Invalid controller specified (index)
Stack trace:

#0 C:\workspace\zf-app\library\Zend\Controller\Front.php(954): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http))
#1 C:\workspace\zf-app\library\Zend\Application\Bootstrap\Bootstrap.php(97): Zend_Controller_Front->dispatch()
#2 C:\workspace\zf-app\library\Zend\Application.php(366): Zend_Application_Bootstrap_Bootstrap->run()
#3 C:\workspace\zf-app\public\index.php(26): Zend_Application->run()
#4 {main}  

Request Parameters:

array (
  'module' => 'Modulname',
  'controller' => 'index',
  'action' => 'index',
)

Der Unterschied ist in den „Request Parameters“ -> „module“ und „controller“ zu sehen.
WICHTIG: Unter Windows ist ein großer Anfangsbuchstabe ein Problem bei dem Pfad zum Modul. Also unbedingt nachträglich das Verzeichnis „Modulname“ in „modulname“ umbenennen, andernfalls kommt statt der korrekten Seite folgende Exception:

An error occurred
Page not found
Exception information:

Message: Invalid controller specified (index)
Stack trace:

#0 C:\DATA\workspace\zf-app\library\Zend\Controller\Front.php(954): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http))
#1 C:\DATA\workspace\zf-app\library\Zend\Application\Bootstrap\Bootstrap.php(97): Zend_Controller_Front->dispatch()
#2 C:\DATA\workspace\zf-app\library\Zend\Application.php(366): Zend_Application_Bootstrap_Bootstrap->run()
#3 C:\DATA\workspace\zf-app\public\index.php(26): Zend_Application->run()
#4 {main}  

Request Parameters:

array (
  'module' => 'machines',
  'controller' => 'index',
  'action' => 'index',
)

Seit Version 1.11.0 fügt das zf-Tool folgende Einstellung eigenständig in die application.ini hinzu:

resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"

C. Anlegen eines Controllers

Zum Anlegen eines Controllers sollte man sich zuvor bewußt sein, wo dieser Controller angelegt werden soll: im default-Module (direkt unter application/controllers) oder in einem zuvor erstellten Modul.
Grundlegende Aufrufstruktur:

$> zf create controller {ControllerName}[(-m |--module=){ModuleName}]

Der Aufruf zum Erstellen im default-Module / entspricht auch dem Aufruf, wenn man keine modulbasierte Applikation hat:

$> zf create controller Index

Dies legt einen IndexController in application/controllers/IndexController.php an.
Der Aufruf für einen Controller innerhalb eines Moduls (das verändert auch den Klassennamen des Controllers und stellt den Modulnamen voran als Pseudo-Namensraum):

$> zf create controller Index -m Modulname

oder

$> zf create controller Index --module=Modulname

Dies erstellt den Modulname_IndexController in application/modules/Modulname/controllers/IndexController.php.
Folgende Antwort kommt nach dem Aufruf:

Creating a controller at C:\workspace\zf-app/application/modules/Machines/controllers/IndexController.php
Creating an index action method in controller Index
Creating a view script for the index action method at C:\workspace\zf-app/application/modules/Machines/views/scripts/index/index.phtml
Creating a controller test file at C:\workspace\zf-app/tests/application/controllers/IndexControllerTest.php
Updating project profile 'C:\workspace\zf-app/.zfproject.xml'

WICHTIG: Dies erstellt die Klassenhülle und eine leere indexAction().
Diese Action braucht also nicht gesondert erstellt werden. Ebenfalls wird das View-Skript index.phtml für die indexAction erzeugt.

D. Anlegen einer Action

Um einem Controller eine Action zu geben führt folgendes Kommando zum Ziel:
Grundlegende Aufrufstruktur:

$> zf create action {actionName} [(-c |--controller-name=|){ControllerName}][ {ModuleName}]

Konkreter Beispielaufruf:

$> zf create action login Index

Folgende Ausgabe liefert das Kommando:

Creating an action named login inside controller at C:\workspace\zf-app/application/controllers/IndexController.php
Updating project profile 'C:\workspace\zf-app/.zfproject.xml'
Creating a view script for the login action method at C:\workspace\zf-app/application/views/scripts/index/login.phtml
Updating project profile 'C:\workspace\zf-app/.zfproject.xml'

Dadurch wird die Action im Controller erzeugt und das dazugehörige View-Skript.

E. Anlegen eines Views

Zum separaten Anlegen eines View-Skriptes wird folgendes Kommando notwendig:
Grundlegende Aufrufstruktur:

$> zf create view [-c ]{ControllerName} [-a ]{view-script-name}

Konkreter Beispielaufruf:

$> zf create view Index logout

Als Ergebnis erscheint folgendes:

Updating project profile 'C:\workspace\zf-app/.zfproject.xml'

Leider steht nicht, dass ein View-Skript angelegt wurde, aber das wurde es trotzdem.
WICHTIG: Es wird lediglich das View-Skript selbst angelegt. Die Verwendung durch eine Action muss selbst umgesetzt werden. Bspw. kann nachträglich nochmal eine Action dazu erstellt werden mittels „zf create action logout Index„. Dies überschreibt allerdings das vorhandene View-Skript mit dem vordefinierten View-Skript. Das ist unschön und sollte beachtet werden.

F. Anlegen eines Models

Für die Implementierung der Businesslogik ist nun alles weitestgehend vorbereit. Lediglich die Akteure und Datenlieferanten [I] fehlen noch. Beginnen wir mit den Akteuren, die Businesslogik/-informationen beinhalten und einen Zugriff per Methoden ermöglichen.
Ein Model sollte eine Objektinstanz widerspiegeln und deswegen mit der Einzahl benamt werden.
Grundlegende Aufrufstruktur:

$> zf create model {ModelName}[-m {ModuleName}]

Konkreter Beispielaufruf:

$> zf create model Option -m system

Dieser Aufruf liefert folgende Ausgabe:

Creating a model at C:\workspace\zf-app/application/modules/system/models/Option.php
Updating project profile 'C:\workspace\zf-app/.zfproject.xml'

Als Ergebnis haben wir folgendes Klassenkonstrukt:

<?php

class system_Model_Option
{

}

Dieses Model kann nun mit Methoden der Businesslogik weiter komplettiert werden. Zum Laden/Speichern von Daten aus einer Datenquelle (meist Datenbank) benötigen wir noch ein DbTable-Objekt passend zum Model. Dies wird in [I] erklärt.

G. Anlegen eines Formulars

Für die Generierung eines Formulars gibt es folgenden Befehl:
Grundlegende Aufrufstruktur:

$> zf create form {FormName}[-m {ModuleName}]

Konkreter Beispielaufruf:

$> zf create form Auth

Daraufhin wird folgendes Ergebnis gezeigt:

Creating a form at C:\workspace\zf-app/application/forms/Auth.php
Updating project profile 'C:\workspace\zf-app/.zfproject.xml'

Mit diesem Aufruf wird eine Formularklasse Application_Form_Auth unter application/forms/Auth.php erstellt.

H. Einrichten einer Datenbankverbindung

Das zf-cli-Tool bietet nur wenige Möglichkeiten, um komfortabel Konfigurationseinstellungen an der application.ini vorzunehmen. Eine davon ist das Aktivieren der Layoutnutzung [J], eine andere ist die Konfiguration einer Datenbankverbindung.
Grundlegende Aufrufstruktur:

$> zf configure dbadapter "adapter={Adapter}&username={Username}&password={Password}&dbname={DbName}"[ [-s ]{SectionName}]

Konkrete Beispielaufrufe:

$> zf configure dbadapter "adapter=Pdo_Mysql&username=root&password=&dbname=zfapp" -s development

Liefert folgende Ausschrift:

A db configuration for the development section has been written to the application config file.
$> zf configure dbadapter "adapter=Pdo_Mysql&username=root&password=&dbname=transfer"

Liefert dagegen folgende Ausschrift:

A db configuration for the production section has been written to the application config file.

I. Anlegen eines DbTable-Objektes als Verbindung zwischen Model und Datenbank

Zum Laden, Aktualisieren und Speichern von datenbankbezogenen Werten mittels Model wird die Strukturkomponente „DbTable“ benötigt. Alternativ kann auch ein anderen Datenlieferant (Provider) genutzt werden – meist ist aber eine Datenbank im Hintergrund.
Grundlegende Aufrufstruktur:

$> zf create dbtable {DbTableName/ModelName}[-a] {TableNameInDatabase}[-m {ModulName}][-f|--force-overwrite]

Damit wird ein DbTable Objekt mit dem korrekten Tabellennamen erstellt. Für eine Aktualisierung dieser Daten kann dies mit dem „–force-overwrite“ Parameter erzwungen werden.
Alternativ kann mit

$> zf [-p] create dbtable.from-database

geschaut werden, welche Tabellen gefunden und welche als DbTable erstellt werden. Der Parameter „-p“ zeigt lediglich an, was getan werden würde, tut dies aber nicht.
Auf diesen Automatismus wird hier nicht weiter eingegangen.
Konkreter Beispielaufruf:

$> zf create dbtable Options options -m system

Dies liefert folgende Ausschrift:

Creating a DbTable at C:\workspace\zf-app/application/modules/system/models/DbTable/Options.php
Updating project profile 'C:\workspace\zf-app/.zfproject.xml'

Erstellt wurde folgendes Klassenkonstrukt:

class system_Model_DbTable_Options extends Zend_Db_Table_Abstract
{

	protected $_name = 'options';

}

Diese Zend_Db_Table-Kindklasse kann mit weiteren Properties entsprechend der Tabellendefinition parametrisiert werden.
Für die Verbindung zwischen Model[F] und DbTable[I] benötigt man einen Mapper [K].

J. Nutzen eines Layouts

Für die einheitliche Darstellung in HTML durch einen gewissen Rahmen, der wiederverwendbare Teilbereiche beinhaltet gibt es das Layout im Zend Framework. Um dieses zu verwenden gibt es folgenden Befehl:

$> zf enable layout

Das Ergebnis sieht folgendermaßen aus:

Layouts have been enabled, and a default layout created at C:\workspace\zf-app/application/layouts/scripts/layout.phtml
A layout entry has been added to the application config file.

Es wurde in application/layouts/scripts ein Layout namens layout.phtml angelegt. Zur Nutzung/Aktivierung des Layouts wurde folgender Konfigurationseintrag in der application.ini ergänzt:

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

Zur Deaktivierung gibt es übrigens:

$> zf disable layout

Dieser Befehl löscht allerdings keine Dateien/Konfigurationen, sondern entfernt lediglich den Hinweis in der .zfproject.xml Datei.
WICHTIG: Eine Reaktivierung des Layouts überschreibt ohne Rückfrage die layout.phtml. Dies ist unschön und sollte beachtet werden.

K. Erstellen eines Mappers zur Verbindung von Model und DbTable

Als Voraussetzung muss ein Model[F] und ein DbTable[I] vorhanden sein. Zur Verbindung dieser beiden völlig eigenständigen Objektklassen wird der Mapper erstellt, der fallbezogen mal mit dem einen, mal mit dem anderen, mal mit beiden Objekten und/oder anderen Objekten zusammen arbeitet.
Den Mapper legen wir ebenfalls in das Models-Verzeichnis. Sonst gibt es Probleme mit der Namens-zu-Klassenauflösung mit dem Auto-Loading-Prozess.
Ganz konkret sieht das bspw. so aus (aus dem Quickstart):

class Application_Model_GuestbookMapper
{
	protected $_dbTable;

	public function setDbTable($dbTable)
	{
		if (is_string($dbTable)) {
			$dbTable = new $dbTable();
		}
		if (!$dbTable instanceof Zend_Db_Table_Abstract) {
			throw new Exception('Invalid table data gateway provided');
		}
		$this->_dbTable = $dbTable;
		return $this;
	}

	public function getDbTable()
	{
		if (null === $this->_dbTable) {
			$this->setDbTable('Application_Model_DbTable_Guestbook');
		}
		return $this->_dbTable;
	}

	/**
	 * take the Model to retrieve data/information and use them
	 * to let the DbTable do the insert/update action
	 * @param Application_Model_Guestbook $guestbook
	 * @return void
	 */
	public function save(Application_Model_Guestbook $guestbook)
	{
		$data = array(
			'email'   => $guestbook->getEmail(),
			'comment' => $guestbook->getComment(),
			'created' => date('Y-m-d H:i:s'),
		);

		if (null === ($id = $guestbook->getId())) {
			unset($data['id']);
			$this->getDbTable()->insert($data);
		} else {
			$this->getDbTable()->update($data, array('id = ?' => $id));
		}
	}

	/**
	 * use the DbTable to load the data for given primary key and
	 * return a Model instance
	 * @param int $id
	 * @return Application_Model_Guestbook
	 */
	public function find($id)
	{
		$result = $this->getDbTable()->find($id);
		if (0 == count($result)) {
			return;
		}
		$row = $result->current();
		$guestbook = new Application_Model_Guestbook();
		$guestbook->setId($row->id)
				  ->setEmail($row->email)
				  ->setComment($row->comment)
				  ->setCreated($row->created);
		return $guestbook;
	}

	/**
	 * retrieve a list of models
	 * @return array of Application_Model_Guestbook
	 */
	public function fetchAll()
	{
		$resultSet = $this->getDbTable()->fetchAll();
		$entries   = array();
		foreach ($resultSet as $row) {
			$entry = new Application_Model_Guestbook();
			$entry->setId($row->id)
				  ->setEmail($row->email)
				  ->setComment($row->comment)
				  ->setCreated($row->created);
			$entries[] = $entry;
		}
		return $entries;
	}
}

Die direkte Nutzung kann dann bspw. in einem Controller [C] erfolgen. Dies wird hier ebenfalls gezeigt.

class GuestbookController extends Zend_Controller_Action
{
	/**
	 * show all guestbook entries
	 */
	public function indexAction()
	{
		// hier den Mapper rufen, anstatt das Model,
		// weil wir nicht eine Instanz wollen, sondern
		// eine Liste von Instanzen
		$guestbook = new Application_Model_GuestbookMapper();
		$this->view->entries = $guestbook->fetchAll();
	}

	/**
	 * insert (or update) an entry
	 */
	public function signAction()
	{
		$request = $this->getRequest();
		$form    = new Application_Form_Guestbook();

		if ($this->getRequest()->isPost()) {
			if ($form->isValid($request->getPost())) {
				//	the constructor has to set the values to internal properties
				$comment = new Application_Model_Guestbook($form->getValues());
				//	the mapper is the communication level to the data storage: database
				$mapper  = new Application_Model_GuestbookMapper();
				//	the mapper saves the model, we do not need any kind of DbTable
				//	object as direct instance
				$mapper->save($comment);

				return $this->_helper->redirector('index');
			}
		}

		$this->view->form = $form;
	}
}

Damit werden die Daten vom Formular in das Model übertragen und der Mapper speichert diese Daten, in dem er sie sich wieder aus dem Model holt.
Sparer würden jetzt eventuell gern abkürzen und dem Mapper gleich die Daten aus dem Formular – komplett ohne Model übergeben. In dem einfachen Beispiel mag das sogar gehen, aber der wichtige Punkt Businesslogik würde dann komplett umgangen werden.
In unserem Beispiel könnte im Guestbook-Model noch eine Blacklist auf die E-Mail-Adresse und eine Bewertung des Kommentars vollzogen werden, der weder im Formular selbst, noch etwas in der Datenbank zu suchen hat.
Außerdem könnte eine Logikimplementierung ebenfalls einen Versand von einer Benachrichtigung auslösen. Diese sollte im Model gestartet werden und nicht im Controller/Action, damit das Model nicht nur im MVC Umfeld korrekt funktioniert, sondern vielleicht auch per API/Shell oder anderen Aufrufmöglichkeiten. Wichtig für den Versand der Benachrichtigung ist dabei nicht die Umgebung, in der erfasst wird, sondern viel mehr, dass erfasst wird.

2 Gedanken zu „RAD im Zend Framework mit dem zf-Tool

  1. Hey, vielen Dank für die Auflistung.
    Ich habe mir auch gerade ein ZF-Projekt mit dem ZF-Tool eingerichtet und hatte mich gewundert, dass beim „disable layout“ die Dateien erhalten bleiben. Hier fand ich also die Info dazu.
    Danke noch mal!

    Gruß,
    Christian

Kommentare sind geschlossen.