TigerZF
🌐Español

3.4. Crear un modelo y una tabla de base de datos

Antes de comenzar, consideremos algo: ¿dónde vivirán estas clases, y cómo las encontraremos? El proyecto por defecto que creamos instancia un autoloader. Podemos adjuntarle otros autoloaders para que sepa dónde encontrar diferentes clases. Normalmente, queremos que nuestras distintas clases MVC estén agrupadas bajo el mismo árbol -- en este caso, application/ -- y casi siempre usando un prefijo común.

Zend_Controller_Front tiene una noción de "módulos", que son mini-aplicaciones individuales. Los módulos imitan la estructura de directorios que la herramienta zf configura bajo application/, y se asume que todas las clases dentro de ellos comienzan con un prefijo común, el nombre del módulo. application/ es en sí mismo un módulo -- el módulo "default" o "application". Como tal, querremos configurar el autoloading para los recursos dentro de este directorio.

Zend_Application_Module_Autoloader proporciona la funcionalidad necesaria para mapear los distintos recursos bajo un módulo a los directorios apropiados, y también proporciona un mecanismo de nomenclatura estándar. Se crea una instancia de la clase por defecto durante la inicialización del objeto bootstrap; el bootstrap de su aplicación usará por defecto el prefijo de módulo "Application". Como tal, nuestros modelos, formularios y clases de tabla comenzarán todos con el prefijo de clase "Application_".

Ahora, consideremos qué compone un libro de visitas. Normalmente, son simplemente una lista de entradas con un comentario, una marca de tiempo, y, a menudo, una dirección de correo electrónico. Suponiendo que las almacenamos en una base de datos, también podríamos querer un identificador único para cada entrada. Probablemente querremos poder guardar una entrada, obtener entradas individuales y recuperar todas las entradas. Como tal, una API simple de modelo de libro de visitas podría verse algo así:

// 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();
}

__get() y __set() nos proporcionarán un mecanismo de conveniencia para acceder a las propiedades individuales de la entrada, y delegarán a los demás getters y setters. También ayudarán a asegurar que solo las propiedades que incluyamos en la lista blanca estarán disponibles en el objeto.

find() y fetchAll() proporcionan la capacidad de obtener una única entrada o todas las entradas, mientras que save() se encarga de guardar una entrada en el almacén de datos.

Ahora, desde aquí, podemos empezar a pensar en configurar nuestra base de datos.

Primero necesitamos inicializar nuestro recurso Db. Al igual que con los recursos Layout y View, podemos proporcionar configuración para el recurso Db. Podemos hacer esto con el comando zf configure db-adapter:

% 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.

Ahora edite su archivo application/configs/application.ini, donde verá que se agregaron las siguientes líneas en las secciones apropiadas.

; 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"

Su archivo de configuración final debería verse como el siguiente:

; 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"

Tenga en cuenta que la(s) base(s) de datos se almacenarán en data/db/. Cree esos directorios, y hágalos escribibles por todos. En sistemas tipo unix, puede hacerlo de la siguiente manera:

% mkdir -p data/db; chmod -R a+rwX data

En Windows, necesitará crear los directorios en el Explorador y establecer los permisos para permitir que cualquiera escriba en el directorio.

Llegados a este punto tenemos una conexión a una base de datos; en nuestro caso, es una conexión a una base de datos Sqlite ubicada dentro de nuestro directorio application/data/. Así que, diseñemos una tabla simple que contendrá las entradas de nuestro libro de visitas.

-- 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");

Y, para que tengamos algunos datos de trabajo desde el principio, creemos algunas filas de información para hacer que nuestra aplicación sea interesante.

-- 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'));

Ahora que tenemos definidos tanto el esquema como algunos datos. Preparemos un script que podamos ahora ejecutar para construir esta base de datos. Naturalmente, esto no es necesario en producción, pero este script ayudará a los desarrolladores a construir los requisitos de la base de datos localmente para que puedan tener la aplicación completamente funcional. Cree el script como scripts/load.sqlite.php con el siguiente contenido:

// scripts/load.sqlite.php

/**
 * Script for creating and loading database
 */

// Initialize the application path and autoloading
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();

// Define some CLI options
$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) {
    // Bad options passed: report usage
    echo $e->getUsageMessage();
    return false;
}

// If help requested, report usage message
if ($getopt->getOption('h')) {
    echo $getopt->getUsageMessage();
    return true;
}

// Initialize values based on presence or absence of CLI options
$withData = $getopt->getOption('w');
$env      = $getopt->getOption('e');
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (null === $env) ? 'development' : $env);

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

// Initialize and retrieve DB resource
$bootstrap = $application->getBootstrap();
$bootstrap->bootstrap('db');
$dbAdapter = $bootstrap->getResource('db');

// let the user know whats going on (we are actually creating a
// database here)
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);
    }
}

// Check to see if we have a database file already
$options = $bootstrap->getOption('resources');
$dbFile  = $options['db']['params']['dbname'];
if (file_exists($dbFile)) {
    unlink($dbFile);
}

// this block executes the actual statements that were loaded from
// the schema file.
try {
    $schemaSql = file_get_contents(dirname(__FILE__) . '/schema.sqlite.sql');
    // use the connection directly to load sql in batches
    $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');
        // use the connection directly to load sql in batches
        $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;
}

// generally speaking, this script will be run from the command line
return true;

Ahora, ejecutemos este script. Desde una terminal o la línea de comandos de DOS, haga lo siguiente:

% php scripts/load.sqlite.php --withdata

Debería ver una salida como la siguiente:

path/to/ZendFrameworkQuickstart/scripts$ php load.sqlite.php --withdata
Writing Database Guestbook in (control-c to cancel):
1
Database Created
Data Loaded.

Ahora tenemos una base de datos y tabla completamente funcionales para nuestra aplicación de libro de visitas. Nuestros próximos pasos son construir el código de nuestra aplicación. Esto incluye construir una fuente de datos (en nuestro caso, usaremos Zend_Db_Table), y un data mapper para conectar esa fuente de datos a nuestro modelo de dominio. Finalmente también crearemos el controlador que interactuará con este modelo tanto para mostrar las entradas existentes como para procesar entradas nuevas.

Usaremos un Table Data Gateway para conectarnos a nuestra fuente de datos; Zend_Db_Table proporciona esta funcionalidad. Para empezar, creemos una clase de tabla basada en Zend_Db_Table. Al igual que hicimos para los layouts y el adaptador de base de datos, podemos usar la herramienta zf para ayudarnos, usando el comando create db-table. Esto toma como mínimo dos argumentos, el nombre por el cual desea referirse a la clase, y la tabla de base de datos a la que se mapea.

% zf create db-table Guestbook guestbook
Creating a DbTable at application/models/DbTable/Guestbook.php
Updating project profile 'zfproject.xml'

Al mirar su árbol de directorios, ahora verá que se creó un nuevo directorio, application/models/DbTable/, con el archivo Guestbook.php. Si abre ese archivo, verá el siguiente contenido:

// 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';
}

Observe el prefijo de la clase: Application_Model_DbTable. El prefijo de clase para nuestro módulo, "Application", es el primer segmento, y luego tenemos el componente, "Model_DbTable"; este último se mapea al directorio models/DbTable/ del módulo.

Todo lo que es realmente necesario al extender Zend_Db_Table es proporcionar un nombre de tabla y opcionalmente la clave primaria (si no es "id").

Ahora creemos un Data Mapper. Un Data Mapper mapea un objeto de dominio a la base de datos. En nuestro caso, mapeará nuestro modelo, Application_Model_Guestbook, a nuestra fuente de datos, Application_Model_DbTable_Guestbook. Una API típica para un data mapper es la siguiente:

// application/models/GuestbookMapper.php

class Application_Model_GuestbookMapper
{
    public function save($model);
    public function find($id, $model);
    public function fetchAll();
}

Además de estos métodos, agregaremos métodos para establecer y recuperar el Table Data Gateway. Para crear la clase inicial, use la herramienta CLI zf:

% zf create model GuestbookMapper
Creating a model at application/models/GuestbookMapper.php
Updating project profile '.zfproject.xml'

Ahora, edite la clase Application_Model_GuestbookMapper que se encuentra en application/models/GuestbookMapper.php para que quede de la siguiente manera:

// 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;
    }
}

Ahora es momento de crear nuestra clase de modelo. Lo haremos, una vez más, usando el comando zf create model:

% zf create model Guestbook
Creating a model at application/models/Guestbook.php
Updating project profile '.zfproject.xml'

Modificaremos esta clase PHP vacía para facilitar la población del modelo pasando un array de datos ya sea al constructor o a un método setOptions(). La clase de modelo final, ubicada en application/models/Guestbook.php, debería verse así:

// 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;
    }
}

Por último, para conectar todos estos elementos, creemos un controlador de libro de visitas que liste las entradas que actualmente están dentro de la base de datos.

Para crear un nuevo controlador, use el comando zf create controller:

% 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'

Esto creará un nuevo controlador, GuestbookController, en application/controllers/GuestbookController.php, con un único método de acción, indexAction(). También creará un directorio de scripts de vista para el controlador, application/views/scripts/guestbook/, con un script de vista para la acción index.

Usaremos la acción "index" como página de aterrizaje para ver todas las entradas del libro de visitas.

Ahora, desarrollemos la lógica básica de la aplicación. Al acceder a indexAction(), mostraremos todas las entradas del libro de visitas. Esto se vería de la siguiente manera:

// application/controllers/GuestbookController.php

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

Y, por supuesto, necesitamos un script de vista que lo acompañe. Edite application/views/scripts/guestbook/index.phtml para que quede de la siguiente manera:

<!-- 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>
[Note] Punto de control

Ahora navegue a "http://localhost/guestbook". Debería ver lo siguiente en su navegador:

[Note] Uso del script de carga de datos

El script de carga de datos introducido en esta sección (scripts/load.sqlite.php) puede usarse para crear la base de datos para cada entorno que haya definido, así como para cargarla con datos de ejemplo. Internamente, utiliza Zend_Console_Getopt, lo que le permite proporcionar varios modificadores de línea de comandos. Si pasa el modificador "-h" o "--help", le dará las opciones disponibles:

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)]]

El modificador "-e" le permite especificar el valor a usar para la constante APPLICATION_ENV -- lo que a su vez le permite crear una base de datos SQLite para cada entorno que defina. Asegúrese de ejecutar el script para el entorno que elija para su aplicación al desplegarla.