1 <?xml version="1.0" encoding="UTF-8"?>
2 <!-- EN-Revision: 21740 -->
4 <sect1 id="learning.quickstart.create-model">
5 <title>Utworzenie modelu oraz tabeli w bazie danych</title>
8 Przed rozpoczęciem należy zastanowić się nad następującym zagadnieniem: gdzie tworzone
9 klasy będą przechowywane i w jaki sposób będzie można je odnaleźć? Utworzony właśnie
10 domyślny projekt, używa automatycznego dołączania plików (autoloader). Możliwe jest
11 dołączenie do niego kolejnych autoloaderów tak by umożliwić odnajdywanie tworzonych
12 klas. Typowym rozwiązaniem jest umieszczenie różnych klas w głównym katalogu - w tym
13 wypadku nazwanego <filename>application/</filename> - i nazywanie ich z zachowaniem
18 Klasa <classname>Zend_Controller_Front</classname> umożliwia tworzenie modułów (modules)
19 - odrębnych części, które same w sobie są mini-aplikacjami. W ramach każdego
20 modułu odwzorowywana jest taka sama struktura katalogów jaka jest tworzona przez
21 narzędzie <command>zf</command> w katalogu głównym aplikacji. Nazwy wszystkich
22 klas w jednym module muszą rozpoczynać się od wspólnego prefiksu - nazwy modułu.
23 Katalog główny - <filename>application/</filename> również jest modułem (domyślnym)
24 dlatego też jego zasoby zostaną uwzględnione w procesie automatycznego dołączania plików.
28 <classname>Zend_Application_Module_Autoloader</classname> oferuje funkcjonalność
29 niezbędną do odwzorowania zasobów modułów na odpowiednie ścieżki katalogów oraz
30 ułatwia zachowanie spójnego standardu nazewnictwa. Instancja tej klasy jest tworzona
31 domyślnie podczas uruchamiania klasy bootstrap. Domyślnym prefiksem używanym przez
32 bootstrap jest "Application" więc modele, formularze oraz klasy tabel będą rozpoczynały
33 się prefiksem "Application_".
37 Teraz należy się zastanowić co składa się na księgę gości. Typowo będzie w niej
38 lista wpisów z <emphasis>komentarzem</emphasis>, <emphasis>czasem zapisu</emphasis>
39 oraz <emphasis>adresem email</emphasis>. Zakładając użycie bazy danych, pole
40 <emphasis>unikalny identyfikator</emphasis> może również być przydatne. Aplikacja
41 powinna umożliwiać zapis danych, pobieranie wpisów pojedynczo oraz wszystkich na raz.
42 Prosty model oferujący opisaną funkcjonalność może przedstawiać się następująco:
45 <programlisting language="php"><![CDATA[
46 // application/models/Guestbook.php
48 class Application_Model_Guestbook
55 public function __set($name, $value);
56 public function __get($name);
58 public function setComment($text);
59 public function getComment();
61 public function setEmail($email);
62 public function getEmail();
64 public function setCreated($ts);
65 public function getCreated();
67 public function setId($id);
68 public function getId();
71 class Application_Model_GuestbookMapper
73 public function save(Application_Model_Guestbook $guestbook);
74 public function find($id);
75 public function fetchAll();
80 Metody <methodname>__get()</methodname> oraz <methodname>__set()</methodname>
81 stanowią mechanizmy ułatwiające dostęp do poszczególnych właściwości oraz
82 pośredniczą w dostępie do innych getterów i setterów. Dzięki nim, można
83 również upewnić się, że jedynie pożądane właściwości będą dostępne.
87 Metody <methodname>find()</methodname> oraz <methodname>fetchAll()</methodname> umożliwiają
88 zwrócenie pojedynczego lub wszystkich rekordów natomiast <methodname>save()</methodname>
93 W tym momencie można zacząć myśleć o skonfigurowaniu bazy danych.
97 Na początku należy zainicjować zasób <classname>Db</classname>. Jego konfiguracja
98 jest możliwa na podobnych zasadach jak w przypadku zasobów
99 <classname>Layout</classname> oraz <classname>View</classname>.
100 Można to osiągnąć za pomocą polecenia <command>zf configure db-adapter</command>:
103 <programlisting language="shell"><![CDATA[
104 % zf configure db-adapter \
105 > 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook.db"' \
107 A db configuration for the production has been written to the application config file.
109 % zf configure db-adapter \
110 > 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook-testing.db"' \
112 A db configuration for the production has been written to the application config file.
114 % zf configure db-adapter \
115 > 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook-dev.db"' \
117 A db configuration for the production has been written to the application config file.
121 Teraz, po otworzeniu pliku <filename>application/configs/application.ini</filename>
122 można zobaczyć instrukcje, jakie zostały do niego dodane w odpowiednich sekcjach.
125 <programlisting language="ini"><![CDATA[
126 ; application/configs/application.ini
130 resources.db.adapter = "PDO_SQLITE"
131 resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook.db"
133 [testing : production]
135 resources.db.adapter = "PDO_SQLITE"
136 resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-testing.db"
138 [development : production]
140 resources.db.adapter = "PDO_SQLITE"
141 resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-dev.db"
145 Ostatecznie plik konfiguracyjny powinien wyglądać następująco:
148 <programlisting language="ini"><![CDATA[
149 ; application/configs/application.ini
152 phpSettings.display_startup_errors = 0
153 phpSettings.display_errors = 0
154 bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
155 bootstrap.class = "Bootstrap"
156 appnamespace = "Application"
157 resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
158 resources.frontController.params.displayExceptions = 0
159 resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
161 resources.db.adapter = "PDO_SQLITE"
162 resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook.db"
164 [staging : production]
166 [testing : production]
167 phpSettings.display_startup_errors = 1
168 phpSettings.display_errors = 1
169 resources.db.adapter = "PDO_SQLITE"
170 resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-testing.db"
172 [development : production]
173 phpSettings.display_startup_errors = 1
174 phpSettings.display_errors = 1
175 resources.db.adapter = "PDO_SQLITE"
176 resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-dev.db"
180 Należy zauważyć, iż baza (bazy) danych będzie przechowywana w katalogu
181 <filename>data/db/</filename>.
182 Te katalogi powinny zostać utworzone i udostępnione wszystkim. W systemach
183 unix można tego dokonać następująco:
186 <programlisting language="shell"><![CDATA[
187 % mkdir -p data/db; chmod -R a+rwX data
191 W systemach Windows należy utworzyć odpowiednie katalogi w eksploratorze
192 oraz ustawić uprawnienia w taki sposób aby każdy użytkownik miał prawo zapisu.
196 Połączenie z bazą danych zostało utworzone - w tym przypadku bazą jest Sqlite
197 znajdująca się w katalogu <filename>application/data/</filename>. Następnym krokiem jest
198 utworzenie tabeli przechowującej rekordy księgi gości.
201 <programlisting language="sql"><![CDATA[
202 -- scripts/schema.sqlite.sql
204 -- Poniższy kod SQL należy uruchomić w bazie danych
206 CREATE TABLE guestbook (
207 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
208 email VARCHAR(32) NOT NULL DEFAULT 'noemail@test.com',
210 created DATETIME NOT NULL
213 CREATE INDEX "id" ON "guestbook" ("id");
217 Aby mieć zestaw danych do testowania można utworzyć w tabeli kilka rekordów.
220 <programlisting language="sql"><![CDATA[
221 -- scripts/data.sqlite.sql
223 -- Poniższy kod SQL może posłużyć do zapełnienia tabeli testowymi danymi
225 INSERT INTO guestbook (email, comment, created) VALUES
226 ('ralph.schindler@zend.com',
227 'Hello! Hope you enjoy this sample zf application!',
229 INSERT INTO guestbook (email, comment, created) VALUES
231 'Baz baz baz, baz baz Baz baz baz - baz baz baz.',
236 Mamy zdefiniowany schemat bazy danych oraz niewielką ilość danych do zaimportowania.
237 Teraz można napisać skrypt tworzący bazę danych. Ten krok nie jest potrzebny w środowisku
238 produkcyjnym. Dzięki niemu można lokalnie wypracować odpowiednią strukturę bazy danych
239 aby tworzona aplikacja działała zgodnie z założeniami. Skrypt
240 <filename>scripts/load.sqlite.php</filename> można wypełnić w poniższy sposób:
243 <programlisting language="php"><![CDATA[
244 // scripts/load.sqlite.php
247 * Skrypt tworzący bazę danych i wypełniający ją danymi
250 // Inicjalizacja ścieżek oraz autoloadera
251 defined('APPLICATION_PATH')
252 || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
253 set_include_path(implode(PATH_SEPARATOR, array(
254 APPLICATION_PATH . '/../library',
257 require_once 'Zend/Loader/Autoloader.php';
258 Zend_Loader_Autoloader::getInstance();
260 // Zdefiniowanie opcji CLI
261 $getopt = new Zend_Console_Getopt(array(
262 'withdata|w' => 'Load database with sample data',
263 'env|e-s' => 'Application environment for which to create database (defaults to development)',
264 'help|h' => 'Help -- usage message',
268 } catch (Zend_Console_Getopt_Exception $e) {
269 // Bad options passed: report usage
270 echo $e->getUsageMessage();
274 // W przypadku zażądania pomocy, wyświetlenie informacji o użyciu
275 if ($getopt->getOption('h')) {
276 echo $getopt->getUsageMessage();
280 // Inicjalizacja wartości na podstawie opcji CLI
281 $withData = $getopt->getOption('w');
282 $env = $getopt->getOption('e');
283 defined('APPLICATION_ENV')
284 || define('APPLICATION_ENV', (null === $env) ? 'development' : $env);
286 // Inicjalizacja Zend_Application
287 $application = new Zend_Application(
289 APPLICATION_PATH . '/configs/application.ini'
292 // Inicjalizacja oraz zwrócenie zasobu bazy danych
293 $bootstrap = $application->getBootstrap();
294 $bootstrap->bootstrap('db');
295 $dbAdapter = $bootstrap->getResource('db');
297 // Powiadomienie użytkownika o postępie (w tym miejscu jest tworzona baza danych)
298 if ('testing' != APPLICATION_ENV) {
299 echo 'Writing Database Guestbook in (control-c to cancel): ' . PHP_EOL;
300 for ($x = 5; $x > 0; $x--) {
301 echo $x . "\r"; sleep(1);
305 // Sprawdzenie czy plik bazy danych istnieje
306 $options = $bootstrap->getOption('resources');
307 $dbFile = $options['db']['params']['dbname'];
308 if (file_exists($dbFile)) {
312 // Wywołanie poleceń zawartych w pliku tworzącym schemat
314 $schemaSql = file_get_contents(dirname(__FILE__) . '/schema.sqlite.sql');
315 // bezpośrednie użycie obiektu połączenia w celu wywołania poleceń SQL
316 $dbAdapter->getConnection()->exec($schemaSql);
317 chmod($dbFile, 0666);
319 if ('testing' != APPLICATION_ENV) {
321 echo 'Database Created';
326 $dataSql = file_get_contents(dirname(__FILE__) . '/data.sqlite.sql');
327 // bezpośrednie użycie obiektu połączenia w celu wywołania poleceń SQL
328 $dbAdapter->getConnection()->exec($dataSql);
329 if ('testing' != APPLICATION_ENV) {
335 } catch (Exception $e) {
336 echo 'AN ERROR HAS OCCURED:' . PHP_EOL;
337 echo $e->getMessage() . PHP_EOL;
341 // Ten skrypt powinien zostać uruchomiony z wiersza poleceń
346 Teraz należy wywołać powyższy skrypt. Można to zrobić z poziomu terminala lub
347 wiersza poleceń poprzez wpisanie następującej komendy:
350 <programlisting language="shell"><![CDATA[
351 % php scripts/load.sqlite.php --withdata
355 Powinien pojawić się następujący komunikat:
358 <programlisting language="text"><![CDATA[
359 path/to/ZendFrameworkQuickstart/scripts$ php load.sqlite.php --withdata
360 Writing Database Guestbook in (control-c to cancel):
367 Po zdefiniowaniu bazy danych aplikacji księgi gości można przystąpić do budowy kodu
368 samej aplikacji. W następnych krokach zostanie zbudowana klasa dostępu do danych
369 (poprzez <classname>Zend_Db_Table</classname>), oraz klasa mapująca - służąca do
370 połączenia z wcześniej opisanym modelem.
371 Na koniec utworzony zostanie kontroler zarządzający modelem, którego zadaniem będzie
372 wyświetlanie istniejących rekordów oraz obróbka nowych danych.
376 Aby łączyć się ze źródłem danych użyty zostanie wzorzec
377 <ulink url="http://martinfowler.com/eaaCatalog/tableDataGateway.html">Table Data
378 Gateway</ulink> udostępniany poprzez klasę <classname>Zend_Db_Table</classname>.
379 Na początek należy utworzyć klasę opartą o <classname>Zend_Db_Table</classname>.
380 Podobnie jak przy layoucie oraz adapterze bazy danych - można skorzystać z narzędzia
381 <command>zf</command> i jego komendy <command>create db-table</command>. Należy przy tym
382 podać minimalnie dwa argumenty: nazwę tworzonej klasy oraz nazwę tabeli bazy danych,
386 <programlisting language="shell"><![CDATA[
387 % zf create db-table Guestbook guestbook
388 Creating a DbTable at application/models/DbTable/Guestbook.php
389 Updating project profile 'zfproject.xml'
393 Spoglądając na strukturę katalogów należy zwrócić uwagę na nowy katalog
394 <filename>application/models/DbTable/</filename> zawierający plik
395 <filename>Guestbook.php</filename>. Ten plik powinien zawierać następującą
399 <programlisting language="php"><![CDATA[
400 // application/models/DbTable/Guestbook.php
403 * This is the DbTable class for the guestbook table.
405 class Application_Model_DbTable_Guestbook extends Zend_Db_Table_Abstract
408 protected $_name = 'guestbook';
413 Należy zwrócić uwagę na prefiks: <classname>Application_Model_DbTable</classname>.
414 Prefiks klas aplikacji "Application" znajduje się na pierwszym miejscu. Po nim
415 występuje komponent "Model_DbTable", który jest mapowany do katalogu
416 <filename>models/DbTable/</filename> znajdującego się w module.
420 Jedyne dane niezbędne przy tworzeniu klasy pochodnej w stosunku do
421 <classname>Zend_Db_Table</classname> to nazwa tabeli i opcjonalnie klucz pierwotny
422 (jeśli jest inny niż "id").
426 Teraz należy utworzyć klasę mapującą obiekt w aplikacji na obiekt w bazie danych czyli
427 <ulink url="http://martinfowler.com/eaaCatalog/dataMapper.html">Data Mapper</ulink>.
428 Obiektem w bazie danych jest <classname>Application_Model_Guestbook</classname> natomiast
429 za obiekt bazy danych odpowiada <classname>Application_Model_DbTable_Guestbook</classname>.
430 Typowe API takiej klasy wygląda następująco:
433 <programlisting language="php"><![CDATA[
434 // application/models/GuestbookMapper.php
436 class Application_Model_GuestbookMapper
438 public function save($model);
439 public function find($id, $model);
440 public function fetchAll();
445 Dodatkowo można zdefiniować metody ustawiające i zwracające Table Data Gateway.
446 Do utworzenia klasy można użyć narzędzia <command>zf</command>:
449 <programlisting language="shell"><![CDATA[
450 % zf create model GuestbookMapper
451 Creating a model at application/models/GuestbookMapper.php
452 Updating project profile '.zfproject.xml'
456 Po otwarciu klasy <classname>Application_Model_GuestbookMapper</classname> z lokalizacji
457 <filename>application/models/GuestbookMapper.php</filename> widać następujący kod:
460 <programlisting language="php"><![CDATA[
461 // application/models/GuestbookMapper.php
463 class Application_Model_GuestbookMapper
467 public function setDbTable($dbTable)
469 if (is_string($dbTable)) {
470 $dbTable = new $dbTable();
472 if (!$dbTable instanceof Zend_Db_Table_Abstract) {
473 throw new Exception('Invalid table data gateway provided');
475 $this->_dbTable = $dbTable;
479 public function getDbTable()
481 if (null === $this->_dbTable) {
482 $this->setDbTable('Application_Model_DbTable_Guestbook');
484 return $this->_dbTable;
487 public function save(Application_Model_Guestbook $guestbook)
490 'email' => $guestbook->getEmail(),
491 'comment' => $guestbook->getComment(),
492 'created' => date('Y-m-d H:i:s'),
495 if (null === ($id = $guestbook->getId())) {
497 $this->getDbTable()->insert($data);
499 $this->getDbTable()->update($data, array('id = ?' => $id));
503 public function find($id, Application_Model_Guestbook $guestbook)
505 $result = $this->getDbTable()->find($id);
506 if (0 == count($result)) {
509 $row = $result->current();
510 $guestbook->setId($row->id)
511 ->setEmail($row->email)
512 ->setComment($row->comment)
513 ->setCreated($row->created);
516 public function fetchAll()
518 $resultSet = $this->getDbTable()->fetchAll();
520 foreach ($resultSet as $row) {
521 $entry = new Application_Model_Guestbook();
522 $entry->setId($row->id)
523 ->setEmail($row->email)
524 ->setComment($row->comment)
525 ->setCreated($row->created);
534 W obecnym momencie można przystąpić do utworzenia klasy modelu używając
535 polecenia <command>zf create model</command>:
538 <programlisting language="shell"><![CDATA[
539 % zf create model Guestbook
540 Creating a model at application/models/Guestbook.php
541 Updating project profile '.zfproject.xml'
545 Nowo utworzoną klasę <acronym>PHP</acronym> można zmodyfikować tak aby
546 ułatwić umieszczanie danych
547 w modelu poprzez przekazanie tablicy do konstruktora lub do metody
548 <methodname>setOptions()</methodname>. Ostatecznie model znajdujący się w
549 <filename>application/models/Guestbook.php</filename> powinien wyglądać następująco:
552 <programlisting language="php"><![CDATA[
553 // application/models/Guestbook.php
555 class Application_Model_Guestbook
562 public function __construct(array $options = null)
564 if (is_array($options)) {
565 $this->setOptions($options);
569 public function __set($name, $value)
571 $method = 'set' . $name;
572 if (('mapper' == $name) || !method_exists($this, $method)) {
573 throw new Exception('Invalid guestbook property');
575 $this->$method($value);
578 public function __get($name)
580 $method = 'get' . $name;
581 if (('mapper' == $name) || !method_exists($this, $method)) {
582 throw new Exception('Invalid guestbook property');
584 return $this->$method();
587 public function setOptions(array $options)
589 $methods = get_class_methods($this);
590 foreach ($options as $key => $value) {
591 $method = 'set' . ucfirst($key);
592 if (in_array($method, $methods)) {
593 $this->$method($value);
599 public function setComment($text)
601 $this->_comment = (string) $text;
605 public function getComment()
607 return $this->_comment;
610 public function setEmail($email)
612 $this->_email = (string) $email;
616 public function getEmail()
618 return $this->_email;
621 public function setCreated($ts)
623 $this->_created = $ts;
627 public function getCreated()
629 return $this->_created;
632 public function setId($id)
634 $this->_id = (int) $id;
638 public function getId()
646 W ostatnim kroku, aby połączyć wszystkie elementy, należy utworzyć kontroler,
647 którego zadaniem będzie zaprezentowanie listy zapisanych rekordów oraz obsługa
648 dodawania nowych danych.
652 Aby to osiągnąć należy użyć polecenia <command>zf create controller</command>:
655 <programlisting language="shell"><![CDATA[
656 % zf create controller Guestbook
657 Creating a controller at
658 application/controllers/GuestbookController.php
659 Creating an index action method in controller Guestbook
660 Creating a view script for the index action method at
661 application/views/scripts/guestbook/index.phtml
662 Creating a controller test file at
663 tests/application/controllers/GuestbookControllerTest.php
664 Updating project profile '.zfproject.xml'
668 Powyższe polecenie tworzy nowy kontroler - <classname>GuestbookController</classname>
669 w pliku <filename>application/controllers/GuestbookController.php</filename> zawierający
670 jedną akcję - <methodname>indexAction()</methodname>.
671 Na użytek tego kontrolera utworzony zostaje również katalog widoków:
672 <filename>application/views/scripts/guestbook/</filename> zawierający skrypt
673 widoku dla akcji index.
677 Akcja "index" będzie stanowić domyślny punkt kontrolera pokazujący zapisane rekordy.
681 Teraz należy zaprogramować logikę aplikacji. Aby pokazać zapisane rekordy użytkownikowi
682 wchodzącemu do <methodname>indexAction()</methodname> można użyć poniższego kodu:
685 <programlisting language="php"><![CDATA[
686 // application/controllers/GuestbookController.php
688 class GuestbookController extends Zend_Controller_Action
690 public function indexAction()
692 $guestbook = new Application_Model_GuestbookMapper();
693 $this->view->entries = $guestbook->fetchAll();
699 Dodatkowo potrzebny jest jeszcze widok wyświetlający dane. W pliku
700 <filename>application/views/scripts/guestbook/index.phtml</filename> można umieścić
704 <programlisting language="php"><![CDATA[
705 <!-- application/views/scripts/guestbook/index.phtml -->
707 <p><a href="<?php echo $this->url(
709 'controller' => 'guestbook',
713 true) ?>">Sign Our Guestbook</a></p>
715 Guestbook Entries: <br />
717 <?php foreach ($this->entries as $entry): ?>
718 <dt><?php echo $this->escape($entry->email) ?></dt>
719 <dd><?php echo $this->escape($entry->comment) ?></dd>
725 <title>Punkt kontrolny</title>
728 Teraz, po przejściu do "http://localhost/guestbook" powinna się pojawić lista
733 <inlinegraphic width="525" scale="100" align="center" valign="middle"
734 fileref="figures/learning.quickstart.create-model.png" format="PNG" />
739 <title>Użycie skryptu ładującego dane</title>
742 Skrypt ładujący dane pokazany we wcześniejszej części tego rozdziału
743 (<filename>scripts/load.sqlite.php</filename>) może zostać użyty do utworzenia
744 bazy danych jak i do zaimportowania przykładowych danych dla każdego środowiska.
745 Wewnętrznie korzysta z klasy <classname>Zend_Console_Getopt</classname>, dzięki
746 czemu możliwe jest podanie parametrów sterujących skryptem. Podając parametr
747 "-h" lub "--help" można zapoznać się z dostępnymi opcjami:
750 <programlisting language="php"><![CDATA[
751 Usage: load.sqlite.php [ options ]
752 --withdata|-w Load database with sample data
753 --env|-e [ ] Application environment for which to create database
754 (defaults to development)
755 --help|-h Help -- usage message)]]
759 Parametr "-e" pozwala na nadanie wartości stałej <constant>APPLICATION_ENV</constant>
760 określającej środowisko, co z kolei umożliwia utworzenie bazy danych SQLite dla
761 każdego środowiska oddzielnie. Należy się upewnić, że skrypt jest uruchamiany z
762 odpowiednią wartością tego parametru dla każdego ze środowisk.