14 dec
2010

Navigatie in stijl

Tijd om het framework van onze nieuwe site weer eens wat verder op te vullen. Dat wil zeggen: we gaan een aantal nieuwe controllers maken, een navigatiesysteem installeren, en een minimum aan styling aanbrengen. Het zal niet verbazen dat het Zend Framework hiervoor de nodige hulpmiddelen aanbiedt.

Controllers

Welke hebben we eigenlijk precies nodig? Dat wordt bepaald door de functies die we willen aanbieden. Om nog even het geheugen op te frissen: we willen nieuws presenteren, en recensies over e-boeken en gadgets om ze te lezen: Ipads, Kindles, maar ook andere apparaten om e-boeken te lezen. Een zoekfunctie is natuurlijk belangrijk, er komen reacties onder artikelen. Er moet een uitgebreid beheersgedeelte komen.

Als je ervan uitgaat dat elke action verantwoordelijk wordt gemaakt voor een bepaalde functie, dan ligt het voor de hand om de controller-structuur zo in te richten dat elke controller een serie gelijksoortige actions groepeert. Die redenering volgend, betekent dat dat we een artikelController nodig hebben waarin nieuws en recensies worden gebundeld; en een gebruikerscontroller die we gebruiken om nieuwe gadgets te publiceren.

Maar functionaliteit is niet het enige criterium op grond waarvan we onze controllers en actions indelen. Ook de navigatie voor de gebruiker speelt een rol. Zoals je je wellicht herinnert, werkt de naamgeving van je controllers en actions rechtstreeks door in de URL-structuur; /user/registratie komt overeen met de registratieAction in de UserController. Nu kunnen we met een specifieke ZF-component, de Router, wel aanpassingen doorvoeren aan deze structuur, maar dat brengt weer zijn eigen complicaties met zich mee. In een latere blogpost zal ik nog terugkomen op de router.

Deze overwegingen leiden tot de volgende controller-structuur; naast de al bestaande Index- en UserController heb ik een BoekenController, een GadgetsController, een NieuwsController en een ZoekController gemaakt. Het procédé ken je inmiddels, als het goed is: met behulp van Zend_Tool maak je in één klap de controller, de indexAction van de controller en de benodigde views aan. Voor elk van bovengenoemde controllers volstaat het in een terminal dit commando in te geven:

php bin/zf.php create controller Boeken

Later gaan we in een aparte module alle beheerstaken onderbrengen. Voor het moment volstaan bovengenoemde controllers; deze leveren de pagina’s aan die voor elke gebruike in principe, aan de ‘voorkant’ van de applicatie, benaderbaar zullen zijn.

Navigatie

We hebben nu weliswaar de juiste pagina’s gemaakt, maar die zijn nog niet benaderbaar. Om onze navigatie in te richten, gebruiken we Zend_Navigation. Dit is een uiterst praktische component; op basis van een xml-structuur kun je een menu samenstellen, maar ook een sitemap en een ‘kruimelpad’; dat is al veel functionaliteit voor een enkel xml-bestand, maar hiermee houdt het nog niet op. Zend_Navigation is slim genoeg om de menu-structuur aan te passen aan de al dan niet ingelogde status van de gebruiker, en de juiste submenu’s te tonen afhankelijk van de pagina waar de gebruiker zich bevindt.

De xml-structuur zelf is heel simpel. Mijn bestand ziet er op dit moment als volgt uit:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <nav>
  3. <home>
  4. <label>Home</label>
  5. <controller>index</controller>
  6. <action>index</action>
  7. </home>
  8. <nieuws>
  9. <label>Nieuws</label>
  10. <controller>nieuws</controller>
  11. <action>index</action>
  12. <pages>
  13. <page1>
  14. <label>Boeken</label>
  15. <controller>nieuws</controller>
  16. <action>boeken</action>
  17. </page1>
  18. <page2>
  19. <label>Gadgets</label>
  20. <controller>nieuws</controller>
  21. <action>gadgets</action>
  22. </page2>
  23. <page3>
  24. <label>Algemeen</label>
  25. <controller>nieuws</controller>
  26. <action>algemeen</action>
  27. </page3>
  28. </pages>
  29. </nieuws>
  30. <boeken>
  31. <label>Boeken</label>
  32. <controller>boeken</controller>
  33. <action>index</action>
  34. <pages>
  35. <page1>
  36. <label>Fictie</label>
  37. <controller>boeken</controller>
  38. <action>fictie</action>
  39. </page1>
  40. <page2>
  41. <label>Non-fictie</label>
  42. <controller>boeken</controller>
  43. <action>nonFictie</action>
  44. </page2>
  45. <page3>
  46. <label>Thrillers</label>
  47. <controller>boeken</controller>
  48. <action>thrillers</action>
  49. </page3>
  50. <page4>
  51. <label>Jeugd</label>
  52. <controller>boeken</controller>
  53. <action>jeugd</action>
  54. </page4>
  55. </pages>
  56. </boeken>
  57. <gadgets>
  58. <label>Gadgets</label>
  59. <controller>gadgets</controller>
  60. <action>index</action>
  61. <pages>
  62. <page1>
  63. <controller>gadgets</controller>
  64. <action>eReaders</action>
  65. <label>E-readers</label>
  66. </page1>
  67. <page2>
  68. <controller>gadgets</controller>
  69. <action>tablets</action>
  70. <label>Tablets</label>
  71. </page2>
  72. </pages>
  73. </gadgets>
  74. <users>
  75. <label>Gebruikers</label>
  76. <controller>user</controller>
  77. <action>index</action>
  78. </users>
  79. </nav>

Zoals je ziet hebben we een root-element nav, waarbinnen onze navigatie is ondergebracht. Ons navigatiemenu voorziet verder in vijf pagina’s, het eerste niveau in het xml-bestand: home, nieuws, boeken, gadgets en gebruikers. Als deze pagina’s geen submenu hebben, volstaat het om drie kenmerken van elk item weer te geven: het label, en de bijbehorende controller en action. Zijn er wel submenu’s, dan worden er xml-elementen onder het eerste niveau geplakt: onder ‘pages’ kun je een onbeperkt aantal pagina’s plaatsen, elk weer gekarakteriseerd door een controller, een action en een label.

Je kunt in principe nog dieper gaan, maar dat is bijna nooit aan te bevelen; een menustructuur die meer dan twee niveau’s kent, wordt al gauw onoverzichtelijk.

Nu we een indeling hebben voor onze navigatie, moeten we onze applicatie nog vertellen waar deze te vinden is en waar ze ingezet moet worden. We gebruiken daarvoor een zogeheten plugin –  een klasse die naar behoefte wordt geladen bij de start van elke request. Zend Framework biedt verschillende soorten plugins, maar we zijn nu op zoek naar een specifieke soort. Omdat onze navigatie nauw samenhangt met de views die worden gegenereerd, hebben we een plugin nodig die in een laat stadium wordt geladen en die logisch samenwerkt met de views. Die plugin wordt geleverd door Zend_Layout – tevens verantwoordelijk voor de weergave van onze paginasjablonen.

De architectuur van een plugin is simpel. Als de applicatie wordt gemeld dat er een plugin-klasse geladen moet worden, zoekt hij na het laden van die klasse naar methoden die een vastgestelde naam moeten hebben. Vaak zijn dat methoden die een fase in het laden van de applicatie weergeven: preDispatch bijvoorbeeld wordt aangeroepen voordat het request wordt doorgegeven aan de juiste controller, routeShutdown vindt plaats meteen nadat bekend is welke controller wordt aangeroepen.

Zo ziet onze layout-pluginklasse eruit:

  1. <?php
  2. class GJB_Plugins_Layout extends Zend_Layout_Controller_Plugin_Layout
  3. {
  4. public function preDispatch(Zend_Controller_Request_Abstract $request) {
  5. $xmlNav = new Zend_Config_Xml(APPLICATION_PATH . '/configs/nav.xml');
  6. $navigation = new Zend_Navigation($xmlNav);
  7. $this->getLayout()->getView()->navigation($navigation);
  8. }
  9. }
  10. ?>

De methode preDispatch accepteert het request als argument; in deze methode hebben het request (nog)  niet nodig, maar in andere plugins kan het erg praktisch zijn om toegang te hebben tot het requestobject. In de eerste regel wordt ons xml-bestand uitgelezen; en vervolgens wordt dit bestand meegegeven als parameter aan een nieuwe instantie van Zend_Navigation. Het echte werk gebeurt eigenlijk pas in de derde regel: het navigation-object wordt aan het view-object doorgegeven; binnen elke view is de navigatie nu beschikbaar met de aanroep $this->navigation(). De navigatie wordt nu weergegeven als een geneste lijst (met ul en li elementen) die je vervolgens op de gebruikelijke manier van een stijl kunt voorzien. Deze aanroep zelf heb ik in het layout-bestand geplaatst; je wilt je navigatie immers constant houden ongeacht op welke pagina de gebruiker zich bevindt.

Het enige wat nog resteert is om onze applicatie te vertellen dat ze een nieuwe pluginklasse moet laden. Er zijn in principe twee manieren om dat te doen: het kan in de bootstrap-klasse, maar we geven hier de voorkeur aan een simpele toevoeging aan het applicatie ini-bestand. Daar wordt immers al aangegeven waar de layout zelf gevonden kan worden. Het volstaat om deze regel toe te voegen:

resources.layout.pluginClass = GJB_Plugins_Layout

Zoals je ziet hoef je alleen maar aan te geven hoe je je plugin hebt genoemd.

Stylesheets

Ik heb een eerste opzet gemaakt van de stylesheet:

  1. @CHARSET "UTF-8";
  2.  
  3. html, body, * {
  4. font-family: Georgia, "Times New Roman", serif;
  5. }
  6. img {
  7. border: 0 none;
  8. }
  9. #logo img {
  10. font-size:44px;
  11. color:#0077FF;
  12. }
  13. #mainWrapper {
  14. width: 960px;
  15. background-color:#fdfdfd;
  16. margin:0 auto;
  17. border-left: 1px solid #c3c3c3;
  18. border-right: 1px solid #c3c3c3;
  19. padding: 0 10px;
  20. }
  21.  
  22. #header {
  23. height: 150px;
  24. border-bottom:1px solid #c3c3c3;
  25. padding: 10px 0;
  26. margin-bottom:10px;
  27. position:relative;
  28. top:0;
  29. left:0;
  30. }
  31. #header div {
  32. float:left;
  33. }
  34. #header #sysMsg {
  35. clear:right;
  36. float:right;
  37. }
  38. #mainContent {
  39. float:left;
  40. width: 620px;
  41. }
  42. #sidebar {
  43. float:right;
  44. width: 336px;
  45. }
  46. #sidebar #login {
  47. text-align:right;
  48. }
  49. .col {
  50. width: 198px;
  51. margin-right:10px;
  52. float:left;
  53. border-right: 1px dotted #c3c3c3;
  54. }
  55. .col.last {
  56. margin-right:0;
  57. }
  58. .colContent {
  59. width: 190px;
  60. margin: 0 auto;
  61. }
  62. #navigation {
  63. position:absolute;
  64. bottom: 5px;
  65. left:300px;
  66. }
  67. ul.navigation li {
  68. display:inline;
  69. margin-right: 20px;
  70. }
  71. ul.navigation li ul {
  72. display:none;
  73. }
  74. .clear {
  75. clear:both;
  76. }
  77. #header #search {
  78. float:right;
  79. width: 336px;
  80. text-align:right;
  81. }
  82. #header #logo {
  83. width: 620px;
  84. }

Ik ben geen designer, dus dit is vast voor verbetering vatbaar; en het echte elesio ligt, op het moment van schrijven, bij de ontwerper. Het definitieve stylesheet zal er dus ongetwijfeld anders uitzien, maar voor het moment doen we het hiermee.

Het valt je misschien op dat een aantal basisregels voor het gelijktrekken van het uiterlijk van de site over verschillende browsers ontbreekt. Dat komt omdat we die regels ook niet zelf gaan maken, maar de CSS-bibliotheek van Yahoo gebruiken. In ons sjabloonbestand layout.phtml roepen we de stylesheets als volgt aan:

  1. <?php
  2. $this->headLink()->appendStylesheet('http://yui.yahooapis.com/3.0.0/build/cssreset/reset-min.css');
  3. $this->headLink()->appendStylesheet('http://yui.yahooapis.com/3.0.0/build/cssbase/base-min.css');
  4. $this->headLink()->appendStylesheet('http://yui.yahooapis.com/3.0.0/build/cssfonts/fonts-min.css');
  5. $this->headLink()->appendStylesheet('/css/main.css');

Elke view-instantie biedt een methode aan die headLink() wordt genoemd; deze methode levert je een headLink object op. Aan dit object hang je alle verschillende links die je in de head-sectie van je html-pagina wil tonen. Dat zijn meestal dus de stylesheets (<link type=’text/css’ rel=’stylesheet’ … />), maar ook je favicon wordt op deze manier aan je document gehangen. De eerste drie linkjes in de code hierboven verwijzen naar de basis-css die Yahoo biedt; de laatste is de link naar ons eigen stylesheet. Omdat we later in ons layout-script de aanroep <?php echo $this->headLink();?> hebben staan, worden deze vier stylesheets beschikbaar gemaakt voor onze pagina’s. In de html-code ziet dat er zo uit:

<link href="http://yui.yahooapis.com/3.0.0/build/cssreset/reset-min.css" media="screen" rel="stylesheet" type="text/css" />
<link href="http://yui.yahooapis.com/3.0.0/build/cssbase/base-min.css" media="screen" rel="stylesheet" type="text/css" />
<link href="http://yui.yahooapis.com/3.0.0/build/cssfonts/fonts-min.css" media="screen" rel="stylesheet" type="text/css" />
<link href="/css/main.css" media="screen" rel="stylesheet" type="text/css" />

Tot besluit

Onze acties van vandaag leiden tot deze pagina:

Elesio voorpagina (voorlopig ontwerp)

Klik op de afbeelding voor een grote versie

Nu je weet hoe stylesheets gekoppeld worden, en je zelf je navigatie kunt aanpassen, kun je het ontwerp helemaal naar je eigen zin bewerken.

We zijn nog niet helemaal klaar met frontend-functionaliteit: de submenu’s worden nog niet getoond. Dat gaan we met behulp van javascript, en meer specifiek met JQuery, in orde brengen. De volgende klus die we gaan ondernemen: de ingelogde gebruikers moeten de mogelijkheid krijgen om gadgets, nieuws en recensies te plaatsen. Dat betekent veel formulier en databasewerk, de volgende keer!

4 dec
2010

Gebruik van imagick en zend_mail

In de vorige aflevering heb ik beloofd dat we nu de nodige controllers en stylesheets zouden gaan aanmaken en een login-systeem zouden gaan implementeren. Die belofte kan ik niet nakomen – we bewaren dat voor de volgende keer. Er zijn nog te veel losse eindjes op te ruimen. Zo heb ik nog niet uitgelegd hoe we omgaan met het bestand dat door de gebruiker wordt geupload om een profielplaatje te tonen. En we moeten nog een manier vinden om onze modellen te bewerken. We gaan met dat laatste beginnen.

Een mapper maken

Er kwam een gepost formulier in de indexAction van de UserController terecht, die vervolgens rechtstreeks de database tabel users aansprak en de gevalideerde data opsloeg in de tabel. Dat is niet de juiste gang van zaken. Zo is er een aantal standaard eigenschappen die van elke user worden opgeslagen en waarmee we nog geen rekening hebben gehouden; en daarnaast moeten we ook nog iets ondernemen met het profielplaatje dat eventueel geupload is. En we moeten ten slotte niet vergeten om de nieuw geregistreerde gebruiker een mail te sturen om het opgegeven mailadres te checken.

Nou komt het natuurlijk voor dat er een één op één relatie bestaat tussen je objecten en de database-tabellen waarin ze worden opgeslagen. Maar vaak zal een object worden opgebouwd uit verschillende onderdelen die in verschillende tabellen worden bewaard. Dat geldt voor ons Userobject bijvoorbeeld voor de profielafbeeldingen; die komen in de images-tabel terecht. Het is daarom een goede gewoonte om je database-tabellen niet rechtstreeks aan te spreken en alleen te gebruiken voor de database-operaties zelf; we bouwen in plaats daarvan mappers, die een relatie aanbrengen tussen onze applicatie-objecten enerzijds en de database anderzijds.

Maak een nieuwe map DbMap in je folder ‘models’. Binnen die nieuwe map kun je een klasse definiëren die ik Model_DbMap_User heb genoemd. De code van die klasse ziet er als volgt uit:

  1. <?php
  2. class Model_DbMap_User
  3. {
  4. protected $_dbTable;
  5. protected $_dbRow;
  6. public function __construct ()
  7. {
  8. $this->_dbTable = new Model_DbTable_Users();
  9. $this->_dbRow = $this->_dbTable->createRow();
  10. }
  11. }

Natuurlijk moet de database-tabel wel tot je beschikking staan in deze klasse; daarom wordt de tabel en het bijbehorende rij-object al in de constructor gedefinieerd. Vervolgens definiëren we een nieuwe methode die we newUser noemen; deze methode accepteert als parameter een array van de door de gebruiker geposte data:

  1. <?php
  2. public function newUser (array $data)
  3. {
  4. $data['password'] = sha1($data['password']);
  5. $data['lastLoginIp'] = $_SERVER['REMOTE_ADDR'];
  6. $data['lastLoginDate'] = new Zend_Db_Expr('now()');
  7. $data['role'] = 'unconfirmed';
  8. $data['confirmationString'] = md5(
  9. $data['email'] . $data['userName'] . $data['password'] . uniqid());
  10. $this->_dbRow->setFromArray($data);
  11. $this->_dbRow->save();
  12. $mailMgr = new GJB_Utils_Mail();
  13. $mailMgr->sendConfirmationMail($this->_dbRow);
  14. $result = $this->createProfilePicForNewUser($data['profilePic']);
  15. }

Zoals je ziet passen we enkele bewerkingen toe op de data, en vullen we de array aan met gegevens die niet door de gebruiker worden ingegeven: we encoderen het wachtwoord, we zetten het ip-adres, de datum en de rol van de nieuwe gebruiker in de array. Die rol is op dit moment nog ‘unconfirmed’. Om zijn registratie te bevestigen, moet de gebruiker op een linkje klikken in de mail die we hem gaan toesturen. Zo weten we zeker dat het emailadres correct is. De link wordt opgebouwd uit een md5-gecodeerde hash van het emailadres, gebruikersnaam, wachtwoord en een uniqid()-aanroep; zo weten we zeker dat die md5-string altijd uniek zal zijn en uitsluitend gebruikt wordt door de nieuwe registrant.

Nu blijkt hoe handig het is dat we al een database-rowobject tot onze beschikking hebben; we kunnen dit object in één keer vullen met de methode setFromArray(), en het object bewaren met de aanroep save().

De profielafbeelding bewerken en opslaan

Wat ons nu nog resteert, is het aanmaken van de mail, alsmede het eventueel bewerken van de profielafbeelding. We zullen beginnen met de methode createProfilePicForNewUser(), die wordt aangeroepen.

  1. <?php
  2. public function createProfilePicForNewUser ($imageName)
  3. {
  4. $img = new Model_DbMap_Image();
  5. $result = $img->createProfilePic($this->_dbRow->id, $imageName);
  6. if ($result instanceof GJB_Exception_Image) {
  7. return $result;
  8. } else {
  9. return true;
  10. }
  11. }

Zoals je ziet is dit een simpele methode, die een andere mapper gebruikt om de profielafbeelding te verwerken. Deze klasse, Model_DbMap_Image, doet feitelijk hetzelfde als onze UserMapper; hij vormt een tussenklasse tussen de afbeeldingsobjecten die we in onze applicatie gebruiken en de tabel images in de database. Het enige wat deze imagemapper nodig heeft, is het gebruikersnummer van de nieuwe user, en de naam van het bestand dat is geupload. Het gevalideerde bestand zelf staat al tot onze beschikking; daarvoor heeft Zend_Form gezorgd.

De imagemapper zelf hoeft niet het zware werk te verrichten van het hernoemen van de afbeeldingen en het resizen tot het juiste formaat. Die klus is gedelegeerd aan een algemene utility klasse voor afbeeldingsbewerking, die we in onze library folder onze eigen namespace hebben aangemaakt.

  1. <?php
  2. class Model_DbMap_Image
  3. {
  4. protected $_dbTable;
  5. protected $_dbRow;
  6.  
  7. public function __construct()
  8. {
  9. $this->_dbTable = new Model_DbTable_Images();
  10. $this->_dbRow = $this->_dbTable->createRow();
  11. }
  12.  
  13. public function createProfilePic($userId, $imageLocation)
  14. {
  15. $imageLocation = Zend_Registry::get('uploadDir') . '/' . $imageLocation;
  16. $img = new GJB_Utils_Image();
  17. try {
  18. $img->initFile($imageLocation);
  19. $img->makeThumb();
  20. $img->makeLarge();
  21. $img->removeImage($imageLocation);
  22. $this->_dbRow->reference = 'user';
  23. $this->_dbRow->referenceId = $userId;
  24. $this->_dbRow->filePath = $img->getUrlPath();
  25. $this->_dbRow->fileName = $img->getFileName();
  26. $this->_dbRow->save();
  27. return true;
  28. } catch (GJB_Exception_Image $e) {
  29. return $e;
  30. }
  31. }
  32. }

De interessante methode hier is natuurlijk createProfilePic, wat voornamelijk een serie aanroepen vormt naar onze utility-klasse. Ik zal de code van die klasse hier niet tonen, omdat die weinig met het Zend Framework te maken heeft; maar waar het in feite op neerkomt is dat de klasse op basis van de imagick-extensie van PHP een aantal methoden bevat voor het hernoemen en resizen van onze afbeeldingen. Als dat is gebeurd, wordt de afbeelding in de database opgeslagen met de nodige metadata: gebruikers-id, en de opslaglocatie.

Wat nog wel het vermelden waard is: het systeem dat ik hanteer om de afbeeldingen op te slaan. Ik ga ervan uit dat elke afbeelding in twee varianten wordt bewaard: als thumbnail, en in een vergrote versie. De afmetingen van die verschillende versies worden ingegeven in ons bestand application/configs/application.ini. Daarin zien we de volgende passage terugkomen:

img.thumb.width = 100
img.thumb.height = 100
img.large.width = 600
img.large.height = 600

Onze image-utility klasse gebruikt imagick om de afbeeldingen in deze verschillende formaten op te slaan. De naam van de bestanden is ‘thumb_’ gevolgd door de timestamp en de extensie, respectievelijk ‘large_’ gevolgd door timestamp en extensie. In de database slaan we alleen het pad en de naam timestamp plus extensie op; we laten het aan de views over die de image moeten tonen om de juiste source aan te roepen door achter het pad en voor de database-naam respectievelijk ‘thumb_’ en ‘large_’ te plaatsen.

De mail-utility

Onze usermapper is ook nog verantwoordelijk voor het versturen van de bevestigingsmail aan de nieuwe geregistreerde gebruiker. Hij delegeert deze taak aan een mail-utility manager. De code van deze managerklasse ziet er op dit moment als volgt uit:

  1. <?php
  2. class GJB_Utils_Mail
  3. {
  4.  
  5. private $zmail;
  6. private $senderAddress;
  7. private $senderName;
  8.  
  9. public function __construct()
  10. {
  11. $this->zmail = new Zend_Mail('utf-8');
  12. $config = Zend_Registry::get('mail');
  13. $this->senderAddress = $config['admin']['address'];
  14. $this->senderName = $config['admin']['name'];
  15. $this->zmail->setDefaultFrom($this->senderAddress, $this->senderName);
  16. }
  17. public function sendConfirmationMail(Model_DbRow_User $user)
  18. {
  19. $url = $_SERVER['HTTP_HOST'] . '/user/confirm/hash/' . $user->confirmationString;
  20. $name = $user->userName;
  21. $body = "<h2>Bevestig uw registratie</h2><p>Beste $name,</p>";
  22. $body .= "<p>U heeft u zojuist aangemeld op elesio.nl. Wilt u uw aanmelding bevestigen door op <a href='$url'>deze link</a> te klikken? Dan is uw aanmelding afgerond.</p>";
  23. $body .= "<p>Met dank, en vriendelijke groet,</p><p>" . $this->senderName . "</p>";
  24. $body .= "<p>Link niet aanklikbaar? Kopieer de link en plak hem in het adresvenster van uw browser: $url</p>";
  25.  
  26. $this->zmail->addTo($user->email, $user->userName);
  27. $this->zmail->setSubject('Uw aanmelding bij Elesio.nl');
  28. $this->zmail->setBodyHtml($body);
  29. $this->zmail->setBodyText(strip_tags($body));
  30. $this->zmail->send();
  31. }
  32.  
  33. }

Zoals je ziet maakt deze klasse heftig gebruik van een nieuwe Zend-component, namelijk Zend_Mail. Dit is iets méér dan een simpele wrapper om de php mail functies: los van het feit dat je er ook heel gemakkelijk mails mee kunt ophalen, hoef je je ook geen zorgen te maken over het verzenden van html- dan wel tekst-mails. Door zowel de html als de tekst te definiëren, zorgt Zend_Mail dat de goede versie bij de geadresseerde terecht komt.

In de constructor wordt een nieuwe instantie van Zend_Mail aangemaakt, en wordt ook het configuratiebestand uitgelezen op een aantal constanten die we daar hebben gedefinieerd: het return adres van de sitebeheerder, alsmede de naam die bij dit adres hoort.

We gaan ervan uit dat deze mailmanager later nog meer te doen krijgt. Door deze variabelen in de constructor te definiëren, komen ze later in alle methoden die we willen aanroepen tot onze beschikking. De methode sendConfirmationMail gebruiken we om de bevestigingsmail te versturen. In de eerste regel stellen we de URL samen waar we de gebruiker naartoe sturen. De rest van de code spreekt voor zich. Overigens zou het een goed alternatief zijn om voor de opbouw van de inhoud van de mail een nieuwe instantie van Zend_View te maken; dat biedt de mogelijkheid om gebruik te maken van placeholders, partiële views en alle andere hebbedingetjes die Zend_View verschaft. Omwille van de overzichtelijkheid bouwen we hier de body op de traditionele wijze op.

Als de gebruiker zijn registratie bevestigt

Zoals je hierboven ziet, krijgt de nieuwe gebruiker een bevestigingsmail toegestuurd met een link naar /user/confirm/hash/XXX, waarbij de XXX staat voor de md5-hash die we in de vorige stap hebben gegenereerd. Dat betekent dat we een nieuwe methode aan de UserController moeten toevoegen, de confirmAction. Deze methode is simpel van opzet:

  1. <?php
  2. public function confirmAction()
  3. {
  4. $hash = $this->_request->getParam('hash');
  5. $mpUser = new Model_DbMap_User();
  6. $result = $mpUser->confirm($hash);
  7. if ($result instanceof Model_DbRow_User) {
  8. $this->sysMsg->addMessage('U kunt nu inloggen met uw emailadres en wachtwoord');
  9. } else {
  10. $this->sysMsg->addMessage('De bevestiging van uw registratie is niet gelukt. Wilt u het nog eens proberen?');
  11. }
  12. $this->_redirect('/');
  13. }

Deze methode gebruikt de UserMapper om de juiste gebruiker op te zoeken aan de hand van zijn unieke hash. Als blijkt dat er een nieuwe gebruiker is met de ‘role’ als ‘unconfirmed’ en een hash die klopt, doen we het volgende: we zetten de role op ‘geregistreerd’ en maken de hash leeg. Vervolgens geven we de gebruiker de boodschap mee dat zijn registratie nu actief is en dat hij kan gaan inloggen.

Maar dat gaan we in de volgende episode doen van deze serie!

28 nov
2010

Formulieren en gegevensbewerking

Dit is het zevende deel van een serie over de bouw van een nieuwe website op basis van Zend Framework. Kijk in de zijbalk voor de linkjes naar de eerste delen.

Een van de meer gecompliceerde componenten van het Zend Framework is Zend_Form. Het duurt even voordat je doorhebt hoe de verschillende onderdelen van Zend_Form met elkaar samenwerken om het de developer uiteindelijk gemakkelijk te maken. Maar als je eenmaal die steile leercurve hebt doorlopen, zul  je merken dat je inspanningen niet voor niets zijn geweest.

Wat Zend_Form gecompliceerd maakt, is de veelheid van verschillende taken die eraan worden toegekend:

  • Het moet op eenduidige wijze formulieren weergeven in de pagina’s
  • Het moet gebruikersinput filteren en valideren
  • Het moet kunnen omgaan met de verschillende formulierelementen die in de verschillende HTML-specificaties zijn opgenomen.

Voor de weergave van formulieren worden decorators gebruikt; voor filtering en validatie filters en validators; en voor de verschillende elementen een deelcomponent die Zend_Form_Element wordt genoemd.

Zend_Form nader bekeken

Het belangrijkste dat je moet weten van Zend_Form is dat je deze component nooit rechtstreeks benadert. Je ontwerpt je eigen formulierklassen die een child vormen van Zend_Form. Binnen je formulierklasse hoef je geen aparte constructor te maken; sterker, het is aan te bevelen om dat niet te doen, omdat je dan een aantal standaardinstellingen van Zend_Form kunt overschrijven.

Zend_Form levert een hook, een lege methode die vanuit de constructor wordt aangeroepen, die je kunt gebruiken om je formulier te initialiseren. Die methode wordt, heel toepasselijk, ‘init’ genoemd. Je standaard formulier zal dus meestal zo’n opbouw kennen:

<?php
class Form_Registration extends Zend_Form
{
	public function init()
	{
		// hierin de code om je formulier op te bouwen
	}
}

Read More »

22 nov
2010

De database in Zend Framework

De kern van de meeste applicaties wordt gevormd door de aanwezige data. Die kunnen op veel verschillende manieren worden opgeslagen, maar bij meer complexe toepassingen zal er vrijwel altijd sprake zijn van een database. ZF zou natuurlijk onvolledig zijn als er niet een uitgebreide serie componenten beschikbaar was om toegang te bieden tot de database, zijn tabellen en gegevens.

Twee klassen zijn met name van belang: Zend_Db_Table_Abstract, en Zend_Db_Table_Row_Abstract. De eerste biedt een aantal methoden om toegang te krijgen tot de rijen gegevens in een tabel; de tweede tot de eigenschappen (de kolommen) van een afzonderlijke rij.

Opbouw van de database

Voordat we ons gaan richten op de interactie tussen de elesio en de database, moeten we eerst de juiste tabellen inrichten. Dat doen we op basis van de beschrijving in hoofdstuk 2, ‘Uitwerking van het concept’. In Mysql Workbench, het freeware GUI programma van Mysql, heb ik dit schema gemaakt:

Database schema van Elesio

Klik op de afbeelding om haar in groter formaat te zien.

Centraal in het schema staan twee tabellen, products en articles.

  1. Products is de moeder van twee afgeleide tabellen, books en gadgets. Boeken en apparaten worden beschouwd als de twee concrete producttypes die besproken kunnen worden op onze website. Die delen een aantal eigenschappen die in de moedertabel worden opgeslagen: het ean (een uniek productnummer, vergelijkbaar met het vroegere ISBN; maar dan algemener), de prijs, de invoerdatum, een herzieningsdatum, en een gemiddelde waardering van de gebruiker. Books en gadgets voeren daar nog hun specifieke eigenschappen aan toe, zoals titel, auteur, producent enzovoorts. Via de index products_id wordt de relatie gelegd met products tabel. De relatie tussen books en gadgets enerzijds en products anderzijds is één op één.
  2. In articles wordt alle content opgeslagen. Over elk product kunnen meerdere artikelen worden geschreven. Die relatie wordt belichaamd door de vreemde sleutel products_id in articles. Bovendien kan een artikel alleen worden geschreven door een geregistreerde gebruiker. Deze bevindt zich in de users tabel, en komt terug in de article tabel door de vreemde sleutel users_id.
  3. Er zijn voor de users vier verschillende rollen gedefinieerd: unconfirmed, registered, writer, admin. Wie zich registreert, is in eerste instantie een nieuwe, unconfirmed gebruiker. Zodra hij op de link in een bevestigingsmail klikt, wordt hij registered. Daarna is het aan de admin om een registered user te promoveren tot writer of zelfs tot admin. We hebben nog niet de exacte verschillen tussen deze rollen gedefinieerd, maar we willen onze applicatie toekomstbestendig maken; daarom is het handig om onze database daarop nu al in te richten. Wachtwoorden in onze applicatie worden versleuteld opgeslagen (sha1). Read More »
16 nov
2010

Controllers, Models en Views

Dit is het vijfde deel van een serie over de bouw van een nieuwe website op basis van Zend Framework. Kijk in de zijbalk voor de linkjes naar de eerste delen.

Zoals we al eerder hebben gezegd, vormen controllers de tussenlaag tussen het model (waar de logica van de applicatie wordt uitgevoerd) en de views (waar zich de presentatie naar de buitenwereld afspeelt). Er heerst de laatste jaren een consensus in kringen van software architecten dat het het best is om te streven naar skinny controllers and fat models: houd de controllers klein en overzichtelijk, en verplaats zoveel mogelijk code naar de models.

Een nieuwe pagina maken

We wijken even af van onze elesio-applicatie om met een simpel voorbeeld aan te tonen hoe we een nieuwe pagina maken in onze website. Open application/views/scripts/index/index.phtml en plaats onder je bestaande code een linkje:

<p><a href='/index/calculate'>Voer een berekening uit</a></p>

Klik je daar nu op, dan zul je een foutmelding te zien krijgen. Dat gaan we snel verhelpen. Open een command-prompt, navigeer naar je projectmap, en geef dit commando op:

php bin/zf.php create action calculate

Als het goed is, zul je de boodschap terugkrijgen dat er code is toegevoegd aan je indexcontroller, en dat er een view script is aangemaakt. Open nu /application/controllers/IndexController.php: daar zie je dat de methode CalculateAction is toegevoegd aan de klasse. En in de map /application/views/scripts/index is een nieuw bestand aangemaakt: calculate.phtml.

Read More »

13 nov
2010

Zend Framework nader bekeken

Dit is het vierde deel van een serie over de bouw van een nieuwe website op basis van Zend Framework. Kijk in de zijbalk voor de linkjes naar de eerste delen.

Nu we de applicatie hebben opgezet, is het handig om eens van dichtbij al die mappen en bestanden te bekijken die Zend_Tool voor ons heeft klaargemaakt.

Application

Dit is de centrale map waarin je veruit het grootste deel van je ontwikkelwerk zult doen. Application zelf is weer onderverdeeld in

1. Configs – hierin worden de configuratiebestanden van je applicatie opgezet.

2. Controllers – de verzamelplaats waar de requests worden opgevangen die door de browser van de bezoeker naar je applicatie worden verstuurd, en waar de views worden aangestuurd die de bezoeker als gevolg van zijn request uiteindelijk te zien krijgt

3. Models – hierin wordt de logica van je applicatie ondergebracht

4. Views – de scripts waarin de html terecht komt die je uiteindelijk moet produceren om de pagina’s te tonen

Een van de belangrijkste besluiten die je neemt als je een applicatie bouwt met ZF, is de beslissing om een strikte scheiding aan te brengen tussen de logica die je applicatie nodig heeft om goed te functioneren, de pagina’s die getoond moeten worden wil de bezoeker kaas kunnen maken van deze logica, en de verbinding tussen logica en pagina’s.

Deze scheiding is nuttig: als je die altijd hanteert, betekent dat dat je je eigen code aan een strikte discipline onderwerpt. Je weet dat je nooit in je html hoeft te zaken naar databasefunctionaliteit of login-code, want die heeft keurig zijn eigen plek in de applicatiestructuur. Dit ontwerppatroon (design pattern) heeft MVC: Model-View-Controller. En laat die benaming nou net overeenkomen met de submappen onder application!

Read More »

10 nov
2010

Uitwerking van ons concept

Nu we de infrastructuur voor onze nieuwe applicatie op poten hebben, is het hoog tijd dat we beter nadenken over het soort site dat we eigenlijk willen. Normaal gesproken is dat natuurlijk de eerste stap bij het ontwikkelen van een nieuwe site; maar omdat we snel op weg willen met het Zend Framework, heb ik die uit didactische overwegingen wat naar achter geschoven. Totaal onverantwoord natuurlijk, maar in dit leerproces mogen we een steekje laten vallen. Luister eens, ik zou ook liever meteen in de code duiken, maar dat zou echt dom zijn.

Beschrijving in woorden

Een site over e-boeken en e-readers dus. Wat willen we? In ieder geval recensies lezen, van zowel boeken als readers. Die recensies gaan we niet allemaal zelf schrijven; geregistreerde gebruikers kunnen hun eigen recensies uploaden. Tegelijk willen we het voor uitgevers van e-boeken en producenten van e-readers gemakkelijk maken om hun nieuwe producten en boeken in te voeren – dan hoeven we dat allemaal niet zelf te doen.

Als er veel producten en recensies zijn, moet je die gemakkelijk kunnen terugvinden. Een zoekfunctie is daarom essentieel.

Het zou prettig zijn als die gebruikers zich werkelijk deel voelen van een gemeenschap. Kenmerken daarvan zijn onder andere dat het mogelijk moet zijn om contacten te leggen met je bestaande netwerken op Facebook, Twitter en Hyves; je moet je Outlook, Gmail en MSN-adressenlijst kunnen uitnodigen om een kijkje te komen nemen; en het moet gemakkelijk worden gemaakt om de onderlinge contacten op peil te houden door bijvoorbeeld een forum of zoiets dergelijks. Wie een recensie post, wil daar bovendien zijn relaties van op de hoogte te stellen; dus het zou fijn zijn als er meteen een berichtje op de Twitter-tijdlijn of de Facebook-wall van de gebruiker geplaatst kon worden. Ingelogde gebruikers kunnen reageren op nieuwsartikelen en recensies. Read More »

9 nov
2010

De goede ontwikkelomgeving voor je Zend Framework applicatie

OK, nu we duidelijk hebben waarom we Zend Framework willen inzetten voor de bouw van onze site voor e-boeken en e-readers, wordt het tijd om je computer zodanig in te richten dat je  deze applicatie ook gemakkelijk kunt gaan bouwen. Ik ga ervan uit dat je al een werkende WAMP/MAMP/LAMP installatie hebt, omdat dit immers niet de eerste keer is dat je een php-website bouwt. Mocht dat niet zo zijn, dan raad ik je aan je te registreren bij Zend, en vervolgens Zend Server Community Edition te downloaden en installeren. Hierin zit alles wat je nodig hebt, inclusief een volledige versie van het Zend Framework. Moet je Zend Framework apart downloaden, dan is registratie niet per se nodig. Dat kan in tar.gz- of in zip-formaat.

Heb je wel de beschikking over de goede installatie, dan is het nu eerst tijd om de infrastructuur op te zetten om een nieuwe website aan te maken. Dit betekent:

  1. Check van je Apache configuratie
  2. Aanpassing van je hosts file
  3. Aanmaken van een nieuwe map, Zend Framework beschikbaar maken
  4. Start een nieuw Zend Framework project
  5. Een virtual host inrichten Read More »
8 nov
2010

Werken met Zend Framework

Sinds een paar jaar ben ik gewoon om mijn nieuwe sites te bouwen in het Zend Framework. Ik begon daar al mee bij de Volkskrant; de belangrijkste reden is dat je in een team van developers gedwongen bent om codeerstandaarden in te voeren. Je collega’s moeten gemakkelijk en snel in jouw code kunnen kijken, zonder dat ze moeten zoeken naar de exacte locatie van een bug of feature. Overigens is het gebruik van een framework dat standaarden afdwingt niet alleen handig als je in een team werkt; ook voor solisten is het praktisch. Het is mij ten minste al vaak genoeg voorgekomen dat ik mijn eigen oude code moest nakijken en een uurtje of wat nodig had om mij de eigenaardigheden weer helemaal eigen te maken.

Waarom Zend Framework, en niet Symfony, Code Igniter of een andere? Dat is deels toeval (op zoek naar het beste framework kwam ik deze als eerste tegen), maar voor een groter deel heeft het te maken met de namen achter het framework en Zend. Per slot van rekening is dit Israëlische bedrijf de drijvende kracht achter de Zend Engine, het motortje waarop PHP nu al een flink aantal jaren draait. Je mag verwachten dat een framework dat door Zend is gebouwd, op de beste manier gebruik maakt van de programmeertaal.

Zend Framework (ZFW) is een verzameling losse componenten die je naar believen kunt inzetten in je eigen projecten. Je kunt een volledige website op het ZFW bouwen, waarin je gebruik maakt van het Model-View-Controller ontwerppatroon; of je kunt ervoor kiezen om in een simpele pagina bijvoorbeeld alleen Zend_Validate in te zetten voor het valideren van gebruikersinput.

Zend zelf beweert dat het ZFW heeft uitgevoerd met ‘extreme simpelheid en productiviteit’ in het achterhoofd. Dat is reclame-bullshit. Althans, wat betreft de simpelheid. Vergis je niet, ZFW is niet eenvoudig. Het kost tijd om je erin te verdiepen, en alle hoeken en gaten te leren kennen. Maar die investering betaalt zich dubbel en dwars terug. Als je eenmaal die initiële horden hebt genomen, dan is het werken met het ZFW een genot, en word je wel degelijk productiever. Je hergebruikt de standaard objecten die Zend maakt, maar ook die je zelf hebt opgebouwd of die je hebt gedownload van het internet. Een login-systeem, een webservice, een zoeksysteem, navigatie, sjablonen – uiteindelijk wordt het bouwen van een website niet veel meer dan het opzoeken van de juiste objecten en die in een nieuwe samenstelling met elkaar laten samenwerken.

Er is bij mijn weten geen goede Nederlandstalige tutorialsite voor het ZFW. In dit blog hoop ik enigszins in deze leemte te kunnen voorzien. We gaan een nieuwe applicatie bouwen waarin we gebruik maken van alle snufjes die ZFW, maar ook webservices als Flickr, Google, Amazon en anderen ons te bieden hebben. Het wordt een applicatie gericht op product reviews van e-readers en elektronische boeken, gekoppeld aan een blogging systeem. Tijdens de bouw komen we in aanraking met alle belangrijke componenten van ZFW, en leren we hoe we die het beste met elkaar kunnen laten samenwerken.

Voorkennis: je hoeft niks te weten van Zend, maar wel is het handig als je enigszins thuis bent in het object-georiënteerd programmeren in PHP5. Ook moet je weten hoe je een lokale server aan de praat krijgt en houdt, zodat je een ontwikkelomgeving tot je beschikking hebt waarin je naar hartelust kunt experimenteren. In de komende eerste aflevering komen we te praten over hoe die server ingericht moet worden.

Ik hoop dat je regelmatig terugkomt op dit blog om met me mee te bouwen en de voortgang te volgen. Ben benieuwd naar jullie commentaren en suggesties – laat het alsjeblieft weten als je bijzondere vragen hebt.

Veel plezier!

Follow Me!

Follow Me! Follow Me! Follow Me! Follow Me!