Od samego początku istnienia programowania, jedną z głównych idei wpajanych programistom jest modułowość. Każdy kto ma na swoim koncie kilka/kilkanaście aplikacji, na pewno stworzył gotowy zestaw funkcjonalności, powielany w kolejnych projektach. Problem pojawia się momencie, gdy trzeba stworzyć kolejny moduł lub zastosować zewnętrzną bibliotekę. Po pierwsze należy uważać, by nazwy wszystkich klas i funkcji były unikatowe. Prowadzi to do powstawania ogromnej ilości dziwnych, nic nie mówiących nazw. Po drugie, korzystając z zewnętrznych mechanizmów, musimy dopasować się do innego sposobu ładowania klas. Najlepiej widać to na przykładzie Zend Framework, gdzie dodanie biblioteki z niestandardowym ładowaniem klas, stanowi nie lada wyzwanie.
Na szczęście od PHP 5.3 można korzystać z przestrzeni nazw, co skutecznie eliminuje problem ze zduplikowanymi nazwami. Nie rozwiązuje to jednak problemu różnych sposobów ładowania klas. Lekarstwem na to jest phar oraz wspomniane wcześniej przestrzenie nazw. Moduł można “skompilować” do jednego pliku o rozszerzeniu phar, dzięki czemu ładowanie klas odbywa się w obrębie archiwum. Ponadto do każdego modułu można dołączyć inny mechanizm ładowania klas.
Zanim będzie można korzystać z modułów, trzeba wiedzieć jak je tworzyć. Posłuży w tym celu klasa Phar. Pakowanie skryptów PHP jest banalne i sprowadza się do 3 wierszy kodu.
$phar = new Phar('libs/db.phar', null, 'db.phar');
$phar->buildFromDirectory('libs/db');
$phar->setStub($phar->createDefaultStub('bootstrap.php', 'bootstrap.php'));
Pierwszy wiersz tworzy obiekt, który posłuży to stworzenia archiwum. Konstruktor przyjmuje 3 parametry: ścieżka i nazwa do archiwum, flagi, które zostaną przekazane go RecursiveDirectoryIterator, po którym dziedziczy klasa Phar oraz nazwę po jakiej archiwum będzie rozpoznawane. Wymagana jest tylko ścieżka.
W drugim kroku wskazywany jest katalog, na podstawie którego archiwum zostanie utworzone.
Na końcu ustawiany jest stub, czyli główny plik, który zostanie uruchomiony, w momencie dołączenia archiwum do skryptu lub uruchomienia go z wiersza poleceń. Plik ten można nazwać punktem wejścia. Jako argument metody setStub podany został wynik działania innej metody – createDefaultStub. Metoda ta jest odpowiedzialna za utworzenie głównego pliku. Ponieważ skrypty PHP można uruchamiać na dwa sposoby (z serwera jako aplikacje www oraz z wiersza poleceń), można stworzyć dwa różne pliki uruchamiane w zależności od środowiska.
Po wykonaniu powyższego skryptu, wszystko co znajduje się katalogu libs/db zostanie spakowane. Wystarczy teraz napisać prosty loader klas, który zostanie zamieszczony w pliku bootstrap (w tym przykładzie jest to główny plik archiwum) i możemy cieszyć się gotowym modułem.
Samo stworzenie modułu nie zagwarantuje, że nie powtórzą się nazwy klas lub funkcji. W celu nadania im unikalności, należy zastosować przestrzenie nazw (namespace). Przestrzenie nazw pomogą również napisać odpowiednie loadery klas. Manual dokładnie wyjaśnia zasady tworzenia i korzystania z przestrzeni nazw, więc nie będę tych informacji powielał. W skrócie napiszę, że przestrzeń nazw jest specjalnym obszarem, w którym nazwy klas oraz funkcji muszą być unikatowe. Oznacza to, że można stworzyć dwie klasy o takiej samej nazwie i nie spowoduje to błędu, o ile będą one znajdowały się w różnych przestrzeniach nazw. Dodanie przestrzeni nazw do każdego modułu zagwarantuje, że nie pojawią się błędy związane ze zduplikowanymi nazwami.
Najwyższa pora przejść do praktyki. Założenia są następujące. Aplikacja składa się z dwóch modułów. Moduły służą do komunikacją z bazą danych. Tak się składa, że jeden z nich obsługuje MySQL, a drugi Postgresa. Niestety nazwy klas są takie same, co powoduje konflikt nazw. Mimo, że klasy nazywają się tak samo, różni się struktura katalogów, co wymaga dwóch różnych loaderów. Drzewo aplikacji wygląda następująco:
Jeśli teraz dodalibyśmy do tego plik index.php, w którym chcielibyśmy skorzystać z obu modułów, otrzymalibyśmy ładny błąd informujący nas o zduplikowanej klasie. Poza tym jeśli mamy jakiś globalny loader klas, musimy stworzyć specjalne reguły, które uwzględnią różne ścieżki.
Kod klas wygląda dla db następująco:
// Klasa Connection z pliku Connection.php
class Connection
{
public function connect()
{
echo 'polaczono z baza mysql';
}
public function disconnect()
{
echo 'rozlaczono z baza mysql';
}
}
// Klasa Query z pliku Query.php
class Query
{
public function execute()
{
echo 'wykonano zapytanie mysql';
}
}
Dla uproszenia, klasy z db2 wyglądają identycznie.
Po dodaniu przestrzeni nasze klasy zmienią się do następującej postaci:
// Klasa Connection z pliku Connection.php
namespace Db\Mysql;
class Connection
{
public function connect()
{
echo 'polaczono z baza mysql';
}
public function disconnect()
{
echo 'rozlaczono z baza mysql';
}
}
// Klasa Query z pliku Query.php
namespace Db\Mysql;
class Query
{
public function execute()
{
echo 'wykonano zapytanie mysql<br />';
}
}
W przypadku db2 przestrzeń nazw nazywa się Db\Pgsql. Ok, problem ze zduplikowanymi nazwami został rozwiązany. Pozostaje jeszcze kwestia loadera. Wystarczy, że do db oraz db2 dodany zostanie plik bootstrap.php (nazwa może być inna, należy pamiętać aby ją zmienić w pliku tworzącym archiwum), w którym zawarta będzie logika loadera. Nie można zapomnieć o przestrzeni nazw.
namespace Db\Mysql;
class Loader
{
public static function load($name)
{
$parts = explode('\\', $name);
$file = array_pop($parts) . '.php';
require_once dirname(__FILE__) . '/mysql/' . $file;
}
}
spl_autoload_register('\Db\Mysql\Loader::Load');
Bootstrap.php dla db2 będzie się jedynie różnił katalogiem, z którego dołączane będą klasy oraz przestrzenią nazw w funkcji spl_autoloader_register.
Ostatnim elementem jest napisanie buildera archiwum.
// plik build.php
buildDbPhar();
buildDb2Phar();
echo 'ok';
function buildDbPhar()
{
$phar = new Phar('libs/db.phar', null, 'db.phar');
$phar->buildFromDirectory('libs/db');
$phar->setStub($phar->createDefaultStub('bootstrap.php', 'bootstrap.php'));
}
function buildDb2Phar()
{
$phar = new Phar('libs/db2.phar', null, 'db2.phar');
$phar->buildFromDirectory('libs/db2');
$phar->setStub($phar->createDefaultStub('bootstrap.php', 'bootstrap.php'));
}
Po uruchomieniu tego skryptu, w katalogu libs zostaną utworzone dwa pliki – db.phar oraz db2.phar. Wystarczy, że dołączymy oba do naszej aplikacji i możemy korzystać z niezależnych od siebie modułów.
Jeśli podczas tworzenia archiwum pojawi się błąd “Fatal error: Uncaught exception ‘UnexpectedValueException’ with message ‘Cannot write to archive – write operations restricted by INI setting’”, należy zmienić w pliku php.ini ustrawienia archiwum phar
phar.readonly = Off
A tak wygląd plik, który korzysta z gotowych modułów.
// index.php require_once 'libs/db.phar'; require_once 'libs/db2.phar'; use \Db\Pgsql as Baza; use \Db\Mysql as Baza2; $conn = new Baza\Connection(); $query = new Baza\Query(); $conn->connect(); $query->execute(); $conn->disconnect(); $conn2 = new Baza2\Connection(); $query2 = new Baza2\Query(); $conn2->connect(); $query2->execute(); $conn2->disconnect();
Informacje organizacyjne
Od dzisiejszego wpisu, wszystkie przykłady będę zamieszczał w repozytorium svn w serwisie code.google.com. Jeśli nie korzystacie z żadnego klienta svn, nie martwicie się. Kod można przeglądać na stronie projektu w zakładce Source, po wybraniu opcji Browse.
Wszystko spoko, ladnie i czytelnie opisane. Fajnie ze ktos opisal w koncu nowosci PHP5.3 z zywym przykladem.
A dlaczego SVN Google a nie Github?
~Anonimowy
Ponieważ nie znam ani wskazanego przez Ciebie serwisu ani Git-a.
Interesująco i treściwie – dzięki!
~Roomcays
Będzie więcej. Cierpliwości
Przyznam, że nie rozumiem do czego może być potrzebny Phar – do spakowania całego katalogu powiązanych klas w jeden plik? Jeżeli jest to jedyny argument to jest bardzo słaby moim zdaniem. Po każdej zmianie w klasach 'spakowanych' muszę przecież uruchomić skrypt pakujący na nowo :/
Poza tym, że umożliwia to uniknięcie sytuacji, w której wrzucasz na serwer setki, jeśli nie tysiące plików, możesz w pliku bootstrap.php dodać dowolną funkcjonalność, która ma się uruchomić podczas dołączania archiwum. Loader to tylko przykład. Można inicjować połączenie do bazy danych, generować cache, pobierać zdalną zawartość i wiele innych.
~Konradzik – załadowanie biblioteki spakowanej Phar, jeśli była ona mocno rozbudowana i rozdrobniona na wiele plików i katalogów będzie szybsze niż zainkludowanie wersji niespakowanej. Minusem może być debugowanie spakowanego kodu, ale zakładam, że pakuje się wersje stabilne i przetestowane.