CakePHP Tutorial: Views, Layouts und Helpers

CakePHP Tutorial: Views, Layouts und Helpers
Von Lars Ebert am 27.05.13, 11:00
Kategorien: CakePHP, PHP, Programmieren and Tutorials

Im letzten Teil dieser Artikelserie haben wir uns mit den Grundfunktionen von CakePHP vertraut gemacht. Nun wollen wir uns etwas tiefer in die Höhle des Löwen wagen und uns mit Views, Layouts und Helpers beschäftigen. Dazu bauen wir in diesem Artikel eine kleine, statische Webseite auf. Am Ende sollten wir eine solide Grundlage für den Schritt in die Dynamik der Applikation haben.

Wir schauen uns zuerst das HTML-Design an, woraus wir dann ein Layout machen, dann fügen wir mit Views den Inhalt hinzu und zuletzt erstellen wir noch einen Helper für die Navigation. Viel Spaß!

Artikelserie: CakePHP-Tutorial: Web-Anwendung mit MVC

Dieser Artikel ist Teil einer mehrteiligen Artikelserie. Lies dir auch die restlichen Teile durch!

  1. CakePHP Tutorial: Grundfunktionen und Hallo Welt
  2. CakePHP Tutorial: Views, Layouts und Helpers
  3. CakePHP Tutorial - Model und Controller - ein Gästebuch programmieren
  4. CakePHP Tutorial: Admin-Routen und Login - erweitertes Gästebuch
  5. CakePHP Tutorial: Epsilon-Greedy-Tests - ein Plugin erstellen

Das HTML-Design

Um aus einem HTML-Design eine CakePHP-App zu erstellen, brauchen wir natürlich zunächst ein Design. Freundlicherweise hat René Winkelmann vom Numboo-Magazin ein Template bereitgestellt, vielen Dank an dieser Stelle! Ihr könnt euch die fertige Version des HTML-Designs hier herunterladen.

Um in CakePHP nun Zugriff auf die CSS-Datei und die Bilder zu haben, kopieren wir die vier Ordner aus dem Archivs in den Webroot-Ordner. Die vier Ordner, die bereits im Webroot-Ordner liegen, können wir getrost löschen, sie gehören nur zur Standard-Demoseite von CakePHP. Wenn alle Dateien an Ort und Stelle sind, können wir mit dem Grundlayout, welches auf jeder Seite zu finden sein wird, beginnen.

Die Konfiguration

Bevor wir jedoch mit dem HTML-Code beginnen, passen wir zunächst die Konfiguration unserer App etwas an. Dazu öffnen wir die Datei Config\core.php. Ich würde hier aus Gründen der Lesbarkeit alle Kommentare zunächst einmal in Ruhe durchlesen und dann entfernen.

<?php
	
	Configure::write('debug', 2);
	
	Configure::write('Error', array(
		'handler' => 'ErrorHandler::handleError',
		'level' => E_ALL & ~E_DEPRECATED,
		'trace' => true
	));
	Configure::write('Exception', array(
		'handler' => 'ErrorHandler::handleException',
		'renderer' => 'ExceptionRenderer',
		'log' => true
	));
	
	Configure::write('App.encoding', 'UTF-8');
	Configure::write('Session', array(
		'defaults' => 'php'
	));
	
	Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi');
	Configure::write('Security.cipherSeed', '76859309657453542496749683645');
	
	Configure::write('Acl.classname', 'DbAcl');
	Configure::write('Acl.database', 'default');
	
	$engine = 'File';
	
	$duration = '+999 days';
	if (Configure::read('debug') > 0) {
		$duration = '+10 seconds';
	}
	
	$prefix = 'myapp_';
	
	Cache::config('_cake_core_', array(
		'engine' => $engine,
		'prefix' => $prefix . 'cake_core_',
		'path' => CACHE . 'persistent' . DS,
		'serialize' => ($engine === 'File'),
		'duration' => $duration
	));
	
	Cache::config('_cake_model_', array(
		'engine' => $engine,
		'prefix' => $prefix . 'cake_model_',
		'path' => CACHE . 'models' . DS,
		'serialize' => ($engine === 'File'),
		'duration' => $duration
	));

Mithilfe der Configure-Klasse folgt die Konfiguration von CakePHP. Mit Configure::write() können wir Konfigurations-Variablen schreiben und mit Configure::read() auch wieder auslesen.

Als erstes sollten wir die Werde Security.salt und Security.cipherSeed anpassen! Einfach nur aus Sicherheitsgründen. So stand es zumindest bis gerade eben in den Kommentaren.

Jetzt können wir nach Herzenslust neue Konfigurations-Variablen schreiben. Ich habe zum Beispiel noch folgende Zeilen ergänzt:

	Configure::write('Email.from', 'info@advitum.de');
	Configure::write('Email.to', 'info@advitum.de');
	
	Configure::write('Config.site_title', 'Copedio');
	Configure::write('Config.long_title', 'Copedio - Deine Seite für irgendwas, wasweißich!');
	Configure::write('Config.description', 'Auf Copedio gibt es bisher absolut gar nichts zu sehen!');
	Configure::write('Config.keywords', 'Copedio,nichts,langweilig,demo');
	Configure::write('Config.language', 'de');

Wozu diese im Detail dienen, werden wir gleich sehen!

Das Layout

Wenn ein View gerendert werden soll, greift CakePHP (wenn nicht anders definiert) auf das Default-Layout zu. Dieses finden wir in der Datei View/Layouts/default.ctp.

Aus meiner HTML-Datei habe ich für CakePHP folgenden Code gemacht.

<?php
    
    if(!isset($this->pageTitle) || $this->pageTitle == '') {
        $this->pageTitle = Configure::read('Config.long_title');
    } else {
        $this->pageTitle .= ' - ' . Configure::read('Config.site_title');
    }
    
    if(!isset($this->pageDescription) || $this->pageDescription == '') {
        $this->pageDescription = Configure::read('Config.description');
    }
    
    if(!isset($this->pageKeywords) || $this->pageKeywords == '') {
        $this->pageKeywords = Configure::read('Config.keywords');
    }
    
?>
<!DOCTYPE html>

<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->

<head>
	<?php echo $this->Html->charset(); ?>
	<title>
		<?php echo $this->pageTitle; ?>
	</title>
	<?php
		echo $this->Html->meta('icon');
		
		echo $this->fetch('meta');
		echo $this->fetch('css');
		echo $this->fetch('script');
		
		echo $this->Html->meta(
            'keywords',
            $this->pageKeywords
        );
        echo $this->Html->meta(
            'description',
            $this->pageDescription
        );
        echo $this->Html->meta(array('name' => 'author', 'content' => 'Lars Ebert'));
		echo $this->Html->meta(array('name' => 'viewport', 'content' => 'width=device-width'));
        
        echo $this->Html->css('main');
	?>
</head>

<body<?php if(isset($this->bodyClass)) { echo ' class="' . h($this->bodyClass) . '"'; } ?>>
	<div id="root">
		<!--[if lt IE 7]>
		<p class="chromeframe">Sie verwenden einen veralteten Browser. Bitte <a href="http://browsehappy.com/">laden Sie einen aktuellen Browser</a> oder <a href="http://www.google.com/chromeframe/?redirect=true">aktivieren Sie Google Chrome Frame</a>, um gut und sicher zu surfen.</p>
		<![endif]-->
		
		<header>
			<div class="container">
				<a href="#" id="logo"><strong>Cope</strong>dio</a>
				<nav id="mainMenu">
					<a href="#">Home</a>
					<a href="#">Beispiele</a>
					<a href="#">Blog</a>
					<a href="#">Kontakt</a>
				</nav>
				<img id="eyeCatcher" src="img/header.jpg" />
			</div>
		</header>
		
		<section id="content" class="container">
			<?php echo $this->fetch('content'); ?>
			<div id="root_footer"></div>
		</section>
	</div>
	
	<div id="footer">
		<footer>
			<div class="container">
				<div class="columns">
					<div class="column">
						Footer Spalte 1
					</div>
					<div class="column">
						Footer Spalte 2
					</div>
					<div class="column">
						Footer Spalte 3
					</div>
				</div>
			</div>
		</footer>
	</div>
</body>

</html>

Am besten schauen wir uns den Code jetzt Schritt für Schritt an und schauen, was jeweils passiert.

In den ersten 17 Zeilen frage ich einige Eigenschaften des Views an. Mit der Variable $this können wir im gesamten Layout (und auch in Views) auf das jeweilige View-Objekt zugreifen. Durch diese paar Zeilen kann ich später im View mit $this->pageTitle = 'fubar'; den Seitentitel definieren. Wie man sieht, greife ich hier auch wieder auf meine Konfigurations-Variablen zu. Wenn ein Titel definiert wurde, hänge ich einfach noch den Wert aus Config.site_title an, wenn kein Titel definiert ist, wird einfach Config.long_title verwendet. Ähnlich gehe ich auch bei Beschreibung und Schlüsselwörtern vor.

Anschließend kommen der Doctype und ein paar HTML-Tags. In Zeile 26 lasse ich über den HtmlHelper von CakePHP den Zeichensatz ausgeben. Hier siehst du auch, wie wunderbar einfach man auf Helper zugreifen kann! Der HtmlHelper ist verfügbar unter $this->Html, der FormHelper zum Beispiel ist verfügbar unter $this->Form.

Als nächstes gebe ich im Title-Tag den vorbereiteten Seiten-Titel aus.

In Zeile 33-35 lasse ich CakePHP alle CSS-, JavaScript- und Meta-Tags ausgeben, die etwaige Plugins generiert haben könnten. Dazu aber später mehr!

Nun geben wir die Keywords und Description aus, wieder benutzen wir den HtmlHelper. Außerdem geben wir noch ein paar weitere Meta-Tags aus. Zuguterletzt binden wir noch unsere CSS-Datei main.css ein. Sehr schön ist hier, dass CakePHP in Zeile 48 automatisch aus main den Pfad /css/main.css macht.

In Zeile 52 öffnen wir nun den Body-Tag und sorgen wieder dafür, dass gegebenenfalls im View eine Klasse für diesen definiert werden kann. CakePHP bietet übrigens, wie man hier sieht, eine Abkürzung für die Funktion htmlspecialchars() an, diese lautet hier einfach h().

Das restliche Markup besteht, bis auf Zeile 72, in welcher wir den Inhalt aus dem View einbinden, lediglich aus ganz normalem HTML. Bis hierhin also nicht spektakuläres.

Wenn wir diese Datei nun so speichern und uns unsere Webseite noch einmal anschauen, sehen wir nun den View aus dem letzten Teil der Serie in neuem Gewand. Wenn wir diese Datei nun so speichern und uns unsere Webseite noch einmal anschauen, sehen wir nun den View aus dem letzten Teil der Serie in neuem Gewand.

Routing in CakePHP

Woher weiß CakePHP überhaupt, welche Action in welchem Controller aufgerufen wird, wenn eine bestimmte URL aufgerufen wird. Die Antwort sind Routes.

Mit Routes kann jeder beliebigen URL eine beliebige Action übergeben werden. Routes sind sehr flexibel, denn man kann auch URL-Patterns erstellen oder Parameter aus URLs extrahieren und an den Controller übergeben.

Im Core von CakePHP werden schon einige nützliche Routes definiert. So kann zum Beispiel mit der Url /Controller/action jede beliebige Aktion in jedem Controller angesprochen werden. Das ist quasi die Grundlegende Route, wenn auf die URL keine andere Route angewendet werden kann, wird standardmäßig der Controller und die Action, die in der URL definiert wurden, aufgerufen.

Die Konfiguration von Routes erfolgt in der Datei Config\routes.php. Hier finden sich auch wieder sehr viele nützliche Hinweise in den Kommentaren, ließ sie dir am besten einmal durch.

Wenn man alle Kommentare entfernt, sieht die Datei so aus:

<?php
	
	Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
	Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display'));
	
	CakePlugin::routes();
	
	require CAKE . 'Config' . DS . 'routes.php';

Beim Aufruf einer URL geht CakePHP alle definierten Routes der Reihe nach durch und prüft, ob sie sich auf die URL anwenden lässt. Sobald er eine passende Route findet, wird der entsprechende Controller aufgerufen.

Standardmäßig sind bereits zwei Routes definiert. Die erste Route in Zeile 3 sorgt dafür, dass die Action display im PagesController mit dem Parameter 'home' aufgerufen wird, wenn die URL / aufgerufen wird. Die nächste Route sorgt dafür, dass ein Aufruf von zum Beispiel /pages/impressum einen Aufruf der Display-Action im PagesController auslöst und an die Action 'impressum' übergeben wird. Der Controller würde dann versuchen, die Datei View/Pages/impressum.ctp darzustellen, ähnlich wie er es mit der Datei View/Pages/home.ctp tut. Hier sehen wir auch schon das erste Beispiel für eine flexible Route. Ohne diese Route müssten wir jeder Seite eine statische URL zuordnen.

Die Zeilen 6 und 8 laden die Standard-Routes von CakePHP und Routes, die Plugins eventuell definiert haben.

Wir modifizieren hier unsere zweite Route ein kleines bisschen, da ich nicht das Schlüsselwort »pages« in allen URLs habe möchte:

<?php
	
	Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
	Router::connect('/*', array('controller' => 'pages', 'action' => 'display'));
	
	CakePlugin::routes();
	
	require CAKE . 'Config' . DS . 'routes.php';

Views für eine Seite erstellen

Im letzten Teil der Artikelserie habe ich die Views schon angesprochen, dies will ich hier noch einmal kurz aufgreifen.

Views sind eigentlich relativ unspektakulär, wir können in ihnen beliebigen HTML- oder PHP-Code platzieren, der ausgeführt und dann zusammen mit dem Layout ausgegeben wird.

Jetzt können wir unserem Home-View erst einmal etwas mehr Inhalt geben:

<p>
	Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.
	Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis,
	ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo,
	fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae,
	justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum
	semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend
	ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus
	varius laoreet. Quisque rutrum.
</p>
<div class="columns">
	<div class="column">
		<h2>Überschrift H2</h2>
		<p>
			Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean
			massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec
			quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
			Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
		</p>
	</div>
	<div class="column">
		<h2>Überschrift H2</h2>
		<p>
			Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean
			massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec
			quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
			Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
		</p>
	</div>
	<div class="column">
		<h2>Überschrift H2</h2>
		<p>
			Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean
			massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec
			quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.
			Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
		</p>
	</div>
</div>
<figure>
	<?php echo $this->Html->image("/img/copedio.jpg"); ?>
	<figcaption>
		Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi.
		Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget.
	</figcaption>
</figure>

Für unsere kleine Demo sollte der Inhalt reichen. Übrigens habe ich hier in Zeile 41 den HtmlHelper für die Einbindung der Grafik benutzt, dazu aber später mehr! Ich lege auch direkt noch eine zweite Seite an!

<h1>Beispiele</h1>

<p>
	Hier könnten nun Beispiele stehen.
</p>

Rufen wir nun die Url /beispiele auf, sehen wir die Inhalte unseres neuen Views. Doch es gibt noch ein Problem: Die Navigation funktioniert noch nicht.

Wir könnten jetzt natürlich einfach das Layout ändern und die Navigation anpassen, aber es gibt einen viel einfacheren Weg, eine Navigation umzusetzen.

Der NavHelper

Für die Navigation erstellen wir jetzt einen eigenen Helper. Helper sind in CakePHP dazu da, um Funktionen, die man in mehreren Views teilen möchte, zu bündeln. Der Vorteil ist, dass wir den Helper später einfach in ein neues Projekt kopieren können und so den Code für die Generierung der Navigation wiederverwenden können. Aber schon in einem einzelnen Projekt kann uns der Helper von Vorteil sein, wenn wir zum Beispiel mehrere Navigationen brauchen.

Vielleicht wird das Konzept von Helpern ja klarer, wenn wir einfach einen erstellen. Legen wir als erstes eine neue Datei View/Helper/NavHelper.php an. Hier reicht zu beginn folgender Code:

<?php
	
	class NavHelper extends AppHelper
	{
		public $name = 'Nav';
		
		public $helpers = array('Html');
	}
	
?>

Hier erstellen wir also eine Klasse »NavHelper«, die wir von der Klasse »AppHelper« ableiten. Übrigens werden wir immer unsere Helper-, Controller- und Model-Klassen von der AppHelper-, AppController- bzw AppModel-Klasse ableiten. in diesen Elternklassen können wir so sehr leicht Code für alle Helper, Controller oder Models hinterlegen. Dies wird später noch praktisch werden.

Die Zeile 5 teilt älteren PHP-Versionen den Namen des Helpers mit. In Zeile 7 binden wir den HtmlHelper ein, damit wir in unserem NavHelper auch Links generieren können.

Als nächstes speichern wir in unserer Klasse die Navigations-Struktur, die unsere Navigation später aufweisen soll:

<?php
	
	class NavHelper extends AppHelper
	{
		public $name = 'Nav';
		
		public $helpers = array('Html');
		
		private $navItems = array(
			array(
				'title' => 'Startseite',
				'url' => array('controller' => 'pages', 'action' => 'display', 'home')
			),
			array(
				'title' => 'Beispiele',
				'url' => array('controller' => 'pages', 'action' => 'display', 'beispiele')
			)
		);
	}
	
?>

Wir speichern ein Array mit allen benötigten Informationen in der privaten Eigenschaft $navItems.

Als nächstes folgen drei Methoden, die im Zusammenspiel die Navigation generieren:

<?php
	
	class NavHelper extends AppHelper
	{
		public $name = 'Nav';
		
		public $helpers = array('Html');
		
		private $navItems = array(
			array(
				'title' => 'Startseite',
				'url' => array('controller' => 'pages', 'action' => 'display', 'home')
			),
			array(
				'title' => 'Beispiele',
				'url' => array('controller' => 'pages', 'action' => 'display', 'beispiele')
			)
		);
		
		public function main() {
			return '<nav id="mainMenu">' . $this->nav($this->navItems) . '</nav>';
		}
		
		private function nav($items) {
			$content = '';
			
			foreach($items as $item) {
                $class = array();
				
                if($this->isActive($item)) {
                    $class[] = 'active';
                }
				
				$url = $this->getUrl($item);
				
                $content .= $this->Html->link($item['title'], $url, array(
					'escape' => false,
					'class' => implode(' ', $class)
				));
            }
			
			return $content;
		}
		
		private function getUrl($item) {
			$url = false;
			if(isset($item['url'])) {
				$url = $item['url'];
			}
			
			return $url;
		}
		
        private function isActive($item) {
			$url = $this->Html->url($this->getUrl($item));
            if($this->here == $url || ($url != '/' && strlen($this->here) > strlen($url) && substr($this->here, 0, strlen($url)) == $url)) {
                $active = true;
            } else {
                $active =  false;
            }

            return $active;
        }
	}
	
?>

Die Methode main() wird zur Generierung der Haupt-Navigation aufgerufen und ruft wiederum zur Generierung der Links die Methode nav() auf. An diese Methode werden außerdem die zu rendernden Links übergeben, so könnten wir bei Bedarf zum Beispiel einfach eine neue Methode footer() definieren, die ein anderes Array an die Methode übergibt und so eine Footer-Navigation rendert.

Schauen wir uns die Methode nav() mal genauer an. Mit foreach() iterieren wir über alle Elemente des Arrays. Mit der Methode isActive() wird überprüft, ob der Link als aktiv gekennzeichnet werden muss. Praktisch: wenn wir irgendwann eine Unter-Navigation brauchen, können wir uns mit dieser Methode und etwas Rekursion auch direkt darum kümmern, die Unter-Elemente zu prüfen.

Als nächstes wird der Link generiert. Dazu benutzen wir den HtmlHelper. Die Methode link() erwartet als ersten Parameter den Link-Text und als zweiten Parameter die URL. Toll ist hier, dass automatisch ein Array wie array('controller' => 'pages', 'action' => 'display', 'home') mithilfe der Routes in eine URL umgewandelt wird. Ändern wir später die Routes, brauchen wir hier nichts zu ändern! Als dritter Parameter können wir Optionen definieren. Hier deaktivieren wir zum einen das Escapen des Link-Text (Praktisch, um zum Beispiel Bilder als Linktext zu verwenden), zum anderen übergeben wir hier die definierten Klassen.

Jetzt haben wir einen fertigen Helper, den wir nur noch einbinden und benutzen müssen. Um im View per $this->Nav auf den Helper zugreifen zu können, müssen wir diesen im Controller laden. Damit das in allen Controllern passiert, passen wir den AppController an:

class AppController extends Controller {
	public $helpers = array('Nav');
}

Nun wird automatisch der NavHelper geladen. Im Layout können wir diesen nun verwenden:

		<header>
			<div class="container">
				<a href="#" id="logo"><strong>Cope</strong>dio</a>
				<?php echo $this->Nav->main(); ?>
				<img id="eyeCatcher" src="img/header.jpg" />
			</div>
		</header>

Ich habe hier die gesamte Navigation durch einen Aufruf des NavHelpers ersetzt. Schauen wir uns nun wieder die App im Browser an, finden wir eine funktionierende Navigation!

Fazit

Das war jetzt ein sehr kompakter, dafür aber praktisch orientierter Überblick über Layouts, Views und Helpers in CakePHP. Im nächsten Artikel fangen wir mit Models und Controllers an, etwas dynamik ins Spiel zu bringen. Ich hoffe, dieses Tutorial hat dir gefallen und du hinterlässt mir einen Kommentar!

Nächster Artikel der Serie

Dieser Artikel ist Teil der Artikelserie »CakePHP-Tutorial: Web-Anwendung mit MVC«.

Hier geht es zum nächsten Artikel der Serie: CakePHP Tutorial - Model und Controller - ein Gästebuch programmieren