This post comes from the first version of this blog.
Please send me an email if anything needs an update. Thanks!
Ci, którzy śledzą stronę niniejszego blogu na Facebooku, na pewno zauważyli status, jakim się z nimi podzieliłem w środę. Strasznie się wtedy zdenerwowałem, bo straciłem kilka godzin życia, a popełniony błąd był trywialny. Podczas analizy działania zwyczajnie skupiłem się na bardziej zaawansowanych częściach kodu, zapominając o podstawach, co odbiło się niestety na procesie “debugowania”. Skoro powstał problem, to na pewno istnieje dla niego rozwiązanie, dlatego w niniejszym wpisie chciałbym przedstawić Wam sposób na zabezpieczenie się przed tym “błędem”.
Funkcja require_once() działa... tylko "once".
O co chodziło w całym problemie? Otóż, moja praca inżynierska, w ramach której piszę framework w języku PHP w pewnym momencie przekazuje sterowanie do modułu widoku [View]. Widok ten, pomijając całą abstrakcję, jaka za nim stoi, w pewnym momencie wykonuje zwyczajną instrukcję require_once(), włączając plik z kodem szablonu.Nie ma w tym nic nadzwyczajnego, poza tym, że czasem w Widoku może wystąpić błąd, co dzięki wymyślonej przeze mnie strukturze kodu zawsze kończy się rzuceniem wyjątku. Wyjątek ten, o ile nie zostanie “złapany” i wcześniej obsłużony, kończy się przekierowaniem przepływu sterowania na uprzednio zdefiniowany tzw. “routing 404”, czyli arbitralnie wybrany kontroler i akcję, która wyświetla stosowny błąd.
Problem polega na tym, że oba widoki - ten od “normalnej” akcji i ten od wyświetlania błędu korzystają w wielu miejscach z tych samych plików partiali [plików, zawierających powtarzalne fragmenty widoku, np. nagłówek dokumentu HTML]. W tym momencie każdy “siedzący w temacie” widzi już problem. Jeśli wystąpi błąd, “powtórzone” pliki zwyczajnie nie zostaną włączone do kodu, co istotnie zuboży wyświetlane dane. W moim przypadku nie mogłem znaleźć przyczyny, dla której widok 404 się nie wyświetlał.
Wszystkiemu “winna” jest funkcja require_once(), którą niejako “z rozpędu” wstawiłem do metody render() głównej klasy modułu widoku. Winny oczywiście jestem ja, jako programista, aczkolwiek nie znaczy to, że zamierzam siedzieć z rozłożonymi rękami i czekać, aż ktoś znowu “potknie się” o ten sam problem.
Funkcja get_included_files().
Okazuje się, że możemy się zabezpieczyć się przed takimi problemami w relatywnie prosty sposób, w obie strony - możemy sprawdzić, czy plik był już "includowany" [ktoś zna lepsze słowo określające ten proces?], aby:- zabezpieczyć skrypt przed kolejnymi próbami wstawiania np. pliku tej samej klasy.
- przetestować przepływ sterowania aplikacji i ilość odwołań do danego pliku
Mając tego typu informacje, możemy przeprowadzić mały eksperyment, pokazujący działanie funkcji. Struktura plików wygląda następująco:
Skrypt testowy:
|
|
Wynik działania:
ONLY INDEX.PHP:
array (
0 => 'PATH/get_included_files/index.php',
)
NEXT TWO FILES:
array (
0 => 'PATH/get_included_files/index.php',
1 => 'PATH/includes/file.php',
2 => 'PATH/requires/foo.php',
)
ANOTHER TWO FILES:
array (
0 => 'PATH/get_included_files/index.php',
1 => 'PATH/includes/file.php',
2 => 'PATH/requires/foo.php',
3 => 'PATH/includes/other.php',
4 => 'PATH/requires/bar.php',
)
Jak widać, kolejne pliki były dołączane do zawartości zwracanej tablicy automatycznie. Wykorzystajmy zatem poznane informacje, aby umieć zareagować na odpowiednie sytuacje.
Jak sprawdzić, czy plik został już włączony do kodu?
Aby sprawdzić, czy plik był już includowany należy jedynie sprawdzić istnienie odpowiedniej wartości w tej tablicy, np. za pomocą funkcji in_array(). Przykładowy kod, umieszczony bezpośrednio pod pierwszym listingiem:
|
|
Daje następujące wyniki:
PATH\index.php: included
PATH\includes\file.php: included
PATH\includes\other.php: included
PATH\requires\foo.php: included
PATH\requires\bar.php: included
PATH\nonexistent.php: not included
PATH\somedir\inner.php: not included
Przejdźmy zatem do omówienia strategii “obrony” przed różnego rodzaju problemami związanymi z włączaniem zawartości plików do kodu.
Metody "obrony" - reakcje na próbę włączenia nowego pliku.
Mając takie narzędzia możemy w łatwy sposób zareagować na próbę włączania nowego pliku w odpowiedni dla naszej sytuacji sposób. Jeśli nie chcemy włączania duplikatu, pomińmy go:
|
|
wynik:
PATH\index.php: duplicate ignored
PATH\includes\file.php: duplicate ignored
PATH\includes\other.php: duplicate ignored
PATH\requires\foo.php: duplicate ignored
PATH\requires\bar.php: duplicate ignored
PATH\nonexistent.php: included
PATH\somedir\inner.php: included
lub rzućmy wyjątkiem:
|
|
wynik:
PATH\index.php: exception thrown
PATH\includes\file.php: exception thrown
PATH\includes\other.php: exception thrown
PATH\requires\foo.php: exception thrown
PATH\requires\bar.php: exception thrown
PATH\nonexistent.php: included
PATH\somedir\inner.php: included
Być może zależy nam na zliczaniu ilości włączeń tego samego pliku:
|
|
wynik:
index.php: included 2 times.
includes\file.php: included 2 times.
includes\other.php: included 2 times.
requires\foo.php: included 2 times.
requires\bar.php: included 2 times.
nonexistent.php: included 0 times.
somedir\inner.php: included 0 times.
Do włączania do kodu plików, które zwyczajnie nie muszą być unikalne polecam jednak po prostu usunąć przyrostek “_once” z wykorzystywanej przez nas funkcji. Nie ma co się aż tak męczyć. ;]
Wszystko zależy tylko od naszej wyobraźni, zatem wykorzystajmy ją najlepiej, jak się tylko da. Czy przychodzą Wam na myśl inne metody reakcji na wyżej wymienione sytuacje? Podzielcie się swoimi opiniami w komentarzach.