PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for Juli, 2009

Sinnvolle SVN Hooks für PHP Projekte

with 11 comments

Dieser Artikel ist nur für diejenigen gedacht, die SVN bereits kennen. Wer jetzt die Stirn runzelt, möge sich vorher bei wikipedia oder youtube informieren und mal erste Versuche mit einem SVN-Server sammeln.

Erstmal zur Begrifflichkeit ansich: Hooks sind Interfaces zu kleinen externen Programmen, die zu bestimmten Zeiten während eines Programmablaufs aufgerufen werden können. Sie klinken sich also in den Ablauf ein.

Beim SVN gibt es 3 interessante Hooks, die häufig genutzt werden (insgesamt gibt es 9):

– start-commit
– pre-commit
– post-commit

Im SVN Handbuch kann man genauer nachlesen, wann diese Hooks aufgerufen werden.

Das erste Script, welches wir als pre-commit-Hook aufrufen wollen, ist ein einfaches PHP-Lint, wir wollen also die PHP-Syntax testen. Dieses Beispiel mache ich etwas ausführlicher, alle anderen Hook-Scripte hänge ich einfach an dieses Posting dran.

#!/bin/bash
REPOS="$1"
TXN="$2"

PHP="/usr/local/php5/bin/php"
SVNLOOK="/usr/bin/svnlook"
AWK="/usr/bin/awk"
GREP="/bin/egrep"
SED="/bin/sed"

CHANGED=`$SVNLOOK changed -t "$TXN" "$REPOS" | $GREP "^[U|A]" | $AWK '{print $2}' | $GREP \\.php$`

for FILE in $CHANGED
do
    MESSAGE=`$SVNLOOK cat -t "$TXN" "$REPOS" "$FILE" | $PHP -l`
    if [ $? -ne 0 ]
    then
        echo 1>&2
        echo "***********************************" 1>&2
        echo "PHP error in: $FILE:" 1>&2
        echo `echo "$MESSAGE" | $SED "s| -| $FILE|g"` 1>&2
        echo "***********************************" 1>&2
        exit 1
    fi
done

Da ich mein SVN auf einem Linux-Server betreibe, habe ich hier das entsprechende Bash-Script. Falls ihr euren SVN-Server unter Windows betreibt, muß man das Script natürlich anpassen.

Was passiert hier? Es werden mittels svnlook alle geänderten oder neu hinzugefügten Dateien gesucht, aufgelistet und dann noch die Dateien mit einer .php Endung gefiltert. Für jede Dieser Dateien wird dann wiederum via svnlook der Quelltext geholt und mittels der Pipe an „php -l“ übergeben. Im Falle eines Fehlers gibt es eine Fehlermeldung, die dann im SVN-Client ausgegeben wird. Der Commit wird also scheitern (da dies ja ein pre-commit-Hook ist).

Ein weiteres Hook-Script, welches pre-commit ausgeführt wird, ist zum Beispiel der PEAR PHP-CodeSniffer. Dieses kleine Script kann PHP-Code auf Coding-Standards überprüfen, also ob beispielsweise PHPdoc vorhanden ist, oder ob die geschweiften Klammern an den richtigen Stellen stehen. Ich persönlich habe dieses Script allerdings nicht als Hook eingebunden, da ich auch ab und zu fremden Code ins SVN packe, der natürlich nicht meinen Code-Standards entspricht. PHP-CodeSniffer führe ich lokal ab und zu aus, und dann auch nur auf meine Verzeichnisse. Beim CodeSniffer wird ein entsprechendes Hook-Script gleich mitgeliefert.

Noch ein einfaches kleines Hook-Script wäre das hier:

/*
  test to see if svn commit comment length is greater than or equal to 10 chars
*/
$log = exec("svnlook log -t ". $argv[2] ." ". $argv[1]);
if(strlen($log) > 9){
      exit(0);
}else{
      exit(1);
}
---------

Weitere Hook-Scripte:

Falls ihr noch andere Hook-Scripte habt, nur her damit!

EDIT: Stefan empfiehlt noch ein tolles Hook-Script, mit dem man automatisch nach einem Commit die geänderten Dateien auf einen FTP, SFTP oder Filesystem synchronisieren kann: http://svn2web.sourceforge.net

Written by Michael Kliewe

Juli 16th, 2009 at 12:24 pm

Posted in PHP

Tagged with , , ,

PHP Profiling mit XDebug und KCachegrind

with 6 comments

So, nun will ich das Thema nachholen, was ich am letzten Donnerstag hab anklingen lassen. Am Wochenende ist dann jedoch das Gewinnspiel dazwischengekommen, sodass ich nun etwas zum Thema PHP-Profiler schreibe.

Profiler? Sind das nicht die FBI-Psychiater, die anhand von Tatorten und den Opfern etwas über den Täter aussagen können, wie er lebt und denkt? Naja, vielleicht…

Profiler gibt es auch für die meisten Programmiersprachen. Profiler schauen „unter die Haube“ und untersuchen die kleinsten Einheiten und Funktionen einen PHP-Programms. Sie messen die Zeiten für die Kommandos, zählen die Anzahl der Aufrufe, merken sich welche Funktion andere Funktionen aufruft und kann daraus sehr ausführliche und interessante Tabellen und Graphen generieren. Diese nutzt man dann am häufigsten, um (zeitliche) Flaschenhälse bei der Programmierung zu finden.

Aber wie erhält man diese Zeiten? Wenn man nur einen kleinen Teil seines Programms beobachten möchte und die Zeit messen möchte, die es braucht, macht man das wahrscheinlich so:

$start = microtime(true);
 // Hier einige Befehle, die gemessen werden sollen
 $end = microtime(true);
 $diff = $end - $start;
 echo "Benötigte Zeit: " . $diff;

Das ist meistens völlig ausreichend für den Anfang, um einen kurzen Überblick zu finden. Wenn das Programm aber sehr groß ist, und man nicht tausende dieser Messungen einbauen möchte, kann man gleich das ganze Script profilen lassen.

Wir machen das ganze mal praktisch an einem kleinen Beispiel:

class User
{
	private $username;
	private $newsletter;

	public function __construct() {
		$this->username = $this->getRandomString(8);
		$this->newsletter = rand(0, 1);
	}

	public function getUsername() {
		usleep(500000);
		return $this->username;
	}

	public function getNewsletter() {
		return $this->newsletter;
	}

	private function getRandomString($stringLength) {
	    //srand ((double)microtime() * 1000000);
	    return substr(md5(rand()), 0, $stringLength);
	}
}

for ($i = 0; $i < 5; $i++) {
	$user = new User();
	if (rand(0, 1)) {
		echo $user->getUsername()."\n";
	} else {
		echo $user->getNewsletter()."\n";
	}
}

Nun muß ich noch XDebug in der php.ini aktivieren. Dazu aktiviert man einfach die PHP-Extension XDebug wie folgt in der php.ini:

[XDebug]
; Only Zend OR (!) XDebug
zend_extension_ts=“C:\xampp\php\ext\php_xdebug.dll“
xdebug.remote_enable=true
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_handler=dbgp
xdebug.profiler_enable=1
xdebug.profiler_output_dir=“C:\xampp\tmp“

Natürlich sollte man vorher die php_xdebug.dll downloaden und in den entsprechenden Ordner legen. Nachdem man die php.ini so geändert hat, wird für jedes PHP-Script ein sogenannter CacheGrind-Dump im Output-Dir abgelegt. Diese Datei kann, je nach Komplexität und Umfang des Script, auch mehrere hundert MB groß werden.

Nun führe ich das Script ganz normal aus, im Hintergrund wird dann die CacheGrind Datei erzeugt.

run

Die generierte cachegrind.out.2916 sieht so aus, und ist (noch) nicht wirklich brauchbar:

cachegrundtxt

Öffnen und tabellarisch bzw. grafisch darstellen kann man diese Textdatei dann zum Beispiel mit dem Windows Programm WinCacheGrind:

wincachegrind2916

Hier erkennt man zwar schon etwas, aber viel besser ist unter Linux KCachegrind, das sieht dann so aus:

kcachegrind_2916

Man schaue sich nun die Anzahl der Aufrufe sowie die Zeitwerte an, und kann unzweifelhaft feststellen, dass in unserem einfachen Beispiel die usleep-Funktion die meiste Zeit gebraucht hat. Aber auch getRandomString() ist nicht zu vernachlässigen, vielleicht könnte man da noch etwas optimieren. Bei großeren Programmen mit SQL-Abfragen und komplexeren Algorithmen und Abläufen wird das ganze natürlich noch viel interessanter, probiert es einfach mal bei euren großen Projekten aus!

Man kann sowohl nach den absoluten als auch den prozentualen Zeitwerten sortieren, sieht Callees und Caller, kann sich einen Call Graph anzeigen lassen usw, ein wirklich tolles Programm.

Ich würde auf jeden Fall für ernsthafte Profiler das KCachegrind empfehlen. Falls man gerade kein laufendes Linux-System zur Hand hat, ist es höchste Zeit, mittels VirtualBox und Ubuntu schnell eins aufzusetzen. Beides ist natürlich kostenlos, und nach einer Stunde ist es lauffähig. Man braucht auch keine umständlichen Dual-Boot-Sachen und zerschießt sich wohlmöglich sein System, VirtualBox kann „ein Betriebssystem in einem Fenster“ laufen lassen. Einfach mal ausprobieren und informieren.

Mittels Profiling kann man sehr einfach herausfinden, wie oft welche Funktion aufgerufen wird. Man erkennt ziemlich schnell, wieviele Datenbank-Queries gemacht werden und von wo diese kommen. Man sieht, wieviele Objekte erstellt werden und wo dies passiert (und wie lang das dauert).

Die gefundenen Flaschenhälse kann man dann beseitigen. Entweder durch effizientere SQL-Abfragen, bessere Algorithmen, Caching oder oder. Das ist von Fall zu Fall verschieden.

Mich würde interessieren, ob und wie ihr profiled, und was ihr dadurch für gewöhnlich an Fehlern/Flaschenhälsen findet und wie ihr sie behebt.

Written by Michael Kliewe

Juli 15th, 2009 at 9:13 am

Posted in PHP

Tagged with , , ,

Gewinner des Buches steht fest

with 9 comments

So, ich habe es doch noch geschafft, der Gewinner der Verlosung steht fest. Fix einen schönen Zufallsgenerator geschrieben (natürlich in PHP mit dem Zend Framework, Dojo und ein bischen Javascript), und dann das ganze auch noch aufgenommen. Die php-Dateien gibts natürlich auch zum Download, falls ihr auch mal einen so schönen Zufallsgenerator braucht 😉

(Also das .swf bzw .flv, was mein Programm generiert hat, ist ja mal zum fürchten. Mit welchen Programmen nehmt ihr Desktop-Flash-Movies auf?) Hier gibts das avi als Download (Rechtsklick -> Speichern unter).

[flv:https://www.phpgangsta.de/wp-content/uploads/randomizer.flv 702 544]

——-

Der Gewinner: Ghost! (Die Nummer 2). Herzlichen Glückwunsch!

Bitte schick mir deine Adresse via Email rüber an mail, dann schick ich es dir zu.

Danke auch an alle, die mitgemacht haben!

——-

Download des Zufallsgenerators als zip-Datei. Nicht mit reingepackt habe ich das Zend Framework und Dojo, das muß noch an die richtigen Stellen gelegt werden damit es funktioniert (library und public/js )

Written by Michael Kliewe

Juli 14th, 2009 at 1:03 am

Posted in Allgemein

Tagged with , ,

Verlosung des Buches „PHP 5.3 – Die Neuerungen“

with 20 comments

So, nun ist es soweit, ich verlose das vor kurzem von mir gewonnene Buch „PHP 5.3 – Die Neuerungen“ von entwickler.press.PHP 5.3 - Die Neuerungen

Was müßt ihr dafür tun? Eigentlich nicht viel. Da ich keine Adressen sammeln möchte (weder postalische noch Email-Adressen), habe ich mir etwas anderes ausgedacht. Nicht ganz uneigennützig, aber ihr sollt ja auch etwas dafür tun 😉

Um meinen Blog etwas bekannter zu machen, sollt ihr einfach Links zu meinem Blog setzen. Ihr postet dann hier in den Kommentaren den Link, wo ihr den Backlink positioniert habt, und seid im Verlosungstopf drin. Dann habt ihr auch gleichzeitig euren Blog bzw. eure Webseite verlinkt.

Da der Blog noch neu und unbekannt ist, habt ihr sicherlich gute Chancen! Die Verlosung endet Sonntag, 12.07.09 um 24 Uhr. Nils (phphatesme) ist bereits als erster im Topf, da er heute einen Artikel über meinen Blog veröffentlicht hat. Danke dafür!

Ich setze mich dann mit euch in Verbindung.
Also dann mal ran an die „Arbeit“.

Written by Michael Kliewe

Juli 10th, 2009 at 9:58 am

PHP beschleunigen mittels Caching: Zend_Cache

with 6 comments

Der Titel ist vielleicht nicht ganz korrekt: PHP selbst kann man durch Caching nicht direkt beschleunigen, aber PHP-Applikationen.

Sobald man mit Datenbanken und Objekten arbeitet, fällt einem schnell auf, dass man gern für vieles eigene Objekte bastelt. Häufig ist es so, dass es für fast jede Tabelle eine Klasse gibt, und jede Zeile einer Tabelle einem Objekt entspricht. Das artet recht schnell aus, sodass man sehr viele Objekte hat, die auch hier und dort mehrfach erstellt werden. Das kostet vor allem Rechenkapazität.

Hier mal einige Klassen, mit denen wir weiter unten arbeiten werden:

class App_User
{
	private $username;
	private $newsletter;
	
	public function __construct($id) {
		$db = Zend_Registry::get('Zend_Db');
		$data = $db->fetchAll('SELECT Username, Newsletter FROM User WHERE UserID='.$id);
		$this->setUsername($data['Username']);
		$this->setNewsletter($data['Newsletter']);
	}
	
	public function getUsername() {
		return $this->username;
	}
	
	public function setUsername($username) {
		$this->username = $username;
	}
	
	public function getNewsletter() {
		return $this->newsletter;
	}
	
	public function setNewsletter($newsletter) {
		$this->newsletter = $newsletter;
	}
	
	public static function getAllUsers() {
		$db = Zend_Registry::get('Zend_Db');
		$allIds = $db->fetchCol('SELECT UserID FROM User');
		
		$users = array();
		foreach ($allIds as $id) {
			$users[] = new App_User($id);
		}
		return $users;
	}
}

Wenn man nun zum Beispiel alle User der Webseite überprüfen will, ob sie den Newsletter empfangen wollen, tut man dies objektorientiert dann so:

$newsletterCounter = 0;
$allUsers = App_User::getAllUsers();
foreach ($allUsers as $user) {
	if ($user->getNewsletter()) {
		$newsletterCounter++;
	}
}

An anderer Stelle irgendwo anders im Code (möglicherweise tief in anderen Klassen versteckt) möchte man dann vielleicht noch alle User durchgehen und ihre Usernamen ausgeben:

$allUsers = App_User::getAllUsers();
foreach ($allUsers as $user) {
	echo $user->getUsername().'<br>';
}

Nehmen wir weiter an, wir haben 5000 User in unserer Datenbank. Was passiert nun? Richtig, es werden in beiden Fällen jeweils 5000 User-Objekte erzeugt, ein Attribut abgefragt, und dann braucht man sie nicht mehr. 10000 Datenbankabfragen + 10000 Objektinstanziierungen.

Was können wir dagegen tun? Es gibt mehrere Möglichkeiten. Wir können zum Beispiel nach dem ersten Aufruf der getAllUsers()-Funktion das Ergebnis in einer globalen Variablen speichern:

$allUsers = App_User::getAllUsers();
$GLOBALS['allUsers'] = $allUsers;

Das ist vergleichbar mit der Zend_Registry, es funktioniert intern ähnlich, ist aber weit schöner und ein Zugriff „aus Versehen“ wird vermieden:

$allUsers = App_User::getAllUsers();
Zend_Registry::set('allUsers', $allUsers);

Der Zugriff würde dann so aussehen:

Zend_Registry::get('allUsers');

Das würde zwar funktionieren, ist aber ziemlich unpraktisch, da man nie weiß, wo genau der erste Zugriff ist, man also nicht genau weiß, ob die Informationen bereits in der Zend_Registry sind oder nicht. Das bedeutet viele if-Abfragen und ist unhandlich. Also verschieben wir den „Cache“ etwas weiter nach „innen“, wir verändern die getAllUsers()-Funktion wie folgt:

public static function getAllUsers() {
	if (!Zend_Registry::isRegistered('allUsers')) {
		$db = Zend_Registry::get('Zend_Db');
		$allIds = $db->fetchCol('SELECT UserID FROM User');
		
		$users = array();
		foreach ($allIds as $id) {
			$users[] = new App_User($id);
		}
		Zend_Registry::set('allUsers', $users);
	}
	
	return Zend_Registry::get('allUsers');
}

Nun wird also beim Aufruf von getAllUsers() beim ersten Mal die Datenbank abgefragt, und das Ergebnis in der Zend_Registry gespeichert. Beim zweiten Aufruf wird nun das bereits gespeicherte Ergebnis genommen. Wir sparen uns also viele Datenbankabfragen und Objekterstellungen. Von „außen“ kann man die Funktion ganz normal verwenden, man merkt nicht, dass intern gecacht wird.

Zwischenstand: Wir können innerhalb eines Scriptes viele Abfragen sparen, indem wir Ergebnisse und Objekte in der Zend_Registry speichern und diese bei Bedarf wiederverwenden.

Doch wir können noch mehr an Performance gewinnen, indem wir prozessübergreifend cachen. Wenn also 10 Besucher innerhalb von 5 Sekunden auf unserer Webseite unterwegs sind, sollen diese wenn möglich die selben Daten teilen, sodass für diese 10 Besucher nur einmal die 5000 Datensätze abgefragt und die entsprechenden Objekte erstellt werden müssen. Das geht nun nicht mehr mit globalen Variablen bzw. Zend_Registry, man muß mittels gemeinsamen Speichers (zB Memcached-Server, Festplatte, Netzspeicher) diese Daten austauschen. Diese gemeinsamen Daten sollen allerdings nach einer gewissen Zeit „ungültig“ werden, sodass regelmäßig frische und aktuelle Daten aus der Datenbank geholt werden. Genau das alles kann Zend_Cache.

Zend_Cache besteht grundlegend aus zwei Schichten: Dem Frontend und dem Backend. Das Backend definiert man nur einmal am Anfang, indem man den gewünschten Storage wählt und spezifiziert. Zur Auswahl stehen derzeit: File, Sqlite, Memcached, Apc, Xcache, ZendPlatform, TwoLevels, ZendServer_Disk
Die gebräuchlichsten dürften die ersten vier sein.

Wenn wir nun beispielsweise Zend_Cache_Backend_File wählen, müssen wir nur den Dateipfad angeben, die anderen Einstellungen können wir vorerst vernachlässigen.

Das Frontend ist die Schicht, über die wir den eigentlichen Cache ansprechen. Hier stehen uns mehrere Möglichkeiten zur Verfügung: Wir können beispielsweise einfache Variablen cachen, aber auch ganze Funktionen, Klassen, Dateien oder Seiten. Wir wollen uns hier erstmal nur um Variablen kümmern, die anderen Dinge könnt ihr euch ja im ZF-Manual nachlesen.

Nun aber Butter bei die Fische:

$frontendOptions = array(
   'lifetime' => 60, // cache lifetime of 1 minute
   'automatic_serialization' => true
);

$backendOptions = array(
    'cache_dir' => './tmp/' // Directory where to put the cache files
);

// getting a Zend_Cache_Core object
$cache = Zend_Cache::factory('Core',
                             'File',
                             $frontendOptions,
                             $backendOptions);
Zend_Registry::set('Zend_Cache', $cache);

Hier haben wir nun den Cache erstellt. Hier sieht man auch, dass man eine Lifetime definieren kann. Liegt ein Element länger als eine Minute im Cache, wird es gelöscht und muß dementsprechend neu aus der Datenbank geholt werden.

public static function getAllUsers() {
$cache = Zend_Registry::get(‚Zend_Cache‘);
// see if a cache already exists:
if(!$allUsers = $cache->load(‚allUsers‘)) {
// cache miss; connect to the database
$db = Zend_Registry::get(‚Zend_Db‘);
$allIds = $db->fetchCol(‚SELECT UserID FROM User‘);

$allUsers = array();
foreach ($allIds as $id) {
$allUsers[] = new App_User($id);
}
$cache->save($allUsers, ‚allUsers‘);
}

return $allUsers;
}
Wie man sieht, es ist der Zend_Registry Lösung sehr ähnlich. Es braucht nicht mehr als 20 Zeilen, um Caching zu aktivieren und zu nutzen. Nun haben wir die Freiheit, das Backend zu wählen, die Lebensdauer der Elemente zu spezifizieren, und bei Bedarf kann man auch Tags setzen. Tags sind vor allem dazu da, alle Elemente mit einem bestimmten Tag gleichzeitig zu löschen. Näheres dazu auch im ZF-Manual.

Ich hoffe man sieht, dass man durch Caching ordentlich Performance gewinnen kann, und sowohl die Besucher als auch die Hardware schonen kann. Natürlich macht Caching nicht überall Sinn (bei einem Ajax-Chat wäre es wohl eher hinderlich), aber die meisten Inhalte ändern sich nicht sekündlich, sondern eher in größeren Zeitabständen, und wenn ein Besucher einige Minuten „veraltete“ Inhalte zu sehen bekommt, ist das nicht unbedingt schlimm.

Morgen poste ich einen Artikel, der auch zu diesem Themenkomplex passt, aber das ganze von der anderen Seite betrachtet.

Written by Michael Kliewe

Juli 9th, 2009 at 6:50 pm

Posted in PHP

Tagged with , ,