Jak wydobyć rozszerzenie z nazwy pliku.

Jednym z ważniejszych zabiegów optymalizujących kod aplikacji jest używanie przy wykonywaniu poszczególnych operacji, funkcji do tego dedykowanych. Powszechną jest opinia o nadużywaniu np. wyrażeń regularnych, czy też pisaniu własnych funkcji, których odpowiedniki są już zaimplementowane w samym PHP.

Wydobycie rozszerzenia z nazwy pliku nie jest operacją zbyt skomplikowaną i zasobożerną jednak da się ją wykonać na kilka różnych sposobów. Postanowiłem przeprowadzić mały test sprawdzający, które z przyjętych rozwiązań wykona owo zadanie najsprawniej.

Na początek przedstawię kilka sposobów wyekstrahowania rozszerzenia z pełnej ścieżki do pliku.

Na początek użycie funkcji pathinfo

function get_ext_using_pathinfo($path) {
	$parts = pathinfo($path);
	return $parts['extension'];
}

Użycie funkcji pathinfo ze stałą PATHINFO_EXTENSION – dostępną od wersji PHP 5.2

function get_ext_using_pathinfo_with_const($path) {
	return pathinfo($path, PATHINFO_EXTENSION);
}

Użycie funkcji operujących na tekście substr i strrpos

function get_ext_using_substr_and_strrpos($path) {
	return substr($path, strrpos($path, ".") + 1);
}

Użycie wyrażeń regularnych a dokładnie funkcji preg_replace

function get_ext_using_preg_replace($path) {
	return preg_replace('/.*\.([a-zA-Z0-9]*)$/', '\\1', $path);
}

Użycie wyrażeń regularnych z tymże tym razem funkcji preg_match

function get_ext_using_preg_match($path) {
	preg_match('/.*\.([a-zA-Z0-9]*)$/', $path, $matches);
	return $matches[1];
}

Wykonanie exploda i wydobycie ostatniego elementu tablicy funkcją array_pop

function get_ext_using_explode($path) {
	return array_pop(explode('.', $path));
}

Przez każdą z wyżej wymienionych funkcji – w pętli for – 10 tys. razy przepuściłem zmienną zawierającą przykładową ścieżkę do pliku.

$path = '/home/user/workplace/project/images/file.sufix.ext';

Operację tę wykonałem kilkukrotnie zapisując wyniki do pliku. Po uśrednieniu i zaokrągleniu wyników wyszło mi:

substr i strrpos preg_replace preg_match pathinfo ze stałą PATHINFO_EXTENSION explode i array_pop pathinfo
0.077 0.0822 0.0904 0.0952 0.1049 0.1310

Jak wynika z powyższego zestawienia najlepiej spisały się funkcje operujące na stringach. Funkcje preg_replace i preg_match pozytywnie mnie zaskoczyły gdyż spodziewałem się, że zaprzęgnięcie do tak prostej pracy tak potężnej maszyny jaką są wyrażenia regularne będzie obarczone sporą stratą czasu. Z kolei bardzo zawiodłem się na wbudowanej funkcji pathinfo, którą dotąd najczęściej wykorzystywałem do wydobycia potrzebnych mi informacji ze ścieżki. Myślałem, że jest bardziej wydajna, a tu się okazało, że w wersji uniemożliwiającej użycie stałej PATHINFO_EXTENSION jest ona wolniejsza nawet od exploda. Nie skreślałbym tej funkcji w przypadku chęci uzyskania kompletu danych o ścieżce jednak dla atomowej operacji wydobycia rozszerzenia warto pomyśleć o innym sposobie. Z drugiej strony, różnice jak widać nie są znowu tak wielkie, więc dla tego prostego zadania w 99% przypadków użycia nie będzie miało znaczenia, którego z rozwiązań użyjemy i spokojnie możemy kierować się wygodą.

Pobieranie listy plików o wybranym rozszerzeniu w PHP

Aby pobrać listę plików o wybranym rozszerzeniu z danego katalogu najprościej jest użyć funkcji [glob](http://pl.php.net/manual/en/function.glob.php)

glob("*.jpg");

Jeśli chcemy pobrać pliki z danego katalogu i jego podkatalogów trzeba sobie dopisać wersję rekurencyjną funkcji glob:

function glob_recur($pattern='*', $flags = 0, $path = '')
{
    $paths = glob($path.'*', GLOB_MARK|GLOB_ONLYDIR|GLOB_NOSORT);
    $files = glob($path.$pattern, $flags);
    foreach ($paths as $path) {
        $files = array_merge($files, glob_recur($pattern, $flags, $path));
    }
    return $files;
}

Funkcja glob umożliwia użycie tzw. wilkardów czyli w najprostszym ujęciu pytanik „?” zastępuje dowolny znak, a gwiazdka „*” dowolny ciąg znaków. Wilkardy to nie wyrażenia regularne i szybko odczujemy różnicę choćby w przypadku, kiedy chcielibyśmy pobrać pliki o rozszerzeniu np. css i js. W takiej sytuacji musielibyśmy dwa razy wywołać funkcję glob. Do dyspozycji mamy jednak funkcję [scandir](http://pl.php.net/manual/en/function.scandir.php), która ułatwia listowanie katalogu.

function get_files_from_dir($path, $extensions = array())
{
    $entries = scandir($path.'/');
    $files = array();
    foreach($entries as $entry)
    {
        if ($entry == '.' || $entry == '..') continue;
 
        $_path = $path.'/'.$entry;
        if(is_file($_path)) {
            if (!empty($extensions))
            {
                $ext = pathinfo($filename, PATHINFO_EXTENSION);
                if (!in_array($ext, $extensions)) continue;
            }
            $files[] = $_path;
        }
    }
    return $files;
}

Scandir wybierze wszystkie pliki i katalogi z zadanej lokalizacji. Odróżnienie plików od katalogów to prosty tekst przy użyciu funkcji is_file lub is_dir z kolei wydobycie rozszerzenia z pliku można przeprowadzić na kilka sposobów:

$ext = pathinfo($filename, PATHINFO_EXTENSION);
// lub
$ext = preg_replace('/.*\.([\d\w]{2,5})/i', '$1', $filename); //zakładam, że rozszerzenie ma od 2 do 5 znaków
// lub 
$ext = substr($filename , strrpos($filename , '.') +1);

Modyfikacja funkcji get_files_from_dir tak aby działała rekurencyjnie wymaga jedynie niewielkiej zmiany:

function get_files_from_dir_recur($path, $extensions = array())
{
    $entries = scandir($path.'/');
    $files = array();
    foreach($entries as $entry)
    {
        if ($entry == '.' || $entry == '..') continue;
 
        $_path = $path.'/'.$entry;
        if(is_file($_path)) {
            if (!empty($extensions))
            {
                $ext = pathinfo($filename, PATHINFO_EXTENSION);
                if (!in_array($ext, $extensions)) continue;
            }
            $files[] = $_path;
        } else if (is_dir($_path)) {
            $files = array_merge($files, self::get_files_from_dir_recur($path, $extensions));
        }
    }
    return $files;
}

W zasadzie to samo można osiągnąć za pomocą iteratorów z biblioteki [SPL](http://www.php.net/~helly/php/ext/spl/):

function get_files_from_dir($path, $extensions = array())
{
    $files = array();
    $it = new DirectoryIterator($path);
    foreach($it as $file) {
        $filename = $file->getFilename();
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
        if ($file->isFile() && (empty($extensions) || in_array($ext, $extensions))) {
            $files[] = $file->getPathname();
        }
    }
    return $files;
}

i wersja rekurencyjna

function get_files_from_dir_recur($path, $extensions = array())
{
    $files = array();
    $it = new RecursiveDirectoryIterator($path);
    foreach(new RecursiveIteratorIterator($it) as $file) {
        $filename = $file->getFilename();
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
        if ($file->isFile() && (empty($extensions) || in_array($ext, $extensions))) {
            $files[] = $file->getPathname();
        }
    }
    return $files;
}

Funkcja glob jest jest najprostsza w użyciu. Funkcja z użyciem scandir sprawdza się kiedy glob zawodzi i jest szybsza od iteratorów, które dla każdego znalezionego pliku powołują do życia obiekt.

Kiedy sięgać po iteratory i czy w ogóle? W podanych przeze mnie przykładach interesują nas jedynie ścieżki do plików, tymczasem iteratory zwracają pliki w formie obiektów [SplFileInfo](http://pl.php.net/manual/en/class.splfileinfo.php) z których można wyciągnąć dużo więcej informacji o plikach. Niekiedy może się to przydać i wtedy użycie iteratorów jest zasadne. Jeśli jednak chcemy otrzymać jedynie listę plików wtedy scandir jest lepszym rozwiązaniem bo szybszym.

Funkcja scandir i iteratory są dostępne dopiero od 5 wersji PHP. W PHP4 zawartość katalogu możymy odczytać dzięki funkcji [opendir](http://pl.php.net/manual/en/function.opendir.php).

function get_files_from_dir($path, $extensions = array()) {
    $files = array();
    if (!is_dir($path)) {
        return $files;
    }
    if ($dh = opendir($path)) {
        while (($file = readdir($dh)) !== false) {
            $ext = pathinfo($file, PATHINFO_EXTENSION);
            if (empty($extensions) || in_array($ext, $extensions)) {
                $files[] = $path.'/'.$file;
            }
        }
        closedir($dh);
    }
    return $files;
}

Posted in PHP by Zbigniew Heintze · Tag:

is_ajax, is_cli, is_ssl

Zarejestrowałem się w serwisie [9fingers](http://9fingers.pl/) stworzonym przez helion jako portal społecznościowy dedykowany programistom. Spotkałem się tam z pytaniem o [Najlepszy projekt Open Source do nauki PHP](http://9fingers.pl/questions/560/Najlepszy-projekt-Open-Source-do-nauki-PHP). Autorowi mówiąc krótko chodziło o to na jakim projekcie się wzorować? Cóż osobiście nie znam najlepszego projektu, ale za to wiele równie dobrych, po analizie których można się wielu rzeczy dowiedzieć. Wszystko zależy od tego jaki poziom się reprezentuje i czego człowiek szuka. Zasad ogólnych, tzw. dobrych praktyk uczył bym się raczej z książek, natomiast nic nie stoi na przeszkodzie aby podejrzeć sposób zaimplementowania konkretnych rozwiązań. Powtarzam **podejrzeć** i dodaję **zrozumieć**. Nie mam tu na myśli od razu całych frameworków.

Ja na przykład podpatrzyłem kilka prostych, ale bardzo przydatnych funkcji w php wykrywających w jakim trybie pracuje nasza aplikacja.

Metoda jednego z helperów w [Kohana PHP Framework](www.kohanaphp.com) sprawdzająca czy żądanie do hosta przyszło za pośrednictwem tzw. AJAX-a. Bardzo przydatna kiedy człowiek nie chce pisać osobnych kontrolerów do obsługi zapytań przychodzących w sposób tradycyjny i via AJAX.

/**
 * Tests if the current request is an AJAX request by checking the X-Requested-With HTTP
 * request header that most popular JS frameworks now set for AJAX calls.
 *
 * @return  boolean
 */
public static function is_ajax()
{
	return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) AND strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
}
// From Kohana PHP Framework

Jeśli pracujesz nad aplikacją w której w grę wchodzą rozliczenia finansowe albo dane poufne to przyda Ci się funkcja rodem z [Word Pressa](http://wordpress.com/) sprawdzająca czy użytkownik łączy się z serwerem używając SSL-a. W przypadku wykrycia uchybienia wystarczy przekierowanie na adres z protokołem HTTPS.

/**
 * Check if accessed via HTTPS
 *
 * Apache leaves ,$_SERVER['HTTPS'] empty when not available, IIS sets it to 'off'.
 * 'false' and 'disabled' are just guessing
 *
 * @returns bool true when SSL is active
 */
function is_ssl(){
	if (!isset($_SERVER['HTTPS']) ||
	preg_match('/^(|off|false|disabled)$/i',$_SERVER['HTTPS'])){
		return false;
	}else{
		return true;
	}
}
// From Word Press

Wbrew pozorom w PHP – CLI (Command Line Interface) – przydaje się nie tylko do tzw. [scaffoldingu](http://en.wikipedia.org/wiki/Scaffold_%28programming%29). Choć zaprezentowana poniżej funkcja pochodzi z instalatora [Symfony](http://www.symfony-project.org/), wierzcie mi, że nie trudno znaleźć powód do tego by uruchomić skrypt z linii komend.

/**
 * Check if PHP using CLI interface
 *
 * @return  boolean
 */
function is_cli()
{
    return !isset($_SERVER['HTTP_HOST']);
}

[PHP CLI](http://www.php-cli.com/) jest proste w użyciu i dla średnio obeznanego programisty PHP, który nie radzi sobie z bashem, albo zwyczajnie używa Windowsa umożliwia w szybki i prosty sposób własnoręczne stworzenie sobie przydatnych narzędzi operujących np. na wielu plikach itp. W prawdziwej webowej aplikacji też się znajdzie zastosowanie dla PHP CLI jak choćby skrypt do wysyłania newsletterów uruchamiany z Crona czy też spider chodzący po stronach i zbierający adresy email – no do tego to bym akurat użył Pythona, ale jak ktoś nie ma na serwerze Pythona czy też innego rozsądnego języka do dyspozycji to PHP CLI mu wystarczy.

Typ interfejsu możemy w PHP sprawdzić przy pomocy specjalnie do tego stworzonej funkcji [php_sapi_name](http://www.php.net/manual/en/function.php-sapi-name.php) i tak dla przykładu dla CLI wyglądało by to tak:

function is_cli()
{
    return php_sapi_name() === 'cli';
}

Może to nawet lepsze rozwiązanie niż to użyte przez programistów Symfony, ale tamto też działa. W każdym razie niekiedy warto wiedzieć czy PHP pracuje np. w trybie CGI choć przyznam – nie miałem dotąd takiej potrzeby.

Autoload, call_user_func_array i parent::__construct

Pracuje właśnie nad mechanizmem automatycznie generującym klasy potomne dziedziczące po istniejącej już w systemie klasie bazowej.

class A
{
	public function __construct()
	{}
 
	public function index()
	{
		// do something
	}
}

Mając powyższą klasę bazową wystarczy wygenerować klasę potomną, która w konstruktorze wywoła konstruktor parenta.

class B extends A
{
	public function __construct()
	{
		parent::__construct();
	}
}

Tyle, że konstruktor klasy bazowej może mieć jakieś parametry.

class A
{
	private $param;
 
	public function __construct($param)
	{
		$this->param = $param;
	}
 
	public function index()
	{
		// do something
	}
}

Ponieważ chcę napisać generator w miarę uniwersalny, a nie wiem ile parametrów i jakie może mieć, każda z klas bazowych, które będę chciał rozszerzyć dobrym rozwiązaniem wydawało by się wywoływanie klasy parenta przy pomocy funkcji „call_user_func_array”.

class B extends A
{
	public function __construct()
	{
		$args = func_get_args();
		call_user_func_array(array('parent', '__construct'), $args);
	}
}

Po utworzeniu obiektu:

$obj = new B('foo');

Wszystko działało zgodnie z oczekiwaniami, było fajnie tylko jak włączyłem raportowanie błędów ze sprawdzaniem składni

error_reporting(E_ALL^E_STRICT);
ini_set("display_errors", 1);

To zobaczyłem taki nieprzyjemny komunikat „**Strict standards: Non-static method A::__construct() cannot be called statically, assuming $this from compatible context B**”.

Powiem szczerze, że tak nie za bardzo wiedziałem co mam począć w tej sytuacji bo najbardziej intuicyjne rozwiązanie okazało się niezgodne ze standardami. Zajrzałem oczywiście do manuala gdzie znalazłem rozwiązanie. Przy okazji dowiedziałem się jeszcze, że inaczej powinno to wyglądać dla PHP w wersji 5.0 – 5.2, a już inaczej w PHP 5.3 i prawdopodobnie 6.

class D extends A
{
	public function __construct()
	{
		$args = func_get_args();
		if (version_compare(PHP_VERSION, '5.3.0', '>')) {
			call_user_func_array('parent::__construct', $args);
		} else {
			call_user_func_array(array($this, 'parent::__construct'), $args);
		}
	}
}

Gdyby to zamykało temat zapewne nie stworzyłbym tego wpisu, tyle, że pojawił się kolejny problem.

W moim eksperymentalnym frameworku używam loadera klas, który w razie nieznalezienia wywoływanej klasy skanuje zadane foldery w poszukiwaniu plików php, a następnie sprawdza czy znajdują się w nich definicje klas lub interfejsów. Następnie tworzy mapę w postaci tablicy, w której kluczem jest nazwa klasy, a wartością ścieżka do pliku zawierającego definicję owej klasy.

Otóż zauważyłem, że przy każdym wywoływaniu „call_user_func_array(array($this, ‚parent::__construct’), $args);” następuje uruchomienie autoloadera i w moim przypadku skanowanie plików w poszukiwaniu klasy „parent”. Gdyby nie związane z tym wydłużenie obsługi requesta zapewne nie zauważyłbym nawet wywołanie autoloadera gdyż pomimo, że funkcja autoloadera zwraca false kod wykonuje się prawidłowo nie generując żadnych błędów lub ostrzeżeń.

Nie mniej koniecznym okazało się spatchowanie autoloadera

function autoload($class) {
	if ($class == 'parent') {
		return false;
	}
	// ...
}
spl_autoload_register('autoload');

FirePhp

[Firebug](http://getfirebug.com/) jest już obowiązkowym dodatkiem do Firefoxa dla każdego webdevelopera. Jest on wręcz niezbędny i osobiście nie wyobrażam sobie pracy z css-em bez niego. Jako programista produkujący skrypty wykonywane raczej po stronie serwera oczywiście doceniam jego użyteczność, ale dopiero w połączeniu z [FirePHP](http://www.firephp.org/) popadam w euforię ;).

Przy okazji tylko wspomnę, że istnieje odpowiednik FirePhp dla Pythona [FirePy](http://code.google.com/p/firepy/wiki/Documentation), Ruby, Perla i paru jeszcze innych języków o czym można się przekonać wchodząc na stronę [wiki projektu](http://www.firephp.org/Wiki/). W tym samym miejscu przekonamy się, że FirePHP lub jego odpowiedniki został już zintegrowany z całą masą co bardziej popularnych frameworków i aplikacji jak np. Zend Framework, Symfony, Kohana, Typo3, Prado, Seagull, WordPress, eZ itd. Nie dziwi to gdyż jak się sam przekonałem użycie FirePhp jest wręcz banalne.

Wziąwszy pod uwagę wielkość [dokumentacji użycia](http://www.firephp.org/HQ/Use.htm) ograniczającą się do jednej, niewielkiej strony, można by pomyśleć, że FirePHP nie jest zbyt imponującą biblioteką albo jest kiepsko udokumentowana. Okazuje się, że funkcjonalność FirePHP jest rzeczywiście ograniczona, ale jest to ograniczenie w sensie pozytywnym. Znajdziemy tu tylko to, co jest naprawdę przydatne. I tak mamy:

* możliwość wielopoziomowego (log, info, warn, error) logowania danych do konsoli Firebuga – możemy też dane przedstawić w formie tabeli.
* możliwość zrzutu danych prezentowanego w zakładce „sieć” Firebuga
* możliwość włączenia errorhandlera i exceptionhandlera przechwytujących błędy i wyjątki php i logujących je do konsoli Firrebuga
* możliwość wyświetlenia backtrace-a

FirePHP nie jest pozbawiony błędów lub raczej niedoskonałości, które ujawniają się w trakcie próby przesłania zbyt wielkiej liczby danych lub zbyt długo trwającej rekurencji dlatego też boję się oprzeć swojego debuggera jedynie o ten dodatek. Z drugiej strony podoba mi się np. [firebug_profiler](http://github.com/nickdunn/firebug_profiler) do CMS-a Symphony napisany przez Nicka Dunna, co skłania mnie do eksperymentowania z tym dodatkiem i badania granic jego przydatności.

Na razie ograniczam użycie FirePHP do bieżącego debugowania aplikacji. Dotychczas chcąc szybko wyświetlić zawartość jakieś zmiennej używałem var_dump-a. Ponieważ użycie var_dump-a w wielu miejscach rodziło pewne problemy z jego późniejszym odszukaniu zdefiniowałem sobie w Eclipsie taki mały snippet, który oprócz właściwej zmiennej wyświetlał też nazwę pliku oraz linię w której var_dump został wywołany. Wyglądało to mniej więcej tak:

var_dump('FILE:'.__FILE__. ' LINE:'.__LINE__, $var); //DEBUG

Zważywszy, że snippet wywoływany był za pomocą skrótu klawiaturowego dodawanie ‚FILE:’.__FILE__. ‚ LINE:’.__LINE__ nie było zbyt upierdliwe, jednak postanowiłem tę kwestię rozwiązać w bardziej elegancki sposób.

class Logger
{
	private static $_firePhp;
 
	public static function log($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->log($object, $label);
		$_fb->groupEnd();
	}
 
	public static function warn($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->warn($object, $label);
		$_fb->groupEnd();
	}
 
	public static function info($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->info($object, $label);
		$_fb->groupEnd();
	}
 
	public static function error($object, $label = null)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->error($object, $label);
		$_fb->groupEnd();
	}
 
	public static function table($label, $table)
	{
		$_fb = self::_getFirePhp();
		$_label = self::_prepareMetaData();
		$_fb->group($_label);
		$_fb->table($label, $table);
		$_fb->groupEnd();
	}
 
	public static function dump($key, $variable)
	{
		$_fb = self::_getFirePhp();
		$_fb->dump($key, $variable);
	}
 
	private static function _getFirePhp()
	{
		if (is_null(self::$_firePhp)) {
			require_once('FirePHPCore/FirePHP.class.php');
			$_firephp = FirePHP::getInstance(true);
			$_options = array('maxObjectDepth' => 10,
				'maxArrayDepth' => 20,
				'useNativeJsonEncode' => true,
				'includeLineNumbers' => true);
			$_firephp->setOptions($_options);
			self::$_firePhp = $_firephp;
		}
		return self::$_firePhp;
	}
 
	private static function _prepareMetaData()
	{
		$_level = 1;
		try {
			throw new Exception();
		} catch (Exception $_e) {
			$_trace = $_e->getTrace();
			return sprintf('FILE: %s LINE %s TIME %s'
				, $_trace[$_level]['file']
				, $_trace[$_level]['line']
				, date("h:i:s", time()));
		}
	}
}

Jak widać do wyłuskania meta danych użyłem wyjątku. Szybka analiza stacktracea i w miarę prosty sposób uzyskałem informację o miejscu wywołania loggera. Teraz wyświetlając jakąś zmienną w konsoli FireBuga wiem nie tylko o tym co ta zmienna zawiera, ale też w sytuacji, w której zapomniałbym o usunięciu tymczasowego wpisu, mogę łatwo go odnaleźć i naprawić swoje niedopatrzenie 😀

Sortowanie tablicy wielowymiarowej w php

Zdecydowałem się wreszcie opublikować mój artykuł na temat wzorca projektowego DTO (Data Transfer Object), który napisałem już jakiś czas temu. Przy okazji parę akapitów poprawiłem i dopisałem nowe przykłady zastosowania. Przygotowując przykład dotyczący Recordset-u odkopałem przydatną funkcję ułatwiającą [sortowanie tablicy wielowymiarowej](http://php.net/manual/pl/function.array-multisort.php#87268). Jest uniwersalna i moim zdaniem warto zwrócić na nią uwagę. Ponad to wbrew pozorom nie musi służyć jedynie do sortowania wyników zapytań SQL-owych.

<?php
/**
 * Sort DB result
 *
 * @param array $data Result of sql query as associative array
 *
 * Rest of parameters are optional
 * [, string $name  [, mixed $name or $order  [, mixed $name or $mode]]]
 * $name string - column name i database table
 * $order integer - sorting direction ascending (SORT_ASC) or descending (SORT_DESC)
 * $mode integer - sorting mode (SORT_REGULAR, SORT_STRING, SORT_NUMERIC)
 *
 * <code>
 * <?php
 * // You can sort data by several columns e.g.
 * $data = array();
 * for ($i = 1; $i <= 10; $i++) {
 *     $data[] = array( 'id' => $i,
 *                      'first_name' => sprintf('first_name_%s', rand(1, 9)),
 *                      'last_name' => sprintf('last_name_%s', rand(1, 9)),
 *                      'date' => date('Y-m-d', rand(0, time()))
 *                  );
 * }
 * $data = sortDbResult($data, 'date', SORT_DESC, SORT_NUMERIC, 'id');
 * print_r($data);
 * $data = sortDbResult($data, 'last_name', SORT_ASC, SORT_STRING, 'first_name', SORT_ASC, SORT_STRING);    
 * print_r($data);
 * ?>
 * </code>
 *
 * @return array $data - Sorted data
 */
function sortDbResult(array $data /*$name, $order, $mode*/) {
    $_argList = func_get_args();
    $_data = array_shift($_argList);
    if (empty($_data)) {
        return $_data;
    }
    $_max = count($_argList);
    $_params = array();
    $_cols = array();
    $_rules = array();
    for ($_i = 0; $_i < $_max; $_i += 3)
    {
        $_name = (string) $_argList[$_i];
        if (!in_array($_name, array_keys(current($_data)))) {
            continue;
        }
        if (!isset($_argList[($_i + 1)]) || is_string($_argList[($_i + 1)])) {
            $_order = SORT_ASC;
            $_mode = SORT_REGULAR;
            $_i -= 2;
        } else if (3 > $_argList[($_i + 1)]) {
            $_order = SORT_ASC;
            $_mode = $_argList[($_i + 1)];
            $_i--;
        } else {
            $_order = $_argList[($_i + 1)] == SORT_ASC ? SORT_ASC : SORT_DESC;
            if (!isset($_argList[($_i + 2)]) || is_string($_argList[($_i + 2)])) {
                $_mode = SORT_REGULAR;
                $_i--;
            } else {
                $_mode = $_argList[($_i + 2)];
            }
        }
        $_mode = $_mode != SORT_NUMERIC
                    ? $_argList[($_i + 2)] != SORT_STRING ? SORT_REGULAR : SORT_STRING
                    : SORT_NUMERIC;
        $_rules[] = array('name' => $_name, 'order' => $_order, 'mode' => $_mode);
    }
    foreach ($_data as $_k => $_row) {
        foreach ($_rules as $_rule) {
            if (!isset($_cols[$_rule['name']])) {
                $_cols[$_rule['name']] = array();
                $_params[] = &$_cols[$_rule['name']];
                $_params[] = $_rule['order'];
                $_params[] = $_rule['mode'];
            }
            $_cols[$_rule['name']][$_k] = $_row[$_rule['name']];
        }
    }
    $_params[] = &$_data;
    call_user_func_array('array_multisort', $_params);
    return $_data;
}
?>

Posted in PHP by Zbigniew Heintze · Tags: ,

Dynamiczne wywołanie funkcji parenta

Wielokrotnie używałem funkcji [call_user_func()](http://pl.php.net/function.call-user-func) i [call_user_func_array()](http://pl.php.net/function.call-user-func-array) do wywoływania zarówno metod obiektów, jak i metod statycznych. Nigdy dotąd jednak nie miałem potrzeby wywoływania w ten sposób metod „parenta”.

Jakież było moje zdziwienie kiedy po „odpaleniu” kodu

class A {
    protected function func($str) {
        echo $str;
    }
}
 
class B extends A {
    public function func($str) {
	call_user_func_array(array('parent', 'func' ), array($str));
    }
}
 
$b = new B;
$b->func('Hello World');

Zobaczyłem ostrzeżenie:

**Strict standards: Non-static method A::func() cannot be called statically, assuming $this from compatible context B in …**

No niby logiczne bo przecież wywoływana funkcja nie jest statyczna ale co zrobić w tej sytuacji? Przyznam się, że raczej nie wpadł bym samodzielnie na prawidłowe rozwiązanie.

    call_user_func_array(array($this, 'parent::func' ), array($str));

Posted in PHP by Zbigniew Heintze · Tag:

DTO – zastosowanie – Recordset, Rejestr, Menadżer zmiennych, Konfiguracja

Przed przeczytaniem tego artykułu zapoznaj się z wpisem DTO – wprowadzenie i charakterystyka.

Recordset

Wzorzec projektowy Data Transfer Object jest często używany np. w połączeniu z wzorcemi Data Access Objects i/lub Recordset. Obiekty Recordset są wykorzystywane do operowania danymi w bazie danych na poziomie rekordów. Nie wszyscy pamiętają, że pod pojęciem Bazy Danych może kryć się serwer MySQL, ale też pliki tekstowe, pliki XML itd. PHP ma świetne moduły do pracy z tradycyjnie rozumianymi Bazami Danych takie jak np. PDO. Wyciągając dane z kwerendy możemy otrzymać je zarówno w formie tablicy, obiektu, pogrupowane, posortowane itd. Pobierając dane z innych źródeł nie zawsze mamy taki komfort. Tymczaem standardowy obiekt DTO świetnie nadaje się jako podstawa konstrukcyjna uniwersalnego obiektu Recordset.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Recordset extends bigWeb_Dto
{
    protected $_data;
 
    public function __construct(array $properties = array()) 
    {
       	if (!empty($properties)) {
    		foreach ($properties as $_key => $_value) {
    			if (is_array($_value)) {
    				$properties[$_key] = new bigWeb_Dto($_value);
    			}
    		}
    	}
        parent::__construct($properties);
    }
 
 
    public function multisort()
    {
    	$_args = func_get_args();
    	array_unshift($_args, $this->getArrayCopy());
    	$_result = call_user_func_array(
            array(get_class($this), '_sortDbResult'), $_args);
    	$this->exchangeArray($_result);
    }
 
    protected static function _sortDbResult(array $data ) 
    {
        $_argList = func_get_args();
        $_data = array_shift($_argList);
        if (empty($_data)) {
            return $_data;
        }
        $_max = count($_argList);
        $_params = array();
        $_cols = array();
        $_rules = array();
        for ($_i = 0; $_i < $_max; $_i += 3)
        {
            $_name = (string) $_argList[$_i];
            if (!in_array($_name, array_keys(current($_data)))) {
                continue;
            }
            if (!isset($_argList[($_i + 1)]) || is_string($_argList[($_i + 1)])) {
                $_order = SORT_ASC;
                $_mode = SORT_REGULAR;
                $_i -= 2;
            } else if (3 > $_argList[($_i + 1)]) {
                $_order = SORT_ASC;
                $_mode = $_argList[($_i + 1)];
                $_i--;
            } else {
                $_order = $_argList[($_i + 1)] == SORT_ASC ? SORT_ASC : SORT_DESC;
                if (!isset($_argList[($_i + 2)]) || is_string($_argList[($_i + 2)])) {
                    $_mode = SORT_REGULAR;
                    $_i--;
                } else {
                    $_mode = $_argList[($_i + 2)];
                }
            }
            $_mode = $_mode != SORT_NUMERIC
                        ? $_argList[($_i + 2)] != SORT_STRING 
                            ? SORT_REGULAR : SORT_STRING
                        : SORT_NUMERIC;
            $_rules[] = array(
                'name' => $_name
                , 'order' => $_order
                , 'mode' => $_mode);
        }
        foreach ($_data as $_k => $_row) {
            foreach ($_rules as $_rule) {
                if (!isset($_cols[$_rule['name']])) {
                    $_cols[$_rule['name']] = array();
                    $_params[] = &$_cols[$_rule['name']];
                    $_params[] = $_rule['order'];
                    $_params[] = $_rule['mode'];
                }
                $_cols[$_rule['name']][$_k] = $_row[$_rule['name']];
            }
        }
        $_params[] = &$_data;
        call_user_func_array('array_multisort', $_params);
        return $_data;
    }
}

Obiekt Recordset oparty o naszą podstawową klasę DTO posada przede wszystkim wygodne mechanizmy dostępu do danych. Można do niego załadować dane pobrane z Bazy Danych (BD), jak równierz z każdego innego źródła lub też wygenerowane dynamicznie co przedstawia przykład poniżej. Dane pobierane z BD wygodniej jest sortować już na etapie konstruowania zapytania SQL-owego. Nie zawsze mamy taką możliwość dlatego też moja propozycja obiektu Recordset otrzymała uniwersalną metodę sortującą.

$data = array();
for ($i = 1; $i <= 10; $i++) {
    $data[] = array( "id" => $i,
                     "first_name" => sprintf("first_name_%s", rand(1, 9)),
                     "last_name" => sprintf("last_name_%s", rand(1, 9)),
                     "date" => date("Y-m-d", rand(0, time()))
                 );
}
$ob = new bigWeb_Recordset($data);

Po wygenerowaniu danych testowych i załadowaniu do obiektu Recordset sortujemy dane po kolumnie „date” malejąco, a w drugiej kolejności po kolumnie „id” …

$ob->multisort("date", SORT_DESC, SORT_NUMERIC, "id");
/*
array
  0 => 
    array
      'id' => int 10
      'first_name' => string 'first_name_3' (length=12)
      'last_name' => string 'last_name_2' (length=11)
      'date' => string '2004-10-20' (length=10)
  1 => 
    array
      'id' => int 7
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_9' (length=11)
      'date' => string '2003-07-25' (length=10)
  2 => 
    array
      'id' => int 1
      'first_name' => string 'first_name_4' (length=12)
      'last_name' => string 'last_name_3' (length=11)
      'date' => string '1990-07-30' (length=10)
  3 => 
    array
      'id' => int 9
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1981-02-09' (length=10)
  4 => 
    array
      'id' => int 2
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_1' (length=11)
      'date' => string '1979-11-25' (length=10)
  5 => 
    array
      'id' => int 4
      'first_name' => string 'first_name_9' (length=12)
      'last_name' => string 'last_name_6' (length=11)
      'date' => string '1976-09-28' (length=10)
  6 => 
    array
      'id' => int 8
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1976-02-14' (length=10)
  7 => 
    array
      'id' => int 6
      'first_name' => string 'first_name_8' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1975-11-15' (length=10)
  8 => 
    array
      'id' => int 3
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1974-06-02' (length=10)
  9 => 
    array
      'id' => int 5
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1972-11-07' (length=10)
*/

… albo według nazwiska i imienia.

$ob->multisort(
    "last_name", SORT_ASC, SORT_STRING
    , "first_name", SORT_ASC, SORT_STRING);
/*
array
  0 => 
    array
      'id' => int 2
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_1' (length=11)
      'date' => string '1979-11-25' (length=10)
  1 => 
    array
      'id' => int 10
      'first_name' => string 'first_name_3' (length=12)
      'last_name' => string 'last_name_2' (length=11)
      'date' => string '2004-10-20' (length=10)
  2 => 
    array
      'id' => int 1
      'first_name' => string 'first_name_4' (length=12)
      'last_name' => string 'last_name_3' (length=11)
      'date' => string '1990-07-30' (length=10)
  3 => 
    array
      'id' => int 4
      'first_name' => string 'first_name_9' (length=12)
      'last_name' => string 'last_name_6' (length=11)
      'date' => string '1976-09-28' (length=10)
  4 => 
    array
      'id' => int 3
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1974-06-02' (length=10)
  5 => 
    array
      'id' => int 5
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1972-11-07' (length=10)
  6 => 
    array
      'id' => int 6
      'first_name' => string 'first_name_8' (length=12)
      'last_name' => string 'last_name_7' (length=11)
      'date' => string '1975-11-15' (length=10)
  7 => 
    array
      'id' => int 8
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1976-02-14' (length=10)
  8 => 
    array
      'id' => int 9
      'first_name' => string 'first_name_2' (length=12)
      'last_name' => string 'last_name_8' (length=11)
      'date' => string '1981-02-09' (length=10)
  9 => 
    array
      'id' => int 7
      'first_name' => string 'first_name_1' (length=12)
      'last_name' => string 'last_name_9' (length=11)
      'date' => string '2003-07-25' (length=10)
*/

Multisortowanie to tylko prosta demonstracja tego co możemy zrobić, aby ułatwić sobie pracę. Nic nie stoi na przeszkodzie aby do objektu Recordset rozbudować o dodatkowe funkcjonalności np. dodać metody umożliwiające zapis danych do szeroko rozumianej Bazy Danych i tym samym przekształcić go w „Active Record”.

Rejestr

W aplikacji zawsze pojawiają się takie mechanizmy, które mają charakter globalny. Istnieje wiele różnych rozwiązań problemu globalnego zasięgu określonych zmiennych. Jednym z prostszych, a zarazem dobrych rozwiązań zapanowania nad obiektami i wszelkimi innymi wartościami, do których konieczny jest dostęp z dowolnego miejsca aplikacji jest skorzystanie ze wzorca „Registry”. Rejestr daje dostęp do dowolnej zarejestrowanej w nim zmiennej pod warunkiem, że sam jest ogólnie dostępny. Globalny dostęp do rejestru łatwo jest zrealizować implementując wzorzec „Singletona”. Ponieważ jednak Singleton wymaga, aby konstruktor był prywatny, a przynajmniej chroniony nie możemy w tym wypadku skorzstać z prostego dziedziczenia po obiekcie DTO. Z pomocą przyjdzie nam wzorzec „Factory”.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Registry
{
    private static $_instance;
 
    private function __construct()
    {}
 
    public static function getInstance($properties = null) {
        if (is_null(self::$_instance)) {
        	self::$_instance = new bigWeb_Dto($properties);
        }
        return self::$_instance;
    }
}

Proste użycie plus dostęp z dowolnego miejsca aplikacji to w zasadzie wszystko czego nam potrzeba.

$registry = bigWeb_Registry::getInstance();
 
$dsn = "mysql:dbname=testdb;host=127.0.0.1";
$user = "dbuser";
$password = "dbpass";
 
$registry->db = new PDO($dsn, $user, $password);

Czy rzeczywiście wszystko? Rozbudowana aplikacja internetowa wykonuje szereg różnorakich akcji. Wykonanie zainicjalizowanych przez użytkownika zadań wymaga współdziałania różnych mechanizmów lecz nie zawsze tych samych. Nie zawsze potrzebny jest dostęp do bazy danych, nie zawsze konieczną jest też inicjalizacja obiektów związanych z warstwą widoku. W zasadzie stosunkowo rzadko potrzebujemy klasy, która wysyła maila. Zadajmy sobie pytanie. Jaki jest sens tworzenia tych wszystkich – często zbędnych – obiektów za każdym razem, kiedy użytkownik wpisze w pole adresowe przeglądarki jakiś url? Chcąc w efektywny sposób korzystać z zasobów nie powinniśmy na dzień dobry wywalać całej skrzynki z narzędziami, a jedynie sięgać po te z narzędzi, które pozwolą nam zrealizować bierzące zadanie. Dobrym pomysłem byłoby zaimplementowanie w Rejestrze mechanizmu leniwej inicjalizacji „Lazy initialization”, która powoła do życia obiekt dopiero w chwili gdy jakiś proces o niego poprosi.

Menadżer zmiennych

Właściwie nagminnie posługujemy się tablicami GET, POST, SESSION itp. co w praktyce często okazuje się niewygodne zwłaszcza kiedy nie wiemy do końca jakie zmienne zawiera tablica. Najbardziej uciążliwe są zwłaszcza testy sprwadzające czy dany indeks w ogóle istnieje w tablicy. Słowniki w pythonie posiadają metodę umożliwiającą pobranie danego elementu, a jeżeli on nie występuje zastąpienie go domyślną wartością. Tablice w PHP nie mają takiej funkcjonalności, ale prosty obiekt DTO możemy wzbogacić o podobną metodę.

Drugą z metod o jaką warto się pokusić w tym konkretnym przypadku jest funkcja umożliwiająca zastąpienie określonego zestawu wartości jaki przyjmuje dana zmienna na inny zestaw.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Vars extends bigWeb_Dto
{
 
    public function __construct($properties = null){
        parent::__construct($properties);
    }
 
    public function get($property, $default = null)
    {
        return $this->offsetExists($property) ? $this->offsetGet($property) : $default;
    }
 
    public function map($property, array $map = array(), $default = null)
    {
        if (!$this->offsetExists($property)) {
            return $default;
        }
        $_val = $this->offsetGet($property);
        return (array_key_exists($_val, $map)) ? $map[$_val] : $_val;
    }
}

Załużmy taki scenariusz. Mamy tablicę POST, którą otrzymaliśmy po zatwierdzeniu formularza.

$_POST = array(
	"id" => 10,
	"login" => null,
	"email" => "jan.kowalski@domain.com",
	"first_name" => "Jan",
	"last_name" => "Kowalski",
	"gender" => "m",
	"submit" => "OK");

W naszym formularzu było jeszcze pole status określające czy dany użytkownik jest aktywny, ale ponieważ było to pole typu checkboks i nie zostało ono zaznaczone dlatego w POST nie ma zmiennej „status”.

Chcemy nasze dane zapisać w bazie danych z tym, że jeżeli nie został podany login to należy w jego miejsce wpisać adres email. Z kolei zapisując płeć dla kobiety musimy ustawić „0” a dla mężczyzny „1” bo pole w bazie danych jest typu INT. Podobnie pole status też przyjmuje wartości „0” i „1”. Operujac na bezposrednio na tablicy POST musielibyśmy popełnić coś na wzór.

if (isset($_POST["submit"])) {
	$data = array(
		"id" => (int) @$_POST["id"],
		"nick" => (!empty($_POST["login"]) ? $_POST["login"] : @$_POST["email"]),
		"email" => @$_POST["email"],
		"first_name" => @$_POST["first_name"],
		"last_name" => @$_POST["last_name"],
		"gender" => (isset($_POST["gender"]) && $_POST["gender"] == "m") ? 1 : 0,
		"status" => (int) @$_POST["status"],
	);
	$db->save($data);
}

Maskowania błędów @ użyłem po to aby ukryć ewentualne ostrzeżenia. Chcąc to wykonać prawidłowo powinienem za każdym razem wykonać test z użyciem np. funkcji „isset”, ale wymagałoby to napisania jeszcze dłuższego kodu. Wybrałem rozwiązanie mniej eleganckie ale krótsze chcąc pokazać, że użycie obiektu DTO nie powoduje powstania dłuższego kodu, za to jest rozwiązaniem o wiele bardziej przejrzystym i eleganckim.

$post = new bigWeb_Dto_Vars($_POST);
if (isset($post->submit)) {
	$data = array(
		"id" => (int) $post->id,
		"nick" => $post->get("login", $post->email),
		"email" => $post->email,
		"first_name" => $post->first_name,
		"last_name" => $post->last_name,
		"gender" => $post->map("gender", array("m" => 1, "k" => 0), 0),
		"status" => (int) $post->staus,
	);
	$db->save($data);
}

Mając już menadżer zmiennych jesteśmy zaledwie o krok od zbudowania profesjonalnej klasy Request lub też narzędzia do zarządzania sesjami.

Konfiguracja

Projektując aplikację lub też konstruując framework szybko przekonasz się, o potrzebie wydzielenia danych konfiguracyjnych. Wraz z rozwojem aplikacji rozrasta się też jej część konfiguracyjna, która dodatkowo powoli zaczyna przyjmować kształt hierarchicznej struktury drzewiastej. Konfigurację można trzymać w plikach ini, xml, yaml lub w postaci tablic. Wybór formatu przechowywania to jedna sprawa, a zarządzanie i łatwosć dostępu do zmiennych to już zupełnie coś innego. Dążąc do maksymalnej wygody użycia przerobiłem nieco mój podstawowy obiekt DTO, dzięki czemu nabrał on nowych „cudownych” właściwości.

<?php
require_once 'bigWeb/Dto.php';
 
class bigWeb_Dto_Recurrence extends bigWeb_Dto {
 
	protected $_isWritable;
 
	public  function __construct($properties = null)
	{
		$this->_isWritable = true;
		parent::__construct($properties);
		$this->setIsWritable(false);
	}
 
	public function setIsWritable($mode)
	{
		$this->_isWritable = (bool) $mode;
		$_class = get_class($this);
		foreach ($this as $_value) {
			if ($_value instanceof $_class) {
				$_value->setIsWritable($mode);
			}
		}
		return $this;
	}
 
	public function offsetSet($property, $value)
	{
		if (true !== $this->_isWritable) {
			return false;
		}
		$_class = get_class($this);
		if (is_array($value)) {
			$value = new $_class($value);
			$value->setIsWritable($this->_isWritable);
		}
		parent::offsetSet($property, $value);
		return true;
	}
 
	public function offsetGet($property)
	{
		$_value = parent::offsetGet($property);
		if (true === $this->_isWritable && is_null($_value)) {
			$this->offsetSet($property, array());
			return $this->offsetGet($property);
		}
		return $_value;
	}
 
	public function __isset($property)
	{
		$_value = parent::offsetGet($property);
		if (is_null($_value)) return false;
		if ($_value instanceOf ArrayObject) {
			if (count($_value) == 0) return false;
		}
		return true;
	}
 
	public function getArrayCopy()
	{
		$_array = parent::getArrayCopy();
		foreach ($_array as $_key => $_value) {
			if (is_array($_value) && empty($_value)) {
				unset($_array[$_key]);
			}
		}
		return $_array;
	}
 
	protected function _getRecursiveArrayCopy($array)
	{
		$_array = array();
		foreach ($array as $_key => $_value) {
			if ($_value instanceOf ArrayObject) {
				if (count($_value) === 0) {
					continue;
				} else {
					$_value = $this->_getRecursiveArrayCopy($_value->getArrayCopy());
					if (count($_value) === 1) {
						$_val = current($_value);
						if (empty($_val)) {
							continue;
						}
					}
				}
			}
			$_array[$_key] = $_value;
		}
		return $_array;
	}
 
	public function __clone()
	{
		$_isWritable = $this->_isWritable;
		$this->setIsWritable(true);
		foreach ($this as $_property => $_value) {
			$_class = get_class($this);
			if ($_value instanceof $_class) {
				$this->offsetSet($_property, clone $_value);
			}
		}
		$this->setIsWritable($_isWritable);
	}
}

Rekurencyjny obiekt DTO działa w dwóch trybach.

$config = new bigWeb_Dto_Recurrence(array(
	"database" => array(
		"host" => "localhost"
		, "pass" => "dbpass"
		, "user" => "dbuser"
		, "db" => "testdb"
	)));
 
$config->database->user;
// string 'dbuser' (length=6)
 
$config->database->pass;
// string 'dbpass' (length=6)
 
$config->database->host;
// string 'localhost' (length=9)
 
$config->database->db
// string 'testdb' (length=6)
 
$config->database->port;
// null

W domyślnym trybie do odczytu dane w obiekcie można zapisać jedynie w trakcie tworzenia nowej instancji obiektu poprzez przekazanie za pośrednictwem konstruktora.

$config->database->port = 25;
$config->database->port;
// null
 
$config["database"]["port"] = 25;
$config["database"]["port"];
// null

Włączając tryb do zapisu ujawnia się magia.

$config->setIsWritable(true);
 
$config->database->port = 25;
$config->database->port;
// int 25
 
$config->zmienna->na->dowolnym->poziomie->zagniezdrzenia = "foo";
$config->zmienna->na->dowolnym->poziomie->zagniezdrzenia;
// string 'foo' (length=3)

Powodem, dla którego zdecydowałem się wprowadzić tryb zapisu jest jedna nieporządana cecha, która się ujawnia właśnie w trybie do zapisu. Możemy się bezkarnie odwoływać do nieistniejącej zmiennej i nigdy nie otrzymamy wartości NULL tylko pusty obiekt DTO.

$config->to->jest->nieistniejaca->zmienna
/*
object(bigWeb_Dto_Recurrence)[15]
  protected '_isWritable' => boolean true
*/

Mając taką bazę wystarczy dopisać metodę pobierającą dane z pliku oraz, metodę zachowującą zmiany w pliku i otrzymujemy całkiem zaawansowany menadżer konfiguracji.

<?php
require_once 'bigWeb/Dto/Recurrence.php';
 
class bigWeb_Config extends bigWeb_Dto_Recurrence
{
	protected $_file;
 
	public function __construct($file)
	{
		if (is_string($file)) {
			$this->setFile($file);
			$_properties = include $file;
		} else {
			$_properties = $file;
		}
		parent::__construct($_properties);
	}
 
	public function setFile($file)
	{
		if (!file_exists($file)) {
			throw new bigWeb_Config_Exception('File "'.$file.'" does not exist');
		}
		$this->_file = $file;
		return $this;
	}
 
	public function save()
	{
		if (!$this->_isWritable) {
			return $this;
		}
		if (is_null($this->_file)) {
			throw new bigWeb_Config_Exception('File does not set');
		}
		if (!is_writable($this->_file)) {
			throw new bigWeb_Config_Exception('File "'.$this->_file.'" is not writable');
		}
		$_content = "<?php\nreturn ".var_export($this->getArrayCopy(), true).';';
		$_result = self::createOrReplaceFile($this->_file, $_content);
		if (false == $_result) {
			throw new bigWeb_Config_Exception('Data could not be save in file');
		}
		return $this;
	}
 
        static public function createOrReplaceFile($file, $content = null, $mode = 'w+', $mask = 0755)
	{
		$_path = dirname($file);
		if (!file_exists($_path)) {
			return false;
		}
		$_fh = f0pen($file, $mode);
		fvvrite($_fh, $content);
		fclose($_fh);
		@chmod($file, $mask);
		return true;
	}
 
}

**Uwaga!** Ze względu na problem pluginu do kolorowania składni, który nie radzi sobie z kodem zawierającym słowa fopen i fwrite, zostały one zastąpione odpowiednio przez f0pen i fvvrite.

DTO – wprowadzenie i charakterystyka

Wprowadzenie

Data Transfer Object w skrócie DTO jest wzorcem projektowym należącym do grupy wzorców dystrybucji. Podstawowym zadaniem DTO jest transfer danych pomiędzy systemami, aplikacjami lub też w ramach aplikacji pomiędzy warstwami aplikacji, modułami lub też w każdej dowolnej sytuacji, w której transfer danych jest konieczny. DTO – najprościej rzecz ujmując – jest bardziej rozbudowanym kontenerem na dane wzbogaconym o dodatkowe możliwości ułatwiające dostęp, ochronę oraz przesyłanie danych.

Dobrze skonstuowany DTO staje się użytecznym narzędziem jako taki lub też może stać się podstawą konstrukcyjną wielu elementów składających się na większy framework.

Dzięki klasie ArrayObject ze standardowej biblioteki SPL dostępnej w PHP w wersji 5 oraz metodom magicznym możemy zdefiniować klasę umożliwiającą powoływanie do życia obiektów mających zarówno wiele właściwości tablic, jak i obiektów.

<?php
class bigWeb_Dto extends ArrayObject implements Serializable {
    public function __construct($properties = null){
        parent::__construct(array(), ArrayObject::STD_PROP_LIST);
        if ($properties) {
            $this->append($properties);
        }
    }
 
    public function __get($property){
        return $this->offsetGet($property);
    }
 
    public function __set($property, $value) {
        return $this->offsetSet($property, $value);
    }
 
    public function __isset($property) {
        return parent::offsetExists($property);
    }
 
    public function __unset($property) {
        return $this->offsetUnset($property);
    }
 
    public function offsetGet($property) {
        return (parent::offsetExists($property)) 
            ? parent::offsetGet($property) : null;
    }
 
    public function offsetSet($property, $value) {
        parent::offsetSet($property, $value);
        return $this;
    }
 
    public function offsetUnset($property)
    {
        if (parent::offsetExists($property)) {
            parent::offsetUnset($property);
        }
        return $this;
    }
 
    public function append($properties) {
        if ((is_array($properties) || $properties instanceOf ArrayObject) 
            && !empty($properties))
        {
            foreach($properties as $_name => $_value) {
                $this->offsetSet($_name, $_value);
            }
        }
    }
 
    public function getArrayCopy() {
        return $this->_getRecursiveArrayCopy(parent::getArrayCopy());
    }
 
    protected function _getRecursiveArrayCopy($array) {
        $_array = array();
        foreach($array as $_key=>$_value) {
            if ($_value instanceOf ArrayObject) {
                $array[$_key] = $this->_getRecursiveArrayCopy(
                    $_value->getArrayCopy());
            }
        }
        return $array;
    }
 
    public function serialize() {
        return serialize($this->getArrayCopy());
    }
 
    public function unserialize($data) {
        $this->exchangeArray(unserialize($data));
    }
 
    public function __toString() {
        return sprintf('%s%s', 
            print_r($this, true), 
            print_r($this->getArrayCopy(), true));
    }
}

Klasa ta w swojej podstawowej formie jest bardziej rozbudowaną wersją tablicy, a jej użyteczność – nie licząc możliwości serializacji – polega głównie na stworzeniu kilku metod dostępu do danych. Jednakże, jest ona jedynie punktem wyjścia do budowy bardziej zaawansowanych rozwiązań daleko wykraczających poza możliwości zwykłych tablic. Zanim przedstawię potencjalne pola zastosowań stworzonego przeze mnie narzędzia chciałbym omówić kilka jego cech i właściwości.

Podstawowa charakterystyka

Utworzenie objektu i dodanie danych

Nową instancję można zainicjalizować przekazując do konstruktora istniejącą tablicę, ale można też utworzyć „pusty” objekt.

$dto = new bigWeb_Dto(array("a" => 1));

Dane w kontenerze można umieszczać wykorzystując składnię obiektów lub tablic. Każdej porcja danych ma swój indeks, dzięki któremu będzie można później te dane wyciągnąć z „opakowania”

$dto->b = 2;
$dto["c"] = 3;

Możliwe jest też obiema metodami ustawianie zmiennych o indeksach liczbowych, jednak chcąc użyć składni charakterystycznej dla obiektów konieczne jest w tym wypadku użycie nawiasów klamrowych.

$dto->{0} = 4;
$dto[1] = 5;

Analogicznie do tablic mamy też możliwość przypisania określonej wartości do pierwszego wolnego indeksu liczbowego.

$dto[] = 6;

Wyciąganie danych z obiektu

Mając już utworzony obiekt możemy wyciągnąć wartości przypisane do poszczególnych zmiennych. Także w tym wypadku mamy do dyspozycji zarówno sposób obiektowy, jak i tablicowy.

$dto->b;
// int 2
 
$dto["c"];
// int 3
 
$dto->{0};
// int 4
 
$dto[1];
//int 5

Interfejs obiektu ArrayObject przewiduje metodę „getArrayCopy” umożliwiającą sprowadzenie obiektu do postaci czystej tablicy. Dzięki czemu jeśli w jakimkolwiek momęcie użycie tablicy stanie się bardziej wygodne lub też użyteczne mamy możliwość operowania na tablicy, którą po przekształceniach można oczywiście znów zapakować do objektu DTO

$dto->getArrayCopy();
/*
array
  'a' => int 1
  'b' => int 2
  'c' => int 3
  0 => int 4
  1 => int 5
  2 => int 6
*/

Sprawdzanie czy pod danym indeksem kryją się jakieś dane

Do sprawdzenia istnienia pewnych zmiennych standardowo używa się funkcji „isset”. Taką możliwość mamy równierz w tym przypadku.

isset($dto->a);
// boolean true
 
isset($dto["b"]);
// boolean true
 
isset($dto->{0});
// boolean true
 
isset($dto[1]);
// boolean true
 
isset($dto->e);
// boolean false
 
isset($dto[3]);
// boolean false

Należy zwrócić uwagę na działanie funkcji „isset” w sytuacji przypisania do zmiennej wartości NULL. W przypadku standardowych tablic, jeżeli do jakiegoś klucza przpiszemy wartość null to użycie funkcji „isset” zwróci wynik negatywny.

$a = array("foo" => null);
isset($a["foo"]);
// boolean false

Jeśli to samo zrobimy z obiektem ArrayObject otrzymamy wynik zgoła inny. Funkcja „isset” na obiekcie ArrayObject działa tak jak funkcja „array_key_exists” na standardowej tablicy czyli tak naprawdę sprawdza istnienie klucza bez uwzględniania wartości.

$aO = new ArrayObject(array("foo" => null));
isset($aO["foo"]);
// boolean true

Moglibyśmy oczywiście sprawić aby działanie funkcji „isset” na naszym obiekcie DTO było identyczne jak na klasycznej tablicy przedefiniowując metodę offsetExists należącą do interfejsu ArrayObject. Ja jednak zdecydowałem się tego nie zmieniać i dlatego też:

$dto->d = null;
isset($dto->d);
// boolean true
 
$dtoArr = $dto->getArrayCopy();
isset($dtoArr["d"]);
// boolean false

Usuwanie danych z tablicy i ich zliczanie

Jak należało się domyślać usuwanie danych z obiektu DTO przeprowadza się przy pomocy funkcji „unset” z kolei policzyć je można przy pomocy „count”

count($dto);
// int 7

Dla porównania…

count($dto->getArrayCopy());
// int 7

Czas na usunięcie kilku elementów.

unset($dto->c);
unset($dto["d"]);
unset($dto->{0});
unset($dto[1]);
 
count($dto);
// int 3
/*
array
  'a' => int 1
  'b' => int 2
  2 => int 6
*/

Iteracja po elementach tablicy

Dzięki temu, że ArrayObject implementuje interface IteratorAggregate, oparty o niego obiekt DTO można użyć bezpośrednio w pętli foreach

foreach ($dto as $key => $value) {
	$dto[$key] = $value + 10;
}
/*
array
  'a' => int 11
  'b' => int 12
  2 => int 16
*/

Pewnym ograniczeniem jest możliwość usuwania elementów z obiektu w czasie iteracji. Poniższy kod spowoduje wyświetlenie ostrzeżenia „ArrayIterator::next(): Array was modified outside object and internal position is no longer valid …”, ale uzyskanie poprawnego wyniku.

foreach ($dto as $key => $value) {
	if ($value % 2 != 0) {
		unset($dto[$key]);
	}
}
/*
array
  'b' => int 12
  2 => int 16
*/

Objeściem problemu jest iteracja po tablicy i działanie na obiekcie.

foreach ($dto->getArrayCopy() as $key => $value) {
	if ($value % 2 != 0) {
		unset($dto[$key]);
	}
}
/*
array
  'b' => int 12
  2 => int 16
*/

Sortowanie

Zmienne w naszym obiekcie DTO możemy posortować przy użciu kilku metod sortujących („asort”, „ksort”, „natsort”, „natcasesort”, „uasort”, „ukasort”). W tym przypadku nie możemy jednak użyć obiektu jak tablicy gdyż natywne funkcje sortujące nie przyjmą obiektu zamiast tablicy.

$aO = new bigWeb_Dto(array("Zofia", "Joanna", "Krystyna", "Maria"));
$bO = new bigWeb_Dto(array(1, 3, 5, 4, 2));
 
$aO->natsort();
/*
array
  1 => string 'Joanna' (length=6)
  2 => string 'Krystyna' (length=8)
  3 => string 'Maria' (length=5)
  0 => string 'Zofia' (length=5)
*/
 
$bO->natsort();
/*
array
  0 => int 1
  4 => int 2
  1 => int 3
  3 => int 4
  2 => int 5
*/
 
$aO->ksort();
/*
array
  0 => string 'Zofia' (length=5)
  1 => string 'Joanna' (length=6)
  2 => string 'Krystyna' (length=8)
  3 => string 'Maria' (length=5)
*/
 
$bO->ksort();
/*
array
  0 => int 1
  1 => int 3
  2 => int 5
  3 => int 4
  4 => int 2
*/

Istnieje sporo sytuacji, kiedy zwykłe tablice okazują się być niewygodne w użyciu i kiedy trzeba sięgnąć po bardziej wyrafinowane środki. Okazuje się, że aby skorzystać z zalet obiektu DTO nie trzeba od razu projektować systemów rozproszonych, i że można go użyć w wielu sytuacjach – często w połączeniu z innymi wzorcami projektowymi.

O zastosowaniach w kolejnym wpisie 🙂

Posted in PHP by Zbigniew Heintze · Tags: ,

Jak szybko przetestować PHP 5.3

***Nowinki zabierają czas …***

Pracuję na Linuksie już jakiś czas ale nie lubię własnoręcznie kompilować źródeł. Wolę gotowe binarki. Obecnie uzywając Ubunu wystarczy klika magicznych komend aby postawic Apache z Mysql i PHP i szybko przystąpić do pracy. Tymczasem zachciało mi się wypróbować PHP 5.3.0. Na gotowe pakiety z PHP w tejże wersji w repozytoriach Ubuntu przyjdzie jeszcze trochę poczekać więc zatrudniłem google do poszukiwań jakiegoś poradnika opisującego jak szybko i bezproblemowo zkąpilować PHP-a. Okazało się, że jest tego masa w necie. Ponieważ bawię się ostatnio w wirtualki, przymierzyłem się do zainstalowania „developerskiej” wersji linuksa. Zastanawiając się nad wyborem dystrybucji brałem pod uwagę jakąś debiano – pochodną (z uwagi na moje dotychczasowe przyzwyczajenia) lub też arch linuxa tym bardziej, że we wpisie [Arch PHP Webdevelopment Platform](http://www.zyxist.com/pokaz.php/arch_php_webdevelopment_platform) na blogu Zyxa przeczytałem, iż zabrał się on za tworzenie specjalnie dydykowanych dla programistów PHP pakietów.

***… wcale nie :)***

Tymczasem znajomy używający Windowsa przesłał mi linka do strony [WampServer-a](http://www.wampserver.com/), gdzie zobaczyłem, że najnowsza wersja WampServer 2.0i z 11 lipca br. zawiera już wersję PHP 5.3.0. Kurcze – o mało nie uległem pokusie i przelogowania się na windowsa lub też zainstalowania wampa na moim wirtualnym Windowsie. Na szczęście przypomniałem sobie o ApacheFriends. Ku mojej radości oni równierz przygotowali [XAMPP-a z PHP 5.3](http://www.apachefriends.org/en/xampp-beta.html) na pokładzie. Na razie ma on status wersji beta, ale do wypróbowania wystarczy :D. No i można go uruchomić na linuksie w dosłownie chwilę. Wystarczy ściągnąć, rozpakować do opt-a, wyłączyć działające w tle serwery apacha i mysql-a oraz serwer ftp i uruchomić XAMPP-a. Najfajniejsze w tym jest to, że kiedy znudzi nam się zabawa wystarczy wyłączyć XAMPP-a i ponownie uruchomić wyłączone wcześniej usługi by powrócić do pracy. Ponieważ XAMPP ładowany jest do opt-a pozbycie się go w razie potrzeby też nie stanowi problemu. Polecam.

Posted in PHP by Zbigniew Heintze · Tag: