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.