Agregar funcionalidades nuevas a un sistema web no tiene por qué ser complejo. A partir de un listado de libros ya existente, es posible construir una repisa personal donde cada usuario registre qué libros posee, creando desde cero la tabla en base de datos, el modelo en PHP y la acción en el controlador, todo conectado con la vista en pocos pasos.
¿Cómo se crea la tabla de relación entre usuarios y libros?
El primer paso es definir la estructura de datos. En lugar de usar un ALTER TABLE, se opta por escribir directamente en el esquema un CREATE TABLE IF NOT EXISTS llamado user_books [0:20]. Esta tabla actúa como una tabla de relación (también conocida como tabla pivote) que vincula dos entidades: usuarios y libros.
sql
CREATE TABLE IF NOT EXISTS user_books (
user_book_id INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id INTEGER NOT NULL,
book_id INTEGER NOT NULL,
created_at DATETIME
);
La estructura es intencionalmente sencilla: un identificador único con auto increment, las dos llaves foráneas (user_id y book_id) marcadas como NOT NULL, y un campo created_at para registrar la fecha de creación [0:40]. Una vez ejecutada en la base de datos, la tabla queda lista.
¿Cómo se conecta la vista con el controlador para registrar un libro?
El flujo parte desde la vista de detalle del libro (book detail). Ahí se agrega un enlace HTML que dice "Yo tengo este libro" y apunta a una nueva acción del controlador [1:30].
php
<?= Html::a('Yo tengo este libro', ['book/i-own-this', 'bookId' => $book->id]) ?>
Este enlace envía el bookId como parámetro vía GET a través de la URL. Es importante incluir el helperHtml con su respectivo use para que el enlace funcione correctamente [1:55].
Al hacer clic, si la acción no existe todavía en el controlador, el sistema devuelve un error 404 [2:10]. Por eso el siguiente paso es crear la acción correspondiente.
¿Qué lógica lleva la acción en el controlador?
Dentro del BookController se crea una función pública con el nombre en camelCase: actionIOwnThisBook, que recibe como parámetro el bookId [2:20].
php
public function actionIOwnThisBook($bookId)
{
if (Yii::$app->user->isGuest) {
return $this->goHome();
}
Primero se valida que el usuario no sea invitado (is guest). Si lo es, se redirige al inicio [3:30].
Se instancia un nuevo objeto UserBook y se asignan los valores directamente desde código: el user_id se obtiene de Yii::$app->user->identity->id y el book_id del parámetro recibido [3:00].
Se llama a save() para persistir el registro.
Se establece un flash message de éxito y se redirige al detalle del libro [3:40].
¿Por qué se necesita un modelo nuevo y sin reglas de validación?
Para interactuar con la tabla user_books se crea el modelo UserBook dentro de la carpeta de modelos [2:45]. Su estructura es mínima:
php
class UserBook extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'user_books';
}
}
Un detalle clave es que no se definen rules en este modelo [4:25]. La razón es que los datos no provienen de un formulario del usuario, sino que se asignan directamente en el controlador con lógica de negocios. Cuando la información es controlada por código, las reglas de validación no son estrictamente necesarias.
¿Qué se puede mejorar en esta funcionalidad?
Al verificar en la base de datos con SELECT * FROM user_books, el registro aparece correctamente: usuario uno posee el libro sesenta y cuatro [4:05]. Sin embargo, hay oportunidades de mejora:
Verificar que el libro exista antes de guardarlo.
Evitar duplicados para que un usuario no registre el mismo libro dos veces.
Agregar reglas de negocio adicionales sobre permisos del usuario.
Este patrón de enviar datos vía GET a través de la URL es funcional para acciones simples. En el siguiente paso se abordará cómo calificar un libro enviando información vía POST, un método más adecuado cuando se manejan datos sensibles o formularios.
¿Has implementado relaciones similares entre usuarios y recursos en tus proyectos? Comparte tu experiencia en los comentarios.