Постове 6 - 10 от 19 с таг Coding

Aug 17

Днес почетох този пост във veselin.bg, пробвах в един коментар да събера най-основните “стандарти” по който пиша PHP код. Обаче коментарът доста набъбна и реших да го направя като пост.

В PHP за разлика от повечето (да не кажа всички) програмни езици няма точно установен стандарт структориране и писане на кода. Донякъде това се дължи на самата история на езика и това, че написан на PHP3 код може да върви дори и на PHP6. От друга страна PHP се води най-лесния за учене език, точно защото каквото и да напишеш все ще ти излезе резултат. Тази страна на PHP колкото и позитиви да има, като цяло според мен е грешка на самия език и придава малко статус “аматьори” на PHP програмистите му.

В послените години започват да се оформя що годе няколко стандарта на писане, но като цяло поне според мен докато PHP5.3 – PHP6 не започне да се ползва масово нещата няма много да се променят.

Основния начин да се започне да се работи по стандарт е да се работи с някой PHP framework, защото всички те си имат свой достатъчно добре дефинирани “стандарти” и самия framework ви кара да пишете по неговия стандарт за да изглежда кода ви добре. И тъй като никой PHP framework неможа да ме спечели, аз си направих мой. Който вече от 1 година се ползва във всички проекти които правя за Pixeldepo ( и които са на PHP разбира се ) и съм страшно доволен от това как работи. В неговите coding conventions съм “взаимствал” доста идеи, главно от  Zend Framework и CodeIgniter.

Именуване на променливи и функции и методи.

Променливите и методите ми са със CamelCase, фунцкиите са с подчертавки. Което е малко старанно за повечето хора, но поне според мен има известна логика ( иначе нямаше да пиша по този начин де :) ). Самия PHP, като език ако се погледне дългия списък със фунцкии ще се види че основно те са с почертаване и когато се ползва фунция инстиктивно знам че е такава. Докато класовете  (повечето от тях) използват CamelCase стандарт подобно на Java и когато се работи с класове изглеждат някак естествено. По принцип използвам много малко фунции и обикновенно те са просто wrapper/shortcut към някой клас.

Не използвам долни черти за разделянето на private променливи фунции. Пример за променлива / фунция / метод

$name = '';
$totalPrice = "";

function url_for($params){ /* ... */ }

class Foo {
public function isValid(){ /* ... */ }
}

Именуване на класове

Начина на именуване на класовете в Zend Framework, ми допадна най-много, от висички останали които бях гледал в различни PHP-та. Общо взето идеята е в името на класа да се съдърже “namespece” му. И същевременно класа да се намира в директория, пътя на която отговаря на това име. Примери:


// намира се в ActiveRecord/Plugin/Attachable.php
class ActiveRecord_Plugin_Attachable {}

// намира се в Model/Validation/Errors.php
class Model_Validation_Errors {}

// намира се в Tool/Script/Genererate/Applition.php
class Tool_Script_Generate_Application {}

Това е много ползено за _autoload на класове и доста ще улесни бъдеща интеграцията на PHP5.3 – PHP6 namespace-овете. Само да вметна, че доста хора не го знаеха за autoload, най-добре да се ползва spl_autoload.

Като тук проблема с абстрактните класове го решавам, като ако класа е абстрактен се кръщава Base. Тъй като Zend имаха проблем със класове като Zend_Controller_Plugin_Abstract, който в щеше да стане просто клас Abstract от PHP6 и щеше да е в namespace Zend\Controller\Plugin.  А Abstract е ключова дума и неможе да има клас който се казва така. При мен такъв клас се казва просто Base.

С подобна структура на класовете е много удобна и въпреки че много хора казват, че с такива имена писането става трудно.  Има доста техники с който се избягва писането на целите имена. Като пример ще дам моята script функция която изпълнява script  класове.

script('generate:model'); // Прави клас Tool_Script_Generate_Model го изпълнява
script('db:dump:data'); // Прави клас Tool_Script_Db_Dump_Data и го изпълнява

// като могат много лесно се добавят плъгини:
script('your:custom:script'); // Това е клас Tool_Script_Your_Custom_Plugin

В заключение…

Основната идея на стандартите е не само вие да може да си разчетете кода след 1-2 месеца, но и други хора да могат да работят над него. Също така е важно и да не ви е толкова срам от него след време, което рядко ще стане, но е важно да се опита. Kода да изглежда естествено в средата в която работите било то PHP / Ruby / Java и т.н. За PHP това не е лесно, защото самия език не изглежда естествено понякога.

За стандарти за писане лично аз препоръчвам тези на Zend Framework и донякъде тези на CodeIgniter. Ако пишете изцяло собствени неща, ако не пишете по стандартие на системата в която сте.

To be continue…

Това е тема с продължение, която най-вероятно утре ще продължа :)

Тъй като исках да напиша още за коментари / гетъри и сетъри / php файлове плюс няколко други неща, ще има още един пост.

Ако имате някви коментари и / или съвети може да ги споделите. Също така ще се радвам и да чуя и по какви стандарти пишете и вие.

Aug 09

В един от последните ми проекти ми се налагаше да генерирам много html елементи с javascript. Като за това основно използвах new Element() и Element#insert от Prototype. Като въпреки че като цяло двата метода са много изчистени и правят кода доста добре четим, винаги може и по-добре.

В началото написах един прост builder, но като малко го поизползвах тук и там, се оказа , че като код е доста по-малко, но не и като четимост. Затова написах и тези две малки добавки към Element#insert които ми спестиха доста писане и направиха кода още по-четим и добър. И същевременно показаха (за пореден път) няколко от добрите страни на Prototype.

Element.insert(element, { into: content });
Element.insert(element, [content, content, content]);

Element#insert(element, { into: content });

Тук просто добавих нов insert position, into. Общо взето е ясно какво прави той добавя елемента в друг елемент. Защото поне на 20 места имах нещо такова(или много подобно:

var element = new Element('div', {/* атрибут */});
document.body.appendChild(element);

// като с into това просто става
var element = new Element('div', {/* атрибут */}).insert({into: document.body });

За тези, които са запознати как работи точно от вътре Element#insert, няма да е никак трудно да разберат как съм добавил ‘into’. Prototype използва един object, който да съхранява всички insert функции – before, after, top, bottom. Той е
Element._insertionTranslations, и когато бива извикан Element#insert(element, {position: content}) се вика грубо казано Element._insertionTranslations[position](element, content). Заради това добавянето на нов insert метод е много лесно и става просто така:

Element._insertionTranslations.into = function(element, node){
	node.appendChild(element);
};

Внимание! Element._insertionTranslations не е част от Public Api-то на Prototype и затова може да се промени в бъдещи версии!

Страничен ефект

Element#insert(element, { into: content }) има много интересен ефект, за който не се бях сетил когато го писах. Но когато случайно “открих” бях много доволен.
Element#insert може да приема не само html елементи, но и нормални javascript обекти, които имат метод toElement ( тои трябва да връща html елемент). И така примерно имам клас подобен на Custom Select класа ми от ControlDepo 3 Widgets, който има един контейнер, към когото може да се добавят елементи. Чрез into могат да се правят такива неща:

var list = new (Class.create({
	initialize: function(ul){
		this.element = $(ul);
	},
	toElement: function(){
		return this.element;
	}
}))('some');

Тук, за декларацията на класа, ползвам прост номер описан, по-добре тук.

Element.insert(element, [content, content, content]);

Идеята тук е да може да се предава масив от елементи, които да се добавят в element.

var ul = new Element('ul');
ul.insert(li1);
ul.insert(li2);
ul.insert(li3);
// ... и т.н.

// като това понякога го пиша и така
[li1, li2, li3].each(Element.insert.curry(ul));

// с новата версия става просто
ul.insert([li1, li2, li3]);

Такава функционалност може да стане по 2 начина. Първо като пиша директно в кода на Element#insert ( или си направя мой си fork на Prototype). И втория вариянт е Function#wrap.

Колкото и да се изкушавам от първия вариант реших само да пусна един билен ( ticket ) на Prototype и да се надявам да го добавят като функционалност.  И след това си написах каквото ми трябваше чрез Function#wrap:

Element.addMethods({
    insert: Element.insert.wrap(function(insert, element, insertation){
        if (!Object.isArray(insertation)) return insert(element, insertation);

        element = $(element);
        insertation.each(insert.curry(element));
        return element;
    })
});

Тези двете добавки ги има в git.github – http://gist.github.com/164751. Там ще ги актуализирам ако правя промяна.

Ако някой има предложение или идея как може да се доразвият тези добавки или за други подобни, може да драсне някои коментар :)

Jul 18

В по-голямата част от PHP кода ми е OOP. Но все пак имам и няколко функции, като три от тях бих искал да споделя тук. Те са tag / func_array_cut / profile.

Функцията tag

Както се и казва функцията tag генерира html тагове. Тъй като в 99% от времето PHP връща html, е много важно да имам нещо с което бързо и лесно да си създавам тагове. Основно тази функция я използвам в различни помощни методи:

function tag($tag, $options, $content = null){
    if (is_string($options) && !is_string($content)){
        list($content, $options) = array($options, $content);
    }

    $attributes = '';

    if (is_string($options)){
        $attributes = ' ' . $options;
    } else if (is_array($options)){
        foreach($options as $key => $value){
            $attributes .= ' ' . $key . '="' . htmlspecialchars($value, ENT_COMPAT, 'utf-8') . '"';
        }
    }

    return '<' . $tag . $attributes . (is_string($content) || strlen($content) > 0 ? ">{$content}</{$tag}>" : ' />');
}

Таg може да се извиква по няколко начина, като основната ми идея беше да не се налага да се замислям кои аргумент какъв е – атрибут или текст.

// <input type="text" value="text" />
tag('input', array('type' => 'text', 'value' => 'text'));

// <input type="text" value="&quot;qoute&quot; text" />
tag('input', array('type' => 'text', 'value' => '"qoute" text'));

// <div>content in div</div>
tag('div', 'content in div');

// 3 начина за генериране на
// <a href="http://www.pixeldepo.com">Pixeldepo</a>
tag('a', array('href' => 'http://www.pixeldepo.com'), 'Pixeldepo');
tag('a', 'Pixeldepo', array('href' => 'http://www.pixeldepo.com'));
tag('a', 'href="http://www.pixeldepo.com"', 'Pixeldepo');

Функцията array_cut

Едно нещо което е изключително досадно в STRICT режима на PHP, е непрекъснатите проверки isset / empty за това дали в даден масив има даден ключ. Затова създадох array_cut, тя проверява дали в дадения масив го има ключа и ако го има му връща стойността, ако я няма връща $default стройност. Като същевременно ключът се изтрива от оригиналния масив.
Това като се замисля е първата функция която написах за PHP framework-а който използвам ControlDepo 3.

function array_cut(array &$array, $key, $default = null){
    if (!isset($array[$key])){
        return $default;
    }

    $value = $array[$key];
    unset($array[$key]);

    return $value;
}

Тази функция е изключително полезна и може да се използва в редица случаи, ето два примера:

// това може да се напише така:
$name = isset($_POST['name']) ? $_POST['name'] : '[no-name]';
// може да се напише така:
$name = array_cut($_POST, 'name', '[no-name]');

// използване на array_cut за примерна функция за генериране на url-та
function url($options){
    $url = '/' . join('/', array_filter(array(
        array_cut($options, 'controller'),
        array_cut($options, 'action', 'index'),
        array_cut($options, 'id')
    )));

    if ($query = http_build_query($options)){
        $url .= '?' . $query;
    }

    return $url;
}
// използване
// -> /products/view/1
url(array('controller' => 'products', 'action' => 'view', 'id' => 1));

// -> /users/index?page=1
url(array('controller' => 'users', 'page' => 1));

// -> /index?sort=name
url(array('sort' => 'name'));

Преди време си мислех, че в PHP5.3/6 смисълът от функцията ще се намали след като се добавиха ?: и  ifsetor. Но просто ?: не е толкова мощен колкото ми се искаше, а ifsetor няма да го има.

Функцията profile

Докато другите 2 функции ги използвам много (и също така цялото ControlDepo 3 ги ползва). Функцията profile() е просто бърз начин за замерване, докато работя. В действителност тази функция се използва чрез чрез друга моя функция p(), която просто вика profile(), но записва резултата в лога на request-a и така малко по-лесно може да го анализирам резултата:

function profile($scope = '__default__'){
    static $time = array();

    if (isset($time[$scope])){
        return microtime(true) - array_cut($time, $scope, 0);
    }

    $time[$scope] = microtime(true);
}

Тя се ползва просто така:

// имаме 2 нива на profile
profile('test');
sleep(1);

profile();

sleep(1);

echo profile(), "\n";
echo profile('test');

Надявам се тези 3 функции да са полезни на някои и ако някои има идеи как може да се подобри някоя от тях, ще се радвам да го сподели.

Jun 18

В JavaScript във всяка функция има предефинирана променлива arguments. Тя представлява масиво подобна структура съдържаща аргументите подадени към функцията  и callee . В поста ще се постарая да обясня какво точно е callee и как би било полезно.

Само да поясня за думата “масиво подобна структура”, преди да продължа нататък. Това значи, че arguments изглежда като масив. Т.е може да се достъпват  елементи от него с arguments[n] и има свойството length, което оказва броя на елементите. Но до тук свършват прилики му с масива, в повечето браузъри arguments няма нито един метод. Това е една от главните причина в prototype.js да я има $A функцията.

Така, но да се върнем на arguments.callee, какво точно е това? В callee се записва връзка (или референция, ако това звучи по-добре) към текущата функция, т.е. функция в която е arguments. Това може да се илюстрира със следния пример:

function foo(){
    console.log(arguments.callee == foo);
}

foo(); // true
Защо толкова често foo се ползва в примери, някои знае ли ? Даже страница в wikipedia има – foobar.

Два прости примера

// пример едно:
// функция която брои колко колко пъти е била викана.
function example1(){
    console.log(++arguments.callee.count);
}

example1.count = 0;
example1(); // 1
example1(); // 2
// и така на татък

// пример две:
// функция, която проверява дали броя на подадените аргументи е толкова колкото се очакват
function example2(a, b){
    if (arguments.length == arguments.callee.length){
        // тук може и throw "error" да се хвърли, но за примера и това върши работа
        console.log(
            'expected ' + arguments.callee.length + ' argument(s) given ' + arguments.length
        );
    }
}

example2();          // грешка
example2(1);         // грешка
example2(1, 2);      // работи
example2(1, 2 , 3);  // грешка</pre>
Във втория пример използвам Functon.length, който указва колко аргумента очаква дадената функция.

Два полезни примера

Като цяло с argument.callee може да се правят доста магии. В практиката повечето случаи, в които съм го ползвал е, когато съм имал анонимна функция и искам да направя нещо със самата функция и ми трябва връзка към нея.

Пример1: Имате event  handler при click и искате след първото натискане този handler да се махне.
В допълнение може да се направи при click event handler-ът да се предава от елемент на елемент, но това няма да го пиша, за да не натоварвам примера (ако някои иска може да го напиша допълнително).

// предполагам, че се ползва prototype.js
$('element').observe('click', function(){
    alert('this will be shown only one time');

    this.stopObserving('click', arguments.callee);
});

Пример2: прост ефект – увеличаване на размерите на квадрат. Използва анонимна функция която се само извиква с timeout, докато div-а не достигне максималния си размер. В повечето случаи interval би вършил работа, но принципа е важен в случая.

// тук създавам примерен div
var element = new Element('div').setStyle({
    width: '10px',
    height: '10px',
    backgroundColor: 'red'
});

document.body.appendChild(element);

var step = 10, max = 1000, time = 10;

(function(){
    var width = parseInt(element.style.width) + step,
        height = parseInt(element.style.height) + step;

    element.style.width = width + 'px';
    element.style.height = height + 'px';

    if (max > width && max > height){
        setTimeout(arguments.callee, time);
    }
})();

Малко допълнителна информация за arguments

Оказва се, че някои браузъри дефинират arguments само, когато е необходимо. Т.е. ако в тялото на фунцията не се използва никъде arguments, тя въобще не се дефинира.  Което спестява време и ускорява самия кода.  Тук има повече информация по този въпрос. Този факт е интересен и си мисля, че повече javascript интерпретатори ще използват подобен похват в бъдеще.

May 21

Наскоро имах да направя следното, елементарно нещо, в проект:

За дадена поръчка има отстъпка в зависимост от сумата:

  • до 1000 лева  отстъпка 0%
  • от 1000 – 3000 лева отстъпка 2%
  • от 3000 – 5000 лева отстъпка 3%
  • от 5000 – 10000 лева отстъпка 4 %
  • над 10000 лева  отстъпк 5%

Нищо сложно и преди съм правил такива неща, под 2 минути работа.
Тъй като беше края на работния ден, имах малко свободно време и си помислих, как бях написал кода за подобна задача преди време. Затова измъкнах от “прашните” архиви няколко проекта в които имаше подобни задачи и успях да синтезирам четири варианта, които съм писал преди време. Като ги видях така събрани на едно място един след друг, реших да пусна един пост, и дано някой да ги намери за полезен. И така започвам ги по ред на номерата.

Вариант 1 преди около 5 години

function getDiscountPercent($sum){
	$percent = 0;

	if ($sum >= 1000 && $sum < 3000){
		$percent = 2;
	} else if ($sum >= 3000 && $sum < 5000){
		$percent = 3;
	} else if ($sum >= 5000 && $sum < 10000){
		$percent = 4;
	} else if ($sum >= 10000){
		$percent = 5;
	}

	return $percent;
}

Това беше в началото на моя кариера, и като цяло е много добро решение, което доста хора биха използвали. Лесно се вижда какво става в кода и е доста по-просто е от варианти 2 и 3. Общо взето само излишното дефиниране на променливата $percent е по-дразнещо в случая.

Вариант 2 преди около 3 години

function getDiscountPercent($sum){
	$points = array(1000, 3000, 5000, 10000);
	$percent = 0;
	foreach($points as $key => $value){
		if (isset($points[$key+1])){
			if ($sum >= $value && $sum < $points[$key+1]){
				$percent = $key + 2;
				break;
			}
		}
	}

	return $percent;
}

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

Вариант 3 преди около 1 година

function getDiscountPercent($sum){
	return $sum < 1000 ? 0 : ($sum < 3000 ? 2 : ($sum < 5000 ? 3 : ($sum < 10000 ? 4 : 5)));
}

Само да вметна тук, че в оригиналната за проекта версия са само четири вида отстъпки, кода който съм показал сега приспособен за горната задача. Това е бързичко и кратко решение, поне аз лесно си го чета, но надали някой друг ще има същия успех.
Поне до колкото си спомням в този случаи в началото беше с 2 вида отстъпки, после се увеличиха до горе споменатите  четири вида отстъпки. Което и проблема на този вариант при повече варианти на отстъпките, става мазало.

Вариант 4 преди около 4~5 месеца

function getDiscountPercent($sum){
	if ($sum < 1000){
		return 0;
	}
	if ($sum < 3000){
		return 2;
	}
	if ($sum < 5000){
		return 3;
	}
	if ($sum < 10000){
		return 4;
	}
	return 5;
}

Това решение представлява просто разпънат Вариант 3, което от своя страна си е чист Вариант 1 без излишния шум. Има два аспекта тук, което е добре да се отбележат. Първо променливата от Вариант 1 – $percent, е премахната, за сметка на това в кода в момента в който се разбере колко е отстъпката , тя се връща което прави кода много ясен и бърз. Второто нещо, го имаше във Вариант 3(просто не се виждаше), е доста по-малкия брои логически операции, докато във Вариант 1 се проверяваха всички граници тук просто се гледа горната граница. Така това е най-доброто решение до сега, но винаги може и по-добре.

Финален вариант преди около няколко дни

function getDiscountPercent($sum){
	if ($sum < 1000)  return 0;
	if ($sum < 3000)  return 2;
	if ($sum < 5000)  return 3;
	if ($sum < 10000) return 4;

	return 5;
}

Това същност е кода който написах, и който започна този пост. Като функционалност си е едно към едно с Вариант 4, единствената разлика е че съм го подредил малко по-добре(според мен), така че от пръв поглед да виждам от каква сума каква отстъпка става.

Много ми е интересно след 2 – 3 години как бих написал подобен код  (да се надяваме на ruby ;) ). Ако някой има идеи как може още по-добре да се напише това нека ги сподели.