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
PHP jest znany z tego, że posiada dosyć mało „zwartą” bibliotekę standardową, przez co podczas przeglądania manuala można natknąć się na bardzo ciekawe pozycje, które bardzo ułatwiają życie programisty. Jedną z takich ciekawostek jest funkcja get_included_files(), która zwraca nam tablicę [array] ze ścieżkami wszystkich plików, jakie były włączane do kodu aż do linijki wywołania. Funkcja ta posiada także swój odpowiednik o nazwie get_required_files(), jednak jest on tylko „aliasem” dla wersji ze słowem „include”, także możemy go pominąć w opisie.
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:
<?php
function showIncludedFiles()
{
$files = get_included_files();
foreach($files as $number => $file)
{
$elements = explode('\\', $file);
$count = count($elements);
$files[$number] = 'PATH/'.$elements[$count - 2].'/'.$elements[$count - 1];
}
var_export($files);
}
echo '<pre>';
echo 'ONLY INDEX.PHP:'."\n";
showIncludedFiles();
echo "\n\n";
echo 'NEXT TWO FILES:'."\n";
require_once('includes/file.php');
require_once('requires/foo.php');
showIncludedFiles();
echo "\n\n";
echo 'ANOTHER TWO FILES:'."\n";
require_once('includes/other.php');
require_once('requires/bar.php');
showIncludedFiles();
echo "\n\n";
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:
function checkInclusion($file)
{
$includedFiles = get_included_files();
return
in_array($file, $includedFiles)
? 'included'
: 'not included';
}
$files = array(
'index.php',
'includes\\file.php',
'includes\\other.php',
'requires\\foo.php',
'requires\\bar.php',
'nonexistent.php',
'somedir\\inner.php',
);
foreach($files as $file)
{
$path = dirname(__FILE__).'\\'.$file;
echo 'PATH\\'.$file.': '.checkInclusion($path)."\n";
}
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:
function ignoreDuplicate($file)
{
$path = dirname(__FILE__).'\\'.$file;
$status = checkInclusion($path);
if('included' == $status)
{
echo 'PATH\\'.$file.': duplicate ignored';
}
else
{
require_once($file);
echo 'PATH\\'.$file.': included';
}
}
echo "\n";
foreach($files as $file)
{
echo ignoreDuplicate($file)."\n";
}
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:
function exceptionDuplicate($file)
{
$path = dirname(__FILE__).'\\'.$file;
$status = checkInclusion($path);
if('included' == $status)
{
throw new Exception('PATH\\'.$file.': exception thrown');
}
else
{
require_once($file);
echo 'PATH\\'.$file.': included';
}
}
echo "\n";
foreach($files as $file)
{
try
{
echo exceptionDuplicate($file)."\n";
}
catch(Exception $e)
{
echo $e->getMessage()."\n";
}
}
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:
$files = array(
'index.php' => 0,
'includes\\file.php' => 0,
'includes\\other.php' => 0,
'requires\\foo.php' => 0,
'requires\\bar.php' => 0,
'nonexistent.php' => 0,
'somedir\\inner.php' => 0,
);
echo "\n";
foreach($files as $file => $number)
{
$path = dirname(__FILE__).'\\'.$file;
if('included' == checkInclusion($path))
{
$files[$file]++;
}
}
foreach($files as $file => $number)
{
$path = dirname(__FILE__).'\\'.$file;
if('included' == checkInclusion($path))
{
$files[$file]++;
}
}
foreach($files as $file => $number)
{
echo $file.': included '.$number.' times.'."\n";
}
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.
Warto przeczytać.
Trwa ładowanie…

Witaj,
Zastanawiam się jednak jak to jest z wydajnością takiego includowania plików z klasami w porównaniu do __autoload()? Wiadomo że wszystko zależy od implementacji tej funkcji, ale czy testowałeś/porównywałeś może te dwa rozwiązania?
Ciekaw też jestem co przemawia za „ręcznym” includowaniem plików, zamiast automatycznym – w __autoload().
BTW. Niezły blog, buszuję po nim od kilku dni :)
Pozdrowienia
Witaj Kamilu, dziękuję za miłe słowa i także pozdrawiam. ;]
Nie robiłem żadnych testów, ale różnica w wydajności jest raczej niewielka – zauważ, że w obu przypadkach najbardziej „kosztowną” operacją będzie samo require_once() – wymaga ono odczytu bezpośrednio z dysku twardego, po prostu w przypadku autoloadingu masz jeszcze narzut na wywołanie samej funkcji loadera, który nawet w przypadku wielkich frameworków jest raczej pomijalny [co najwyżej kilkadziesiąt wywołań].
Za ręcznym ładowaniem plików według mnie przemawia… nic. Implementacja prostego autoloadera zgodnego z ideami standardu PSR-0 to cztery linijki kodu i zapominasz w ogóle, że coś się ładuje. Chyba, że faktycznie specyfika jakiejś struktury kodu będzie niemożliwa do „wtłoczenia” w ramy przyjętej konwencji – zawsze musimy sobie zostawić jakiś zapas bezpieczeństwa. ;]
Grep- Wyświetlanie linii otaczających znalezione wyniki