<?xml version="1.0" encoding="UTF-8"?>
<!-- EN-Revision: 24249 -->
<!-- Reviewed: no -->
<sect1 id="learning.quickstart.create-model">
    <title>Créer un modèle et une table en base de données</title>

    <para>
        Avant de démarrer, considérons ceci: où vont se trouver ces classes et comment les retrouver?
        Le projet par défaut que nous avons conçu instancie un autoloader. Nous pouvons lui attacher
        d'autres autoloaders pour qu'il puisse trouver des classes différentes. Typiquement nous
        voulons que nos classes MVC soient groupées sous une même arborescence -- dans ce cas,
        <filename>application/</filename> -- et nous utiliserons un préfixe commun.
    </para>

    <para>
        <classname>Zend_Controller_Front</classname> a une notion de "modules", qui sont des
        mini-applications individuelles. Les modules reflètent la structure de répertoires que la
        commande <command>zf</command> crée sous <filename>application/</filename>, et toutes les
        classes à l'intérieur sont supposées commencer par un préfixe étant le nom du module.
        <filename>application/</filename> est lui-même un module -- le module "default" ou
        "application". Ainsi, nous allons vouloir configurer un autoload pour les ressources sous
        ce dossier.
    </para>

    <para>
        <classname>Zend_Application_Module_Autoloader</classname> propose la fonctionnalité nécessaire
        à la correspondance entre les ressources d'un module et ses dossiers, il propose pour cela
        un mécanisme de nommage standard. Une instance de la classe est créee par défaut pendant
        l'initialisation de l'objet de bootstrap et utilisera le préfixe de module "Application".
        De ce fait, nos classes de modèles, formulaires, et tables commenceront toutes par le préfixe
        de classe "Application_".
    </para>

   <para>
        Maintenant voyons de quoi est fait un livre d'or. Typiquement il existe simplement une liste
        d'entrées avec un <emphasis>commentaire</emphasis>, <emphasis>timestamp</emphasis>, et souvent
        une <emphasis>adresse email</emphasis>. En supposant que nous stockons cela dans une base de
        données, nous aurons aussi besoin d'un <emphasis>identifiant unique</emphasis> pour chaque
        entrée. Nous voudrons aussi sauvegarder une entrée, récupérer une entrée individuelle ou encore
        récupérer toutes les entrées. De ce fait, l'<acronym>API</acronym> du modèle d'un simple livre
        d'or ressemblerait à ceci:
    </para>

    <programlisting language="php"><![CDATA[
// application/models/Guestbook.php

class Application_Model_Guestbook
{
    protected $_comment;
    protected $_created;
    protected $_email;
    protected $_id;

    public function __set($name, $value);
    public function __get($name);

    public function setComment($text);
    public function getComment();

    public function setEmail($email);
    public function getEmail();

    public function setCreated($ts);
    public function getCreated();

    public function setId($id);
    public function getId();
}

class Application_Model_GuestbookMapper
{
    public function save(Application_Model_Guestbook $guestbook);
    public function find($id);
    public function fetchAll();
}
]]></programlisting>

    <para>
        <methodname>__get()</methodname> et <methodname>__set()</methodname> nous simpliferons l'accès
        aux attributs et proxieront vers les autres getters et setters. Ils nous permettront de même
        de nous assurer que seuls les attributs que nous avons définis seront accessibles dans l'objet.
    </para>

    <para>
        <methodname>find()</methodname> et <methodname>fetchAll()</methodname> permettent de récupérer
        une seule entrée ou toutes les entrées alors que <methodname>save()</methodname> offrira la
        possibilité de stocker une entrée dans le support de stockage.
    </para>

    <para>
        Maintenant à partir de là, nous pouvons commecer à penser en terme de base de données.
    </para>

    <para>
        Nous devons d'abord intialiser une ressource <classname>Db</classname>. Comme pour les
        ressources <classname>Layout</classname> et <classname>View</classname>, nous pouvons utiliser
        de la configuration pour <classname>Db</classname>. Cela est possible au moyen de la commande
        <command>zf configure db-adapter</command>:
    </para>

    <programlisting language="shell"><![CDATA[
% zf configure db-adapter \
> 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook.db"' \
> production
A db configuration for the production has been written to the application config file.

% zf configure db-adapter \
> 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook-testing.db"' \
> testing
A db configuration for the production has been written to the application config file.

% zf configure db-adapter \
> 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/guestbook-dev.db"' \
> development
A db configuration for the production has been written to the application config file.
]]></programlisting>

    <para>
        Editez maintenant le fichier <filename>application/configs/application.ini</filename>, vous
        verrez que les lignes suivantes ont été ajoutées dans les sections appropriées:
    </para>

    <programlisting language="ini"><![CDATA[
; application/configs/application.ini

[production]
; ...
resources.db.adapter = "PDO_SQLITE"
resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook.db"

[testing : production]
; ...
resources.db.adapter = "PDO_SQLITE"
resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-testing.db"

[development : production]
; ...
resources.db.adapter = "PDO_SQLITE"
resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-dev.db"
]]></programlisting>

    <para>
        Votre fichier de configuration final devrait ressembler à ceci:
    </para>

    <programlisting language="ini"><![CDATA[
; application/configs/application.ini

[production]
phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
appnamespace = "Application"
resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
resources.frontController.params.displayExceptions = 0
resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
resources.view[] =
resources.db.adapter = "PDO_SQLITE"
resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook.db"

[staging : production]

[testing : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
resources.db.adapter = "PDO_SQLITE"
resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-testing.db"

[development : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
resources.db.adapter = "PDO_SQLITE"
resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-dev.db"
]]></programlisting>

    <para>
        Notez que la/les base(s) de données seront stockées sous <filename>data/db/</filename>. Créez ces dossiers
        et affectez leur les bons droits. Sur les systèmes Unix utilisez:
    </para>

    <programlisting language="shell"><![CDATA[
% mkdir -p data/db; chmod -R a+rwX data
]]></programlisting>

    <para>
        Sur Windows, vous devrez créer le dossier avec l'explorateur et lui donner les bonnes permissions pour que
        tout le monde puisse y écrire.
    </para>

    <para>
        Dès lors, nous possédons une connexion à une base de données, dans notre cas il s'agit de Sqlite et la base
        est placée sous le dossier <filename>application/data/</filename>. Créons maintenant une table pour
        stocker nos entrées de livre d'or.
    </para>

    <programlisting language="sql"><![CDATA[
-- scripts/schema.sqlite.sql
--
-- You will need load your database schema with this SQL.

CREATE TABLE guestbook (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    email VARCHAR(32) NOT NULL DEFAULT 'noemail@test.com',
    comment TEXT NULL,
    created DATETIME NOT NULL
);

CREATE INDEX "id" ON "guestbook" ("id");
]]></programlisting>

    <para>
        Puis pour ne pas travailler dans le vide, créons quelques enregistrements de départ.
    </para>

    <programlisting language="sql"><![CDATA[
-- scripts/data.sqlite.sql
--
-- You can begin populating the database with the following SQL statements.

INSERT INTO guestbook (email, comment, created) VALUES
    ('ralph.schindler@zend.com',
    'Hello! Hope you enjoy this sample zf application!',
    DATETIME('NOW'));
INSERT INTO guestbook (email, comment, created) VALUES
    ('foo@bar.com',
    'Baz baz baz, baz baz Baz baz baz - baz baz baz.',
    DATETIME('NOW'));
]]></programlisting>

    <para>
        Maintenant que nous avons la définition de la base de données ainsi que des données, créons un
        script qui pourra être lancé pour entièrement initialiser la base de données de manière
        autonomme. Bien sûr cela ne sera pas nécessaire en production. Créez le script
        <filename>scripts/load.sqlite.php</filename> avec le contenu suivant:
    </para>

    <programlisting language="php"><![CDATA[
// scripts/load.sqlite.php

/**
 * Script pour créer et charger la base
 */

// Initialise le chemin vers l'application et l'autoload
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
set_include_path(implode(PATH_SEPARATOR, array(
    APPLICATION_PATH . '/../library',
    get_include_path(),
)));
require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();

// Definit des options CLI
$getopt = new Zend_Console_Getopt(array(
    'withdata|w' => 'Load database with sample data',
    'env|e-s'    => 'Application environment for which to create database (defaults to development)',
    'help|h'     => 'Help -- usage message',
));
try {
    $getopt->parse();
} catch (Zend_Console_Getopt_Exception $e) {
    // Mauvaises options passées: afficher l'aide
    echo $e->getUsageMessage();
    return false;
}

// Si l'aid eest demandée, l'afficher
if ($getopt->getOption('h')) {
    echo $getopt->getUsageMessage();
    return true;
}

// Initialise des valeurs selon la présence ou absence d'options CLI
$withData = $getopt->getOption('w');
$env      = $getopt->getOption('e');
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (null === $env) ? 'development' : $env);

// Initialise Zend_Application
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);

// Initialise et récupère la ressoucre DB
$bootstrap = $application->getBootstrap();
$bootstrap->bootstrap('db');
$dbAdapter = $bootstrap->getResource('db');

// Informons l'utilisateur de ce qui se passe (nous créons une base de données
// ici)
if ('testing' != APPLICATION_ENV) {
    echo 'Writing Database Guestbook in (control-c to cancel): ' . PHP_EOL;
    for ($x = 5; $x > 0; $x--) {
        echo $x . "\r"; sleep(1);
    }
}

// Vérifions si un fichier pour la base existe déja
$options = $bootstrap->getOption('resources');
$dbFile  = $options['db']['params']['dbname'];
if (file_exists($dbFile)) {
    unlink($dbFile);
}

// Chargement du fichier de la base de données.
try {
    $schemaSql = file_get_contents(dirname(__FILE__) . '/schema.sqlite.sql');
    // utilise la connexion directement pour charger le sql
    $dbAdapter->getConnection()->exec($schemaSql);
    chmod($dbFile, 0666);

    if ('testing' != APPLICATION_ENV) {
        echo PHP_EOL;
        echo 'Database Created';
        echo PHP_EOL;
    }

    if ($withData) {
        $dataSql = file_get_contents(dirname(__FILE__) . '/data.sqlite.sql');
        // utilise la connexion directement pour charger le sql
        $dbAdapter->getConnection()->exec($dataSql);
        if ('testing' != APPLICATION_ENV) {
            echo 'Data Loaded.';
            echo PHP_EOL;
        }
    }

} catch (Exception $e) {
    echo 'AN ERROR HAS OCCURED:' . PHP_EOL;
    echo $e->getMessage() . PHP_EOL;
    return false;
}

// Ce script sera lancé depuis la ligne de commandes
return true;
]]></programlisting>

    <para>
        Exécutons ce script. Depuis un terminal ou un invite DOS, effectuez:
    </para>

    <programlisting language="shell"><![CDATA[
% php scripts/load.sqlite.php --withdata
]]></programlisting>

    <para>
        Vous devriez voir ceci:
    </para>

    <programlisting language="text"><![CDATA[
path/to/ZendFrameworkQuickstart/scripts$ php load.sqlite.php --withdata
Writing Database Guestbook in (control-c to cancel):
1
Database Created
Data Loaded.
]]></programlisting>

    <para>
        Nous avons maintenant une base de données et une table pour notre application de livre d'or.
        Les prochaines étapes seront de créer le code applicatif. Ceci incluera une source de données
        (dans notre cas nous utiliserons <classname>Zend_Db_Table</classname>), un datamapper pour
        connecter cette source à notre modèle et enfin un contrôleur pour intéragir avec le modèle
        et afficher du contenu divers.
    </para>

    <para>
        Nous allons utiliser un <ulink url="http://martinfowler.com/eaaCatalog/tableDataGateway.html">Table
        Data Gateway</ulink> pour se connecter à notre source de données; <classname>Zend_Db_Table</classname>
        propose cette fonctionnalité. Créons les classes basées sur <classname>Zend_Db_Table</classname>.
        Comme nous avons opéré pour les layouts ou la base, nous pouvons utiliser la commande
        <command>zf</command> pour nous aider, avec la commande complète
        <command>create db-table</command>. Celle-ci prend deux arguments au minimum, le nom de la classe à
        créer et la table qui y fera référence.
    </para>

    <programlisting language="shell"><![CDATA[
% zf create db-table Guestbook guestbook
Creating a DbTable at application/models/DbTable/Guestbook.php
Updating project profile 'zfproject.xml'
]]></programlisting>

    <para>
        En regardant l'orborescence du projet, un nouveau dossier
        <filename>application/models/DbTable/</filename> a été crée contenant le fichier
        <filename>Guestbook.php</filename>. Si vous ouvrez ce fichier, vous y verrez le contenu suivant:
    </para>

    <programlisting language="php"><![CDATA[
// application/models/DbTable/Guestbook.php

/**
 * This is the DbTable class for the guestbook table.
 */
class Application_Model_DbTable_Guestbook extends Zend_Db_Table_Abstract
{
    /** Table name */
    protected $_name    = 'guestbook';
}
]]></programlisting>

    <para>
        Notez le préfixe de classe: <classname>Application_Model_DbTable</classname>. Le premier segment
        est "Application", le nom du module, puis vient le nom du composant "Model_DbTable" qui est lié
        au dossier <filename>models/DbTable/</filename> du module.
    </para>

    <para>
        Pour étendre <classname>Zend_Db_Table</classname>, seuls un nom de table et éventuellement un
        nom de clé primaire (si ce n'est pas "id") sont nécessaires.
    </para>

    <para>
        Créons maintenant un <ulink url="http://martinfowler.com/eaaCatalog/dataMapper.html">Data
            Mapper</ulink>. Un <emphasis>Data Mapper</emphasis> fait correspondre un objet métier à la
        base de données. Dans notre cas <classname>Application_Model_Guestbook</classname> vers la
        source de données <classname>Application_Model_DbTable_Guestbook</classname>. Une
        <acronym>API</acronym> typique pour un data mapper est:
    </para>

    <programlisting language="php"><![CDATA[
// application/models/GuestbookMapper.php

class Application_Model_GuestbookMapper
{
    public function save($model);
    public function find($id, $model);
    public function fetchAll();
}
]]></programlisting>

    <para>
        En plus de ces méthodes nous allons ajouter des méthodes pour affecter/récupérer l'objet
        Table Data Gateway. Pour créer la classe initiale, utilsez l'outil CLI
        <command>zf</command>:
    </para>

    <programlisting language="shell"><![CDATA[
% zf create model GuestbookMapper
Creating a model at application/models/GuestbookMapper.php
Updating project profile '.zfproject.xml'
]]></programlisting>

    <para>
        Maintenant, éditez la classe <classname>Application_Model_GuestbookMapper</classname> dans
        <filename>application/models/GuestbookMapper.php</filename> pour y voir ceci:
    </para>

    <programlisting language="php"><![CDATA[
// application/models/GuestbookMapper.php

class Application_Model_GuestbookMapper
{
    protected $_dbTable;

    public function setDbTable($dbTable)
    {
        if (is_string($dbTable)) {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract) {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }

    public function getDbTable()
    {
        if (null === $this->_dbTable) {
            $this->setDbTable('Application_Model_DbTable_Guestbook');
        }
        return $this->_dbTable;
    }

    public function save(Application_Model_Guestbook $guestbook)
    {
        $data = array(
            'email'   => $guestbook->getEmail(),
            'comment' => $guestbook->getComment(),
            'created' => date('Y-m-d H:i:s'),
        );

        if (null === ($id = $guestbook->getId())) {
            unset($data['id']);
            $this->getDbTable()->insert($data);
        } else {
            $this->getDbTable()->update($data, array('id = ?' => $id));
        }
    }

    public function find($id, Application_Model_Guestbook $guestbook)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $guestbook->setId($row->id)
                  ->setEmail($row->email)
                  ->setComment($row->comment)
                  ->setCreated($row->created);
    }

    public function fetchAll()
    {
        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();
        foreach ($resultSet as $row) {
            $entry = new Application_Model_Guestbook();
            $entry->setId($row->id)
                  ->setEmail($row->email)
                  ->setComment($row->comment)
                  ->setCreated($row->created);
            $entries[] = $entry;
        }
        return $entries;
    }
}
]]></programlisting>

    <para>
        Maintenant il faut créer la classe de modèle. Une fois de plus, nous utiliserons la commande
        <command>zf create model</command>:
    </para>

    <programlisting language="shell"><![CDATA[
% zf create model Guestbook
Creating a model at application/models/Guestbook.php
Updating project profile '.zfproject.xml'
]]></programlisting>

    <para>
        Nous allons modifier cette classe <acronym>PHP</acronym> vide pour simplifier le remplissage
        du modèle via un tableau dans le constructeur ou une méthode
        <methodname>setOptions()</methodname>. Le code final de la classe de modèle stockée dans
        <filename>application/models/Guestbook.php</filename> devrait ressembler à ceci:
    </para>

    <programlisting language="php"><![CDATA[
// application/models/Guestbook.php

class Application_Model_Guestbook
{
    protected $_comment;
    protected $_created;
    protected $_email;
    protected $_id;

    public function __construct(array $options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }

    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid guestbook property');
        }
        $this->$method($value);
    }

    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method)) {
            throw new Exception('Invalid guestbook property');
        }
        return $this->$method();
    }

    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }

    public function setComment($text)
    {
        $this->_comment = (string) $text;
        return $this;
    }

    public function getComment()
    {
        return $this->_comment;
    }

    public function setEmail($email)
    {
        $this->_email = (string) $email;
        return $this;
    }

    public function getEmail()
    {
        return $this->_email;
    }

    public function setCreated($ts)
    {
        $this->_created = $ts;
        return $this;
    }

    public function getCreated()
    {
        return $this->_created;
    }

    public function setId($id)
    {
        $this->_id = (int) $id;
        return $this;
    }

    public function getId()
    {
        return $this->_id;
    }
}
]]></programlisting>

    <para>
        Enfin, pour connecter tous ces éléments ensemble, créons un contrôleur qui listera les entrées
        de la base de données.
    </para>

    <para>
        Pour créer le nouveau contrôleur, utilisez la commande <command>zf create controller</command>:
    </para>

    <programlisting language="shell"><![CDATA[
% zf create controller Guestbook
Creating a controller at
    application/controllers/GuestbookController.php
Creating an index action method in controller Guestbook
Creating a view script for the index action method at
    application/views/scripts/guestbook/index.phtml
Creating a controller test file at
    tests/application/controllers/GuestbookControllerTest.php
Updating project profile '.zfproject.xml'
]]></programlisting>

    <para>
        Ceci va créer <classname>GuestbookController</classname> dans
        <filename>application/controllers/GuestbookController.php</filename>, avec une seule action
        <methodname>indexAction()</methodname>. Un script de vue sera aussi crée pour ce contrôleur,
        il sera logé dans <filename>application/views/scripts/guestbook/</filename>, avec une vue
        pour l'action index.
    </para>

    <para>
        Nous allons utiliser l'action "index" pour lister toutes les entrées du livre d'or.
    </para>

    <para>
        Un aterrissage sur <methodname>indexAction()</methodname> devra lister toutes les
        entrées du livre d'or. Ceci ressemblera à ce qui suit:
    </para>

    <programlisting language="php"><![CDATA[
// application/controllers/GuestbookController.php

class GuestbookController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $guestbook = new Application_Model_GuestbookMapper();
        $this->view->entries = $guestbook->fetchAll();
    }
}
]]></programlisting>

    <para>
        Et bien sûr un script de vue sera nécessaire. Editez
        <filename>application/views/scripts/guestbook/index.phtml</filename> pour y
        inclure ceci:
    </para>

    <programlisting language="php"><![CDATA[
<!-- application/views/scripts/guestbook/index.phtml -->

<p><a href="<?php echo $this->url(
    array(
        'controller' => 'guestbook',
        'action'     => 'sign'
    ),
    'default',
    true) ?>">Sign Our Guestbook</a></p>

Guestbook Entries: <br />
<dl>
    <?php foreach ($this->entries as $entry): ?>
    <dt><?php echo $this->escape($entry->email) ?></dt>
    <dd><?php echo $this->escape($entry->comment) ?></dd>
    <?php endforeach ?>
</dl>
]]></programlisting>

    <note>
        <title>Checkpoint</title>

        <para>
            Naviguez maintenant vers "http://localhost/guestbook". Vous devriez voir ceci
            apparaitre dans votre navigateur:
        </para>

        <para>
            <inlinegraphic width="525" scale="100" align="center" valign="middle"
                fileref="figures/learning.quickstart.create-model.png" format="PNG" />
        </para>
    </note>

    <note>
        <title>Utiliser le script de chargement des données</title>

        <para>
            Le script de chargement des données montré dans la section en question
            (<filename>scripts/load.sqlite.php</filename>) peut être utilisé pour créer une base
            de données pour chaque environnement défini et la remplir de données d'exemple. En
            interne, il utilise <classname>Zend_Console_Getopt</classname>, qui permet de préciser
            des options à la commande. Si vous passez "-h" ou "--help", toutes les options
            disponibles seront affichées:
        </para>

        <programlisting language="php"><![CDATA[
Usage: load.sqlite.php [ options ]
--withdata|-w         Load database with sample data
--env|-e [  ]         Application environment for which to create database
                      (defaults to development)
--help|-h             Help -- usage message)]]
]]></programlisting>

        <para>
            L'option "-e" permet de préciser la valeur de la constante
            <constant>APPLICATION_ENV</constant> -- ce qui en effet permet de créer une base de
            données SQLite pour chaque environnement défini. N'oubliez pas l'envrionnement
            lorsque vous utilisez ce script.
        </para>
    </note>
</sect1>
