PHP/Automatyczne ładowanie

< PHP
Poprzedni rozdział: Iteratory
Następny rozdział: Ćwiczenia

Automatyczne ładowanie edytuj

Wraz z rozbudową projektu pojawia się w nim coraz więcej plików i bibliotek, które musimy dołączać. W PHP 4 i wcześniejszych wersjach początek większych skryptów prezentował się mniej więcej następująco:

<?php

define('DIR_INC', './includes/');

require(DIR_INC.'config.php');
require(DIR_INC.'base.php');
require(DIR_INC.'validation.php');
require(DIR_INC.'forms.php');
require(DIR_INC.'database.php');
require(DIR_INC.'i18n.php');
require(DIR_INC.'controller.php');
require(DIR_INC.'helpers.php');
// itd.

Oczywiście nie trzeba wielkiej filozofii, by zauważyć, że zarządzanie taką listą jest niezwykle trudne, a przy tym niewydajne. Nie wszystkie podstrony muszą przecież korzystać z całej funkcjonalności oferowanej przez silnik strony, w związku z czym ładowanie wszystkiego to niepotrzebna strata czasu. Remedium na te bolączki był wprowadzony w PHP 5.0.0 mechanizm automatycznego ładowania klas. Jego działanie jest bardzo proste. W momencie gdy próbujemy utworzyć obiekt nieznanej klasy, PHP wywołuje specjalną, napisaną przez nas funkcję, której zadaniem jest odnalezienie i załadowanie pliku zawierającego tę klasę. Dopiero gdy nie zostanie on znaleziony, generowany jest błąd.

Automatyczne ładowanie ułatwia życie programiście. Nie musimy już zastanawiać się czy dana klasa została załadowana czy jeszcze nie, a gdyby nie - szukać miejsca, w którym można ją bezpiecznie załadować. Z naszej perspektywy kod wygląda tak, jakby PHP miał już "z definicji" załadowane wszystkie możliwe klasy. Co więcej, ponieważ ładujemy tylko to, czego faktycznie potrzebujemy, możemy często zyskać na wydajności, mimo iż musimy przy okazji wykonać operację tłumaczenia nazwy klasy na ścieżkę do pliku.

Automatyczne ładowanie - PHP 5.0 edytuj

Pierwotny mechanizm automatycznego ładowania wprowadzony w PHP 5.0 był prosty, lecz dość prymitywny. Wszystko, co musieliśmy zrobić, to utworzyć magiczną funkcję o nazwie __autoload(), która jako argument pobierała nazwę klasy do załadowania. Utwórzmy najpierw dwa pliki testowe:

Klasa1.php:

<?php
class Klasa1
{
   public function __construct()
   {
      echo 'Tworzę obiekt klasy Klasa1<br/>';
   } // end __construct();
} // end Klasa1;

Klasa2.php:

<?php
class Klasa2
{
   public function __construct()
   {
      echo 'Tworzę obiekt klasy Klasa2<br/>';
   } // end __construct();
} // end Klasa2;

Nazwa pliku jednoznacznie mówi nam, jaka klasa się w nim znajduje, zatem pliki mogą zawierać tylko po jednej klasie bądź interfejsie. Spróbujmy teraz napisać skrypt, który będzie potrafił ładować je automatycznie:

<?php
function __autoload($className)
{
   echo 'Ładuję klasę '.$className.'<br/>';
   require('./'.$className.'.php');
} // end __autoload();

// Możemy od razu korzystać z klas
$obiekt1 = new Klasa1;
$obiekt2 = new Klasa2;
$obiekt3 = new Klasa1;

Wynikiem działania naszego skryptu powinno być:

Ładuję klasę Klasa1
Tworzę obiekt klasy Klasa1
Ładuję klasę Klasa2
Tworzę obiekt klasy Klasa2
Tworzę obiekt klasy Klasa1

W linijce 9 od razu próbujemy utworzyć obiekt klasy Klasa1 bez jej uprzedniego ładowania. PHP szybko odkrywa, że nie ma jej jeszcze w swojej pamięci, dlatego wywołuje funkcję __autoload(), prosząc ją o jej odnalezienie. Funkcja wyświetla na ekranie informację, że coś ładuje, a następnie dokleja do nazwy klasy rozszerzenie i ścieżkę, a całość przesyła do instrukcji require. Gdy dwie linijki niżej próbujemy ponownie utworzyć obiekt tej samej klasy, PHP wie już, jak ona działa i nie musi nic doładowywać, tylko od razu przystępuje do tworzenia obiektu.

Automatyczne ładowanie - SPL edytuj

Funkcja __autoload() ma jedną, ale bardzo poważną wadę. Wyobraźmy sobie sytuację, w której korzystamy z dwóch bibliotek rozwijanych przez niezależne ekipy programistów. Każda biblioteka posiada swój własny mechanizm ładowania klas, a my także w naszym projekcie chcemy mieć jeszcze jeden. Występuje konflikt, ponieważ w pamięci nie może być trzech funkcji __autoload(), lecz co najwyżej jedna. Pojawia się konflikt nazw między bibliotekami.

Problem szybko dostrzegli również twórcy PHP i już w wersji 5.1 w znanym już nam pakiecie Standard PHP Library pojawił się nowy mechanizm automatycznego ładowania, który umożliwia równoczesną pracę kilku ładowarek. Jeśli zdecydujemy się na jego wykorzystanie, PHP automatycznie wyłączy funkcję __autoload(). Wykorzystujmy te same przykładowe klasy, zmieniając jedynie korzystający z nich skrypt:

<?php
function myClassLoader($className)
{
   echo 'Ładuję klasę '.$className.'<br/>';
   require('./'.$className.'.php');

   // Zwróć "true", informując, że klasa została znaleziona.
   return true;
} // end myClassLoader();

spl_autoload_register('myClassLoader');

// Możemy od razu korzystać z klas
$obiekt1 = new Klasa1;
$obiekt2 = new Klasa2;
$obiekt3 = new Klasa1;

Kod powinien dać dokładnie taki sam wynik, jak poprzednio, jednak w jego budowie jest zasadnicza różnica. Nasza funkcja ładująca może mieć teraz dowolną nazwę, jednak przez to musimy poinformować PHP, że ma ją wykorzystywać. Służy do tego funkcja spl_autoload_register(), do której podajemy nazwę funkcji jako argument.

Jak wspomnieliśmy, w projekcie może być użyte kilka ładowarek. Razem tworzą one stos. Zauważmy, że nasza funkcja zwraca wartość true. To informacja dla PHP, że plik został znaleziony. Jeśli nasza funkcja zwróci co innego, PHP wywoła kolejny autoloader i tak aż do skutku lub wyczerpania zapasu. Założenie jest takie, że funkcja ładująca powinna po nazwie klasy rozpoznać czy potrafi ją załadować i jeśli nie, natychmiast zakończyć działanie, by przekazać sterowanie innej. Możemy dodać do naszych klas unikalne prefiksy, np. MojProjekt i funkcją strpos() sprawdzać czy są one obecne:

<?php
function myClassLoader($className)
{
   if(strpos($className, 'MojProjekt') !== 0)
   {
      // Te klase obsluguje jakas inna ladowarka - nie ma prefiksu!
      return false;
   }
   echo 'Ładuję klasę '.$className.'<br/>';
   require('./'.$className.'.php');

   // Zwróć "true", informując, że klasa została znaleziona.
   return true;
} // end myClassLoader();

Teraz zadanie dla Ciebie. Dodaj do klas oraz ich plików prefiks MojProjekt, utwórz analogiczne klasy z prefiksem InnyProjekt i napisz dla nich drugą ładowarkę. Zarejestruj ją obok pierwszej, użyj nowych klas i obserwuj działanie skryptu.

Jeżeli jakaś ładowarka nagle nam przeszkadza, możemy ją w każdej chwili usunąć funkcją spl_autoload_unregister().

Ładowarki obiektowe edytuj

Ładowarki SPL nie muszą być funkcjami, ale także metodami klas. Umożliwia to budowę całkiem przyjemnego i łatwego w konfiguracji interfejsu wielokrotnego użytku do ładowania klas. Zamienimy teraz nasze ładowarki z poprzedniego przykładu na takie właśnie klasy, w których będziemy mogli ustawić ścieżki dostępu oraz rozpoznawane prefiksy.

<?php
class Loader
{
   private $_prefix;
   private $_directory;
   private $_valid = false;

   public function __construct($prefix, $directory)
   {
      $this->_prefix = (string)$prefix;
      if(!is_dir($directory))
      {
         throw new RuntimeException($directory.' is not a valid directory path.');
      }
      $this->_directory = $directory;
      $this->_valid = true;
   } // end __construct();

   public function autoload($className)
   {
      if(strpos($className, $this->_prefix) !== 0)
      {
         return false;
      }
      require($this->_directory.$className.'.php');
      return true;
   } // end autoload();

   public function register()
   {
      if($this->_valid)
      {
         spl_autoload_register(array($this, 'autoload'));
         $this->_valid = false; // aby się nie dało drugi raz zarejestrować
      }
   } // end register();
} // end Loader;

$firstLoader = new Loader('MojProjekt', './MojProjekt/');
$secondLoader = new Loader('InnyProjekt', './InnyProjekt/');

$firstLoader->register();
$secondLoader->register();

// Tutaj reszta skryptu

Cały proces rejestracji ukryty jest teraz wewnątrz klasy ładowarki. Argumenty takie, jak prefiks czy katalog, przechowywane są w obiekcie, który dzięki metodzie register() potrafi się sam zarejestrować jako ładowarka SPL, wywołując w linii 33 funkcję spl_autoload_register(). Rejestrowanie metod realizujemy poprzez przekazanie jako argument tablicy z obiektem oraz nazwą metody.

Nasza ładowarka ma charakter uniwersalny - za pomocą tego samego kodu zainicjowanego różnymi parametrami możemy obsługiwać teraz dwie różne biblioteki znajdujące się w różnych katalogach. Spróbuj przy jej pomocy załadować klasy z poprzedniego rozdziału.

Standard nazewnictwa klas edytuj

W dużych projektach niezbędne jest przestrzeganie pewnych konwencji, aby nie pogubić się we własnym kodzie. Jedna z nich może dotyczyć sposobu nadawania klasom nazwy. Także i w tym podręczniku stosowaliśmy jednolitą konwencję cechującą się używaniem angielskich słów oraz rozpoczynaniem każdego z nich dużą literą, np. FormInput czy ConfigLoader. Na nasze potrzeby wystarczała ona dotąd w zupełności, ale zauważmy, że klasa ConfigLoader może być użyta w wielu różnych bibliotekach. Gdybyśmy chcieli użyć ich jednocześnie, wystąpiłby konflikt nazw. Co więcej, nie jest to jedyna konwencja i inni programiści mogą swoje klasy nazywać inaczej. Utrudnia to tworzenie przenośnych ładowarek, a także wprowadza zamieszanie w kodzie, gdy część klas nazywa się np. Foo_Bar_Joe, drugą część FooBarJoe, a trzecią jeszcze inaczej.

Problem dostrzegli niedawno programiści pracujący przy największych projektach PHP i powołali stowarzyszenie, którego celem było rozwiązanie tej sytuacji i wypracowanie jednolitego standardu nazewnictwa. Po przyjęciu, miał on być stopniowo wdrożony we wszystkich projektach. Zapoznamy się teraz z jego założeniami i od tego momentu będziemy go konsekwentnie stosować w dalszej części podręcznika.

Zgodnie z założeniami nowego standardu, nazwy klas składają się z co najmniej dwóch członów oddzielonych od siebie znakiem _ lub \ (separator przestrzeni nazw, które poznamy już wkrótce). Nazwa każdego członu pisana jest w konwencji FooBar. Pierwszy człon powinien zawsze określać tzw. dostawcę, czyli np. nazwę projektu lub grupy programistycznej odpowiedzialnej za jego rozwój. Przypuśćmy, że tworzymy projekt o nazwie "Ala". Wtedy nasze klasy powinny dostać nazwy:

  1. Ala_Klasa1
  2. Ala_Klasa2
  3. Ala_Klasa3
  4. Ala_JakisElement_Klasa1
  5. Ala_JakisElement_Klasa2
  6. itd.

Podkreślenia pełnią bardzo ważną rolę, ponieważ podczas ładowania są zamieniane na separator katalogów w ścieżce do pliku. Oznacza to, że klasa Ala_Klasa1 będzie znajdować się w pliku Ala/Klasa1.php, a Ala_JakisElement_Klasa1 - w Ala/JakisElement/Klasa1.php. Jak widać, możemy w ten sposób nasze pliki grupować w podkatalogi. Ładowarka po nazwie dostawcy może rozpoznać czy dana klasa należy do niej i w jakim katalogu powinna szukać plików.

Pełen tekst standardu można znaleźć na liście dyskusyjnej Grupy Standaryzacyjnej PHP (j. angielski).

Uniwersalne ładowarki edytuj

Zauważmy, że standard uwzględnia sytuację, w której korzystamy z kilku różnych bibliotek. Jeśli ich konwencja nazewnictwa jest identyczna, szybko dochodzimy do wniosku, że można by stworzyć jedną, uniwersalną ładowarkę podobną do tej, którą napisaliśmy wyżej, ale uwzględniającą potrzeby wszystkich projektów. Takie ładowarki faktycznie powstają i najczęściej wchodzą w skład różnych projektów.

Oprócz tego, także sama grupa standaryzacyjna PHP rozpoczęła opracowywanie domyślnej ładowarki, która w przyszłości ma szansę stać się częścią pakietu SPL, a co za tym idzie - być obecna w każdym interpreterze PHP. Obecnie dostępna jej jest implementacja w PHP, a także rozszerzenie w języku C do samodzielnej kompilacji jako moduł PHP. Jej użycie jest bardzo proste:

<?php
$classLoader = new SplClassLoader('Ala', '/ścieżka/do/projektu/Ala');
$classLoader->register();

Zakończenie edytuj

Automatyczne ładowanie klas jest powszechnie spotykane w niemal wszystkich nowoczesnych projektach i bibliotekach programistycznych napisanych w PHP. Rozwiązuje problem zależności między poszczególnymi klasami, zapanowania nad ich ładowaniem, a dzięki dodatkowej standaryzacji może być także remedium na konflikty nazw. Pamiętaj, że automatyczne ładowanie działa wyłącznie dla klas oraz interfejsów. Nie da się przy jego pomocy ładować zwyczajnych funkcji, tak więc decydując się na jego użycie, najlepiej będzie w całości przejść na programowanie obiektowe i zrezygnować z ich stosowania.

To był już ostatni temat związany bezpośrednio z programowaniem obiektowym w języku PHP. W rozdziale tym poznaliśmy po kolei wszystkie elementy tego paradygmatu programowania dostępne w języku, od podstawowych pojęć takich, jak klasy, poprzez dziedziczenie aż do iteratorów i automatycznego ładowania. Jest to duża dawka materiału, i to niezbyt łatwego. O programowaniu obiektowym powstają grube książki, a jego pełną potęgę można poznać dopiero dzięki praktyce. Nie zrażaj się, jeśli zapomniałeś niektórych rzeczy lub wciąż nie do końca rozumiesz, jak wykorzystać je w praktyce. Niemal cała dalsza część podręcznika będzie bazować na poznanych tu informacjach, dlatego będzie jeszcze sporo okazji do przećwiczenia wszystkiego. Nie obawiaj się wracania do tych rozdziałów; z pewnością Ci się jeszcze przydadzą. Tymczasem przed nami zestaw ćwiczeń, które już teraz pomogą Ci utrwalić zdobyte informacje.