Dec 01

Работейки по новата ми CMS система - Control Depo 3 ми се наложи да имам малко по advanced конфигурационен файл, така че за в бъдеще да ми е по-лесно да се наместват различните части на системата. Преди ползвах стандартното за едно PHP приложение - един файл config.php:


// start config
$_CONFIG = array();

// database
$_CONFIG['db_host'] = 'localhost';
$_CONFIG['db_name'] = 'project';
$_CONFIG['db_user'] = 'root';
$_CONFIG['db_pass'] = 'password';

// languages
$_CONFIG['default_language'] = 'bg';
$_CONFIG['laguages'] = array('bg' => 1, 'en' => 2, /* ... */);

// session
$_CONFIG['session_salt'] = 'SD23aeda';
$_CONFIG['session_expire'] = 4*60*60;
// ... и така много много реда код

После тази глобална променлива( $_CONFIG) когато ми трябва се вика със global и се ползва. Обаче от една страна че не е много красиво така написано, но от друга и това с global просто прави кода малко разхвърлян(на английски имат страхотна дума за това - messy) .

Това което ми трябваше основно е може да имам няколко enviroment конфигурационни файла подобно на Rails. Другото важно нещо беше да се побира в един екран, така че да не се налага да скролирам и да мога с един поглед да виждам всичко което ми трябва. И естествено да не е много сложно и да работи достатъчно бързо.

Първо погледнах Zend_Config, защо все пак са component-based-framework и може директно да видя как работи конфигурационната им система. И общо взето това което видях не ми хареса въобще. Много ми напомня на LEGOs, Play-Doh, and Programming от Jamis Buck, за която бях писал преди време(даже има я качена на видео в confreaks заедно с цялото rubyconf 2008).
От една страна колко пъти ще искам да ползвам XML и INI за конфигурация?! От една страна php си има array(), с която идеално може да се запишат всички неща който ни трябват и да са достатъчно четими. От друга самото четене на огромен xml/ini/yml/… файл отнема време и ресурси и са доста по-бавни от прост php код.

Добре че поне тук от Zend са сложили подразбиране да се ползва само php масив. Но от тук дойде 2рото ми учудване. Това а именно самата работа на имплементация на Zend_Config. Идеята е доста проста подава се масив, който се обгръща(wrap) от Zend_Config обект и след това се работи само със Zend_Config обект. Пример:


// Create the object-oriented wrapper upon the configuration data
$config = new Zend_Config(array(
    'webhost'  => 'www.example.com',
    'database' => array(
        'adapter'   => 'pdo_mysql',
        'params'    => array(
            'host'      => 'db.example.com',
            'username'  => 'dbuser',
            'password'  => 'secret',
            'dbname'    => 'mydatabase'
        )
    )
));

// Print a configuration datum (results in 'www.example.com')
echo $config->webhost;

// Use the configuration data to connect to the database
$db = Zend_Db::factory($config->database->adapter,
$config->database->params->toArray());

// Alternative usage: simply pass the Zend_Config object.
// The Zend_Db factory knows how to interpret it.
$db = Zend_Db::factory($config->database);

Идеята като цяло е много хубава, и при добро желание може човек да си направи основната част класовете му приемат Zend_Config обекти. Но има едно НО и то доста голямо. Защо ние е това? Единствения плюс който се сетих е че може да се направи immutable config обект. Но за сметка на това ще има доста излишен код и много памет за съхраниението на Zend_Config обекти и техните атрибути. А и колкото и да обичам __set/__get магиите тук ми се виждат напълно излишни защото както в примера:


$config->database->params->toArray()
// Това би представлявало нещо такова като backtrace
$config->__get('database')   // магически се търси атрибута 'database'
$config->_data['database']   // това което се връща тук пак е Zend_Config обект, който ще го нарека $object
$object->__get('params')     // пак същото за
$object->_data['params']     // друг $object
$object->toArray();          // автоматично всеки Zend_Config обект, който се стрещне му се вика Zend_Config::toArray()
$object->_data               // това за което ни трябва

Zend_Config си има и своите плюсове(immutable, секции, default стойности) и може за някое наистина “enterprise” приложение с много разчленена конфигурация (и много стабилен и мощен сървър :) ) да върши работа. Но не е за мен и аз си предпочитам “голите” array(). Все пак

No code is faster than no code

- някъде го чух това.

Така че какво реших ? Ами в предишния ми пост - PHP tips - include и return, описах метода за връщане на стойности от php файл и реших него да ползвам плюс малко правила и подрежанки. Сега имам следните файлове:

  • /config/config.php - тук ще е основната конфигурация
  • /config/enviroment/ - в тази папка ще се съдържат конфигурационните файлове за различните среди за работа, като стандартно имам 3 вида среди
  • /config/enviroment/development.php
  • /config/enviroment/test.php
  • /config/enviroment/production.php
  • /config/mailer.php - тук ще има конфигурация за mail системата, тя е изнесена в отделен файл, защото тук няма само да се връщат config данни, а и ще се вързва към smtp сървър (ако трябва), ще се настройват достъпи и други подобни неща. За разлика от другите конфигурационни файлове mailer.php се вика само когато се налага да се изпращат писма не по-рано.
  • /config/routes.php - това са настройките ми за различните пътища, подобно на Rails (пак)

Това са различните конфигурационни файлове (поне за сега).


return array(
    'database'  => array(
        'engine'    => 'mysql',
        'host'      => 'localhost',
        'name'      => 'project',
        'user'      => 'root',
        'pass'      => 'password'
    ),
    'i18n'      => array(
        'default'   => 'bg',
        'languages' => array('bg' => 1, 'en' => 2, /* ... */)
    ),
    'session'   => array(
        'engine'    => 'cookie',
        'salt'      => 'vW34Aaasa',
        'expire'    => 4*60*60
    ), // на последния ред имам , защото при добавяне на нов ред да не ми се налага да я слагам
    // а и SVN/Git ще го сметне като изтрит ред и после добавен ред
    // ... още конфигурация ...
);
// /config/enviroment/development.php - примерно
return array(
    'database'  => array(
        'engine'    => 'mysql',
        'host'      => 'localhost',
        'user'      => 'root',
        'pass'      => '',
        'name'      => 'project_dev'
    ),
    'smarty'    => array(
        'compile_check'     => true,
        'force_compile'     => false,
        'debugging'         => false,
        'caching'           => false,
        'cache_lifetime'    => 0
    ),
    'logging'           => true,
    'display_errors'    => 1,
);

А в bootstrap-а имам просто това


// ENVIROMENT е просто константа в която казва в кой режим на работа е приложението
$_CONFIG = array_merge_recursive(include($_CFGDIR . '/config.php'), include($_CFGDIR . '/enviroments/' . ENVIROMENT . '.php'));

// ... код ...
// общо взето извличам всичко което ми трябва от $_CONFIG и го разпределям по обекти
// така че да не ми трябва повече $_CONFIG
// ... код ...

unset($_CONFIG /* заедно с още няколко вече не потребни ми променливи */);

Това решение ми се вижда най-елегантно, в моя случай. Може и да не може да се нарече “система” но е достатъчно надежно и ефективно да ми свърши работа. Все пак съм фен на Convention over configuration И ако ми се наложи да имам подобен на Zend_Config обект, с които да работя мисля да не променям основните неща, а просто този обект да използва ArrayAccess, но за това друг път, че сега май малко по-дълго стана от плануваното.

Надявам се някой да намери този пост полезен и ако съвети и идеи ще се радвам да ги чуя :) .

Nov 30

Изненадващо е колко много хора не знаят, че в PHP файловете може да връщат резултат с return, който може да бъде прочетен с include / require. Даже го има и в документацията на PHP, eто пример:


// file1.php
return array('key' => 'value');

// file2.php
$arr = include 'file1.php';

echo $arr['key'];

// резултат: value

Като тук вместо include, може да се използва require (единствената разлика между двете е грешката, която възниква при проблем с отварянето на файла). Но аз лично предпочитам да използва include само когато php скрипта връща резултат, a require когато добавям нещо.

Тук трябва да се обърне внимание на 2 неща:

  1. require_once / include_once ако има return връщат стойност само първия път в който са извикани, а после нищо не връщат. Но те по принцип е добре да се избягват, особено ако се връща резултат
  2. всяка променлива / функция / клас / … която е била декларирана във include файла (file1.php в примера) си остава записана и достъпна. Така че ако в file1 се декларира $name = ‘Radoslav’; например във file2.php, $name ще е пак ‘Radoslav’. Затова е добре да се unset -ват всички ненужни глобални променливи, които не са нужни
Sep 17

Преди време писах за PHP тестване в - PHP Unit testing. И сега искам да споделя един метод на тестване, с който използвах вече няколко пъти, което ми спести писане и като цяло се получи много добре.

Наскоро имах да тествам един базов клас и неговите наследници. Базовия клас се казва Session_Base и представлява абстрактен клас, в който съм си дефинирал основните операции за работа със сесии - set, unset, get, cut ( това е смесица между get и unset ), start, stop, fingerprint и други. Имам и няколко класа които го наследяват:

  • Session_Default - това си е просто нормалната php сесия и $_SESSION
  • Session_File - разликата с Session::Default е само че, този файл се грижи за записване и четене на session файловете
  • Session_Database - Session::File но за база данни
  • Session_Custom - Това е версията на стария ми php session клас ( много стара версия може да му видите на http://www.phpclasses.org/browse/package/2879.html )

Така това прави най-малко 4-5 TestCases. които във своята основа са едни и същи тест. В който се проверяват get/set/unset….open/write/read….

Затова реших да направя така:


// Test_TestCase си е един мой клас в който си слагам помощни работи и екстендва PHPUnit_Framework_TestCase
abstract class SessionBaseTest extends Test_TestCase {
private $sess;
function setUp();

function tearDown(){
$this->sess = null;
}

function testSettingVariable() { /* .... */ }
function testGettingVariable() { /* .... */ }
/* oще тестове */
}

Това ще ми служи като база за останалите тестове на сесията. И при едно наследяване PHPUnit ще изпълни и методите (тестовете).


// Test_TestCase си е един мой клас в който си слагам помощни работи и екстендва PHPUnit_Framework_TestCase
abstract class SessionDefaultTest extends Test_TestCase {
private $sess;
function setUp(){
$this->sess = new Session_Default();
}

// тестове специално към Session_Default();
}

Подобни тест класове си правя и за останалите класове. setUp() е абстрактен за да може в $this->sess да се сложи съответния клас.

Подобен метод използвах и в друга ситуация, разликата беше че имах редица от наследяващи се класове - RecordValidator -> FormValidator -> ActiveRecordValidator като всеки клас добавя нова функционалност, но основните действия, не трябваше да се променят. Тук направих TestCase за RecodValidator, после TestCase- за FormValidator го наследи и накрая ActiveRecordValidator наследи FormValidatorТest. Така си хванах сумати грешки и си спестих излишно писане на код ;)

Sep 07

Онзи ден трябваше да кача файлове на ftp сървър, нищо интересно. Проблема е че са ми много досадни повечетo FTP клиенти за Windows, просто една свястна FTP програма няма.

Днес имах малко свободно време и реших да направя нещо, което от доста време си мисля. А то е просто скиптче което само да го стартирам и то да си качи каквото трябва на избрано от мен място на ftp сървър (после мисля да го направя да прави и други работи, но за друг път)

Избрах си да го напиша на Ruby или по-точно JRuby, защото така и така го имах инсталирано и все се чудех за какво да го ползвам. Оказа че Ruby има наистина богат набор от стандартни класове. Поне за сега възползвах от Logger и Net::FTP.

Много харесвам Ruby като език, особено block-овете :) Но е малко по различен от това с което съм работи до сега, например с PHP да направя нещо да обикаля директория и нейните под-директори е просто нещо такова:


function read_dir($directory){
	foreach(new DirectoryIterator($directory) as $file){
		if ($file == '.' || $file == '..') continue;

		if ($file->isDir()){
			read_dir($file->getRealPath());
			// do something with directory
		} else{
			// do something with file
		}
	}
}

read_dir('my_dir');

или ползвам RecursiveDirectoryIterator, за който все забравям че съществува.

Пробвах да напиша нещо на същия принцип с ruby, но не стана много добре, даже никак добре не стана. Работеше, но бях се по оплел със функции, Proc :( Порових малко в net-a и с малко работа  написах това:


def read_dir (path, &block)
	paths = [path]
	while file = paths.shift
		yield file.dup
		next unless File.directory? file
		for f in  Dir.open( file )
			next if f == '.' or f == '..'
			paths.unshift File.join(file,  f)
		end
	end
end

read_dir('my_dir') do |file|
	# do something with file
end

Винаги съм мислил, че за да се обиколи директория и нейните под-директории трябва някаква рекурсия или нещо такова. А се по този начин просто се пазят всичките директории в масив(или опашка) и просто се обикаля докато се свърши масива.

Обаче се оказа че в Ruby вече има такова нещо - Find:


require 'find'
Find.find('my_dir') do |file|
	# do something with file
end

Е малко това Find.find() не ми харесва как изглежда, но само със едно “alias :find :from”, може да имаме Find.from() което да е същото :) А ако сте фенове на Monkey patch може да се вложи Find.find във File класа и да се направят още доста “магии”.

Sep 01

Днес(т.е. на 27, просто от тогава този пост седи във draft-a) прочетох това:

Database helper for PHPUnit

Във него се показва интересен метод за работа със PHPUnit и тестване на база данни. Този пост, ме “вдъхнови” и мен да споделя, как аз работя със PHPUnit.
Първо директорийната структура. В една папка тест имам следните неща:

  • benchmarks/ - съдържа различни benchmark тестове ( за тях някои друг път по-подробно)
  • fixtures/ - fixtures на тестовете
  • reports/ - code coverage докладите на xdebug
  • unit/ - самите unit тестове
  • enviroment.php - различни настройки

Под папките на unit, отговарят на разлините testcase -ове. Например testcase за ActiveRecord::Relations::Many се намира в unit/ActiveRecord/Relations/ActiveRecordRelationManyTest.php. Това което е по-интересно в случая е, че в повечето папките имам един файл: setup.php този файл се изпълнява преди тестовете от съответния testcase да се активират. Идеята му е се заредят класовете, файловете, да се изчисти базата данни преди тестване.

За тестове съм си направил едни клас Test_TestCase който наследява PHPUnit_Framework_TestCase и в него съм си добавил няколко помощни метода за тестване.

И накрая имам един клас Test_Runner който стартира тестовете. Кода на Test_Runner може да намерите тук.

Кода се използва така:


// константите DS и TESTS_DIR се използват при инициализация на Test_Runner
define('DS', DIRECTORY_SEPARATOR);
define('TESTS_DIR',  		ROOT_DIR . DS . 'test' . DS . 'unit');

$r = new Test_Runner();
$r->run($options);

Също така използвам и 2-3 php скриптчета за генериране на скелета за тест класа, директориите, setup.php файловете и други.

По принцип, не харесвам много PHPUnit, но за нещастие няма за сега по-добро нещо за тестове в PHP света, до колкото знам. A и ако добре се документира и структурира тест кода, нещата стават доста по-поносими.

Ако някои има идеи и съвети за PHPUnit или тестването като цяло, да се чувства поканен да остави коментар :)