Skip to content

Instantly share code, notes, and snippets.

@OlivierParent
Last active August 29, 2015 14:07
Show Gist options
  • Save OlivierParent/87fad906f3123f040391 to your computer and use it in GitHub Desktop.
Save OlivierParent/87fad906f3123f040391 to your computer and use it in GitHub Desktop.
Symfony.md

NMDAD III — Symfony

New Media Design & Development III - The Symfony Framework Academiejaar 2014-2015, Arteveldehogeschool ©2014 Olivier Parent


[TOC]


I. Installatie en configuratie

1. Ontwikkelomgeving

1.1 Installatie

Zie AHS Laravel Homestead voor instructies.

1.2 Configuratie

De domeinnamen die we gaan gebruiken:

Deze domeinen zijn toegevoegd aan Homestead.yaml:

sites:
    - map: dev.nmdad-iii.arteveldehogeschool.be
      to: /home/vagrant/Code/nmdad-iii.arteveldehogeschool.be/www/web/
      sh: symfony_dev
    - map: www.nmdad-iii.arteveldehogeschool.be
      to: /home/vagrant/Code/nmdad-iii.arteveldehogeschool.be/www/web/
      sh: symfony_prod

2. Database

Tip: Elke webapplicatie heeft bij voorkeur een eigen databasegebruiker met zo weinig mogelijk rechten. Zo maak je het leven van hackers lastiger en bovendien kan de ene applicatie niet per ongeluk de database van een andere applicatie beschadigen.

We gaan een nieuwe databasegebruiker aanmaken voor onze applicatie. We geven die een makkelijk te onthouden naam, en wachtwoord, maar denk eraan dat die voor een productieserver onvoldoende complex zijn.

2.1 MySQL Command-Line Tool

Zie ook:

2.1.1 Aanmelden

Meld je aan op de databaseserver via de MySQL Command-Line Tool en gebruik als credentials:

  • Databasegebruikersnaam: homestead
  • Databasewachtwoord: secret

Aanmelden doe je via mysql:

vagrant@homestead$ mysql -uhomestead -p
password: _
mysql> _
2.1.2 Afmelden

Afmelden doe je met exit

mysql> exit
Bye
vagrant@homestead$ _

2.2 Instellingen Databaseserver

Zie ook:

Open het configuratiebestand my.cnf met een teksteditor:

vagrant@homestead$ sudo nano /etc/mysql/my.cnf

Zoek [mysqld] (MySQL Server Daemon) met <ctrl>+<w> en er onder voegen we dit toe:

character-set-server = utf8
collation-server     = utf8_unicode_ci

Herstart tenslotte de MySQL Server met:

vagrant@homestead$ sudo service mysql restart

Controleer de instellingen:

mysql> SHOW VARIABLES LIKE 'character\_set\_%';
mysql> SHOW VARIABLES LIKE 'collation%';

Of met één enkele query:

mysql> SHOW VARIABLES WHERE Variable_name LIKE 'character\_set\_%' OR Variable_name LIKE 'collation%';

2.3 Databasegebruiker

We moeten enkel een nieuwe databasegebruiker aanmaken en rechten geven. Dit kan met één enkel SQL-statement:

mysql> GRANT ALL PRIVILEGES
    -> ON `nmdad3_arteveldehogeschool_be`.*
    -> TO 'nmdad3_db_user' IDENTIFIED BY 'nmdad3_db_password';

De database bestaat nog niet, maar de nieuwe databasegebruiker heeft nu wel rechten om die zelf aan te maken. We gaan de database niet rechtstreeks in de MySQL Command-Line Tool aanmaken, maar later via de Symfony Console.

3. Framework

3.1 Introductie

Symfony

Symfony is niet alleen een opensource web framework, maar ook een verzameling losstaande componenten die in andere projecten waaronder Laravel (sinds versie 4) of Drupal (sinds versie 8) gebruikt worden.

Symfony word ontwikkeld door Fabien Potencier (@fabpot) en wordt door zijn bedrijf SensioLabs.

Fabien Potentier SensioLabs

3.1.1 Waarom Symfony?

Populair, goede reputatie en Enterprise Level

  • Wordt gebruikt door grote organisaties zoals onder meer de BBC en TED gebruikt.
  • Uitstekende documentatie.
  • Wordt ondersteund door populaire IDE's waaronder NetBeans IDE en PhpStorm.

Matuur, actief ontwikkeld en op regelmatige basis geüpdatet

  • In ontwikkeling sinds 2005
  • Symfony 2.0 in juli 2011 gereleaset
  • Strikte releaseplanning voor de volgende jaren en met LTS (Long Term Support) releases.

Modern, state of the art en front-end friendly

  • PHP 5.3.3+
  • Volgt de industriestandaarden (PSR, PHPUnit) en maakt gebruik van architecurale en design patterns (MVC, DI …) en best practices.
  • Twig is de geïntegreerde Templating Engine.

Opensource

Sterke Documentatie, Community en Commerciële diensten

3.1.2 Quick Tour

Om een beetje vertrouwd te geraken met Symfony kan je de 10 minuten durende Quick Tour volgen.

3.2 Voorbereiding

3.2.1 Composer updaten

We gaan het framework met Composer installeren, maar eerst moeten we er zeker van zijn dat we de allerlaatste versie van Composer gebruiken door te updaten:

vagrant@homestead$ sudo composer self-update

of van achter de proxy:

vagrant@homestead$ proxy on
vagrant@homestead$ sudo -E composer self-update

Tip: Als te veel anonieme requests van hetzelfde IP-adres komen, kan GitHub vragen dat je aanmeldt. Mocht het nodig zijn, dan kan je je GitHub-credentials als volgt configureren in Git::

vagrant@homestead$ git config --global user.name "Olivier Parent"
vagrant@homestead$ git config --global user.email "olivier.parent@arteveldehs.be"
3.2.2 Projectmap maken

We moeten eerst een map maken voor het project. Als naam nemen we dezelfde naam als de domeinnaam:

vagrant@homestead$ mkdir ~/Code/nmdad-iii.arteveldehogeschool.be
vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/

3.3 Installatie

3.3.1 Project aanmaken

In de nieuwe map gaan we met Composer het framework (symfony/framework-standard-edition) installeren in een submap www:

vagrant@homestead$ composer create-project symfony/framework-standard-edition www/

Gebruik volgende opties:

* Would you like to install Acme demo bundle [y/N]  y
* Some parameters are missing. please provide them.
    * database_driver (pdo_mysql):                  <return>
    * database_host (127.0.0.1):                    localhost 
    * database_port (null):                         <return>
    * database_name (symfony):                      nmdad3_arteveldehogeschool_be
    * database_user (root):                         nmdad3_db_user
    * database_password (null):                     nmdad3_db_password
    * mailer_transport (smtp):                      <return>
    * mailer_host (127.0.0.1):                      <return>
    * mailer_user (null):                           <return>
    * mailer_password (null):                       <return>
    * locale (en):                                  <return>
    * secret (ThisTokenIsNotSoSecretChangeIt):      <return>
    * debug_toolbar (true):                         <return>
    * debug_redirects (false):                      <return>
    * use_assetic_controller (true):                <return>

enter image description here

3.3.2 Project updaten

Eens het framework geïnstalleerd is, kan je het met Composer updaten naar de laatste versie:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ composer update

3.4 Beveiliging aanpassen

OPGELET: Na een Composer update kan het nodig zijn dat je de beveiliging opnieuw aangepast moet worden!

Omdat we voor de lokale ontwikkelomgeving niet het domein localhost gebruiken, moeten we de beveiliging aanpassen die verhindert dat bestanden die enkel binnen de ontwikkelomgeving gebruikt mogen worden op de productieomgeving uitgevoerd kunnen worden.

De twee bestanden die we moeten aanpassen zijn app_dev.php en config.php.

Mappenstructuur

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── web/
        ├── app_dev.php
        └── config.php

Dit doen we eenvoudigweg door de beveiliging enkel uit te voeren als het domein niet dev.nmdad-iii.arteveldehogeschool.be is.

if ('dev.nmdad-iii.arteveldehogeschool.be' !== $_SERVER['HTTP_HOST']) {
    …
}
3.4.1 In app_dev.php
if ('dev.nmdad-iii.arteveldehogeschool.be' !== $_SERVER['HTTP_HOST']) {
    if (isset($_SERVER['HTTP_CLIENT_IP'])
        || isset($_SERVER['HTTP_X_FORWARDED_FOR'])
        || !(in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', 'fe80::1', '::1')) || php_sapi_name() === 'cli-server')
    ) {
        header('HTTP/1.0 403 Forbidden');
        exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
    }
}

Test met http://dev.nmdad-iii.arteveldehogeschool.be/

3.4.2 In config.php
if ('dev.nmdad-iii.arteveldehogeschool.be' !== $_SERVER['HTTP_HOST']) {
    if (!in_array(@$_SERVER['REMOTE_ADDR'], array(
        '127.0.0.1',
        '::1',
    ))
    ) {
        header('HTTP/1.0 403 Forbidden');
        exit('This script is only accessible from localhost.');
    }
}

Test met http://dev.nmdad-iii.arteveldehogeschool.be/config.php

3.5 Controle van de vereisten

De volgende stap is controleren of de serveromgeving voldoet aan de vereisten van Symfony.

Mappenstructuur

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── app/
        └── check.php
vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ php app/check.php

We krijgen twee waarschuwingen:

  • xdebug.max_nesting_level should be above 100 in php.ini Set "xdebug.max_nesting_level" to e.g. "250" in php.ini* to stop Xdebug's infinite recursion protection erroneously throwing a fatal error in your project.
  • intl extension should be available Install and enable the intl extension (used for validators).
3.5.1 Xdebug configureren

We passen het Xdebug-configuratiebestand aan met nano:

vagrant@homestead$ sudo nano /etc/php5/mods-available/xdebug.ini

En voegen volgende lijn toe om de waarschuwing op te lossen:

xdebug.max_nesting_level = 250

Tenslotte moeten we de PHP-service herstarten om de wijzigingen actief te maken op de webserver:

vagrant@homestead$ sudo service php5-fpm restart

Tip: Je kan de lijn ook toevoegen met één enkele opdracht:

vagrant@homestead$ sudo sh -c "echo xdebug.max_nesting_level = 250 >> /etc/php5/mods-available/xdebug.ini" && sudo service php5-fpm restart
3.5.2 intl installeren met apt-get

De PHP Internationalization Functions installeer je met:

vagrant@homestead$ sudo apt-get update && sudo apt-get install -y php5-intl

Of achter de proxy:

vagrant@homestead$ proxy on
vagrant@homestead$ sudo -E apt-get update && sudo -E apt-get install -y php5-intl

Ga naar http://dev.nmdad-iii.arteveldehogeschool.be/config.php

3.6 Git Ignore

Om Git bepaalde bestanden en mappen van de IDE en het besturingssysteem te laten negeren moeten we het bestand .gitignore aanpassen:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ nano .gitignore

Het resultaat moet er zo uitzien:

/web/bundles/
/app/bootstrap.php.cache
/app/cache/*
/app/config/parameters.yml
/app/logs/*
!app/cache/.gitkeep
!app/logs/.gitkeep
/build/
/vendor/
/bin/
/composer.phar

# Optional files to ignore
/.idea
/composer.lock
.DS_Store
Thumbs.db

4. PhpStorm

4.1 Installeren

Download en installeer de allerlaatste versie van PhpStorm.

Tip: Zie Project and IDE Settings om mappen en instellingen van vorige versies te verwijderen.

4.2 Nieuw project dat reeds bestaat

Welcome to PhpStormCreate New Project from Existing FilesSource files are in a local directory, no Web server is yet configured.

Rechtsboven klikken op Edit Configurations…+PHP Web Application

Nieuwe servers aanmaken:

  • Name: development
    • Host: dev.nmdad-iii.arteveldehogeschool.be : 80 Xdebug
  • Name: production
    • Host: www.nmdad-iii.arteveldehogeschool.be : 80 Xdebug

Run configuraties aanmaken:

  • Name: dev
    • Server: Development Server
    • Start URL: /
  • Name: prod
    • Server: Production Server
    • Start URL: /

4.3 Instellingen

  • PhpStormPreferences…: Project Settings
    • PHP
      • PHP language level: 5.6
      • Interpreter:+Remote…:
        • Vagrant
        • Vagrant Instance Folder: ~/Homestead
    • SSH Terminal
      • Current Vagrant
    • Vagrant
      • Vagrant executable: vagrant
      • Instance folder: ~/Homestead

II. Framework gebruiken

Zie ook:

  • [Symfony / The Book](http://symfony.com/doc/current/book/index.html)
    
  • [Symfony / The Cookbook](http://symfony.com/doc/current/cookbook/index.html)
    
  • [Symfony / The Components](http://symfony.com/doc/current/components/index.html)
    
  • [Symfony / Reference](http://symfony.com/doc/current/reference/index.html)
    
  • [Symfony / Bundles](http://symfony.com/doc/current/bundles/index.html)
    
  • [Symfony / Best Practices](http://symfony.com/doc/current/best_practices/index.html)
    

1. Bundle Maken

Zie ook:

Een Symfony-applicatie bestaat uit minstens één Bundle. Een Bundle is een modulair deel van de applicatie die je bouwt. Het is een map met deze elementen:

  • Configuratiebestanden;
  • PHP-klasses;
  • JavaScript-bestanden;
  • CSS-bestanden;

De map staat in een map die de naamruimte voorstelt en deze staat in de src/ map.

Mappenstructuur

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/

Je een applicatie bijvoorbeeld opsplitsen in aparte Bundles voor:

  • Front-office
  • Back-office
  • API
  • ...

Wat we gaan doen

We gaan vier Bundles maken:

  • CommonBundle: voor de gemeenschappelijke delen van de app, zoals de klasses voor Modellen.
  • BackOfficeBundle
  • FrontOfficeBundle
  • ApiBundle: voor de RESTful service.

We gaan annotations gebruiken om de Routes te bepalen.

1.1 CommonBundle

vagrant@homestead$ php app/console generate:bundle --namespace=Artevelde/CommonBundle --format=annotation

Met volgende opties:

* Bundle name [ArteveldeCommonBundle]:                              <return>
* Target directory
  [/home/vagrant/Code/nmdad-iii.arteveldehogeschool.be/www/src]:    <return>
* Do you want to generate the whole directory structure [no]?       <return>
* Do you confirm generation [yes]?                                  <return>
* Confirm automatic update of your Kernel [yes]?                    <return>
* Confirm automatic update of the Routing [yes]?                    <return>

1.2 BackOfficeBundle

vagrant@homestead$ php app/console generate:bundle --namespace=Artevelde/BackOfficeBundle --format=annotation

Met volgende opties:

* Bundle name [ArteveldeBackOfficeBundle]:                          <return>
* Target directory
  [/home/vagrant/Code/nmdad-iii.arteveldehogeschool.be/www/src]:    <return>
* Do you want to generate the whole directory structure [no]?       <return>
* Do you confirm generation [yes]?                                  <return>
* Confirm automatic update of your Kernel [yes]?                    <return>
* Confirm automatic update of the Routing [yes]?                    <return>

1.3 FrontOfficeBundle

vagrant@homestead$ php app/console generate:bundle --namespace=Artevelde/FrontOfficeBundle --format=annotation

Met volgende opties:

* Bundle name [ArteveldeFrontOfficeBundle]:                         <return>
* Target directory
  [/home/vagrant/Code/nmdad-iii.arteveldehogeschool.be/www/src]:    <return>
* Do you want to generate the whole directory structure [no]?       <return>
* Do you confirm generation [yes]?                                  <return>
* Confirm automatic update of your Kernel [yes]?                    <return>
* Confirm automatic update of the Routing [yes]?                    <return>

1.4 ApiBundle

vagrant@homestead$ php app/console generate:bundle --namespace=Artevelde/ApiBundle --format=annotation

Met volgende opties:

* Bundle name [ArteveldeApiBundle]:                                 <return>
* Target directory
  [/home/vagrant/Code/nmdad-iii.arteveldehogeschool.be/www/src]:    <return>
* Do you want to generate the whole directory structure [no]?       <return>
* Do you confirm generation [yes]?                                  <return>
* Confirm automatic update of your Kernel [yes]?                    <return>
* Confirm automatic update of the Routing [yes]?                    <return>

1.5 Bundle Verwijderen

Zie ook:

Voordat je een Bundle verwijdert moeten ook alle referenties naar de Bundle in deze bestanden verwijderd worden:

  • app/AppKernel.php
  • app/config/routing.yml

Mappenstructuur

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── app/
        └── AppKernel.php

2. Models

Zie ook:

Gegevens op een website zouden verdwijnen van zodra de gebruikersessie op de server eindigt, tenzij het ergens opgeslagen kan worden waar het niet meer kan wijzigen. In de computerwetenschappen noemt men dit Persistentie (Persistence). Om gegevens van een website te persisteren, worden die op een harde schijf (meestal meerdere harde schijven) opgeslagen. Dit kan eenvoudigweg door een bestand weg te schrijven, maar het is in de meeste gevallen makkelijker om een DBMS te gebruiken, een 'database' laten we maar zeggen.

Gegevens worden opgeslagen in modellen die vervolgens in de persistentielaag opgeslagen moeten worden. De interactie met de persistente opslag zoals een RDBMS gebeurt in de meeste gevallen door vele complexe databasequery's te schrijven in een bepaald SQL-dialect en de resultaatarray's naar OOP-objecten om te zetten en vice versa. Uiteraard is dit een moeilijk, traag en vermoeiend proces, daarenboven vergt het overschakelen op een ander DBMS veel wijzigingen in de code. Een DAL (Database Abstraction Layer) kan dit gevoelig vereenvoudigen door SQL (met zijn vele dialecten) schrijven voor zijn rekening te nemen. Bovenop een DAL kan een ORM (Object Relational Mapper) gebouwd worden.

2.1 ORM (Object Relational Mapper)

Een Object Relational Mapper kan OOP-objecten mappen op databasetabellen. Het ORM zal dus objecten wegschrijven naar rijen in databasetabellen en de rijen terug uitlezen en omzetten naar objecten.

Symfony ondersteunt verschillende ORM's om de database aan te spreken. De twee ORM's die out-of-the-box ondersteund worden zijn:

2.1.1 Doctrine

We kiezen voor Doctrine ORM omdat deze populairder is, en omdat de maker van Symfony actief meewerkt aan het project. Doctrine ORM is bovenop Doctrine DBAL gebouwd. Dit is een DAL (Database Abstraction Layer). Concreet wil dit zeggen dat het op vlak van code vrijwel geen verschil maakt welk DBMS er gebruikt wordt, behalve dan enkele specifieke configuratie-instellingen. Er moet wel nog altijd rekening gehouden worden met het specifieke SQL-dialect en beschikbare functionaliteiten voor elk DBMS, maar in principe zou het overschakelen van het ene systeem op het andere relatief eenvoudig moeten zijn. Het nadeel van ORM is wel dat je niet alle functionaliteiten van je DBMS zal kunnen gebruiken, omdat het ORM compatibel moet zijn met alle ondersteunde systemen.

Symfony en Doctrine ORM ondersteunen verschillende DBMS'en via een Doctrine DBAL-stuurprogramma, waaronder deze:

Database Stuurprogramma Type Beschikbaar op Laravel Homestead?
IBM DB2 ibm_db2 Native Zelf te installeren
IBM DB2 pdo_ibm PDO Zelf te installeren
Microsoft SQL Server pdo_sqlsrv PDO Nee
MySQL pdo_mysql PDO Ja
Oracle oci8 Native Zelf te installeren
Oracle pdo_oci PDO Zelf te installeren
ProgreSQL pdo_pgsql PDO Ja
SQLite pdo_sqlite PDO Ja
2.1.2 Beginnen met ORM

Er zijn drie mogelijke pistes om met ORM te beginnen:

  1. Code First
  2. Model First
  3. Database First

Code First Eerst de code schrijven en pas daarna de database opzetten via het ORM. Dit heeft als voordeel dat je code compatibel is met zoveel mogelijk DBMS'en, zodat makkelijk naar een ander systeem gemigreerd kan worden.

Model First Beginnen met het modelleren van de gegevens met een tool (bijv. MySQL Workbench).

Database First Beginnen met het aanmaken van de database, of een reeds bestaande database gebruiken. Vertrekken vanaf een bestaande database laat toe dat functionaliteiten gebruikt die speciek zijn voor je DBMS. Je kan dus de maximale efficiëntie halen uit het systeem door zo weinig mogelijk compromissen te sluiten. Het nadeel is dan wel dat je project sterk verankerd is met het gebruikte DBMS.

2.2 Modellen

Symfony maakt gebruik van het Model-View-Controller (MVC) architectural pattern. In Doctrine ORM noemt men het Model-gedeelte een Entity, omdat ze een entiteit in de databasetabel voorstellen.

Een Entity kan optioneel ook een Entity Respository-klasse hebben. Deze klasse zal zorgen voor de link tussen Entity en Database Tabel. Deze klasse is enkel nodig als je zelf het lezen en wegschrijven naar de databasetabel wil regelen.

Wat we gaan doen

We gaan een miniblog maken met:

  • Een Post (Article of Image)
  • Een Post behoort tot een Category, dit kunnen er zelfs verscheidene zijn
  • Een Category kan tot een andere Category behoren.
2.2.1 Entity-klasses Genereren

Zie ook:

Doctrine ondersteunt de volgende datatypes die:

  • array, simple_array, json_array
  • object
  • boolean, integer, smallint, bigint
  • string, text
  • datetime, datetimetz (tz: tijdzone, niet compatibel met MySQL!), date, time
  • decimal
  • float
  • blob
  • guid

OPGELET: Er zijn een aantal DBMS-vendorspecifieke issues.

Zie ook:

Zo wordt datetimetz niet ondersteund door MySQL, en wordt die door datetime vervangen. Dit leidt tot problemen met de synchronisatie van het Schema.

We maken een entity-klasses met deze gegevens:

Entity Field name Field type Opmerking
User name: users
first_name string length: 255
last_name string length: 255
birthday date nullable
created_at datetime
enabled_at datetime nullable
locked_at datetime nullable
expired_at datetime nullable
expired_credentials_at datetime nullable
updated_at datetime nullable
deleted_at datetime nullable
username string length: 255, unique
password string length: 64, fixed
PostAbstract name: posts
title string length: 255
created_at datetime
updated_at datetime nullable
deleted_at datetime nullable
published_at datetime nullable
Article name: articles
body text
Image name: images
uri string length: 255
Category name: categories
name string length: 255
created_at datetime
updated_at datetime nullable
deleted_at datetime nullable

Via de Symfony Console kunnen we Doctrine ORM een Entity-klasse laten generen in src/Artevelde/CommonBundle/Entity.

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ php app/console doctrine:generate:entity --entity=ArteveldeCommonBundle:User

Met volgende opties:

*    The Entity shortcut name [ArteveldeCommonBundle:User]:            <return>
*    Configuration format (yml, xml, php, or annotation) [annotation]: <return>
*    New field name (press <return> to stop adding fields):            first_name
*    Field type [string]:                                              <return>
*    Field length [255]:                                               <return>
*    New field name (press <return> to stop adding fields):            last_name
*    Field type [string]:                                              <return>
*    Field length [255]:                                               <return>
*    New field name (press <return> to stop adding fields):            created_at
*    Field type [string]:                                              datetime
*    … 
*    New field name (press <return> to stop adding fields):            username
*    Field type [string]:                                              <return>
*    Field length [255]:                                               <return>
*    New field name (press <return> to stop adding fields):            password
*    Field type [string]:                                              <return>
*    Field length [255]:                                               64
*    New field name (press <return> to stop adding fields):            <return>
*    Do you want to generate an empty repository class [no]?           y
*    Do you confirm generation [yes]?                                  <return>

Daarna maken we nog de overige Entity-klasses aan:

vagrant@homestead$ php app/console doctrine:generate:entity --entity=ArteveldeCommonBundle:Article
vagrant@homestead$ php app/console doctrine:generate:entity --entity=ArteveldeCommonBundle:Image
vagrant@homestead$ php app/console doctrine:generate:entity --entity=ArteveldeCommonBundle:Category

Post noemen we PostAbstract omdat Post enkel zal gebruikt worden om van over te erven en niet als instantie. We hebben ook geen empty repository class nodig.

vagrant@homestead$ php app/console doctrine:generate:entity --entity=ArteveldeCommonBundle:PostAbstract

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/
        └── Artevelde/
            └── CommonBundle/
                └── Entity/
                    ├── Article.php
                    ├── ArticleRepository.php
                    ├── Category.php
                    ├── CategoryRepository.php
                    ├── Image.php
                    ├── ImageRepository.php
                    ├── PostAbstract.php
                    ├── User.php
                    └── UserRepository.php
2.2.2 Entity-klasses Aanpassen
A. Tabelnaam

Zie ook:

We moeten elke Entity-klasse aanpassen zodat we een eigen naam aan de tabel kunnen geven, zoniet wordt de naam van de Entity gebruikt. Dit doen we door name="" toe te voegen aan @ORM\Table() met de naam van de tabel. We gebruiken telkens de meervoudsvorm, in kleine letters geschreven:

  • @ORM\Table(name="articles")
  • @ORM\Table(name="categories")
  • @ORM\Table(name="images")
  • @ORM\Table(name="posts")
  • @ORM\Table(name="users")
B. Eigenschappen

Zie ook:

Kolomeigenschappen in @ORM\Column()
CHAR(64) type="string", length=64, options={"fixed":true}
NULL nullable=true
UNIQUE unique=true
INT UNSIGNED type="integer", options={"unsigned":true}

Pas ook alle $id's aan zodat ze UNSIGNED zijn.

C. Standaard Huidige Tijd voor een DATETIME

Hoewel je een optie "default": bestaat, kan je die niet gebruiken voor de huidige tijd. Je kan de kolomdefinitie manueel aanpassen met columnDefinition (met alle problemen van dien qua synchronisatie van het schema), maar het is makkelijker om de huidige tijd voor created_at toevoegen via de Constructor van de Entity-klasse:

    public function __construct()
    {
        $this->createdAt = new \DateTime();
    }
D. Overerving

Zie ook:

  1. De Entity's Article en Image erven over van PostAbstract:

    • PostAbstract.php
      • Vervang class PostAbstract door abstract class PostAbstract zodat nooit een instantie van PostAbstract gemaakt kan worden.
      • Vervang private $id door protected $id
    • Article.php
      • Vervang class Article door class Article extends PostAbstract
      • Vervang private $id door protected $id
    • Image.php
      • Vervang class Image door class Image extends PostAbstract
      • Vervang private $id door protected $id
  2. We moeten overal private $id vervangen door protected $id

/**
 * PostAbstract
 *
 * @ORM\Table(name="posts")
 * @ORM\Entity
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="discriminator", type="string", length=1)
 * @ORM\DiscriminatorMap({
 *     PostAbstract::POST_ARTICLE="Article",
 *     PostAbstract::POST_IMAGE  ="Image"
 * })
 *
 */
abstract class PostAbstract
{
    const POST_ARTICLE = 'A';
    const POST_IMAGE   = 'I';
// …

PhpStorm Tip: Met <alt>+<klik> kan je meerdere cursors gebruiken, wat handig is om meerdere lijnen tegelijk aan te passen. Met <esc> maak je de meerdere cursors ongedaan.

E. Relaties leggen

Zie ook:

2.2.3 Beveiliging van de entity User

Zie ook:

A. Entity uitbreiden

Er zit al een stuk van gebruikersfunctionaliteit in Symfony waarvan je de interfaces kan implementeren:

  • UserInterface moet deze methodes implementeren:
    • getUsername()
    • getSalt()
    • getPassword()
    • getRoles()
    • eraseCredentials()
  • AdvancedUserInterface moet bovendien ook nog deze methodes implementeren:
    • isAccountNonExpired()
    • isAccountNonLocked()
    • isCredentialsNonExpired()
    • isEnabled()
B. Beveiligingsinstellingen

In app/config/security.yml:

security:
    encoders:
#        Symfony\Component\Security\Core\User\User: plaintext
        Artevelde\CommonBundle\Entity\User:
            algorithm: bcrypt
            cost: 15
#
    providers:
#        in_memory:
#            memory:
#                users:
#                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
#                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
        default:
            entity: { class: ArteveldeCommonBundle:User, property: username }

Tip: Controleer de geldigheid van het YAML-bestand met de Console-opdracht opdracht yaml:lint:

vagrant@homestead$ php app/console yaml:lint app/config/security.yml

2.3 Database en Schema

In het DBMS moet eerst een database aangemaakt worden. Doctrine maakt een onderscheid tussen een Database en een Schema. In principe is een Schema een naamruimte binnen een Database. Het is eigenlijk een verzameling databaseobjecten (bijv. tabellen).

Opmerking: De interpretatie van wat de termen 'Database' en 'Schema' precies zijn, verschilt heel erg van databasemanagementsysteem tot databasemanagementsysteem. Zo maakt MySQL geen onderscheid tussen beide (de SQL-keywords DATABASE en SCHEMA synoniemen voor MySQL), maar doet PostgreSQL dat dan weer wel.

Doctrine kan de database aanmaken (en verwijderen), op voorwaarde dat:

  • de databaseconfiguratie in app/config/parameters.yml correct is;
  • de databasegebruiker de nodige rechten heeft op de database;
  • de database nog niet bestaat.
2.3.1 Database Aanmaken

De database aanmaken doe je met de Console-opdracht doctrine:database:create:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ php app/console doctrine:database:create
2.3.2 Database Verwijderen

Om de database te verwijderen gebruik je de Console-opdracht doctrine:database:drop met de optie --force (omdat verwijderen een risicovolle opdracht is):

vagrant@homestead$ php app/console doctrine:database:drop --force
2.3.3 Schema Aanmaken

Het schema (met tabellen op basis van de Modellen) maken doe je met de Console-opdracht doctrine:schema:create:

vagrant@homestead$ php app/console doctrine:schema:create

Tip: Gebruik de optie --dump-sql om de SQL-statements te zien, zonder ze uit te voeren.

2.3.4 Schema Bijwerken

Na elke wijziging aan de Entity's moet het schema bijgewerkt worden. Het schema bijwerken doe je met de Console-opdracht doctrine:schema:update met de optie --force (omdat bijwerken een risicovolle opdracht is):

vagrant@homestead$ php app/console doctrine:schema:update --force

Tip: Gebruik de optie --dump-sql (in plaats van --force) om de SQL-statements te zien, zonder ze uit te voeren.

2.3.5 Schema Verwijderen

Om het schema te verwijderen gebruik je de Console-opdracht doctrine:schema:drop met de optie --force (omdat verwijderen een risicovolle opdracht is):

vagrant@homestead$ php app/console doctrine:schema:drop --force
2.3.5 Schema Valideren

Om te controleren of er geen fouten in de Entity's staan en of alles up to date is, kan je het schema laten valideren met de Console-opdracht doctrine:schema:validate:

 vagrant@homestead$ php app/console doctrine:schema:validate

2.4 EER-diagram

Tip: Je kan met MySQL Workbench via Reverse Engineer een EER-diagram laten genereren van je schema.

2.5 Doctrine Data Fixtures

Zie ook:

Data Fixtures worden gebruikt om testgegevens te seeden (in de database inladen).

2.5.1 DoctrineFixturesBundle Installeren
A. Voorbereiding

Eerst moeten we zorgen dat we de allerlaatste versie van Composer hebben:

vagrant@homestead$ sudo composer self-update

Daarna updaten we ook ons Symfony-project:

vagrant@homestead$ composer update
B. Bundle Installeren

Dan installeren we de we de Bundle met Composer:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ composer require doctrine/doctrine-fixtures-bundle:"2.2.*"

Dan moeten we de bundle nog registreren in door deze onder DoctrineBundle() toe te voegen aan de array $bundles in app/AppKernel.php:

    public function registerBundles()
    {
        $bundles = array(
            // …
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),
            // …
        );
    }
C. Bundle Testen

Vraag alle Console-opdrachten voor doctrine:fixtures op met list:

vagrant@homestead$ php app/console list doctrine:fixtures
2.5.2 Data Fixtures Aanmaken

Zie ook:

A. Mappen en Bestand Aanmaken

We maken de mappen aan met mkdir en de optie -p of (--parents) om alle niet-bestaande mappen in het pad ook aan te maken:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ mkdir -p src/Artevelde/CommonBundle/DataFixtures/ORM/

We maken 3 fixtures aan, voor de Entity's User, Article en Image.

Opmerking: De fixtures worden uitgevoerd volgens de alfabetische volgorde van de bestandsnaam. Om zelf de volgorde te kiezen kan je een prefix toevoegen aan de bestandsnaam, zoals bijvoorbeeld: 001_LoadArticleData.php

Met touch kunnen we alvast de lege bestanden aanmaken:

vagrant@homestead$ cd src/Artevelde/CommonBundle/DataFixtures/ORM/
vagrant@homestead$ touch LoadArticleData.php
vagrant@homestead$ touch LoadImageData.php
vagrant@homestead$ touch LoadUserData.php

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/
        └── Artevelde/
            └── CommonBundle/
                └── DataFixtures/
                    └── ORM/
                        ├── LoadArticleData.php
                        ├── LoadImageData.php
                        └── LoadUserData.php
B. Data Fixture Aanmaken

In de Data Fixture-klasse implementeren we de methode om de volgorde van uitvoeren te bepalen en implementeren we ook de methode om de gegevens in te laden.

Tip: Standaard wordt de volgorde van uitvoeren bepaald door de alfabetische volgorde van de bestandsnaam (niet van de fixture-klassenaam). Je kan de volgorde ook laten bepalen door twee interfaces. Maar OPGELET, je moet dan één van beide kiezen, want ze kunnen niet door elkaar gebruikt worden!

  1. Als de interface OrderedFixtureInterface geïmplementeerd wordt, dan bepaalt de methode getOrder() de volgorde van uitvoeren door een integer terug te geven. De alfabetische volgorde van de bestandsnaam wordt dan genegeerd. Met deze manier kan je heel precies de volgorde van uitvoeren bepalen, maar kan veel werk vergen en complex zijn als er veel fixtures zijn.

  2. Als de interface DependentFixtureInterface geïmplementeerd wordt, dan bepaalt de methode getDependencies() de volgorde van uitvoeren door een array met volledig gekwalificeerde fixture-klassenamen terug te geven. Hiervoor kan je ook __NAMESPACE__ gebruiken. Met deze manier heb je iets minder controle over de volgorde van uitvoeren, maar ze is wel flexibeler dan de eerste manier.

We gebruiken we $this->addReference() (overgeërfd van AbstractFixture) om een referentie naar de User-objecten te bewaren voor gebruik in een volgende Data Fixture.

In het bestand LoadUserData.php:

<?php

namespace Artevelde\CommonBundle\DataFixtures\ORM;

use Artevelde\CommonBundle\Entity\User;

use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

class LoadUserData extends AbstractFixture implements OrderedFixtureInterface
{
    /**
     * {@inheritdoc}
     */
    public function getOrder()
    {
        return 1; // The order in which fixture(s) will be loaded.
    }

    /**
     * {@inheritdoc}
     */
    public function load(ObjectManager $em)
    {
        $userA = new User();
        $em->persist($userA); // Manage Entity for persistence.
        $userA
            ->setFirstName('Test User')
            ->setLastName('A')
            ->setUsername('testuserA')
            ->setPassword('testuserA');
        $this->addReference('TestUserA', $userA); // Reference for the next Data Fixture(s).

        $userB = new User();
        $em->persist($userB); // Manage Entity for persistence.
        $userB
            ->setFirstName('Test User')
            ->setLastName('B')
            ->setUsername('testuserB')
            ->setPassword('testuserB');
        $this->addReference('TestUserB', $userB); // Reference for the next Data Fixture(s).

        $em->flush(); // Persist all managed Entities.
    }
}

In het bestand LoadArticleData.php:

<?php

namespace Artevelde\CommonBundle\DataFixtures\ORM;

use Artevelde\CommonBundle\Entity\Article;

use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

class LoadArticleData extends AbstractFixture implements OrderedFixtureInterface
{
    /**
     * {@inheritdoc}
     */
    public function getOrder()
    {
        return 2; // The order in which fixture(s) will be loaded.
    }

    /**
     * {@inheritdoc}
     */
    public function load(ObjectManager $em)
    {
        $articleA = new Article();
        $em->persist($articleA); // Manage Entity for persistence.
        $articleA
            ->setTitle('Test Artikel A')
            ->setBody('Lorem Ipsum A')
            ->setUser($this->getReference('TestUserB')); // Get reference from a previous Data Fixture.

        $articleB = new Article();
        $em->persist($articleB); // Manage Entity for persistence.
        $articleB
            ->setTitle('Test Artikel B')
            ->setBody('Lorem Ipsum B')
            ->setUser($this->getReference('TestUserA')); // Get reference from a previous Data Fixture.

        $em->flush(); // Persist all managed Entities.
    }
}
C. Wachtwoorden Hashen in een Fixture

Om wachtwoorden te hashen hebben we de service security.encoder_factory nodig die we via de Service Container moeten aanspreken. In tegenstelling tot een Controller heeft een Data Fixture niet zomaar toegang tot de Service Container.

Tip: Je kan alle services zien met deze Console-opdracht container:debug.

vagrant@homestead$ php app/console container:debug | grep 'security'

We kunnen de Service Container aanspreken als we eerst ContainerAwareInterface implementeren.

// …
class LoadUserData extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
{
    private $container;

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    public function hashPassword($user)
    {
        $factory = $this->container->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        $hashedPassword = $encoder->encodePassword($user->getPassword(), $user->getSalt());
        $user->setPassword($hashedPassword);
    }
// …
}
2.5.3 Data Fixtures Inladen

Met de Console-opdracht doctrine:fixtures:load kan je de gegevens inladen:

vagrant@homestead$ cd ~/Code/…-iii.arteveldehogeschool.be/www/
vagrant@homestead$ php app/console doctrine:fixtures:load

Met deze optie:

* Careful, database will be purged. Do you want to continue Y/N             y

OPGELET: Voor het inladen wordt de database geleegd ('purged'). Indien je dit niet wenst moet je de --append (toevoegen) optie toevoegen:

vagrant@homestead$ php app/console doctrine:fixtures:load --append

Je kan de opdracht ook zonder interactie laten uitvoeren (zal de gegevens wissen):

vagrant@homestead$ php app/console doctrine:fixtures:load --no-interaction

2.6 Doctrine Migrations

Zie ook:

Migrations zijn een soort van versiebeheer op het niveau van databaseschema's.

2.6.1 DoctrineMigrationsBundle Installeren
A. Voorbereiding

Eerst moeten we zorgen dat we de allerlaatste versie van Composer hebben:

vagrant@homestead$ sudo composer self-update

Daarna updaten we ook ons Symfony-project:

vagrant@homestead$ composer update
B. Bundle Installeren

Dan installeren we de we de Bundle met Composer:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ composer require doctrine/migrations:"1.0.*@dev" doctrine/doctrine-migrations-bundle:"2.1.*@dev"

Dan moeten we de bundle nog registreren in door deze onder DoctrineBundle() en DoctrineFixturesBundle() toe te voegen aan de array $bundles in app/AppKernel.php:

    public function registerBundles()
    {
        $bundles = array(
            // …
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),
            new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
            // …
        );
    }
C. Bundle Testen

Vraag alle Console-opdrachten voor doctrine:migrations op met list:

vagrant@homestead$ php app/console list doctrine:migrations
2.6.1 DoctrineMigrationsBundle Gebruiken

Migrations vervangen de Console-opdrachten van doctrine:schema grotendeels.

A. Migration Status

De huidige status van de migrations opvragen doe je met:

vagrant@homestead$ php app/console doctrine:migrations:status
B. Migration Opslaan

Je kan wijzigingen in het schema bewaren met:

vagrant@homestead$ php app/console doctrine:migrations:diff

Dit maakt een versiebestand aan in de map app/DoctrineMigrations/ waarin de schemawijzigen worden bijgehouden, vergelijkbaar met doctrine:schema:update --dump-sql. Daarnaast bevat het ook instructies om de wijzigingen weer ongedaan te maken.

Opmerking: Als deze Console-opdracht voor de eerste keer uitgevoerd wordt, dan wordt er ook een lege tabel migrations_version gemaakt, waarin de uitgevoerde Migrations bijgehouden worden.

Het bestand heeft de vorm VersionJJJJMMDDuummss.php, waarbij JJJJMMDDuummss het versienummer voorstelt in de vorm van datum en tijd.

C. Migration Uitvoeren

De laatste Migration uitvoeren ('migreren') doe je met deze Console-opdracht:

vagrant@homestead$ php app/console doctrine:migrations:migrate
D. Migrations Beheren

Je kan naar een bepaalde versie migreren door het versienummer (JJJJMMDDuummss) achter de Console-opdracht doctrine:migrations:migrate te zetten:

vagrant@homestead$ php app/console doctrine:migrations:migrate JJJJMMDDuummss

Je kan altijd teruggaan naar de oorspronkelijke toestand (versie 0) van voor de migratie met:

vagrant@homestead$ php app/console doctrine:migrations:migrate 0

2.7 Doctrine Migrations uitbreiden met Doctrine Data Fixtures

Zie ook:

Om de Migrations uit te breiden met Data Fixtures moeten we de Console kunnen aanspreken vanuit de Migrations. Om de Symfony Console aan te spreken moeten we eerst toegang hebben tot de Service Container, daarom moeten we de ContainerAwareInterface implementeren, zodat de Service Container via de methode setContainer() in de Migration geïnjecteerd wordt. Deze injectie gebeurt door middel van een speciaal Design Pattern dat Dependency Injection heet.

In een nieuwe methode loadFixtures() kunnen we aan de Symfony HttpKernel (de kern van Symfony) via de Service Container, en deze HttpKernel hebben we nodig om een nieuwe Console Application instantie te maken waarmee we de Symfony Console kunnen aanspreken.

Eens de up() methode gedaan heeft, kan de loadFixtures() methode aangeroepen worden vanuit de postUp() methode.

app/DoctrineMigrations/VersionJJJJMMDDuummss.php:

// …
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
// …
class VersionJJJJMMDDuummss extends AbstractMigration implements ContainerAwareInterface
{
    private $container;

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    public function loadFixtures($fixturesDirectory, $append = true)
    {
        $input = new ArrayInput([
            'command'    => 'doctrine:fixtures:load',
            '--fixtures' => $fixturesDirectory,
            '--append'   => (boolean) $append,
        ]);

        $kernel = $this->container->get('kernel');
        $application = new Application($kernel);
        $application->setAutoExit(false);
        $application->run($input);
    }

    public function postUp(Schema $schema)
    {
        $this->loadFixtures('src/Artevelde/CommonBundle/DataFixtures/ORM');
    }
// …    

3. Testing

Zie ook:

3.1 Inleiding

3.1.1 Soorten Testen

Er zijn verschillende soorten testen. Hieronder zijn er een aantal courante testen beschreven van gedetailleerd naar globaal.

A. Unit Testing

De Unit Test test bouwstenen (units of code) van een applicatie correct werken volgens de specificaties. Een unit is doorgaans een volledige klasse, maar kan bijvoorbeeld ook een functie of methode zijn.

B. Integration Testing

De Integration Test test of de bouwstenen correct samen werken volgens de specificaties..

C. Functional Testing

De Functional Test test een functionaliteit van een applicatie. Een Functional Test zegt weinig over de interne werking van de code en is dus een aanvulling op Unit Testing en Integration Testing.

D. Acceptance Testing

De Acceptance Test test of de applicatie werkt volgens de specificaties van de opdrachtgever. Na het succesvol doorlopen van de Acceptance Testing, accepteert de opdrachtgever het eindresultaat op als zijnde voldoende voor oplevering.

Hiervoor worden Acceptance Criteria gebruikt. Deze kunnen zowel functioneel (functionaliteiten) als niet-functioneel zijn (bijv. snelheid, gebruiksvriendelijkheid, etc.).

3.1.2 Softwareontwikkelingsmethodologieën

Omdat testen vaak niet of te weinig uitgevoerd worden (omwille van bijvoorbeeld hybris van de ontwikkelaar, te kleine budgetten, tijdsgebrek, etc.); heeft men een aantal softwareontwikkelingsmethodologieën ontwikkeld waarbij men begint met het schrijven van testen voordat met aan de eigenlijke softwareontwikkeling begint.

Deze methodologieën zijn allen nauw verbonden met Agile Software Development.

A. TDD (Test-Driven Development)

TDD wordt toegeschreven aan de Amerikaan Kent Beck, die ook XP (Extreme Programming) bedacht, een vorm van Agile Software Development. TDD vorm een onderdeel van XP.

Bij TDD begint men met het schrijven van een test en voert men alle testen uit om na te gaan of alle testen nog werken, vooraleer men de eigenlijke code schrijft.

B. BDD (Behaviour-Driven Development)

De Brit Dan North breidde TDD verder uit tot BDD, een methodologie die niet enkel voor programmeurs bedoeld was, maar ook voor testers, analisten, projectmanagers, domeinexperts, etc. BDD heeft als extra troef dat de documenten ook door niet-programmeurs gelezen en geschreven kunnen worden.

Een deel van de mosterd werd bij DDD (Domain-Driven Development) gehaald. Bij DDD speelt het domein (een kennisgebied met gemeenschappelijke vereisten, terminologie en functionaliteiten) een centrale rol. Bij DDD betrekt men mensen met veel domeinexpertise maar zonder programmeerervaring heel nauw bij het softwareontwikkelingsproces.

3.2 Unit Testing

We kunnen de Entity-klasses testen met PHPUnit. Dit is een unit testing framework dat ontwikkeld werd door de Duitser Sebastian Bergmann

Sebastian Bergmann

3.1.1 PHPUnit Installeren

Om PHPUnit in de Command-Line Interface te installeren:

vagrant@homestead$ omposer global require "phpunit/phpunit=4.3.*"

Test of PHPUnit correct werkt door de versie op te vragen:

vagrant@homestead$ phpunit --version
3.1.2 PHPUnit Updaten
vagrant@homestead$ phpunit --self-update
3.1.3 PHPUnit Gebruiken

Om alle testen in het Symfony-project uit te voeren:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ phpunit --configuration app/

Of korter:

vagrant@homestead$ phpunit -c app/
3.1.4 PHPUnit Test Schrijven

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/
        └── Artevelde/
            └── CommonBundle/
                └── Tests/
                    └── Entity/
                        └── PostAbstractTest.php
// …
class PostAbstractTest extends WebTestCase
{
    private $doctrine;

    /**
     * {@inheritDoc}
     */
	public function setUp()
    {
        static::$kernel = static::createKernel();
        static::$kernel->boot();
        $this->doctrine = static::$kernel->getContainer()->get('doctrine'); // Doctrine service uit de container opvragen
        $em = $this->doctrine->getManager();
        $em->getConnection()->beginTransaction(); // Geen auto-commit gebruiken.
    }
    
    /**
     * Test for the Entity Article.
     */
    public function testArticleEntity()
    {
		$article = new Article();

		$article
			->setTitle('Testartikel')
			->setBody('Lorem ipsum');

        $em = $this->doctrine->getManager(); // Entity Manager
        $em->persist($memberB); // Object managen voor persistentie.
        $em->flush(); // Gemanagede objecten wegschrijven naar de persistentielaag.
    }    

    /**
     * {@inheritDoc}
     */
    protected function tearDown()
    {
        parent::tearDown();

        $em = $this->doctrine->getManager();
        $em->getConnection()->rollback(); // Alle wijzigingen ongedaan maken, maar auto_increment waarde zal toch verhoogd blijven.
        $em->close();
    }
}

3.3 BDD-methodologie

Zie ook:

BDD is TDD met een aantal Best Practices:

  1. Vertrek van de doelen van de business of organisatie.
  2. Gebruik voorbeelden om de requirements te verduidelijken.
  3. Ontwikkel een universele taal voor zowel de domeinexperten als de ontwikkelaars.

BDD formaliseert deze TDD-Best Practices en zorgt ervoor dat alle belanghebbenden (stakeholders) begrijpen wat er gebouwd wordt, doordat er begrijpbare documentatie ontstaat.

3.4 BDD met Behat

Zie ook:

3.4.1 Inleiding
A. Behat

Behat is een BDD-framework voor PHP geïnspireerd op het in Ruby geschreven Cucumber, gemaakt door Konstantin Kudryashov.

B. Gherkin

In Cucumber worden de testen (Behaviour Tests) geschreven in de taal Gherkin. Behat neemt deze taal over en sinds Behat 3 is de Behat-implementatie van Gherkin zelfs de officiële PHP-implementatie van Gherkin.

Gherkin is vertaald in ca. 40 verschillende gesproken talen.

C. Mink

Mink is een Web Acceptance Test Suite bovenop Behat. Concreet wil dit zeggen dat Mink gebruikt wordt om geautomatiseerde Acceptance Tests uit te voeren door een browser te emuleren, of door echte browser aan te sturen. Hiervoor heb je keuze uit een aantal stuurprogramma's (drivers) die je nog apart moet installeren.

3.4.2 Installatie
A. Voorbereiding:

Eerst moeten we zorgen dat we de allerlaatste versie van Composer hebben:

vagrant@homestead$ sudo composer self-update

Daarna updaten we ook ons Symfony-project:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ composer update
B. Behat en Symfony2Extension voor Behat

Symfony2Extension zorgt voor de integratie van Behat in Symfony.

vagrant@homestead$ composer require behat/behat:"~3.0" behat/symfony2-extension:"~2.0"

Je kan testen of Behat correct geïnstalleerd is door het versienummer op te vragen:

vagrant@homestead$ bin/behat --version

Help:

vagrant@homestead$ bin/behat --help
C. Mink met MinkExtension voor Behat

MinkExtension zorgt voor de link tussen Behat en Mink. Daarnaast hebben we ook minstens één browserstuurprogramma nodig. Hier gebruiken we Mink BrowserKit Driver

vagrant@homestead$ composer require behat/mink:"~1.6" behat/mink-extension:"~2.0" behat/mink-browserkit-driver:"~1.2"

In de rootmap van ons project maken we een bestand behat.yml aan om de Behat Symfony2Extension, MinkExtension in te stellen.

default:
    translation:
        locale: en
        fallback_locale: en
    extensions:
        Behat\Symfony2Extension: ~
        Behat\MinkExtension:
            base_url: 'http://dev.nmdad-iii.arteveldehogeschool.be'
            sessions:
                default:
                    symfony2: ~

Tip: Controleer de geldigheid van het YAML-bestand met de Console-opdracht opdracht yaml:lint:

vagrant@homestead$ php app/console yaml:lint behat.yml

Naslagwerk over het configuratiebestand:

vagrant@homestead$ bin/behat --config-reference
3.4.3 Test Suites

Onder suites: staan de Behat Test Suites, een manier om testen te groeperen. Voor Symfony is het handig om Test Suites te koppelen aan Symfony Bundles, wat zeer eenvoudig is dankzij de Behat Symfony2Extension. In dit voorbeeld is de Test Suite acme_demo_features geconfigureerd voor de Bundle AcmeDemoBundle.

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── behat.yml
default:
    translation:
        locale: en
        fallback_locale: en
    extensions:
        Behat\Symfony2Extension: ~
        Behat\MinkExtension:
            base_url: 'http://dev.nmdad-iii.arteveldehogeschool.be'
            sessions:
                default:
                    symfony2: ~
    suites:
        acme_demo_features:
            type: symfony_bundle
            bundle: AcmeDemoBundle

Bij het initialiseren van Behat zal een map Features in de Bundles aangemaakt worden.

vagrant@homestead$ bin/behat --init

In dit voorbeeld werd scr/Acme/DemoBundle/Features/ aangemaakt

3.4.4 Functionaliteit (Feature)

Zie ook:

We gaan een Functionaliteit schrijven in de taal Gherkin. Gherkin bestaat voor verschillende gesproken talen, maar we houden het op Engels omdat IDE's voorlopig enkel de Engelstalige sleutelwoorden herkent. Maar, het is dus mogelijk om functionaliteiten in het Nederlands te schrijven, indien nodig.

Een Functionaliteit (Feature) heeft (optioneel) een Achtergrond (Background) en één of meerdere Scenario's (Scenario). Naast een gewoon Scenario bestaat er ook een Abstract Scenario (Scenario Outline of Scenario Template) dat eigenlijk een scenario is met parameters. De argumenten voor die parameters staan in een soort tabel: Voorbeelden (Examples of Scenarios).

Elk Scenario heeft een aantal stappen die beginnen met één van deze sleutelwoorden:

Nederlands Nederlands Alternatief English
Gegeven Stel Given
En And
Als When
Dan Then
Maar But

Eens de Funtionaliteit geschreven is, moeten we voor elke stap in de Scenario's een Step Definition schrijven. Dit is een methode in een PHP-klasse.

Opmerking: Er bestaan BDD-frameworks voor verschillende programmeertalen zoals o.m. Cucumber voor Ruby en SpecFlow voor .NET, die allen met dezelfde, in Gherkin geschreven Funtionaliteit-bestanden werken.

We gaan eerst naar de rootmap van ons Symfony-project:

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
Gherkin Syntaxis

De syntaxis van een Functionaliteit

vagrant@homestead$ bin/behat --story-syntax

Om de syntaxis te zien in een specifieke taal:

vagrant@homestead$ bin/behat --story-syntax --lang=de
vagrant@homestead$ bin/behat --story-syntax --lang=en
vagrant@homestead$ bin/behat --story-syntax --lang=fr
vagrant@homestead$ bin/behat --story-syntax --lang=nl

We maken een nieuwe Functionaliteit: Login om de Behavior Tests voor http://dev.nmdad-iii.arteveldehogeschool.be/demo/secured/login te schrijven.

Met het commando touch kunnen we een leeg bestand maken.

vagrant@homestead$ cd scr/Acme/DemoBundle/Features/
vagrant@homestead$ touch login.feature

Open daarna de bestand login.feature met een teksteditor.

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/
        └── Acme/
            └── DemoBundle/
                └── Features/
                    └── login.feature
# language: en
Feature: Login Form
 In order to access secured content of the website
 As a user or an admin
 I need to log in

 Background:
   Given the users are:
   |username|password |role      |
   |user    |userpass |ROLE_USER |
   |admin   |adminpass|ROLE_ADMIN|

 @mink:default
 Scenario Outline: log in with valid credentials
   Given I am on the homepage
   When I follow "Run The Demo"
   Then I should be on "/demo/"
   When I follow "Access the secured area"
   Then I should be on "/demo/secured/login"
   When I fill in "username" with "<username>"
     And I fill in "password" with "<password>"
     And I press "Login"
   Then I should be on "/demo/secured/hello/World"
     And I should see "<text>"

# De reeds voorgedefineerde stappen in de context (MinkContext) vind je met:
# vagrant@homestead$ bin/behat -di
# vagrant@homestead$ bin/behat -dl

   Examples:
       |username|password |text              |
       |user    |userpass |logged in as user |
       |admin   |adminpass|logged in as admin|

Opmerking: Met @mink: kan je instellen welke sessie (cfr. Mink Driver) gebruikt moet worden. default is de Mink BrowserKit Driver.

Initialiseer opnieuw

vagrant@homestead$ cd ~/Code/nmdad-iii.arteveldehogeschool.be/www/
vagrant@homestead$ bin/behat --init

Behat uitvoeren

vagrant@homestead$ bin/behat

Je kan Behat ook voor een specifieke Test Suite uitvoeren:

vagrant@homestead$ bin/behat --suite=acme_demo_features

of voor een specifieke Symfony Bundle:

vagrant@homestead$ bin/behat @AcmeDemoBundle

En zelf een specifieke feature:

vagrant@homestead$ bin/behat @AcmeDemoBundle/login.feature
3.4.5 Uitvoeren

Nu moeten we nog Actions maken voor elke stap. Dit zijn methodes in de Context-klasse features/bootstrap/Acme/DemoBundle/Features/Context/FeatureContext.php

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── features/
        └── bootstrap/
            └── Acme/
                └── DemoBundle/
                    └── Features/
                        └── Context/
                            └── FeatureContext.php

Als je Behat uitvoert zal die snippets tonen voor elke ontbrekende stap in de Context-klasse.

vagrant@homestead$ bin/behat

Je kan de snippets aan de klasse laten toevoegen net:

vagrant@homestead$ bin/behat --dry-run --append-snippets

--dry-run zorgt ervoor dat enkel de Feature Suite getoond wordt, zonder de onderliggende code

De methodes maken (voorlopig) gebruik van de klasse PendingException, die we dus moeten toevoegen:

use Behat\Behat\Tester\Exception\PendingException;

De klasse FeatureContext laten we MinkContext extenden:

    namespace Acme\DemoBundle\Features\Context;

    use Behat\Behat\Context\SnippetAcceptingContext;
    use Behat\Gherkin\Node\TableNode;
    use Behat\MinkExtension\Context\MinkContext;

    /**
     * Behat context class.
     */
    class FeatureContext extends MinkContext implements SnippetAcceptingContext
    {
        /**
         * Initializes context.
         *
         * Every scenario gets its own context object.
         * You can also pass arbitrary arguments to the context constructor through behat.yml.
         */
        public function __construct()
        {
        }
        
        /**
         * @Given the users are:
         */
        public function theUsersAre(TableNode $table)
        {
            // Do nothing.
        }
    }

MinkContext geeft toegang tot heel wat voorgedefinieerde stappen. Klik in PhpStorm rechts op MinkExtension en dan Go ToDeclaration om de klasse te openen en de stappen te zien.

Je kan de Context klasses ook toevoegen in behat.yml en hoef/mag de klasse FeatureContext deze niet meer extenden.

4. Controllers

Zie ook:

4.1 Controller

vagrant@homestead$ php app/console generate:controller --controller=ArteveldeFrontOfficeBundle:Articles

Met volgende opties:

* Controller name [ArteveldeFrontOfficeBundle:Articles]:      <return>
* Routing format (php, xml, yml, annotation) [annotation]:    <return>
* Template format (twig, php) [twig]:                         <return>
* New action name (press <return> to stop adding actions):    indexAction
* Action route [/index]:                                      /articles
* …
* New action name (press <return> to stop adding actions):    <return>
* Confirm automatic update of your Kernel [yes]?              <return>
* Do you confirm generation [yes]?                            <return>

4.2 Controller op basis van Doctrine Entity

Controllers op basis van een Doctrine Entity worden gegenereerd in de Bundle waar de Entity Staat.

vagrant@homestead$ php app/console generate:doctrine:crud --entity=ArteveldeCommonBundle:User

Met volgende opties:

* The Entity shortcut name [ArteveldeCommonBundle:User]:               <return>
* Do you want to generate the "write" actions [no]?                    y
* Configuration format (yml, xml, php, or annotation) [annotation]:    <return>
* Routes prefix [/user]:                                               <return>
* Do you confirm generation [yes]?                                     <return>

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/
        └── Artevelde/
            └── CommonBundle/
                ├── Controller/
                |   └── UserController.php
                ├── Forms/
                |   └── UserType.php
                └── Resources/
                    └── views/
                        └── User/
                            ├── edit.html.twig
                            ├── index.html.twig
                            ├── new.html.twig                                 
                            └── show.html.twig

Ga naar: http://dev.nmdad-iii.arteveldehogeschool.be/user/

4.3 Wachtwoord hashen in de controller

4.3.1 Securityinstellingen

Voor de hashing van het wachtwoord van de Entity User gaan we het bcrypt algoritme gebruiken met een cost van 15. Hoe hoger de cost, hoe langer het duurt om het wachtwoord te hashen en hoe minder praktisch het is om een succesvolle brute force-aanval uit te voeren. Bcrypt zorgt zelf voor een Salt, vandaar dat de getSalt() methode null mag teruggeven.

app/config/security.yml:

security:
#
    encoders:
#        Symfony\Component\Security\Core\User\User: plaintext
        Artevelde\CommonBundle\Entity\User:
            algorithm: bcrypt
            cost: 15
#
	providers:
#        in_memory:
#            memory:
#                users:
#                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
#                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
        default:
            entity: { class: ArteveldeCommonBundle:User, property: username }
#
4.3.2 Hashen

In de Controller Action:

// Password Hashing
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($entity);
$hashedPassword = $encoder->encodePassword($entity->getPassword(), $entity->getSalt());
$entity->setPassword($hashedPassword);

5. Routes

Zie ook:

Om alle routes in dev (developmentomgeving) te zien:

vagrant@homestead$ php app/console router:debug

Om alle routes in prod (productieomgeving) te zien:

vagrant@homestead$ php app/console router:debug --env=prod

Normaal zijn er nog geen routes gedefineerd voor de productieomgeving.

6. Views

6.1 Templates

Zie ook:

Views kunnen automatisch aangemaakt worden door de Generator als een Controller Action aangemaakt wordt.

Views worden in de Twig Templating Language geschreven. Het kan in principe in elke tekstgebaseerd bestandsformaat toegepast worden.

Twig is een autonoom project van SensioLabs dat standaard in Symfony2 geïntegreerd, maar het kan ook in andre projecten gebruikt worden. Enkele alternatieven voor Twig:

Twig-sjablonen (Twig Templates) eindigen op de extensie .twig. Courante extensies:

  • .html.twig
  • .css.twig
  • .js.twig
  • .xml.twig

Twig Templates worden automatisch tot gewone tekst gecompileerd, zodat het geen impact heeft op laadtijden.

6.1.1 Twig Syntaxis
Twig afbakeningstekens Betekenis
{# en #} Commentaar.
{% en %} Statement uitvoeren.
{{ en }} Toont resultaat van een expressie evaluatie.
6.1.2 Formulieren
{# volledig formulier renderen #}
{{ form(formulier_naam) }}

{# begintag van een formulier #}
{{ form_start(formulier_naam) }}

{# eindtag van een formulier #}
{{ form_end(formulier_naam) }}

{# Label van een element renderen #}
{{ form_label(formulier_naam.element_naam) }}

{# Element zelf renderen #}
{{ form_widget(formulier_naam.element_naam) }}

{# Validatiefouten renderen #}
{{ form_errors(formulier_naam.element_naam) }}
6.1.2 Debuggen

In de developmentomgeving kan je de Twig-functie dump() gebruiken. Deze is vergelijkbaar met de standaard PHP-functie var_dump().

6.2 Assets

Je kan assets rechtstreeks in de map web/ zetten, maar je kan ze ook in de bundles zetten in de Resources/public/ map van de bundle.

Met deze Console-opdracht kan je de assets kopiëren naar de map web/ :

vagrant@homestead$ php app/console assets:install

Tip: Wil je echter onmiddellijk resultaat zien zonder telkens een install uit te voeren, dan kan je ook symbolische koppelingen gebruiken (al dan niet relatief ten opzichte van de projectmap).

vagrant@homestead$ php app/console assets:install --symlink --relative

OPGELET: niet elke server laat toe dat je symbolische koppelingen gebruikt!

6.3 Bower & Gulp

Zie ook:

6.3.1 Gitinore
vagrant@homestead$ cd src/Artevelde/CommonBundle/Resources/
vagrant@homestead$ mkdir assets/
.sass-cache/
bower_components/
node_modules/

6.3.2 Node updaten

OPGELET: Deze stap is potentieel gevaarlijk!

vagrant@homestead$ sudo apt-get upgrade node
vagrant@homestead$ sudo npm -g update
6.3.3 Node achter een proxyserver

Proxyinstellingen worden bewaard in het bestand ~/.npmrc

proxy on
sudo -E npm set http-poxy $proxy && sudo -E npm set https-poxy $proxy
6.3.4 Bower

Bower bewaart de instellingen in een bestand bower.json dat je kan laten aanmaken met de opdracht init:

vagrant@homestead$ cd src/Artevelde/CommonBundle/Resources/
vagrant@homestead$ mkdir assets/
vagrant@homestead$ cd assets/
vagrant@homestead$ bower init

Een package installeren in een submap met de naam bower_components doe je met de opdracht install en als je de dependency wil bewaren in het JSON-bestand, dan voeg je daar de optie --save aan toe.

Bootstrap Installeren

We installeren de officiële SASS-versie van Bootstrap:

vagrant@homestead$ bower install --save bootstrap-sass-official
FontAwesome Installeren
vagrant@homestead$ bower install --save fontawesome
6.3.5 Gulp
NMP initialiseren

We bewaren de package dependency's in een bestand package.json zodat we die niet in ons project moeten bewaren, maar later kunnen installeren met behulp npm install.

vagrant@homestead$ cd src/Artevelde/CommonBundle/Resources/assets/
vagrant@homestead$ npm init
Gulp Packages.

Gulp is in Laravel Homestead al globaal geïnstalleerd (met de optie --global of -g), maar nu moet je het nog lokaal in je project installeren met de --save-dev optie.

We hebben de volgende NPM-packages nodig:

Tip: Installeer de packages één voor één in plaats van allemaal tezamen. Zo voorkom je build-problemen.

De meeste NMP-packages installeren we als volgt:

vagrant@homestead$ npm install --save-dev gulp
vagrant@homestead$ npm install --save-dev gulp-bower
vagrant@homestead$ npm install --save-dev gulp-notify
vagrant@homestead$ npm install --save-dev gulp-ruby-sass
vagrant@homestead$ npm install --save-dev gulp-uglify

OPGELET: Op Windows moet je PowerShell 3.0 gebruiken, opgestart als Administrator om vervolgens Vagrant met SMB (Server Message Block) folder sharing te starten:

In Homestead.yaml:

- map: ~/Code
  to: /home/vagrant/Code
  type: smb

Gebruik daarna npm met de --no-bin-link optie zodat geen Symbolische Koppelingen (symlinks) gebruikt worden:

vagrant@homestead$ npm install --no-bin-link

Tip: gulp-notify maakt gebruik van notify-send om notificaties naar de desktop te sturen. Aangezien Ubuntu Server geen desktop heeft, is deze niet geïnstalleerd. Om de foutmelding te vermijden kan je deze toch installeren.

vagrant@homestead$ sudo apt-get install -y libnotify-bin

Of achter de proxyserver:

vagrant@homestead$ proxy on
vagrant@homestead$ sudo -E apt-get install -y libnotify-bin

We moeten ook nog SASS installeren met Ruby Gem:

vagrant@homestead$ sudo gem install sass

Of achter de proxyserver:

vagrant@homestead$ proxy on
vagrant@homestead$ sudo -E gem install sass

Daarna maken we een nieuwe bestand gulpfile.js waarin we de Gulp Tasks schrijven.

De Gulp API heeft een aantal methoden:

Methode Actie
task() Taak definiëren.
src() Geeft stroom van ingelezen bronbestanden terug.
pipe() Opdracht uitvoeren op ingelezen bronbestanden.
dest() Bestanden wegschrijven naar doelbestemming.
watch() Taken uitvoeren als bestanden wijzigen.

gulpfile.js:

'use strict';

var gulp   = require('gulp'),
	bower  = require('gulp-bower'),
	notify = require('gulp-notify'),
	rename = require('gulp-rename'),
    uglify = require('gulp-uglify'),
	sass   = require('gulp-ruby-sass'),

// Tasks
// -----
/**
 * Run all tasks when 'gulp' is executed.
 */
gulp.task('default', ['css', 'js'], function() {
    // Default task must exist.
});

/**
 * Install Bower packages defined in bower.json into the given components folder.
 */
gulp.task('bower', function() {
	// …
});

/**
 * Create font files in destination folder.
 */
gulp.task('fonts', ['bower'], function() {
	// …
});

/**
 * Create CSS files in destination folder.
 */
gulp.task('css', ['bower', 'fonts'], function() {
	// …
});

/**
 * Create JS files in destination folder.
 */
gulp.task('js', ['bower'], function() {
	// …
});
6.3.5 Bundle Assets installeren

Met symbolische koppelingen:

vagrant@homestead$ php app/console assets:install --symlink --relative

Zonder (voor Windows Host Besturingssystemen):

vagrant@homestead$ php app/console assets:install

7. Forms

Zie ook:

7.1 Form Types met Doctrine

Formulieren worden in een View gemaakt en bevatten velden uit een Entity. Omdat niet altijd alle velden van de Entity nodig zijn, kan je Form Types gebruiken. Deze bevatten een selectie en configuratie van de velden. Van een Form Type kan Symfony een Form renderen.

Doctrine kan eenvoudige Form Types generen op basis van een Entity. Deze kan je dan verder aanpassen en uitbreiden.

Om bijvoorbeeld een Form Type te genereren op basis van de Entity ArteveldeCommonBundle:User:

vagrant@homestead$ php app/console generate:doctrine:form ArteveldeCommonBundle:User

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/
        └── Artevelde/
            └── CommonBundle/
                ├── Controller/
                ├── Forms/
                |   └── UserType.php
                └── Resources/

In src/Artevelde/CommonBundle/Forms/UserType.php scheakelen we nog een paar velden uit die niet in het formulier getoond mogen worden:

<?php

namespace Artevelde\CommonBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class UserType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstName')
            ->add('lastName')
//            ->add('createdAt')
//            ->add('enabledAt')
//            ->add('lockedAt')
//            ->add('expiredAt')
//            ->add('expiredCredentialsAt')
//            ->add('deletedAt')
            ->add('username')
            ->add('password')
        ;
    }
    
    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Artevelde\CommonBundle\Entity\User'
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'artevelde_commonbundle_user';
    }
}

7.2 Aanmeldformulier

Zie ook:

7.2.1 Controller

We vertrekken van de SecurityController. In de controller hebben we nieuwe Methoden nodig:

  • loginAction() met als routenaam artevelde_frontoffice_security_login

De controller maken we met:

vagrant@homestead$ php app/console generate:controller --controller=ArteveldeFrontOfficeBundle:Security

Met volgende opties:

* Controller name [ArteveldeFrontOfficeBundle:Articles]:      <return>
* Routing format (php, xml, yml, annotation) [annotation]:    <return>
* Template format (twig, php) [twig]:                         <return>
* New action name (press <return> to stop adding actions):    loginAction
* Action route [/login]:                                      /login
* Templatename (optional) [ArteveldeFrontOfficeBundle:Security:login.html.twig]: <return>
* New action name (press <return> to stop adding actions):    <return>
* Confirm automatic update of your Kernel [yes]?              <return>
* Do you confirm generation [yes]?                            <return>

Mappenstructuur en bestanden

nmdad-iii.arteveldehogeschool.be/
└── www/
    └── src/
        └── Artevelde/
            └── FrontofficeBundle/
                ├── Controller/
                |   └── SecurityController.php
                └── Resources/
                    └── views/
                        └── Security/
                            └── login.html.twig
7.2.2 Routes

We kunnen de routes opvragen met:

vagrant@homestead$ php app/console router:debug | grep 'artevelde_frontoffice_security'

Er is nu al één route, maar we hebben nog twee speciale routes nodig die door Symfony afgehandeld worden:

  • Een route om de aanmeldgegevens te verifiëren.
  • Een route om terug af te melden

We maken een nieuw routeconfiguratiebestand aan src/Artevelde/FrontOffice/Resources/config/routing.yml

artevelde_frontoffice_security_check:
    pattern: /login_check

artevelde_frontoffice_security_logout:
    pattern: /logout

Nu moeten we de routes nog activeren door dit nieuwe bestand als resource toe te voegen aan app/config/routing.yml

artevelde_front_office_extensions:
    resource: "@ArteveldeFrontOfficeBundle/Resources/config/routing.yml"
    prefix:   /security

Uiteindelijk zouden we deze routes moeten hebben:

  • artevelde_frontoffice_security_login ANY ANY ANY /security/login
  • artevelde_frontoffice_security_check ANY ANY ANY /security/login_check
  • artevelde_frontoffice_security_logout ANY ANY ANY /security/logout
7.2.3 Form

We kopieren src/Artevelde/CommonBundle/Form/UserType.php naar src/Artevelde/FrontOfficeBundle/Form/UserLoginType.phpen passen het aan:

<?php

namespace Artevelde\FrontOfficeBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class UserLoginType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username', 'text', [
                'label' => 'Username',
                'attr'  => ['placeholder' => 'Enter your username.'],
            ])
            ->add('password', 'password', [
                'label' => 'Password',
                'attr'  => ['placeholder' => 'Enter your password.'],
            ])
            ->add('remember', 'checkbox', [
                'label' => 'Remember me',
                'data' => true,
            ])
            ->add('btn_login', 'submit', [
                'label' => 'Sign on',
            ])
        ;
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'Artevelde\CommonBundle\Entity\User'
        ]);
    }

    /**
     * Name of the form.
     *
     * @return string
     */
    public function getName()
    {
        return 'artevelde_frontoffice_security_login_form';
    }
}
7.2.4 Controller Aanpassen
<?php

namespace Artevelde\FrontOfficeBundle\Controller;

use Artevelde\CommonBundle\Entity\User;
use Artevelde\FrontOfficeBundle\Form\UserLoginType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContext;

/**
 * Class SecurityController
 * @package Artevelde\FrontOfficeBundle\Controller
 *
 * @route("/security")
 */
class SecurityController extends Controller
{
    /**
     * @Route("/login")
     * @Template()
     */
    public function loginAction(Request $request)
    {
        $entity = new User();
        $form = $this->createForm(new UserLoginType(), $entity, [
            'action' => $this->generateUrl('artevelde_frontoffice_security_check'),
            'method' => 'POST',
        ]);

        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $session = $request->getSession();
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        }

        /**
         * Return array with variables for Twig.
         */
        return [
            'form'  => $form->createView(),
            'error' => $error,
        ];
    }
}
7.2.5 Twig Template Aanpassen

Zie ook:

src/Artevelde/FrontOfficeBundle/Resources/views/Security/login.html.twig:

{% extends "::base.html.twig" %}

{% block title %}ArteveldeFrontOfficeBundle:Security:login{% endblock %}

{% block body %}
<h1>Welcome to the Security:login page</h1>
    {% if error %}
        <div>{{ error.message }}</div>
    {% endif %}
    {{ form(form) }}
{% endblock %}

Bekijk het formulier op http://dev.nmdad-iii.arteveldehogeschool.be/security/login

Let op de waarden in de name-attributen van de formulierelementen, die hebben we straks nodig om de Security Firewall te configureren.

7.2.6 Security Firewall

Nu de routes en het formulier gekend zijn kunnen we de Symfony Security Firewall configureren om deze routes te gebruiken.

app/config/security.yml:

#
    firewalls:
		#
        artevelde_frontoffice_secured_area:
            pattern: ^/
            anonymous: ~
            form_login:
                login_path: artevelde_frontoffice_security_login
                check_path: artevelde_frontoffice_security_check
                username_parameter: artevelde_frontoffice_security_login_form[username]
                password_parameter: artevelde_frontoffice_security_login_form[password]
            remember_me:
                key:      "%secret%"
                lifetime: 31536000 # 365 days in seconds
                path:     /
                domain:   ~ # Defaults to the current domain from $_SERVER
                remember_me_parameter: artevelde_frontoffice_security_login_form[remember]
            logout:
                path:   artevelde_frontoffice_security_logout
                target: /
#
7.2.7 Entity Aanpassen

Er moet nog een publiek toegankelijke membervariabele aangemaakt worden voor de Remember Me functionaliteit.

// …
    /**
     * Remember me.
     *
     * @var bool
     */
    public $remember = true;
// …
7.2.8 Testen
  1. Maak een gebruiker aan op http://dev.nmdad-iii.arteveldehogeschool.be/user/new
  2. Meld de gebruiker aan op http://dev.nmdad-iii.arteveldehogeschool.be/security/login
  3. Als het aanmelden succesvol was, dan moet de gebruikersnaam in de Symfony Web Debug Toolbar verschijnen.
  4. Afmelden kan eenvoudigweg door naar http://dev.nmdad-iii.arteveldehogeschool.be/security/logout te gaan.

Doctrine

Zie ook:

Standaardmethodes voor de Entity-repository:

  • find()
  • findAll()
  • findBy()
  • findOneBy()

Daarnaast maak je je eigen methodes in Repository-klasse die aan de Entity gelinkt is via deze annotatie: @ORM\Entity(repositoryClass="").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment