Programowanie
PHP 5.3 – dirname(__FILE__) vs __DIR__
0Jedną 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ć.
Atrybut Title w Internet Explorer – jak usunąć?
1Ostatnio napotkałem na dość dziwny problem w przeglądarce Internet Explorer (a jakże) z atrybutem title. Miałem za zadanie stworzyć własny system wyświetlający podpowiedzi do danego wiersza w zestawie pól formularza. Naturalnym rozwiązaniem dla mnie było użycie atrybutu “title” Algorytm sprowadził się do tego, że na zdarzeniu :hover pobierałem atrybut title przekazywałem do mojego systemu a z tego usuwałem. Wszystko to wykonane było za pomocą jQuery
$('.row').hover(function() {
var title = $(this).attr('title');
$(this).removeAttr('title');
});
Wydawałoby się, że to dosyć proste i banalne, jednakże Internet Explorer zupełnie nie rozumie o co chodzi. Usunięcie atrybutu title zwyczajnie nie przynosi rezultatów. Chmurka od przeglądarki nadal się pojawia. Rozwiązanie jakie udało mi się wynaleźć to ustawienie title na wartość pustą
$('.row').hover(function() {
var title = $(this).attr('title');
$(this).attr('title', '');
});
I voila! Teraz Internet Explorer jak i inne przeglądarki nie mają problemu. Chmurka przestaje się pojawiać. Co ciekawe… jeśli jednak dodatkowo potem jeszcze usunę ten atrybut w ten sposób:
$('.row').hover(function() {
var title = $(this).attr('title');
$(this).attr('title', '');
$(this).removeAttr('title');
});
to chmurka znowu się pojawia z oryginalnym tekstem. Wygląda na to, że ustawienie samodzielne title jakoby podmienia zawartość kontenera zatem usunięcie tego co sami zrobimy “odsłania” wartość pierwotną. Dziwne?
Pliki konfiguracyjne i aplikacje w PHP
0Ostatnio 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ę:
- Serializowana tablica
- JSON
- plik INI
- 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)
- Plik INI (0.9728s)
- Require (1.0595s)
- Unserialize (1.0884s)
- JSON (1.1447s)
Linux (PHP 5.3.6)
- JSON (0.1267s)
- Plik INI (0.1744s)
- Unserialize (0.1892s)
- 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
2Jeż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ż:
- Działa ona szybciej gdyż nie wykonuje ponownie załączonego kodu
- 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:
- 1000 użytkowników -> request -> skrypt (załączenie pliku “A.php”)
- 1 użytkownik -> request -> skrypt (1000x załączenie pliku A)
- 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
0Witam 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.
- Informacje o danych POST
- Informacje o danych GET
- Informacja o adresie żądania
- Informacja o ciasteczkach
- 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:
- <przegladanie> <= kontroler
- <kategoria> <= akcja
- <nazwa> <= nazwa parametru pierwszego
- <AGD> <= wartość parametru pierwszego
- <strona> <= 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
<?php
/**
* Controls the request info including GET, POST, COOKIE and PARAMS
* @package Prime
* @version 1.0
* @author Quadric
* @copyright (c) 2009 Quadric
* @license http://quadric.goblix.pl/license.html
*/
class Request {
/**
* Contains Request URI
*/
private $_requestUri = null;
private $_post = null;
private $_get = null;
private $_cookie = null;
private $_paramAssoc = null;
private $_paramEnum = null;
/**
* Get Request URI and save into variable
*/
function __construct() {
// Grab data
$this->_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
0Tak 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:
- Prezentacja poszczególnych części systemu i wyjaśnienie ich najważniejszych elementów
- Tworzenie projektu w NetBeans IDE
- 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 - Uruchomienie “Hello world” opartego o framework
- 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
Optymalny kod HMTL / Hack IE
1W dobie nowoczesnych przeglądarek internetowych oraz wchodzącego coraz większymi krokami HTML5 mało sensowne wydaje się zajmowanie problemami które powodują przeglądarki starsze. Jednak warto w związku z tą fazą “przejścia” do Internetu “nowej jakości” zastosować pewne sztuczki które w przyszłości pozwolą gładko przejść w tą erę.
Chciałbym także zaprezentować współczesne rozwiązywanie problemów niekompatybilności starszych przeglądarek oraz problemów jakie sprawiają – tak, żeby strona była nowoczesna, ale jednocześnie działała jeszcze na starszych przeglądarkach.
Nie będę w tej chwili poruszał kwestii HTML5 (ale o tym niedługo
) za to chciałbym oprzeć się na obecnych popularnych technologiach i przyjrzeć się technologii odseparowania kodu który uznajemy za poprawny od kodu który musi się znaleźć aby strona działała i wyświetlała się na przeglądarkach które normalnie sobie z tym nie radzą. Postaram się także poddać subiektywnej ocenie każdą z metod. Zaczynamy:
Hacki nadmiarowe
Hacki nadmiarowe – czyli technika, która jest całkowitym zaprzeczeniem poruszanego tematu. Zakłada ona w uproszczeniu, że np w kodzie CSS tworzymy standardowy kod w postaci:
div {
color:red;
}
Często okazuje się, że jakaś przeglądarka inaczej dziedziczy klasy CSS lub występuje jakiś inny problem (np na jednej przeglądarce linki są czerwone a na innej zielone i nie wiadomo dlaczego. Można wtedy zastosować Hacki dla przeglądarek – czyli tak zapisać kod aby tylko dana przeglądarka potrafiła go zinterpretować. Np dla przeglądarki Internet Explorer wyglądałoby to tak:
div {
color:red;
}
div\ {
color:green;
}
}
Tego drugiego “div’a” zinterpretuje tylko IE. Więc we wszystkich przeglądarkach div będzie miał czerwony tekst, a na IE zielony. Jest jeszcze mnóstwo technik które pozwalają uzyskać te różnice na przeglądarkach, ale ja tej metody nie polecam więc nie będę ich wypisywał. Dlaczego nie polecam tej metody?
wady:
- Nadmiarowość kodu współdzielona przez wszystkie przeglądarki
- W 99% przypadków nie jest to potrzebne ponieważ różnica wynika z błędu programisty lub niewłaściwego zaprojektowania zależności
- Hacki to tak naprawdę celowe zapisanie czegoś “źle” żeby wykorzystać błąd przeglądarki – w efekcie 2 minusy dalą plus
zalety:
- Można zrealizować bardzo prosto pewne różnice.
Hacki warunkowe
Hacki warunkowe to technika zdecydowanie bardziej odpowiednia. Pozwala ona oddzielić pewne fragmenty kodu tylko dla specyficznej wersji danej przeglądarki – np Internet Explorer w wersji 6. Dzięki temu możemy napisać stronę pod najnowsze przeglądarki i a pewne różnice skorygować za pomocą odrębnych zasad. Kod taki nie jest wtedy mieszany z prawidłowym kodem i nie jest interpretowany przez przeglądarki które działają poprawnie. Warunki zapisuje się w prosty sposób:
<!--[if IE]> kod tylko dla IE <![endif]-->
Można także precyzyjnie dobrać przeglądarkę:
<!--[if IE 5.5]> Internet Explorer 5.5 <![endif]-->
Można także zaprzeczać wersji przegladarki
<!--[if !IE 5.5]> Internet Explorer w innej wersji niż 5.5 <![endif]-->
Lub jedne z najczęściej spotykanych (dla wszystkich wersji IE mniejszych niż 7)
<!--[if lt IE 7]> Internet Explorer w wersji starszej niż 7 <![endif]-->
Efektywny kod w sekcji HEAD mógłby wyglądać tak:
<link href="/css/main.css" rel="stylesheet" type="text/css" /> <!--[if lt IE 7]> <link href="/css/ie6.css" rel="stylesheet" type="text/css" /> <![endif]-->
Zamiast tworzyć udziwnienia i mieszać kod możemy wszystkie różnice i reguły przenieść do kodu specyficznego dla danej wersji przeglądarki. Co nad to daje? W momencie gdy zostanie podjęta decyzja o zaprzestaniu wsparcia dla danej przeglądarki wystarczy usunąć zapis warunkowy ze swojego kodu i już. Nie trzeba potem odkręcać nic więcej.
wady:
- Trzeba stworzyć osobny plik i jest to nadal pewna nadmiarowość, ale już tylko w obrębie przeglądarki która jest “pokrzywdzona” – najczęściej jednak na niej właśnie najmniej nam zależy i się na to godzimy<
zalety:
- Kod jest odseparowany. Nie tworzą się nieprawidłowości.
- W dowolnym momencie można dany warunek usunąć, zmienić lub dodać nowy bez wpływania na to co już działa poprawnie.
- W momencie decyzji i zaprzestaniu wspierania danej przeglądarki dany warunek wystarczy usunąć z kodu.
Emulacja funkcjonalności
Różnice w zasadzie działania to jedno, ale co gdy dana przeglądarka zwyczajnie nie ma danej funkcji, nie potrafi czegoś zrobić lub robi to źle? Najczęstsze problemy z przeglądarką Internet Explorer poniżej 7 to:
- Brak wsparcia przezroczystości plików PNG
- Zastosowanie elementów hover tylko na znacznikach typu a
Paradoksalnie nawet bardzo stare przeglądarki Internet Explorer mają różne ciekawe funkcje których nie mają współcześnie najnowsze przeglądarki. Te funkcje to m.in:
- Filters
- Behaviors
Filtry - IE posiada zestaw filtrów różnego typu które wprowadzają zmiany w elementach strony. Jednym z najczęściej używanych filtrów jest “gray”. Dany selektor konwertowany jest do postaci czarnobiałeł. Można w ten sposób na elemencie body stworzyć całą stronę na czarno-biały. Przydatne gdy chcemy włączyć “żałobę”. Co ciekawe żadna współczesna przeglądarka nie posiada takiej funkcji.
Behaviors - W uproszczeniu są to pewne reguły napisane za pomocą skryptu. Dzięki temu można je wykorzystać do zasymulowania pewnych zachowań których standardowo przeglądarka nie potrafi.
Rozwiązanie problemu HOVER
W tym celu można utworzyć plik HTC. Polecam plik ze strony http://www.xs4all.nl/~peterned/htc/csshover3.htc – zestaw bardzo fajnych reguł do hoverowania elementów.
Załączyć do IE można go tak
body {
behavior:url("../htc/csshover3.htc");
}
Oczywiście korzystamy w tym celu a jakże z Hacku warunkowego, żeby nie śmiecić innym przeglądarkom. (Uwaga na ścieżkę do pliku htc bo można się naciąć – oprócz tego jeśli uruchamiasz stronę pod Apache to plik ten musi być przez niego rozumiany)
Rozwiązanie problemu PNG
To nie prawda, że IE6 nie potrafi obsłużyć przezroczystości dla plików PNG. Potrafi – ale robi to źle
Można zatem naprostować ją i użyć. Można w sieci znaleźć wiele technik do poradzenia sobie z tym problemem – np również pliki HTC z odpowiednimi funkcjami symulującymi “włączenie przezroczystości”, różne nakładki i skrypty konwertujące. Ogólnie sprowadza się to do wywołania pewnej wariancji mniej lub bardziej skomplikowanej następującej funkcji:
#logo {
background:transparent url(/gfx/logo.png) no-repeat scroll left top;
height:10px;
width:10px;
}
* html #logo {
/* Alpha transparencies hack for IE */
background-image:none;
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/gfx/logo.png', sizingMethod='crop');
}
Oczywiście zastosowano tutaj brzydki Hack dla IE (na początku napisane jest * html ” co rozumie tylko IE. Zostawiłem tak celowo żeby porównać z tym co mówiłem na początku. Oczywiście idealna wersja powinna wyglądać tak, że zostanie to przeniesione do osobnego pliku css dla danej wersji IE.
Podsumowanie
Na ten temat można dużo pisać. W sieci jest niesamowita ilość informacji i rozwiązań problemów. Nie jest to miejsce by opisać je wszystkie i nie taki jest cel tego posta. Chciałbym tylko przybliżyć, że istnieją takie metody i rozwiązania i że warto z nich korzystać aby tworzyć fajny, czytelny i wydajny kod – jeśli potrzeba by strona musiała mimo wszystko działać jeszcze na starszych przeglądarkach to wyżej wspomniane techniki pozwolą na osiągnięcie tego stanu rzeczy bez modyfikowania już gotowego wydajnego kodu. Należy jednak pamiętać aby nie korzystać z tego nagminnie i najpierw samemu upewnić się, że wszystko co zostało zrobione jest poprawne (np częsty problem w różnicy wyświetlania to błąd polegający na tym, że stworzony został kontener rodzic o mniejszej szerokości niż jego element potomny – IE wtedy świetnie nadaje się to znajdowania tego typu problemów gdyż rozszerza kontenery o najszerszą długość jaką znajdzie)
Polecam także korzystanie z walidatora kodu W3C w standardzie SGML i Tidy oraz walidatora CSS. Dopiero potem czas na uzupełnianie różnic.
Pozdrawiam.
Tworzenie VHost’a w Apache
0
Zapraszam do lektury kursy tworzenia VHostów. Okazuje się, że wiele osób ma jednak problem z ruszeniem tego tematu dlatego postanowiłem w jak najprostszy sposób przedstawić sposób na utworzenie VHostów.
Komentarze jak zwykle mile widziane.
Pozdrawiam.
Tworzenie VHost’a w Apache
5Dlaczego?
Często spotykam się z zainteresowaniem rozwiązaniami vhostów (zwłaszcza dla programistów/opiekunów wielu serwisów) jednak ciężko jest przebrnąć przez kilka kwestii. Tworzenie vhostów jest bardzo proste jednak po drodze jest parę kruczków, które mogą niejednego doprowadzić do białej gorączki. Wystarczy poznać zestaw kilku zasad i korzystanie z vhostów stanie się tylko i wyłącznie przyjemnością. Zapraszam do lektury!
o Virtual Host’ach
Bardzo często zdarza się, że mamy do dyspozycji tylko jedną maszynę a chcemy/musimy posiadać kilka serwisów (hostów). Jeśli na maszynie zainstalowany jest Apache to z pomocą przychodzi nam rozwiązanie zwane “Virtual Hosts”. Dzięki temu rozwiązaniu możemy zdefiniować w serwerze klika niezależnych hostów, działających na własnych domenach i z własną konfiguracją.
Jeśli posiadasz już zainstalowany serwer i chciałbyś stworzyć wirtualne hosty musisz wykonać 3 czynności:
- Stworzyć nazwę domenową
- Utworzyć wpis vhosta w pliku konfiguracyjnym
- Zmodyfikować uprawnienia
1. Nazwa domenowa
Dzięki nazwom domenowym nie musimy chodzić po stronach za pomocą adresów IP tylko za pomocą przyjaznych dla nas nazw. Nazwy te tłumaczone są przez serwery DNS na IP aby przeglądarka wiedziała gdzie ma się faktycznie połączyć. Na każdym komputerze (czy to z Windowsem czy Linuxem) można stworzyć własną uproszczoną wersję takiego serwera – a konkretnie jest to plik w którym zdefiniowane są nazwy domenowe i przypisane im konkretne IP.
W systemie Windows jest to plik: C:\Windows\System32\drivers\etc\hosts
W systemie Linux jest to plik /etc/hosts
Posiada on mniej więcej taką strukturę:
IP domena
127.0.0.1 localhost
IP 127.0.0.1 jest adresem własnego komputera. A “localhost” jest nazą domenową dla tego IP. Jeśli wpiszesz w przeglądarce “localhost” to podłączysz się sam do siebie (jeśli masz działający serwer)
Gdy wpisujesz w przeglądarkę “wp.pl” to faktycznie łączysz się z pewnym adresem IP, który “podpowiedział” serwer DNS. Jednak można wymusić aby pewne nazwy domenowe łączyły się na inne IP. Sprawdźmy to:
Otwórz wspomniany plik “hosts” i dopisz w nim na koniec taką linijkę:
74.125.43.147 onet.pl
Zapisz plik i spróbuj odwiedzić stronę onet.pl – niespodzianka? Ustawiłeś właśnie aby domena onet.pl wskazywała na IP google. Dobra jeśli już znamy zasadę działania to usuń ten wpis żebyś nie zapomniał
Zamiast tego utwórz wpis
127.0.0.1 pierwszyvhost
127.0.0.1 drugivhost
W ten sposób stworzysz drugą nazwę domenową dla własnego komputera. Tworzymy to po to, gdyż apache rozpoznaje który vhost ma uruchomić właśnie po nazwie domenowej. IP mamy tylko jedno, ale różne domeny wskazują na to samo IP i po tych nazwach właśnie apache wie co uruchomić.
2. Tworzenie vhosta
W zasadzie Apache posiada główny plik konfiguracyjny httpd.conf i to w nim powinno dokonywać się wszelkich zmian. Jednakże w zależności od dystrybucji ten plik może być podzielony na inne mniejsze pliki które zawierają konkretne rzeczy (np właśnie vhosty).
Jeśli posiadasz pakiet XAMPP to otwórz plik i przejdź na koniec:
c:\xampp\apache\conf\extra\httpd-vhosts.conf
Jeśli posiadasz zwykłą dystrybucję bądź nie ma osobnego pliku na vhosty to otwórz plik httpd.conf i przejdź na koniec
VHost składa się z pewnego bloku informacji obowiązujących w określonej strukturze drzewa. Struktura takiego podstawowego bloku przedstawia się następująco:
<VirtualHost *:80>
ServerAdmin [adres email administratora]
DocumentRoot [ścieżka do katalogu]
ServerName [nazwa domenowa serwera]
ServerAlias [alias nazwy serwera - np druga nazwa]
ErrorLog [plik z logami błędów]
</VirtualHost>
Dopisz zatem następującą treść (nie potrzebujemy wszystkich wpisów)
<VirtualHost *:80>
ServerAdmin adres@email.pl
DocumentRoot C:\www\vhost1\html
ServerName pierwszyvhost
</VirtualHost>
<VirtualHost *:80>
ServerAdmin adres@email.pl
DocumentRoot C:\www\vhost2\html
ServerName drugivhost
</VirtualHost>
Zapisz plik. Utwórz oczywiście wymienione katalogi i wrzuć do każdego katalogu “html” jakiś plik index.html (np jeden o treści “Pierwszy vhost” a drugi o treści “Drugi vhost”) i vhosty gotowe… no cóż… prawie….
Zrestartuj serwer apache (w XAMPP musisz włączyć i wyłączyć serwer), lub w konsoli na linuxie polecenie:
/etc/init.d/httpd restart
czy tez inne odpowiednie dla Twojej dystrybucji.
Wpisz teraz w przeglądarce
http://pierwszyvhost
lub
http://drugivhost
W obu przypadkach prawdopodobnie otrzymasz komunikat “Forbidden” i teraz przechodzimy do punktu 3
3. Uprawnienia
Problem z uprawnieniami wynika z tego, że dla głównego drzewa serwera zostały ograniczone możliwości. Metodą najprostszą jest odnalezienie w pliku httpd.conf następującej sekcji:
<Directory />
Domyślnie znajdują się w niej opcje ustawione w taki sposób:
Options FollowSymLinks AllowOverride None Order deny,allow Deny from all
Musisz zmienić je następujące:
Options FollowSymLinks AllowOverride All Order allow,deny Allow from all
Zrestartuj ponownie serwer apache. Wpisz w przeglądarce:
http://pierwszyvhost
Hurra działa! (a przynajmniej powinno). Jednak gdy wpiszesz
http://drugivhost
To prawdopodobnie nie zadziała. Jest to związane z nasłuchiwaniem portów. Musisz odnaleźć opcję:
NameVirtualHost *:80
Jeśli jest zakomentowana (znajdują się przed nią znaki “#” czyli jest wyłączona) to usuń te znaki aby doprowadzić ją do postaci j/w. Ponownie musisz oczywiście zrestartować serwer. Jeśli nie masz tej opcji ani w pliku httpd-hosts.conf ani w httpd.conf to dopisz taką linijkę samodzielnie (tylko jeśli drugi host nie działa).
Po restarcie serwera drugi vhost również powinien działać.
Podsumowanie
Mam nadzieję, że przybliżyłem nieco tematykę co pozwoliło osobom zainteresowanym zrealizować swoje potrzeby. Wszelkie uwagi i wątpliwości proszę zgłaszać za pomocą poniższego narzędzia.
Pozdrawiam.
3 część kursu Router
0Zapraszam 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:
Uwagi i komentarze mile widziane