CakePHP Tutorial: Admin-Routen und Login - erweitertes Gästebuch

CakePHP Tutorial: Admin-Routen und Login - erweitertes Gästebuch
Von Lars Ebert am 17.06.13, 11:00
Kategorien: CakePHP, PHP, Programmieren and Tutorials

Im letzten Teil dieser Artikelserie haben wir ein einfaches Gästebuch programmiert. Unsere Nutzer können nun einfache Einträge hinterlassen und diese werden auf der Gästebuch-Seite aufgelistet. In diesem Artikel wollen wir uns nun darum kümmern, dem Admin der Webseite eine Gästebuch-Verwaltung bereitzustellen. Dank CakePHP ist das mit relativ wenig Aufwand sehr schön möglich.

Wichtig: Dieser Artikel baut inhaltlich auf den letzten Artikel auf, wenn du den Rest der Artikelserie noch nicht gelesen hast, tu dies am besten jetzt!

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

Schritt 1: Admin-Prefix

In CakePHP gibt es eine tolle Funktion namens Route-Prefixes. Damit ist es, in unserem Fall, sehr leicht möglich, einen Admin-Bereich anzulegen. Folgende Konfiguration müssen wir in Config/core.php hinzufügen:

Configure::write('Routing.prefixes', array('admin'));

Mit dieser Zeile teilen wir CakePHP mit, dass wir z.B. planen, eine Action EntriesController::admin_index() anzulegen. Alle Actions mit dem admin-Prefix gehören später zu unserem Admin-Bereich!

Schritt 2: Deb Admin-Bereich anlegen

Als nächstes legen wir im EntriesController die besagte Action an.

<?php
	
	class EntriesController extends AppController
	{
		public $name = 'Entries';
		
		public function index() {
			if($this->request->is('post')) {
				if($this->Entry->save(
					$this->request->data,
					true,
					array('author', 'content', 'ip')
				)) {
					$this->Session->setFlash('Dein Eintrag wurde gespeichert!');
					$this->request->data = array();
				} else {
					$this->Session->setFlash('Dein Eintrag konnte nicht gespeichert werden. Prüfe deine Angaben!');
				}
			}
			
			$entries = $this->Entry->find('all', array(
				'order' => 'created DESC'
			));
			
			$this->set('entries', $entries);
		}
		
		public function admin_index() {
			$entries = $this->Entry->find('all', array(
				'order' => 'created DESC'
			));
			
			$this->set('entries', $entries);
		}
	}
	
?>

Die neue Action braucht auch noch einen neuen View!

<h1>Gästebuch-Verwaltung</h1>
<?php echo $this->Session->flash(); ?>
<?php foreach($entries as $entry) { ?>
	<article class="entry">
		<div class="meta">
			<?php echo $entry['Entry']['author']; ?> (<?php echo $entry['Entry']['ip']; ?>)
			<time><?php echo date('j. n. Y \u\m H:i \U\h\r', strtotime($entry['Entry']['created'])); ?></time>
		</div>
		<div class="content">
			<?php echo $entry['Entry']['content']; ?>
		</div>
	</article>
<?php } ?>

Nun legen wir noch die URL fest, über welche der Admin-Bereich aufzurufen ist:

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

Mit 'admin' => true aktivieren wir für diese Route den Admin-Prefix. So weiß CakePHP, dass dieser Aufruf nicht an die Action »index«, sondern »admin_index« gerichtet ist.

Nun können wir die URL /admin aufrufen und finden uns in der Gästebuch-Verwaltung. Allerdings haben wir nun ein kleines Problem: Wenn wir uns im Admin-Bereich befinden, führen die Links in der Navigation ins Leere, da CakePHP auch hier nun 'admin' => true hinzufügt. Dies können wir allerdings leicht im NavHelper ändern:

<?php
	
	class NavHelper extends AppHelper
	{
		public $name = 'Nav';
		
		public $helpers = array('Html');
		
		private $navItems = array(
			array(
				'title' => 'Startseite',
				'url' => array('admin' => false, 'controller' => 'pages', 'action' => 'display', 'home')
			),
			array(
				'title' => 'Gästebuch',
				'url' => array('admin' => false, 'controller' => 'entries', 'action' => 'index')
			),
			array(
				'title' => 'Beispiele',
				'url' => array('admin' => false, 'controller' => 'pages', 'action' => 'display', 'beispiele')
			)
		);
		
		[...]

Wir können einfach bei allen drei Links 'admin' => false hinzufügen, was dafür sorgt, dass diese Links nie zum Admin-Bereich führen.

Unser Admin-Bereich ist jetzt schon einmal erreichbar, auch wenn man hier noch nichts editieren oder moderieren kann. Aber bevor wir diese Funktionen hinzfügen, sollten wir dafür sorgen, dass nur der Admin Zutritt zum Admin-Bereich hat.

Schritt 3: Authentication in CakePHP - User und Login

CakePHP hat auch für die Authentifizierung bereits viele Hilfsmittel. Um diese zu nutzen, müssen wir nur eine Tabelle »users« anlegen:

Feldname Feldtyp
id bigint(20) unsigned Auto-Inkrement
username varchar(50)
password varchar(50)

Als nächstes legen wir für die Nutzer ein Model an!

<?php
	
	class User extends AppModel
	{
		public $name = 'User';
	}
	
?>

Außerdem brauchen wir noch einen UsersController, der den Login und Logout regelt.

<?php
	
	class UsersController extends AppController
	{
		public function login() {
			if($this->request->is('post')) {
				if($this->Auth->login()) {
					$this->redirect($this->Auth->redirect());
				} else {
					$this->Session->setFlash(__('Falscher Nutzername oder falsches Passwort. Versuche es nochmal!'));
				}
			}
		}

		public function logout() {
			$this->redirect($this->Auth->logout());
		}
	}
	
?>

Für die Logout-Action brauchen wir keinen View, da der Nutzer so oder so weitergeleitet wird, aber bei der Login-Action brauchen wir ein Formular!

<?php echo $this->Session->flash('auth'); ?>
<h2>Admin-Bereich</h2>
<p>Gib deinen Nutzernamen und dein Passwort ein!</p>
<?php echo $this->Form->create('User'); ?>
    <?php echo $this->Form->input('username', array('label' => 'Nutzername')); ?>
    <?php echo $this->Form->input('password', array('label' => 'Passwort')); ?>
<?php echo $this->Form->end(__('Login')); ?>

Nun legen wir noch eine Route für die Login-Seite an:

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

Nun sollten wir unter /login unser neues Login-Formular sehen. Allerdings können wir uns noch nicht einloggen, da es noch gar keinen Admin in der Datenbank gibt.

CakePHP nutzt verschlüsselte Passworte, also müssen wir uns erst ein Passwort verschlüsseln. Da die Passwortverschlüsselung bei jeder Installation etwas anders geschieht, musst du das Passwort direkt in deiner App generieren! Ich habe dazu folgende Zeilen zum Login-View hinzugefügt:

<?php
	App::uses('AuthComponent', 'Controller/Component');
	echo AuthComponent::password('PASSWORT');
?>

Wenn du hier ein Passwort einfügst und dann das Login-Formular aufrufst, wird dir dein verschlüsseltes Passwort angezeigt. In meinem Fall wird der String "password" zum Beispiel zu "4d864efbbbe3d9568c7991dc65aba4e5919f6822" verschlüsselt. Dieses Passwort können wir dann zusammen mit einem Nutzernamen in der Datenbank speichern.

Wichtig: Lösche unbedingt die eben eingefügten Zeilen aus View/Users/login.ctp, damit dein Passwort nicht jedes mal ausgegeben wird!

Wir müssen in CakePHP nun nur noch definieren, welche Views nur von eingeloggten Nutzern angesehen werden können. Dies und vieles mehr können wir mit der AuthComponent steuern. Da wir diese überall brauchen werden, fügen wir sie in den AppController ein:

class AppController extends Controller {
	public $helpers = array('Nav');
	public $components = array(
		'Session',
		'Auth' => array(
			'loginRedirect' => array('controller' => 'entries', 'action' => 'index', 'admin' => true),
			'logoutRedirect' => array('controller' => 'pages', 'action' => 'display', 'home', 'admin' => false),
			'loginAction' => array('controller' => 'users', 'action' => 'login', 'admin' => false),
			'authError' => 'Melde dich an, um diesen Bereich zu sehen!'
		)
	);
}

LoginRedirect definiert, wohin der Nutzer nach einem erfolgreichen Login weitergeleitet wird, LogoutRedirect ebenso nur für den Logout. LoginAction definiert, wo das Login-Formular zu finden ist und AuthError definiert die Fehlermeldung, wenn ein nicht eingeloggter Nutzer den Admin-Bereich betreten will.

Nun definieren wir, dass alle Actions mit Admin-Prefix nur für angemeldete Nutzer aufrufbar sind:

class AppController extends Controller {
	public $helpers = array('Nav');
	public $components = array(
		'Session',
		'Auth' => array(
			'loginRedirect' => array('controller' => 'entries', 'action' => 'index', 'admin' => true),
			'logoutRedirect' => array('controller' => 'pages', 'action' => 'display', 'home', 'admin' => false),
			'loginAction' => array('controller' => 'users', 'action' => 'login', 'admin' => false),
			'authError' => 'Melde dich an, um diesen Bereich zu sehen!'
		)
	);
	
	public function beforeFilter() {
		if(!empty($this->params['prefix']) && $this->params['prefix'] == 'admin'){
			$this->Auth->deny();
         } else {
			$this->Auth->allow();
		}
	}
}

Ruf jetzt den Admin-Bereich unter /admin auf! Wenn alles geklappt hat, solltest du zum Login-Formular weitergeleitet werden. Melde dich mit dem eben definierten Nutzernamen und Passwort an, danach solltest du sofort zum Admin-Bereich weitergeleitet werden.

Alles richtig? Super! Dann kümmern wir uns jetzt um die eigentlichen Funktionen des Admin-Bereiches!

Schritt 4: Moderation von Gästebuch-Einträgen

Momentan können die Benutzer noch ungefiltert Inhalte ins Gästebuch schreiben. Wir werden das Gästebuch nun so anpassen, dass die Einträge erst moderiert werden müssen, bevor sie im Gästebuch erscheinen. Dazu müssen wir in der Datenbank die Tabelle entries etwas erweitern:

Feldname Feldtyp
id bigint(20) unsigned Auto-Inkrement
author varchar(50)
ip varchar(50)
content longtext
comment longtext NULL
active tinyint(1) unsigned NULL [0]
created datetime

In der Spalte active können wir später speichern, ob der Eintrag bereits moderiert wurde. Außerdem habe ich noch eine Spalte comment hinzugefügt, in der der Admin den Eintrag gegebenenfalls später kommentieren kann, quasi als direkte Antwortmöglichkeit für den Admin.

Als nächstes modifizieren wir den EntriesController, sodass nur noch moderierte Einträge angezeigt werden:

<?php
	
	class EntriesController extends AppController
	{
		public $name = 'Entries';
		
		public function index() {
			if($this->request->is('post')) {
				if($this->Entry->save(
					$this->request->data,
					true,
					array('author', 'content', 'ip')
				)) {
					$this->Session->setFlash('Dein Eintrag wurde gespeichert!');
					$this->request->data = array();
				} else {
					$this->Session->setFlash('Dein Eintrag konnte nicht gespeichert werden. Prüfe deine Angaben!');
				}
			}
			
			$entries = $this->Entry->find('all', array(
				'order' => 'created DESC',
				'conditions' => array(
					'active' => 1
				)
			));
			
			$this->set('entries', $entries);
		}
		
		public function admin_index() {
			$entries = $this->Entry->find('all', array(
				'order' => 'created DESC'
			));
			
			$this->set('entries', $entries);
		}
	}
	
?>

Als nächstes passen wir die Validation der Einträge an, um auch die beiden neuen Felder zu kontrollieren. Für das Feld comment brauchen wir keine Validierung, da es kein Pflichtfeld ist. Für active müssen wir aber sicherstellen, dass nur ein Bool übergeben wird.

<?php
	
	class Entry extends AppModel
	{
		public $name = 'Entry';
		
		public $validate = array(
			'author' => array(
				'rule' => 'notEmpty',
				'required' => true,
				'message' => 'Bitte gib deinen Namen ein!'
			),
			'content' => array(
				'rule' => 'notEmpty',
				'required' => true,
				'message' => 'Was möchtest du ins Gästebuch schreiben?'
			),
			'active' => array(
				'rule' => 'boolean',
				'required' => false
			)
		);
		
		public function beforeSave($options = array()) {
			if(!isset($this->id)) {
				$this->data['Entry']['ip'] = $_SERVER['REMOTE_ADDR'];
				
				$this->data['Entry']['content'] = preg_replace('|(?<!</p>)\s*\n|', "<br />", (preg_replace('/\n?(.+?)(\n\n|\z)/s', "<p>$1</p>", (preg_replace("/\n\n+/", "\n\n", preg_replace("/(\r\n|\n|\r)/", "\n", $this->data['Entry']['content']))))));
				
			}
			
			return parent::beforeSave($options);
		}
	}
	
?>

Nun erstellen wir die Action admin_edit, diese ist dafür zuständig, ein Formular zum Bearbeiten anzuzeigen und den Eintrag zu speichern.

<?php
	
	class EntriesController extends AppController
	{
		public $name = 'Entries';
		
		public function index() {
			if($this->request->is('post')) {
				if($this->Entry->save(
					$this->request->data,
					true,
					array('author', 'content', 'ip')
				)) {
					$this->Session->setFlash('Dein Eintrag wurde gespeichert!');
					$this->request->data = array();
				} else {
					$this->Session->setFlash('Dein Eintrag konnte nicht gespeichert werden. Prüfe deine Angaben!');
				}
			}
			
			$entries = $this->Entry->find('all', array(
				'order' => 'created DESC',
				'conditions' => array(
					'active' => 1
				)
			));
			
			$this->set('entries', $entries);
		}
		
		public function admin_index() {
			$entries = $this->Entry->find('all', array(
				'order' => 'created DESC'
			));
			
			$this->set('entries', $entries);
		}
		
		public function admin_edit($id) {
			$entry = $this->Entry->findById($id);
			
			if($entry == null) {
				$this->redirect(array('action' => 'index'));
			}
			
			if($this->request->is('post') || $this->request->is('put')) {
				if($this->Entry->save(
					$this->request->data,
					true,
					array('author', 'content', 'comment', 'active')
				)) {
					$this->Session->setFlash('Der Eintrag wurde gespeichert.');
					$this->request->data = array();
					$this->redirect(array('action' => 'index'));
				} else {
					$this->Session->setFlash('Der Eintrag konnte nicht gespeichert werden. Prüfe deine Angaben!');
				}
			}
			
			$this->request->data = $entry;
		}
	}
	
?>

Das Speichern des Eintrages läuft genau so ab wie im letzten Artikel. Nun passen wir die beiden Admin-Views an, um das neue Formular auch anzuzeigen!

<h1>Gästebuch-Verwaltung</h1>
<?php echo $this->Session->flash(); ?>
<?php foreach($entries as $entry) { ?>
	<article class="entry">
		<div class="meta">
			<?php echo $entry['Entry']['author']; ?> (<?php echo $entry['Entry']['ip']; ?>)
			<time><?php echo date('j. n. Y \u\m H:i \U\h\r', strtotime($entry['Entry']['created'])); ?></time>
			<?php if($entry['Entry']['active'] == 0) { ?><div class="moderate">Dieser Kommentar muss moderiert werden.</div><?php } ?>
		</div>
		<div class="content">
			<?php
				$entry['Entry']['content'] = preg_replace('|(?<!</p>)\s*\n|', "<br />", (preg_replace('/\n?(.+?)(\n\n|\z)/s', "<p>$1</p>", (preg_replace("/\n\n+/", "\n\n", preg_replace("/(\r\n|\n|\r)/", "\n", $entry['Entry']['content']))))));
				
				echo $entry['Entry']['content'];
			?>
		</div>
		<div class="edit"><?php
			echo $this->Html->link('Diesen Eintrag bearbeiten', array('controller' => 'entries', 'action' => 'edit', 'id' => $entry['Entry']['id']));
		?></div>
	</article>
<?php } ?>
<h1>Gästebuch-Eintrag bearbeiten</h1>
<?php echo $this->Session->flash(); ?>
<?php echo $this->Form->create(); ?>
	<?php echo $this->Form->hidden('id'); ?>
	<?php echo $this->Form->input('author', array(
		'label' => 'Name',
		'placeholder' => 'Der Name des Autors'
	)); ?>
	<?php echo $this->Form->input('content', array(
		'label' => 'Inhalt',
		'placeholder' => 'Der Inhalt des Eintrags'
	)); ?>
	<?php echo $this->Form->input('comment', array(
		'label' => 'Kommentar',
		'placeholder' => 'Willst du dem Autor einen Kommentar hinterlassen?'
	)); ?>
	<?php echo $this->Form->input('active', array(
		'label' => 'Eintrag freigeschaltet'
	)); ?>
<?php echo $this->Form->end('Speichern'); ?>

Als letztes erstellen wir noch eine Route für den neuen View:

<?php
	
	Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
	Router::connect('/guestbook', array('controller' => 'entries', 'action' => 'index'));
	
	Router::connect('/login', array('controller' => 'users', 'action' => 'login'));
	Router::connect('/admin', array('controller' => 'entries', 'action' => 'index', 'admin' => true));
	Router::connect(
		'/admin/edit/:id',
		array('controller' => 'entries', 'action' => 'edit', 'admin' => true),
		array(
			'pass' => array('id'),
			'id' => '[0-9]+'
		)
	);
	
	Router::connect('/*', array('controller' => 'pages', 'action' => 'display'));
	
	CakePlugin::routes();
	
	require CAKE . 'Config' . DS . 'routes.php';

Diese Route sorgt dafür, dass die ID aus der URL an die entsprechende Methode im Controller übergeben wird. Hier ist zu sehen, wie flexibel die Routes von CakePHP sind.

Nun müssen wir nur noch gegebenenfalls den Kommentar des Admins unter dem Eintrag anzeigen:

<h1>Das Gästebuch</h1>
<?php echo $this->Session->flash(); ?>
<?php echo $this->Form->create(); ?>
	<?php echo $this->Form->input('author', array(
		'label' => 'Name',
		'placeholder' => 'Dein Name'
	)); ?>
	<?php echo $this->Form->input('content', array(
		'label' => 'Inhalt',
		'placeholder' => 'Was möchtest du ins Gästebuch schreiben?'
	)); ?>
<?php echo $this->Form->end('Absenden'); ?>

<?php foreach($entries as $entry) { ?>
	<article class="entry">
		<div class="meta">
			<?php echo $entry['Entry']['author']; ?>
			<time><?php echo date('j. n. Y \u\m H:i \U\h\r', strtotime($entry['Entry']['created'])); ?></time>
		</div>
		<div class="content">
			<?php
				$entry['Entry']['content'] = preg_replace('|(?<!</p>)\s*\n|', "<br />", (preg_replace('/\n?(.+?)(\n\n|\z)/s', "<p>$1</p>", (preg_replace("/\n\n+/", "\n\n", preg_replace("/(\r\n|\n|\r)/", "\n", $entry['Entry']['content']))))));
				
				echo $entry['Entry']['content'];
				
				if(!empty($entry['Entry']['comment'])) {
			?>
			<div class="comment">
				<?php
					$entry['Entry']['comment'] = preg_replace('|(?<!</p>)\s*\n|', "<br />", (preg_replace('/\n?(.+?)(\n\n|\z)/s', "<p>$1</p>", (preg_replace("/\n\n+/", "\n\n", preg_replace("/(\r\n|\n|\r)/", "\n", $entry['Entry']['comment']))))));
					
					echo $entry['Entry']['comment'];
				?>
			</div>
			<?php } ?>
		</div>
	</article>
<?php } ?>
<h1>Gästebuch-Verwaltung</h1>
<?php echo $this->Session->flash(); ?>
<?php foreach($entries as $entry) { ?>
	<article class="entry">
		<div class="meta">
			<?php echo $entry['Entry']['author']; ?> (<?php echo $entry['Entry']['ip']; ?>)
			<time><?php echo date('j. n. Y \u\m H:i \U\h\r', strtotime($entry['Entry']['created'])); ?></time>
			<?php if($entry['Entry']['active'] == 0) { ?><div class="moderate">Dieser Kommentar muss moderiert werden.</div><?php } ?>
		</div>
		<div class="content">
			<?php
				$entry['Entry']['content'] = preg_replace('|(?<!</p>)\s*\n|', "<br />", (preg_replace('/\n?(.+?)(\n\n|\z)/s', "<p>$1</p>", (preg_replace("/\n\n+/", "\n\n", preg_replace("/(\r\n|\n|\r)/", "\n", $entry['Entry']['content']))))));
				
				echo $entry['Entry']['content'];
				
				if(!empty($entry['Entry']['comment'])) {
			?>
			<div class="comment">
				<?php
					$entry['Entry']['comment'] = preg_replace('|(?<!</p>)\s*\n|', "<br />", (preg_replace('/\n?(.+?)(\n\n|\z)/s', "<p>$1</p>", (preg_replace("/\n\n+/", "\n\n", preg_replace("/(\r\n|\n|\r)/", "\n", $entry['Entry']['comment']))))));
					
					echo $entry['Entry']['comment'];
				?>
			</div>
			<?php } ?>
		</div>
		<div class="edit"><?php
			echo $this->Html->link('Diesen Eintrag bearbeiten', array('controller' => 'entries', 'action' => 'edit', 'id' => $entry['Entry']['id']));
		?></div>
	</article>
<?php } ?>

Ich habe außerdem die Umwandlung der Umbrüche aus dem Entry-Model entfernt und direkt zur Ausgabe hinzugefügt, damit die Einträge ohne HTML-Umbrüche bearbeitet werden können!

Fazit: Code lässt sich oft wiederverwerten

Für die Erstellung des Admin-Bereichs konnten wir uns oft am Frontend-Bereich orientieren und teilweise sogar die Views, etwas verändert, übernehmen. Hier zeigen sich die Stärken von CakePHP. Das Schreiben der Logik in der App ist viel schneller möglich, da wir an sehr vielen Stellen an die Klassen von CakePHP anknüpfen können. Außerdem entsteht hier quasi automatisch schön geordneter Code, da alles Models, Views und Controller aufgeteilt wird.

Am Anfang der Artikelserie hatte ich den Ablauf des Tutorials bis hierhin schon im Kopf. Was würde euch nun weiter interessieren? Welche Funktionen von CakePHP sollte ich im weiteren Verlauf der Artikelserie vorstellen? Wie soll es weitergehen? Bitte hinterlasst mir Kommentare, damit ich weiß, was euch interessiert.

Ich freue mich auf euer Feedback!

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: Epsilon-Greedy-Tests - ein Plugin erstellen