El módulo Features de Drupal es uno de los módulos más útiles cuando queremos manejar en código configuraciones como variables de entorno, vocabularios de taxonomías o servicios web. Uno de los usos más extensos que se da de este módulo es el manejo de tipos de contenido. Gracias este módulo, podemos exportar nuestros tipos de contenido personalizados a un módulo featurizado en código, que podemos instalar en cualquier instancia de Drupal para replicarlos fácilmente.

Al cambiar el tipo de contenido que tenemos featurizado, por ejemplo, al ser añadido un campo, simplemente podemos recrear el módulo, o feature, y subir los cambios generados en nuestro código como un nuevo commit.

Por ejemplo, digamos que tenemos un tipo de contenido llamado Producto. Si queremos featurizar este tipo de contenido, podemos hacerlo desde la interfaz gráfica en Structure->Features->Create feature, seleccionar el tipo de contenido y descargar el código generado. Este código lo podemos extraer en /sites/all/modules y habilitarlo como un módulo cualquiera en otra instalación de Drupal, y nuestro tipo de contenido Producto estará disponible para su uso.

Drush

Drush (Drupal Shell) es la mejor manera de interactuar con Drupal cuando tenemos a disposición la línea de comando del servidor. Cuando se tiene un ambiente de despliegue, por ejemplo, se suele desplegar y configurar Drupal haciendo uso de Drush. Para exportar un tipo de contenido, teniendo por sentado que el módulo Features ya está instalado, corremos el siguiente comando:

drush fe nombre_del_modulo node:producto

Esto va a generar y poner el código en el directorio adecuado, para que lo podamos añadir a nuestro sistema de versionamiento.

Añadiendo campos

Cuando el tipo de contenido ha evolucionado, y hay nuevos campos que queremos incluir en nuestro módulo featurizado, simplemente corremos:

drush fu nombre_del_modulo

Y tendremos el nuevo código listo en nuestro módulo. Para aplicar los cambios de nuestro código en nuestra otra instancia de Drupal (que puede ser, por ejemplo, la instancia de pruebas o la instancia de producción), añadimos el comando:

drush features-revert nombre_del_modulo -y --force

al script que está realizando el despliegue. Hasta ahora todo tiene sentido, excepto por una cosa: ¿por qué necesitamos la bandera --force? Ya lo veremos más adelante.

Eliminando campos

Ahora viene lo bueno. Por la manera en que el módulo Features, y los módulos featurizados funcionan, al eliminar un campo de nuestra instancia local y re-generar el módulo, y luego aplicar los cambios en una instancia de Drupal que ya tenga la versión anterior instalada, el campo eliminado no se elimina de nuestra segunda instancia.

En pocas palabras, Features usa una lista de todos los componentes que están exportados en un dado módulo featurizado. Si un componente no está en dicha lista, simplemente lo ignora, y no lo toca. Ahora, los campos de un tipo de contenido son a su vez un tipo de componente, de modo que si eliminamos ese campo de la lista de componentes que están exportados en nuestro módulo, Features no va a hacer nada con los componentes ya existentes, por más que ya no estén en nuestra lista. Simplemente los va a ignorar.

Para eliminar efectivamente un campo de un módulo featurizado, esto es lo que yo hago:

Eliminar los campos por código

Primero, implemento en el archivo .module del módulo featurizado hook_post_features_revert, así como una función de ayuda para mantener las cosas DRY:

<?php

/**
 * Implements hook_post_features_revert()
 */
function nombre_del_modulo_post_features_revert($component) {
  $fields = array(
    'field_campo_a_eliminar'
  );
  
  // Let's delete those fields!
  foreach ($fields as $field) {
    _nombre_del_modulo_remove_field_instance('node', 'producto', $field);
  }
}

/**
 * This function deletes an instance of a field if it exists.
 *
 * @param string $entity_type The type of the entity, for example 'node'.
 * @param string $bundle_name The machine name of the bundle.
 * @param string $field_name The machine name of the field.
 */
function _nombre_del_modulo_remove_field_instance($entity_type, $bundle_name, $field_name) {
  // First, check that what we want to delete actually exists.
  if ($instance = field_info_instance($entity_type, $field_name, $bundle_name)) {
    // Invoke this hook to do all migration-related work.
    module_invoke_all('pre_delete_featurized_field', $instance);
    // Perform actual deletion.
    field_delete_instance($instance);
  }
}

Cuando deseo eliminar un campo, simplemente lo añado al arreglo $fields.

En la función _nombre_del_modulo_remove_field_instance estoy invocando un hook personalizado, pre_delete_featurized_field. Esto con el objeto de, en la implementación de dicho hook, realizar todos los movimientos de información y otras operaciones que tengamos que hacer antes de eliminar el campo definitivamente. Recordemos que al eliminar una instancia de un campo, todos los datos guardados allí también se eliminan. Muchas veces un requerimiento puede ser cambiar el tipo de un campo, de modo que para el usuario final sea transparente. Esto por lo general involucra crear un nuevo campo, migrar los datos, y eliminar el anterior campo. Se podría hacer tocando directamente la base de datos, pero a mi me gusta mantenerme alejado de la base de datos cuando trabajo con Drupal.

A continuación, en mi máquina local, revierto el módulo featurizado: drush features-revert nombre_del_modulo -y --force. Esta es la primera vez que vemos el sentido en usar --force: Si no lo usamos, Features nos dirá que el módulo ya se encuentra aplicado en su totalidad, y que no es necesario revertirlo. Pero lo que queremos es forzar la ejecución de hook_post_features_revert, de modo que le decimos que lo revierta de todas maneras. Ahora, si vamos a la interfaz de configuación de nuestro tipo de contenido, veremos que el campo ya no existe.

Regenerar el módulo featurizado

Ahora nos queda correr el comando para actualizar nuestro módulo:

drush fu nombre_del_modulo

Esto eliminará de la lista de componentes (y del código del módulo) los campos que ya no existen. Con esto, podemos correr drush features-revert nombre_del_modulo -y --force en nuestra otra instancia de Drupal, y no tendremos que preocuparnos por que los campos que eliminamos no queden realmente eliminados. Features va a omitir los campos que no aparecen en la lista, pero la implementación de hook_post_features_revert se va a encargar de eliminar los campos que no deseamos.

Como nota extra, acá nos percatamos por segunda vez de la necesidad de usar la bandera --force: si bien la lista de componentes cambió, es posible que los componentes mismos sigan en el mismo estado, por lo que Features nos dirá que el módulo ya se encuentra aplicado. Es por eso que necesitamos forzar la ejecución de la reversión.

En la entrada del día de hoy vamos a ver cómo podemos hacer plantear la arquitectura de una aplicación MVC en Zend Framework 2 usando una capa de servicios entre en los controladores y el modelo. Realmente, muchas aplicaciones MVC hacen uso de este patrón en la vida real, no necesariamente apliaciones hechas con ZF2 o ni siquiera con PHP. Pero en esta entrada me voy a enfocar en demostrar cómo se puede legar a hacer con este framework en específico.

Vamos a continuar la aplicación de la entrada anterior que se refería a cómo implementar una Lista de Control de Acceso.

Modelando la entidad Ticket

En la entrada anterior alcanzamos a modelar dos de nuestras entidades, usuario y rol. También alcanzamos a crear un esqueleto de lo que podía ser nuestro controlador de Tickets, TicketConroller. Además, restringimos el acceso de los usuarios a ciertas acciones del controlador dependiendo en si tenían el permiso de realizar acciones en el recurso Ticket.

Ahora, vamos a modelar nuestra entidad Ticket. Para los tiquetes de soporte de nuestro sistema, queremos los siguientes campos:

  1. id: el identificador únido del tiquete.
  2. title: título del tiquete.
  3. description: descripción.
  4. creator: el usuario que creó el tiquete.
  5. status: el estado de resolución del tiquete.
  6. assignee: a qué agente está asignado el tiquete en este momento.
  7. created: la fecha y hora en que el tiquete fue creado.

Con esto en mente, vamos a modelar nuestra entidad, usando anotaciones de Doctrine:

<?php
/**
 * File: Ticket.php.
 */

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Class Ticket
 * @package Application\Entity
 *
 * @ORM\Table(name="ticket")
 *
 * @ORM\Entity
 */
class Ticket
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=200, nullable=false)
     */
    private $title;

    /**
     * @var string
     *
     * @ORM\Column(name="description", type="text", nullable=true)
     */
    private $description;

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User")
     * @ORM\JoinColumn(name="creator_id", referencedColumnName="id", nullable=false)
     */
    private $creator;

    /**
     * @var string
     *
     * @ORM\Column(name="status", type="string", length=30, nullable=false)
     */
    private $status;

    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User")
     * @ORM\JoinColumn(name="assignee_id", referencedColumnName="id", nullable=true)
     */
    private $assignee;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="created", type="datetime", nullable=false)
     */
    private $created;

    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param integer $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param string $title
     */
    public function setTitle($title)
    {
        $this->title = $title;
    }

    /**
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * @param integer $description
     */
    public function setDescription($description)
    {
        $this->description = $description;
    }

    /**
     * @return User
     */
    public function getCreator()
    {
        return $this->creator;
    }

    /**
     * @param User $creator
     */
    public function setCreator($creator)
    {
        $this->creator = $creator;
    }

    /**
     * @return string
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * @param string $status
     */
    public function setStatus($status)
    {
        $this->status = $status;
    }

    /**
     * @return User
     */
    public function getAssignee()
    {
        return $this->assignee;
    }

    /**
     * @param User $assignee
     */
    public function setAssignee($assignee)
    {
        $this->assignee = $assignee;
    }

    /**
     * @return \DateTime
     */
    public function getCreated()
    {
        return $this->created;
    }

    /**
     * @param \DateTime $created
     */
    public function setCreated($created)
    {
        $this->created = $created;
    }
}

El contenido arriba expuesto lo vamos a copiar en el archivo module/Application/src/Application/Entity/Ticket.php. Ahora, para regenerar la base de datos sin perder nuestros usuarios y roles existentes, corremos desde una terminal:

$ ./vendor/bin/doctrine-module orm:schema-tool:update --force
Updating database schema...
Database schema updated successfully! "3" queries were executed

En este momento veremos que tenemos una nueva tabla llamada ticket en nuestra base de datos.

Interactuando con la Base de Datos

Para darnos una idea de cómo interactuar con la base de datos desde nuestra aplicación vamos a añadir a nuestro controlador la acción listAction:

<?php
// ...
class TicketController extends AbstractActionController
{
    // ...
    /**
     * Only users that can read tickets can list them.
     */
    public function listAction()
    {
        if (!$this->isAllowed('Ticket', 'read')) {
            throw new UnAuthorizedException();
        }
        /** @var EntityManager $em */
        $em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
        /** @var Ticket[] $tickets */
        $tickets = $em->getRepository('Application\Entity\Ticket')->findAll();

        foreach ($tickets as $ticket) {
            echo $ticket->getTitle() . ', by ' . $ticket->getCreator()->getEmail() . '<br/>';
        }
        return false;
    }
}

Podemos ver en $em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager'), que estamos usando una funcionalidad de Zend Framework llamada ServiceManager. El ServiceManager es un componente disponible en los controladores que se encarga de instanciar o traer servicios, de modo que podamos usarlos dentro de nuestro código. Pero primero, ¿qué es un servicio?

Un servicio es, en pocas palabras, un conjunto de funcionalidades que pueden ser reusadas. En este contexto, un servicio es una clase que contiene funcionalidad relacionada con otros componentes de la aplicación. Para poder hacer uso de un servicio, debemos indicarle al ServiceManager dónde está nuestro servicio, así como una clave que vamos a usar cuando queramos traerlo. ¿Dónde está esto en el caso del servicio Doctrine\ORM\EntityManager? La respuesta está en el código fuente del módulo doctrine-orm-module, que instalamos en la entrada pasada para manejar la base de datos con Doctrine.

Servicio de tiquetes

Vamos a crear nuestro propio servicio, que vamos a usar para realizar todas la operaciones con los tiquetes de nuestro sistema. Inicialmente, crearemos una clase llamada TicketService bajo module/Application/src/Application/Service/:

<?php
/**
 * File: TicketService.php.
 */

namespace Application\Service;

use Application\Entity\Ticket;

/**
 * Class TicketService
 * @package Application\Service
 */
class TicketService
{
    /**
     * @param Ticket $ticket
     *
     * @return Ticket
     */
    public function saveTicket(Ticket $ticket)
    {

    }

    /**
     * @return Ticket[]
     */
    public function getAllTickets()
    {

    }

    /**
     * @param integer $id
     *
     * @return Ticket
     */
    public function getTicketById($id)
    {

    }
}

Esta clase será nuestro servicio de tiquetes. Aún no hemos implementado ningún método en ella, pero por ahora veamos lo que hará cada método:

  • saveTicket(Ticket $ticket): este método se encargará se guardar a base de datos un Ticket, retornándolo inmediatamente.
  • getAllTickets(): se encarga de traer todos los tiquetes de la base de datos y retornarlos en un arreglo.
  • getTicketById($id): se encarga de buscar un tiquete por su id, y retornarlo.

La manera en que vamos a implementar cada uno de esos métodos es, como no, usando el EntityManager de doctrine. Sin embargo, esto posa un problema, puesto que no tenemos cómo acceder al ServiceLocator desde nuestro servicio. Vamos, por ahora, a inventarnos una manera muy sencilla de tener acceso al EntityManager en nuestro servicio: por medio del constructor.

<?php
// ...
use Doctrine\ORM\EntityManager; // añadimos este use

// ...
class TicketService
{
    /**
     * @var EntityManager
     */
    private $entityManager;

    /**
     * @param EntityManager $entityManager
     */
    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }
    
    // ...
}

Ahora ya tenemos acceso al EntityManager en nuestro servicio. Lo siguiente que haremos es implementar la funcion de listar tiquetes, similarmente a como lo implementamos en el controlador:

<?php
    // ...
    public function getAllTickets()
    {
        return $this->entityManager->getRepository('Application\Entity\Ticket')->findAll();
    }
    // ...

Y llamar al servicio desde TicketController:

<?php
// ...
use Application\Service\TicketService; // añadimos este use

    //...
    /**
     * Only users that can read tickets can list them.
     */
    public function listAction()
    {
        if (!$this->isAllowed('Ticket', 'read')) {
            throw new UnAuthorizedException();
        }
        /** @var EntityManager $em */
        $em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');

        $service = new TicketService($em);
        $tickets = $service->getAllTickets();

        foreach ($tickets as $ticket) {
            echo $ticket->getTitle() . ', by ' . $ticket->getCreator()->getEmail() . '<br/>';
        }
        return false;
    }
    // ...

Por ahora sólo vamos a implementar ese método de nuestro servicio.

Registrando el servicio e inyección de dependencias

Ahora vamos a registrar nuestro servicio en el ServiceManager de Zend Framework, de modo que podamos hacer uso de él simplemente llamando al SeviceLocator. Para esto, vamos a modificar el archivo module\Application\config\module.config.php de nuestro módulo, y añadimos al arreglo con clave service_manager['factories'] la siguiente entrada:

'Application\Service\Ticket' => 'Application\Factory\TicketServiceFactory'

Esto le va a decir a Zend Framework que existe una clase factoría que retorna nuestro servicio. La razón por la que necesitamos una factoría en este caso, es porque nuestro servicio tiene una dependencia, el EntityManager, que estamos inyectando a en el constructor. A esto se refiere la inyección de dependencias, a pasar las dependencias de una clase por medio del constructor o de métodos setters, en vez de instanciarlas en la misma clase.

Nuestra factoría entonces va a ser una muy sencilla. La creamos en module/Application/src/Application/Factory/TicketServiceFactory.php:

<?php
/**
 * File: TicketServiceFactory.php.
 */

namespace Application\Factory;
use Application\Service\TicketService;
use Doctrine\ORM\EntityManager;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

/**
 * Class TicketServiceFactory
 * @package Application\Factory
 */
class TicketServiceFactory implements FactoryInterface
{
    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return TicketService
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var EntityManager $entityManager */
        $entityManager = $serviceLocator->get('Doctrine\ORM\EntityManager');
        return new TicketService($entityManager);
    }
}

Con esta factoría registrada en nuestra aplicación, podemos cambiar la acción de nuestro controlador para que use nuestro servicio recién registrado:

<?php
// ...

    //...
    /**
     * Only users that can read tickets can list them.
     */
    public function listAction()
    {
        if (!$this->isAllowed('Ticket', 'read')) {
            throw new UnAuthorizedException();
        }

        /** @var TicketService $service */
        $service = $this->getServiceLocator()->get('Application\Service\Ticket');
        $tickets = $service->getAllTickets();

        foreach ($tickets as $ticket) {
            echo $ticket->getTitle() . ', by ' . $ticket->getCreator()->getEmail() . '<br/>';
        }
        return false;
    }
    // ...

Si vamos a /application/ticket/list, veremos que nuestro sitio sigue funcionando como siempre. Lo único que ha cambiado es que ahora, si por alguna razón nuestro TicketService tiene nuevas dependencias, podemos añadirlas fácilmente en la factoría, y tener el servicio funcional en todas las ocasiones en que lo llamemos usando el ServiceLocator.

Hay que recordar que en nuestro ejemplo, cuando hacemos el llamado a $this->getServiceLocator()->get('Application\Service\Ticket');, 'Application\Service\Ticket' no es más que el nombre que le dimos a nuestro servicio cuando lo registramos en el Service Manager. En Zend Framework 2, la convención parece ser que los nombres de los servicios son parecidos a los nombres canónicos de las clases, sin la palabra Service al final. Pero bien hubiéramos podido registrar nuestro servicio con cualquier otro nombre, y llamarlo así desde el ServiceLocator.

El día de hoy vamos a construír la primera parte de un sistema de tickets en Zend Framework 2, específicamente la parte de usuarios y permisos, usando dos módulos muy populares para ello:

  • Zfc-user
  • BjyAuthorize

Además, lo haremos usando Doctrine para la comunicación con la base de datos.

La aplicación

La aplicación que vamos a desarrollar es la forma más básica de un sistema de tickets de soporte. Tendremos dos roles, usuario y agente. Los usuarios pueden crear tickets. Los agentes pueden leer y cambiar de estado a los tickets, pero no pueden modificar nada más. Las personas no se pueden registrar en el sitio.

Eso es todo lo que hará esta pequeña aplicación. En esta entrada solo nos vamos a concentrar en la parte de roles y permisos, y en siguientes entradas iremos ahondando en otros temas como eventos, servicios o formularios.

Módulos necesarios

Para empezar, vamos a crear nuestra aplicación con base en la aplicación esqueleto de Zend Framework 2. Para obtenerla, vamos a la página oficial y seguimos las instrucciones: https://github.com/zendframework/ZendSkeletonApplication.

Una vez tengamos la aplicación instalada, es hora de añadir los módulos que vamos a usar. Estos se añaden a el archovp composer.json, de modo que debe quedar más o menos así:

{
    "name": "seperez/application",
    "description": "Implementing a simple application with users ans permissions",
    "license": "BSD-3-Clause",
    "keywords": [
        "zf2",
        "framework",
        "users",
        "permissions"
    ],
    "homepage": "http://example.com",
    "require": {
        "php": ">=5.5",
        "zendframework/zendframework": "~2.5",
        "doctrine/doctrine-orm-module": "0.9.2",
        "doctrine/doctrine-module": "0.10.0",
        "zf-commons/zfc-user-doctrine-orm": "1.0.1",
        "bjyoungblood/bjy-authorize": "1.4.0"
    }
}

Guardamos el archivo, y corremos php composer.phar update, para actualizar e instalar los módulos necesarios.

Activar los módulos en nuestra aplicación

Ahora que hemos añadido y descargado los módulos necesarios, vamos a tomar un vistazo a lo que hemos instalado:

  • doctrine/doctrine-module y doctrine/doctrine-orm-module: estos son los módulos que proveen la integración con el ORM Doctrine a nuestra aplicación en Zend Framework 2.
  • zf-commons/zfc-user-doctrine-orm: este módulo provee la funcionalidad de zfcuserintegrada con Doctrine. Al instalar este módulo, el sistema de resolución de dependencias de Composer instalará también los módulos zfc-basey zfc-user. Más acerca de zfcuser más adelante.
  • bjyoungblood/bjy-authorize: es el módulo que usaremos para proveer la funcionalidad de permisos. Este módulo es básicamente una manera más fácil de usar la ACL (Access Control List) proveída por Zend Framework 2 por defecto. Ya veremos cómo se configura.

Para activar dichos módulos en nuestra aplicación, vamos al archivo config/application.config.php y añadimos los módulos al arreglo modules:

<?php
/*
 * File: application.config.php
 */
return [
    //...
    'modules' => [
        'DoctrineModule',
        'DoctrineORMModule',
        'ZfcBase',
        'ZfcUser',
        'ZfcUserDoctrineORM',
        'BjyAuthorize',
        'Application',
    ],
    //...
];

Esto activará los módulos recién descargados en nuestra aplicación. El siguiente paso es configurarlos.

Configuración de Doctrine 2

Con nuestros módulos instalados, vamos a configurar primero Doctrine, el ORM, de manera que nos podamos comunicar con la base de datos.

Para configurar Doctrine vamos a modificar (o agregar) dos archivos: config/autoload/doctrine.global.phpy config/autoload/doctrine.local.php. En el primero vamos a configurar el nombre de la base de datos, el host y el puerto, mientras que en el segundo vamos a configurar las credenciales de acceso. La idea de tener los dos archivos por separado es que cuando subamos nuestro trabajo a un repositorio como GitHub o Sourceforge, sólo el archivo global se suba.

<?php
/**
 * File: doctrine.global.php.
 */

return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
                'params' => [
                    'host' => 'localhost',
                    'port' => '3306'
                ]
            ]
        ]
    ]
];
<?php
/**
 * File: doctrine.local.php.
 */

return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'params' => [
                    'user' => 'application',
                    'password' => 'password',
                    'dbname' => 'application'
                ]
            ]
        ]
    ]
];

En este caso estamos asumiendo que vamos a usar una base de datos MySql en localhost, cuyas credenciales son las dadas (application/password).

Ahora vamos a decirle a Doctrine en dónde se encuentran las entidades para nuestra aplicación. En el archivo de configuración de nuestro módulo (por defecto module/Application/config/module.config.php, aunque yo recomiendo crear un módulo aparte), añadimos al arreglo que es retornado:

<?php
/*
 * File: module.config.php
 */
return [
	//...
    'doctrine' => [
        'driver' => [
            'application_entities' => [
                'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
                'cache' => 'array',
                'paths' => (__DIR__ . '/../src/Application/Entity')
            ],
            'orm_default' => [
                'drivers' => [
                    'Application\Entity' => 'application_entities'
                ],
            ],
        ],
    ],
    //...
];

Esto le dirá a Doctrine que las entidades de nuestra aplicación se encuentran en el directorio module/Application/src/Application/Entity, y que tendrán el namespace Application\Entity.

Generando entidades

Ahora vamos a generar las entidades que vamos a necesitar por ahora. Para los usuarios y roles, afortunadamente el módulo BjyAuthorize viene con entidades de ejemplo que podemos usar directamente para nuestro sistema. En otra entrada veremos cómo extender estas entidades, así como las que tienen que ver con el resto de nuestro sistema.

Por ahora, vamos a copiar los archivos User.php.dist y Role.php.dist que se encuentran en el directorio vendor/bjyoungblood/bjy-authorize/data, de modo que queden en module/Application/src/Application/Entity, y con la extensión php.

Al copiarlos, recordemos que tenemos que cambiar el namespace a Application\Entity. También tenemos que cambiar las referencias en la propiedad roles de la clase User y en la propiedad parent de la clase Role.

Lo siguiente que tenemos que hacer es decirle a ZfcUser que tome nuestra nueva entidad User como la entidad de usuario del sistema. Para ello, vamos a crear un archivo llamado zfcuser.global.php en config/autoload/, y vamos a retornar el siguiente arreglo:

<?php
/**
 * File: zfcuser.global.php.
 */

return [
    'zfcuser' => [
        'user_entity_class' => 'Application\Entity\User',
        'enable_default_entities' => false
    ]
];

Esto le dirá al módulo ZfcUser que no vamos a usar la clase que viene por defecto con el módulo, sino que vamos a usar nuestra propia clase (que acabamos de copiar de las proveídas por BjyAuthorize). Ahora, validamos las entidades con el siguiente comando:

$ ./vendor/bin/doctrine-module orm:validate-schema
[Mapping]  OK - The mapping files are correct.
[Database] FAIL - The database schema is not in sync with the current mapping file.

Y eso es lo que nos debería salir. Para generar la base de datos:

$ ./vendor/bin/doctrine-module orm:schema-tool:create
ATTENTION: This operation should not be executed in a production environment.

Creating database schema...
Database schema created successfully!

En este momento, tenemos las tablas en la base de datos creadas. Podemos ver esto corriendo un simple SHOW TABLES; en MySQL:

mysql> SHOW TABLES;
+-----------------------+
| Tables_in_application |
+-----------------------+
| role                  |
| user_role_linker      |
| users                 |
+-----------------------+
3 rows in set (0.00 sec)

Si vamos a nuestra applicación en el navegador, y accedemos a la ruta user/register, podemos ver el formulario de registro de usuarios. Creemos un par de usuarios para hacer pruebas.

El controlador

Vamos a definir nuestros controladores, para luego configurar el módulo de autorización basados en ellos. La aplicación esqueleto de Zend Framework 2 viene con un controlador por defecto llamado IndexController, que sirve la página de inicio. Dentro del mismo directorio module/Application/src/Application/Controller creamos el controlador TicketController:

<?php
/**
 * File: TicketController.php
 */

namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;

/**
 * Class TicketController
 * @package Application\Controller
 */
class TicketController extends AbstractActionController
{
    /**
     * Only users with the role "user" can create tickets.
     */
    public function createAction()
    {
        echo "Creating a ticket.";
        return false;
    }

    /**
     * Only users with the role "agent" can change a ticket status.
     */
    public function changeStatusAction()
    {
        echo "Changing a ticket's status.";
        return false;
    }

    /**
     * Only users with the role "agent" can read tickets.
     */
    public function readAction()
    {
        echo "Reading a ticket.";
        return false;
    }
}

Este contolador tiene las acciones más básicas que usaremos en la entrada de hoy: crear, leer y cambiar el estado de los tickets. Si vamos a las rutas application/ticket/read, application/ticket/create y application/ticket/change-status podremos ver los textos que se muestran.

Configurar BjyAuthorize

Es hora de configurar el módulo que nos va a hacer la vida fácil cuando estemos trabajando con roles y permisos. En esta sección vamos a configurar los roles, los permisos, los recursos y las reglas.

Para esto, creamos el archivo de configuración bjyauthorize.global.php en config/autoload/. En este archivo vamos a retornar el siguente arreglo, que pasaré a describir más adelante:

<?php
/**
 * File: bjyauthorize.global.php
 */

return [
    'bjyauthorize' => [
        'default_role' => 'guest',
        'identity_provider' => 'BjyAuthorize\Provider\Identity\AuthenticationIdentityProvider',

        'role_providers' => [
            'BjyAuthorize\Provider\Role\ObjectRepositoryProvider' => [
                'object_manager' => 'doctrine.entitymanager.orm_default',
                'role_entity_class' => 'Application\Entity\Role',
            ],
            'BjyAuthorize\Provider\Role\Config' => [
                'guest' => []
            ]
        ],

        'resource_providers' => [
            'BjyAuthorize\Provider\Resource\Config' => [
                'Ticket' => [],
                'Home' => []
            ]
        ],

        'rule_providers' => [
            'BjyAuthorize\Provider\Rule\Config' => [
                'allow' => [
                    ['authenticated', 'Home', 'see'],
                    ['user', 'Ticket', 'create'],
                    ['agent', 'Ticket', ['change_status', 'read']]
                ]
            ]
        ],
    ]
];

En este punto realmente vale la pena descargar y activar el módulo Zend Developer Tools, de modo que podamos visualizar el rol del usuario actual. Si en este momento vamos a la aplicacion sin loguearnos, veremos que tenemos el rol guest. Esto, ya que le hemos dicho a BjyAuthorize que el rol por defecto debe ser guest, acá: 'default_role' => 'guest'. Para esto, también le decimos que el rol guest en realidad sí existe. Pero la verdad es que no queremos poner ese rol en la base de datos, puesto que es muy poco probable que cambie; de modo que lo configuramos explícitamente en 'BjyAuthorize\Provider\Role\Config' => ['guest' => []].

A continuación, vamos a generar los roles en MySQL. Haremos esto directamente en la base de datos:

INSERT INTO application.role (id, parent_id, roleId) VALUES (1, null, 'authenticated');
INSERT INTO application.role (id, parent_id, roleId) VALUES (2, 1, 'user');
INSERT INTO application.role (id, parent_id, roleId) VALUES (3, 1, 'agent');

El rol con id 1 es el rol authenticated, que es el rol padre que los usuarios van a tener. Con este rol, un usuario va a poder ver al recurso Home, que es la pantalla de inicio. Los roles con ids 2 y 3 son user y agent, o usuario y agente. Es con estos roles que jugamos a crear reglas para definir qué puede hacer cada rol sobre el recurso Ticket. Como podemos ver, estamos diciendo que el rol user puede realizar la acción create en el recurso Ticket, mientras que el rol agent puede realizar las acciones read y change_status. Estas reglas las definimos en el arreglo rule_providers.

El arreglo resource_providers es para definir los recursos. Como podemos ver, cada arreglo *_providers tiene como valor a su vez otro arreglo, con un elemento que tiene como llave una cadena que parece como un nombre de clase. Por ejemplo, BjyAuthorize\Provider\Role\ObjectRepositoryProvider para el arreglo role_providers. Estos no son más que proveedores de roles, recursos y reglas que vienen por defecto en BjyAuthorize. Afortunadamente, el autor del módulo también pone a nuestra disposición una interfaz para cada uno de ellos, de modo que nosotros podemos implementar nuestros propios proveedores si quisiéramos.

Usando autorización

Ahora vamos a poner esto a funcionar. Para ejemplificar, vamos a modificar las acciones en nuestro TicketController de este modo:

<?php
/**
 * File: TicketController.php
 */

namespace Application\Controller;
use BjyAuthorize\Exception\UnAuthorizedException;
use Zend\Mvc\Controller\AbstractActionController;

/**
 * Class TicketController
 * @package Application\Controller
 */
class TicketController extends AbstractActionController
{
    /**
     * Only users with the role "user" can create tickets.
     */
    public function createAction()
    {
        if (!$this->isAllowed('Ticket', 'create')) {
            throw new UnAuthorizedException();
        }
        echo "Creating a ticket.";
        return false;
    }

    /**
     * Only users with the role "agent" can change a ticket status.
     */
    public function changeStatusAction()
    {
        if (!$this->isAllowed('Ticket', 'change_status')) {
            throw new UnAuthorizedException();
        }
        echo "Changing a ticket's status.";
        return false;
    }

    /**
     * Only users with the role "agent" can read tickets.
     */
    public function readAction()
    {
        if (!$this->isAllowed('Ticket', 'read')) {
            throw new UnAuthorizedException();
        }
        echo "Reading a ticket.";
        return false;
    }
}

Y para nuestro IndexController:

<?php
/**
 * File: IndexController.php
 */

namespace Application\Controller;

use BjyAuthorize\Exception\UnAuthorizedException;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        if (!$this->isAllowed('Home', 'see')) {
            throw new UnAuthorizedException();
        }
        return new ViewModel();
    }
}

Ahora, si vamos al home sin estar autenticados, veremos un error 403. En estos momentos podemos crear entradas en la tabla user_role_linker para asignar roles a usuarios específicos, y ver como BjyAuthorize protege nuestras acciones de roles no autorizados.

Es posible consumir servicios web SOAP usando C++, usando una librería llamada gSOAP. Esta librería funciona también para crear servicios usando C++ que pueden ser desplegados con CGI, pero ese es tema de otra entrada. En esta entrada, me enfocaré en qué hacer para consumir un servicio web desde una aplicación C++, gracias a la librería mencionada, gSOAP.

GSOAP es una librería de código abierto para interactuar con servicios web SOAP desde programas en C y C++. El programa resultante de esta entrada consumirá el servicio de calculadora que se encuentra en http://ws1.parasoft.com/glue/calculator. Este servicio se encuentra listado en algunos directorios públicos, y tiene las operaciones add, divide, multiply y substract, para cada una de las operaciones aritméticas básicas.

Obtener gSOAP

Si estamos trabajando en un ambiente Linux, es muy posible que el paquete esté incluido en los paquetes oficiales de la distribución. En mi caso, estoy trabajando en Ubuntu 14.04, y tuve que instalar los paquetes gsoap, libgsoap4 y libgsoap-dev. GSOAP contiene dos ejecutables que usaremos en el proceso, wsdl2h y soapcpp2. El primero genera las cabeceras de nuestro programa a partir de un documento WSDL que le demos, mientras que el segundo genera las implementaciones y las estructuras de datos.

En caso de no tener gSOAP en el repositorio oficial de nuestra distribución, o de estar trabajando con otro sistema operativo, tendremos que ajar gSOAP.GSOAP se puede obtener desde su página web oficial, http://www.cs.fsu.edu/~engelen/soap.html.

Antes de empezar a generar el código usando la herramienta, tenemos que tener clara la ubicación de los siguientes archivos:

  • typemap.dat
  • stlvector.h

Estos se encuentran en la carpeta que descargamos de la página oficial, o en mi caso, en /usr/share/gsoap.

Generar código a partir de WSDL

Primero, ejecutamos wsdl2h sobre el WSDL del servicio que queremos consumir.

$ wsdl2h -o calculator.h -I/usr/share/gsoap/WS http://soaptest.parasoft.com/calculator.wsdl

Con la opción -o, especificamos el nombre del archivo de salida. Con la opción -I, le decimos a wsdl2h el directorio donde puede encontrar ciertos archivos que necesita, como typemap.dat. Como último parámetro pasamos la dirección del WSDL. Ahora tenemos un archivo en nuestro directorio: calculator.h.

A continuación vamos a usar la herramienta soapcpp2 para generar el código que vamos a usar para escribir nuestro cliente.

$ soapcpp2 -j -C -I/usr/share/gsoap/import/ calculator.h

Con la opción -j, generamos objetos y proxies en C++, que vamos a usar más adelante desde nuestro código. Con la opción -C, le decimos a la herramienta que sólo genere código del lado del cliente. Como dije al principio de esta entrada, crear servicios usando C++ y gSOAP es un tema para otro día. Al igual que con el comando anterior, la opción -I le dice a la herramienta en qué directorios puede encontrar archivos necesarios, como stlvector.h.

Al ejecutar este comando tendremos en el directorio de trabajo todos los archivos necesarios para empezar a escribir un pequeño programa de prueba de cliente.

Crear proyecto y escribir el cliente

A continuación vamos a nuestro IDE favorito y creamos un proyecto con los siguientes archivos:

  • ICalculator.nsmap
  • soapC.cpp
  • soapH.h
  • soapCalculatorProxy.cpp
  • soapCalculatorProxy.h
  • soapStub.h

Si estamos trabajando con la versión que bajamos desde la página oficial, también añadimos stdsoap2.cpp y stdsoap2.h.

Creamos un archivo main.cpp, y pegamos el siguiente código:

#include "soapICalculatorProxy.h"
#include "ICalculator.nsmap"

int main() {
    // define the objects
    ICalculatorProxy service;
    soap *theSoap = soap_new();
    SOAP_ENV__Header theHeader;

    _ns1__multiply theMultiplication;
    theMultiplication.x = 22.0;
    theMultiplication.y = 59.0;
    _ns1__multiplyResponse theMultiplicationResponse;

    // connect up the objects
    theSoap->header = &theHeader;
    service.soap = theSoap;

    printf("Calling Web Service...\n");
    // run the multiplication
    if (service.multiply(&theMultiplication, &theMultiplicationResponse) == SOAP_OK) {
        // if we got results, print them out
        printf("Result: %f\n", theMultiplicationResponse.Result);
    } 

    // delete the service object and free the memory
    service.destroy();

    return 0;
}

El anterior es el código de prueba en el que vamos a llamar a los proxies generados por gSOAP, para hacer uso del servicio web. Como podemos ver, creamos una variable tipo _ns1__multiply, la cual llenamos con los parámetros que queremos pasar al servicio. El llamado al proxy, cuando es exitoso, retorna SOAP_OK, y guarda el resultado en una variable tipo _ns1__multiplyResponse. La salida de el programa anterior es:

Calling Web Service...
Result: 1298.000000

Hace varios años, no recuerdo bien dónde, encontré el código para enviar mensajes a la pantalla desde un programa .NET. Con un poco de lectura de la documentación (y acá), escribí un programa en C# para hacer sólo una cosa: apagar la pantalla de mi computador portátil.

Soy consciente de que esta probalemente no es la manera más elegante de lograr eso, pero desde que lo desarrollé (cosa que no recuerdo me haya tardado más de 5 minutos), puse un acceso directo en mi barra de tareas y es de las cosas más útiles que tengo allí. Cada vez que deseo dejar el portátil prendido pero no deseo esperar a que se apague la pantalla, pulso en el ícono o la combinación de teclas y la pantalla se apaga. A continuación pongo a disposición el código a manera de curiosidad.

using System.Runtime.InteropServices;

namespace TurnOffDisplay
{
    class Program
    {
        static public int WM_SYSCOMMAND = 0x0112;
        static public int SC_MONITORPOWER = 0xF170;
        [DllImport("user32.dll")]
        private static extern int SendMessage(int hWnd, int hMsg, int wParam, int lParam);

        static void Main(string[] args)
        {
            SendMessage(-1, WM_SYSCOMMAND, SC_MONITORPOWER, 2);

        }
    }
}

Para reproducir, crear un proyecto en Visual Studio (puede ser Express) con un único archivo y pegar el código.