PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for the ‘PHP’ tag

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 , ,

Nicht-HTML-Responses mit dem Zend Framework

with 5 comments

Wenn man dynamische Bilder oder RSS-Feeds oder einen AJAX/JSON-Service oder ein Excel-Export mithilfe des Zend Frameworks erstellen will, mußt man 2-3 wichtige Dinge beachten. Der Code soll dann in einem Rss-/Graph-/Ajax-/Export-Controller stehen.

Ein Problem bekommt man, wenn man ein Layout benutzt (Zend_Layout). Denn dann wird dieses Layout immer ausgegeben. Im hier betrachteten Fall wäre das aber sehr schädlich, denn dadurch würden wir unser Bild/RSS-Feed/AJAX/Excel-Response zerstören.

Unschön kann man das wie folgt lösen:

public function rssAction()
{
	// calculate rss data and echo it (with correct headers)

	exit;
}

Richtig und deutlich schöner ist das Abschalten des Layout in der Action, wie folgt:

public function rssAction()
{
	// disable layout
	$this->_helper->layout()->disableLayout();
	
	// disable view rendering
	$this->_helper->viewRenderer->setNoRender();
	
	// calculate rss data and echo it (with correct headers)
}

Wir schalten auch gleich noch den ViewRenderer mit aus, damit auch nicht versucht wird, ein Viewscript zu rendern (das es wahrscheinlich garnicht gibt).

Dieses RSS-Beispiel funktioniert natürlich genauso für die anderen Beispiele, wo kein klassischer HTML-Quelltext zurückgegeben werden soll, sondern eine Antwort in einem anderen Format gefordert ist.

Damit kann man dann seine dynmisch erstellten Bilder (z.B. mittels pChart, jpgraph oder direkt die GD-Funktionen/image* in php), RSS-Feeds (Zend_Feed), Ajax-Services (Zend_Json) usw. realisieren.

Hier noch schnell ein Beispiel eines Excel-Exports aus einer Datenbank, mit Hilfe der Spreadsheet-Klasse aus dem PEAR-Framework (vereinfacht auf das Wesentliche):

class ExportController extends Zend_Controller_Action
{	
	public function excel() {
		// disable layout
		$this->_helper->layout()->disableLayout();
		
		// disable view rendering
		$this->_helper->viewRenderer->setNoRender();

		
		// get some data from database here
		
		       
		// create empty file
		//include 'Spreadsheet/Excel/Writer.php';
		$excel = new Spreadsheet_Excel_Writer();
		// add worksheet
		$sheet =& $excel->addWorksheet('Daily Export');

		$sheet->setColumn(0,0,20);
		$sheet->setColumn(1,1,15);
		$sheet->setColumn(2,2,18);
		$sheet->setColumn(3,3,23);
		$sheet->setColumn(4,4,35);
		$sheet->setColumn(5,5,15);

		$format_bold =& $excel->addFormat();
		$format_bold->setBold();

		$format_headline =& $excel->addFormat();
		$format_headline->setBold();
		$format_headline->setSize(20);
		$format_headline->setAlign('center');

		// headline
		$sheet->write(0, 0, 'Results: '.date('d.m.Y H:i'), $format_headline);
		$sheet->mergeCells(0,0,0,5);

		// add data to worksheet
		$rowCount=2;

		foreach ($data as $groupName=>$serverData) {
			$sheet->write($rowCount, 0, $groupName, $format_bold);
			$rowCount++;

			foreach ($serverData as $row) {
				$colcount = 0;
				foreach ($row as $key => $value) {
					$sheet->write($rowCount, $colcount, $value);
					$colcount++;
				}
				$rowCount++;
			}
			$rowCount++;
		}
		// send client headers
		$excel->send('daily_export_'.date("Ymd-His").'.xls');
	}		
}

Dies hier ist alter Code, mittlerweile nutzen wir PHPExcel.

Written by Michael Kliewe

Juli 2nd, 2009 at 2:52 pm

Posted in PHP

Tagged with , , , , , ,

PHP 5.3 released!

without comments

Closures, Late Static Binding, Namespaces, neuer Mysql Native Driver, Garbage Collection und und und.
Wem das (noch) nichts sagt, sollte sich in den nächsten Tagen und Wochen damit beschäftigen (hier im Blog werde ich sicherlich auch einiges davon vorstellen), denn

PHP 5.3

wurde soeben offiziell released! http://www.php.net/downloads.php

Ich persönlich würde damit zwar noch nicht auf Produktiv-Systeme gehen, aber zuhause in Entwicklungsumgebungen und vielleicht auf kleinen Homepages kann man damit schon arbeiten denke ich. Wenn in 4-6 Wochen dann die ersten Bugfixes verfügbar sind, werden wir in der Firma sicher auch mal intensiver damit testen, um unsere alten Systeme auf kurz oder lang umzustellen. Das ist aber häufig ein monatelanger Prozess, wir werden sehen.

Da dies kein langer Post werden soll, kann ich nur sagen: Probiert es aus und testet die neuen Funktionen!

Written by Michael Kliewe

Juni 30th, 2009 at 2:38 pm

Posted in PHP

Tagged with , ,

Firewall Beschränkungen prüfen

without comments

Zuhause hat man diese Probleme wohl nicht so sehr, und auch auf einem Rootserver braucht man sich nur sehr selten damit rumplagen: Firewalls, die den Zugriff auf andere Systeme blocken.

Firewalls sind im Prinzip nichts anderes als Programme (oder Hardware), die im Netzwerk oder auf einem System installiert sind und Netzwerkpakete (nicht nur TCP/IP, sondern auch UDP, ICMP Pakete usw) untersuchen kann. Jede Firewall hat Regeln, die beschreiben, was passieren soll wenn bestimmte Pakete ankommen. Zum Beispiel kann sie die Verbindung komplett unterbinden (d.h. die ankommenden Pakete droppen) wenn ein Quellsystem 1.2.3.4 das Zielsystem 6.7.8.9 auf Port 25 versucht zu erreichen. Das wäre ein Blacklist-Eintrag.

Sicherer und häufig einfacher ist aber eine Whitelist. Dann wird prinzipiell erstmal alles gedroppt, und nur einige definierte Verbindungen dürfen durchgelassen werden. Bei einer lokalen Firewall zuhause oder auf einem Server resultiert das in 1-20 Regeln. In einer größeren Firma mit komplexen Netzwerken, vielen vielen Servern und Arbeitsrechnern, einigen dutzend IP-Bereichen und Hardware-Firewalls, die ganze Netze voneinander trennen (also nicht hunderte lokal installierte Software-Firewalls), artet diese Sicherheit häufig auch in viel Arbeit und komplexe Regeln aus.

Kürzlich mußten wir ein System mit einer komplexen Software, welche Verbindungen zu aktuell 65 anderen Systemen (es werden kontinuierlich mehr) aufbaut, um dort Daten abzuholen und abzuliefern, von einem Server auf einen anderen Server in einem anderen Netz umziehen. Da wir keine Listen hatten, zu welchen Zielsystemen eine Verbindung möglich ist und zu welchen nicht, hilft nur ausprobieren.

Da wir Informatiker ja bekanntlich faul sind (was nicht immer schlecht ist, denn faule Programmierer meiden redundanten Code, programmieren selten mehr Schnick-Schnack in eine Anwendung als benötigt usw) haben wir uns ein kleines Script geschrieben, das alle benötigten Zielsysteme durchprobiert und eine schöne Liste der nicht erfolgreichen Verbindungen ausgibt.

Unsere erste Version tat genau das: Es versucht, alle Zielsysteme auf einem bestimmten Port zu erreichen, indem es eine Socketverbindung aufbaut und dann wieder trennt. Ein schicker 40-Zeiler:

<?php
require_once('../init.php');
$configIni = Zend_Registry::get($configIni);

echo 'We are using database: ' . DATABASE_DBNAME."\n\n";

$portChecks = array();
$portChecks[] = array('Host' => $configIni->mail->standard->host, 'Port' => $configIni->mail->standard->port);
$portChecks[] = array('Host' => $configIni->mail->secure->host, 'Port' => $configIni->mail->secure->port);
$portChecks[] = array('Host' => $configIni->db->config->hostnameonly, 'Port' => $configIni->db->config->port);
$portChecks[] = array('Host' => $configIni->product1->host, 'Port' => $configIni->product1->port);

$db = MDB2::singleton('mssql://' . DATABASE_USER . ':' . DATABASE_PASS . '@' . DATABASE_SERVER . ':' . DATABASE_PORT . '/' . DATABASE_DBNAME);
$db->setFetchMode(MDB2_FETCHMODE_ASSOC);

// add FTP Checks from DB
$ftpModules = $db->queryAll('SELECT DISTINCT Server, Serverport FROM FTP');
foreach ($ftpModules as $ftpModule) {
$portChecks[] = array('Host' 	=> $ftpModule['server'],
'Port' 		=> $ftpModule['serverport'] == 0 ? 21 : $ftpModule['serverport']	);
}

// add SFTP Checks from DB
$sftpModules = $db->queryAll('SELECT DISTINCT Server, Serverport FROM SFTP');
foreach ($sftpModules as $sftpModule) {
$portChecks[] = array('Host' 	=> $sftpModule['server'],
'Port' 		=> $sftpModule['serverport'] == 0 ? 22 : $sftpModule['serverport']	);
}

echo "\nStarting Port Tests:\n";
foreach ($portChecks as $portCheck) {
$ret = @fsockopen($portCheck['Host'], $portCheck['Port']);
if (!$ret) {
echo 'Port ' . $portCheck['Port'] . ' on Host ' . $portCheck['Host'] . " (IP: ".gethostbyname($portCheck['Host']).") cannot be opened. Please check!\n";
} else {
fclose($ret);
echo ".";
}
}

In der zweiten Version kann es aktuell auch noch etwas detaillierter die eigentlichen Protokolle nutzen und sich zB auf einem FTP-Server einloggen oder SMB-Pfade prüfen. Das geht über den eigentlichen Porttest hinaus, ist aber für unsere Einsatzzwecke durchaus interessant, um zB falsche Login-Informationen oder anderweitige Probleme aufzudecken.

Written by Michael Kliewe

Juni 29th, 2009 at 5:46 pm

Posted in PHP

Tagged with , , , ,