PHP

PHP 5.3 – dirname(__FILE__) vs __DIR__

Jedną z najczęstszych optymalizacji kodu jest ładowanie plików z lokalizacji bezwzględnej gdyż ogranicza to konieczność przeszukiwania przez PHP ścieżek include_path. Skrypt, który wykonuje:

require 'd:/www/frame/dev/Core.php';

zadziała szybciej niż

require 'Core.php'

Dlatego przy dyrektywach require i include można najpierw zbadać położenie skryptu aby opracować ścieżkę dostępu. Najczęściej stosuje się polecenie:

$path = dirname(__FILE__);

w rezultacie otrzymamy katalog w którym znajduje się obecnie uruchomiony skrypt. W bardzo wielu systemach jest to używane. Jednakże od wersji PHP 5.3 istnieje stała __DIR__, która zwraca dokładnie to co powyższy kod. Dlaczego warto jej używać? Szybki benchmark:

ile trwa wywołanie po 1 mln razy:

__DIR__ 0.1562s
dirname(__FILE__) 0.4114s

Wynik nie powinien nikogo dziwić, zatem myślę, że warto się na tę stałą przestawić.

Pliki konfiguracyjne i aplikacje w PHP

Ostatnio po raz kolejny mierzę się z problemem przechowywania informacji konfiguracyjnych dla aplikacji napisanych w języku PHP. Jak to zrobić? Czego użyć? Co będzie najprostsze i najbardziej wygodne i przede wszystkim dla mnie…. co będzie najszybsze? Oczywiście chciałbym znaleźć złoty środek, który łączy wszystkie te cechy w jednym. Czy tak się da? Zobaczymy… ale najpierw mały wstęp:

Nie wszystko złoto co się świeci

PHP to taki dziwny język. Przez jednych kochany przez innych nienawidzony. Jednak jest w nim coś takiego co sprawia, że mnie cały czas zadziwia i ciągle uczę się o nim czegoś nowego. Ciekawe jest to, że pewne rzeczy, które wydawałoby się, że powinny działać lepiej/szybciej od innych zachowują się zupełnie odwrotnie.

Osoby, które mają jakieś pojęcie o językach takich jak np C i starają się pisać kod w sposób wydajny i pamięciooszczędny mają wyrobione pewne reguły postępowania, które pomagają ten cel przybliżyć takie jak:

  • wskaźniki do obiektów
  • zmienne właściwego typu
  • zarządzanie pamięcią
  • dyrektywa define
  • itd

Co ciekawe… z racji tego, że język PHP jest składniowo bardzo podobny do języka C++ a ponadto występują w nim podobne konstrukcje to naturalnym odruchem wydaje się chęć ich używania. Niestety tutaj możemy wpaść w pułapkę. Przede wszystkim trzeba sobie za każdym razem przypomnieć, że aplikacja w języku PHP i jego specyfika jest zupełnie inna niż ta w języku C. Weźmy na tapetę “define”. W jeżyku C taka konstrukcja #define PI  3.14 pozwala “nazwać” pewną stałą wartość np liczbę 3.14 nazwać PI i zamiast pisać w kodzie 3.14 używamy PI. Kompilator zamieni wszystkie wystąpienia PI na liczbę 3.14.  Dzięki temu uzyskamy bardzo wydajny kod, jednocześnie mając wszystko ładnie poukładane i pod kontrolą. Oczywiście można tworzyć o wiele bardziej skomplikowane rzeczy ale nie o to teraz chodzi. Wydawałoby się, że to samo można zastosować do PHP. Przecież tutaj tez mamy konstrukcję define(“PI”, 3.14); Jednakże tutaj pojawia się problem. Trzeba wziąć pod uwagę, że w aplikacji napisanej w jezyku C interesuje już nas samo uruchomienie. Natomiast w PHP na każde uruchomienie skryptu składa się komplikacja i wykonanie. W związku z tym, nie daje to żadnego przyspieszenia – a wręcz zwalnia to program. PHP więcej czasu potrzebuje na uruchomienie funkcji define niż stworzenie zwykłej zmiennej. Takie przykłady można mnożyć jednak po tym przydługim wstępie chciałem przejść do meritum:

Kod PHP wolniej działa w PHP

Nagłówek może nieco wydawać się enigmatyczny lub pozbawiony sensu ale istocie oddaje on całą prawdę. Okazuje się, że niektóre konstrukcje samego języka PHP mogą działać wolniej niż funkcje dostarczone do języka. Nawiązując do tematu tego posta stanąłem przed problemem zdecydowania się na system, za pomocą którego mógłbym przechowywać dane konfiguracyjne aplikacji w języku PHP (ściślej mówiąc projektowanego frameworka). Co mnie interesowało i wziąłem pod uwagę:

  1. Serializowana tablica
  2. JSON
  3. plik INI
  4. plik PHP z tablicą

Żeby zbyt wiele nie wróżyć postanowiłem przeprowadzić testy wydajnościowe. Format danych który chciałbym załadować to węzeł x1, który zawiera 2 pary wartości y1 = 1 oraz y2 = 2 oraz węzeł x2, który zawiera 2 pary y3= 1 oraz y4 = 2

Dla każdej z metod zapisałem dane w odpowiednim formacie:

serializowana tablica

a:2:{s:2:”x1″;a:2:{s:2:”y1″;s:1:”1″;s:2:”y2″;s:1:”2″;}s:2:”x2″;a:2:{s:2:”y3″;s:1:”1″;s:2:”y4″;s:1:”2″;}}

JSON
{“x1”:{“y1″:”1″,”y2″:”2″},”x2”:{“y3″:”1″,”y4″:”2”}}

plik INI
[x1]
y1 = 1
y2 = 2
[x2]
y3 = 1
y4 = 2

plik PHP z tablicą
$conf_2 = array(
‘x1’ => array( ‘y1’ => 1, ‘y2’ => 2 ), ‘x2’ => array( ‘y3’ => 1, ‘y4’ => 2 ));

Procedura testowa

Test polegał na 10000 krotnym wywołaniu danej funkcji i odczytaniu danych. Dla każdej z metod wykonałem odpowiednie procedury:

serializowana tablica

$conf_3 = unserialize(file_get_contents(‘./array.ser’));

JSON
$conf_5 = json_decode(file_get_contents(‘./array.json’), TRUE);

plik INI
$conf_1 = parse_ini_file(‘./array.ini’, true);

plik PHP z tablicą
require(‘./array.php’);

Wyniki testu

Windows 7 (PHP 5.3.1)

  1. Plik INI (0.9728s)
  2. Require (1.0595s)
  3. Unserialize (1.0884s)
  4. JSON (1.1447s)

Linux (PHP 5.3.6)

  1. JSON (0.1267s)
  2. Plik INI (0.1744s)
  3. Unserialize (0.1892s)
  4. Require (0.2466s)

Podsumowanie

W tym momencie ciężko wyłonić zwycięzce. Dziwi mnie tylko tak duża różnica w wydajności działania funkcji json_decode między platformą Windows i Linux. Co prawda nie jest to ta sama wersja PHP ale nie powinno to mieć wpływu na działanie tej funkcji. Test ten wskazuje prędkość działania konkretnych rozwiązań, ale nie mówi on nic o czytelności i łatwości korzystania a tutaj nie trudno wskazać zwycięzce – moim zdaniem plik INI jest w tej kwestii bezkonkurencyjny. Po pierwsze większość edytorów koloruje składnie plików INI. Poza tym (co może być uważane także za minus) można w nim stworzyć tylko rzeczy, które służą strice do konfiguracji – a więc nie ma obawy, że ktoś przecholuje i zacznie wrzucać tam jakieś funkcje, dziwne zależności, zmienne globalne itd. Poza tym parser plików INI w PHP potrafi także interpretować stałe (np E_NOTICE –  tak jakby widział je w kodzie) oraz przechowywać tablice. Jedyny minus to taki, że nie potrafi on skonwertować danych na rzeczywiste typu… np “true” to nie będzie w PHP “true” tylko “1” .. każda inna nieprawda  to będzie pusty ciąg znaków.

Ponadto widać tu także inną rzecz. Wykonanie funkcji “parse_ini_file”, która uruchamia funkcję mającą zaczynać plik z dysku, sparsować i pozmieniać jego dane na format rozpoznawalny przez PHP działa szybciej niż załadowanie już gotowego formatu danych PHP z pliku PHP za pomocą instrukcji języka. Dziwne? I tak nie. parse_ini_file jest funkcją napisaną w języku C przez co wykonuje się ona zdecydowanie szybciej niż instrukcja PHP. Myślę, że istnieje jeszcze wiele takich króczków i na pewno będę je dalej badał.

Chciałbym jeszcze wspomnieć, że oczywiście zdaję sobie sprawę z istnienia akceleratorów, optcode’u, gotowych klas parsujących (np w Zend Framework) języka YAML, memcache’a itd itd, ale chciałem w czystym środowisku ukazać specyfikę samego języka PHP aby poznać skąd się co wzięło i z czego wynika. Jeśli nie jesteśmy zmuszeni aby używać systemu poprawiającego działanie innego systemu to dobrze. W programowaniu im mniej skomplikowany algorytm tym lepiej.

Wydajne aplikacje w PHP

Jeżeli nasza aplikacja pracuje pod dużym obciążeniem odpowiednie zoptymalizowanie jej jest kluczowe.

W tym artykule chciałbym zaprezentować różne często popełniane błędy w podejściu do pisania aplikacji w języku PHP a także sztuczki i propozycje, które poprawiają wydajność. Postaram się pokazać jak różne niewydajne elementy zastąpić elementami wydajnymi. Niektóre zabiegi są naprawdę bardzo proste a poprawiają wydajność aplikacji nawet kilkukrotnie (oczywiście w skali makro). Aby zobaczyć różnicę w wydajności będę konkretne operacje powtarzał w pętli po 100 lub 1000 razy, żeby uzbierał się odpowiedni czas na testowanie. Pozwoli to odwzorować przeciętne obciążenie strony.

Maszyna testowa:

Procesor: P4 3,2 GHz (s478 Northwood)
Pamięć: 2 GB DDR Dual (Kingston)
Dysk: 2x 60GB WD (Raid 0, SATA)
Płyta: Abit IC-7g

Windows Vista, XAMPP (PHP 5.3)

Elementy obciążające PHP

Przygotowuje sobie w międzyczasie implementację ActiveRecord do środowiska Prime. Jednym z elementów założenia jest zwrócenie listy obiektów (aktywnych rekordów), z których każdy posiada zestaw wartości (kolumny wiersza) oraz metody. W zasadzie jest instancją takiego samego obiektu jak element potomny.

zwykłe wykonanie kodu, który za pomocą PDO, wykonuje zapytanie do bazy i zwraca po prostu tablicę z wynikami przy 1000 wywołaniach:

Mniej więcej wygląda to tak:

	public function select($where = null, $args = array()) {

		$sql = 'SELECT ' . $this->_columns . ' FROM ' . $this->_table;

		if ($where) {
			$sql .= ' WHERE ' . $where;
		}

		$data = array();

		$stmt = $this->_dbh->prepare($sql);

		foreach($args as $k => $v) {
			$stmt->bindValue($k+1, $v);
		}

		$stmt->execute();

		while ($row = $stmt->fetch(PDO::FETCH_OBJ)) {
			$data[] = $row;
		}

		$stmt->closeCursor();

		return $data;
	}

I teraz test:

$Test = new Test();
for($i=1;$i<1000;$i++)
	$a = $Test->select();

Jak widać. Klasa wykonuje odpala zapytanie 1000 razy i zwraca obiekt z wynikami. Obiekt PDO typu FETCH_OBJ

To daje średni czas: 0,688 sekundy

Problem zaczyna się gdy zwrócona lista nie ma być zwykłą listą wyników lecz listą obiektów tej klasy. Przedstawia się to mniej więcej tak

public function findAll($conditions = '', $arguments = array(), $order = '', $limit = '')
 	{
		$add = null;

		if (strlen($order) > 0)
 			$add .= ' order by '.$order;

		if (strlen($limit) > 0)
 			$add .= ' limit '.$limit;

		$rows = $this->_select($conditions, $arguments, $add);

		$classname = get_class($this);
 		$retval = array();

		foreach($rows as $row) {
			$obj = new $classname();
 			$obj->loadFromArray($row);
 			$retval[] = $obj;
		}

 		return $retval;
 	}

I test:

$Test = new Test();
for($i=1;$i<1000;$i++)
	$a = $Test->findAll();

W takim oto zestawieniu czas wynosi średnio: 5,1265 sekundy

Ups! Chyba trochę za dużo. Zaczęły się więc poszukiwania “wąskiego gardła”. Jak widać, w pętli metody findAll tworzy się za każdym razem obiekt tej samej klasy (żeby rezultat był active recordem) Dotarłem więc do konstruktora, którzy wygląda tak:

	public function init($classname, $conditions = '', $order = '')
 	{
 		if (isset($this))
 			$obj = &$this;
 		else
 			$obj = new $classname();

 		if ($obj->table == '')
 			$obj->table = strtolower($classname);

 		if ($obj->data === false)
			$obj->clear();

		require_once 'Database.php';
		$this->_dbh = Database::getInstance();
 	}

Mamy tu jedną bardzo istotną rzecz: “require_once”. Każdy programista PHP wie do czego ta instrukcja służy ale jednak jak widać nie zawsze wiadomo jak właściwie ją zastosować. “_once” zapewnia nam, że ponowne wywołanie tej instrukcji już się nie wykona jeśli wykonało się wcześniej. Jeśli napisałbym “require” to za każdym razem require tego pliku by się wykonywał. Jakie to ma znaczenie dla aplikacji?

W sieci można znaleźć wiele testów mających na celu wykazanie wyższości jednego nad drugim, jednakże wiele z tego zależy od użytych systemów dodatkowych takich jak APC, systemu operacyjnego, sprzętu, buforowania dysku i pamięci podręcznej, itd – takie testy nie są zbyt pomocne jeśli nie można do końca przewidzieć środowiska którym aplikacja będzie pracować. Teoretycznie, “require” czy też “include” powinny być szybsze od swoich odpowiedników “_once”, ale trzeba wziąć pod uwagę ich zastosowanie.

Jeżeli taki test przeprowadzony jest wewnątrz aplikacji (tak jak u mnie), to “require” przegrywa na całej linii: Dla porównania:

1000x wykonany require_once: 3,423 sekundy
1000x wykonany require 6,356 sekundy

Jednak trzeba na problem spojrzeć z innej strony. W takim przypadku warto stosować zawsze funkcje “_once” gdyż:

  1. Działa ona szybciej gdyż nie wykonuje ponownie załączonego kodu
  2. Zapewnia, że aplikacja nie wyłoży się w momencie załadowania pliku jeszcze raz (bo się po prostu drugi raz nie załaduje – będzie już rezydowała w pamięci dla danego czasu życia aplikacji)

Gdy jednak spojrzy się na to pod względem testu całej aplikacji, tzn “1000x żądanie odpyta się skrypt” a nie “1000x skrypt odpyta plik” to wtedy warto zastosować “require” gdyż jest szybsze. Dokładniej przedstawić takie scenariusze:

  1. 1000 użytkowników -> request -> skrypt (załączenie pliku “A.php”)
  2. 1 użytkownik -> request -> skrypt (1000x załączenie pliku A)
  3. 1 użytkownik -> request -> skrypt (załadowanie 1000 różnych plików)

Jak należy postąpić w przypadku w/w scenariuszy?

1) Jeżeli pojawi się wiele requestów skryptu, który załącza sobie podelementy samego siebie ale tylko RAZ (np jakieś klasy do obsługi bazy, do obsługi wyjątków itd, i robi to tylko raz w ciągu działania aplikacji – użyj “require

2) Jeżeli jeden użytkownik uruchomi skrypt, który gdzieś ma 1000x załączenie tego samego pliku, użyj “require_once

3) Jeżeli jeden użytkownik uruchomi skrypt, który gdzieś ma załączenie 1000 różnych plików użyj “require” (jeśli robi to tylko raz, lub “require_once” jeśli robi to wiele razy)

Wszystko to sprowadza się do bardzo prostej konkluzji, która mówi, że używanie funkcji “require” jest jak widać dosyć czasochłonne, więc należałoby to robić jak najrzadziej.

Omawianą klasę do obsługi ActiveRecord wystarczy więc przerobić tak, aby nie robiła “require” za każdym razem gdyż jest to niepotrzebne. Jeżeli w swojej aplikacji przewidujesz, że ta klasa będzie używana w 99% przypadków to załącz ją do “rdzenia”

Zatem, gdy usunę tylko ten require z konstruktora, co prezentuje się tak:

	public function init($classname, $conditions = '', $order = '')
 	{
 		if (isset($this))
 			$obj = &$this;
 		else
 			$obj = new $classname();

 		if ($obj->table == '')
 			$obj->table = strtolower($classname);

 		if ($obj->data === false)
			$obj->clear();

		$this->_dbh = Database::getInstance();
 	}

Wynik testu zmienia się na: 0,913 sekundy

Przed zmianą było to: 5,1265 sekundy

A więc widać, że znaczący miało to wpływ.

Poprawianie wydajności

Udało nam się poprawić czas. Podstawowe zwrócenie wyniku przez PDO przypomnę, że było to: 0,688 sekundy

Zwrócenie obiektu danej klasy zdecydowanie opóźniło czas, ale dzięki różnym zabiegom udało się zejść z 5,1265 sekundy do 0,913 sekundy

Ale czy da się zrobić coś więcej? Tak.

Zacytuję jeszcze linijkę:

foreach($rows as $row) {
			$obj = new $classname();
 			$obj->loadFromArray($row);
 			$retval[] = $obj;
		}

Chcemy zwrócić tablice activerecordów, które są de facto obiektamy tej samej klasy. Po co jednak za każdym razem tworzyć nowy obiekt? Czy nie lepiej byłoby skopiować go i podmienić tylko wartości? Tutaj z pomocą przychodzi nam polecenie “clone“. Clone tworzy nam dokładną kopię danego obiektu jednak w postaci zupełnie osobnej, niezależnej instancji. Tworzy się dokładna kopia takiego obiektu i możemy na nim pracować zupełnie niezależnie – ale dzięki temu nie musimy tworzyć całej klasy od nowa, uruchamiać konstruktorów itd itd. Jak to wpływa na wydajność? Gdy zastąpimy tworzenie obiektu klonowaniem:

foreach($rows as $row) {
			$obj = clone $this;
 			$obj->loadFromArray($row);
 			$retval[] = $obj;
		}

Czas wykonania skryptu spada do: 0,804 sekundy

Nieźle? Praktycznie dotarliśmy do wyniku klasy, która robi tylko zwykłe zapytanie. Tak naprawdę w tym momencie nie musimy się już przejmować optymalizacją funkcji w klasie gdyż obciążenie będzie już wynikało tylko z tego, że sama klasa jest bardziej skomplikowana jako struktura. Na to już nic poradzimy. Widać jednak, że dzięki kilku prostym sztuczkom udało się bardzo mocno zoptymalizować działanie takiej aplikacji.

Prime – Request

Witam w serii artykułów opisujących implementację komponentów budujących zintegrowany system Prime.

Zacznij od przeczytania artykułu wstępnego jeśli tego nie zrobiłeś.

Z punktu widzenia użytkownika na początku pojawia się “żądanie“. Obsługą żądań zajmuje się komponent o nazwie “Request

Pola

Komponent posiada pola w których przechowuje wartości o żądaniu.

  1. Informacje o danych POST
  2. Informacje o danych GET
  3. Informacja o adresie żądania
  4. Informacja o ciasteczkach
  5. Informacja o parametrach adresu (coś podobnego do Zend’a)
private $_requestUri = null;
private $_post = null;
private $_get = null;
private $_cookie = null;
private $_paramAssoc = null;
private $_paramEnum = null;

Info! Można zauważyć, że dla parametrów adresu przewidziane są dwa pola. Pole paramAssoc będzie przechowywało wartości w postaci tablicy asocjacyjnej. Natomiast paramEnum będzie przechowywało wartości w postaci numerycznej. Obrazując to na przykładzie:

Jeżeli zostanie wywołany taki adres:

http://www.mojastrona.pl/przegladanie/kategoria/nazwa/AGD/strona/1/

Dla nieobytych z rozwiązaniami Zend’a można to podzielić w taki sposób:

  • <= kontroler
  • <= akcja
  • <= nazwa parametru pierwszego
  • <= wartość parametru pierwszego
  • <= nazwa parametru drugiego
  • <1> <= wartość parametru drugiego

Tablice paramAssoc i paramEnum przechowują tylko nazwy i wartości parametrów z tym, że w taki oto sposób:

Gdybyśmy wypluli zawartość tablicy paramAssoc:

Array
(
    [nazwa] => AGD,
    [strona] => 1
)

Gdybyśmy wypluli zawartość tablicy paramEnum:

Array
(
    [0] => nazwa,
    [1] => AGD,
    [2] => strona,
    [3] => 1
)

Mam nadzieję, że widać na czym polega różnica. Po co to rozbicie? Za chwilę wyjaśnię.

Metody

Komponent oferuje dostęp do poniższych metod:

 // Pobieranie adresu żądania
public function getUri()

// Pobranie parametru
public function getParam($param = null, $default = null)

// Pobranie wartości post
public function getPost($param = null, $default = null)

// Pobranie wartości get
public function getGet($param = null, $default = null)

// Pobranie informacji czy zostało wykonane żądanie POST
public function isPost()

Implementacja

Przy tworzeniu instancji obiektu Request uruchamiany jest konstruktor

function __construct() {
	$this->_requestUri = $_SERVER['REQUEST_URI'];
	$this->_post = $_POST;
	$this->_get = $_GET;
	$this->_cookie = $_COOKIE;

	$this->_requestUri = trim(str_replace('?' . $_SERVER['QUERY_STRING'], '', $this->_requestUri), '/ ');
}

Nie ma tutaj nic bardzo magicznego. Do odpowiednich pól zapisywane są wartości z odpowiednich tablic. Jedyny bardziej skomplikowany kawałek kodu (linijka 7) polega na tym by z requestu usunąć parametry tzw “Query String” – czyli te parametry po znaku “?” w adresie. Mamy je zapisane w tablicy GET i nie są one nam w adresie potrzebne. Nie znaczy to, że są niedostępne w frameworku. Chodzi o to by komponent zrozumiał o jakie żądanie chodzi i właściwie sobie podzielił elementy.

Kolejną ciekawą metodą wartą wyjaśnienia jest getParam

public function getParam($param = null, $default = null) {
	if ($param === null) {
		return $this->_paramAssoc;
	}

	if (is_int($param)) {
		if (isset($this->_paramEnum[$param])) {
			return $this->_paramEnum[$param];
		} else {
			return $default;
		}
	} else {
		if (isset($this->_paramAssoc[$param])) {
			return $this->_paramAssoc[$param];
		} else {
			return $default;
		}
	}
}

Pierwszym argumentem jest nazwa parametru. I teraz własnie wyjaśnię wykorzystanie osobnych tablic paramAssoc i paramEnum

Jeżeli żaden argument nie zostanie przekazany to metoda zwróci całą tablicę wszystkich parametrów. Jeżeli natomiast przekazany argument będzie liczbą całkowitą to zostanie pobrana wartość z tablicy paramEnum. W przeciwym wypadku z tablicy paramAssoc.

Drugi parametr służy do tego by zwrócić wartość domyślą jeśliby żądany parametr nie istniał.

Przykład:

$this->getParam(0)

Zwróci w naszym przypadku wartość “nazwa”

$this->getParam(1)

Zwróci w naszym przypadku wartość “AGD”

Jeżeli jednak zamiast wartości numerycznej zażądamy parametru po nazwie to:

$this->getParam('kategoria')

Zwróci “AGD”

Używanie parametrów po nazwach jest zalecane gdyż nie wprowadza zamieszania i wiadomo na pewno którą wartość pobieramy. Wywołanie

$this->getParam('AGD')

Zwróci NULL gdyż nie ma parametru o takiej nazwie. Jest to tylko wartość parametru kategoria.

Co w przypadku gdyby ktoś nie podał kategorii a chcemy aby system domyślnie coś pod ten parametr podstawił, żeby już nie trzeba było tego sprawdzać? Wtedy używamy drugiego argumentu:

$this->getParam('strona', 1)

Jeżeli ktoś poda parametr “strona” to zostanie on użyty. W przypadku braku parametru “strona” zostanie zwrócona wartość 1.

Metody getGet oraz getPost działają w zasadzie identycznie jak getParam. Z tą różnicą, że przyjmują parametry jedynie jako nazwy (nie można ich pobierać w sposób numeryczny)

To w zasadzie tyle. Komponent posiada jeszcze kilka metod odpowiedzialnych za komunikację z komponentem “Request”, który będzie bohaterem kolejnego odcinka, ale nie będę ich tu przedstawiał gdyż nie są one istotne do zrozumienia działania komponentu.

Pełny kod źródłowy

_requestUri = $_SERVER['REQUEST_URI'];
		$this->_post = $_POST;
		$this->_get = $_GET;
		$this->_cookie = $_COOKIE;

		// Cleanup data
		$this->_requestUri = trim(str_replace('?' . $_SERVER['QUERY_STRING'], '', $this->_requestUri), '/ ');
	}

	public function getUri() {
		return $this->_requestUri;
	}

	/**
	 * Get param by name or index. Having user/info/id/4 is posibly to get
	 * value of "id" param by getParam('id') is equal to '4' or getParam(0) is
	 * equal to 'id' or getParam(1) is equal to '4' and getParam('4') is not a
	 * parameter. Optionally if param doesn't is not perform a default value:
	 * while getParam('b') doesn't exists getParam('b', 'Hello') will return
	 * 'Hello'
	 * @param mixed $param Param name of Index
	 * @param mixed $default Use if param is not set0
	 * @return string Requested param value
	 */
	public function getParam($param = null, $default = null) {
		if ($param === null) {
			return $this->_paramAssoc;
		}

		if (is_int($param)) {
			if (isset($this->_paramEnum[$param])) {
				return $this->_paramEnum[$param];
			} else {
				return $default;
			}
		} else {
			if (isset($this->_paramAssoc[$param])) {
				return $this->_paramAssoc[$param];
			} else {
				return $default;
			}
		}
	}

	public function setParamEnum($param) {
		$this->_paramEnum = $param;
	}

	public function setParamAssoc($param) {
		$this->_paramAssoc = $param;
	}

	public function setParam($param, $value) {
		$this->_paramEnum[] = $value;
		$this->_paramAssoc[$param] = $value;
	}

	public function getPost($param = null, $default = null) {
		if ($param === null) {
			return $this->_post;
		}

		if (isset($this->_post[$param])) {
			return $this->_post[$param];
		} else {
			return $default;
		}
	}

	public function getGet($param = null, $default = null) {
		if ($param === null) {
			return $this->_get;
		}

		if (isset($this->_get[$param])) {
			return $this->_get[$param];
		} else {
			return $default;
		}
	}

	/**
	 * Zwraca info czy był POST
	 * @todo zrobić to lepiej
	 */
	public function isPost() {
		if ( ! empty($_POST)) {
			return true;
		} else {
			return false;
		}
	}
}

Możesz użyć tej klasy i wywołać ją samodzielnie. Zobaczyć jak działa i jak się zachowuje.

Na koniec pytanie – zagadka: Czy pobieranie parametrów w postaci tablicy numerycznej ma w ogóle sens i jest przydatne? Czekam na komentarze w tej sprawie.

Pozdrawiam.

Prime

Tak to jest, że czasem aby pójść dalej trzeba zawrócić i wybrać inną drogę. Tak samo jest z rozwijaniem umiejętności, projektów i celów sobie postawionych. GRAD Framework był rozwijany tylko przeze mnie a inicjatywa postała z chęci lepszego zrozumienia Zend Framework. ZF rozwija się bardzo szybko, rośnie do niebagatelnych rozmiarów i często przy pierwszym “starciu” z nim można się przerazić. GRAD rozwijał się w taki sposób, że “to co jest fajne w zendzie” starałem się zaimplementować do GRADA jednak nie korzystając w ogóle z tego jak kod w Zendzie jest napisany. Myślę, że mogę śmiało stwierdzić, że część rzeczy działa nawet lepiej i na pewno szybciej. GRAD Framework przyjął się bardzo dobrze w kilku środowiskach developerskich (zawodowych i amatorskich). Otwarcie projektu na innych pozwoliło mi na spojrzenie na framework z dystansu, dowiedziałem się wielu rzeczy o usability oraz o rzeczach, które w nim działają słabo lub czego w nim brakuje.

Z kolei z inicjatywy dalszych badań nad frameworkami takimi jak Code Igniter czy później Kohana, oraz badań nad frameworkiem Symfony, zrodziła się idea podobna do “GRAD’owej” a mianowicie Luna Framework. Niestety okazało się przy dalszym badaniu, że idea wykracza poza to do czego ludzie się przyzwyczaili przez co była zbyt “niestandardowa” aby programista chętnie do tego przysiadł i zgłębił jego założenia. Robienie kolejnego framework’a który robi wszystko inaczej wydała się bez sensu dlatego projekt porzuciłem.

Kilka miesięcy potem analizując ponownie GRAD’a, Lunę oraz powstający system CMS (który kiedyś zaprezentuję) oraz analizując kierunki którymi podąża ZF oraz to jak rozwija się PHP stworzyłem ideę zintegrowanego systemu pod nazwą Prime. W tej chwili pracuję nad rozwojem frameworka wykorzystującym komponenty w zwarty sposób. Tzn, idea polega na czymś zupełnie przeciwnym do założeń ZF. Framework stanowi całość – nie ma w nim w zasadzie rzeczy opcjonalnych. Dzięki temu system działa bardzo szybko i zajmuje niewiele pamięci. Ponadto pozwala na dzielenie swoich komponentów między wieloma aplikacjami. Podobnie jak w GRAD system będzie pozwalał na uruchamianie wielu aplikacji na tym samym czasie. Ponadto zintegrowany system będzie szybszy i czytelniejszy. (Zaprezentuję wkrótce porównanie wydajności).

W kolejnej serii artykułów zaprezentuję sposób w jaki zaimplementowane są poszczególne elementy systemu a na końcu dojdziemy do pełnej integracji całego komponentu.

Postaram się zaprezentować w kolejnych odcinkach:

  1. Prezentacja poszczególnych części systemu i wyjaśnienie ich najważniejszych elementów
  2. Tworzenie projektu w NetBeans IDE
  3. Podłączanie do swojego projektu pakiet frameworka Prime
    a) Tworzenie “externala” dla projektów wersjonowanych w SVN
    b) Tworzenie zrzutu (checkout) dla projektów, które nie są wersjonowane
  4. Uruchomienie “Hello world” opartego o framework
  5. Zaawansowane funkcje systemu

Info! Będę używał do prezentacji środowiska NetBeans tylko dlatego, że mi ono odpowiada. Do zrealizowania i zrozumienia tematu możesz używać dowolnego innego środowiska (np Eclipse) czy nawet zwykłego edytora z kolorowaniem składni (np Notepad++). Podobnie z SVN. Możesz użyć samodzielnego klienta (np Tortoise SVN) do ściągania repozytorium. Ja używam NetBeans dlatego, że jest tam “All in One”. Jeżeli, na którymś etapie pojawią się problemy to oczywiście chętnie pomogę.

Zapraszam do artykułów

  1. Request

3 część kursu Router

Zapraszam na trzecią część kursu o wzorcu Router. W tej części pokazałem jak zrobić algorytm odnajdywania właściwego kontrolera oraz jak zapisać nazwę kontrolera, metody i argumentów. Zapraszam serdecznie do lektury:

Router – część 3

Uwagi i komentarze mile widziane 🙂

Wzorce projektowe: Router – cz. 3

Informacje wstępne

Ten artykuł poświęcony jest omówieniu procesu przetwarzania pozyskanego wejścia na kontroler, metodę i argumenty oraz na metodzie odnajdywania właściwego pliku. Zapraszam!

Implementacja

Ostatni raz klasa miała następującą postać

class Router {

    public static $current_uri  = '';

    public static function get_uri()
    {
        if (!empty($_SERVER['PATH_INFO']))
        {
            self::$current_uri = $_SERVER['PATH_INFO'];
        }

        self::$current_uri = trim(self::$current_uri, '/ ');

        if (self::$current_uri !== '')
        {
            self::$current_uri = preg_replace('#//+#', '/', self::$current_uri);

            self::$current_uri = str_replace(' ', '_', self::$current_uri);

            self::$current_uri = preg_replace("/[^a-z0-9\\-\\_\\/\\,]/i", '', self::$current_uri);
        }
    }
}

Obecna postać klasy wygląda następująco. Poniżej kodu wytłumaczę każdą linijkę po kolei:

class Router {

    public static $current_uri  = '';
    public static $segments;
    public static $controller;
    public static $controller_path;
    public static $method    = 'index';
    public static $arguments = array();

    public static function get_uri()
    {
        if (!empty($_SERVER['PATH_INFO']))
        {
            self::$current_uri = $_SERVER['PATH_INFO'];
        }

        self::$current_uri = trim(self::$current_uri, '/ ');

        if (self::$current_uri !== '')
        {
            self::$current_uri = preg_replace('#//+#', '/', self::$current_uri);
            self::$current_uri = str_replace(' ', '_', self::$current_uri);
            self::$current_uri = preg_replace("/[^a-z0-9\\-\\_\\/\\,]/i",'',self::$current_uri);
        }
    }

    function init() {

        self::get_uri();

        self::$segments = self::$current_uri === ''
            ? array()
            : explode('/', self::$current_uri);

        $controller_path = 'controllers/';
        $method_segment  = NULL;

        foreach (self::$segments as $key => $segment)
        {
            $controller_path .= $segment;

            if ($c = $controller_path.'.php' AND is_file($c))
            {
                self::$controller = $segment;
                self::$controller_path = $c;

                $method_segment = $key + 1;

                break;
            }
            elseif (is_dir($controller_path) === FALSE)
            {
                break;
            }

            $controller_path .= '/';
        }

        if (self::$controller === NULL AND $c = $controller_path.'index.php' AND is_file($c))
        {
            self::$controller = 'index';
            self::$controller_path = $c;

            $method_segment = $key + 1;
        }

        if ($method_segment !== NULL AND isset(self::$segments[$method_segment]))
        {
            self::$method = self::$segments[$method_segment];

            if (isset(self::$segments[$method_segment + 1]))
            {
                self::$arguments = array_slice(self::$segments, $method_segment + 1);
            }
        }

        if (self::$controller === NULL)
        {
            die('error 404');
        }
    }
}

Metoda “get_uri()” została zaimplementowana już w poprzednim artykule i spełnia ona swoje zadanie w sposób całkowicie wystarczający. Przypominam, iż jej zadaniem jest pobranie i zabezpieczenie adresu podanego na wejściu w adresie naszej strony.
Nowa metoda, która pojawiła się tutaj to “init()“. Spójrzmy po kolei co ona robi:

    function init() {

        self::get_uri();

Na początku metody init() uruchamiamy naszą napisaną wcześniej metodę get_uri(), dzięki temu uzyskamy potrzebne dane (dane wejścia w zmiennej $current_uri) do dalszej pracy. Lecimy dalej:

        self::$segments = self::$current_uri === ''
            ? array()
            : explode('/', self::$current_uri);

Aby móc wyszukiwać właściwy kontroler i dobrać metodę oraz argumenty musimy stworzyć tablicę segmentów. Segmenty to po prostu elementy, które na wejściu oddzielone są slashem. Przykładowo, jeśli w przeglądarce zostanie podany taki adres:

http://moja.strona/katalog/kategoria/agd

To kolejne segmenty to: katalog, kategoria i agd, zatem utworzy to tablicę o takiej postaci

Array
(
    [0] => katalog
    [1] => kategoria
    [2] => agd
)

Algorytm zatem przy zapisywaniu wartości do $segments sprawdza czy $current_uri zawiera jakiś konkretny kontroler. Jeżeli nie to zwracamy pustą tablicę (bo nie ma żadnych segmentów). W przeciwnym wypadku zwracana jest tablica utworzona przez standardową funkcję PHP explode(). Dzieli ona tekst wg podanego znaku (w naszym przypadku “/”) na kolejne elementy tablicy.

        $controller_path = 'controllers/';
        $method_segment  = NULL;

Tutaj nie ma nic szczególnego, jednakże wypada wytłumaczyć do czego te zmienne będą potrzebne.

$controller_path

Jest to ścieżka do poszukiwanego kontrolera. Na początku zawiera ona nazwę katalogu w którym znajdują się kontrolery jednakże w dalszej części algorytmu stopniowo będzie ta zmienna uzupełniana o dalsze katalogi i pliki które ostatecznie doprowadzą (bądź nie) do poszukiwanego kontrolera

$method_segment

Ta zmienna będzie pamiętała numer segmentu wg, którego algorytm będzie uznawał, że jest to metoda do wywołania. Innymi słowy. Jeśli w ścieżce:

http://moja.strona/katalog/kategoria/agd

kontroler zostanie odnaleziony pod nazwą “katalog.php” i otrzyma on numer segmentu “0” to metoda otrzyma numer segmentu “1” (pod indeksem nr “1” jest segment o nazwie kategoria”) . Jeśli natomiast okaże się, że “katalog” i “kategoria” to tylko katalogi (w sensie ścieżki) a właściwy kontroler nazywa się “agd.php” to otrzyma on numer segmentu “2” a więc metoda otrzyma numer “3”. Pod indeksem “3” nie ma żadnego segmentu więc system uzna, że należy uruchomić metodę domyślną. Być może w tej chwili brzmi to trochę niezrozumiale ale to wszystko wytłumaczę w dalszej części algorytmu i wszystko stanie się jasne. Lećmy dalej:

        foreach (self::$segments as $key => $segment)
        {
            $controller_path .= $segment;

            if ($c = $controller_path.'.php' AND is_file($c))
            {
                self::$controller = $segment;
                self::$controller_path = $c;

                $method_segment = $key + 1;

                break;
            }
            elseif (is_dir($controller_path) === FALSE)
            {
                break;
            }

            $controller_path .= '/';
        }

Mamy trochę większy “kąsek” 🙂 Pamiętacie jak do zmennej “$segments” zapisywaliśmy pustą tablicę bądź wyniki działania funkcji “explode”? Teraz zostanie uruchomiona pętla iterująca po tych “segmentach” Jeśli tablica była pusta to pętla się nie wykona (zostanie wykonana procedura domyślna znajdująca za tą pętlą). Jeśli jednak zostały pobrane jakieś segmenty to pętla się wykona i oto co ona robi:

  1. Pierwszym krokiem pętli jest “doklejenie” do ścieżki w zmiennej “$controller_path” pierwszego segmentu. W naszym przykładzie pierwszy segment nazywa się “katalog”. Zatem w pierwszym kroku zmienna $controller_path przyjmie wartość “/controllers/katalog”
  2. W wyrażeniu warunkowym zapisujemy wartość $controller_path do zmiennej $c wraz z rozszerzeniem pliku “.php” i sprawdzamy za pomocą funkcji “is_file()” czy jest taki plik. Jeśli jest to znaczy, że znaleźliśmy kontroler. Do pola “$controller” zapisujemy nazwę segmentu (to będzie nazwa kontrolera, który należy uruchomić). Zapisujemy także tą ścieżkę do pola $controller_path oraz ustawiamy do zmiennej $method_segment numer obecnego indeksu zwiększony o 1 (bo metoda jest zawsze kolejnym segmentem po kontrolerze – tłumaczyłem to wcześniej) i przerywamy pętle.
  3. Jeżeli jednak to nie był plik to sprawdzamy w warunku alternatywnym czy przypadkiem nie jest to może jakiś katalog. Jeśli nie jest to przerywamy całą pętle. Oznacza to, że nie możemy iść dalej bo nie ma ani takiego katalogu ani pliku. Jeśli natomiast jest taki katalog to znaczy, że możemy przejść do kolejnego kroku w pętli. Algorytm najpierw doda “slash” a następnie w kolejnym kroku (jeśli jakiś pozostał) doda kolejny segment i punkty 1,2,3 zostaną powtórzone.

Cały ten cykl będzie trwał do momentu aż cała tablica segmentów zostanie przejrzana lub gdy nie znaleziono ani pliku ani katalogu. OK, tak czy siak pętla się kończy a mamy jeszcze trochę algorytmu dalej:

        if (self::$controller === NULL AND $c = $controller_path.'index.php' AND is_file($c))
        {
            self::$controller = 'index';
            self::$controller_path = $c;

            $method_segment = $key + 1;
        }

Ten warunek zostanie spełniony jeśli zawiedzie poprzednia pętla bądź gdy nie została w ogóle uruchomiona. W uproszczeniu: gdy nie uda się wybrać żadnego kontrolera. Jeśli taka sytuacja wystąpi sprawdzamy czy istnieje na dysku kontroler, który nazywa się “index”. Przyjąłem w algorytmie, że wszystko co ma być domyślne (kontroler, metoda, szablon) nazywało się index. Jeśli znajdziemy taki plik to wykonuje się właściwie ten sam algorytm co w pętli tylko, że dla określonego już pliku. Zauważ, że szukamy w obecnej wartości zmiennej $controler_path. Zatem jeśli w cała pętla przeszła do końca i nadal nie znaleziono kontrolera – oznacza to, że wszystkie elementy tej ścieżki to były tylko katalogi. Zatem w naszym przykładzie wartość zmiennej $controller_path na tym etapie będzie równa:

/controllers/katalog/kategoria/agd/

Zatem system będzie szukał pliku

/controllers/katalog/kategoria/agd/index.php

Jest to na tyle bezpieczne rozwiązanie, że nie musimy zmuszać się do wymyślania i podawania zawsze jakiejś nazwy kontrolera bądź metody. Jeśli mają one być bardzo specyficzne to możemy im nadać nazwię “index” i nie trzeba ich będzie podawać w adresie – co przełoży się na większą czytelność.

Ostatni element to wybór metody do uruchomienia oraz pobranie atrybutów:

        if ($method_segment !== NULL AND isset(self::$segments[$method_segment]))
        {
            self::$method = self::$segments[$method_segment];

            if (isset(self::$segments[$method_segment + 1]))
            {
                self::$arguments = array_slice(self::$segments, $method_segment + 1);
            }
        }

Warunek uruchomi się gdy numer segmentu metody został ustalony i gdy znajduje się on w tablicy segmentów. Jeśli tak to zapisujemy nazwę tego segmentu jako nazwa metody (pole $method). Dodatkowo sprawdzamy czy tablica kończy się na wielkości określonej przez numer segmentu metody. Jeśli tablica jest większa tzn, że mamy tam jakieś dodatkowe argumenty. Wycinamy z tablicy segmentów fragment od numeru segmentu metody. Pozostałe elementy to tablica argumentów.

Mamy jeszcze jeden mały fragment kodu, który zostanie później rozwinięty

        if (self::$controller === NULL)
        {
            die('error 404');
        }

Tu chyba nie ma nic tajemniczego. Jeśli nasz $controller nie został znaleziony to “uśmiercamy” nasz skrypt informacją z kodem 404.

Przykładowe wywołanie kodu:

Router::init();
echo '';
echo '';
echo '';
echo '';
echo '';
echo '';
echo '
URI:'.Router::$current_uri.'
SCIEZKA:'.Router::$controller_path.'
KONTROLER:'.Router::$controller.'
FUNKCJA:'.Router::$method.'
PARAMETRY:'.implode(', ',Router::$arguments).'
';

Podsumowanie

Myślę, że przedstawione przeze mnie rozwiązanie jest w miarę czytelne i zrozumiałe. Z moich testów wynika, że algorytm działa bardzo szybko i sprawnie jednocześnie zachowując bardzo elastyczny system organizowania sobie kontrolerów w zależności od upodobań programisty. System pozwala na właściwie nieograniczony stopień zagnieżdżeń zachowując dowolność w systemie tworzenia i organizowania plików. Być może jest jeszcze parę rzeczy, które można w tym algorytmie poprawić. Z chęcią czekam na Wasze propozycje i opinie. Na pewno wezmę je pod uwagę.

Serdecznie polecam pobawić się tym algorytmem i zobaczyć jakie to proste i użyteczne. Mam nadzieję, że to rozwiązanie Wam się podoba.

W następnym artykule spróbuję użyć Router do uruchomienia metod kontrolera

Poprzednia część: Router – cz. 2

Wzorce projektowe: Router – cz. 2

Informacje wstępne

W tej części chciałbym skupić się na prostej metodzie filtrowania niebezpiecznych znaków na wejściu.

Implementacja

Ostatni raz klasa miała następującą postać

class Router {

    public static $current_uri  = '';

    public static function get_uri()
    {
        if (!empty($_SERVER['PATH_INFO']))
        {
            self::$current_uri = $_SERVER['PATH_INFO'];
        }
    }
}

Dodajmy więc następne elementy. Zaznaczone na zielono.

class Router {

    public static $current_uri  = '';

    public static function get_uri()
    {
        if (!empty($_SERVER['PATH_INFO']))
        {
            self::$current_uri = $_SERVER['PATH_INFO'];
        }

        self::$current_uri = trim(self::$current_uri, '/ ');

        if (self::$current_uri !== '')
        {
            self::$current_uri = preg_replace('#//+#', '/', self::$current_uri);

            self::$current_uri = str_replace(' ', '_', self::$current_uri);

            self::$current_uri = preg_replace("/[^a-z0-9\\-\\_\\/\\,]/i", '', self::$current_uri);
        }
    }
}
  1. W linii 12 dodaliśmy funkcję “trim”, która obcina po lewej i po prawej stronie tekstu znaki, które podamy w parametrze. Domyślnie usuwa białe znaki (spacja, enter, tabulator). My chcemy pozbyć się niepotrzebnych slashy i białych znaków.
  2. Następnie sprawdzamy czy cokolwiek po tej operacji zostało w zmiennej $current_uri (linia 14). Jeśli jest różne od pustego ciągu do wykonujemy dodatkowe zabezpieczenie
  3. W linii 14 wyrażenie regularne usuwa wielokrotne slashe, np: jeśli na wejściu będzie “konto///klient//zobacz” to zamieni to na “konto/klient/zobacz”
  4. W linii 18 spacje zamieniane są na podkreślniki. To jeden z najprostszych sposobów oddzielania wyrazów w adresach stron. Ewentualnie można jeszcze zastosować myślnik
  5. Wreszcie w linii 20 wyrażenie regularne usuwa z wejścia wszystko co nie jest znakiem dopuszczanym (nie znajduje się na liście). W tym przypadku wyrażenie regularne pozostawi tylko znaki alfanumeryczne, przecinek i slash. W zależności od potrzeb można ta listę wydłużyć/zmienić

Ostateczna postać klasy:

class Router {

    public static $current_uri  = '';

    public static function get_uri()
    {
        if (!empty($_SERVER['PATH_INFO']))
        {
            self::$current_uri = $_SERVER['PATH_INFO'];
        }

        self::$current_uri = trim(self::$current_uri, '/ ');

        if (self::$current_uri !== '')
        {
            self::$current_uri = preg_replace('#//+#', '/', self::$current_uri);

            self::$current_uri = str_replace(' ', '_', self::$current_uri);

            self::$current_uri = preg_replace("/[^a-z0-9\\-\\_\\/\\,]/i", '', self::$current_uri);
        }
    }
}

Przykładowe wywołanie kodu:

Router::get_uri();
echo Router::$current_uri;

Kod wywołania nie zmienił się. Spróbuję jednak podać przykład “brzydkiego” wejścia.

http://moja.strona/stroąna[45],param1/zob&*acz

Efekt powinien być taki:

strona45,param1/zobacz

Jak widać wejście dobrze zabezpieczone – zezwala tylko na takie znaki jakie ma zdefiniowane

Podsumowanie

Starałem się krótko przedstawić proste zasady uniknięcia problemów i nieprzyjemności z dalszymi operacjami na wejściu. Można oczywiście mnożyć ilość przypadków, znaków i sytuacji które należy obsłużyć ale podane reguły sa na obecną chwilę całkowicie wystarczające.

Przedstawione zabezpieczenia nie mają na celu doprowadzić adresu z wejścia do czegoś konkretnego. Np jeśli nasza aplikacja posiada akcję “/klient/logowanie” a ktoś zamiast tego wpisze (np haker) /klient/logowanie” albo sql injection w stylu “/klient/’ or 1=1 ‘” itd to system niebezpieczne ciągu znaków odrzuci i zostawi tylko to co się nadaje i jest bezpieczne. W związku z tym dalej możemy szukać czy mimo wszystko coś sensownego z tego da się wyciągnąć. Tworząc linki w naszej aplikacji wiemy jakie one są: np na stronie głównej zrobimy “/klient/logowanie” ale to nie znaczy, że ktoś ręcznie w przeglądarce wpisze sobie coś innego (czego nie jesteśmy w stanie przewidzieć). Dzięki tym zabezpieczeniom system najwyżej takiej osobie, która próbuje manipulować linkami powie, że taka strona nie istnieje i nie wpłynie to na bezpieczeństwo samej aplikacji. Ustalmy, że to Router ma dostarczyć dane w sposób bezpieczny tak, aby reszta aplikacji nie musiała się już tym martwić

W następnym artykule przedstawię sposób rozbicia wejścia na użyteczne informacje

Nowy kurs – Router

Przygotowałem dosyć krótki i prosty wstęp z przykładową implementacją wzorca projektowego Router. Krok po kroku będę prezentował kolejne etapy rozwoju i implementacji poszczególnych elementów tego obiektu.

Zapraszam serdecznie do zapoznania się z pierwszym artykułem na ten temat – Router
Zobacz także ogólnie – Wzorce projektowe

Wzorce projektowe

Wzorce Projektowe, które powstały po to aby w miarę spójnie rozwiązywać problemy przy projektowaniu systemów.

O wzorcach projektowych powstało na pewno wiele mądrych i obszernych książek więc nie ma sensu abym ja się tutaj rozpisywał.

Przygotowałem kursy i implementacje popularnych i często używanych w programowaniu wzorców projektowych.

Go to Top