Propel, najbardziej popularny system mapowania relacyjno-obiektowego w PHP, w sposób bardzo istotny skraca cykl produkcyjny aplikacji internetowej. Poradnik zawiera zestawienie popularnych problemów, z jakimi borykają się początkujący użytkownicy Propel-a.
07/01/08
Klasy dostępu do bazy danych mogą stosować dowolne kodowanie znaków, niezależne od ustawień serwera.
Przed uruchomieniem generatora Propel (tj. skryptu propel-gen.bat) w pliku runtime-conf.xml ustalamy kodowanie znaków dla połączenia:
<connection> ... <encoding>utf8</encoding> </connection>
Wygenerowane klasy będą stosowały podane kodowanie. Rozwiązanie takie ma dwie zalety:
Skrypt PHP może — za pośrednictwem propelowych obiektów — łączyć się z wieloma bazami danych.
Przed uruchomieniem generatora klas (tj. skryptu propel-gen.bat) w pliku runtime-conf.xml należy wymienić wszystkie bazy danych:
<datasources default="osoby">
<datasource id="osoby">
<adapter>mysql</adapter>
<connection>
<phptype>mysqli</phptype>
<hostspec>localhost</hostspec>
<database>osoby</database>
<username>osobyadm</username>
<password>osobypass</password>
<encoding>utf8</encoding>
</connection>
</datasource>
<datasource id="wyrazy">
<adapter>mysql</adapter>
<connection>
<phptype>mysqli</phptype>
<hostspec>localhost</hostspec>
<database>wyrazy</database>
<username>wyrazyadm</username>
<password>wyrazypass</password>
<encoding>utf8</encoding>
</connection>
</datasource>
</datasources>
Dla każdego wymienionego źródła danych przygotowujemy osobny plik XML z opisem struktury bazy, np.
wyrazy-schema.xml osoby-schema.xml
Tak skonfigurowany Propel wygeneruje plik konfiguracyjny -conf.php, umożliwiający łączenie się obiektów z wieloma bazami, przy czym wszystkie wygenerowane klasy trafią do jednego folderu.
Jeśli w pliku build.properties dodasz wpis:
propel.packageObjectModel = true
zaś w plikach -schema.xml umieścisz atrybut package:
<database package="wyrazy" name="wyrazy" ... >
to generowane klasy zostaną umieszczone w osobnych folderach. Atrybut package zawierający kropkę:
package="core.system"
spowoduje dalszy podział generowanych folderów na podfoldery:
core/system
W skrypcie PHP, który korzysta z kilku połączeń należy najpierw wywołać metodę init():
Propel::init('dwiebazy-conf.php');
a następnie utworzyć zmienne umożliwiające korzystanie z połączeń:
$con_osoby = Propel::getConnection('osoby');
$con_wyrazy = Propel::getConnection('wyrazy');
Metody pobierające rekordy z baz danych otrzymają dodatkowy parametr ustalający połączenie:
$wyrazy = WyrazPeer::doSelect(new Criteria, $con_wyrazy); $osoby = OsobaPeer::doSelect(new Criteria, $con_osoby);
Pierwsza z powyższych instrukcji pobiera dane z bazy o nazwie wyrazy, a druga — z bazy o nazwie osoby.
Utworzone obiekty nie wymagają podawania połączenia. Korzystamy z nich identycznie jak w skryptach, które stosowały jedną bazę danych:
echo $osoby[0]->getImie()
$wyrazy[0]->setWyraz('Lorem');
$wyrazy[0]->save();
Obiekty tworzone na podstawie informacji zapisanych w bazie, np. metodą retrieveByPK(), pobierają z bazy danych wszystkie kolumny. To prowadzi do dużych nieoptymalności. Wyświetlenie tytułów artykułów (np. lista nowości na stronie) będzie powodowało pobieranie kompletnych artykułów.
Kolumny, które powinny być pobrane, możemy wskazać korzystając z metod klasy Criteria(). Utworzona poniżej tablica $studenci zawiera tylko imiona studentów:
$c = new Criteria();
$c->clearSelectColumns();
$c->addSelectColumn(StudentPeer::IMIE);
$rs = StudentPeer::doSelectRS($c);
$studenci = array();
while($rs->next()) {
$tmp = array(
'imie' => $rs->get(1),
);
$studenci[] = $tmp;
}
Tak wygenerowane wyniki nie mogą być wykorzystane do operacji save() czy delete(), gdyż nie są obiektami, a stringami.
Kod zwracający wyłącznie wybrane kolumny dodajemy w postaci nowych metod do wygenerowanych klas. Metody te mogą przyjmować parametr klasy Criteria, który pozwoli na wskazywanie wybranych wierszy i sortowanie wyników.
Metody toArray() oraz fromArray() pozwalają na konwersje obiektów w tablice i na odwrót.
W celu przekształcenia obiektu $student:
$student = StudentPeer::retrieveByPK(2);
w tablicę wywołujemy metodę toArray():
$t = $student->toArray(BasePeer::TYPE_FIELDNAME);
Jeśli jako parametr podamy stałą TYPE_FIELDNAME, to indeksami tablicy będą nazwy kolumn w bazie danych.
Konwersję odwrotną realizuje metoda fromArray():
$t = array( 'imie' => 'Tomasz', 'nazwisko' => 'Nijaki', 'plec' => 'M', 'wiek' => '33', 'numerindeksu' => '00000000001', 'kierunek' => 'marketing', ); $s2 = new Student(); $s2->fromArray($t, BasePeer::TYPE_FIELDNAME); $s2->save();
Metody toArray() oraz fromArray() możemy wykorzystać w połączeniu z klasami XML_Serializer oraz XML_Unserializer. W ten sposób możemy:
Oto, jak przebiega konwersja obiektu na XML:
$s = StudentPeer::retrieveByPK(1); $t = $s->toArray(BasePeer::TYPE_FIELDNAME); $serializer = new XML_Serializer(); $serializer->serialize($t); $wynik = $serializer->getSerializedData();
Niekiedy zachodzi konieczność wykonania konkretnych zapytań SQL. W takiej sytuacji należy wykorzystać statyczną metodę getConnection(). Obiekt zwracany przez tę metodę pozwala na wysyłanie do serwera bazy danych zapytań w języku SQL:
$con = Propel::getConnection('produkty');
$sql = 'SELECT SUM(ilosc * cena) as wartosc FROM produkt';
$rs = $con->executeQuery($sql);
$rs->next();
$wartosc = $rs->getString('wartosc');
Podane wyżej zapytanie wyznacza wartość towaru zapisanego w bazie danych (tj. sumę iloczynów: liczba sztuk * cena jednostki). Wykonanie takiego zadania za pośrednictwem obiektów byłoby znacznie bardziej czasochłonne.
Szczególnym przypadkiem zapytań SQL jest zliczanie rekordów. W tym celu nie musimy jednak uciekać się do języka SQL, gdyż generowane klasy zawierają metody doCount().
Oto, w jaki sposób możemy wyznaczyć liczbę wierszy zapisanych w bazie danych:
$ile = WyrazPeer::doCount(new Criteria);
Stronicowanie wyników wykonujemy stosując metody setLimit() oraz setOffset() klasy Criteria. Pierwsza z nich ustala liczbę zwracanych rekordów, a druga — numer pierwszego zwracanego rekordu:
$c = new Criteria(); $c->setLimit(4); $c->setOffset(2); $wyrazy = WyrazPeer::doSelect($c);
Gdy zachodzi konieczność pobrania dokładnie jednego rekordu, przydatna okazuje się metoda doSelectOne(). Z sytuacją taką mamy do czynienia np. wtedy, gdy chcemy do bazy danych wstawić rekord, pod warunkiem, że takiego rekordu nie było. Należy najpierw wyszukać rekord, a następnie użyć instrukcji if do zbadania, czy obiekt został utworzony:
$c = new Criteria();
$c->add(WyrazPeer::WYRAZ, 'lorem');
$wyraz = WyrazPeer::doSelectOne($c);
if (!$wyraz) {
$wyraz = new Wyraz();
$wyraz->setWyraz($n);
$wyraz->save();
}
Obiekty połączone relacją 1:n są wyposażone w metody, o nazwie getXs() udostępniające dane stojące w relacji. Litera X w nazwie metody jest zastępowana nazwą odpowiedniej tabeli.
Jeśli tabele poeta i wiersz połączymy relacją 1:n (każdy poeta może być autorem wielu wierszy), to w klasie Poeta pojawi się metoda getWierszs(). Metoda ta będzie zwracała obiekty klasy Wiersz:
Oto skrypt drukujący tytuły wierszy poety o identyfikatorze 3:
$poeta = PoetaPeer::retrieveByPK(3);
foreach ($poeta->getWierszs() as $wiersz) {
echo $wiersz->getTytul();
}
Metoda getWierszs() zwraca wyniki nieuporządkowane. Możemy ją nadpisać w klasie Poeta, by w rezultacie otrzymać metodę getWierszs(), która zachowując pełną funkcjonalność domyślnie zwraca wyniki posortowane alfabetycznie.
W drugą stronę korzystamy z odwołań kaskadowych. Oto jak ustalić nazwisko poety, który napisał wiersz o identyfikatorze 7:
$wiersz = WierszPeer::retrieveByPK(7); echo $wiersz->getPoeta()->getImie();
Obiekty stojące w relacji n:m również mają metody dostępu do skorelowanych danych. Metody te nazywają się zgodnie ze schematem
getZsJoinX() (w klasie X) getZsJoinY() (w klasie Y)
gdzie Z jest nazwą tabeli haszującej, a X oraz Y — nazwami tabel połączonych relacją.
Jeśli tabele film oraz aktor połączymy relacją n:m i tabelę haszującą nazwiemy film_has_aktor, to Propel wygeneruje trzy klasy: Film, Aktor oraz FilmHasAktor. W klasie Film znajdziemy metodę getFilmHasAktorsJoinAktor(), a w klasie Aktor — metodę getFilmHasAktorsJoinFilm(). Metody te będą zwracały obiekty tabeli film_has_aktor.
Oto skrypt drukujący tytuły wszystkich filmów, w których wystąpił aktor o identyfikatorze 1:
$aktor = AktorPeer::retrieveByPK(1);
$c = new Criteria();
$objs = $aktor->getFilmHasAktorsJoinFilm($c);
foreach ($objs as $obj) {
echo $obj->getFilm()->getTytul();
}
oraz skrypt, który drukuje nazwiska wszystkich aktorów grających w filmie o identyfikatorze 7:
$film = FilmPeer::retrieveByPK(7);
$c = new Criteria();
$objs = $film->getFilmHasAktorsJoinAktor($c);
foreach ($objs as $obj) {
echo $obj->getAktor()->getNazwisko();
}
Korzystając z powyższych metod możemy opracować własne metody, których wyniki, będą obiektami klas Film lub Aktor. Przykładem takiej metody jest getFilmsByAktor() (nowa metoda, ręcznie dodana w klasie Aktor), której użycie upraszcza kod skryptu:
$aktor = AktorPeer::retrieveByPK(1);
$c = new Criteria;
$filmy = $aktor->getFilmsByAktor($c);
foreach ($filmy as $film) {
...
}
Ciekawym efektem ubocznym metod getXs() oraz getZsJoinX() jest to, że jeden obiekt może dać dostęp do całej bazy danych.
Umieśćmy w bazie danych książki podzielone na rozdziały, które z kolei zawierają zadania. Otrzymamy trzy tabele: ksiazka, rozdzial oraz zadanie. Relacją 1:n łączymy tabele ksiazka i rozdzial (każda książka zawiera wiele rozdziałów) oraz rozdzial i zadanie (każdy rozdział zawiera wiele zadań). Propel wygeneruje klasy:
Ksiazka metoda getRozdzials() Rozdzial metoda getZadanies() Zadanie
Jeśli w bazie danych znajduje się jeden rekord o identyfikatorze 1 (zbiór zadań z programowania w języku C++), to wywołanie:
$ksiazka = KsiazkaPeer::retrieveByPK(1);
zapewni dostęp do całej bazy danych. Podwójna pętla foreach wydrukuje całą książkę (wszystkie rozdziały i wszystkie zadania):
foreach ($ksiazka->getRozdzials() as $rozdzial) {
echo $rozdzial->getTytul();
foreach ($rozdzial->getZadanies() as $zadanie) {
echo $zadanie->getTekst();
}
}
Szablony Smarty domyślnie nie pozwalają na wielokrotne wywoływanie metod. Instrukcje szablonu:
PRZYKŁAD NIEPOPRAWNY
{$wiersz->getAutor()->getImie()}
będą powodowały błąd. W celu ominięcia tego problemu możemy zmodyfikować klasę Smarty_Compiler. Jeśli w pliku Smarty_Compiler.class.php wymienisz wyrażenie regularne zawarte w linijce 155 i w miejsce:
...$this->_dvar_guts_regexp . ')';
wpiszesz:
..$this->_dvar_guts_regexp . '(?:\(\))?)';
wielokrotne wywołanie metod będzie działało poprawnie.
Powyższa niedogodność sytemu Smarty jest na tyle dokuczliwa, że rozsądnym wydaje się rezygnacja z szablonów Smarty na rzecz surowych szablonów PHP.
Metoda __toString() służy do konwersji obiektu na typ string. Jeśli wygenerowane klasy wzbogacisz o metody __toString(), to będziesz mógł stosować obiekty jako parametry instrukcji echo, np.:
$poeta = PoetaPeer::retrieveByPK(3);
echo $poeta;
foreach ($poeta->getWierszs() as $wiersz) {
echo $wiersz;
}
Instrukcje:
echo $poeta; echo $wiersz;
będą działały poprawnie, pod warunkiem, że w klasach Poeta oraz Wiersz dodasz metody __toString():
class Poeta extends BasePoeta {
function __toString()
{
return $this->getImie() . ' ' .
$this->getNazwisko();
}
}
class Wiersz extends BaseWiersz {
function __toString()
{
return $this->getTytul();
}
}
W przypadku wystąpienia błędów, obiekty Propel-a generują wyjątki. Obsługę wyjątków realizujemy instrukcją try-catch. Metody obiektu $e pozwalają poznać przyczynę błędu:
$wyraz = new Wyraz();
$wyraz->setWyraz('żółw');
try {
$wyraz->save();
} catch (PropelException $e) {
$c = $e->getCause();
$komunikat = $c->getNativeError();
echo $komunikat;
}
Porządek sortowania zwracanych wyników ustalamy, korzystając z metod addAscendingOrderByColumn() oraz addDescentingOrderByColumn() klasy Criteria:
$c = new Criteria(); $c->addAscendingOrderByColumn(StudentPeer::NAZWISKO); $c->addDescendingOrderByColumn(StudentPeer::IMIE); $studenci = StudentPeer::doSelect($c);
Każda z powyższych porad jest zilustrowana przykładami. Pojedynczy przykład składa się ze skryptów tworzących bazę danych (pliki .sql i .bat) oraz ze skryptów PHP, które pobierają dane z bazy i prezentują je w postaci strony WWW.
Uruchamianie każdego przykładu należy rozpocząć od utworzenia bazy danych. Następnie przeglądarką odwiedzamy stronę skrypt.php.
Oto jak przebiega uruchomienie przykładu jedenastego. W folderze 11-cala-zawartosc/ znajdziemy dwa podfoldery, a w nich pliki:
11-cala-zawartosc/
1-zrzut-db/
tworz-baze-cpp.bat
baza-cpp.sql
2-skrypt/
skrypt.php
W pliku tworz-baze-cpp.bat, w miejsce napisu AX1BY2CZ3 umieść hasło administratora serwera MySQL:
c:\mysql\bin\mysql -uroot -pAX1BY2CZ3 < baza-cpp.sql
Następnie uruchom skrypt tworz-baze-cpp.bat, po czym — wykorzystując aplikację phpMyAdmin — sprawdź, czy baza danych o nazwie cpp została utworzona. Nazwę tworzonej bazy danych znajdziesz w skrypcie baza-cpp.sql:
... DROP DATABASE IF EXISTS cpp; ...
Powinieneś otrzymać bazę danych cpp wstępnie wypełnioną pewną liczbą rekordów, co ilustruje rysunek 1.
Rysunek 1. Sprawdzanie poprawności tworzenia bazy danych o nazwie cpp programem phpMyAdmin
Następnie przeglądarką WWW odwiedź plik skrypt.php. Ujrzysz stronę przedstawioną na rysunku 2.
Rysunek 2. Przykład 11: zbiór zadań z podstaw programowania w języku C++
Szczegóły rozwiązania poznasz analizując plik skrypt.php.
| lp. | Przykład |
|---|---|
| 1. | Propel. Porady. Przykładowe skrypty |
Tabela 1. Przykłady do pobrania
Tabela 2. Adresy