PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for the ‘PHP’ Category

Pflichtfelder mit Sternchen markieren via CSS

with 7 comments

Oft möchte man in einem Formular eine Prüfung einbinden, die anschlägt, wenn das Feld nicht gefüllt wurde. Das geht mit dem Zend Framework und Zend_Form sehr einfach:

class Wb_Form_Subscription extends Zend_Form
{
	public function __construct($options = null)
	{
		$element = new Zend_Form_Element_Text('Email');
		$element->setLabel('Email')
				->setRequired(true);
		$this->addElement($element);
	}
}

Mit der setRequired() Funktion fügen wir automatisch eine NotEmpty-Prüfung hinzu, außerdem erhält das Element die CSS-Klasse „required“:

<label for="Email" class="required">Email</label>

Wenn das Formular nun abgeschickt wird und das Feld leer bleibt, erhalten wir eine Fehlermeldung. Diese werden wir nachher noch anpassen. Doch erst möchten wir noch das Feld als Pflichtfeld markieren, mit einem kleinen roten Stern, so wie hier:

required_star

Das können wir auf 3 Arten erledigen, wobei die dritte mein Favorit ist:

$element->setLabel('Email *')
$element->getDecorator('label')->setOption('requiredSuffix', ' * ');

oder via CSS:

.required {
	background-image:url(/img/required-field.png);
	background-position:top right;
	background-repeat:no-repeat;
	padding-right:10px;
}

Eine Beispieldatei kann hier required-field runtergeladen werden.

Achso, die englische Fehlermeldung wollen wir noch anpassen, das geht so:

$element = new Zend_Form_Element_Text('Email');
$element->setLabel('Email')
		->setAttrib('size', 90)
		->setRequired(true)
		->addErrorMessage('Diese Email-Adresse ist ungültig')
		->addValidator('EmailAddress');
$this->addElement($element);

Wenn man für die verschiedenen Validatoren unterschiedliche Fehlermeldungen haben möchte, geht das so:

$element = new Zend_Form_Element_Text('Email');
$element->setLabel('Email')
		->setAttrib('size', 90)
		->setRequired(true)
		->addValidator('NotEmpty', false, array('messages' => 'Leer'))
		->addValidator('EmailAddress', false, array('messages' => 'Ungültig'));
$this->addElement($element);

error_messages

Möchte man nur eine Fehlermeldung haben, muss man den zweiten Parameter ($breakChainOnFailure) auf true setzen. Sobald ein Validator fehlschlägt, wird die Prüfung aller weiteren Validatoren abgebrochen.

Setzt man Zend_Translate ein, sollte man übrigens die Fehlermeldungen mittels Zend_Translate übersetzen lassen, und nicht so wie oben gezeigt. Dazu fügt man einfach eine neue Übersetzung hinzu, mit dem Schlüssel, der sich in den Klassenkonstanten der jeweiligen Validator-Klasse versteckt, wie hier beispielsweise beim EmailAddress-Validator.

Written by Michael Kliewe

Dezember 14th, 2009 at 9:50 am

Posted in PHP

Tagged with , , ,

Programmierprinzipien: Law of Demeter

with 14 comments

Professor Ian Holland hat zu Beginn der objektorientierten Zeit (1989) bereits eine wichtige Richtlinie definiert, die die lose Kopplung von Klassen sicherstellen soll: Das „Gesetz von Demeter“ (Law of Demeter, LoD).

Wenn man Klassen soweit es geht voneinander trennt, sind sie übersichtlicher, besser wartbar und testbar, leichter weiterzuentwickeln und wiederzuverwenden. Umgangssprachlich könnte man es beschreiben als „Sprich nur mit deinen nächsten Freunden“ und „Verrate keine Geheimnisse, die andere nichts angehen“.

Ein Praxisbeispiel:

class Order
{
	public $orderStatus = 0;

	public function changeOrderStatus($newStatus, $customer)
	{
		$this->orderStatus = $newStatus;

		if ($newStatus == 3) {
			$this->sendEmail(
					$customer->getData()->getContactInformation()->getEmail(),
					'New Order Status: ' . $newStatus
			);
		}
	}
}

Wer bei diesem Code keine Bauchschmerzen hat, sollte unbedingt weiterlesen. Wenn wir darauf nun die Regeln des LoD loslassen, sehen wir die Probleme.

Eine Klassenmethode sollte nur folgende andere Methoden verwenden:

  • Methoden der eigenen Klasse
  • Methoden der übergebenen Parameter
  • Methoden der mit eigenen Klasse assoziierten Klassen
  • Methoden von Objekten, die die Methode selbst erzeugt hat

Die Zeile 11 wäre also verboten, da sie eine zu enge Kopplung bzw. ein zu großes Wissen über andere Klassen voraussetzt. Lösung wäre hier, nicht das ganze Kundenobjekt an die Methode zu übergeben, sondern nur die für hier wichtigen Kontaktinformationen.

Außerdem verstößt der Code gegen das Geheimnisprinzip, da das Attribut $orderStatus public ist und man so den Status ändern könnte ohne eine Email zu versenden.

Wenn wir einen Test für die oben beschriebene Methode schreiben möchten, müssen wir vorher erst ein Kundenobjekt erzeugen, darin Daten hinterlegen, Kontaktinformationen usw. Doch eigentlich würden die Kontaktinformationen reichen für den Test, das Kundenobjekt ansich ist uns eigentlich egal, da es nicht genutzt wird. Auch Testen wird also durch lose Kopplung einfacher.

Besser wäre z.B. der folgende Code.

class Order
{
	private $_orderStatus = 0;

	public function changeOrderStatus($newStatus, $customerInformation)
	{
		$this->_orderStatus = $newStatus;

		if ($newStatus == 3) {
			$this->_sendEmail(
					$customerInformation->getEmail(),
					'New Order Status: ' . $newStatus
			);
		}
	}
}

Oder man macht es wie im unten stehenden Beispiel. Darin ist auch das Prinzip „Tell don’t ask“ abgebildet, welches besagt, dass man lieber Befehle gibt als Informationen abzufragen:

class Order
{
	private $_orderStatus = 0;

	public function changeOrderStatus($newStatus, $customer)
	{
		$this->_orderStatus = $newStatus;

		if ($newStatus == 3) {
			$customer->sendEmail('New Order Status: ' . $newStatus);
		}
	}
}

Wir geben also dem Kundenobjekt den Befehl, eine Email zu versenden (an den Kunden). Dann ersparen wir uns das Abfragen von Informationen, und wir müssen nicht wissen, wie im Kundenobjekt die Email-Adresse abgespeichert wird. Änderungen der Kundenklasse sind so also viel einfacher und problemloser machbar. Die Klasse Order kümmert sich also hauptsächlich um seine eigenen Dinge, und überlässt alles was den Kunden betrifft wenn es geht der Kundenklasse. Stichwort ist da das Single Responsibility Principle bzw. das  „Eine-Verantwortlichkeit-Prinzip„.

Um nochmal schnell die 4 Prinzipien an Code darzustellen, hier die erlaubten Methodenaufrufe:

Methoden der eigenen Klasse:

class A
{
    public function method1() {
        $this->method2();
    }

    public function method2() {
    }
}

Methoden der Parameter:

class A
{
    public function method1(B $b) {
        $b->method2();
    }
}
class B
{
    public function method2() {
    }
}

Methoden assoziierter Klassen:

class A
{
    private $b;
    public function method1() {
        $this->b->method2();
    }
}
class B
{
    public function method2() {
    }
}

Methoden selbst erzeugter Objekte:

class A
{
    public function method1() {
        $b = new B();
        $b->method2();
    }
}
class B
{
    public function method2() {
    }
}

Weitere Informationen zum LoD gibts im deutschen oder englischen Artikel in der Wikipedia. Oder direkt im Paper des Professors (PDF).

Written by Michael Kliewe

Dezember 2nd, 2009 at 9:02 am

Taugt Zend_Queue etwas?

with 2 comments

In einem älteren Artikel schrieb ich bereits über asynchrone Aufgaben. Heute möchte ich Zend_Queue vorstellen, womit man Aufgaben speichern kann, die später erledigt werden sollen.

Eine solche Message-Queue hat zwei Aufgaben: Aufgaben sollen erstellt werden können inklusive Details was zu tun ist, sowie Möglichkeiten bereitstellen, diese Aufgaben später wieder auslesen zu können, um sie abzuarbeiten.

Diese Aufgaben können wir nun in verschiedenen Backends speichern, Zend_Queue bietet einige Adapter an, wie beispielsweise eine Datenbank, ZendPlatformJobQueue, Apache ActiveMQ und MemcacheQ (bald wohl auch Amazon SQS).

Diese asynchrone Abarbeitung hat viele Vorteile. Für den Webseiten-Besucher erscheint die Webseite schneller, da einige Aufgaben später (wenige Sekunden, vielleicht auch sehr viel später) abgearbeitet werden. Für uns als Entwickler und Betreiber des Services besteht die Möglichkeit, Aufgaben in lastarmen Zeiträumen zu erledigen, sie auf mehrere Server aufzuteilen, die Last von den Frontendservern zu nehmen usw. Beim ZendPlatformJobQueue-Adapter kann man Jobs auch priorisieren, zu bestimmten Zeiten erledigen lassen, Abhängigkeiten zwischen den Jobs definieren und einiges mehr.

Mit einer Queue können wir auch regelmäßige Aufgaben abarbeiten lassen, beispielsweise das Aufräumen der Datenbank jede Nacht um 3 Uhr, pausieren von Aufgaben und spätere Wiederaufnahme der Tätigkeit, oder das Aufschieben von Aufgaben um x Stunden, beispielsweise soll immer exakt 2 Stunden nach dem Eintreffen eines Ereignisses eine Email versendet werden, wenn der Nutzer bis dahin nicht auf der Webseite war. Das geht allerdings wie gesagt nicht mit jedem Adapter.

Allgemein lässt sich sagen, dass wir alles offline abarbeiten sollten was nicht für ein Feedback des Besuchers benötigt wird.

Ein gutes Beispiel wäre ein Forum. Wenn ein Besucher einen neuen Beitrag schreibt, gibt es viel zu tun: Der Beitrag muss in der Datenbank gespeichert werden, der neue Inhalt muss in den Index gepumpt werden, Themenabonenten müssen benachrichtigt werden, RSS-Feeds müssen aktualisiert werden, und am Ende soll dem Benutzer sein fertiger Beitrag gezeigt werden. Das schreit doch nach Parallelität bzw. Queuing:

jobqueue

Wie funktioniert nun Zend_Queue? Hier ein einfaches Beispiel, wo wir eine Datenbank als Backend nutzen und einen Job eintragen. Zuerst müssen wir die Tabellen anlegen, die dazu benötigt werden. Das entsprechende SQL-Script befindet sich unter Zend/Queue/Adapter/Db/mysql.sql

Die entsprechenden PHP-Scripte zum Eintragen von Jobs und zum Auslesen von Jobs sehen so aus:

initQueue.php

<?php
include('Zend/Loader/Autoloader.php');
$autoloader = Zend_Loader_Autoloader::getInstance();

$config = array(
	'name' => 'myqueue',
	'driverOptions' => array(
		'host'     => 'localhost',
		'port'     => '3306',
		'username' => 'root',
		'password' => '',
		'dbname'   => 'Queue',
		'type'     => 'pdo_mysql'
	),
	'options' => array(
		// use Zend_Db_Select for update, not all databases
		// can support this feature.
		Zend_Db_Select::FOR_UPDATE => true
	)
);

// Create a database queue
$queue = new Zend_Queue('Db', $config);
$queue->createQueue('myqueue');

insert.php

<?php
require('initQueue.php');

$queue->send(serialize(array(	
			'action' => 'new article',
			'articleId' => 13))
);

receive.php

<?php
require('initQueue.php');

$messages = $queue->receive(5);
foreach($messages as $msg) {
	$jobInfo = unserialize($msg->body);

	// do the work
	var_dump($jobInfo);

	$queue->deleteMessage($msg);
}

Die Datenbank unterstützt nur einfache Jobs, es ist also nur für einfache Entkopplung zu gebrauchen, aber hier soll es als Beispiel reichen. Es werden aktuell keine Prioritäten, Ausführungszeiten oder ähnliches unterstützt, es ist eine reine Message-Queue. Der ZendPlatformJobQueue-Adapter unterstützt beispielsweise diese Features, sodass darauf zurückgegriffen werden sollte bei komplexeren Aufgaben. Soweit ich das bisher gelesen habe wird die Job Queue aber nur im kostenpflichtigen ZendServer enthalten sein, nicht in der Community Edition. Das ist echt schade, denn von den Features her ist das echt spitze was da geboten wird.

Wer also eine einfache Message-Queue benötigt, kommt mit Zend_Queue super klar, wer mehr braucht, sollte den ZendPlatformJobQueue-Adapter und den ZendServer nutzen (und vorher kaufen), oder sich bei Projekten wie Gearman oder Dropr umschauen, die ich in naher Zukunft auch noch vorstellen möchte.

Written by Michael Kliewe

November 27th, 2009 at 9:58 am

Die eigene Suchmaschine in PHP leicht gemacht: Lucene

with 10 comments

Nicht nur Google hat ausgereifte Suchalgorithmen, jeder Programmierer kann sich auch seine eigene Volltextsuche auf die Webseite bauen. Das können zum Beispiel alle Unterseiten sein, die durchsucht werden sollen, aber auch Dateien, Emails, Dokumente und Texte jeglicher Art.

Ich werde am Ende auch kurz aufzeigen, warum der Mysql-Volltextindex kein guter bzw. schneller Index ist, und warum Lucene und andere Suchengines ihre Daseinsberechtigung haben.

In diesem Artikel soll es also um Lucene gehen. Lucene ist ein Open-Source-Suchalgorithmus, der als Apache-Projekt weiterentwickelt wird und auf den viele weitere Produkte aufbauen (das bekannteste ist wohl Solr). Der Grundaufbau einer solchen „Suchmaschine“ besteht aus 2 Teilen: Dem Indexer und der Suche.

Der Indexer ist zum Befüllen des Datenbestandes (des Indexes) zuständig. Ihm übergibt man also alle Texte und Dokumente, und sagt ihm dabei, welche Felder und Daten davon wichtig sind, und eventuell noch wie wichtig die einzelnen Dokumente sind. Lucene ist zum Beispiel auch in der Lage, HTML-Dateien zu parsen und daraus title, meta-tags, header usw zu extrahieren. Man spart also Arbeit, und kann die Suche später auf die entsprechenden Bereiche beschränken. Der Index wird dann im Dateisystem abgelegt.

Die Suche spuckt dann die Ergebnisse aus, wenn man sie mit mehr oder minder komplexen Suchaufgaben befeuert. Dabei sind nicht nur einfache Stichwortsuchen möglich, sondern auch „ungefähre Treffer“, man erhält einen Relevanzwert(Score) und noch einige weitere Informationen.

Wenn wir nun in PHP einen solchen Index aufbauen wollen, nutzen wir am besten die Zend_Search_Lucene Klassen dafür. Hier ein einfaches Beispiel, wie man den Index füllt:

<?php

include_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

$index = Zend_Search_Lucene::create('/tmp/index');

$document = new Zend_Search_Lucene_Document();
$document->addField(Zend_Search_Lucene_Field::Text('title', 'Titel 1 des Dokuments'));
$document->addField(Zend_Search_Lucene_Field::Text('content', 'Hier ist ein toller Text'));
$index->addDocument($document);

$document = new Zend_Search_Lucene_Document();
$document->addField(Zend_Search_Lucene_Field::Text('title', 'Das hier ist der zweite Titel'));
$document->addField(Zend_Search_Lucene_Field::Text('content', 'Und hier steht der Inhalt eines Buches'));
$index->addDocument($document);

Wir definieren also ein Verzeichnis, in dem der Index abgelegt werden soll. Dann erstellen wir ein Dokument, zu dem wir dann ein Feld hinzufügen, in diesem Fall ein Textfeld. Dieses wird gesplittet und jedes Wort kann als Suchwort genutzt werden. Text-Felder werden zum Index hinzugefügt und komplett gespeichert, um sie bei den Ergebnissen auszugeben. Es gibt auch noch weitere Feldtypen, die zum beispiel nur indiziert aber nicht gespeichert werden, oder nur gespeichert und nicht indiziert. Hier gibt es eine Übersicht der Feldtypen.
Zum Schluss fügen wir das Dokument noch zum Index hinzu. Um die Suche nachher etwas interessanter zu machen, fügen wir noch ein weiteres Dokument hinzu. Das Ergebnis sieht dann so aus:

index

Reingucken brauchen wir da nicht, denn der Inhalt ist relativ kryptisch. Wir wollen ja auch nicht direkt auf diese Dateien zugreifen, sondern mittels der Suche. Das geht wie folgt:

<?php

include_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

$index = Zend_Search_Lucene::open('/tmp/index');

$queries = array('Buch', 'toller', 'ist', 'title:ist');

foreach ($queries as $query) {
	$results = $index->find(
	  		Zend_Search_Lucene_Search_QueryParser::parse($query)
	);
	echo "Suche: " . $query . "\n";
	echo count($results) . " Ergebnisse \n\n";
	foreach ($results as $result) {
		echo 'Inhalt: ' . $result->content . "\n";
		echo 'Score: ' . $result->score . "\n";
		echo  "\n";
	}
}

Die Abfragen können normale Sucheworte sein, man kann nur in bestimmten Feldern suchen, boolsche Operatoren (AND/OR) nutzen als auch noch viel komplexere Abfragen starten.

Die Ausgabe sieht wie folgt aus:

luceneresult

Es ist also wirklich kein Hexenwerk, mit knapp 30 Zeilen haben wir sowohl den Index gefüllt als auch einige Suchabfragen gestartet und die Ergebnisse ausgegeben.
Mit Lucene kann man noch sehr viel mehr machen, alles hier aufzuzählen würde den Rahmen sprengen. Einfach mal im Zend Framework Manual gucken, dann bekommt man einen Eindruck, was alles möglich ist.

Achso, ich erwähnte ja noch, dass ein Mysql-Volltextindex nicht so sinnvoll ist. Sobald große Mengen an Daten anfallen, wird Mysql langsam. Hier gibt es ein wunderbares PDF-Dokument mit Benchmarks.

Interessant sieht auch Sphinx aus, habe mich allerdings noch nicht damit beschäftigt.

Lucene ist also besonders interessant bei Daten, die nicht bereits in der Datenbank vorhanden sind. Als Beispiele wären da Dokumente, Twitter-Nachrichten, Emails oder statische HTML-Dateien genannt. Bevor man das also in seine Datenbank pumpt, nur um eine langsame Volltextsuche zu erhalten, sollte man lieber Lucene benutzen.

Written by Michael Kliewe

November 26th, 2009 at 9:48 am

PHP 5.3.1 und Zend Framework Bug Hunt Day

without comments

Kurze Info: PHP 5.3.1 ist gerade released worden. Über 100 Bugs wurden gefixt. Auf php.net/downloads und windows.php.net/download steht alles zum Download bereit.

Außerdem läuft gerade der November Bug Hunt Day des Zend Frameworks. Nach einem Tag sind bereits 59 Issues geschlossen worden, ich bin sehr gespannt wo der Counter morgen Abend steht!

Achja, noch eine Kleinigkeit: Falls ihr memcached 1.4.3 verwendet und mittels PHP darauf zugreift, euch sei gesagt dass die delete()-Funktion des Memcache-PECL-Moduls mit der Version nicht funktioniert. Mit 1.4.2 funktioniert alles wunderbar. Also downgraden (bzw. nicht upgraden) und auf ein Update von PECL warten. Nur so nebenbei.

Written by Michael Kliewe

November 20th, 2009 at 12:00 am

Posted in PHP

Tagged with , , ,