PHP Autoloader – automatyczne dołączanie klas przez spl_autoload_register() zamiast __autoload()

Pisząc nawet niewielkie aplikacje webowe często ilość klas rozrasta się do pokaźnej liczby. Rozwiązaniem problemu jest automatyczne dołączanie plików klas w momencie próby utworzenia obiektu danej klasy. Powszechnie stosuje się dwa rozwiązania: __autoload() oraz spl_autoload_register()

Aby możliwe było wykorzystanie którejś z funkcji należy sprawdzić czy są dostępne.
W przypadku funkcji __autoload() sprawa wydaje się prosta – przede wszystkim wymagane jest PHP 5.0 (nie jest dostępna w przypadku korzystania z PHP jako CLI). W przypadku spl_autoload_register() wymagane jest PHP w wesji >=5.1.2.

Główne różnice

__autoload() spl_autoload_register()
Wiele funkcji ładujących NIE TAK
Obsługa wyjątków NIE TAK

Możliwości rozwiązań

Możliwości rozwiązań może być tyle ilu programistów. Wszystko zależy od tego, czego wymagamy od projektu. Możemy skorzystać z prostego dołączania klas w jednej lokalizacji, rozbudować o większą liczbę lokalizacji, dodać możliwość zastosowania różnych postfiksów, sprawdzania czy klasa/interfejs zostały już wywołane, dodać możliwość przeszukiwania katalogów w głąb, stworzyć mapę klas i interfejsów i odwoływać się do nich jak do zwykłej tablicy. Możliwości jest na prawdę wiele.

Wersja super lite __autoload()

Jeśli mamy do czynienia z małą stroną, możemy ujednolicić nazewnictwo klas, przechowywać je w jednej lokalizacji i użyć najprostszego dołączania:

set_include_path($DOCUMENT_ROOT. 'classes');
// lub 
ini_set('include_path', $DOCUMENT_ROOT. 'classes');
function __autoload($classname) {
  require_once($classname.".class.php");
}

Jednak powyższe rozwiązanie może zostać wykorzystane tylko w bardzo prostych projektach, nie daje żadnej elastyczności, możliwości konfiguracji, zmian, narzuca szereg ograniczeń.

Wersja bardziej rozbudowana __autoload()

W przypadku korzystania chociażby z interfejsów w nazewnictwie interfejsA.interface.php poprzednia metoda już nie zadziała (zresztą nie zadziała w prawie żadnym wypadku poza podstawowym 😉 ). W pierwszej kolejności możemy rozszerzyć lokalizacje o dodatkowe katalogi:

set_include_path(
        get_include_path()
        .PATH_SEPARATOR."/classes/"
        .PATH_SEPARATOR."/interfaces/"
        .PATH_SEPARATOR."/modules/"
);

Dzięki temu pliki klas będą dołączane z wielu lokalizacji, nie tylko z jednej. Problem pojawia się w momencie próby użycia klasy, której nie ma. Rozszerzmy funkcję __autoload o dodatkowe funkcjonalności:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function __autoload($classname){
    try {
        if (class_exists($classname, false) || interface_exists($classname, false)){
            return;
        }
        $class = explode('_', strtolower(strval($classname)));
        $deeps = count($class);
        $file = 'class';
        for ($i=0;$i<$deeps;$i++){
            $file .= '/'.$class[$i];
        }
        $file .= '.class.php';
        if (!file_exists($file) || !is_readable($file) || class_exists($classname, false) || interface_exists($classname, false)){
            throw new Exception('Class cannot be found ( ' . $classname . ' )');
        }else{
            require_once($file);
        }
    } catch (Exception $e){
        echo $e->getMessage()."\n"; 
    }
}

Dodaliśmy kilka funkcjonalności:

  • Sprawdzanie czy klasa bądź interfejs zostały już zdefiniowane
  • Następnie dodaliśmy możliwość zagnieżdżania klas w głąb katalogów. Wymaganiem tutaj postawionym jest nazewnictwo klas z użyciem dolnego podkreślenia jako separatora katalogu
    katalog_nazwaKlasy.class.php lub katalog__podkatalog_nazwaKlasy.class.php
  • Sprawdzenie czy plik istnieje, czy jest możliwy do odczytu

Jednak nadal nie możemy przechwycić wyjątku blokiem try-catch, zatem przy stosowaniu tej metody wymagana jest pewność iż wszystkie wywoływane klasy mają swoje odpowiedniki w plikach.

Wersja lite spl_autoload_register()

Sam proces dołączania będzie identyczny jak wersji lite dla __autoload() z tą różnicą, że wywołanie loadera będzie inne.

1
2
3
4
5
6
7
8
9
function classLoader($classname){
    $file = $DOCUMENT_ROOT.'/class/'.$classname.'.class.php';
    if (file_exists($file) && is_readable($file) && !class_exists($classname, false)){
        require_once($file);
    }else{
        throw new Exception('Class cannot be found ( ' . $classname . ' )');
    }
}
spl_autoload_register('classLoader');

Możliwość korzystania z funkcji w spl_autoload_functions nie jest jedyną, którą mamy do dyspozycji. Równie dobrze, możemy skorzystać z klas. Prosty przykład na pokazanie zasady funkcjonowania:

class VLoader{
    public static function autoload($classname){
        // kod
    }
}
spl_autoload_register(array('VLoader', 'autoload'));

Wersja bardziej rozbudowana spl_autoload_register()

Możemy utworzyć oddzielne funkcje (metody) dla rożnych rodzajów danych. Dzięki temu, możemy dać zewnętrznym aplikacjom czy programistom możliwość utworzenia własnej funkcji automatycznego ładowania plików klas. Jest to rozwiązanie lepsze od poprzednich, nie ogranicza programistów piszących dodatki do naszej aplikacji do ścisłego przestrzegania narzuconej przez nas struktury i nazewnictwa klas.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$extensions = array('.php','.class.php','.interface.php');
$ext = implode(',',$extensions);
spl_autoload_register(null, false);
spl_autoload_extensions($ext);
 
function classLoader($classname){
    $extensions = array('.php','.class.php','.interface.php');
    if (class_exists($classname, false)){ return; }
    $class = explode('_', strtolower(strval($classname)));
    $deeps = count($class);
    $file = $DOCUMENT_ROOT;
    for ($i=0;$i<$deeps;$i++){
        $file .= '/'.$class[$i];
    }
    foreach ($extensions as $ext){
        $fileClass = $file.$ext;
        if (file_exists($fileClass) && is_readable($fileClass) && !class_exists($classname, false)){
            require_once($fileClass);
            return true;
        }
    }
    return false;
}
spl_autoload_register('classLoader');
/* kod funckji interfaceLoader */
spl_autoload_register('interfaceLoader');

W powyższym przykładzie wyzerowaliśmy wszystkie zarejestrowane autoloadery, następnie ustawiliśmy listę rozszerzeń możliwych do dopisania do plików. Dalej zmodyfikowaliśmy lekko poprzednią funkcję do ładowania klas. Jak widać na końcu wystarczy dopisać własną funkcję, która będzie przeszukiwać wskazany katalog i dołączyć ja poprzez spl_autoload_register(). Dzięki temu, zewnętrzny programista może uzupełnić aplikację przez swoje klasy, które zostaną automatycznie dodane.

Nie jest to rozwiązanie bez wad. Wymaga napisania przez takiego programistę swojego loadera, w dodatku nastręcza trudności w jego dołączeniu. Rozwiązaniem byłoby nakazanie stworzenia funkcji loadera w oddzielnym pliku oraz umieszczenia go w określonej lokalizacji, w celu późniejszego dołączenia go do aplikacji. Co w rezultacie narzuca kolejne ograniczenia, czasem nie mniejsze niż te, których chcieliśmy uniknąć na początku.

Problemy i wymagania

Zbierając wszystkie trudności napotkane w procesie tworzenia autoloadera tworzy nam się lista problemów i wymagań, których spełnienie da nam szansę stworzenia takiego mechanizmu, który będziemy mogli z łatwością wykorzystywać w różnych projektach.

  1. Uniknięcie konieczności użycia require, require_once
  2. Możliwość umieszczania klas w dowolnych lokalizacjach
  3. Połączenie z zewnętrznymi bibliotekami
  4. Szybkość działania
  5. Łatwość użycia
  6. Automatyzacja procesu
  7. Dołączanie tylko wymaganych klas
  8. Brak bądź niewielkie ograniczenia w nazewnictwie plików

Spora ta lista 😉 Jak możemy z tego wybrnąć? Otóż wymienione w treści wpisu metody nie będą miały zastosowania do tak zaawansowanych wymagań. Przemyślmy sytuację:
Ad. 1. Łatwizna, stosując system autoloadera automatycznie pozbywamy się tej konieczności. Jeżeli tylko spełnimy ograniczenia systemu.
Ad. 2. Abyśmy mogli umieszczać klasy w wielu lokalizacjach możemy dodawać ścieżki do nich w autoloaderze, bądź utworzyć kolejne funkcje dla nowych lokalizacji. Ewentualnością może być przeskanowanie wszystkich możliwych lokalizacji w poszukiwaniu wybranego pliku.
Ad. 3. W przypadku zewnętrznych bibliotek, zwykle wystarczy wywoływać główną klasę, która już później sama dołącza potrzebne jej dodatkowe klasy, interfejsy. Tak czy inaczej, jakoś musimy się dostać do tej głównej klasy. Pół biedy, jeśli główny plik klasy jest oddzielnie, a w katalogu pozostałe pliki, możemy wtedy taką paczkę wrzucić do katalogu klas i sprawa załatwiona. Gorzej natomiast z tymi, które są porozrzucane wszystkie w jednym folderze. Wtedy musimy przemyśleć opcję dodawania listy lokalizacji.
Ad. 4. Szybkość działania jest bardzo ważnym elementem w każdym systemie, szczególnie aplikacji internetowej. W tym punkcie występuje tu w dwóch aspektach: szybkość aplikacji, szybkość pracy z danym rozwiązaniem. Drugie rozumienie nie jest takie ważne, gdyż występuje tylko w momencie pisania aplikacji, ewentualnie w procesie aktualizacji i poprawek. Natomiast szybkość działania aplikacji ma już ogromne znaczenie. Gdyby przy każdej odsłonie strony, narzut na autoloaderze wynosił 1 sec, pomnożone przez tysiące odsłon generowałoby drastyczne spadki wydajności. Zatem nacisk na szybkość działania, wydajność jest bardzo ważny. Ważniejszy niż łatwość kodowania, szybkość pisania kodu, przenośność.
Ad. 5. Łatwość użycia, implementacji, przenośności kodu. Łatwość kodowania ma znaczenie dla użyteczności klasy od strony programisty. Dobrze jeśli ten, podczas kodowania nowych części aplikacji nie musiał dodatkowo pisać dodatkowych funkcji autolodera, bądź pilnować dodawania ścieżek do już istniejących. Najwygodniejszym rozwiązaniem jest informacja, że jest stosowany autoloader i nie trzeba zajmować się dodatkowym kodem, obsługą ładowania plików.
Ad. 6. Automatyzacja w tym przypadku występuje w rozumieniu braku konieczności ręcznych zmian w kodzie, dodawania ścieżek do katalogów, dopisywania własnych funkcji dla autoloadera. Aby to osiągnąć musimy stworzyć system na tyle uniwersalny, aby był w stanie sam dostosować się do różnic w aplikacji.
Ad. 7. Może wydawać się dziwne, jednak niektórzy proponują rozwiązanie, które przeskanuje wybrane lokalizacje i dołączy wszystkie pliki, tak aby ich późniejsze wykorzystanie ograniczyć tylko do utworzenia nowego obiektu.
Ad. 8. Nazewnictwo plików powinno być czytelne i wskazujące, jeśli nie jednoznaczne. Chociażby ze względu na wygodę programowania, późniejszej kontroli plików i kodu. Zatem jakieś wymagania musimy założyć. Może to być dodanie postfiksu class.php, interface.php, nazwa klasy w nazwie pliku. Dobrze by było, gdyby można było łączyć klasy w jeden plik, aby klasy na stale związane ze sobą mogły być zapisane w jednym pliku.

Rozwiązanie

Jedynym sensownym rozwiązaniem, spełniającym powyższe, jest utworzenie mapy klas zawierających parę kluczy nazwa klasy => ścieżka
Aby sprostać wymaganiom szybkości działania mapa taka powinna być tworzona tylko w przypadku zmian w układzie plików, bądź przenosin aplikacji do nowej lokalizacji, a w pozostałym czasie należy korzystać z już utworzonej mapy. Możemy ja cachować jako zserializowaną tablicę, zapisywać jako pliki ini, czy w inny, szybki i wygodny sposób. Dzięki temu najbardziej absorbująca praca jaką jest przeszukiwanie katalogów będzie wykonana bardzo rzadko, a w przypadku uruchomienia finalnej wersji aplikacji prawdopodobnie prawie nigdy.
Kwestią do rozwiązania pozostaje tylko na jakiej zasadzie tworzona by była taka mapa.

    Pomysłów jest kilka:

  • Stałe nazewnictwo w formie nazwa_klasy.class.php, nazwa_interfejsu.interface.php skanowanie plików, wybieranie z nazwy członu przed .class.php lub interface.php, i zapisywanie tej nazwy do tablicy.
    Wymaga stałego nazewnictwa, po jednej klasie, interfejsie na plik.
  • Przeskanowanie plików, otworzenie każdego z nich, wyszukanie w nim ciągu inicjującego klasę lub interfejs i ten ciąg doda do tablicy.
    Ma to swoje wady: konieczność otworzenia plików, przeszukania ich zawartości. Takie działanie na pewno nie wpłynie pozytywnie na czas pracy skryptu. Jednak ma też kilka zalet. Uniezależnia od konkretnego nazewnictwa, lokalizacji plików, daje możliwość umieszczenia kilku klas w jednym pliku. Warto oczywiście trzymać się jakiejś sensownej, logicznej struktury.

W drugiej części przedstawię próbę utworzenia takiego sytemu automatycznego logowania.

 

Przeczytaj także

Komentarze: 6

Dodaj komentarz »

 
 
 

Ja w swoich aplikacjach korzystam z wywołania własnej klasy ze statycznym API odpowiedzialnej za ładowanie. Na początku podaję jej wszystkie wymagane klasy [właśnie w formacie ‚klasa’ => ‚ścieżka’], a następnie w funkcji __autoload() wstawiam tylko wywołanie tego narzędzia dla określonej nazwy. Tak więc wielu funkcji ładujących nie potrzebuję, a wyjątki obsługuję sam. ;]

Problem może faktycznie się pojawić przy złączaniu różnych systemów, kiedy oba korzystają z własnych metod ładujących, bo niestety funkcja __autoload(), tak jak napisałeś, może być tylko jedna, więc chyba jednak zainteresuję się splspl_autoload_register(). Pozdrawiam!

 

Odpowiedz

 

Chyba literówka:

function classLoader($classname){
    $extensions = array('.php','.class.php','.interface.php');
    if (class_exists($class, false)){ return; }
    $classname = explode('_', strtolower(strval($class)));
...

z kąd bierzesz $class ? 😛

 

Odpowiedz

 

@Spawnm
A jakże, nie tylko tam gdzie wskazałeś 😉
Dzięki za info 🙂

 

Odpowiedz

 

Stosowanie class_exists w funkcji __autoload czy zarejestrowanej jako loader jest bez sensu ponieważ te funkcje z definicji są wywoływane tylko gdy klasa nie istnieje. Co więcej funkcja class_exists sama domyślnie wywołuje autoloadera by sprawdzić czy klasa istnieje ;>

 

Odpowiedz

 

@sokzzuka, tak class_exists bool class_exists ( string $nazwa_klasy [, bool $autoload ] ) domyślnie uruchamia __autoload, jednak można to wyłączyć poprzez ustawienie parametru autoload na false, tak jak w listingu powyżej:

class_exists($classname, false);
 

Odpowiedz

 

Dzięki! naprawdę bardzo przydatny artykuł 🙂

 

Odpowiedz

 

Dodaj komentarz

 
(nie będzie publikowany)
 
 
Komentarz
 
 

Dozwolone tagi XHTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
© 2009 - 2023 Vokiel.com
WordPress Theme by Arcsin modified by Vokiel