lunes, 6 de octubre de 2014

Finalizando el objeto Menus y el uso de getServiceLocator

En la entrada anterior expliqué tanto el listado como la creación de registros de nuestro módulo "Menús", para finalizarlo tan solo debemos añadir las acciones con sus correspondientes vistas a la edición y el borrado.

Previo a incluir los nuevos métodos en nuestro controlador, no podemos olvidar que hemos de espeficar a las rutas de acceso de nuestro módulo esperará un parámetro ID. Para ello modificaremos el fichero module.config.php del módulo Menus:

Contenido del fichero /module/Menus/config/module.config.php:

<?php
return array(
    'controllers' => array(
        'invokables' => array(
            'Menus\Controller\Index' => 'Menus\Controller\Index',
            'Menus\Controller\Menus' => 'Menus\Controller\MenusController',
        ),
    ),
    'router' => array(
        'routes' => array(
            'menus' => array(
                'type'    => 'Literal',
                'options' => array(
                    // Change this to something specific to your module
                    'route'    => '/menus',
                    'defaults' => array(
                        // Change this value to reflect the namespace in which
                        // the controllers for your module are found
                        '__NAMESPACE__' => 'Menus\Controller',
                        'controller'    => 'Index',
                        'action'        => 'index',
                    ),
                ),
                'may_terminate' => true,
                'child_routes' => array(
                    // This route is a sane default when developing a module;
                    // as you solidify the routes for your module, however,
                    // you may want to remove it and replace it with more
                    // specific routes.
                    'default' => array(
                        'type'    => 'Segment',
                        'options' => array(
                            'route'    => '/[:controller[/:action[/:id]]]',
                            'constraints' => array(
                                'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                                'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                                'id'     => '[0-9]*',
                            ),
                            'defaults' => array(
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
    'view_manager' => array(
        'template_path_stack' => array(
            'Menus' => __DIR__ . '/../view',
        ),
    ),
);

Vamos ahora sí a por faena y vamos a incluir en el controlador ambas acciones:

Añadir los siguientes métodos en el fichero /module/Menus/src/Menus/Controller/MenusController.php:
    public function editAction() {
        $form = new MenuForm();
        $form->setInputFilter(new MenuFilter());
        $viewVars = array (
            'title' => 'Edit Menu',
            'form' => $form
        );
        if (! $this->request->isPost ()) {
            $id = ( int ) $this->params ()->fromRoute ( 'id' );
            $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
            $menusTable = new MenusTable($adapter);
            $menuDb = $menusTable->getMenuById ( $id );
            $form->setData ( $menuDb );
            return new ViewModel ( $viewVars );
        } else {
            $post = $this->request->getPost ();
            $id = ( int ) $post->id_menu;
            $form->setData ( $post );
            if (! $form->isValid ()) {
                $viewVars ['error'] = true;
                return new ViewModel ( $viewVars );
            }
        }
        // Edit menu
        try {
            $formData = $form->getData ();
            $menu = (isset ( $formData ['menu'] )) ? $formData ['menu'] : null;
            $name = (isset ( $formData ['name'] )) ? $formData ['name'] : null;
            $label = (isset ( $formData ['label'] )) ? $formData ['label'] : null;
            $module = (isset ( $formData ['module'] )) ? $formData ['module'] : null;
            $controller = (isset ( $formData ['controller'] )) ? $formData ['controller'] : null;
            $action = (isset ( $formData ['action'] )) ? $formData ['action'] : null;
            $order = (isset ( $formData ['order'] )) ? $formData ['order'] : null;
    
            $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
            $menusTable = new MenusTable($adapter);
            $menusTable->updateMenu ( $id, $menu, $name, $label, $module, $controller, $action, $order );
            return $this->redirect ()->toRoute ( NULL, array (
                'controller' => 'menus',
                'action' => 'index'
            ) );
        } catch ( \Exception $e ) {
            $viewVars ['error'] = true;
            $viewVars ['message'] = $e->getMessage ();
            return new ViewModel ( $viewVars );
        }
    }
    
    public function deleteAction() {
        try {
            $id = ( int ) $this->params ()->fromRoute ( 'id' );

            $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
            $menusTable = new MenusTable($adapter);
            $menusTable->deleteMenu( $id );
            return $this->redirect ()->toRoute ( NULL, array (
                'controller' => 'Menus',
                'action' => 'index'
            ) );
        } catch ( \Exception $e ) {
            $viewVars ['title'] = 'Delete Menu';
            $viewVars ['message'] = $e->getMessage ();
            return new ViewModel ( $viewVars );
        }
    }

Por cada acción necesitaremos una vista asociada, así pues hemos de crear ambas, edit.phtml y delete.phtml.

Contenido del archivo /module/Menus/view/menus/menus/edit.phtml :
<section class="edit">
<h2><?php echo $this->translate($this->title); ?></h2>

<?php if ($this->error){ ?>
<p class="error">
    <?php echo $this->translate('There were one or more isues with your submission. Please correct them as indicated below.'); ?>
    <?php if(isset($this->message) && $this->message != '') { ?>
     <br>
     <?php echo $this->message; ?>
     <?php } ?>
</p>
<?php } ?>

<?php 
$form = $this->form;
$form->prepare();
$form->setAttribute('action', $this->url(NULL, array('controller'=>'menus', 'action' => 'edit')));
$form->setAttribute('method', 'post');

echo $this->form()->openTag($form);
?>
    <div class="form-group">
<?php echo $this->formLabel($form->get('menu')); ?>
<?php 
    echo $this->formElement($form->get('menu'));
    echo $this->formElementErrors($form->get('menu'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('name')); ?>
<?php 
    echo $this->formElement($form->get('name'));
    echo $this->formElementErrors($form->get('name'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('label')); ?>
<?php 
    echo $this->formElement($form->get('label'));
    echo $this->formElementErrors($form->get('label'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('module')); ?>
<?php 
    echo $this->formElement($form->get('module'));
    echo $this->formElementErrors($form->get('module'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('controller')); ?>
<?php 
    echo $this->formElement($form->get('controller'));
    echo $this->formElementErrors($form->get('controller'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('action')); ?>
<?php 
    echo $this->formElement($form->get('action'));
    echo $this->formElementErrors($form->get('action'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('order')); ?>
<?php 
    echo $this->formElement($form->get('order'));
    echo $this->formElementErrors($form->get('order'));
?>
 </div>   
    <div class="form-group">
<?php 
    echo $this->formElement($form->get('submit'));
    echo $this->formElementErrors($form->get('submit'));
?>&nbsp;
<a class="btn btn-default" 
   href="<?php echo $this->url('menus/default', array('controller' => 'menus', 'action' => 'index'), null); ?>"><?php echo $this->translate('Return back');?></a>
 </div>

<?php echo $this->formElement($form->get('id_menu')); ?>
<?php echo $this->form()->closeTag() ?>
</section>

Contenido del archivo /module/Menus/view/menus/menus/delete.phtml :
<section class="delete-confirm">
<h3><?php echo $this->translate($this->title); ?></h3>
<p class="error">
    <?php echo $this->translate('There were one or more isues with your submission. Please correct them as indicated below.'); ?>
    <?php if(isset($this->message) && $this->message != '') { ?>
     <br>
     <?php echo $this->message; ?>
     <?php } ?>
</p>

<p><a href="<?php echo $this->url('menus/default', array('controller' => 'groups'), null); ?>"><?php echo $this->translate('Return back');?></a></p>
</section>

Para finalizar debemos actualizar el modelo de la tabla Menus para incluir la actualización y borrado de registros, así como la búsqueda de un registro por identificador.

Añadir los siguientes métodos en el fichero /module/Menus/src/Menus/Model/MenusTable.php:

    public function getMenuById($id) {
        $id  = (int) $id;
        $rowset = $this->select(array('id_menu' => $id));
        $row = $rowset->current();
        if (!$row) {
            throw new \Exception("Could not find row $id");
        }
        return $row;
    }
    
    public function updateMenu($id, $menu, $name, $label, $module, $controller, $action, $order){
        $row = $this->getMenuById($id);
        $data['menu'] = $menu;
        $data['name'] = $name;
        $data['label'] = $label;
        $data['module'] = $module;
        $data['controller'] = $controller;
        $data['action'] = $action;
        $data['order'] = $order;
        if ($row) {
            $this->update($data, array('id_menu' => $id));
        } else {
            throw new \Exception('Menu ID does not exist');
        }
    }
    
    public function deleteMenu($id){
        $id = (int) $id;
        $rowset = $this->select(array('id_menu' => $id));
        $row = $rowset->current();
        if (!$row) {
            throw new \Exception("Could not find row $id");
        }
        $this->delete(array('id_menu' => $id));
    }

Podemos probar los scripts y validar tanto la edición como el borrado de entradas.

El uso de getServiceLocator

Puede ser interesante encapsular las llamadas tanto al modelo como al formulario mediante el uso de getServiceLocator.
Si editamos el fichero /module/Module.php y incluimos el siguiente método a la clase Module:

    public function getServiceConfig(){
        return array(
            'abstract_factories' => array(),
            'aliases' => array(),
            'factories' => array(
                'MenusTable' => function ($sm) {
                    return new MenusTable( $sm->get('\Zend\Db\Adapter\Adapter') );
                },
                // Forms
                'MenuForm' => function ($sm) {
                    $form = new \Menus\Form\MenuForm();
                    $form->SetInputFilter($sm->get('MenuFilter'));
                    return $form;
                },
                //Filters
                'MenuFilter' => function ($sm) {
                    return new \Menus\Form\MenuFilter();
                },
            ),
            'invokables' => array(),
            'services' => array(),
            'shared' => array(),
        );
   }

En nuestro código podremos sustituir todas las llamadas a MenusTables y MenuForm usando getServiceLocator.

En el caso de MenusTable el código:
        $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
        $menusTable = new MenusTable($adapter);

Podremos sustituirlo por:
        $menusTable = $this->getServiceLocator ()->get ( 'MenusTable' );

En el caso de la llamada al formulario:
        $form = new MenuForm();
        $form->setInputFilter(new MenuFilter());

Podremos sustituirlo por:
        $form = $this->getServiceLocator ()->get ( 'MenuForm' );

El código queda más limpio.

viernes, 3 de octubre de 2014

Accediendo a la base de datos con ZF2

Zend Framework nos proporciona un potente conjunto de funciones para la interacción con nuestra base de datos. En mi caso uso MySQL y para acceder a él trabajaremos con el controlador PDO (Objetos de datos para PHP).
Con los componentes Zend\Db\Resultset y Zend\Db\Sql nos permitirán hacer consultas a nuestra base de datos.
Mientras que con Resultset podremos crear la SQL directamente con Sql dispondremos de una capa de abstracción que mediante el uso de sus métodos crearemos la consulta.
Otra forma de acceder a los datos contenidos en nuestra base de datos es medianet Zend\Db\TableGateway, usaremos este método cuando queramos trabajar con "factory's". Creando una clase que derive de TableGateway nos permitirá invocar sus métodos nativos para interaccionar con los datos de la base de datos.

Antes de plantearnos nada hemos de definir la cadena de conexión con nuestro sistema gestor, para ello es en el fichero local.php de la carpeta autoload de nuestra aplicación donde deberán estar ubicada esta definición. Este fichero no existe, pero existe otro de nombre local.php.dist que podemos renombrar y usar.

En nuestro caso y como ejemplo hemos creado una base de datos llamada myapp, con un usuario myAppUsername con privilegios de acceso y una contraseña myAppPassword.

El esquema de nuestra base de datos es:

  /* Tabla menus */
  CREATE TABLE `menus` (
    `id_menu` int(10) unsigned NOT NULL,
    `menu` varchar(255) NOT NULL DEFAULT 'navigation',
    `name` varchar(255) NOT NULL,
    `label` varchar(255) NOT NULL,
    `module` varchar(255) NOT NULL,
    `controller` varchar(255) NOT NULL,
    `action` varchar(255) NOT NULL,
    `order` smallint(6) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

  /* Insertar menus */
  INSERT INTO `menus` (`id_menu`, `menu`, `name`, `label`, `module`, `controller`, `action`, `order`) VALUES
  (1, 'topnav', 'Home', 'Home', 'Application', 'Index', 'index', 0),
  (2, 'admin', 'Menus', 'Menus', 'Menus', 'Menus', 'index', 0); 

Contenido del fichero /config/autoload/local.php:

<?php

return array(
  'db' => array(
   'driver' => 'Pdo',
   'dsn'  => 'mysql:dbname=myapp;host=localhost',
   'username' => 'myAppUsername',
   'password' => 'myAppPassword',
   'driver_options' => array(
    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'utf8\''
   ), 
  ),
  'service_manager' => array(
   'factories' => array(
    'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory'
   ),
  ),
);

El componente TableGateway es el que encuentro es el más extendido para la interacción con los datos de nuestra base datos.
Lo más correcto es crear una clase que nos proporcione métodos para la interacción con los datos. Al igual que las clases formularios estarán ubicadas en su carpeta src/ModuleName/Form las clases encargadas del acceso a la base de datos las ubicaremos en la carpeta src/ModuleName/Model. Así pues crearemos la carpeta Model en /module/Menus/src/Menus/, y dentro crearemos nuestra clase en el fichero MenusTable.php.

Contenido del fichero /module/Menus/src/Menus/Model/MenusTable.php:

<?php
namespace Menus\Model;

use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Sql\Select;
use Zend\Db\ResultSet\ResultSet;

class MenusTable extends TableGateway
{
    protected $table = 'menus';

    public function __construct(Adapter $adapter = null, $databaseSchema = null, ResultSet $selectResultPrototype = null)
    {
        return parent::__construct($this->table, $adapter, $databaseSchema, $selectResultPrototype);
    }
    
    public function saveMenu($menu, $name, $label, $module, $controller, $action, $order){
        $data['menu'] = $menu;
        $data['name'] = $name;
        $data['label'] = $label;
        $data['module'] = $module;
        $data['controller'] = $controller;
        $data['action'] = $action;
        $data['order'] = $order;
        
        $this->insert($data);
        return $this->lastInsertValue;
    }
}

Una vez definido la clase modelo ya estamos capacitados a interaccionar con la base de datos desde nuestro controlador. Así editaremos nuestro fichero MenusController.php y añadiremos la funcionalidad de creación de un nuevo registro.

Contenido del fichero /module/Menus/src/Menus/Controller/MenusController.php:

<?php
namespace Menus\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Menus\Form\MenuForm;
use Menus\Form\MenuFilter;
use Menus\Model\MenusTable;

class MenusController extends AbstractActionController {
    /**
     * Crea un nuevo registro
     *
     * @return \Zend\View\Model\ViewModel
     */
    public function createAction() {
        $form = new MenuForm();
        $form->setInputFilter(new MenuFilter());
        $viewVars = array (
            'title' => 'Add Menu',
            'form' => $form
        );
        if (! $this->request->isPost ()) {
            return new ViewModel ( $viewVars );
        }
        $post = $this->request->getPost ();
        $form->setData ( $post );
        if (! $form->isValid ()) {
            $viewVars ['error'] = true;
            return new ViewModel ( $viewVars );
        }
        // Create menu
        try {
            $formData = $form->getData();
            $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
            $MenusTable = new MenusTable($adapter);
            $menu = (isset ( $formData ['menu'] )) ? $formData ['menu'] : null;
            $name = (isset ( $formData ['name'] )) ? $formData ['name'] : null;
            $label = (isset ( $formData ['label'] )) ? $formData ['label'] : null;
            $module = (isset ( $formData ['module'] )) ? $formData ['module'] : null;
            $controller = (isset ( $formData ['controller'] )) ? $formData ['controller'] : null;
            $action = (isset ( $formData ['action'] )) ? $formData ['action'] : null;
            $order = (isset ( $formData ['order'] )) ? $formData ['order'] : null;
            $MenusTable->saveMenu ( $menu, $name, $label, $module, $controller, $action, $order );
            return $this->redirect()->toRoute(NULL, array(
                'controller' => 'menus',
                'action' => 'index'
            ));
        } catch (\Exception $e) {
            $viewVars['error'] = true;
            $viewVars['message'] = $e->getMessage();
        }
        return new ViewModel ( $viewVars );
    }
}

En este métod podemos acceder de dos formas, mediante la llamada directa desde URL, en ese caso $this->request->isPost () será true, con lo que directamente saldremos del método y mostraremos el formulario de entrada de datos, o, el acceso es mediante el action del formulario, en ese caso $this->request->isPost () devolverá un valor false y seguirá ejecutando las instrucciones del método.
En el caso de que nos encontremos que es el formulario envía los datos mediante el método post, debemos validarlos mediante $form->isValid (), que nos devolverá un valor false en caso de que no cumpla con las reglas establecidas.
El siguiente paso será recuperar los datos enviados en el request y pasarlos a nuestra clase modelo para que realice una inserción de un nuevo registro.
Para recuperar valores del post en nuestro controlador siempre podemos usar:

$data = $this->request->getPost();
$variable = $data['variable']; 

Para finalizar este ejemplo deberemos crear el método indexAction dentro de nuestro controlador, así como la vista al mismo asociado.

Antes editaremos nuestra clase modelo y añadiremos la función fetchAll.
Añadiremos un nuevo método al fichero /module/Menus/src/Menus/Model/MenusTable.php:

    public function fetchAll($where = array(), $order = array('order asc')){
        $rowset = $this->select(function (Select $select) use ($where, $order){
            if( count($where) > 0 ){
                $select->where ( $where );
            }
            if( count($order) > 0 ){
                $select->order ( $order );
            }
        } );
        return $rowset;
    }

En nuestro controlador añadiremos el método indexAction.
Añadiremos un nuevo método al fichero /module/Menus/src/Menus/Controller/MenusController.php:

    public function indexAction()
    {
        $adapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
        $menusTable = new MenusTable($adapter);
        $menus = $menusTable->fetchAll(array(), array(
            'menu',
            'order'
        ));
        $viewVars = array(
            'title' => 'Menus list',
            'menus' => $menus
        );
        return new ViewModel($viewVars);
    } 
Ahora tan solo nos falta definir la nueva vista asociada a la acción de crear un nuevo registro en el controlador. Para ello crearemos un fichero llamado index.phtml en /module/Menus/view/menus/.
Contenido del fichero /module/Menus/view/menus/index.phtml:

<section class="Menus">
<?php if( isset($this->menus) ){ ?>
 <table class="table table-striped table-bordered table-condensed">
 <tr>
  <th><?php echo $this->translate('Menu'); ?></th>
  <th><?php echo $this->translate('Menu Name'); ?></th>
  <th><?php echo $this->translate('Label'); ?></th>
  <th><?php echo $this->translate('Module'); ?></th>
  <th><?php echo $this->translate('controller'); ?></th>
  <th><?php echo $this->translate('action'); ?></th>
  <th><?php echo $this->translate('order'); ?></th>
  <th width="300px">&nbsp;</th> 
 </tr>
 <?php  
  foreach ($this->menus as $menu){ ?>
 <tr>
  <td><?php echo $menu['menu']; ?></td>
  <td><?php echo $menu['name']; ?></td>
  <td><?php echo $menu['label']; ?></td>
  <td><?php echo $menu['module']; ?></td>
  <td><?php echo $menu['controller']; ?></td>
  <td><?php echo $menu['action']; ?></td>
  <td><?php echo $menu['order']; ?></td>
  <td><a href="<?php echo $this->url('menus/default', array('controller' => 'menus', 'action' => 'edit', 'id' => $menu['id_menu']), null); ?>"><?php echo $this->translate('Edit'); ?></a>
   | <a href="<?php echo $this->url('menus/default', array('controller' => 'menus', 'action' => 'delete', 'id' => $menu['id_menu']), null); ?>"><?php echo $this->translate('Delete');?></a>   
   </td>
 </tr>
 <?php
  } ?> 
 </table>
<?php } ?> 
 <p><a class="btn btn-primary" href="<?php echo $this->url('menus/default', array('controller' => 'menus', 'action' => 'create'), null); ?>"><?php echo $this->translate('Add new menu');?></a></p>
</section>

Sólo nos resta comprobar el resultado de la creación de nuevas entradas de menú, para ello navegaremos a la dirección http://localhost/myApp/menus/menus e iremos insertando nuevos registros:


Dejaremos para más adelante la edición y el borrado de entradas.

jueves, 2 de octubre de 2014

Creando formularios con Zf2

Toda aplicación necesita un mecanismo de intercambio de información mediante formularios. Zend Framework dota de una potente capa de abstracción para realizar dicha tarea.
Es recomendable consultar la documentación oficial de Zend al respecto: Introduction to Zend\Form

Generalmente organizaremos nuestros formularios en una carpeta para dicho propósito, dicha carpeta estará ubicada en el fuente de nuestro módulo, en el caso de ser el módulo "Menus", para seguir con los ejemplos anteriores, la carpeta la podríamos llamar "Form" y la ubicariamos en /module/Menus/src/Menus/.

El objetivo es crear un formulario para la inserción de los campos de las entradas de menú, para ello a parte del formulario crearemos su controlador y vistas asociadas.

Nuestro formulario lo crearemos en el fichero MenuForm.php:
Contenido del fichero /module/Menus/src/Menus/Form/MenuForm.php:

<?php
// filename : /module/Menus/src/Menus/Form/MenuForm.php
namespace Menus\Form;

use Zend\Form\Form;

class MenuForm extends Form
{

    public function __construct($name = null)
    {
        parent::__construct('Menus');
        $this->setAttribute('method', 'post');
        $this->setAttribute('enctype', 'multipart/form-data');
        
        $this->add(array(
            'name' => 'id_menu',
            'attributes' => array(
                'type' => 'hidden'
            )
        ));
        
        $this->add(array(
            'name' => 'menu',
            'attributes' => array(
                'type' => 'text',
                'required' => 'required',
                'class' => 'form-control'
            ),
            'options' => array(
                'label' => 'Menu Type'
            )
        ));
        
        $this->add(array(
            'name' => 'name',
            'attributes' => array(
                'type' => 'text',
                'required' => 'required',
                'class' => 'form-control'
            ),
            'options' => array(
                'label' => 'Menu Name'
            )
        ));
        
        $this->add(array(
            'name' => 'label',
            'attributes' => array(
                'type' => 'text',
                'required' => 'required',
                'class' => 'form-control'
            ),
            'options' => array(
                'label' => 'Label'
            )
        ));
        
        $this->add(array(
            'name' => 'module',
            'attributes' => array(
                'type' => 'text',
                'required' => 'required',
                'class' => 'form-control'
            ),
            'options' => array(
                'label' => 'Module'
            )
        ));
        
        $this->add(array(
            'name' => 'controller',
            'attributes' => array(
                'type' => 'text',
                'required' => 'required',
                'class' => 'form-control'
            ),
            'options' => array(
                'label' => 'Controller'
            )
        ));
        
        $this->add(array(
            'name' => 'action',
            'attributes' => array(
                'type' => 'text',
                'required' => 'required',
                'class' => 'form-control'
            ),
            'options' => array(
                'label' => 'Action'
            )
        ));
        
        $this->add(array(
            'name' => 'order',
            'attributes' => array(
                'type' => 'text',
                'required' => 'required',
                'class' => 'form-control'
            ),
            'options' => array(
                'label' => 'Order'
            )
        ));
        
        $this->add(array(
            'name' => 'submit',
            'attributes' => array(
                'type' => 'submit',
                'value' => 'Submit',
                'class' => 'btn btn-success'
            )
        ));
    }
}

Zend Framework nos proporciona una serie de funciones incorporadas para el tratamiento de los datos en formularios, eso es especialmente interesante para evitar accesos no deseados a nuestra base de datos, además de usar una metodología ordenada que permite separar los datos de su definición.

Procederemos a crear un nuevo fichero php llamado MenuFilter.php en /module/Menus/src/Menus/Form/.
Contenido del fichero /module/Menus/src/Menus/Form/MenuFilter.php.

<?php
namespace Menus\Form;

use Zend\InputFilter\InputFilter;

class MenuFilter extends InputFilter
{

    public function __construct()
    {
        $this->add(array(
            'name' => 'menu',
            'required' => true,
            'filters' => array(
                array(
                    'name' => 'StripTags'
                )
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'encoding' => 'UTF-8',
                        'min' => 2,
                        'max' => 255
                    )
                )
            )
        ));
        
        $this->add(array(
            'name' => 'name',
            'required' => true,
            'filters' => array(
                array(
                    'name' => 'StripTags'
                )
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'encoding' => 'UTF-8',
                        'min' => 2,
                        'max' => 255
                    )
                )
            )
        ));
        
        $this->add(array(
            'name' => 'label',
            'required' => true,
            'filters' => array(
                array(
                    'name' => 'StripTags'
                )
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'encoding' => 'UTF-8',
                        'min' => 2,
                        'max' => 255
                    )
                )
            )
        ));
        
        $this->add(array(
            'name' => 'module',
            'required' => true,
            'filters' => array(
                array(
                    'name' => 'StripTags'
                )
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'encoding' => 'UTF-8',
                        'min' => 2,
                        'max' => 255
                    )
                )
            )
        ));
        
        $this->add(array(
            'name' => 'controller',
            'required' => true,
            'filters' => array(
                array(
                    'name' => 'StripTags'
                )
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'encoding' => 'UTF-8',
                        'min' => 2,
                        'max' => 255
                    )
                )
            )
        ));
        
        $this->add(array(
            'name' => 'action',
            'required' => true,
            'filters' => array(
                array(
                    'name' => 'StripTags'
                )
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'encoding' => 'UTF-8',
                        'min' => 2,
                        'max' => 255
                    )
                )
            )
        ));

        $this->add(array(
            'name' => 'order',
            'required' => true,
            'filters' => array(
                array(
                'name'                   => 'Digits',
                'break_chain_on_failure' => true, 
                )
            ),
            'validators' => array(
                array(
                    'name' => 'Between',
                    'options' => array(
                        'min' => 1,
                        'max' => 99,
                    ), 
                )
            )
        ));
    }
}
Recordemos que el controlador invocará los datos del formulario y pasará en forma de variables a la vista, que será la encargada de generar el código HTML pertinente.
Nuestro controlador lo llamaremos MenusController.php y lo ubicaremos en la carpeta  /module/Menus/src/Menus/Controller.
Contenido del fichero /module/Menus/src/Menus/Controller/MenusController.php:

<?php
namespace Menus\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Menus\Form\MenuForm;
use Menus\Form\MenuFilter;

class MenusController extends AbstractActionController {
    /**
     * Crea un nuevo registro
     *
     * @return \Zend\View\Model\ViewModel
     */
    public function createAction() {
        $form = new MenuForm();
        $form->setInputFilter(new MenuFilter());
        $viewVars = array (
            'title' => 'Add Menu',
            'form' => $form
        );
        if (! $this->request->isPost ()) {
            return new ViewModel ( $viewVars );
        }
        $post = $this->request->getPost ();
        $form->setData ( $post );
        if (! $form->isValid ()) {
            $viewVars ['error'] = true;
            return new ViewModel ( $viewVars );
        }
        // Create menu
        try {
            // ...
        } catch (\Exception $e) {
            $viewVars['error'] = true;
            $viewVars['message'] = $e->getMessage();
        }
        return new ViewModel ( $viewVars );
    }
}   

Es una buena práctica gestionar los datos en el controlador y pasarlos como variables a la vista, en este caso estamos usando el array viewVars para incluir en él todos los parámetros. Posteriormente en la vista podremos acceder fácilmente usando la cláusual this.

Cada acción susceptible de ser invocada en el controlador debe tener una vista asociada. La vista que transformará el contenido en HTML se llamará create.phtml y estará ubicada en la carpeta "menus" (en minúsculas), que no existe, nuestro siguiente paso será crearla en /module/Menus/view.
Contenido del fichero  /module/Menus/view/menus/create.phtml:

<section class="create">
 <h2><?php echo $this->translate($this->title); ?></h2>

<?php if ($this->error){ ?>
<p class="error">
    <?php echo $this->translate('There were one or more isues with your submission. Please correct them as indicated below.'); ?>
    <?php if(isset($this->message) && $this->message != '') { ?>
     <br>
     <?php echo $this->message; ?>
     <?php } ?>
</p>
<?php } ?>

<?php
$form = $this->form;
$form->prepare();
$form->setAttribute('action', $this->url(NULL, array(
    'controller' => 'menus',
    'action' => 'create'
)));
$form->setAttribute('method', 'post');
echo $this->form()->openTag($form);
?>
 <div class="form-group">
<?php echo $this->formLabel($form->get('menu')); ?>
<?php
echo $this->formElement($form->get('menu'));
echo $this->formElementErrors($form->get('menu'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('name')); ?>
<?php
echo $this->formElement($form->get('name'));
echo $this->formElementErrors($form->get('name'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('label')); ?>
<?php
echo $this->formElement($form->get('label'));
echo $this->formElementErrors($form->get('label'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('module')); ?>
<?php
echo $this->formElement($form->get('module'));
echo $this->formElementErrors($form->get('module'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('controller')); ?>
<?php
echo $this->formElement($form->get('controller'));
echo $this->formElementErrors($form->get('controller'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('action')); ?>
<?php
echo $this->formElement($form->get('action'));
echo $this->formElementErrors($form->get('action'));
?>
 </div>
 <div class="form-group">
<?php echo $this->formLabel($form->get('order')); ?>
<?php
echo $this->formElement($form->get('order'));
echo $this->formElementErrors($form->get('order'));
?>
 </div>
 <div class="form-group">
<?php
echo $this->formElement($form->get('submit'));
echo $this->formElementErrors($form->get('submit'));
?>&nbsp;
<a class="btn btn-default"
   href="<?php echo $this->url('menus/default', array('controller' => 'menus', 'action' => 'index'), null); ?>"><?php echo $this->translate('Return back');?></a>
 </div>
<?php echo $this->form()->closeTag()?>
</section>
Como detalle comentar que tenemos acceso directo a las variables que desde el controlador hemos pasado a la vista: p. ej.: $this->title.

Finalmente hemos de incluir nuestro nuevo controlador en la definición de las rutas de nuestro módulo.
Contenido del fichero /module/Menus/config/module.config.php:

<?php
return array(
    'controllers' => array(
        'invokables' => array(
            'Menus\Controller\Index' => 'Menus\Controller\Index',
            'Menus\Controller\Menus' => 'Menus\Controller\MenusController',
        ),
    ),
    'router' => array(
        'routes' => array(
            'menus' => array(
                'type'    => 'Literal',
                'options' => array(
                    'route'    => '/menus',
                    'defaults' => array(
                        '__NAMESPACE__' => 'Menus\Controller',
                        'controller'    => 'Index',
                        'action'        => 'index',
                    ),
                ),
                'may_terminate' => true,
                'child_routes' => array(
                    'default' => array(
                        'type'    => 'Segment',
                        'options' => array(
                            'route'    => '/[:controller[/:action]]',
                            'constraints' => array(
                                'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                                'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                            ),
                            'defaults' => array(
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
    'view_manager' => array(
        'template_path_stack' => array(
            'Menus' => __DIR__ . '/../view',
        ),
    ),
);

Ya podemos comprobar la salida de nuestro formulario creado consultando la página http://localhost/myApp/menus/menus/create:


miércoles, 1 de octubre de 2014

Creando un módulo con Zend Framework 2

La arquitectura Modelo-Controlador-Vista intenta separar la capa de datos (Modelo) de su representación (Vista), y la capa que gestiona las relaciones entre ambas son los Controladores. Dicho de forma sencilla, nuestra aplicación sólo ejecutará acciones de los controladores, las cuales instanciaran modelos de datps de los que extraerán datos que posteriormente pasarán a las vistas para su representación.

La estructura básica de una URL en ZF2 es:
http://app/módulo/controlador/acción/parámetros

Recordemos que el uso de módulos nos permite segmentar el código, haciendo que sea más fácil de mantener y distribuir.
En la estructura de un módulo encontraremos tres carpetas imprescindibles:
  • config: donde se ubicarán los ficheros de configuración del módulo.
  • src: aquí encontraremos los controladores del módulo.
  • view: las vistas y layouts que nos determinaran la salida HTML.
Posteriormente podremos añadir otras carpetas de más interés como diccionarios, plugins, etc.

La creación de un módulo es tan sencilla como la creación de una aplicación, descargaremos el esqueleto base de un módulo desde el repositorio git de Zend Framework, como ejemplo crearemos un módulo llamado Menus:

cd /path_project/module
git clone git://github.com/zendframework/ZendSkeletonModule.git Menus

Este paso nos permitirá crear toda la estructura de directorios básica. Por defecto el esqueleto básico nos trae un controlador llamado ZendSkeletonModule con sus vistas asociadas. Para poder hacer accesible el nuevo módulo creado debemos incluirlo en el fichero /config/application.config.php de nuestra aplicación:

<?php
return array(
    // This should be an array of module namespaces used in the application.
    'modules' => array(
        'Application', 'Menus',
    ),

    // These are various options for the listeners attached to the ModuleManager
    'module_listener_options' => array(
        'module_paths' => array(
            './module',
            './vendor','./module',
        ),

        'config_glob_paths' => array(
            'config/autoload/{,*.}{global,local}.php',
        ),
    ),
);

En el módulo descargado desde git hemos de realizar algunas correcciones para adaptarlo al nombre que le hemos dado.
Fichero /module/Menus/Module.php:
...
namespace Menus;
..

Fichero /module/Menus/config/module.config.php sustituir cada ocurrencia de "ZendSkeletonModule" por "Menus" al igual que "module-specific-root" por "menus". Así mismo redefiniremos el controlador por "Index" en lugar de "SkeletonController":

<?php
return array(
    'controllers' => array(
        'invokables' => array(
            'Menus\Controller\Index' => 'Menus\Controller\Index',
        ),
    ),
    'router' => array(
        'routes' => array(
            'module-name-here' => array(
                'type'    => 'Literal',
                'options' => array(
                    // Change this to something specific to your module
                    'route'    => '/menus',
                    'defaults' => array(
                        // Change this value to reflect the namespace in which
                        // the controllers for your module are found
                        '__NAMESPACE__' => 'Menus\Controller',
                        'controller'    => 'Index',
                        'action'        => 'index',
                    ),
                ),
                'may_terminate' => true,
                'child_routes' => array(
                    // This route is a sane default when developing a module;
                    // as you solidify the routes for your module, however,
                    // you may want to remove it and replace it with more
                    // specific routes.
                    'default' => array(
                        'type'    => 'Segment',
                        'options' => array(
                            'route'    => '/[:controller[/:action]]',
                            'constraints' => array(
                                'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                                'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                            ),
                            'defaults' => array(
                            ),
                        ),
                    ),
                ),
            ),
        ),
    ),
    'view_manager' => array(
        'template_path_stack' => array(
            'Menus' => __DIR__ . '/../view',
        ),
    ),
);

Renombraremos la carpeta /module/Menus/src/ZendSkeletonModule por /module/Menus/src/Menus.
Renombraremos la carpeta /module/Menus/view/zend-skeleton-module por /module/Menus/view/menus.
Renombraremos la carpeta  /module/Menus/view/menus/skeleton por /module/Menus/view/menus/index.
Finalmente renombraremos el fichero /module/Menus/src/Menus/Controller/SkeletonController.php por /module/Menus/src/Menus/Controller/Index.php

Una vez hecho podemos invocar cualquiera de las acciones por defecto que han sido creadas en nuestro módulo:
http://localhost/myApp/menus
http://localhost/myApp/menus/index/foo


Ya hemos creado nuestro primer módulo en Zf2, el siguiente paso es dotarlo de funcionalidad.

martes, 30 de septiembre de 2014

Primeros pasos con Zend Framework 2

Crear un proyecto para Zend Framework 2 es muy sencillo, con la herramienta git podemos descargar el esqueleto básico de nuestra aplicación del repositorio de Zend Framework.

Previamente podemos instalar git, tan solo hemos de descargarlo de la página oficial de git y ejecutar el instalador.
Para crear nuestro proyecto clonaremos el repositorio de Zend Framework, para ello supongamos que deseamos crear una aplicación llamada myApp:

cd myworkspace
git clone git://github.com/zendframework/ZendSkeletonApplication.git myApp
cd myApp
php composer.phar self-update
php composer.phar install


Ahora sólo nos falta configurar nuestro servidor de aplicaciones para que acceda a la carpeta donde tenemos ubicado nuestro proyecto.

En mi caso uso Zend Server, y como IDE Zend Studio. El proceso de creación de una aplicación es muy sencillo, vamos a File -> New -> PHP Project from Git:


Definiremos los campos de Location con el directorio de nuestro área de trabajo.
PHP Server será el servidor Zend destino.
La URI es la dirección del repositorio del esqueleto de Zend:
git://github.com/zendframework/ZendSkeletonApplication.git 

Una vez creado dispondremos del árbol de directorios en nuestro entorno de desarrollo.
En Zend Studio podremos hacer un deploy de nuestra aplicación a Zend Server y poder ver los resultados rápidamente y sin complicaciones:


Ahora ya disponemos de nuestra aplicación para ser explorada (http://localhost/myApp):

Realmente sencillo y efectivo.

Algunos aspectos interesantes a tener en cuenta en Zf2:
  • Se usa el estándar de codificación de Zend, se usa phtml para las vistas pero no para las clases.
  • El código generado en HTML es exclusivo de las vistas, en las clases nunca se generará HTML.
  • Atributos de las clases siempre privados o protegidos, principio de ocultación o encapsulación. Usaremos las funciones setter/getter para acceder a los atributos.
  • Se debe usar el CamelCase:
    En los nombres de clases se empieza siempre por mayúsculas.
    Métodos y variables con minúscula la primera y luego a cada palabra en Mayúsculas:
    variable: miVariable, nombrePersona, contadorPalabras, etc.
    clases: Usuarios, Paginas, ControlUsuarios, etc.
  • No usar short tags. El uso de "<?= ... ?>" debe evitarse, mejor usar en su lugar "<?php .... ?>". Recordar siempre finalizar las instrucciones en ";".
    Ej; <a href="<?php echo $this->url('home'); ?>">
  • Respetar el nombre de los archivos que se llamen igual que la clase contenida.

¿Por qué Zend Framework?

Llega un momento en que se ha de hacer un alto en el camino y reflexionar sobre lo visto. Después de un tiempo trabajando con Zend Framework, de recoger experiencia, de visitar páginas y sobre todo de picar código me quedan claros las ventajas de usar Zend.

¿Por qué usar un Framework?

Un framework es un conjunto de librerías que nos proporcionan un marco de trabajo. Una filosofía de trabajo que nos permite, entre otras cosas:
  • Mayor orden. Sistema modular que nos permite separar desarrollos.
  • Mayor independencia, nos brinda un marco de trabajo común que facilita la distribución del trabajo entre desarrolladores.
  • Aplicación de patrones.
  • Mayor seguridad. La comunidad proporciona los últimos avances y tendencias, delegando las tareas comunes de filtrado y verificación de datos de formularios a la funcionalidad del framework.

¿Por qué Zend Framework?

 En el caso que nos ocupa Zend Framework nos brinda:
  • Está hecho en PHP, actualmente uno de los lenguajes de script para desarrollo en web más populares.
  • Está basado en SPL (Standard PHP library), que son un conjunto de clases nativas de PHP.
  • Es gratuito.
  • Es potente.
  • Es flexible, tanto podemos usarlo como framework como sus módulos por separado.
  • Securizado (en ambiente web). Dota de mecanismos de filtrado de entrada de datos.
  • Orientado a Eventos (Event Manager).
  • Extensible como por ejemplo los plugins de youtube.

¿Por qué Zend Framework 2?

Puede asaltarnos la duda del uso de Zend Framework 1 o 2, está claro que la segunda versión es una revisión de la primera con muchísimos aspectos mejorados:
  • Autoloading mejorado.
  • Namespaces, usado para el manejo de rutas.
  • 100% orientado a Eventos.
  • Trabaja con DOM (jQuery, etc).
  • Empaquetamiento PHAR. Podemos usar ZendSkeleton para desplegar un proyecto.
  • Usa patrones de desarollo MVC (Modelo-Vista-Controlador), Doctrine, etc.
  • Capa de abstracción de la base de datos.
  • Mejorado el soporte multilenguaje.
En Zend Framework 1 disponíamos de los controladores en la raíz de la aplicación, en cambio en Zend Framework 2 se dispone de un nivel de orden jerárquico más siendo este los módulos, donde ubicaremos nuestros controladores, vistas, layouts, etc. Así, en un proyecto simple podríamos pensar que Zend Framework 1 nos puede ser suficiente, pero un desarrollo de ya más alta envergadura, más extenso, separar las diferentes áreas funcionales del aplicativo en módulos nos permitirá tener un código más claro y fácil de mantener.

jueves, 17 de abril de 2014

Creación de una entrada de datos con estilo usando Zend Framework (2a parte)

Nuestro paso inicial es crear el formulario de creación/edición.
Para ello añadiremos el fichero /application/forms/Doc.php:

<?php

class Form_Doc extends Zend_Form
{
    public function init ()
    {
        $this->setMethod('post');
        // Nou element id (hidden)
        $id = $this->createElement('hidden', 'id');
        // Opcions de l'element
        $id->setDecorators(array(
                'ViewHelper'
        ));
        $this->addElement($id);
        // Nou element Títol
        $titol = $this->createElement('text', 'titol')
            ->setLabel('Títol: ')
            ->setRequired(TRUE)
            ->setAttrib('size', 40)
            ->addFilter('StripTags');
        $this->addElement($titol);
        // Nou element descripció
        $descripcio = $this->createElement('text', 'descripcio')
            ->setLabel('Descripció: ')
            ->setRequired(FALSE)
            ->addFilter('StripTags');
        $this->addElement($descripcio);
        // Nou element Autors
        $autors = $this->createElement('multiselect', 'lst_autors')
            ->setLabel('Author: ')
            ->setRequired(TRUE)
            ->setAttrib('class', 'form-field')
            ->setRegisterInArrayValidator(false)
            ->setAttrib('multiple', 'multiple');
        $this->addElement($autors);
        // Nou element submit
        $submit = $this->createElement('submit', 'submit')->setLabel('Submit');
        $this->addElement($submit);
    }
}

En el modelo de datos será necesario incluir la función miembro para la creación del documento. Para ello editaremos el fichero /application/models/Docs.php y añadiremos las siguientes funciones:

    public function createDocs ($titol, $descripcio, $lst_autors)
    {
        // Creació d'un nou registre
        $row = $this->createRow();
        if ($row) {
            // actualitzem valors
            $row->titol = $titol;
            $row->descripcio = $descripcio;
            if ($row->save()) {
                // $id_document = $this->_db->lastInsertId();
                $id_document = $row->id_document;
                $this->salvaAutors($id_document, $lst_autors);
            }
            return true;
        } else {
            throw new Zend_Exception("El document no s'ha pogut crear!");
        }
    }

    private function salvaAutors ($id_document, $lst_autors)
    {
        $db = Zend_Db_Table::getDefaultAdapter();
        foreach ($lst_autors as $id_autor) {
            $data = array(
                    'id_document' => $id_document,
                    'id_autor' => $id_autor
            );
            $rows_affected = $db->insert('autors_documents', $data);
        }
    }

En el controlador añadiremos la acción create para que responda a la petición de creación de nuevo documento, para ello editaremos el fichero /application/controllers/DocsController.php y incluiremos la siguiente función miembro:
    public function createAction ()
    {
        $frm = new Form_Doc();
        if ($this->getRequest()->isPost()) {
            if ($frm->isValid($_POST)) {
                $titol = $frm->getValue('titol');
                $descripcio = $frm->getValue('descripcio');
                $lst_autors = $frm->getValue('lst_autors');
                $mdl = new Model_Docs();
                $result = $mdl->createDocs($titol, $descripcio, $lst_autors);
                if ($result) {
                    // redirecció a l'acció index
                    return $this->_redirect('/docs/index');
                }
            }
        }
        
        $frm->setAction('/docs/create');
        $this->view->form = $frm;
    }

Antes de finalizar hemos de crear una vista para la acción que acabamos de definir, para ello añadiremos el siguiente fichero /application/views/scripts/docs/create.phtml con el contenido:

<h2><?php echo $this->translate('Creació d\' un nou document'); ?></h2>
<p><?php echo $this->translate('Per crear un nou document complementi aquest formulari i polsi acceptar...'); ?></p>
<?php echo $this->form; ?>


Ahora ya podemos ejecutar nuestro formulario para ver que pinta tiene:


Como podemos observar el acabado del multiselección es bastante pobre. Vamos a incluir el plugin de jQuery, a ver que tal queda.
Primero de todo hemos de ubicar los scripts de Multiselect y jQuery en las carpetas adecuadas. Los ficheros deberían ser:
  1. /public/js/jquery.lwMultiSelect.js
  2. /public/js/jquery-1.11.0.js
  3. /public/css/jquery.lwMultiSelect.css
  4. /public/doublearrow.png
Las carpetas /public/js y /public/css deberán ser creadas y ubicar allí los scripts.
Modificaremos la vista de creación de documentos para que incluya los scripts e inicialice el multiselect. Editamos /application/views/scripts/docs/create.phtml:
    
<?php
$this->headScript()->prependFile('/js/jquery.lwMultiSelect.js');
$this->headScript()->prependFile('/js/jquery-1.11.0.js');
$this->headLink()->prependStylesheet('/css/jquery.lwMultiSelect.css');
$strAddAll = $this->translate('Afegir tots');
$strClear = $this->translate('Netejar');
$strSelected = $this->translate('Seleccionats');
$this->headScript()->appendScript('$(document).ready(function() {
$("#lst_autors").lwMultiSelect({
        addAllText:\'' . $strAddAll . '\',
        removeAllText:\'' . $strClear . '\', 
        selectedLabel:\'' . $strSelected . '\'});
});
', $type = 'text/javascript');
?>
<h2><?php echo $this->translate('Creació d\' un nou document'); ?></h2>
<p><?php echo $this->translate('Per crear un nou document complementi aquest formulari i polsi acceptar...'); ?></p>
<?php echo $this->form; ?>


Ahora nuestro formulario tendrá un aspecto mucho más profesional:

Creación de una entrada de datos con estilo usando Zend Framework (1a parte)

El objetivo es poder crear una entrada de datos para un formulario de forma agradable con Zend Framework, para ello crearemos la alimentación de datos de una tabla que contiene más de 1000 registros, y de cómo crear un formulario que nos permita trabajar con tantos datos de forma rápida y ágil.

Para editar un formulario con estilo usaremos un plugin de jQuery qne nos brinda la selección de múltiples opciones usando un select.
La url de descarga es: jQuery plugin Light Weight Multiselect

Consideraciones prévias:

Disponemos de dos tablas: autores y documentos con una relación de N a N entre ellas. La tabla de autores son muchos los registros de que dispone, y la tabla de documentos debe alimentarse de uno o varios autores.
La definición de tablas es:

CREATE TABLE `autors` (
  `id_autor` int(11) NOT NULL AUTO_INCREMENT,
  `descripcio` varchar(250) DEFAULT NULL,
  PRIMARY KEY (`id_autor`)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS `documents` (
  `id_document` int(11) NOT NULL AUTO_INCREMENT,
  `titol` varchar(250) NOT NULL,
  `descripcio` varchar(250) NOT NULL,
  PRIMARY KEY (`id_document`)
) ENGINE=InnoDB;

La tabla que relaciona a ambas:
CREATE TABLE IF NOT EXISTS `autors_documents` (
  `id_document` int(11) NOT NULL DEFAULT '0',
  `id_autor` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id_document`,`id_autor`),
  KEY `id_document` (`id_document`),
  KEY `id_autor` (`id_autor`)
) ENGINE=InnoDB;

ALTER TABLE `autors_documents`
  ADD CONSTRAINT `autors_documents_ibfk_1` 
    FOREIGN KEY (`id_document`) REFERENCES `documents` (`id_document`),
  ADD CONSTRAINT `autors_documents_ibfk_2` 
    FOREIGN KEY (`id_autor`) REFERENCES `autors` (`id_autor`);

Para este ejemplo crearemos un proyecto nuevo en nuestra carpeta de trabajo al que llamaremos ZenSelect, en mi caso la carpeta de trabajo será "C:\Users\carles\Documents\workspaces", así pues en el fichero de configuración de apache incluiremos una directiva nueva para nuestro host:

<VirtualHost *:80>
    DocumentRoot "C:\Users\carles\Documents\workspaces\ZenSelect\public"
    ServerName zenselect.localhost
    SetEnv APPLICATION_ENV development
    <Directory "C:\Users\carles\Documents\workspaces\ZenSelect\public">
        Options Indexes MultiViews FollowSymLinks
        AllowOverride All
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

En nuestro fichero "C:\Windows\System32\drivers\etc\hosts" incluiremos el dominio de nuestro host para que sea accesible:
127.0.0.1 zenselect zenselect.localhost

Reiniciamos servicio web. Hecho esto podremos acceder a nuestro site ejecutando http://zenselect.localhost.

Configuraremos los parámetros de acceso a la base de datos en el fichero "/application/configs/application.ini":
Nota: Previamente hemos creado una base de datos con las credenciales de usuario zendselect y contraseña zendselect.

resources.view.encoding = "ISO-8859-1"
resources.db.adapter = PDO_MYSQL
resources.db.params.host = localhost
resources.db.params.username = zendselect
resources.db.params.password = zendselect
resources.db.params.dbname = zendselect

Incluiremos en nuestro fichero /application/bootstrap.php la función _initView y _initAutoload para definir el layout de salida e incluir los directorios de forms y models con los formularios y modelos correspondientes.
<?php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{

    protected function _initView ()
    {
        $view = new Zend_View();
        $options = $this->getOptions();
        if (isset($options['resources']['view'])){
            $view = new Zend_View($options['resources']['view']);
        } else{
            $view = new Zend_View();
        }         
        $view->doctype("XHTML1_STRICT");
        $view->headMeta()->appendHttpEquiv("Content-Type", 
                "text/html;charset=ISO-8859-1");
        $view->headMeta()->appendHttpEquiv("Content-Language", "es_ES");
        $view->headTitle("Zend Select");
        $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper(
                'ViewRenderer');
        $viewRenderer->setView($view);
        return $view;
    }

    protected function _initAutoload ()
    {
        $autoLoader = Zend_Loader_Autoloader::getInstance();
        $autoLoader->registerNamespace('CMS_');
        $resourceLoader = new Zend_Loader_Autoloader_Resource(
                array(
                        'basePath' => APPLICATION_PATH,
                        'namespace' => '',
                        'resourceTypes' => array(
                                'form' => array(
                                        'path' => 'forms/',
                                        'namespace' => 'Form_'
                                ),
                                'model' => array(
                                        'path' => 'models/',
                                        'namespace' => 'Model_'
                                )
                        )
                ));
        // Return it so that it can be stored by the bootstrap
        return $autoLoader;
    }
}

Crearemos un conjunto de carpetas llamados /application/layout/scripts y allí ubicaremos nuestro fichero de layout layout.phtml, cuyo contenido será:
<?php
    echo $this->doctype();
?>
<html>
<head>
<?php
    echo $this->headMeta();
    echo $this->headTitle();
    echo $this->headScript();
    echo $this->headLink();
?>
</head>
<body>
    <?php echo $this->layout()->content; ?>
</body>
</html>

Hemos de incluir en nuestro fichero de configuración /application/configs/application.ini la definición del fichero de layout.

resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"

Para finalizar las consideraciones iniciales crearemos la carpeta "forms" dentro de "application".


Listado/creación/edición/borrado de Documents

Antes de crear el controlador que nos permita gestionar las acciones que sobre nuestros datos en "Documents", crearemos los modelos que nos permitan interaccionar sobre las tablas de "autors" y "documents".

Fichero /application/models/Autors.php:
<?php
require_once 'Zend/Db/Table/Abstract.php';

class Model_Autors extends Zend_Db_Table_Abstract {
    protected $_name = 'Autors';

    public function getAutors($filters = array(), $sortField = null) {
    $select = $this->select ();
    if ($sortField != null)
        $select->order ( $sortField );
    // afegirem els filtres passats
    if (count ( $filters ) > 0) {
        foreach ( $filters as $field => $filter ) {
            if ($field == 'descripcio ')
                $select->where ( $field . ' LIKE %?', $filter );
            else
                $select->where ( $field . ' = ?', $filter );
        }
    }
    // Afegim l'ordenació si cal
    $rows = $this->fetchAll ( $select );
    if ($rows->count () > 0) {
        return $rows;
    } else {
        return null;
    }
}

Fichero /application/models/Docs.php:
<?php
require_once 'Zend/Db/Table/Abstract.php';
class Model_Docs extends Zend_Db_Table_Abstract {
    protected $_name = 'Documents';

    public function getDocs()
    {
        $select = $this->select();
        $select->order(array('descripcio'));
        $rows = $this->fetchAll($select);
        if($rows->count() > 0) {
            return $rows;
        }else{
            return null;
        }
    }
}
El siguiente paso es crear el controlador de nuestros documentos:
Fichero /application/controllers/DocsController.php:
<?php
class DocsController extends Zend_Controller_Action
{

    public function init (){    } 

    public function indexAction ()
    {
        $mdl = new Model_Docs();
        $this->view->docs = $mdl->getDocs();
    }
}

Fichero /application/views/scripts/docs/index.phtml:

<h2><?php echo $this->translate('Current Documents'); ?></h2>
<?php if($this->docs != null) { ?>
<table class='spreadsheet' cellpadding='0' cellspacing='0'>
    <tr>
        <th>&nbsp;</th>
    <th><?php echo $this->translate('Títol Document'); ?></th>
    </tr>
<?php echo $this->partialLoop('partials/_docs-row.phtml', $this->docs); ?>
</table>
<?php }else{?>
<p><?php echo $this->translate('Encara no hi ha cap document definit.'); ?></p>
<?php }?>
<p>
    <a href='/docs/create'><?php echo $this->translate('Crear nou document'); ?></a>
</p>

Crearemos una carpeta en /application/views/scripts/partials, y dentro crearemos un nuevo Fichero /application/views/scripts/partials/_docs-row.phtml:
 
<tr>
 <td class='links'><a href='/docs/edit/id/<?php echo $this->id_document;?>'><?php echo $this->translate('Editar'); ?></a>
  | <a href='/docs/delete/id/<?php echo $this->id_document;?>'><?php echo $this->translate('Esborrar'); ?></a>
 </td>
 <td><?php echo $this->descripcio ?></td>
</tr>

Para finalizar incluiremos en nuestro archivo /application/configs/application.ini el módulo por defecto de visualización que sea nuestro docs:

resources.frontController.defaultControllerName = "docs"

De esta forma ya tenemos el esqueleto incial de nuestro proyecto.
En el siguiente capítulo veremos como crear un formulario de edición con el código jQuery Multiselect.

martes, 18 de febrero de 2014

Las bases de datos

Ya empieza a tener forma nuestra aplicación, pero falta lo más importante, dotarla de la funcionalidad desead: la lógica de negocio. Como comenté anteriormente la filosofía de Zend Framework se basa en la arquitectura Modelo-Vista-Controlador, siendo el modelo las clases que representan las relaciones de nuestros datos.
Para empezar definiremos los parámetros de conexión en nuestro fichero application/configs/application.ini, en la sección [production] de la siguiente forma:
resources.db.adapter = PDO_MYSQL
resources.db.params.host = localhost
resources.db.params.username = nombre_usuario
resources.db.params.password = password
resources.db.params.dbname = nombre_base_de_datos

Evidentemente con los datos de seguridad adecuados. Con nuestro gestor de datos habitual crearemos la base de datos y el usuario, yo personalmente uso phpMyAdmin. Puede descargarse el paquete para Zend Server o directamente el código he instalarlo en nuestro servidor. Como ayuda la configuración del alias de Apache podría tener esta forma:

   Alias /phpmyadmin "C:\Zend\Apache2\htdocs\phpMyAdmin"
   <Directory "C:\Zend\Apache2\htdocs\phpMyAdmin">
       Options Indexes MultiViews FollowSymLinks
       AllowOverride All
       Order allow,deny
       Allow from all
   </Directory>

Para invocarlo tan solo es necesario navegar a la dirección http://localhost/phpmyadmin.
Al abordar el modelo de datos apreciamos la potencia de Zend Framework, dado que trabajaremos con formularios para introducir/editar la información, y por supuesto las vistas que serán las diferentes representaciones de nuestro modelo de datos.

El modelo de relaciones entre la tabla menú y menu_items es:



Las tablas de menús (menus y menu_items). Con phpMyAdmin crearemos las dos tablas:

--
-- Estructura de tabla para la tabla `menus`
--

CREATE TABLE IF NOT EXISTS `menus` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `access_level` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

--
-- Estructura de tabla para la tabla `menu_items`
--

CREATE TABLE IF NOT EXISTS `menu_items` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `menu_id` int(11) DEFAULT NULL,
  `label` varchar(250) DEFAULT NULL,
  `link` varchar(250) DEFAULT NULL,
  `position` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

Para poder interactuar con estas tablas crearemos un modelo para cada una de ellas en nuestra aplicación. Una opción es crear un modelo por cada entidad, para más información se puede consultar la documentacion de Zend Framework Create a Model and Database Table.

Para indicarle a nuestra aplicación que busque los modelos de datos modificaremos nuestra clase Bootstrap para que los incluya.
Añadiremos la siguiente función miembro a la clase Bootstrap del fichero application/Bootstrap.php:
    protected function _initAutoload()
    {
     // Incluiremos un namespace vacio
     $autoLoader = Zend_Loader_Autoloader::getInstance();
     $autoLoader->registerNamespace('CMS_');
     $resourceLoader = new Zend_Loader_Autoloader_Resource(array(
       'basePath' => APPLICATION_PATH,
       'namespace' => '',
       'resourceTypes' => array(
               'model' => array(
                 'path' => 'models/',
                 'namespace' => 'Model_'
               ),
       ),
     ));
     // Devolvemos el autoloader que sera almacenado por el Bootstrap
     return $autoLoader;
    }

Como podemos observar nuestro modelo ha de tener la forma "Model_[nombre tabla]", y debe estar contenido en el directorio models. Las clases modelo derivan de la clase Zend_Db_Table_Abstract, que es la que nos proporcionará todas las funciones primitivas de acceso a los datos como selects, updates, deletes, etc. Es importante definir la variable protegida $_name que nos indica el nombre del modelo, asimismo en el modelo vendrán definidas las relaciones y las dependencias con el resto de modelos, es importante dado que podemos declarar acciones para garantizar la integridad de los datos. La variable que define la dependencia de tablas es un array $_dependentTables, y la de referencias otro de nombre $_referenceMap. Así nuestra clase Menú (en el fichero application/models/Menu.php) tendrá la siguiente forma:

<?php
require_once ('Zend/Db/Table/Abstract.php');

class Model_Menu extends Zend_Db_Table_Abstract
{

    protected $_name = 'menus';

    protected $_dependentTables = array(
            'Model_MenuItem'
    );
}

?>   
Y nuestra clase menu_items (en el fichero application/models/MenuItem.php) será:
<?php
require_once ('Zend/Db/Table/Abstract.php');

class Model_MenuItem extends Zend_Db_Table_Abstract
{

    protected $_name = 'menu_items';

    protected $_referenceMap = array(
            'Menu' => array(
                    'columns' => array(
                            'menu_id'
                    ),
                    'refTableClass' => 'Model_Menu',
                    'refColumns' => array(
                            'id'
                    ),
                    'onDelete' => self::CASCADE,
                    'onUpdate' => self::RESTRICT
            )
    );
}

?>

Como podemos ver en menu_items está definida la relación entre la clase menu_item (menu_id) y la clase menu (id), el detalle de la definición de las acciones 'onDelete' y 'onUpdate' para salvaguardar la integridad de los datos.

viernes, 14 de febrero de 2014

¿Cómo crear un portal multidiioma con Zend Framework?

Siguiendo con el tema de la codificación Zend Framework nos proporciona herramientas para crear un portal multiidioma.
La clase Zend_Translate dota a nuestros proyectos de forma nativa del soporte multiidioma, es decir, que componentes como Zend_View o Zend_Form son compatibles con esta internacionalización.
El primer paso a dar es determinar que formato tendrán nuestros diccionarios. La clase Zend_Adapter es la encargada de alimentar a Zend_Translate de los términos traducidos. Zend_Translate_Adapter es similar a los componentes de acceso a bases de datos como Zend_Db_Table y ofrecen diversos formatos de entrada:

  • Array: Codificación de los mensajes en código PHP dentro de nuestro proyecto.
  • CSV: texto separado por comas.
  • GetText: Ficheros de traducción *.mo.
  • Ini: Ficheros tradicionales *.ini.
Para más información sobre los diferentes formatos de entrada consultar la documentación de Zend_Translate_Adapters.
En nuestro ejemplo usaremos GetText, para ello es muy recomendable el uso del programa poedit para la generación de los diccionarios.

Para empezar crearemos una carpeta llamada languages en la raíz de application la cual será el destino del repositorio de diccionarios. Dentro crearemos dos ficheros (para empezar) para los idiomas inglés y castellano llamados dict-en.po y dict-es.po.

Contenido del fichero application/languages/dict-en.po:

"Project-Id-Version: CDocCMS"
"POT-Creation-Date:"
"PO-Revision-Date:"
"Last-Translator: Carles <carlesuc@gmail.com>"
"Language-Team:"
"MIME-Version: 1.0"
"Content-Type: text/plain; charset=iso-8859-1"
"Content-Transfer-Encoding: 8bit"
"Language: en"
"X-Generator: Poedit 1.6.4"

#: Diccionario Inglés
msgid "Powered by the Zend Framework"
msgstr "Powered by the Zend Framework"

Contenido del fichero application/languages/dict-es.po:
"Project-Id-Version: CDocCMS"
"POT-Creation-Date:"
"PO-Revision-Date:"
"Last-Translator: Carles <carlesuc@gmail.com>"
"Language-Team:"
"MIME-Version: 1.0"
"Content-Type: text/plain; charset=iso-8859-1"
"Content-Transfer-Encoding: 8bit"
"Language: es"
"X-Generator: Poedit 1.6.4"

#: Diccionario Castellano
msgid "Powered by the Zend Framework"
msgstr "Programado en Zend Framework"

Con la utilidad poedit compilaremos dichos ficheros para generar nuestros diccionarios dict-en.mo y dict-es.mo.
Ahora incluiremos los diccionarios en nuestro fichero application.ini para que sean incluidos como recursos.
Incluiremos las siguientes líneas en el fichero application/configs/application.ini:

resources.translate.adapter = Gettext
resources.translate.default.locale = "es_ES"
resources.translate.default.file = APPLICATION_PATH "/languages/dict-es.mo"
resources.translate.translation.en = APPLICATION_PATH "/languages/dict-en.mo"
resources.translate.translation.es = APPLICATION_PATH "/languages/dict-es.mo"

El siguiente paso es cargar el recurso de traducción en nuestro proyecto. Para ello crearemos un directorio donde ubicaremos nuestros recursos con la siguiente estructura:

/ library
  / CMS
    / Application
      / Resource

Crearemos un fichero de traducción llamado Translate.php. En este fichero crearemos una clase llamada CMS_Aplication_Resource_Translate que deriva de Zend_Application_Resource_ResourceAbstract.
El sistema de traducción necesita registrar la clase Zend_Translate, la cual previamente hemos de informar de que idiomas serán los soportados por nuestra aplicación. Para ello del fichero de configuración iremos iterando por los diferentes elementos resources.translate.translation.xx que existan, y los iremos incluyendo a la clase Zend_Translate.

El contenido del fichero library/CMS/Application/Resource/Translate.php es:
<?php
class CMS_Application_Resource_Translate extends Zend_Application_Resource_ResourceAbstract
{
 public function init ()
 {
  $options = $this->getOptions();
  $adapter = $options['adapter'];
  // Diccionario por defecto
  $defaultTranslation = $options['default']['file'];
  // Idioma por defecto
  $defaultLocale = $options['default']['locale'];
  $translate = new Zend_Translate($adapter, $defaultTranslation, $defaultLocale);
  // Iteramos entre los diferentes diccionarios y los vamos incluyendo
  foreach ($options['translation'] as $locale => $translation) {
   $translate->addTranslation($translation, $locale);
  }
  Zend_Registry::set('Zend_Translate', $translate);
  return $translate;
 }
}

Es necesario incluir en nuestro fichero de configuración la carga de los recuros de traducción, para ello debemos incluir la siguiente línea en el fichero application/configs/application.ini:

pluginPaths.CMS_Application_Resource = APPLICATION_PATH "/../library/CMS/Application/Resource"

Para ver el efecto de la traducción editamos nuestro fichero layout.phtml e incluimos la sentencia $this->translate.
Contenido del fichero application/layouts/scripts/layout.phtml:

<?php
echo $this->doctype ();
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<?php
$this->loadSkin ( $this->skin );
echo $this->headMeta ();
echo $this->headTitle ();
echo $this->headScript ();
echo $this->headLink ();
?>
</head>
<body>
 <div id="container">
  <div id="header">
   <h1>Zend Framework CMS</h1>
  </div>
  <div id="navigation">
  <?php echo $this->layout()->nav;?>
 </div>
  <div id="content-container">
   <div id="content">
   <?php echo $this->layout()->content?>
  </div>
   <div id="aside">
   <?php echo $this->layout()->subNav;?> 
  </div>
   <div id="footer">
    <em><?php echo $this->translate('Powered by the Zend Framework');?></em>
   </div>
  </div>
 </div>
</body>
</html>

Podemos ver el resultado navegando por nuestro http://localhost:


Es cuestión de ir traduciendo los mensajes a nuestro gusto, como por ejemplo el título de la cabecera.

Estableciendo la codificación de nuestro portal

Los no anglo-parlantes nos vemos siempre en la problemática de los acentos, la codificación UTF-8 no los admite, se ha de hacer una conversión previa a esa codificación.
El problema es aún más grave si, como veremos, al usar diccionarios de traducción cuando se crean formularios si los campos tienen acentos veremos que no aparecen correctamente, y eso es debido a la función gettext que espera un tipo de codificación y encuentra caracteres que no son válidos.

Recordemos nuestra clase Bootstrap la cual dijimos que nos permitiría definir parámetros de configuración tanto de entorno como de componentes en concreto como nuestras vistas. Así si incluimos la codificación en esta clase, en la función encargada de la creación de las vistas (_initView), el problema queda solucionado.

Contenido del fichero application/Bootstrap.php:
<?php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
 protected function _initView()
 {
  // Initialize view
  $view = new Zend_View();
  $view->doctype('XHTML1_STRICT');
  $view->headTitle('Centro de Documentación');
  $view->skin = 'liquid';
  // Add it to the ViewRenderer
  $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper(
    'ViewRenderer'
  );
  $viewRenderer->setView($view);
  // Return it, so that it can be stored by the bootstrap
  $view->setEncoding('ISO-8859-1');
  return $view;
 }
}

Ahora sí que podremos ver nuestro título con sus acentos.
Otra forma totalmente válida es incluir estos parámetros en nuestro application.ini, podría quedar más elegante, es una forma de centralizar los parámetros de configuración en un mismo lugar y desvincularlos del código, para que no tengamos que recorrer todo nuestro código buscando allí donde definimos tal parámetro.
Añadimos en el fichero application/configs/application.ini en la sección production las siguientes líneas:

resources.view.encoding = "ISO-8859-1"
resources.view.doctype = "XHTML1_STRICT"
resources.view.contentType = "text/html;charset=ISO-8859-1"
resources.view.Content-Language = "es_ES"
resources.view.headTitle = "Centro de documentación"

Ahora toca modificar nuestra clase Bootstrap para que lea estos parámetros:
Contenido del fichero application/Bootstrap.php:

<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
 protected function _initView()
 {
 // Initialize view     
     $options = $this->getOptions();
     if (isset($options['resources']['view'])){
         $view = new Zend_View($options['resources']['view']);
     }
     else {
         $view = new Zend_View;
     }
     if (isset($options['resources']['view']['doctype'])) {
         $view->doctype($options['resources']['view']['doctype']);
     }
     if (isset($options['resources']['view']['contentType'])) {
         $view->headMeta()->appendHttpEquiv('Content-Type',$options['resources']['view']['contentType']);
     }
     if (isset($options['resources']['view']['Content-Language'])) {
      $view->headMeta()->appendHttpEquiv('Content-Language',$options['resources']['view']['Content-Language']);
     }
     if (isset($options['resources']['view']['headTitle'])) {
      $view->headTitle($options['resources']['view']['headTitle']);
     }
     $view->skin = 'liquid';
     // Add it to the ViewRenderer
     $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper(
       'ViewRenderer'
     );
     $viewRenderer->setView($view);
     // Return it, so that it can be stored by the bootstrap
     return $view;
 }
}

Podemos ver que la instrucción setEncoding ha desaparecido, deja de tener sentido dado que le pasamos al constructor de la vista la codificación.
Al probar de nuevo la página no hemos de apreciar ningún cambio, podemos verificar que deja de funcionar la codificación ISO-8859-1 si eliminamos la línea resources.view.encoding del fichero applicaction.ini.
Sólo nos queda ir al fichero layout.phtml y elminar la línea que especifica la codificación, pues si vemos el código fuente HTML de la página resultante veremos que aparece repetido.

Contenido del fichero application/layouts/scripts/layout.phtml:

<?php
echo $this->doctype ();
?>
<html>
<head>
<?php
$this->loadSkin ( $this->skin );
echo $this->headMeta ();
echo $this->headTitle ();
echo $this->headScript ();
echo $this->headLink ();
?>
</head>

Ya que estamos y para finalizar la configuración del entorno podemos incluir el skin utilizado en nuestro fichoer application.ini de la misma forma que hemos hecho con la codificación.
Añadimos en el fichero application/configs/application.ini en la sección production la siguiente línea:

resources.view.skin = "liquid"
Ahora solo es necesario cargar en skine en la clase Bootstrap.

Contenido final de la clase Bootstrap en el archivo application/Bootstrap.php:

<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
 protected function _initView()
 {
 // Initialize view     
     $options = $this->getOptions();
     if (isset($options['resources']['view']))
         $view = new Zend_View($options['resources']['view']);
     else
         $view = new Zend_View;
     if (isset($options['resources']['view']['doctype'])) 
         $view->doctype($options['resources']['view']['doctype']);
     if (isset($options['resources']['view']['contentType']))
         $view->headMeta()->appendHttpEquiv('Content-Type',$options['resources']['view']['contentType']);
     if (isset($options['resources']['view']['Content-Language'])) 
      $view->headMeta()->appendHttpEquiv('Content-Language',$options['resources']['view']['Content-Language']);
     if (isset($options['resources']['view']['headTitle'])) 
      $view->headTitle($options['resources']['view']['headTitle']);
     if (isset($options['resources']['view']['skin']))
      $view->skin = $options['resources']['view']['skin'];
     else       
      $view->skin = 'liquid';
     // Add it to the ViewRenderer
     $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper(
       'ViewRenderer'
     );
     $viewRenderer->setView($view);
     // Return it, so that it can be stored by the bootstrap
     return $view;
 }
}

martes, 11 de febrero de 2014

Estableciendo skins para nuestro portal

Como he comentado anteriormente la utilización de plantillas (skins) para nuestro portal agiliza enormemente el desarrollo de nuestra aplicación, dado que focalizamos nuestros esfuerzos en el ámbito de la programación o en el del diseño del portal.

Vamos a dotar a nuestro portal de soporte para plantillas (skins), para ello crearemos en la carpeta public la subcarpeta skins. Dentro de skins la carpeta con nuestra plantilla, en nuestro caso la llamaremos liquid.  Dentro de la misma las subcarpetas css e images.
El árbol de directorios queda:
/ public
  / skins
    / liquid
      / css
      / images

Es momento de mover el fichero antes creado public/css/layout.css a public/skins/liquid/css/layout.css, como todas las imágenes que pudiéramos tener de public/images a public/skins/liquid/images.
Crearemos un fichero xml en la raíz de nuestro skin con todos las hojas de estilo necesarias. Así pues creamos el fichero public/skins/liquid/skin.xml.

Contenido de public/skins/liquid/skin.xml:
<?xml version="1.0" encoding="ISO-8859-1"?>
<skin>
  <stylesheets>
    <stylesheet>layout.css</stylesheet>
    <stylesheet>text.css</stylesheet>
  </stylesheets>
</skin>

Crearemos una hoja de estilo para los textos, al igual que layout.css la ubicaremos en public/skins/liquid/css y tendrá como nombre text.css.

@CHARSET "ISO-8859-1";
body {
  font-family:Helvetica, Arial, sans-serif;
}

Nota: Ahora la referencia de nuestras imágenes debe ser relativa al path del skin, por lo tanto no olvidar renombrar la única imagen que tenemos en nuestro css:
Actualizar el fichero public/skins/liquid/css/layout.css:

#content-container {
 float: left;
 width: 100%;
 background: #FFF url(/skins/liquid/images/layout-two-liquid-background.gif) repeat-y 68% 0;
}

En Zend Framework surge la necesidad de la reutilización del código, para ayudarnos Zend Framework pone a nuestra disposición las "View Helpers", que son clases que extienden de Zend_View_Helper_Abstract y nos permiten invocar a los métodos de estas clases desde diferentes lugares de la aplicación.
Las "View Helper" van ubicadas en el directorio application/views/helpers, allí crearemos nuestra "View Helper" llamada LoadSkin.php.

Contenido de application/views/helpers/LoadSkin.php:
<?php
/**
 * Este "helper" cargará las hojas de estilos de nuestro skin
 *
 */
class Zend_View_Helper_LoadSkin extends Zend_View_Helper_Abstract
{
 public function loadSkin ($skin)
 {
  // Leemos del fichero de configuración xml las hojas he iteramos entre ellas
  $skinData = new Zend_Config_Xml('./skins/' . $skin . '/skin.xml');  
  $stylesheets = $skinData->stylesheets->stylesheet;
  // La documentación de Zend_Config_Xml indica que los datos leídos se retornarán
  // como strings, en el caso de haber un solo elemento retornaría un string. 
  // http://framework.zend.com/manual/1.12/en/zend.config.adapters.xml.html
  if($stylesheets instanceof Zend_Config)
   $stylesheetsArray = $stylesheets->toArray();
  else
   $stylesheetsArray = array($stylesheets);
  // Iteramos entre los diferentes elementos
  if (is_array($stylesheetsArray)) {
   foreach ($stylesheetsArray as $stylesheet) {
    $this->view->headLink()->appendStylesheet('/skins/' . $skin .
      '/css/' . $stylesheet);
   }
  }
 }
}

Es necesario pasarle al "View Helper" una plantilla (skin) válida, podemos usar muchos métodos pero de momento una forma rápida de inicializar el sistema de plantillas es establecer el entorno desde la clase Bootstrap, para ello añadiremos un método nuevo llamado _initview:
Contenido del fichero application/Bootstrap.php:
<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
 protected function _initView()
 {
  // Initialize view
  $view = new Zend_View();
  $view->doctype('XHTML1_STRICT');
  $view->headTitle('Centro de Documentación');
  $view->skin = 'liquid';
  // Add it to the ViewRenderer
  $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper(
    'ViewRenderer'
  );
  $viewRenderer->setView($view);
  // Return it, so that it can be stored by the bootstrap
  return $view;
 }
}

Ahora que ya hemos inicializado las vistas con nuestra plantilla, solo nos falta invocarla en el layout: Meta del head del fichero application/layouts/scripts/layout.phtml:

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<?php
$this->loadSkin($this->skin);
echo $this->headMeta();
echo $this->headTitle ();
echo $this->headScript (); 
echo $this->headLink ();
?>
</head>

Podemos ver el resultado navegando en http://localhost:


Realmente no veremos muchos cambios, si acaso la tipografía ya que hemos incluido un estilo específico para la misma.
Nota: Como detalle observaremos que el título de la página no se muestra. Esto es debido a la codificación UTF-8, si en lugar de  $view->headTitle('Centro de Documentación'); fuera  $view->headTitle('Centro de Documentacion'); el título sí se mostraría. Más adelante veremos como solucionar este problema.