code


2010
24
jan

Doctrine és Zend Framework integráció

Jó ideje Zend Frameworkkel gyártom a “kontentot” és az elmúlt két év tapasztalatából azt a fájdalmas konzekvenciát kellett levonnom, hogy a Zend MVC-jének modell rétegével kínkeserves meló egy korrekt Domain Modell leprogramozása, a modell változásainak követése és karbantartása. Amikor a feladat szükségessé teszi egy komolyabb adatbázis-struktúra kezelését akkor a Zend_Db időről-időre csődöt mond. Hiába próbáltam hosszú időn keresztül elkészíteni a tökéletes adatbázis absztrakciós réteget, sajnos sosem jártam igazi sikerrel. Kipróbáltam mások elképzeléseit is (Model Infrastructure, Domain Model Programming With the Zend Framework) de hosszabb távon, az igények növekedésével ezek a megvalósítások mindig zsákutcának bizonyultak. Nem arról van szó, hogy a Zend modell rétege nem működne, sőt egyszerű modellek esetén nagyon kényelmes és gyors is. Arról van szó, hogy egy megfelelően bonyolult domain modell karbantartása indokolatlanul sok feladatot hárít a fejlesztőre, így sokszor a napi maintenance munka nagy része azzal megy el, hogy az alkalmazás működésében vagy az adatmodellben bekövetkezett változások miatt az domain modellt kell püfölni. Egy agilis fejlesztési folyamat során ez sajnos nem megengedhető, túlzottan sok időt és energiát emészt fel.

A legutóbbi nagyobb projekt során az egyik kolléga hívta fel a figyelmet arra, hogy a jelenlegi – Zend_Db-vel készült – modell struktúra enyhén szólva is kurva bonyolult és átláthatatlan. Mivel napi szinten benne voltam a modell karbantartásában és fejlesztésében ez szinte fel sem tűnt, de amikor bármi miatt módosítani kellett az egy rémálom volt. A javításról nem is beszélve. Eldöntöttük, hogy ideje nyugdíjazni a Zend_Db-t a projekt keretein belül és alternatíva után kezdtem kutakodni. Így került képbe a Doctrine, amit már jó ideje figyelemmel kísértem és konkrét tapasztalataim is voltak vele, de Zend Framework integrációval még sosem próbálkoztam. Találtam egy remek bejegyzést Eric Leclerc blogján Doctrine 1.2 is Zend Framework friendly címmel, valamint a zendcasts.com-on (Podcast) található egy screencast sorozat is a témában:

A Doctrine egy object relational mapper (meg sem próbálkozok a fordításával) amely tulajdonképp az adatbázis absztrakciós réteg tetején ül és integrálódik az OOP-s környezetbe. A parancssori interfész segítségével képes adatbázis sémákból php modelleket generálni (és vica versa), tesztadatok betöltésére vagy akár unit tesztelésre is. Szóval tényleg nagyon erős kis eszköz, igazán szerethető. A Symfony projekt is ezt használja például alapértelmezett ORM-ként a Propel mellett. Ami pedig a legszebb, hogy nagyon gyorsan megtanulható és üzembe állítható. Ezt mi sem bizonyítja jobban, mint az imént említett projekt, ahol a Zend_Db alapú modellek elkészítése több hétig tartott, a Doctrine migráció pedig mindösszesen egyetlen napig (bár az is igaz, hogy egy meglehetősen hosszú nap volt).

Lássuk, hogyan is történik ez integrálás a gyakorlatban. A standard Zend projekt fában a képen látható mappaszerkezetet kell létrehozni. A Doctrine projekt honlapjáról letöltött csomag lib könyvtárából másoljuk be a komplett Doctrine és vendor foldereket a projektünk library könyvtárába a Zend mellé valamint a doctrine.php-t szintén a library folderbe. Ezt követően az application.ini-ben kell némi konfigurációt elvégezni:

autoloadernamespaces[]      = "Doctrine_"
doctrine.connection_string  = "mysql://user:password@mysqlhost/dbname"
doctrine.data_fixtures_path = APPLICATION_PATH "/../doctrine/data/fixtures"
doctrine.models_path        = APPLICATION_PATH "/models"
doctrine.migrations_path    = APPLICATION_PATH "/../doctrine/migrations"
doctrine.sql_path           = APPLICATION_PATH "/../doctrine/data/sql"
doctrine.yaml_schema_path   = APPLICATION_PATH "/../doctrine/schema"
doctrine.generate_models_options.pearStyle              = true
doctrine.generate_models_options.generateTableClasses   = false
doctrine.generate_models_options.generateBaseClasses    = true
doctrine.generate_models_options.baseClassesDirectory   = null
doctrine.generate_models_options.classPrefixFiles       = false
doctrine.generate_models_options.classPrefix            = 'Model_'
doctrine.generate_models_options.baseClassPrefix        = 'Base_'

Majd a Bootstrap.php-ban kell a Doctrine-hez kapcsolódó initet elkészíteni, hogy az alkalmazás szintjén müködjön:

public function _initDoctrine()
{
    require_once 'Doctrine.php';
    $autoloader = Zend_Loader_Autoloader::getInstance();
    $autoloader->registerNamespace('sfYaml')->pushAutoloader(array('Doctrine', 'autoload'), 'sfYaml');
    $doctrineConfig = $this->getOption('doctrine');
 
    $manager = Doctrine_Manager::getInstance();
    $conn = $manager->openConnection($doctrineConfig['connection_string']);
    $conn->setCharset('utf8');
    return $manager;
}

Az application/cli_bootstrap.php-ba a következő kerül:

<?php
// Define path to application directory
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH', dirname(__FILE__));
 
// Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));
 
set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APPLICATION_PATH . '/../library'),
    get_include_path(),
)));
 
/** Zend_Application */
require_once 'Zend/Application.php';
 
// Create application, bootstrap, and run
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);

Készíteni kell a projekt mappában egy scripts/doctrine-cli állományt a következő tartalommal:

#!/usr/bin/env php
<?php
define('APPLICATION_ENV', 'development');
require realpath(dirname(__FILE__) . '/../application/cli_bootstrap.php');
 
$application->getBootstrap()->bootstrap('doctrine');
$options = $application->getOption('doctrine'); 
 
$cli = new Doctrine_Cli($options);
$cli->run($_SERVER['argv']);

Majd futtathatóvá kell tenni. Ez lesz a parancssori interfész amelyen keresztül utasításokat lehet adni az ORM-nek. Ezzel a Doctrine teljesen beágyazódik a Zend Frameworkbe és a projekt honlapján lévő dokumentációk alapján már lehet is gyártani a szebbnél szebb modelleket és tolni a kódból a DQL-t :)


2008
06
ápr

String típuskonverzió kérdés

Azt olvastam a Zend PHP 5 Certification Study Guide-ban, hogy egy string típusú változó értékének növelésekor 1-et kapunk vissza eredményül, mert a ++ operátor integer típusúvá konvertálja a string értékét (amely így nulla értéket vesz fel) majd azt növeli egyel.

$a = 'Test';
echo ++$a;

Nálam mégis Tesu szerepel a kimenetben. Miért?


2008
03
ápr

Leopard, BIND, Apache és vhost

Úgy kezdődött, hogy XAMPP-ot telepítettem a gépemre, mert a Leo-s php-ba nincs beleforgatva a MySQL PDO támogatás, a MySQL szervert pedig nem lehet a System Preferences-ből indítani leállítani (meg még a jogosultságokkal is küzdeni kell). Méretes oboa. Egyszer ugyan már újraforgattam a php-t is és az apache-ot is 10.5-re, de a tökömnek sincs kedve ezt minden frissítés után eljátszani, főleg ha van MAMP meg XAMPP.

Aztán felmerült az igény, hogy ha már ilyen jó kis fejlesztői szervert varázsoltunk a MacBookra XAMPP-al, Bazaarral meg Eclipse PDT-vel (az utóbbi kettőről majd máskor bővebben) akkor jó lenne magát a fejlesztést is kényelmesebbé tenni. Ennek az első lépése az lenne, hogy ne kelljen minden egyes új projekt indulásakkor a hosts és a virtual-hosts beállításokat piszkálgatni. A hosts állománynak az a legnagyobb baja, hogy nem eszi meg a wildcard karaktereket. Így mindig be kell püfölni az új aldomain (pl.: mynewproject.devserver: 127.0.0.1) elérhetőségét. Majd ezután elő kell rántani az apache virtuális hosztokra vonatkozó konfigját is és oda is fel kell venni a mynewproject-et www-root-ostól-, mindenestől. Na ebből lett nagyon elegem!

Szerencsére az OS X-ben alapból megtalálható a BIND névszerver, csak nincs aktiválva. Bővebben lehet erről olvasni itt. Bár még Tigeres időkben készült az írás, Leopard alatt is tökéletesen működik. Kivonatosan az alábbiak szerint élesíthető a BIND:

# sudo -s
# rndc-confgen > /etc/rndc.conf
# head -n5 /etc/rndc.conf |tail -n4 > /etc/rndc.key

A /etc/named.conf-ba:

zone "devserver" IN {
        type master;
        file "devserver.zone";
        allow-update { none; };
};

A /var/named/devserver.zone-ba:

$TTL    86400
$ORIGIN devserver.
@       1D IN SOA    @ root (
            42    ; serial (galaxis uk.)
            3H    ; refresh
            15M   ; retry
            1W    ; expiry
            1D )  ; minimum
        1D IN NS   @
        1D IN A     127.0.0.1
* IN A 127.0.0.1

Végezetül a named indítása:

# sudo launchctl load -w /System/Library/LaunchDaemons/org.isc.named.plist
# sudo /usr/sbin/named

Hát nem gyönyörű? A BIND szerverünk már üzemel is és feloldja a *.devserver neveket, csak hozzá kell adni a DNS szerverek listájához:

Már csak egy virtual hostot kell belőni a következő módon: az én fejlesztői notebookomon a ~/dev könyvtár tartalmazza a web alkalmazásokat, minden egyes alkönyvtár egy külön projekt így szeretem ezeket külön aldomainen elérni. Így tehát egyetlen virtual hostot vettem csak fel a /Applications/xampp/etc/extra/httpd-vhosts.conf-ba:

<VirtualHost *:80>
    ServerAdmin webmaster@devserver
    DocumentRoot "/Users/vbali/dev"
    ServerName devserver
    ServerAlias *.devserver
    ErrorLog "logs/devserver-error_log"
    CustomLog "logs/devserver-access_log" common
</VirtualHost>

Ezáltal minden *.devserver-hez irányított kérés a “/Users/vbali/dev” könyvtárban landol, ahol egy .htaccess várja az érdeklődőt:

RewriteEngine On
RewriteCond %{HTTP_HOST} ^(.*)\.devserver$
RewriteRule (.*) /%1/$1 [L]

ami semmi mást nem csinál csak az aldomainként megadott alkönyvtárba irányítja a kérést. Például a mynewproject.devserver-nek intézett kérés a “/Users/vbali/dev/mynewproject” mappából kerül kiszolgálásra. That’s all!


2007
28
dec

Ajax és a Zend Framework

Szerettem volna egy egyszerű, minimalista kis alkalmazást készíteni a Zend Framework segítségével, Ajax-os hívások kiszolgálására, de nem találtam egyetlen példát sem ami valóban az egyszerűséget és a lényegi működést lett volna hivatott bemutatni. Egészen addig amíg bele nem botlottam William Graham Ajax 101: A Simple Example of Using Ajax with the Zend Framework című írásába. Ezen a példán dolgozgattam, gyurmázgattam egy kicsit az alábbi eredménnyel:

Tényleg csak a lényeg van benne, semmi extra. A kliens oldalon begépelt szöveget megfordítja vagy változatlanul hagyja szerver oldalon, persze oldal újratöltés nélkül. Persze az érdemi munkát a Prototype library végzi, de a Zend_Json osztály is besegít a munkába. A lényegi részek az alábbiak:

index.php

<?php
error_reporting(E_ALL|E_STRICT);
date_default_timezone_set('Europe/Budapest');
 
set_include_path('.' . PATH_SEPARATOR . './library'
	. PATH_SEPARATOR . './application/models/'
	. PATH_SEPARATOR . get_include_path());
 
include "Zend/Loader.php";
 
Zend_Loader::loadClass('Zend_Controller_Front');
 
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$frontController->setControllerDirectory('./application/controllers');
 
$frontController->dispatch();

IndexController.php

<?php
 
class IndexController extends Zend_Controller_Action
{
	function init() {
		$this->view->baseUrl = $this->_request->getBaseUrl();
		Zend_Loader::loadClass('Zend_Debug');
		Zend_Loader::loadClass('Zend_Json');
	}
 
	function indexAction() {
		$this->view->title = "Zend Ajax 101";
	}
 
	function getDataAction() {
		$this->_helper->viewRenderer->setNoRender();
		$state = $this->_request->getParam('state');
		$data = Zend_Json::decode($state);
		sleep(1);
 
		switch ($data['func']) {
			case 'reverse':
				echo "The text reversed: " . strrev($data['intext']) . "!!";
				break;
			default:
				echo "The text received: " . $data['intext'] . "!!";
		}
	}
}

index.phtml

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title><?php echo $this->title; ?></title>
	<link rel="stylesheet" href="public/styles/boxy.css" type="text/css" media="screen" title="no title" charset="utf-8">
	<script type="text/javascript" src="public/scripts/prototype.js"></script>
	<script type="text/javascript" src="public/scripts/ajax_funcs.js"></script>
</head>
<body>
	<p>
		<input type="text" name="intext" value="" id="intext" />
		<input type="button" name="buttonReverse" value="Reverse" id="buttonReverse" OnClick="callAjax('intext', 'reverse')" />
		<input type="button" name="buttonNormal" value="Normal" id="buttonNormal" OnClick="callAjax('intext')" />
	</p>
	<img src="public/images/loader.gif" id="update" style="display: none"/>
	<span id="hello">boring pre-Ajax-call text ...</span>
</body>
</html>

ajax_funcs.js

function callAjax(s, method)
{
	if (method===undefined) method='';
	if (method == 'reverse')
		var data = {func: 'reverse', intext: $F(s)};
	else
		var data = {func: '', intext: $F(s)};
	var myAjax = new Ajax.Updater('hello', 'index/getData',
		{ 
			method: 'post', 
			parameters: {state: Object.toJSON(data)},
			onCreate: function(transport) {
				$('update').show();
				$('hello').innerHTML = "Loading...";
			},
			onComplete: function(transport) {
				$('update').hide();
			} 
		}
	);
}

2007
26
dec

FYI: content:encoded olvasása

Küzdöttem a Zend Framework-kel, hogy beolvassa a ‘content:encoded’ tag-eket egy RSS feedből. Az alábbi kóddal sikerült:

public function indexAction()
{
	$ns = array
	(
	        'content' => 'http://purl.org/rss/1.0/modules/content/',
	        'wfw' => 'http://wellformedweb.org/CommentAPI/',
	        'dc' => 'http://purl.org/dc/elements/1.1/',
	);
 
	Zend_Loader::loadClass('Zend_Feed');
	try {
		$rssFeed = Zend_Feed::import('http://blog.linuxforge.hu/feed');
	} catch (Zend_Feed_Exception $e) {
		echo "Exception caught importing feed: {$e->getMessage()}\n";
		exit;
	}
 
	$channel = array(
		'title'			=> $rssFeed->title(),
		'link'			=> $rssFeed->link(),
		'description'	=> $rssFeed->description(),
		'items'			=> array(),
	);
 
	$i = 0;
	$xml = new SimpleXMLElement($rssFeed->saveXML());
 
	foreach ($rssFeed as $item) {
		$content = $xml->channel->item[$i++]->children($ns['content']);
		$channel['items'][] = array(
			'title'			=> $item->title(),
			'link'			=> $item->link(),
			'description'	=> $item->description(),
			'content'		=> trim($content->encoded),
		);			
	}
	$this->view->channel = $channel;
}