Caution: You are browsing the legacy symfony 1.x part of this website.

Día 13: El usuario

1.4 / Propel
Symfony version
1.2
Language ORM

Ayer estuvo lleno de una gran información. Con muy pocas líneas de código PHP, el generador de administración de Symfony permitió al desarrollador crear interfaces backend en cuestión de minutos.

Hoy, vamos a descubrir cómo Symfony gestiona persistentemente datos entre las peticiones HTTP. Como se puede saber, el protocolo HTTP no tiene memoria, lo que significa que cada petición es independiente de lo anterior o de la subsiguiente. Los sitios web modernos necesitan una manera de mantener los datos entre las peticiones para mejorar la experiencia del usuario.

Una sesión de usuario puede ser identificada mediante una cookie. En Symfony, el desarrollador no tiene que manipular directamente las sesiones, sino que usa el objeto sfUser, que representa al usuario final de la aplicación.

Mensajes Flashes del Usuario

Ya hemos visto el objeto de usuario en acción con los mensajes flashes. Un flash es un efímero mensaje almacenado en la sesión de usuario que se eliminará automáticamente después de la próxima petición. Es muy útil cuando se necesita mostrar un mensaje al usuario después de un redireccionamiento. El generador de administración utiliza flashes para mostrar información al usuario cuando se guarda un puesto de trabajo, se borra o extiende su validez.

Flashes

Un flash se establece utilizando el método setFlash() de sfUser:

// apps/frontend/modules/job/actions/actions.class.php
public function executeExtend(sfWebRequest $request)
{
  $request->checkCSRFProtection();
 
  $job = $this->getRoute()->getObject();
  $this->forward404Unless($job->extend());
 
  $this->getUser()->setFlash('notice', sprintf('Your job validity has been extended until %s.', $job->getExpiresAt('m/d/Y')));
 
  $this->redirect($this->generateUrl('job_show_user', $job));
}

El primer argumento es el identificador del flash y el segundo es el mensaje a mostrar. Se puede definir cualquier flashes que quieras, pero notice y error son dos de los más comunes (que son utilizadas intensivamente por el generador de administración).

Corresponde a los desarrolladores incluír el mensaje flash en las plantillas. Para Jobeet, se muestran en el layout.php:

// apps/frontend/templates/layout.php
<?php if ($sf_user->hasFlash('notice')): ?>
  <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div>
<?php endif ?>
 
<?php if ($sf_user->hasFlash('error')): ?>
  <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div>
<?php endif ?>

En una plantilla, el usuario es accesible a través de la variable especial $sf_user.

note

Algunos objetos symfony siempre son accesibles en las plantillas, sin la necesidad de explicitamente pasarlos desde la acción: $sf_request, $sf_user, y $sf_response.

Atributos de Usuario

Lamentablemente, los casos de uso de Jobeet no tienen ningún requisito sobre el almacenamiento de algo en la sesión de usuario. Así que vamos a añadir un nuevo requisito: para facilitar la navegación de los puestos de trabajo, los últimos tres vistos por el usuario se deben mostrar en un menú con enlaces para volver a la página de esos puestos de trabajo más tarde.

Cuando un usuario accede a una página de puestos de trabajo, el objeto job mostrado necesita ser agregado en el historial del usuario y almacenado en la sesión:

// apps/frontend/modules/job/actions/actions.class.php
class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = $this->getRoute()->getObject();
 
    // fetch jobs already stored in the job history
    $jobs = $this->getUser()->getAttribute('job_history', array());
 
    // add the current job at the beginning of the array
    array_unshift($jobs, $this->job->getId());
 
    // store the new job history back into the session
    $this->getUser()->setAttribute('job_history', $jobs);
  }
 
  // ...
}

note

Podríamos haber almacenado los objetos JobeetJob directamente en la sesión. Esto está totalmente desaconsejada ya que las variables de sesión son serializadas (almacenadas) entre las peticiones. Y cuando la sesión se ha cargado, los objetos JobeetJob son de-serializados y pueden quedar "estancos" aun si han sido modificados o borrados en el ínterin.

Los métodos getAttribute(), setAttribute()

Dado un identificador, el método sfUser::getAttribute() obtiene los valores de la sesión de usuario. Por el contrario, el método setAttribute() almacena cualquier variable PHP en la sesión, para un determinado identificador.

El método getAttribute() también tiene un valor predeterminado opcional a devolver si el identificador no está todavía definido.

note

El valor por defecto tomado por el método getAttribute() es un acceso directo para:

if (!$value = $this->getAttribute('job_history'))
{
  $value = array();
}

La Clase myUser

Lo mejor respeto a la separación de las capas, es mover el código a la clase myUser. La clase myUser sobreescribe la clase por defecto base symfony sfUser con comportamientos específicos de la aplicación:

// apps/frontend/modules/job/actions/actions.class.php
class jobActions extends sfActions
{
  public function executeShow(sfWebRequest $request)
  {
    $this->job = $this->getRoute()->getObject();
 
    $this->getUser()->addJobToHistory($this->job);
  }
 
  // ...
}
 
// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
  public function addJobToHistory(JobeetJob $job)
  {
    $ids = $this->getAttribute('job_history', array());
 
    if (!in_array($job->getId(), $ids))
    {
      array_unshift($ids, $job->getId());
 
      $this->setAttribute('job_history', array_slice($ids, 0, 3));
    }
  }
}

El código también ha sido modificado para tener en cuenta todos los requisitos:

  • !in_array($job->getId(), $ids): Un job no se puede almacenar dos veces en el historial

  • array_slice($ids, 0, 3): Sólo los tres últimos puestos de trabajo vistos por el usuario se muestran

En el layout, agrega el código siguiente antes de que la variable $sf_content se muestre:

// apps/frontend/templates/layout.php
<div id="job_history">
  Recent viewed jobs:
  <ul>
    <?php foreach ($sf_user->getJobHistory() as $job): ?>
      <li>
        <?php echo link_to($job->getPosition().' - '.$job->getCompany(), 'job_show_user', $job) ?>
      </li>
    <?php endforeach ?>
  </ul>
</div>
 
<div class="content">
  <?php echo $sf_content ?>
</div>

El layout usa un nuevo método getJobHistory() para obtener el historial job:

// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
  public function getJobHistory()
  {
    $ids = $this->getAttribute('job_history', array());
 
    return JobeetJobPeer::retrieveByPKs($ids);
  }
 
  // ...
}

EL método getJobHistory() usa el método propel retrieveByPKs() para recuperar varios objetos JobeetJob en una llamada.

Historial de Job

El sfParameterHolder

Para completar el API del historial de job, vamos a añadir un método para resetear el historial:

// apps/frontend/lib/myUser.class.php
class myUser extends sfBasicSecurityUser
{
  public function resetJobHistory()
  {
    $this->getAttributeHolder()->remove('job_history');
  }
 
  // ...
}

Los atributos del usuario son gestionados por un objeto de la clase sfParameterHolder. Los métodos getAttribute() y setAttribute() son métodos proxy para getParameterHolder()->get() y getParameterHolder()->set(). Como el método remove() no tiene método proxy en sfUser, necesitas usar el objeto del contenedor de parámetros directamente.

note

La clase sfParameterHolder es también utilizada por sfRequest para almacenar sus parámetros.

Seguridad de la Aplicación

Autenticación

Al igual que muchas otras características, la seguridad es manejada por un archivo YAML, security.yml. Por ejemplo, puedes encontrar la configuración por defecto para la aplicación backend en el directorio config/:

# apps/backend/config/security.yml
default:
  is_secure: false

Si cambia el is_secure a true, toda la aplicación backend necesitará que el usuario deba autenticarse.

Login

tip

En un archivo YAML, un Booleano puede expresarse con la cadena true o false.

Si echas un vistazo a los registros en la barra web de depuración, se puede observar que el método executeLogin() de la clase defaultActions se llama por cada página que intentas acceder.

Web debug

Cuando un usuario no-autenticado intenta acceder a una acción segura, Symfony remite la peticióna la acción login configurada en settings.yml:

all:
  .actions:
    login_module: default
    login_action: login

note

No es posible asegurar a la acción login para evitar una recursión infinita.

tip

Como vimos durante el día 4, el mismo archivo de configuración se pueden definir en varios lugares. Este es también el caso de security.yml. Para sólo asegurar o desproteger una única acción o un módulo, crea un security.yml en the directorio config/ del módulo:

index:
  is_secure: false
 
all:
  is_secure: true

De forma predeterminada, la clase myUser hereda de sfBasicSecurityUser, y no de sfUser. sfBasicSecurityUser proporciona métodos para gestionar la autenticación de usuario y autorización.

Para gestionar la autenticación de usuario, utiliza los métodos isAuthenticated() y setAuthenticated():

if (!$this->getUser()->isAuthenticated())
{
  $this->getUser()->setAuthenticated(true);
}

Autorización

Cuando un usuario está autenticado, el acceso a algunas acciones pueden ser aún más restringida por la definición de credenciales. Un usuario debe tener las credenciales para acceder a la página:

default:
  is_secure:   false
  credentials: admin

El sistema de credenciales de Symfony es bastante simple y poderoso. Una credencial puede representar todo lo que sea necesario para describir la seguridad al modelo de la aplicación (como grupos o permisos).

sidebar

Credenciales Complejas

El item credentials de security.yml soporta operadores Booleanos para describir las necesidades complejas en credenciales.

Si un usuario debe tener credenciales A y B, hay que envolver las credenciales con corchetes:

index:
  credentials: [A, B]

Si un usuario debe tener credenciales A o B, hay que envolver las credenciales con dos pares de corchetes:

index:
  credentials: [[A, B]]

Incluso puedes mezclar y combinar entre paréntesis para describir cualquier tipo de Expresión booleana con cualquier número de credenciales.

Para gestionar las credenciales de usuario, sfBasicSecurityUser da varios métodos:

// Add one or more credentials
$user->addCredential('foo');
$user->addCredentials('foo', 'bar');
 
// Check if the user has a credential
echo $user->hasCredential('foo');                      =>   true
 
// Check if the user has both credentials
echo $user->hasCredential(array('foo', 'bar'));        =>   true
 
// Check if the user has one of the credentials
echo $user->hasCredential(array('foo', 'bar'), false); =>   true
 
// Remove a credential
$user->removeCredential('foo');
echo $user->hasCredential('foo');                      =>   false
 
// Remove all credentials (useful in the logout process)
$user->clearCredentials();
echo $user->hasCredential('bar');                      =>   false

Por el Backend de Jobeet, no vamos a usar ninguna credencial ya que sólo tenemos un perfil: el administrador.

Plugins

Como no nos gusta reinventar la rueda, no vamos a desarrollar la acción de acceso a partir de cero. En lugar de ello, se instalará un plugin symfony.

Uno de los grandes puntos fuertes del framework Symfony son los plugins. Como veremos en los próximos días, es muy fácil crear un plugin. También es bastante potente, ya que un plugin puede contener cualquier cosa, desde la configuración de los módulos hasta los recursos web.

Hoy, vamos a instalar sfGuardPlugin to secure the backend application:

$ php symfony plugin:install sfGuardPlugin

La tarea plugin:install instala un plugin por nombre. Todos los plugins son almacenados bajo el directorio plugins/ y cada uno tiene su propio directorio con el nombre del nombre del plugin.

note

PEAR debe estar instalado para que la tarea plugin:install funcione.

Cuando se instala un plugin con la tarea plugin:install, Symfony instala la última versión estable del mismo. Para instalar una versión específica de un plugin, pasa la opción --release.

La web de los plugins lista todas las versiones disponibles agrupados por la versión de Symfony.

Como un plugin es auto-contenido en un directorio, también puedes descargar el paquete desde el sitio web de Symfony y descomprimirlo, o alternativamente, haces un enlace svn:externals a su Subversion repository.

Seguridad del Backend

Cada plugin tiene un archivo README que explica cómo configurarlo.

Vamos a ver cómo configurar el nuevo plugin. Como el plugin ofrece varias nuevas clases al modelo para la gestión de usuarios, grupos y permisos, necesitas reconstruir tu modelo:

$ php symfony propel:build --all --and-load --no-confirmation

tip

Recuerde que la tarea propel:build --all --and-load elimina todas las tablas existentes antes de volver a crearlas. Para evitar esto, puedes construir los modelos, formularios y filtros y, a continuación, crear las nuevas tablas ejecutando el SQL generado y almacenado en data/sql/.

Como sfGuardPlugin agrega varios métodos a la clase de usuario, es necesario cambiar la clase base de myUser a sfGuardSecurityUser:

// apps/backend/lib/myUser.class.php
class myUser extends sfGuardSecurityUser
{
}

sfGuardPlugin proporciona una acción signin en el modulo sfGuardAuth para autenticar a los usuarios:

Edita el archivo settings.yml para cambiar la acción por defecto empleada por la página de login:

# apps/backend/config/settings.yml
all:
  .settings:
    enabled_modules: [default, sfGuardAuth]
 
    # ...
 
  .actions:
    login_module:    sfGuardAuth
    login_action:    signin
 
    # ...

Como los plugins son compartidos entre todas las aplicaciones de un proyecto, tiene que permitir explícitamente los módulos que desea utilizar agregando el item enabled_modules.

sfGuardPlugin login

El último paso es crear un usuario administrador:

$ php symfony guard:create-user fabien SecretPass
$ php symfony guard:promote fabien

tip

El sfGuardPlugin proporciona funciones para la gestión de usuarios, grupos y permisos desde la línea de comandos. Utiliza la tarea list para listar todas las tareas pertenecientes al espacio de nombres guard:

$ php symfony list guard

Y cuando el usuario no está autenticado, tenemos que ocultar la barra de menú:

// apps/backend/templates/layout.php
<?php if ($sf_user->isAuthenticated()): ?>
  <div id="menu">
    <ul>
      <li><?php echo link_to('Jobs', 'jobeet_job') ?></li>
      <li><?php echo link_to('Categories', 'jobeet_category') ?></li>
    </ul>
  </div>
<?php endif ?>

Cuando el usuario es autenticado, tenemos que añadir un enlace de logout en el menú:

// apps/backend/templates/layout.php
<li><?php echo link_to('Logout', 'sf_guard_signout') ?></li>

tip

Para una lista de todas las rutas previstas por sfGuardPlugin, usa la tarea app:routes.

Para pulir el backend aún más, vamos a añadir un nuevo módulo para la gestión del administrador de usuarios. Afortunadamente, sfGuardPlugin proporciona un módulo de este tipo. Como el módulo sfGuardAuth, que necesitarás habilitarlo en settings.yml:

// apps/backend/config/settings.yml
all:
  .settings:
    enabled_modules: [default, sfGuardAuth, sfGuardUser]

Añadir un enlace en el menú:

// apps/backend/templates/layout.php
<li><?php echo link_to('Users', 'sf_guard_user') ?></li>

Backend menu

Terminamos!

Probando al Usuario

El tutorial de hoy no termina ya que no hemos aún hablado de las pruebas al usuario. Como el symfony browser simula las cookies, es bastante fácil de probar el comportamiento del usuario mediante el uso del tester sfTesterUser.

Vamos a actualizar la pruebas funcionales para el menú que hemos añadido hoy. Agrega el código siguiente al final del módulo de pruebas funcionales job:

// test/functional/frontend/jobActionsTest.php
$browser->
  info('4 - User job history')->
 
  loadData()->
  restart()->
 
  info('  4.1 - When the user access a job, it is added to its history')->
  get('/')->
  click('Web Developer', array(), array('position' => 1))->
  get('/')->
  with('user')->begin()->
    isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))->
  end()->
 
  info('  4.2 - A job is not added twice in the history')->
  click('Web Developer', array(), array('position' => 1))->
  get('/')->
  with('user')->begin()->
    isAttribute('job_history', array($browser->getMostRecentProgrammingJob()->getId()))->
  end()
;

Para facilitar las pruebas, primero hacemos la recarga datos y reiniciamos el navegador para empezar con una sesión.

El método isAttribute() controla un atributo de usuario dado.

note

El tester sfTesterUser también proporciona métodos isAuthenticated() and hasCredential() para poner a prueba la autenticación de usuario y autorizaciones.

Nos vemos mañana

Las clases usuario de Symfony son una buena manera de abstraerse de la gestión de las sesiones PHP. Junto con el sistema de plugins y el plugin sfGuardPlugin, hemos sido capaces de asegurar el backend de Jobeet en cuestión de minutos. E incluso hemos añadido una limpia interfaz de administrador para la gestión de nuestros usuarios de forma gratuita, gracias a los módulos proporcionados por el plugin.

Feedback

tip

Este capítulo ha sido traducido por Roberto Germán Puentes Díaz. Si encuentras algún error que deseas corregir o realizar algún comentario, no dudes en enviarlo por correo a puentesdiaz [arroba] gmail.com