Propel, najbardziej popularny system mapowania relacyjno-obiektowego w PHP skraca cykl produkcyjny aplikacji internetowej. Poradnik zawiera zestawienie popularnych problemów, z którymi borykają się początkujący użytkownicy Propela.
Porada 1: Kodowanie znaków
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 {stala}runtime-conf.xml{/stala} ustalamy kodowanie znaków dla połączenia:
...
utf8
Wygenerowane klasy będą stosowały podane kodowanie. Rozwiązanie takie ma dwie zalety:
- skrypty PHP korzystające z klas nie muszą zawierać żadnych instrukcji ustalających kodowanie,
- konfiguracja kodowania znaków serwera MySQL nie ma wpływu na skrypty.
Porada 2: Korzystanie z kilku baz danych
Skrypt PHP może – za pośrednictwem propelowych obiektów – łączyć się z wieloma bazami danych.
Przed uruchomieniem generatora klas (tj. skryptu {stala}propel-gen.bat{/stala}) w pliku {stala}runtime-conf.xml{/stala} należy wymienić wszystkie bazy danych:
mysql
mysqli
localhost
osoby
osobyadm
osobypass
utf8
mysql
mysqli
localhost
wyrazy
wyrazyadm
wyrazypass
utf8
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 dodamy wpis:
propel.packageObjectModel = true
zaś w plikach {stala}-schema.xml{/stala} umieścimy atrybut package:
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();
Porada 3: Pobieranie tylko wybranych kolumn
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.
Porada 4: Konwersje toArray(), fromArray()
Metody {stala}toArray(){/stala} oraz {stala}fromArray(){/stala} pozwalają na konwersje obiektów w tablice i na odwrót.
W celu przekształcenia obiektu {stala}$student{/stala}:
$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;
}
w tablicę wywołujemy metodę {stala}toArray(){/stala}:
$student = StudentPeer::retrieveByPK(2);
Jeśli jako parametr podamy stałą TYPE_FIELDNAME, to indeksami tablicy będą nazwy kolumn w bazie danych.
Konwersję odwrotną realizuje metoda {stala}fromArray(){/stala}:
$t = $student->toArray(BasePeer::TYPE_FIELDNAME);
Metody {stala}toArray(){/stala} oraz {stala}fromArray(){/stala} możemy wykorzystać w połączeniu z klasami XML_Serializer oraz XML_Unserializer. W ten sposób możemy:
- obiekty konwertować do XML-a ({stala}toArray(){/stala}, {stala}XML_Serializer{/stala}),
- na podstawie danych XML tworzyć obiekty ({stala}XML_Unserializer, fromArray(){/stala}).
Oto, jak przebiega konwersja obiektu na XML:
$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();
Porada 5: Wydawanie zapytań SQL
Niekiedy zachodzi konieczność wykonania konkretnych zapytań SQL. W takiej sytuacji należy wykorzystać statyczną metodę {stala}getConnection(){/stala}. Obiekt zwracany przez tę metodę pozwala na wysyłanie do serwera bazy danych zapytań w języku SQL:
$s = StudentPeer::retrieveByPK(1);
$t = $s->toArray(BasePeer::TYPE_FIELDNAME);
$serializer = new XML_Serializer();
$serializer->serialize($t);
$wynik = $serializer->getSerializedData();
Podane wyżej zapytanie wyznacza wartość towaru zapisanego w bazie danych (tj. sumę iloczynów: liczba sztuk x cena jednostki). Wykonanie takiego zadania za pośrednictwem obiektów byłoby znacznie bardziej czasochłonne.
Porada 6: Zliczanie
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 {stala}doCount(){/stala}.
Oto w jaki sposób możemy wyznaczyć liczbę wierszy zapisanych w bazie danych:
$con = Propel::getConnection(\'produkty\');
$sql = \'SELECT SUM(ilosc * cena) as wartosc FROM produkt\';
$rs = $con->executeQuery($sql);
$rs->next();
$wartosc = $rs->getString(\'wartosc\');
Porada 7: Stronicowanie
Stronicowanie wyników wykonujemy stosując metody {stala}setLimit(){/stala} oraz {stala}setOffset(){/stala} klasy {stala}Criteria{/stala}. Pierwsza z nich ustala liczbę zwracanych rekordów, a druga numer pierwszego zwracanego rekordu:
$ile = WyrazPeer::doCount(new Criteria);
Porada 8: Pobieranie jednego rekordu
Gdy zachodzi konieczność pobrania dokładnie jednego rekordu, przydatna okazuje się metoda {stala}doSelectOne(){/stala}. 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->setLimit(4);
$c->setOffset(2);
$wyrazy = WyrazPeer::doSelect($c);
Porada 9: Relacja 1:n, metoda getXs()
Obiekty połączone relacją 1:n są wyposażone w metody o nazwie {stala}getXs(){/stala}, 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:
$c = new Criteria();
$c->add(WyrazPeer::WYRAZ, \'lorem\');
$wyraz = WyrazPeer::doSelectOne($c);
if (!$wyraz) {
$wyraz = new Wyraz();
$wyraz->setWyraz($n);
$wyraz->save();
}
Metoda {stala}getWierszs(){/stala} zwraca wyniki nieuporządkowane. Możemy ją nadpisać w klasie Poeta, by w rezultacie otrzymać metodę {stala}getWierszs(){/stala}, 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:
$poeta = PoetaPeer::retrieveByPK(3);
foreach ($poeta->getWierszs() as $wiersz) {
echo $wiersz->getTytul();
}
Porada 10: Relacja n:m, metody {stala}getXHasYsJoinX(){/stala}, {stala}getXHasYsJoinY(){/stala}
Obiekty stojące w relacji n:m również mają metody dostępu do skorelowanych danych. Metody te nazywają się zgodnie ze schematem
$wiersz = WierszPeer::retrieveByPK(7);
echo $wiersz->getPoeta()->getImie();
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ę {stala}getFilmHasAktorsJoinAktor(){/stala}, a w klasie Aktor metodę {stala}getFilmHasAktorsJoinFilm(){/stala}. Metody te będą zwracały obiekty tabeli {stala}film_has_aktor{/stala}.
Oto skrypt drukujący tytuły wszystkich filmów, w których wystąpił aktor o identyfikatorze 1:
getZsJoinX() (w klasie X)
getZsJoinY() (w klasie Y)
oraz skrypt, który drukuje nazwiska wszystkich aktorów grających w filmie o identyfikatorze 7:
$aktor = AktorPeer::retrieveByPK(1);
$c = new Criteria();
$objs = $aktor->getFilmHasAktorsJoinFilm($c);
foreach ($objs as $obj) {
echo $obj->getFilm()->getTytul();
}
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 {stala}getFilmsByAktor(){/stala} (nowa metoda, ręcznie dodana w klasie Aktor), której użycie upraszcza kod skryptu:
$film = FilmPeer::retrieveByPK(7);
$c = new Criteria();
$objs = $film->getFilmHasAktorsJoinAktor($c);
foreach ($objs as $obj) {
echo $obj->getAktor()->getNazwisko();
}
Porada 14: Wyjątki
W przypadku wystąpienia błędów obiekty Propela generują wyjątki. Obsługę wyjątków realizujemy instrukcją try-catch. Metody obiektu {stala}$e{/stala} pozwalają poznać przyczynę błędu:
class Poeta extends BasePoeta {
function __toString()
{
return $this->getImie() . \' \' .
$this->getNazwisko();
}
}
class Wiersz extends BaseWiersz {
function __toString()
{
return $this->getTytul();
}
}
Porada 15: Sortowanie wyników wg wybranych kolumn
Porządek sortowania zwracanych wyników ustalamy korzystając z metod {stala}addAscendingOrderByColumn(){/stala} oraz {stala}addDescentingOrderByColumn(){/stala} klasy {stala}Criteria{/stala}:
$wyraz = new Wyraz();
$wyraz->setWyraz(\'żółw\');
try {
$wyraz->save();
} catch (PropelException $e) {
$c = $e->getCause();
$komunikat = $c->getNativeError();
echo $komunikat;
}
Uruchamianie przykładowych rozwiązań
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:
$c = new Criteria();
$c->addAscendingOrderByColumn(StudentPeer::NAZWISKO);
$c->addDescendingOrderByColumn(StudentPeer::IMIE);
$studenci = StudentPeer::doSelect($c);
W pliku {stala}tworz-baze-cpp.bat{/stala}, w miejsce napisu AX1BY2CZ3 umieszczamy hasło administratora serwera MySQL:
11-cala-zawartosc/
1-zrzut-db/
tworz-baze-cpp.bat
baza-cpp.sql
2-skrypt/
skrypt.php
Następnie uruchamiamy skrypt tworz-baze-cpp.bat, po czym – wykorzystując aplikację phpMyAdmin – sprawdzamy, czy baza danych o nazwie cpp została utworzona. Nazwę tworzonej bazy danych znajdziemy w skrypcie {stala}baza-cpp.sql{/stala}:
c:\mysql\bin\mysql -uroot -pAX1BY2CZ3 < baza-cpp.sql
Porada 11: Pobieranie zawartości całej bazy danych
Ciekawym efektem ubocznym metod {stala}getXs(){/stala} oraz {stala}getZsJoinX(){/stala} jest to, że jeden obiekt może dać dostęp do całej bazy danych.
Umieścimy 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:
$aktor = AktorPeer::retrieveByPK(1);
$c = new Criteria;
$filmy = $aktor->getFilmsByAktor($c);
foreach ($filmy as $film) {
...
}
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
metoda getRozdzials()
Rozdzial
metoda getZadanies()
Zadanie
zapewni dostęp do całej bazy danych. Podwójna pętla foreach wydrukuje całą książkę (wszystkie rozdziały i wszystkie zadania):
$ksiazka = KsiazkaPeer::retrieveByPK(1);
Porada 12: Smarty i wielokrotne wywoływanie metod
Szablony Smarty domyślnie nie pozwalają na wielokrotne wywoływanie metod. Instrukcje szablonu:
PRZYKŁAD NIEPOPRAWNY
foreach ($ksiazka->getRozdzials() as $rozdzial) {
echo $rozdzial->getTytul();
foreach ($rozdzial->getZadanies() as $zadanie) {
echo $zadanie->getTekst();
}
}
będą powodowały błąd. W celu ominięcia tego problemu możemy zmodyfikować klasę Smarty_Compiler. Jeśli w pliku {stala}Smarty_Compiler.class.php{/stala} wymienimy wyrażenie regularne zawarte w linijce 155 i w miejsce:
{$wiersz->getAutor()->getImie()}
wpiszemy:
...$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.
Porada 13: Metoda __toString()
Metoda {stala}__toString(){/stala} służy do konwersji obiektu na typ string. Jeśli wygenerowane klasy wzbogacimy o metody {stala}__toString(){/stala}, to będzie można stosować obiekty jako parametry instrukcji echo, np.:
..$this->_dvar_guts_regexp . \'(?:\(\))?)\';
Instrukcje:
$poeta = PoetaPeer::retrieveByPK(3);
echo $poeta;
foreach ($poeta->getWierszs() as $wiersz) {
echo $wiersz;
}
będą działały poprawnie, pod warunkiem że w klasach Poeta oraz Wiersz dodamy metody {stala}__toString(){/stala}:
echo $poeta;
echo $wiersz;