W kwestii obsługi sesji i rodzaju zapisywanych w niej danych programiści PHP dzielą się na dwa skrajne fronty: tych, którzy preferują zapis wyłącznie typów prostych i tych, którzy nie widzą problemu w trzymaniu w niej całych obiektów. Ja stoję murem za typami prostymi, ponieważ wydaje mi się bardziej optymalne zapisywanie tylko niezbędnych informacji, na podstawie których ewentualne obiekty można odtworzyć, pozwala to też na zmniejszenie obciążenia serwera, bo odczytanie sesji wymaga deserializacji [odserializowania? deserializowania? kolejne "ciężkopolskie" słowo...] i parsowania o wiele mniejszego pliku. Jakkolwiek byśmy jednak do tego nie podchodzili, dzisiejszy wpis także może być argumentem in plus dla zapisywania wyłącznie zmiennych typów prostych, ponieważ jak mówi stare programistyczne porzekadło: "Serializujesz obiekty [do sesji]? No to masz problem!". ;]

Problem.

Oczywiście dla równowagi mogę powiedzieć, że osoby podzielające moje zdanie na ten temat także nie należą do tych, które nie mają żadnych problemów z zarządzaniem sesjami, jednak na pewno nie są to problemy aż takiego kalibru / typu. Co mam więc tym razem na myśli? Rozważmy następującą sytuację [zmienne proste]:

  • Rozpoczynamy sesję przez session_start().
  • W tle jest ładowany / tworzony plik sesji.
  • Po chwili mamy już dostępną superglobalną tablicę $_SESSION.
  • Zapisujemy do sesji kilka wartości, załóżmy, że będzie to jakiś float, int i string, żeby nikt nie poczuł się pokrzywdzony.
  • Klikamy w jakiś link na stronie.
  • Ponownie rozpoczynamy sesję przez session_start().
  • Ponownie w tle jest ładowany / tworzony plik sesji.
  • Znów po chwili mamy już dostępną superglobalną tablicę $_SESSION.
  • Korzystamy ze zmiennych w sesji [np. wyświetlamy je na ekranie].
  • Cieszymy się, że działa.

Nie ma tu żadnego problemu, algorytm jest, rzekłbym "idiotoodporny" [dosyć ciekawe i często stosowane pojęcie w informatyce, proszę się w żaden sposób nie obrażać ;]], nie wymaga specjalnego kombinowania. Rozważmy więc analogiczną sytuację, jednak zapisując do sesji dane pochodzące z zserializowanych obiektów [typ bazowy jest nieważny]:

  • Ładujemy pliki klas odpowiedzialnych za powstanie później wykorzystywanych obiektów.
  • Rozpoczynamy sesję przez session_start().
  • W tle jest ładowany / tworzony plik sesji.
  • Po chwili mamy już dostępną superglobalną tablicę $_SESSION.
  • Zapisujemy do sesji kilka wartości, będą to zserializowane obiekty kilku typów.
  • Klikamy w jakiś link na stronie.
  • "Zapominamy" włączyć pliki niektórych klas których obiekty zostały zserializowane w sesji.
  • Ponownie rozpoczynamy sesję przez session_start().
  • Ponownie w tle jest ładowany / tworzony plik sesji.
  • Znów po chwili mamy już dostępną superglobalną tablicę $_SESSION.
  • Chcemy skorzystać ze zmiennych w sesji [np. wyświetlamy je na ekranie], ale pojawia się "mały" problem - niektóre zmienne zamiast zostać ponownie poprawnie załadowane, mają typ "__PHP_Incomplete_Class" i są "nie bardzo" przydatne.
  • Martwimy się i szybko sprawdzamy w Google co jest nie tak z naszym pięknym kodem, trafiamy na mojego bloga i czytamy dalej. ;]

Co jest nie tak? Spokojnie, już śpieszę z wyjaśnieniami. ;]

__PHP_Incomplete_Class.

Czym jest ten nieznany twór? Parafrazując "nieco" Mickiewicza:

A imię jego będzie... __PHP_Incomplete_Class.

Czym jest więc owa __PHP_Incomplete_Class? Manual PHP zawiera jedynie szczątkowe informacje o tym, że jest to jeden z błędów [blok "Parameters]:

Note: unserialize_callback_func directive
It's possible to set a callback-function which will be called, if an undefined class should be instantiated during unserializing. (to prevent getting an incomplete object "__PHP_Incomplete_Class".) Use your php.ini, ini_set() or .htaccess to define 'unserialize_callback_func'. Everytime an undefined class should be instantiated, it'll be called. To disable this feature just empty this setting.

__PHP_Incomplete_Class jest to bardzo ciekawy twór pochodzący z jeszcze ciekawszego procesu. Jest to jedna z wewnętrznych klas biblioteki PHP, a jej wykorzystanie ma miejsce w momencie, kiedy interpreter próbuje stworzyć obiekt posiadając jego dane, ale nie znając deklaracji [czy też definicji, bo w kodzie PHP są one niejako złączone] klasy bazowej nie jest w stanie w pełni go odtworzyć. Serializacja powoduje zapis wszystkich atrybutów obiektu do "zwykłego" stringa, w związku z czym możliwe jest odtworzenia samych danych, jednak będzie niemały problem z metodami. ;]

Rozwiązanie.

Rozwiązanie jest bardzo proste, wystarczy "przypomnieć" sobie o załadowaniu odpowiednich plików klas, ewentualnie użyć jednego z autoloaderów [własna funkcja __autoload(), ew. zarejestrować inną własną funkcję na stosie loadera przez spl_autoload_register()]. Możemy także zarejestrować odpowiednią funkcję callback, która pomoże rozprawić się z "nieistniejącą" klasą.

Czytając różne materiały które pomogły mi w napisaniu tego posta trafiłem też na inny ciekawy pomysł na samodzielne stworzenie odpowiedniej klasy z zapisanymi atrybutami korzystając z danych, jakie zostały odserializowane w tablicy sesji [mam nadzieję, że nikt się nie obrazi, jeśli zacytuję ten komentarz z podaniem źródła, chciałbym tylko dokładniej przyjrzeć się samemu mechanizmowi]:

Odnośnik do komentarza [podany też wyżej]: http://www.php.net/manual/pl/function.unserialize.php#93451

__PHP_Incomplete_Class Object Demystified

1. First take note of the output. A simple example:

__PHP_Incomplete_Class Object (
[__PHP_Incomplete_Class_Name] => SomeObject1
[obj1property1] => somevalue1 [obj1property2] => __PHP_Incomplete_Class Object ( [__PHP_Incomplete_Class_Name] => SomeObject2 [obj2property1] => somevalue1 [obj2property2] => Array (
['key1'] => somevalue3, ['key2'] => somevalue4 ) ) )
2. We analyze this and break it down.
__PHP_Incomplete_Class Object tells you there is an object that needs to be declared somehow.
__PHP_Incomplete_Class_Name simply tells you the expected class name. It is just one of the properties for now.

Tak jak wspomniałem wyżej, zdeserializowany obiekt wygląda całkiem przystępnie, tzn. można po nim w łatwy sposób iterować i korzystać nawet bez "obiektowej otoczki".

So we have:
a) an unknown object that has a class name SomeObject1 (first class)
b) it has 2 properties, namely obj1property1 and obj2property2
c) obj2property2 is itself an object whose class name is SomeObject2 (the second class)
d) SomeObject2 has two properties, obj2property1 and obj2property2
e) obj2property2 is an array that contains two elements

3. Now that we have an idea of the structure, we shall create class definitions based from it. We will just create properties for now, methods are not required as a minimum.

class SomeObject1 {
public $obj1property1;
public $obj1property2;
}
class SomeObject2 {
public $obj2property1;
public $obj2property2;
}
?>

Wyprodukowanie tego typu klas w oddzielnym tymczasowym pliku nie będzie trudne, podobnie jak podstawienie odpowiednich wartości [wszystkie atrybuty są publiczne].

4. Have that accessible to your script and it will solve the __PHP_Incomplete_Class Object problem as far as the output is concerned. Now you will have:

SomeObject1 ( [obj1property1] => somevalue1 [obj1property2] => SomeObject2 ( [obj2property1] => somevalue1 [obj2property2] => Array ( ['key1'] => somevalue3, ['key2'] => somevalue4 ) ) )

As you will notice, __PHP_Incomplete_Class Object is gone and replaced by the class name. The property __PHP_Incomplete_Class_Name is also removed.

Ładując te "tymczasowe" klasy do naszego kodu możemy zauważyć, że __PHP_Incomplete_Class "automagicznie" [ciekawe słowo zapożyczone ze skryptu Readable ;]] zmieniło się na obiekty naszych klas.

5. As for the array property obj2property2, we can directly access that and just assume that it is an array and loop through it:

// this will be SomeObject1
$data = unserialize($serialized_data);

// this will be SomeObject2
$data2 = $data->obj1property2();

foreach($data2->obj2property2 as $key => $value):
print $key.' : '. $value .'
';
endforeach;

?>

Outputs:
key1 : somevalue3
key2 : somevalue4

Jak widać cały proces przebiegł sprawnie, wszystkie atrybuty zostały odczytane poprawnie. Pozostaje problem z metodami, ale w krytycznych sytuacjach wystarczą same dane. ;]

That's it. You can add more methods on the class declarations for the given properties, provided you keep your original output as basis for the data types.

Czyli możemy cieszyć się klasą nawet bez jej deklaracji / definicji. ;]

Podsumowanie.

Na problem z "niekompletnymi klasami" trafiłem przypadkiem podczas poszukiwań rozwiązania mojego małego problemu z variable functions, który ostatecznie musiałem jednak obejść zamiast rozwiązać, ale o tym też na pewno napiszę, może nawet do tego czasu znajdę odpowiedni sposób "poradzenia sobie" z nim. Z wielką chęcią podjąłem się napisania tego wpisu ze względu na to, że zapytanie wyszukiwarki o "__PHP_Incomplete_Class" zwraca miliony wyników - na żadnym z nich nie ma ani jednego konretu w stylu "ta klasa to (...), służy do (...)". Trzeba było zatem zapełnić lukę. ;] Dziękuję za lekturę do samego końca i zapraszam znowu za tydzień!

PS Tym którzy dotrwali do końca artykułu życzę Wesołych Świąt! Przekażcie tym, co odpadli już na wstępie. ;]