Retrieved from: http://ruben.savanne.be/articles/integrating-zend-framework-and-doctrine/nl

Zend Framework en Doctrine integreren

Last Modified: November 30, 2008 (13:11), Keywords: Doctrine, PHP, ZendFramework

Dit artikel zal je de vereiste stappen uitleggen om een project met zowel Zend Framework en Doctrine op te zetten. Stap voor stap zullen we een eenvoudige message board applicatie bouwen.

Note: This article has been translated: Dutch (by myself), French (by Fred Blanc), German (by Mario Guenterberg) and Russian (by Oleg Lobach). I cannot guarantee that they are up-to-date, heck I can't even read some of them, but do check them out. Many thanks to the translators!

Voor we beginnen

Ik heb dit artikel zo simpel mogelijk gehouden, maar het is geen inleiding tot beide technologieën. Ik stel voor dat je met ze allebei apart experimenteert vooraleer je ze probeert te combineren. Allebei hebben ze goede documentatie om mee te starten: Zend Framework Quick Start en Doctrine's My First Project. Akra's Zend Framework Tutorial is ook een heel goede inleiding.

Het Zend Framework heeft een use-at-will architectuur. Dit wil zeggen dat je enkel de componenten dient ge gebruiken die je wil. Dit in tegenstelling tot andere frameworks, waar het meer een alles-of-niets keuze is. Deze use-at-will architectuur is geweldig: het laat ons toe om volwaardige Zend Framework applicaties te ontwikkelen, zonder de ZF database abstractie (Zend_Db) te gebruiken. Zend_Db is zeker geen slechte technologie, het sluit echter nog zeer nauw aan bij de onderliggende database (en is dus nogal low-level). Door Doctrine te gebruiken kan je gegevens manipuleren met gewone objecten, zonder je al te veel zorgen te hoeven maken om de database.

Zend Framework geeft je heel wat vrijheid in hoe je je applicaties bouwt. Met andere woorden: het dwingt je geen vaste structuur te volgen. In dit artikel heb ik geprobeerd de voorgestelde standaard structuur nauwgelet te volgen. Dit is echter gewoon een kwestie van smaak.

Laten we beginnen!

Eerst zullen we de standaard project structuur aanmaken en de bibliotheken installeren. Open een file manager en maak een mappenstructuur aan zoals degene die je hieronder ziet. Ik zal het doel van elk van deze mappen zo dadelijk uitleggen.

Basis mappen structuur
Basis mappen structuur

Een heleboel mappen, maar de meeste zouden je bekend moeten zijn als je reeds een Zend Framework applicatie gemaakt hebt. Er zijn een paar zaken anders:

De volgende stap is Zend Framework en Doctrine installeren. Download de nieuwste versies van de respectievelijke websites en unzip de library (voor ZF) en lib (voor Doctrine) mappen in de mappen die we net aangemaakt hebben. Het resultaat zou er als volgt moeten uitzien:

Zend Framework en Doctrine geïnstalleerd
Zend Framework en Doctrine geïnstalleerd

Tijd om te bootstrappen

Als je je de Zend Framework Quick Start goed herinnert, dan weet je zeker nog dat we een bootstrap.file moeten maken. Dat zullen we nu doen, met een paar wijzigingen om Doctrine te kunnen gebruiken.

Eerst maken we de public/index.php en public/.htaccess bestanden. Start je favoriete editor en kopieer de stukken hieronder (noot: je kan de volledige broncode ook verkrijgen via git, zie de appendix onderaan voor meer details):

public/index.php
  1. <?php
  2. require '../application/bootstrap.php';
public/.htaccess
  1. RewriteEngine on
  2. RewriteCond %{SCRIPT_FILENAME} !-f
  3. RewriteRule ^(.*)$ index.php/$1

Zoals je kan zien zijn deze hetzelfde als elke andere Zend Framework applicatie. Het application/bootstrap.php bestand ziet er ietwat anders uit. Ik heb het opgesplitst in twee bestanden: application/bootstrap.php en application/global.php. Het eerste handelt alle verzoeken af, het tweede zorgt dat de vereiste bestanden gelanden zijn. Ik heb deze bestanden opgesplitst omdat de code van global.php ook vereist is in het Doctrine command line script (dat we zo dadelijk zullen bekijken).

application/global.php
  1. <?php
  2. error_reporting(E_ALL | E_STRICT);
  3. ini_set('display_startup_errors', 1);
  4. ini_set('display_errors', 1);
  5. date_default_timezone_set('Europe/Brussels');
  6.  
  7. /*
  8.  * Setup libraries & autoloaders
  9.  */
  10. set_include_path(dirname(__FILE__).'/../library/zendframework'
  11.         . PATH_SEPARATOR . dirname(__FILE__).'/../library/doctrine'
  12.         . PATH_SEPARATOR . dirname(__FILE__).'/models'
  13.         . PATH_SEPARATOR . dirname(__FILE__).'/models/generated'
  14.         . PATH_SEPARATOR . get_include_path());
  15. require 'Zend/Loader.php';
  16. Zend_Loader::registerAutoload('Zend_Loader');
  17.  
  18. /*
  19.  * Set super-global data
  20.  */
  21. Doctrine_Manager::connection("mysql://user:pass@localhost/database");
  22.  
  23. /*
  24.  * Configure Doctrine
  25.  */
  26. Zend_Registry::set('doctrine_config', array(
  27.         'data_fixtures_path'  =>  dirname(__FILE__).'/doctrine/data/fixtures',
  28.         'models_path'         =>  dirname(__FILE__).'/models',
  29.         'migrations_path'     =>  dirname(__FILE__).'/doctrine/migrations',
  30.         'sql_path'            =>  dirname(__FILE__).'/doctrine/data/sql',
  31.         'yaml_schema_path'    =>  dirname(__FILE__).'/doctrine/schema'
  32.         ));
application/bootstrap.php
  1. require dirname(__FILE__).'/global.php';
  2.  
  3. Zend_Controller_Front::run(dirname(__FILE__).'/controllers');

We zullen dit even stap voor stap overlopen:

  1. error_reporting(E_ALL | E_STRICT);
  2. ini_set('display_startup_errors', 1);
  3. ini_set('display_errors', 1);
  4. date_default_timezone_set('Europe/Brussels');

Het is altijd een goed idee om error handling en een tijdzone in te stellen. Niets speciaal hier.

  1. /*
  2.  * Setup libraries & autoloaders
  3.  */
  4. set_include_path(dirname(__FILE__).'/../library/zendframework'
  5.         . PATH_SEPARATOR . dirname(__FILE__).'/../library/doctrine'
  6.         . PATH_SEPARATOR . dirname(__FILE__).'/models'
  7.         . PATH_SEPARATOR . dirname(__FILE__).'/models/generated'
  8.         . PATH_SEPARATOR . get_include_path());
  9. require 'Zend/Loader.php';
  10. Zend_Loader::registerAutoload('Zend_Loader');

Hier sluiten we Doctrine aan. Zoals je kan zien stellen we het include path in zodat het zowel Zend Framework als Doctirne bevat. We voegen ook de mappen waar Doctrine de model files zal genereren toe. Merk op dat we de Doctrine autoloader niet hoeven in te stellen. De Zend Loader gebruiken werkt evengoed, zolang we het include_path maar goed instellen. Opgelet: in de ZF releases voor 1.8 is er een bug aanwezig wat ervoor zorgt dat er (onschuldige) warnings getoond worden wanneer je Doctrine Class Templates gebruikt. Dit gaat opgelost worden zodra versie 1.8 released is (de fix zit reeds in Subversion).

  1. /*
  2.  * Set super-global data
  3.  */
  4. Doctrine_Manager::connection("mysql://user:pass@localhost/database");
  5.  
  6. /*
  7.  * Configure Doctrine
  8.  */
  9. Zend_Registry::set('doctrine_config', array(
  10.         'data_fixtures_path'  =>  dirname(__FILE__).'/doctrine/data/fixtures',
  11.         'models_path'         =>  dirname(__FILE__).'/models',
  12.         'migrations_path'     =>  dirname(__FILE__).'/doctrine/migrations',
  13.         'sql_path'            =>  dirname(__FILE__).'/doctrine/data/sql',
  14.         'yaml_schema_path'    =>  dirname(__FILE__).'/doctrine/schema'
  15.         ));

Dit laatste stuk code heeft twee taken. Om te beginnen stelt het de database connectie in. Om het simpel te houden heb ik gewoon een string waarde hardcoded opgegeven. In een echter systeem wil je echter iets als Zend_Config gebruiken. Dat laat ik als oefening. Vervolgens stelt het de paden in voor de Doctrine command line tool (die de code en database schemas genereert). Ik sla deze array op in het Zend_Registry, wat een generieke methode is om objecten op te slaan, die je op een later punt tijdens de uitvoering weer kan opvragen.

Verander de regel die de Doctrine connectie instelt zodat ze jouw systeemconfiguratie volgt. Dit dient naar een lege database te verwijzen. We zullen deze later vullen met tabellen.

Het application/bootstrap.php bestand zou geen verrassingen mogen bevatten. Nogmaals, ik heb het voorbeeld zo simpel mogelijk gehouden.

Als laatste configureren we de Doctrine command line interface:

scripts/doctrine-cli
  1. #!/usr/bin/env php
  2. <?php
  3. require dirname(__FILE__).'/../application/global.php';
  4.  
  5. $cli = new Doctrine_Cli(Zend_Registry::get('doctrine_config'));
  6. $cli->run($_SERVER['argv']);

Maak dit script uitvoerbaar: chmod +x scripts/doctrine-cli en je bent helemaal klaar om te beginnen.

Een applicatie bouwen

Nu we de basis code hebben gaan we een simpele applicatie bouwen die Zend Framework en Doctrine gebruikt. We gaan een simpele boodschappen muur bouwen, een plaats waar gebruikers berichten kunnen plaatsen en berichten van anderen kunnen lezen.

We gaan het heel simpel houden: slechts 1 controller en 1 view script. Je hebt volgende bestanden nodig:

application/views/scripts/index/index.phtml
  1. <html>
  2. <head>
  3.     <title>ZF & Doctrine example</title>
  4. </head>
  5.  
  6. <body>
  7. <h1>Submit a message:</h1>
  8. <?=$this->form?>
  9.  
  10. <hr />
  11. <h1>Messages posted:</h1>
  12. <!-- TODO: Show messages here -->
  13. </body>
  14. </html>
application/controllers/IndexController.php
  1. <?php
  2. class IndexController extends Zend_Controller_Action
  3. {
  4.     public function indexAction()
  5.     {
  6.         $form = $this->getForm();
  7.         $req = $this->getRequest();
  8.         if ($req->getPost() && $form->isValid($req->getPost())) {
  9.             // TODO: Insert message into database
  10.         }
  11.         $this->view->form = $form;
  12.  
  13.         // TODO: Retrieve all messages.
  14.     }
  15.  
  16.     private function getForm()
  17.     {
  18.         $form = new Zend_Form();
  19.         $form->addElement('text', 'name', array(
  20.                     'label' => 'Your name',
  21.                     'required' => true
  22.                     ));
  23.         $form->addElement('textarea', 'message', array(
  24.                     'label' => 'Message',
  25.                     'required' => true,
  26.                     'rows' => 4
  27.                     ));
  28.         $form->addElement('submit', 'send');
  29.         return $form;
  30.     }
  31. }
  32. ?>

Zoals je kan zien zijn er nog drie grote TODO items: een in het view script en twee in de controller. Op deze plaatsen gaan we Doctrine aansluiten. Maar om dat te doen moeten we eerst enkele data objecten definiëren. We keren later terug naar de controller en het view script, maar eerst gaan we het database schema maken.

Het database schema definiëren

Doctrine laat je toe om je database structuur te beschrijven in YAML bestanden, een simpele tekstuele voorstelling. We zullen dit gebruiken en de PHP bestanden automatisch laten genereren. Ik heb het volgende schema gedefinieerd:

application/doctrine/schema/schema.yml
  1. ---
  2. Message:
  3.     columns:
  4.         id:
  5.             primary: true
  6.             autoincrement: true
  7.             type: integer(4)
  8.         posted:
  9.             type: timestamp
  10.         name:
  11.             type: string(255)
  12.         message:
  13.             type: string

In deze applicatie hebben we maar een simpel object nodig: Message, met vier kolommen: de verplichte unieke ID, een tijdveld dat aangeeft wanneer het bericht geplaatst is, de naam van de schrijver en het bericht zelf.

We kunnen nu de doctrine command line gebruiken om de model files en de database tabellen te maken. Voer volgende commandos uit op de commandline:

  1. $ ./scripts/doctrine-cli generate-models-yaml
  2. generate-models-yaml - Generated models successfully from YAML schema
  3. $ ./scripts/doctrine-cli generate-sql
  4. generate-sql - Generated SQL successfully for models
  5. $ ./scripts/doctrine-cli create-tables
  6. create-tables - Created tables successfully

Als alles goed ging worden er geen fouten weergegeven. Indien dit wel het geval was, controleer dan of je database verbinding juist ingesteld is.

Alles samenvoegen

Nu gaan we de laatste stukjes TODO aanpakken. We zullen ze stap voor stap vervangen. De volledige code voor de afgewerkte bestanden is onderaan het artikel te vinden. Eerst zullen we code toevoegen om een bericht op te slaan. Vervangen het volgende:

  1. // TODO: Insert message into database

Door dit (negeer de <?php and ?> tags):

  1. $message = new Message();
  2. $message->fromArray($form->getValues(true));
  3. $message->posted = new Doctrine_Expression('NOW()');
  4. $message->save();

Zoals je kan zien gebruiken we een object van de Message klasse. Deze klasse is automatisch aangemaakt door Doctrine. Je kan ze vinden in application/models/. De autoloader zorgt er voor dat alle vereiste bestanden geladen zijn.

We moeten de berichten weer opvragen om ze te tonen. Beginnen doen we bij de controller. Vervang:

  1. // TODO: Retrieve all messages.

Door:

  1. $messages = Doctrine_Query::create()
  2.              ->from('Message m')
  3.              ->orderBy('m.posted DESC')
  4.              ->execute();
  5. $this->view->messages = $messages;

Zeer simpel. Ik heb een DQL query gebruikt om omgekeerd chronologisch te sorteren.

Nu moeten we enkel deze berichten nog weergeven in ons view script. Wederom, vervang:

  1. <!-- TODO: Show messages here -->

Door:

  1. <?php foreach ($this->messages as $message): ?>
  2.     <h2><?=$message->name?> (<?=$message->posted?>)</h2>
  3.     <?=$message->message?>
  4. <?php endforeach; ?>

En we zijn klaar! Het resultaat zou er zo moeten uitzien:

Afgewerkte applicatie
Afgewerkte applicatie

Om de belangrijkste dingen aan te tonen heb ik belangrijke zaken, zoals input validatie, eruit gelaten. Deze applicatie mag je dus niet gebruiken in een productie omgeving! Je kan deze checks als extra oefening zelf toevoegen.

Conclusie

Zo, een propere en simpele applicatie met zowel Zend Framework als Doctrine. Aangezien beide dezelfde filosofieën volgen is het mogelijk om deze proper te integreren, waardoor applicaties bouwen een waar genot wordt.

Als je opmerkingen, reacties of vragen hebt, stuur me gerust een email (ruben@savanne.be), of plaats een reactie op mijn blog.

Appendix: De code verkrijgen

Als je niet graag code kopieert en plakt is er nog een andere optie voor je: downloaden via git. Ik heb zowel de onafgewerkte versie (met TODO items) en een afgewerkte versie online geplaatst in een git repository. Je gebruikt het als volgt:

Appendix: Volledige bestanden

application/controllers/IndexController.php
  1. <?php
  2. class IndexController extends Zend_Controller_Action
  3. {
  4.     public function indexAction()
  5.     {
  6.         $form = $this->getForm();
  7.         $req = $this->getRequest();
  8.         if ($req->getPost() && $form->isValid($req->getPost())) {
  9.             $message = new Message();
  10.             $message->fromArray($form->getValues(true));
  11.             $message->posted = new Doctrine_Expression('NOW()');
  12.             $message->save();
  13.         }
  14.         $this->view->form = $form;
  15.  
  16.         $messages = Doctrine_Query::create()
  17.                     ->from('Message m')
  18.                     ->orderBy('m.posted DESC')
  19.                     ->execute();
  20.         $this->view->messages = $messages;
  21.     }
  22.  
  23.     private function getForm()
  24.     {
  25.         $form = new Zend_Form();
  26.         $form->addElement('text', 'name', array(
  27.                     'label' => 'Your name',
  28.                     'required' => true
  29.                     ));
  30.         $form->addElement('textarea', 'message', array(
  31.                     'label' => 'Message',
  32.                     'required' => true,
  33.                     'rows' => 4
  34.                     ));
  35.         $form->addElement('submit', 'send');
  36.         return $form;
  37.     }
  38. }
  39. ?>
application/views/scripts/index/index.phtml
  1. <html>
  2. <head>
  3.     <title>ZF & Doctrine example</title>
  4. </head>
  5.  
  6. <body>
  7. <h1>Submit a message:</h1>
  8. <?=$this->form?>
  9.  
  10. <hr />
  11. <h1>Messages posted:</h1>
  12. <?php foreach ($this->messages as $message): ?>
  13.     <h2><?=$message->name?> (<?=$message->posted?>)</h2>
  14.     <?=$message->message?>
  15. <?php endforeach; ?>
  16. </body>
  17. </html>