Pokazywanie postów oznaczonych etykietą PHP. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą PHP. Pokaż wszystkie posty

wtorek, 16 października 2012

Syndykacja czyli jak wyświetlić treść na swojej stronie?

Kanały treści, feedy lub po prostu RSS/Atom*. Istnieje wiele określeń na nośnik informacji używany w syndykacji treści stron internetowych, które to nieraz są stosowane wymiennie. W zasadzie dziś mało kto zwraca uwagę na to co kryje się pod magicznym guzikiem symbolizującym właśnie kanał treści danej witryny. Ale czym jest owa syndykacja, której próżno szukać w słownikach języka polskiego (link | link)? W uproszczeniu jest to publikowanie treści podmiotu przez inny podmiot, który tej treści nie utworzył. Definicja może i krótka, ale pewnie nie do końca zrozumiała, dlatego posłużę się przykładem nie związanym z internetem:
Stacja telewizyjna A nakręciła materiał z koncertu super gwiazdy i udostępnia go na swoim kanale. Jednocześnie stacja ta pozwala innym stacjom na używanie tego materiału na swoich kanałach (zwykle za opłatą licencyjną). I właśnie takie udostępnianie innym nazywamy syndykacją.
W świecie internetu gro procesu syndykacji zostało uproszczone do generowania przez właściciela treści pliku z jej zawartością, który to umieszcza na swoim serwerze. Rzadko też spotykamy się z odpłatnymi kanałami, a jeśli już to są one konsekwencją płatnego dostępu do całego serwisu. Dość po słowie, że świat idzie na przód i syndykacja w obecnym obliczu jest jednym z elementów, które powodują odejście starej wizji mediów w niebyt.

JavaScript i AJAX

 Ale wracając do tematu wpisu, czyli wyświetlania kanału treści na swojej witrynie. Niestety najprostsze i najkorzystniejsze dla właściciela hostingu (transfer, czas procesora) podejście wykorzystujące JavaScript w wielu przypadkach nie sprawdzi się z racji polityki pochodzenia z tej samej domeny. Oczywiście jeśli chcemy agregować treści znajdujące się w serwisach tej samej domeny ten problem nas nie dotyczy i bez problemu możemy wykorzystać AJAX (Tak kanały w standardzie RSS jak i w Atom korzystają z XML) choćby z poniższym kodem:
function GetXHR()
{
 var xmlHttp = null;
 try
 {   // Firefox, Opera 8.0+, Safari
  xmlHttp=new XMLHttpRequest();
 }
 catch (e)
 {   // Internet Explorer
  try
  {
   xmlHttp=new ActiveXObject("Msxml2.XMLHTTP");
  }
  catch (e)
  {
   xmlHttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
 }
 if(xmlHttp == null)
  alert("Browser does not support HTTP Request");
 return xmlHttp;
}

window.addEventListener('load', function()
{
 var chk=setInterval(function()
 {  
  doc = document.getElementsByTagName("body");
  if(doc.length == 0 || doc[0].innerHTML.length < 500)
  { return; }
  clearInterval(chk);
  
  var xhr = GetXHR();
  xhr.onreadystatechange=function()
  {
   if(this.readyState == 4 && this.status == 200)
   {
    document.getElementById("articleStuffShowRSSHere").innerHTML = "<a href='"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("link")[4].getAttribute("href")+"'>"+
     this.responseXML.getElementsByTagName("entry")[0].getElementsByTagName("title")[0].textContent+
     "</a>";
   }
   
  };
  xhr.open("GET" , '/feeds/posts/default', true);
  xhr.send();  
 },100);
}, false);
Przykład ten wyświetla obok link do najnowszego artykułu z tego bloga:

PHP i cache po stronie serwera

Jednak jeśli nasz serwis znajduje się w innej domenie to z usług JavaScript skorzystać nie będzie nam dane. Oczywiście możemy próbować korzystać z różnego rodzaju sztuczek lub zewnętrznych serwisów pobierających pliki syndykacji za nas... ale raczej nie o półśrodki tu chodzi, a o mniej lub bardziej profesjonalne i autonomiczne rozwiązanie. PHP na szczęście umożliwia nam pobranie plików z zewnętrznych serwisów i swobodną manipulację na nich, tak więc problem wspólnej domeny odpada. Pojawia się za to inny problem, a mianowicie problem z wydajnością - w przypadku JavaScript cały proces de facto zrzucaliśmy na użytkownika i z punktu widzenia serwera nie odczulibyśmy różnicy przy syndykacji z 2 kanałów lub z 200 kanałów (Choć użytkownik by zapewne odczuł). Przy korzystaniu z PHP natomiast jeśli każda wizyta użytkownika miałaby uruchamiać mechanizm pobierania plików, ich parsowania i transformacji do finalnej postaci prezentowanej użytkownikowi to nie potrzeba agregacji z 200 kanałów, ale wystarczy 1 kanał i 200 użytkowników naszego serwisu byśmy odczuli ten sam efekt. Co więcej istnieje realna groźba, że właściciel serwisu, z którego dane pobieramy uzna nas za źródło ataku DoS (ang. Denial-of-service), - bo wszak wszystkie żądania pobrania będą pochodzić z naszego serwera - i zablokuje nam dostęp do kanału.
Stąd też konieczny jest mechanizm zapisu pobranych danych w taki sposób aby odwiedziny nowych użytkowników nie generowały transferu danych pomiędzy serwisami, a korzystały z już ściągniętych danych. Tu należy się zastanowić na którym etapie procesu przygotowywania do prezentacji należy dane zapisać, bo w zależności od tego jak chcemy je wykorzystać inny moment tego procesu może okazać się najefektywniejszy. W poniższym przykładzie zapis do pliku na serwerze odbywa się po przygotowaniu całości prezentowanego komponentu, a podobny mechanizm można z powodzeniem wykorzystać do zapisu całych stron, których wygenerowanie wymaga wielu obliczeń lub zapytań do baz danych, a których treści nie ulegają częstej zmianie.
Generalnie kod składa się z dwóch części, pierwsza - dłuższa to funkcja przeprowadzająca pobranie, obróbkę i zapis treści na dysk serwera:
<?php
function prepare($ident)
{
 // download data and parse them with SimleXmlElement
 $content = file_get_contents('http://runaurufu.blogspot.com/feeds/posts/default');
 $x = new SimpleXmlElement($content);

 // get data which we want to use
 $entryName = $x->entry[0]->title."";
 $entryUrl = "";
 foreach ($x->entry[0]->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
   $entryUrl = $aux['href']."";
  }
 }

 $blogTitle = $x->title."";
 $blogSubTitle =  $x->subtitle."";
 $blogUrl = "";
 foreach ($x->link as $value)
 {
  $aux = $value->attributes();
  if($aux['rel']."" == 'alternate')
  {
  $blogUrl = $aux['href']."";
  }
 }

 // prepare for cacheing 

 //start output buffering
 ob_start();

 // here do entire presentation generation
 ?>
 <a href="<?php echo $entryUrl; ?>"><?php echo $entryName; ?></a> @ <a href="<?php echo $blogUrl; ?>" title="<?php echo $blogSubTitle; ?>"><?php echo $blogTitle; ?></a>

 <?php
 // save output buffer to $data
 $data = ob_get_contents();
 // clear output buffer and stop buffering
 // (ob_end_flush(); can be used to stop buffering and display buffer content)
 ob_end_clean();

 $handle = fopen($ident, 'wb');
 if(flock($handle, LOCK_EX))
 {
  $bytes = fwrite($handle, $data);
  flock($handle, LOCK_UN);
 }
 fclose($handle);
 return $data;
}
?>

Druga część jest odpowiedzialna za sam proces wyboru, czy chcemy użyć dane zapisane na dysku, lub czy mimo wszystko pobieramy je od nowa:
<?php
$cacheTime = 86400;
$ident = "myFeed.cache";
if(!is_file($ident))
{
 echo prepare($ident);
}
else
{
 if(filemtime($ident) + $cacheTime > time())
 {
  // show content of file
  echo file_get_contents($ident);
 }
 else
 {
  echo prepare($ident);
 }
}
?>
Oczywiście kod ten można w znacznej mierze zmodyfikować i sprawić, aby cały proces obsługi naszego cache wyglądał tak np:
<?php$cache = new Chors_Cache($identifier, $time);
if($cache->check())
{
 return $cache->fetchRender();
}
else
{
 $cache->startOutputCaching();
 // generate cached content begin
 ...
 // generate cached content end
 return $cache->endOutputCachingClean();
}?> 

Słowo na koniec

Jak widać obsługa kanałów syndykacji nie jest czymś trudnym a jedynym problemem na jaki możemy natrafić to konieczność stworzenia parsera wyłuskującego te dane, które nas interesują. Warto jeszcze wspomnieć, że o ile używanie AJAXa nie obciąża naszego serwera, to generuje zbyteczny ruch na serwerze udostępniającym kanał treści i z tego względu będąc "miłym sąsiadem" należałoby mimo wszystko zaprzęgnąć swój serwer do obsługi tych danych.

* Słowem wyjaśnienia: RSS i Atom o ile bywają synonimem udostępniania zawartości witryny to w rzeczywistości są tego udostępniania standardami, a nie samą nazwą procesu.

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)

piątek, 14 października 2011

Array vs Object

Tak jakoś nastał obiektowy szał i wszystko wszędzie musi być obiektowe. Oczywiście PHP ta mania nie ominęła i tak oto mamy coraz to większą społeczność krzyczących, że Object i Class jest super fajne, a Array to zło i anachroniczny przeżytek. Ale czy jest tak na pewno?
Oczywiście tablice nie pozwalają na implementacje funkcji odwołujących się jedynie do danych z konkretnej tablicy - takie coś musimy zrealizować poprzez metodę jakiejś klasy lub po prostu funkcję nie należącą do żadnej klasy (co oczywiście jest mało eleganckie). Jednak jeśli zależy nam jedynie na przechowaniu tudzież przekazaniu danych z jednej funkcji do drugiej to czy warto definiować nowy typ danych (zakładając, że struktura danych nigdy nie ulegnie zmianie)? W innych językach programowania w 99% przypadków odpowiedź brzmiała by "tak", jednak w przypadku PHP nie jest ona wcale taka oczywista.

Dlaczego? Ano załóżmy, że chcemy przesłać dane dotyczące elementu opisywanego za pomocą trzech atrybutów (id, name, color). Przykładowy kod realizujący takie zadanie wyglądałby następująco:
function getArray()
{
 $item = Array();
 $item['id'] = 1;
 $item['name'] = "Pure item";
 $item['color'] = "none";
 return $item;
}
 
function getObject()
{
 $item = new stdClass();
 $item->id = 1;
 $item->name = "Pure item";
 $item->color = "none";
 return $item;
}
function getItem()
{
 $item = new Item();
 $item->id = 1;
 $item->name = "Pure item";
 $item->color = "none";
 return $item;
}
Dla ostatniej funkcji konieczne jest zdefiniowanie typu "Item":

class Item
{
    public $id;
    public $name;
    public $color;
}

Wykonanie tych funkcji jak widać tworzy struktury o tych samych atrybutach - raz jest to tablica, a w dwóch przypadkach są to instancje klas.
getArray      0.1664547920
getObject      0.2381231785
getItem       0.2375209332
Jak widać stworzenie i zapełnienie tablicy trwa ok. 70% czasu potrzebnego na utworzenie odpowiadającej jej klasie. Warto zauważyć, że czasy potrzebne na utworzenie instancji obiektu za pomocą zdefiniowanej klasy (Item) jak i użycia stdClass() są porównywalne.

A jak przedstawia się proces pobierania wartości z tych trzech utworzonych struktur danych?
Acquire from Array     1.7432529926
Acquire from Object     2.1544189453
Acquire from Item     2.3280811310
Znów nie ma większych zaskoczeń- używanie tablic okazuje się o wiele szybsze niż zaprzęganie klas. Co ciekawe pobieranie danych z instancji utworzonych za pomocą stdClass() okazuje się szybsze (choć nieznacznie) od pobierania wartości atrybutów z klas zdefiniowanych przez użytkownika.

Wnioski
To co chciałem pokazać w tym wpisie to to, że nie warto bezkrytycznie podchodzić do obecnych "trendów". Jeśli potrzebujemy maksymalnie wydajnego sposobu na przekazanie zestawu danych z jednego miejsca w programie do drugiego - to definiowanie klas mija się z celem. Oczywiście jeśli chcemy by nasz kod był maksymalnie przenośny to warto (a wręcz należy) użyć klas - ale tych definiowanych przez użytkownika. Wszak po to tworzymy definicję struktury klasy aby móc dołączyć do niej metody, a w przypadku używania stdClass otrzymujemy (pod względem czystej funkcjonalności) tablicę, która w dodatku działa wolniej od tej "standardowo" tworzonej za pomocą Array().

Plik z testami do pobrania z chomika (link).

czwartek, 29 września 2011

unexpected T_PAAMAYIM_NEKUDOTAYIM

Wpis ku pamięci :)

Komunikat ten (unexpected T_PAAMAYIM_NEKUDOTAYIM) otrzymujemy, gdy używamy :: w kontekście, którego PHP nie obsługuje (out of scope), czyli np. odwołujemy się do zwykłych metod, a nie do metod statycznych. Co ciekawe błąd ten wyskakuje w PHP < 5.3 również gdy wywołujemy istniejącą metodę statyczną przy użyciu zmiennych:
$className::$methodName();
Problem ten oczywiście nie istnieje w PHP >= 5.3, jednak przy starszych wersjach wciąż można natknąć się na tego dziwoląga :)

Alternatywą dla tego rozwiązania jest użycie funkcji call_user_func(), ta jednak działa wolniej od takiego bezpośredniego wywołania:
call_user_func($class."::".$method);
Dla porównania sprawdziłem również czas wykonywania zwykłych (niestatycznych) metod:
class::method				    0.0067858696
call_user_func				    0.0170218945
call_user_func (array)			    0.0149610043
class->method				    0.0044798851
call_user_func (array non-static)	    0.0125558376
Kod z testu do pobrania z chomika (link).

poniedziałek, 26 września 2011

Dzielenie ciągów przy pomocy wielu separatorów

Czasem zdarza się tak, że musimy podzielić nasz ciąg tekstowy w wyznaczonych miejscach na kilka mniejszych. W tej sytuacji funkcja explode() sprawdza się wręcz znakomicie. Problem jednak pojawia się gdy chcemy dzielić za pomocą więcej niż jednego separatora, gdyż samo explode() obsługuje jedynie separator w postaci pojedynczego "stringu". Na poniższych przykładach pokazałem różne sposoby rozbicia ciągu przy użyciu trzech separatorów: & ? /.

Rozwiązania
Najtrywialniejszym sposobem na poprawne rozbicie wg kilku separatorów byłoby wywołanie ponownie explode() dla każdego elementu wynikającego z pierwszego użycia tej funkcji, jednak ten sposób aż bije po oczach swym prostactwem więc nawet nie będę bawił się w jego poprawną implementację, a użyję jedynie takiego (dużego i nierealizującego zadania) przybliżenia:
for ($i = 0; $i < 100000; $i++)
{
	$temp = explode ( "/" , $string);
	foreach($temp as $elem)
	{
		$e = explode("?", $elem);
		$e = explode("&", $elem);
	}
}

Kolejnym pomysłem jest iteracyjne podejście - używanie na przemian explode() i implode() w celu stworzenia pojedynczej tablicy z wynikiem.
for ($i = 0; $i < 100000; $i++)
{
	$temp = explode ( "/" , $string);
	$temp = explode("&", implode("&", $temp));
	$temp = explode("?", implode("?", $temp));
}

Skoro już postanowiliśmy iść w stronę podmian to warto by również rozważyć wielokrotne użycie str_replace().
for ($i = 0; $i < 100000; $i++)
{
	$temp = str_replace("&", "/", $string);
	$temp = str_replace("?", "/", $temp);
	$temp = explode ( "/" , $temp);
}

Nim przejdziemy do wyrażeń regularnych przytoczę tu jeszcze jeden przykład, który niestety działa jedynie w przypadku separatorów będących pojedynczymi znakami. Co ciekawe mimo, iż wykorzystanie funkcji strtok() wydaje się być potwornie nieefektywne to wyniki zbliżone są do naprzemiennego wywoływania funkcji explode() i implode().

for ($i = 0; $i < 100000; $i++)
{
	$tok = strtok($string, "/&?");
	$temp = array();
	while ($tok !== false)
	{
		$temp[] = $tok;
		$tok = strtok("/&?");
	}
	unset($tok);
	strtok('', '');
}

Ale czy mamy ograniczać się jedynie do takich kombinowanych sposobów? Może jest jedna "magiczna" funkcja, która zrobi to o czym myślimy? Ano jest i wykorzystuje magię wyrażeń regularnych, a zwie się ona preg_split().
for ($i = 0; $i < 100000; $i++)
{
	$temp = preg_split("/[\/&?]/",$string);
}

Tutaj jednak warto zwrócić uwagę na fakt, że jeśli naszym separatorem jest znak wielobajtowy to funkcja ta będzie generować dodatkowe puste elementy w wynikowej tablicy (jeden dodatkowy element na każde wystąpienie separatora). Sytuacja ta nie ma jednak miejsca jeśli separatorem nie jest znak a ciąg znaków, nawet jeśli wszystkie zawarte w nim elementy są znakami wielobajtowymi.
Tak czy inaczej remedium na ten problem jest funkcja mb_split():
for ($i = 0; $i < 100000; $i++)
{
	$temp = mb_split("[\/&?]",$string);
}

Ostatnim przytoczonym tu przykładem będzie połączenie wyrażeń regularnych wykorzystanych w funkcji preg_replace() wraz z pojedynczym wywołaniem explode():
for ($i = 0; $i < 100000; $i++)
{
	$temp = preg_replace("/[&?]/","/",$string);
	$temp = explode("/", $temp);
}

Testy wydajności
Jak widać istnieje wiele możliwości wykonania zadania dzielenia ciągu z użyciem wielu równorzędnych separatorów, ale które rozwiązanie jest najefektywniejsze? Popatrzmy na wyniki:

single explode:		    0.1573359966
multiple explode:	    1.0633759499
explode & implode:	    0.7556319237
replace & explode:	    0.4618999958
strtok:			    0.6737470627
preg_split:		    0.9344220161
mb_split:		    1.1594960690
preg_replace & explode:	    1.1028780937

Dla separatorów składających się również z kilku znaków otrzymujemy takie wyniki:

single explode:		    0.1560928822
multiple explode:	    1.3294098377
explode & implode:	    1.1651928425
replace & explode:	    1.0913951397
preg_split:		    1.8065340519
mb_split:		    4.9289889336
preg_replace & explode:	    1.8077979088

Jak widać wszystkie czasy wzrosły, przy czym wzrost mb_split() wynika w dużej mierze z używania wielobajtowych znaków.
Jeśli jednak zostaniemy przy tych trzech jedno-znakowych separatorach, a jedynie zwiększymy rozmiar badanego ciągu otrzymamy takie oto wyniki (10x mniej iteracji pętli testujących):

single explode:		    0.2046608925
multiple explode:	    1.8381199837
explode & implode:	    1.0454828739
replace & explode:	    0.5224039555
strtok:			    1.0597200394
preg_split:		    1.8259940147
mb_split:		    2.1428530216
preg_replace & explode:	    1.8069810867

Jeśli dodatkowo zamiast trzech separatorów użyjemy 5 będziemy mieć takie oto wyniki:

single explode:		    0.2125821114
multiple explode:	    4.1121268272
explode & implode:	    3.3804459572
replace & explode:	    1.6378400326
strtok:			    3.4061551094
preg_split:		    2.7953231335
mb_split:		    6.4811940193
preg_replace & explode:	    3.0788750648

Wnioski
Jak widać wydajność metod różni się w zależności tak od długości badanych ciągów, jak i ilości oraz postaci separatorów. Porównując wyniki możemy jednak dojść do wniosku, że najlepszym rozwiązaniem jest użycie duetu str_replace() wraz z explode(), który to we wszystkich testach okazał się najszybszym rozwiązaniem. Pozostałe metody są w zasadzie porównywalne, choć stosunek ich wydajności różni się w zależności od parametrów zadania (długość ciągu i postać separatorów). Pomimo to możemy jasno stwierdzić, że wielokrotne używanie explode() (multiple explode) jest całkowicie nieefektywne w porównaniu do pozostałych metod (zwłaszcza, że nie zaimplementowano tej funkcji w całości). Podobnie sytuacja wygląda w przypadku mb_split(), którą używać należałoby jedynie w przypadku skomplikowanych podziałów, a nie prostego dzielenia wg podanych słów.

Wykorzystany kod do testowania można pobrać z mojego chomika (link).

niedziela, 28 sierpnia 2011

Łączenie ciągów tekstowych

Chyba jednym z najpowszechniejszych działań w PHP jest łączenie różnej maści ciągów. Czy to przy budowaniu zapytań do bazy, czy też przy generowaniu treści do wyświetlenia. Jednak czy zastanawialiście się nad różnicami w sposobie łączenia ciągów? W tym wpisie porównam metodę doklejania kolejnych elementów za pomocą ".", dołączania wartości zmiennych przy pomocy "{$var}" oraz działanie funkcji sprintf. A na koniec porównam wydajność rzutowań bezpośrednich oraz konwersji z użyciem sprintfa.

Na początek mały test pokazujący różnice pomiędzy pustymi sprintf, "" oraz ''.

Sprintf:	    5.5083110332
" ":		    1.8283259869
' ':		    1.7890319824

oraz sam test dla "" i '':

" ":	    1.1731438637
' ':	    1.1480429173
" ":	    1.1413729191
' ':	    1.1829788685

Jak widać różnice w wydajności pomiędzy "" a '' są praktycznie nieistniejące, natomiast sprintf wykonuje się 3 razy dłużej.
Porównajmy zatem wydajność przy dołączaniu tekstu zapisanego w zmiennej pomiędzy inne elementy.

Sprintf:	7.5031311511993
" "." ":	2.9455320835114
" ":		3.1112060546875
{ }:		3.1566998958588

Wyniki te kolejno odnoszą się do kodu:
$temp = sprintf("prefix %s postfix",$text);
$temp = "prefix ".$text." postfix";
$temp = "prefix $text postfix";
$temp = "prefix {$text} postfix";

Okazuje się, że najszybciej działa łączenie kolejnych niezależnych ciągów, następnie w szeregu wydajności znajdują się metody "na skróty", a na końcu wywołanie funkcji sprintf.

Na sam koniec zostawiłem porównanie wydajności rzutowań na typy liczbowe - integer i float. Wyniki prezentują się następująco:
Sprintf %d:	    0.7861609459	666
Sprintf %u:	    0.7842721939	666
Sprintf %f:	    1.2757029533	666.999000
(int):		    0.2504780293	666
(float):	    0.2895848751	666.999

Funkcja sprintf znowu jest trzykrotnie, a w przypadku rzutowania na float ponad czterokrotnie wolniejszy. Jednak sytuacja ta zmienia się diametralnie gdy zaczynamy tworzyć ciąg składający się z wielu zmiennych:
Sprintf %d %f:		    6.8353290558
Sprintf %1$d %2$f:	    7.3985271454
(int), (float)		    8.1965639591

Wyniki te odpowiadają kodowi:
$temp = sprintf("%d %f %d",$numeric, $float, $int);
$temp = sprintf('%1$d %2$f %3$d',$numeric, $float, $int);
$temp = (int)$numeric." ".(float)$float." ".(int)$int;

Przy tworzeniu rozbudowanych ciągów tekstowych jednak warto użyć funkcji sprintf. Jednak z drugiej strony przy pojedynczych zmiennych o wiele lepiej używać bezpośredniego rzutowania. To na co również warto zwrócić uwagę to fakt, że odnoszenie się do zmiennych poprzez %1$, %2$ itd. mimo iż jest z pewnością szybsze do napisania to jednak działa nieco wolniej od bezpośredniego podawania każdego z parametrów (nawet jeśli musimy je podawać wielokrotnie w jednym wywołaniu funkcji).

Wykorzystany kod do testowania można pobrać jak zwykle z mojego chomika (link).

piątek, 5 sierpnia 2011

Dostęp do prywatnych pól klas przodków w PHP

Ten wpis jest w zasadzie rozszerzeniem poprzedniego wpisu. To co tutaj zostanie omówione to stricte dostęp do pól prywatnych danej klasy, jak i również troszkę bardziej rozbudowany przykład wykorzystania klas z rodziny Reflection.

Jak można było zauważyć w poprzednim wpisie funkcja get_object_vars() realizuje de facto zadanie pobrania wartości i nazw pól klas. Jej wadą jest jednak niemożność dojścia do metod prywatnych przodków naszej klasy i właśnie w celu dodania tej funkcjonalności niezbędne okażą się klasy Reflection.

Na początek przypomnijmy jednak sobie naszą hierarchię klas...
class First
{
    private $firstPrivate;
    protected $firstProtected;
    public $firstPublic="DEFAULT";
}

class Second extends First
{
    private $secondPrivate;
    protected $secondProtected;
    public $secondPublic;
}

class Third extends Second
{
    private $thirdPrivate;
    protected $thirdProtected;
    public $thirdPublic;
}

Aby dobrać się do metod prywatnych potrzebujemy o wiele bardziej rozbudowanego kodu niż ten umieszczony w poprzednim wpisie, oraz rekurencji koniecznej do przejścia po grafu dziedziczenia klas.
Takim chyba najprostszym przykładem pobrania wartości wszystkich parametrów jest funkcja:
function getAllFields($object)
{
    $globalFields = array();
    $reflectionClass = new ReflectionClass($object);
    do
    {
        $reflections = $reflectionClass->getProperties();

        foreach($reflections as $reflectionProperty)
        {
            $reflectionProperty->setAccessible(true);
            $globalFields[$reflectionProperty->name] = $reflectionProperty->getValue($object);
        }
        $reflectionClass = $reflectionClass->getParentClass();
    }
    while($reflectionClass);
    return $globalFields;
}
dająca taki oto rezultat
Array
(
    [thirdPrivate] => 
    [thirdProtected] => 
    [thirdPublic] => NEW
    [secondProtected] => 
    [secondPublic] => 
    [firstProtected] => 
    [firstPublic] => DEFAULT
    [secondPrivate] => 
    [firstPrivate] => NEW PRIVATE
)
Funkcję ta jednak ma główną wadę w postaci nadpisywania zmiennych klas bliższych badanemu obiektowi przez zmienne jego przodków. Na szczęście można tą funkcję dowolnie rozwijać i sprawić by generowała wszelkie informacje dotyczące badanej klasy:
function getAllFieldsStrict($object)
{
    $globalFields = array();
    $reflectionClass = new ReflectionClass($object);
    do
    {
        $reflections = $reflectionClass->getProperties();

        foreach($reflections as $reflectionProperty)
        {
            if($reflectionProperty->getDeclaringClass()->name == $reflectionProperty->name)
            {
                switch(true)
                {
                    case $reflectionProperty->isPrivate(): $globalFields[$reflectionProperty->name][$reflectionProperty->name]['access'] = 'private'; break;
                    case $reflectionProperty->isProtected(): $globalFields[$reflectionProperty->name][$reflectionProperty->name]['access'] = 'protected'; break;
                    case $reflectionProperty->isPublic(): $globalFields[$reflectionProperty->name][$reflectionProperty->name]['access'] = 'public'; break;
                }
                $reflectionProperty->setAccessible(true);
                $globalFields[$reflectionProperty->name][$reflectionProperty->name]['value'] = $reflectionProperty->getValue($object);
                $globalFields[$reflectionProperty->name][$reflectionProperty->name]['static'] = $reflectionProperty->isStatic();
            }
        }
        $reflectionClass = $reflectionClass->getParentClass();
    }
    while($reflectionClass);
    return $globalFields;
}
czego rezultatem jest
Array
(
    [Third] => Array
        (
            [thirdPrivate] => Array
                (
                    [value] => 
                    [static] => 
                    [access] => private
                )
            [thirdProtected] => Array
                (
                    [value] => 
                    [static] => 
                    [access] => protected
                )
            [thirdPublic] => Array
                (
                    [value] => NEW
                    [static] => 
                    [access] => public
                )
        )
    [Second] => Array
        (
            [secondPrivate] => Array
                (
                    [value] => 
                    [static] => 
                    [access] => private
                )
            [secondProtected] => Array
                (
                    [value] => 
                    [static] => 
                    [access] => protected
                )
            [secondPublic] => Array
                (
                    [value] => 
                    [static] => 
                    [access] => public
                )
        )
    [First] => Array
        (
            [firstPrivate] => Array
                (
                    [value] => NEW PRIVATE
                    [static] => 
                    [access] => private
                )
            [firstProtected] => Array
                (
                    [value] => 
                    [static] => 
                    [access] => protected
                )
            [firstPublic] => Array
                (
                    [value] => DEFAULT
                    [static] => 
                    [access] => public
                )
        )
)
Wydajność
get_object_vars:  0.28459501266479
getAllFields:   4.3010709285736
getAllFieldsStrict:  4.3191359043121
Podczas testu wykonano każde z zapytań 100000 razy i jak widać wygenerowanie rozbudowanej struktury klas zajmuje praktycznie tyle samo czasu co stworzenie prostszej. Mimo wszystko jednak generowanie trwa ok. 15 razy dłużej w porównaniu do wyciągnięcia informacji za pomocą get_object_vars(), dlatego też jeśli potrzebujemy dostać się do zmiennych klas i mamy możliwość deklarowania pól jako protected a nie private - to zdecydowanie lepiej tak zrobić i używać get_object_vars().

Przykładowy kod do testowania można pobrać z mojego chomika (link).

czwartek, 4 sierpnia 2011

Listowanie nazw pól klasy w PHP

Chcąc dobrać się do zmiennych klasy - ich nazw oraz ewentualnie wartości - dokopałem się do trzech sposobów. Dwa z nich (get_class_vars() i get_object_vars()) pochodzą jeszcze z czasów strukturalnego pisania kodu, zaś trzeci (ReflectionClass) jest już cudem współczesnego przerośniętego obiektowego PHP5.

No ale przechodząc do rzeczy, a właściwie do przykładu obrazującego o co mi chodzi :)

class First
{
    private $firstPrivate;
    protected $firstProtected;
    public $firstPublic="DEFAULT";
}

class Second extends First
{
    private $secondPrivate;
    protected $secondProtected;
    public $secondPublic;
}

class Third extends Second
{
    private $thirdPrivate;
    protected $thirdProtected;
    public $thirdPublic;
}
Otóż mamy 3 klasy, a w każdej z nich 3 zmienne - private, protected i public... Na początku miałem nadzieję, że uda się dobrać do nich wszystkich, jednak okazało się, że do pól prywatnych można dostać się jedynie z kontekstu tej samej klasy - tak więc musiałbym w każdej klasie umieszczać dedykowany kod (lub użyć przerośniętej funkcji działającej w oparciu o Reflection), a takie rozwiązanie w grę nie wchodziło :x Tak więc poświęciłem prywatne zmienne i zamieniłem je na protected i ten problem zniknął ^.^

Pobieranie nazw parametrów
Jednak wracając do zagadnienia - samo uzyskanie nazw zmiennych jest dość banalne i można je osiągnać choćby dzięki dowolnej z poniższych metod:

function getClassVars()
{
    return get_class_vars(get_class($this));
}

function getObjectVars()
{
    return get_object_vars($this);
}

function reflectionGetDefaultProperties()
{
    $reflectionClass = new ReflectionClass($this);
    return $reflectionClass->getDefaultProperties();
}

function reflectionGetProperties()
{
    $reflectionClass = new ReflectionClass($this);
    return $reflectionClass->getProperties();
}

3 pierwsze z nich zwracają tablicę, w której klucze elementów mają nazwę parametrów, a wartości do tych kluczy przypisane równe są wartościom obiektów. Tylko, że tutaj uwaga, gdyż get_class_vars() oraz ->getDefaultProperties zwracają jedynie wartości na sztywno przypisane (domyślne) w kodzie do poszczególnych zmiennych, a nie aktualne wartości tych zmiennych w instancji obiektu.

Tak więc po wykonaniu kodu:

class First
{
    private $firstPrivate;
    protected $firstProtected;
    public $firstPublic="DEFAULT";
    static $firstStatic = "STATIC";

/** metody identyczne do podanych w poprzednim przykładzie */
 function getClassVars() {...}
 function getObjectVars() {...}
 function reflectionGetDefaultProperties() {...}
 function reflectionGetProperties() {...}
}

$object = new A();
$object->firstPublic = "NEW";

print_r($d->getClassVars());
print_r($d->getObjectVars());
print_r($d->reflectionGetDefaultProperties());
print_r($d->reflectionGetProperties());
otrzymamy:
Array
( 
    [firstStatic] => STATIC
    [firstPrivate] => 
    [firstProtected] => 
    [firstPublic] => DEFAULT
)
Array
(
    [firstPrivate] => 
    [firstProtected] => 
    [firstPublic] => NEW
)
Array
(
    [firstPrivate] => 
    [firstProtected] => 
    [firstPublic] => DEFAULT
    [firstStatic] => STATIC
)
Array
(
    [0] => ReflectionProperty Object
        (
            [name] => firstPrivate
            [class] => First
        )

    [1] => ReflectionProperty Object
        (
            [name] => firstProtected
            [class] => First
        )

    [2] => ReflectionProperty Object
        (
            [name] => firstPublic
            [class] => First
        )

    [3] => ReflectionProperty Object
        (
            [name] => firstStatic
            [class] => First
        )
)

Jednak jeśli zależy nam tylko na nazwach zmiennych to generalnie nie ma wielkiej różnicy w wybranej metodzie. Poza tym jak widać ostatnia metoda (->getProperties) zwraca nam obiekty klasy ReflectionProperty.. ale o tym w innym wpisie. Warto jeszcze wspomnieć o tym, że get_object_vars() nie zwraca statycznych pól klasy, ale po co nam one jeśli chodzi nam o stan konkretnej instancji obiektu?
Wydajność
Generalnie jeśli zależy nam jedynie na uzyskaniu nazw parametrów to dlaczego by nie wybrać najszybszej metody? Testy przeprowadziłem na hostingu ovh (większa stabilność wyników niż przy testowaniu na własnym kompie):

get_class_vars:   3.3929491043091
get_object_vars:   2.7406477928162
->getDefaultProperties:  3.6021101474762
->getProperties:   8.8276469707489

Test polegał na wykonaniu pętli
for ($i = 0; $i < 1000000; $i++){ $d->bref(); }
dla każdego z zapytań i zmierzenie czasu za pomocą funkcji microtime(true). Jak widać po wynikach najszybciej działa funkcja get_object_vars(), a najwolniejsze w użyciu są klasy z rodziny Reflection. Oczywiście w przypadku wpisania nazwy klas na sztywno (literka s przy opisie) zamiast używania get_class() wyniki są trochę lepsze (wyniki w dwóch zestawach gdyż sumarycznie przekraczają 30 sekundowy maksymalny czas wykonania skryptu):

get_class_vars:   3.3419530391693
get_class_vars (s):   3.0308630466461
get_object_vars:   2.6966669559479

get_object_vars:   2.6544170379639
->getDefaultProperties:  3.5368349552155
->getDefaultProperties (s):  3.5258779525757
->getProperties:   8.8363699913025
->getProperties (s):   8.7472901344299 

Co ciekawe najbardziej na tej zmianie zyskuje get_class_vars().

Przykładowy kod do przeprowadzenia testów można pobrać (jak zawsze) z mojego chomika (link).