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 -ват всички ненужни глобални променливи, които не са нужни
Nov 15

Това е втория пост за ControlDepo 3 Widgets - Behaviors (по план имам още един :) ).

Преди време ми се наложи да “освежавам” един стар проект използващ една от първите версии на CD3.Behaviors. Тази стара версия имаше само нормален селектор | event-селектори. Там забелязах че много често ползвам pattern (това ако някой на български ми го каже как е, ще съм много благодарен) а именно:


CD3.Behavoirs({
	'#container': function(contaner){
		contener.select('a').invoke('click', doSomeAction);
		contaner.select('a').invoke('mouseover', doSomeOtherAction);
		// .. и така си избирам всичките под елементи на #container и да им добавям event handler
	}
});

Това е примерен код за простичка javascript галерия, която има два бутона предишна / следваща картинка както и списък с thumbnail-и от който пак може да се покаже голямата снимка.  selectImage просто от a таг взема href атрибута и го показва като снимка с ефект и т.н, но то не е важното в случая.


CD3.Behaviors({
	// code
	'#gallery': function(){
		// при натискане на стрелката за предишна снимка да ...
		this.down('a.prev').observe('click', function(){
			var thumbs	 = $('thumbs'),
				selected = thumbs.down('a.selected');

			// покажи или предишнина или послената снимка
			selectImage(((selected && selected.up('li').previous('li')) || thumbs.select('li').last()).down('a'));
		});

		// при натискане на стрелката за следваща снимка да ...
		this.down('a.next').observe('click', function(){
			var thumbs	 = $('thumbs'),
				selected = thumbs.down('a.selected');

			// покажи следващата или първата снимка
			selectImage(((selected && selected.up('li').next('li')) || thumbs.select('li').first()).down('a'));
		});

		// тук правя много прост event delegation
		// избирам натиснатия a таг и викам selectImage с него
		$('thumbs').observe('click', function(e){
			e.stop();
			var a = e.findElement('a');
			if (a) selectImage(a);
		});
	}
	// code
});

Естествено някой от случаите могат да бъдат избегнати с event-delegation | инстанциране на класове, но  ми дойде друга, по-добра (според мен) идея. Добавих нов метод към CD3.Behavoirs - when. CD3.Behavios.when приема 2 параметъра 1вия е selector, а другия е hash object с behavoirs. Идеята се намерят елементи отговарящи на подадения селектор, се добавят и съответните behavoirs. Единственото по особеност тук е че selector-ите на behavoir-ите не се търсят в контекста на document а в контекста на елемента на който отговаря selectora подаден като първи аргумент.

И така със CD3.Behavios.when примера със новинарската джаджа изглежда така:


// ако съществува елементи #galley
// добави слените behaviors от контекста на #gallery
CD3.Behaviors.when('#gallery', {
	// при натискане на стрелката за предишна снимка да ...
	'a.prev:click': function(){
		var thumbs	 = $('thumbs'),
			selected = thumbs.down('a.selected');

		// покажи или предишнина или послената снимка
		selectImage(((selected && selected.up('li').previous('li')) || thumbs.select('li').last()).down('a'));
	},
	// при натискане на стрелката за следваща снимка да ...
	'a.next:click': function(){
		var thumbs	 = $('thumbs'),
			selected = thumbs.down('a.selected');

		// покажи следващата или първата снимка
		selectImage(((selected && selected.up('li').next('li')) || thumbs.select('li').first()).down('a'));
	},
	// избирам натиснатия a таг и викам selectImage с него
	'thumbs:click': {
		a: function(е){
			е.stop();
			selectImage(this);
		}
	}
});

Както се вижда кода е почти същия но сега изглежда доста по-добре и по-четим. В последните ми няколко по-натоварени с javascript проекти си структурирам джаджите в отделни извиквания на when и нещата изглеждат доста добре.

Oct 16

Тези дни бях претрупан от работа, имах 2 deadline-а, семестъра в университета е в разгара си, просто страшна работа. И за това не съм писал от 4ти.

Днес малко се по освободих, предадох проектите, преборих се, до колкото мога с нашата образователна система. Но за малко да пропусна 3 важни събития: Adobe Flash Player 10, Новата серия MacBook-ци на Apple и Silverlight 2 (то 1 имаше ли ?!, шегувам се). То може и още важни неща да има (в Google Readera ми в момента има точно 290 не прочетени неща, като преди това поне 100-200 ги зачекнах направо). Но тези 3те доста ме впечатлиха.

flash_cs3_logoОт доста време чакам Flash 10. Вече има native 3D, което значи че доста по-лесно от преди може да се преминава от 2D в 3D обект. И тъй като Adobe работиха и със доста от създателите на Flash 3D фраймуроци, резултатите са ясни - по-бързо и ефективни 3D-та. Друго ново е възможността да се записват файлове през флаша, което очаквам да стане източник на поне 3-4 security holes. Също има вече нов Text rendering engine. Много се надявам че flash player 10 до Коледа ще е покрил 85-90-95% flash потребителите, зада можем да се възползваме от цялата нова функционалност на flash 10. Adobe AIR още при следващият update ще мине на flash 10. Пълния списък с промените, добавките и bug fix-овете може да се види тук. Има го и на китайски, но на мен за сега ми стига само краткото review от SitePoint.



silverlightТам имаше много интересен коментар от Lee, който ме накара да се посмея малко :D . Но и да погледна малко към Silverlight 2, единствения уж конкурент на Flash ( JavaFx не го смятам още за нещо сериозен конкурент). Не че някои ден смятам да правя нещо на SilveLight, но знае ли човек. А там имаме:

  • Text rendering engine
  • Control Pack - нови компоненти TextBox, ComboBox, StackPanel, Grid, Panel, ScrollViewer, Calendar, DatePicker … всичко стандартно
  • .NET Framework - още повече интеграция с .NET
  • Deep Zoom

… и други, не ги намерих търсих в списък.



Вчера освен RIA технологиите, Apple също показаха че не спят и че ще имаме стабилни машини на които да подкарваме Flash приложенията ( Silverlight го нямам даже инсталиран ), а именно новия MacBookPro:

Тъкмо си мислех, че Apple няма как да го направят още по красив :) Въпреки че като го гледам няма как да не се сетя за тази статия в Signal vs Noise:

How Apple’s small things influence their big things - (37signals)

На официялния сайт на Apple може да видите видео от представянето и малко клипче за това как се произвеждат MacBook-сите.

п.п. Не си бях и поглеждал пощата от 4-5 дни и сега видях 2 писма дошли едно след друго:

:D кои да избера ?! …. то е ясно.

Oct 04

Днес имах малко свободно време и реших да погледна малко един проект, и по-точно javascript честа от него. И се спрях на един простичък Tab панел, нищо особено, но нещо не ми хареса как изглеждаше JavaScipt-а. HTML изглежда така:


<div class="tab-panel">
	<ul class="tab-header">
		<li class="selected">tab 1</li>
		<li>tab 2</li>
		<li>tab 3</li>
	</ul>
	<div class="tab-content">tab 1 content</div>
	<div class="tab-content" style="display: none;">tab 2 content</div>
	<div class="tab-content" style="display: none;">tab 3 content</div>
</div>

За да направя това таб панел използвах просто тази функция:


function tabulize(panel){
	// слагам div.tab-content вместо .tab-content,
	// защото когато селектора има само клас селектира всички елементи и ги проверява,
	// а ако се сложи div.tab-content се селектират се всички div-ове и те се проверяват,
	// което е по-бързо
	var elements	= panel.select('div.tab-content'),
		buttons		= panel.down('ul').select('li');

	// дефинирам тази функция тук,
	// защото искам да имам достъп до elements, buttons
	function activate(item, key){
		// тук 1во се маха клас selected от всички бутони,
		// и се скриват всичките div.tab-content
		buttons.invoke('removeClassName', 'selected');
		elements.invoke('hide');

		// после се показва избрания елемент и се слага клас selected на избрания бутон
		elements[key].show();
		item.addClassName('selected');
	}

	// на всеки бутон се добавя eventhandler за click,
	// който ще подава на activate избрания елемент и неговия номер във buttons масива
	buttons.each(function(item, key){
		item.observe('click', activate.curry(item, key));
	});
}

Това е доста простичко, но и трудно за промени като динамично добавяне на съдържание, ефекти и други. Затова реших да го направя малко по-OOП, като запазя основните идеи:


var TabPanel = Class.create({
	// това е конструктора
	initialize: function(panel){
		panel = $(panel);
		// избираме отново elements и buttons, но ги записваме като инстанс променливи
		this.elements = panel.select('div.tab-content');
		this.buttons = panel.down('ul').select('li').each(function(){
			// единствената разлика е че active не е private функция и инстанс метод
			// и за това тук ползваме bind, а не curry
			item.observe('click', this.activate.bind(this, item, key);
		}.bind(this));
	},
	// active, вече е метод и само "this." e разликата
	activate: function(item, key){
		this.buttons.invoke('removeClassName', 'selected');
		this.elements.invoke('hide');
		this.elements[key].show();
		item.addClassName('selected');
	}
});

Сега става малко по-тежко, но и доста по-extendable и податливо на бъдещи промени. Но тук видях нещо, което пак не ми се хареса е че колкото таб бутони имам толкова пъти и в двете версии викам observe и реших да видя как ще изглежда това, като добавим малко event-delegation, за което трябваха само две промени:


var TabPanel = Class.create({
	initialize: function(panel){
		panel			= $(panel);
		this.elements	= panel.select('div.tab-content');
		this.buttons	= panel.down('ul').select('li');
		// първо променяме тук
		// слагаме click eventhandler на ul (списъка с бутоните)
		// вътре проверяваме дали li елемента, който сме натиснали (ако има такъв)
		// е във масива с бутоните, ако е така активираме табулацията с този номер
		panel.down('ul').observe('click', function(e){
			var key = this.buttons.indexOf(e.findElement('li'));
			if (key != -1) this.activate(key);
		}.bind(this));
	},
	// второто нещо което променяме е тук
	// махаме item параметъра, и на го заменяме с this.buttons[key]
	activate: function(key){
		this.buttons.invoke('removeClassName', 'selected');
		this.buttons[key].addClassName('selected');
		this.elements.invoke('hide');
		this.elements[key].show();
	}
});

И поне на този етап съм доволен, при нужда мога да добавя destroy, addTab и други неща :) А как инстансирам TabPanel-а ? Ами със CD3.Behaviors:


CD3.Behaviors({
	'div.tab-panel': TabPanel
});