Tras realizar la primera parte de este tutorial deberías tener una aplicación muy básica para guardar favoritos.
En este capítulo añadiremos la autenticación y restringiremos los favoritos (bookmarks) para que cada usuario pueda consultar o modificar solamente los suyos.
En CakePHP, la autenticación se maneja mediante Components.
Los componentes pueden verse como una forma de crear trozos reutilizables de código de controlador para una finalidad o idea. Además pueden engancharse al evento de ciclo de vida de los controladores e interactuar con tu aplicación de ese modo.
Para empezar añadiremos el componente AuthComponent a nuestra aplicación.
Como queremos que todos nuestros métodos requieran de autenticación añadimos AuthComponent en AppController del siguiente modo:
// En src/Controller/AppController.php
namespace App\Controller;
use Cake\Controller\Controller;
class AppController extends Controller
{
public function initialize()
{
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
'unauthorizedRedirect' => $this->referer() // Si no está autorizado,
//el usuario regresa a la página que estaba
]);
// Permite ejecutar la acción display para que nuestros controladores de páginas
// sigan funcionando.
$this->Auth->allow(['display']);
}
}
Acabamos de decirle a CakePHP que queremos cargar los compomentes Flash
y
Auth
. Además hemos personalizado la configuración de AuthComponent indicando
que utilice como username el campo email de la tabla Users de la base de datos.
Ahora si vas a cualquier URL serás enviado a /users/login, que mostrará una página de error ya que no hemos escrito el código de la función login todavía, así que hagámoslo ahora:
// En src/Controller/UsersController.php
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error('Tu usuario o contraseña es incorrecta.');
}
}
Y en src/Template/Users/login.ctp añade lo siguiente:
<h1>Login</h1>
<?= $this->Form->create() ?>
<?= $this->Form->input('email') ?>
<?= $this->Form->input('password') ?>
<?= $this->Form->button('Login') ?>
<?= $this->Form->end() ?>
Ahora que tenemos un formulario de login sencillo deberíamos poder loguearnos con algún usuario que tenga contraseña encriptada.
Nota
loadComponent('Auth')
, a continuación edita un usuario y modifica
la contraseña.
Ahora deberías poder loguearte, si no es así asegúrate de que estás utilizando un usuario con contraseña encriptada.
Ahora que la gente puede loguearse probablemente quieras añadir una forma de desloguearse también.
Otra vez en UsersController
, añade el siguiente código:
public function initialize()
{
parent::initialize();
$this->Auth->allow(['logout']);
}
public function logout()
{
$this->Flash->success('Ahora estás deslogueado.');
return $this->redirect($this->Auth->logout());
}
Este código añade la acción logout
como una acción pública e implementa
la función.
Ahora puedes visitar /users/logout
para desloguearte, deberías ser enviado
a la página de inicio.
Si no estás logueado e intentas acceder a /users/add eres reenviado a la página de login. Deberíamos arreglar esto si queremos permitir que la gente se pueda registrar en nuestra aplicación.
En el controlador UsersController
añade lo siguiente:
public function initialize()
{
parent::initialize();
// Añade logout a la lista de actiones permitidas.
$this->Auth->allow(['logout', 'add']);
}
El código anterior le dice a AuthComponent
que la acción add()
no
necesita autenticación ni autorización.
Tal vez quieras tomarte un tiempo para limpiar Users/add.ctp y eliminar los
enlaces erróneos o continuar con el siguiente apartado. No vamos a crear la
edición de usuarios, consulta o listado en este tutorial así que no funcionará
el control de AuthComponent
para el acceso a esas acciones del controlador.
Ahora que los usuarios pueden loguearse queremos restringir los favoritos que uno puede ver a los que creó. Esto lo haremos usando un adaptador de “authorization”.
Ya que nuestro requisito es muy sencillo podremos escribir un código también muy
sencillo en nuestro BookmarksController
.
Pero antes necesitamos decirle al componente AuthComponent cómo va a autorizar
acciones nuestra aplicación. Para ello añade en AppController
:
public function isAuthorized($user)
{
return false;
}
Además añade la siguiente línea a la configuración de Auth
en tu AppController
:
'authorize' => 'Controller',
Tú método initialize()
debería verse así:
public function initialize()
{
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'authorize'=> 'Controller',// línea añadida
'authenticate' => [
'Form' => [
'fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
'unauthorizedRedirect' => $this->referer()
]);
// Permite ejecutar la acción display para que nuestros controladores
// de páginas sigan funcionando.
$this->Auth->allow(['display']);
}
Por defecto denegaremos el acceso siempre y concederemos los accesos donde tenga sentido.
Primero añadiremos la lógica de autorización para favoritos.
En tu BookmarksController
añade lo siguiente:
public function isAuthorized($user)
{
$action = $this->request->getParam('action');
// Las acciones add e index están siempre permitidas.
if (in_array($action, ['index', 'add', 'tags'])) {
return true;
}
// El resto de acciones requieren un id.
if (!$this->request->getParam('pass.0')) {
return false;
}
// Comprueba que el favorito pertenezca al usuario actual.
$id = $this->request->getParam('pass.0');
$bookmark = $this->Bookmarks->get($id);
if ($bookmark->user_id == $user['id']) {
return true;
}
return parent::isAuthorized($user);
}
Ahora si intentas consultar, editar o borrar un favorito que no te pertenece deberías ser redirigido a la página desde la que accediste.
Si no se muestra ningún mensaje de error añade lo siguiente a tu layout:
// En src/Template/Layout/default.ctp
<?= $this->Flash->render() ?>
Deberías poder ver ahora los mensajes de error de autorización.
Mientras que view y delete están funcionando, edit, add e index presentan un par de problemas:
Cuando añades un favorito puedes elegir el usuario.
Cuando editas un favorito puedes elegir un usuario.
La página con el listado muestra favoritos de otros usuarios.
Abordemos el formulario de añadir favorito primero.
Para empezar elimina input('user_id')
de src/Template/Bookmarks/add.ctp.
Con esa parte eliminada actualizaremos la acción add()
de
src/Controller/BookmarksController.php para que luzca así:
public function add()
{
$bookmark = $this->Bookmarks->newEntity();
if ($this->request->is('post')) {
$bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->getData());
$bookmark->user_id = $this->Auth->user('id');
if ($this->Bookmarks->save($bookmark)) {
$this->Flash->success('El favorito se ha guardado.');
return $this->redirect(['action' => 'index']);
}
$this->Flash->error('El favorito podría no haberse guardado. Por favor, inténtalo de nuevo.');
}
$tags = $this->Bookmarks->Tags->find('list');
$this->set(compact('bookmark', 'tags'));
$this->set('_serialize', ['bookmark']);
}
Completando la propiedad de la entidad con datos de la sesión eliminaremos cualquier posibilidad de que el usuario modifique el usuario al que pertenece el favorito. Haremos lo mismo para el formulario de edición.
Tu acción edit()
de src/Controller/BookmarksController.php debería ser
así:
public function edit($id = null)
{
$bookmark = $this->Bookmarks->get($id, [
'contain' => ['Tags']
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->getData());
$bookmark->user_id = $this->Auth->user('id');
if ($this->Bookmarks->save($bookmark)) {
$this->Flash->success('El favorito se ha guardado.');
return $this->redirect(['action' => 'index']);
}
$this->Flash->error('El favorito podría no haberse guardado. Por favor, inténtalo de nuevo.');
}
$tags = $this->Bookmarks->Tags->find('list');
$this->set(compact('bookmark', 'tags'));
$this->set('_serialize', ['bookmark']);
}
Ahora solo necesitamos mostrar los favoritos del usuario actualmente logueado.
Podemos hacer eso actualizando la llamada a paginate()
. Haz que tu método
index()
de src/Controller/BookmarksController.php se vea así:
public function index()
{
$this->paginate = [
'conditions' => [
'Bookmarks.user_id' => $this->Auth->user('id'),
]
];
$this->set('bookmarks', $this->paginate($this->Bookmarks));
$this->set('_serialize', ['bookmarks']);
}
Deberíamos actualizar también el método tags()
y el método finder relacionado,
pero lo dejaremos como un ejercicio para que lo hagas por tu cuenta.
Ahora mismo añadir nuevos tags es un proceso complicado desde que
TagsController
desautorizó todos los accesos.
En vez de permitirlos podemos mejorar la UI para la selección de tags utilizando un campo de texto separado por comas. Esto proporcionará una mejor experiencia para nuestros usuarios y usa algunas de las mejores características de ORM.
Para acceder de forma sencilla a las etiquetas formateadas podemos añadir un campo virtual/calculado a la entidad.
En src/Model/Entity/Bookmark.php añade lo siguiente:
use Cake\Collection\Collection;
protected function _getTagString()
{
if (isset($this->_properties['tag_string'])) {
return $this->_properties['tag_string'];
}
if (empty($this->tags)) {
return '';
}
$tags = new Collection($this->tags);
$str = $tags->reduce(function ($string, $tag) {
return $string . $tag->title . ', ';
}, '');
return trim($str, ', ');
}
Esto nos dará acceso a la propiedad calculada $bookmark->tag_string
que
utilizaremos más adelante.
Recuerda añadir la propiedad tag_string
a la lista _accessible
en tu
entidad para poder “guardarla” más adelante.
En src/Model/Entity/Bookmark.php añade tag_string
a $_accessible
de
este modo:
protected $_accessible = [
'user_id' => true,
'title' => true,
'description' => true,
'url' => true,
'user' => true,
'tags' => true,
'tag_string' => true,
];
Con la entidad actualizada podemos añadir un nuevo campo de entrada para nuestros
tags. En src/Template/Bookmarks/add.ctp y src/Template/Bookmarks/edit.ctp,
cambia el campo tags._ids
por el siguiente:
echo $this->Form->input('tag_string', ['type' => 'text']);
Ahora que podemos ver los tags existentes como un string querremos guardar también esa información.
Al haber marcado tag_string
como accesible el ORM copiará esa información
del request a nuestra entidad. Podemos usar un método de gancho beforeSave()
para parsear el string de etiquetas y encontrar/crear las entidades relacionadas.
Añade el siguiente código a src/Model/Table/BookmarksTable.php:
public function beforeSave($event, $entity, $options)
{
if ($entity->tag_string) {
$entity->tags = $this->_buildTags($entity->tag_string);
}
}
protected function _buildTags($tagString)
{
// Hace trim a las etiquetas
$newTags = array_map('trim', explode(',', $tagString));
// Elimina las etiquetas vacías
$newTags = array_filter($newTags);
// Elimina las etiquetas duplicadas
$newTags = array_unique($newTags);
$out = [];
$query = $this->Tags->find()
->where(['Tags.title IN' => $newTags]);
// Elimina las etiquetas existentes de la lista de nuevas etiquetas.
foreach ($query->extract('title') as $existing) {
$index = array_search($existing, $newTags);
if ($index !== false) {
unset($newTags[$index]);
}
}
// Añade las etiquetas existentes.
foreach ($query as $tag) {
$out[] = $tag;
}
// Añade las etiquetas nuevas.
foreach ($newTags as $tag) {
$out[] = $this->Tags->newEntity(['title' => $tag]);
}
return $out;
}
Aunque este código sea algo más complicado de lo que hemos hecho hasta ahora, nos ayudará a ver lo potente que es el ORM en CakePHP.
Puedes manipular los resultados de la consulta usando los métodos Collections y manejar escenearios en los que estás creando entidades on the fly con facilidad.
Hemos mejorado nuestra aplicación de favoritos para manejar escenarios de autenticación y de autorización/control de acceso básicos.
Además hemos añadido algunas mejoras interesantes de experiencia de usuario sacándole provecho a FormHelper y al potencial de ORM.
Gracias por tomarte tu tiempo para explorar CakePHP. Ahora puedes realizar el tutorial Tutorial Blog, aprender más sobre Acceso a la base de datos & ORM, o puedes leer detenidamente los Using CakePHP.