wtorek, 27 marca 2012

Jak sprawdzać istnienie używanych elementów?

Pytanie brzmi "jak", bo to, że jakoś weryfikować trzeba to mam nadzieję jest oczywiste :) Generalnie rozchodzi się o kilka sytuacji - np. gdy chcemy zinkludować jakiś plik, lub czytamy tablicę i nie jesteśmy pewni, czy jakiś określony klucz jest w niej ustawiony (choćby w przypadku $_POST/$_GET). Oczywiście PHP jako ten nowoczesny język udostępnia nam kilka możliwości sprawdzenia czy faktycznie mamy co czytać, toteż w tym wpisie spróbuję porównać wydajność tych metod.

Elementy tablicy
Na pierwszy ogień pójdzie sprawdzanie istnienia elementów tablicy. By jednak nie było tak całkiem prosto i by było o czym pisać to założymy, że tablica jest wielowymiarowa, czyli składa się z tablic, które składają się z innych tablic:
$arr = array("Nested" => array("Nested" => array("Nested" => "Here")));
Najpopularniejszym sposobem sprawdzania (i generalnie wpajanym wszystkim od maleńkości) czy dany klucz istnieje, jest wykorzystanie funkcji isset(). Tylko, że możemy ją wykorzystać na wiele sposobów...

if(isset())
Dla każdego przykładu użycia dodałem zarówno wywołanie dla elementu w tablicy się znajdującego (Nested), jak i tego, którego w niej nie ma (Indo).
if(isset($arr['Indo']['Indo']['Indo'])) $str = $arr['Indo']['Indo']['Indo'];
if(isset($arr['Nested']['Nested']['Nested'])) $str = $arr['Nested']['Nested']['Nested'];
if & isset (not exist):    0.016935110092163
if & isset (exist):     0.029613971710205
Jak widać PHP szybciej odkryje, że czegoś nie ma... niż, że coś jest :)

Jeden if, wiele isset()
Wydawać by się mogło, że sprawdzenie całej ścieżki w pojedynczym isset() jest o wiele lepszym rozwiązaniem niż wielokrotne wywołanie funkcji...
if(isset($arr['Indo']) && isset($arr['Indo']['Indo']) && isset($arr['Indo']['Indo']['Indo']))
 {} else {}
if & isset (multi - line LR) (not exist):  0.019056081771851
if & isset (multi - line RL) (not exist):  0.021497964859009
if & isset (multi - line LR) (exist):   0.056113004684448
... i niewątpliwie tak jest.. a na pewno jeśli wszystkie wywołania znajdują się w jednym "if". Dlaczego jednak zamieściłem tu ten przykład? Ano dlatego, że można z niego wynieść ważne informacje:
  • PHP sprawdza warunki od lewej do prawej (Left to Right) i kończy obsługę warunku gdy pierwszy z nich == FALSE
    Zatem taki zapis jest bardziej efektywny (po lewej warunki najważniejsze)
    .if(isset($arr['Indo']) && isset($arr['Indo']['Indo']) && isset($arr['Indo']['Indo']['Indo']))
     {} else {}
    niż zapis taki (najważniejsze warunki po stronie prawej)
    if(isset($arr['Indo']['Indo']['Indo']) && isset($arr['Indo']['Indo']) && isset($arr['Indo']))
     {} else {}
  • Sprawdzenie pojedynczego poziomu tablicy jest szybsze niż sprawdzanie kilu poziomów
    if(isset($arr['Indo']['Indo']['Indo'])) {} else {}
    if(isset($arr['Indo'])) {} else {}
    if & isset([][][])(not exist):    0.022554874420166
    if & isset([]) (not exist):    0.016841888427734
    if & isset([][][]) (not exist):   0.032048225402832
    if & isset([]) (not exist):    0.017351150512695

 if i isset() dla każdego poziomu
Taki zapis jest jakby typowym "kaskadowym podejściem" do tego zagadnienia.
if(isset($arr['Indo']))
  if(isset($arr['Indo']['Indo']))
   if(isset($arr['Indo']['Indo']['Indo']))
   {}
Generalnie zaletą tego podejścia jest to, że możemy wcześniej wykryć nieistniejące elementy i nie musimy przeprowadzać analizy całej tablicy. Jeśli jednak nie zależy nam na wykluczeniu istnienia, a jego udowodnieniu, to sens użycia takiego sposobu jest nikły. Do tego dochodzi konieczność obsługi na każdym poziomie kaskady sytuacji nieistnienia elementu.
if & isset (multi - cascade) (not exist):  0.018480777740479
if & isset (multi - cascade) (exist):   0.068065166473389
isset() ? operacja : operacja
Wykorzystanie tej konstrukcji tak na dobrą sprawę ma sens albo gdy chcemy szybciej napisać kod ("? :" to zdecydowanie mniej znaków niż "if() else"), albo gdy do danej zmiennej zamierzamy przypisać jakąś wartość (np. parsowanie danych wprowadzonych przez użytkowników)
isset($arr['Indo']['Indo']['Indo']) ? null : null;
isset & ? (not exist):     0.018883943557739
isset & ? (exist):     0.031083106994629
Bo jak widać pod względem wydajności metoda ta jest nieznacznie gorsza od starego poczciwego if'a.

@element 
Oczywiście poza isset() istnieje również metoda "empiryczna"... a dzięki operatorowi @ możemy ignorować generowane przez PHP błędy (Notice: Undefined index) związane z nieistnieniem elementu, do którego się odwołujemy.
$str = @$arr['Indo']['Indo']['Indo'];
@ (not exist):      0.16418194770813
@ (exist):      0.09358811378479
strict (exist):     0.030779838562012
Tylko, że jak widać czas związany z obsługą błędu jest o wiele większy niż czas potrzebny na przeszukanie tablicy przy pomocy isset().

Pliki
W przypadku plików również mamy kilka możliwości sprawdzenia czy istnieją.. Jednak tutaj opisze tylko trzy: include, file_exists() oraz is_file().

file_exists()
Funkcja ta generalnie służy do sprawdzenia czy dany plik istnieje.
if(file_exists("empty.php")) {} else {}
file_exists (exist):     0.030549049377441
file_exists (not exist):    0.048150777816772
file_exists include (exist):    0.10239315032959
file_exists ? include (exist):    0.10641098022461
file_exists include (not exist):   0.048465013504028

@include
Tym razem znów skorzystamy ze znanego nam już operatora blokującego informacje o błędach.
$var = @include "empty.php";
@include (exist):     0.20435905456543
@include (not exist):     0.073977947235107
Dlaczego przypisujemy to co zwraca include do zmiennej? Ano dlatego, że include zwracać może dowolne wartości, ale domyślnie (jeśli w includowanym pliku nie dodano returna) zwraca TRUE (jeśli plik załadowano) lub FALSE (jeśli pliku nie udało się załadować).

is_file()
Ostatnią zaprezentowaną tutaj metodą jest użycie funkcji is_file(), która to zwraca TRUE jeśli wskazana przez nas ścieżka jest zwykłym plikiem lub FALSE w każdym innym przypadku (czyli jeśli np. wskazuje na folder lub nie wskazuje na nic).
if(is_file("empty.php")) {} else {}
is_file (exist):     0.00079703330993652
is_file (not exist):     0.064265966415405
To co zaskakuje to szybkość z jaką is_file() działa - ok. 40 razy szybciej wykrywa istniejące pliki. Tak więc jeśli szukamy szybkiego sposobu na sprawdzanie czy pliki istnieją - is_file() jest idealnym wyborem. Niestety jego wykrywanie nie istniejących plików jest wolniejsze niż file_exists() - tak więc w zależności od głównego celu naszego bloku kodu powinniśmy wybrać odpowiednią funkcję.

Słowo na koniec
Tak więc jeśli chcemy sprawdzić czy w tablicy znajduje się dany element - to używamy isset(). Jeśli zaś chodzi o pliki, to w zależności od tego czy głównym scenariuszem użycia danego bloku kodu będzie wykorzystanie informacji o istnieniu lub nieistnieniu pliku powinniśmy użyć odpowiedniej funkcji - jeśli chcemy odczytać dany plik to używajmy is_file(), jeśli zaś chcemy stworzyć nowy plik o podanej nazwie - użyjmy file_exists().

A na koniec jak zwykle zapraszam do pobrania pliku z kodem użytym do testów: (link)

niedziela, 25 marca 2012

PHP a wersje językowe

Tworząc aplikację internetową w PHP prędzej lub później zapragniemy dodać do niej wsparcie dla wielu języków. Oczywiście tak to jak i wiele innych rzeczy można zrobić na wiele sposobów. W tym wpisie przedstawię zaledwie kilka z nich, choć myślę, że są to akurat te bardziej popularniejsze rozwiązania (Google it).
Większość metod tłumaczenia używa różnego rodzaju plików do przetrzymywania informacji o "wyglądzie" tekstu w danej wersji językowej, dlatego też znaczną część wpisy poświęcę omawianiu tych różnych "plikowych tworów".

Na początku był define
A jeśli nawet nie na początku, to jest to droga, która w ten czy inny sposób wydaje się "prosta i oczywista".
define ('Hello_World', "Witaj Świecie");

echo constant('Hello_World');
echo Hello_World;
Oczywiście przy wykorzystaniu takiej metody wszelkie definicje tekstów powinny być umieszczane w osobnym pliku dla każdej wersji językowej i w zależności od potrzeb includowawne.
Co ciekawe odwoływanie się do zdefiniowanej stałej przy użyciu constant() trwa dłużej niż bezpośrednie wywołanie:
pure:    0.5458300113678
with constant:   0.85419201850891

Metoda ta jednak nie sprawdza się w przypadku większych portali, gdzie liczba tłumaczeń przechodzi w setki czy nawet tysiące. Do tego dochodzi całkowity brak wsparcia po stronie IDE (brak auto uzupełniania i innych cudów techniki).

A gdyby tak tablicę
Co prawda tablica sama w sobie również wsparcia od IDE nie oferuje, ale daje większe możliwości segregacji wpisów (np. poprzez tworzenie wielu wymiarów).
define:   0.4133129119873
define with constant:  0.88955783843994
array:    0.35322785377502
multi array:   0.42432999610901
Poza tym jak widać po osiąganych czasach wykorzystanie tablicy okazuje się lepszą metodą niż definiowanie stałych (co zaskakujące nie jest) i dopiero wykorzystanie wielowymiarowych tablic zbliża te czasy do siebie.

.ini czyli coś prawie zapomnianego 
Dziś już prawie nikt nie używa plików .ini - bo i po co skoro są choćby XML.. Jednak ogromną zaletą plików .ini jest ich mały narzut związany z obsługą danych - nie potrzeba wieloznakowych tagów zarówno otwieranych, jak i zamykanych... nie - wystarczy klucz i znak '='. Czego rezultatem jest zaskakująca wydajność (czas liczony łącznie z parsowaniem pliku!):
define:   0.43395209312439
define with constant:  0.94658613204956
array:    0.36954712867737
multi array:   0.44773697853088
ini array:   0.42291903495789
Do tego PHP zapewnia wbudowany parser plików .ini - parse_ini_file() - tak więc stworzenie translacji na tej zasadzie jest dość proste. Poza tym pliki .ini są dość popularnym sposobem trzymania danych konfiguracyjnych dla np. bazy danych :)
Minusem wbudowanego parsera jest jednak fakt, że wygenerowana tablica może mieć maksymalnie 2 wymiary.

Profesjonalny gettext
W świecie PHP oczywiście istnieją również rozwiązania dedykowane dla lokalizacji - w tym przypadku jest to zbiór funkcji, do których generalnie odnosi się poprzez słowo "gettext". Funkcje te tak naprawdę dostępne są w wielu różnych językach programowania albo dzięki natywnym bibliotekom, albo dzięki pracy zwykłych "szarych" użytkowników. Zaletą gettexta jest jego profesjonalne podejście do zagadnienia - podczas pracy z nim dostajemy pliki z jasno opisanymi wystąpieniami każdego elementu, który mamy przetłumaczyć. No właśnie - tutaj tak naprawdę specjalny "szperacz" wyszukuje frazy, które mamy przetłumaczyć, więc możemy spokojnie pisać i testować kod nie obawiając się, że nagle dostaniemy błąd o braku danego wpisuj w tablicy czy nieistnieniu jakiejś zmiennej.

Jeśli przypadkiem poszukujecie kompilatra dla gettext to polecam zajrzeć na http://savannah.gnu.org/projects/gettext.

Natywny gettext
Wbudowany gettext wymaga zainstalowania (i uruchomienia) na serwerze rozszerzenia php_gettext.
define:    0.030115127563477
define with constant:   0.061676025390625
array:     0.021489143371582
multi array:    0.027302980422974
ini array:    0.02887487411499
Czas dla gettext (native):  0.87748694419861

Porównując szybkość jego działania jednak szybko dojdziemy do wniosku, że wcale nie jest to taki wyśniony wydajnościowo sposób implementowania lokalizacji. Cóż za to jest on na pewno przyjemniejszy podczas pisania kodu i samego procesu translacji.

gettext dla ubogich 
Może nie koniecznie dla ubogich w sensie dosłownym, a raczej dla ubogich pod względem możliwości serwera. Natywny getext ma to do siebie, że ładuje pliki z tłumaczeniami raz do pamięci i trzyma je generalnie tam aż do restartu serwera - owszem istnieją metody na obejście tego.. ale jeśli ktoś zajmuje się głównie tworzeniem strony i generalnie deweloperką to raczej będzie skłonny poświęcić trochę wydajności podczas testów na rzecz prostszego i szybszego w napisaniu kodu.Do tego dochodzi problem z aktualizacją słownika w przypadku serwerów współdzielonych - mało który admin zgodzi się na restart tylko dlatego, że nasz słowniczek zmienił tłumaczenia dla kilku elementów...
define:    0.031126976013184
define with constant:   0.062520980834961
array:     0.021286010742188
multi array:    0.02723503112793
ini array:    0.027822017669678
Czas dla gettext (native):  0.90787696838379
Czas dla gettext (custom):  1.8490700721741

Tak więc jeśli chcecie wypróbować tą nieoficjalną implementację to zapraszam na https://launchpad.net/php-gettext.

Sposoby z kosmosu
No może nie całkiem z kosmosu, bo czasami inaczej się po prostu nie da (lub wykorzystanie plikowych sposobów nie ma sensu). Ale jeśli ktoś opiera swoją wielojęzyczność elementów statycznych na bazie danych to znak, że coś się dzieje... By była jasność nie chodzi mi o elementy, które użytkownicy mogą dynamicznie dodawać do portalu, ale o tłumaczenia np. strony do logowania ^.^ Generalnie wszelkie odwołania do zewnętrznych zasobów (a baza danych takowym jest - no chyba, że mówimy o serwerze PHP znajdującym się na tej samej maszynie co baza, która to jeszcze trzyma wszystkie dane w pamięci podręcznej) to utrata cennego czasu i zasobów. Oczywiście można użyć tłumaczy online takich jak Google translate, altavista (Babel fish), czy bing translator... ale to trwa i wykorzystanie takich pereł polecałbym przenieść do JavaScriptu odpalanego przez klienta i to tylko i wyłącznie dla treści, których tłumaczenia po prostu nie jesteśmy w stanie dostarczyć.
Ciekawym sposobem podejścia do tematu jest wykorzystanie klas i zawartych w nich zmiennych jako kontenerów na tłumaczenia. Takie podejście ma ten plus, że zapewni nam auto uzupełnianie IDE i łatwość ładowania innych wersji językowych. Wadą jest wydajność, gdyż narzut związany z tworzeniem klasy powoduje, że czas konieczny na obsłużenie tego typu lokalizacji jest ok. 2 razy większy niż w przypadku deklarowania stałych za pomocą define().

Puenty brak
Cóż czasy mówią same za siebie, tak więc jeśli zależy wam na wydajności to z pewnością pomyślicie nad jakimś własnym rozwiązaniu opartym o pliki (dla leniuszków polecam zapoznać się z plikami .ini gdyż są bardzo wdzięczne pod względem tak możliwości łatwej translacji jak i użytkowania w aplikacji). Jeśli jednak już zamierzacie zagłębiać się w tajniki gettexta - technologia ta jest bardzo denerwująca (pomijając już fakt konieczności kompilacji, mergowania tłumaczeń...) choćby z tego powodu, że zwykle ciężko skłonić natywną wersję do poprawnego działania (wersja ~danilo zdecydowanie jest łatwiejsza w obsłudze) o wydajności nie mówiąc...

Chcących zaglądnąć w kod odsyłam do swojego chomika: (link)

poniedziałek, 12 marca 2012

Gdy program nie widzi sieci Hamachi

Dość częstym problemem w przypadku łączenia się z programami uruchomionymi na innych maszynach znajdujących się w sieciach Hamachi jest to, że wszelkie "auto wyszukiwarki" serwerów po prostu takowych komputerów nie uwzględniają. Innymi słowy - o ile często łączenie się po podaniu IP działa bez problemu, to już różnego rodzaju narzędzia pokazujące dostępne serwery po prostu nigdy do sieci Hamachi nie dochodzą. W rezultacie pojawia się taki problem, że np. gdy chcemy podłączyć się do serwera jakiejś gry postawionego u naszego kolegi - to niestety nie mamy takiej możliwości (chyba, że uda się nam podłączyć bezpośrednio po IP).

Dlaczego tak się dzieje? Generalnie Hamachi po instalacji dodaje swój adapter sieciowy na koniec listy połączeń zarejestrowanych w systemie. Samo to w sobie jest akurat zachowaniem pożądanym. Problem pojawia się gdy aplikacja wyszukująca serwer używa tylko i wyłącznie pierwszego połączenia z listy, czyli odnosi się do nadanego nam adresu IP związanego z tym właśnie połączeniem.

Czy da się to jakoś zmienić? Na nasze szczęście tak :)

W celu zmiany kolejności połączeń musimy udać się do naszego Panelu sterowania i w zależności od używanego widoku wybrać "Wyświetl stan sieci i zadania" lub "Centrum sieci i udostępniania".

Następnie wybieramy "Zmień ustawienia karty sieciowej" (na listwie po lewej stronie).

A teraz pora na odrobinę magii.. W otwartym oknie powinna pojawić się nam lista dostępnych połączeń sieciowych, jednak to co nas interesuje to menu, które pokazuje się dopiero po naciśnięciu klawisza [ALT]. W menu tym wybieramy "Zaawansowane" -> "Ustawienia zaawansowane...".

W nowym oknie powinniśmy ujrzeć listę połączeń, a na niej połączenie Hamachi (1). Klikamy więc na połączenie Hamachi, a następnie klikamy na strzałkę skierowaną ku górze (2) tak długo, aż to połączenie nie znajdzie się na szczycie listy. Klikamy OK i gotowe :)
Możliwe, że do uzyskania odpowiedniego efektu konieczne będzie ponowne uruchomienie komputera, ale generalnie ten zabieg powinien rozwiązać problem z niewykrywaniem serwerów :D