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.
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 extend 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 historialarray_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.
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: off
Si cambia el is_secure
a on
, toda la aplicación backend
necesitará que el usuario deba autenticarse.
tip
En un archivo YAML, un booleano puede expresarse con la cadena true
y
false
, o on
y off
.
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.
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: off all: is_secure: on
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: off 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).
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-load --no-confirmation
tip
Recuerde que la tarea propel:build-all-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 siempre, cuando se crean nuevas clases, tendrás que borrar la caché de Symfony:
$ php symfony cc
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
.
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>
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.
note
Si deseas comprobar el código del día de hoy, o de cualquier otro día, el código esta
disponible día a día en el repositorio SVN oficial de Jobeet
(http://svn.jobeet.org/propel/
).
Por ejemplo, puedes obtener el código de hoy de la
etiqueta release_day_13
:
$ svn co http://svn.jobeet.org/propel/tags/release_day_13/ jobeet/
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
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.