CodeIgniter 4 Model: find(), allowedFields, Timestamps y Foreign Keys

- Andrés Cruz - EN In english

Video thumbnail

En este capítulo vamos a crear uno de los componentes fundamentales del patrón MVC: el modelo. Este componente es el que nos permite trabajar con la capa de datos, conectándose directamente con las migraciones en CodeIgniter 4 que creamos anteriormente. Un modelo en CodeIgniter 4 no es más que una clase PHP que extiende de CodeIgniter\Model, al igual que ocurre en frameworks como Laravel. A diferencia de la versión 3, en CI4 es necesario definir varias propiedades para que el modelo sea funcional y esté correctamente vinculado a su tabla.

La función primaria de un modelo es conectar una clase PHP con una tabla específica en la base de datos, centralizando ahí toda la lógica de acceso y manipulación de datos.

Tenemos que indicar los campos insertables, es decir, aquellas columnas que vamos a gestionar desde la aplicación. Para ilustrarlo, vamos a partir de la siguiente tabla de base de datos:

Tabla en MySQL

Dado que queremos administrar todas las columnas de esa tabla, necesitamos reflejarlas en nuestro modelo. Además, debemos indicar con qué tabla hace match nuestro modelo mediante la propiedad $table:

<?php

namespace App\Models;

use CodeIgniter\Model;

class MovieModel extends Model
{
    /**
     * @var string Nombre de la tabla de la base de datos.
     */
    protected $table = 'movies';

    /**
     * @var string Clave primaria de la tabla.
     */
    protected $primaryKey = 'id';

    /**
     * @var string[] Campos permitidos para insert/update (allowedFields).
     */
    protected $allowedFields = [
        'title',
        'description',
        'category_id',
    ];
}

Propiedades Principales del Modelo en CodeIgniter 4

La capa de modelos en el patrón MVC (Modelo-Vista-Controlador) es la responsable de la lógica de negocio y la comunicación con la base de datos: crear, leer, actualizar y eliminar registros (CRUD). Usualmente, cada modelo gobierna una sola tabla. A continuación, veamos las propiedades más comunes que puedes definir en un modelo en CodeIgniter 4 y para qué sirve cada una.

Las siguientes son las propiedades más importantes. Puedes ver la lista completa en la documentación oficial de modelos en CI4.

$table

Especifica el nombre de la tabla de la base de datos con la que trabaja este modelo. Si no se define, CodeIgniter intentará inferirla a partir del nombre de la clase.

$primaryKey

El nombre de la columna que identifica de manera única los registros en la tabla. No necesariamente tiene que coincidir con la clave primaria definida a nivel de base de datos; se usa principalmente con el método find() para saber contra qué columna comparar el valor proporcionado. Por ejemplo, $model->find(5) buscará el registro donde $primaryKey sea igual a 5.

$useAutoIncrement

Especifica si la tabla usa auto incremento para la $primaryKey. Si se establece en false, eres responsable de proporcionar el valor de la clave primaria manualmente en cada inserción.

$returnType

Define el tipo de dato que devuelven las consultas SELECT. Los valores válidos son 'array' (valor por defecto), 'object', o el nombre completo de una clase para usarla con el método getCustomResultObject() del objeto Result. Puedes sobreescribir este comportamiento en tiempo de ejecución con los métodos asArray() o asObject() encadenados en tu consulta.

$useSoftDeletes

Si se establece en true, cualquier llamada al método delete() no eliminará el registro físicamente; en su lugar, establecerá la columna deleted_at con la fecha actual. Esto es útil para preservar datos que podrían estar referenciados en otro lugar o para implementar una "papelera de reciclaje". Cuando está activo, los métodos find*() solo devuelven registros cuyo deleted_at sea NULL, a menos que llames a withDeleted() antes.

$allowedFields

Este array contiene los nombres de las columnas que pueden ser modificadas mediante los métodos save(), insert() o update(). Cualquier campo que no esté en esta lista será descartado silenciosamente. Esto actúa como una capa de seguridad contra ataques de asignación masiva (mass assignment), garantizando que solo las columnas que tú explícitamente permites puedan ser escritas desde la aplicación.

$useTimestamps

Este valor boolean determina si la fecha y hora actual se agrega automáticamente en todas las inserciones y actualizaciones. Si es true, el modelo gestionará automáticamente las columnas definidas en $createdField y $updatedField, estableciendo la hora en el formato especificado por $dateFormat. Esto requiere que la tabla tenga dichas columnas con el tipo de dato adecuado (DATETIME o INT, según tu configuración).

$createdField

Especifica el nombre de la columna de la base de datos que almacenará la marca de tiempo de creación del registro. Por defecto su valor es 'created_at'. Solo se escribe una vez, cuando el registro es insertado por primera vez.

$updatedField

Especifica el nombre de la columna que almacenará la marca de tiempo de la última actualización del registro. Por defecto su valor es 'updated_at'. Se actualiza automáticamente en cada llamada a save() o update().

$validationRules

Con esta propiedad puedes definir directamente en el modelo las reglas de validación que se aplicarán durante las operaciones de creación y actualización. Es una alternativa a definir las reglas en el controlador o en archivos de configuración separados. Por ejemplo: $validationRules = ['title' => 'required|min_length[3]|max_length[255]'].

Puedes ver la lista completa de propiedades disponibles en la documentación oficial de CI4.

Operaciones Comunes con el Modelo en CodeIgniter 4

Para crear un modelo en CodeIgniter 4, puedes utilizar el CLI de Spark con el siguiente comando:

$ php spark make:model NombreDelModelo

Este comando creará una nueva clase en app/Models/ con la estructura base ya definida. Una vez creado, el modelo queda disponible para ser inyectado en cualquier controlador o servicio de la aplicación.

Aparte de las propiedades de configuración, el modelo hereda de CodeIgniter\Model una serie de métodos listos para usar. A continuación, los más relevantes:

  • find($id) — Devuelve el registro cuyo $primaryKey coincide con el ID proporcionado.
  • findAll() — Devuelve todos los registros de la tabla.
  • first() — Devuelve el primer registro del resultado de la consulta.
  • insert($data) — Inserta un nuevo registro. Devuelve el ID generado o false en caso de error.
  • update($id, $data) — Actualiza el registro especificado. Devuelve true o false.
  • save($data) — Inserta o actualiza según si el array contiene la $primaryKey.
  • delete($id) — Elimina el registro (o aplica soft delete si $useSoftDeletes está activo).

getInsertID()

Después de ejecutar un insert(), puedes obtener el ID auto-generado del último registro insertado llamando a $model->getInsertID(). Esto es especialmente útil cuando necesitas ese ID para operaciones posteriores, como crear registros relacionados.

$movieModel = new MovieModel();
$movieModel->insert(['title' => 'Inception', 'category_id' => 3]);
$nuevoId = $movieModel->getInsertID(); // Obtiene el ID del registro recién insertado

Joins en el Query Builder de CodeIgniter 4

Los joins se emplean para combinar resultados de distintas tablas usando el campo relacional. CodeIgniter 4 soporta los mismos tipos de joins que SQL: INNER, LEFT, RIGHT, entre otros. La función join() recibe como primer parámetro la tabla a relacionar y como segundo la condición de igualdad:

$peliculaModel->asObject()
    ->select('peliculas.*, categorias.titulo as categoria')
    ->join('categorias', 'categorias.id = peliculas.categoria_id')
    ->find()

Paginación

CodeIgniter 4 incluye paginación nativa que va más allá de un simple LIMIT/OFFSET en SQL; genera automáticamente los enlaces de paginación. En esencia, reemplazas el método final find() por paginate(), indicando el número de registros por página:

$peliculaModel->asObject()
    ->select('peliculas.*, categorias.titulo as categoria')
    ->join('categorias', 'categorias.id = peliculas.categoria_id')
    ->paginate(10)

Condiciones where

El método where() es fundamental en la mayoría de las consultas. Recibe dos parámetros: la columna a comparar y su valor. Si encadenas múltiples where(), se unen con AND por defecto. También puedes usar operadores de comparación directamente en el nombre del campo: ->where('precio >', 100) o ->where('estado !=', 'inactivo'):

$peliculaModel->asObject()
    ->select('peliculas.*, categorias.titulo as categoria')
    ->join('categorias', 'categorias.id = peliculas.categoria_id')
    ->where('categorias.id', $categoria_id)
    ->find()

Condiciones orWhere

Similar al caso anterior, pero uniendo las condiciones con OR en lugar de AND. Es muy útil, por ejemplo, para buscar un usuario por email o por nombre de usuario indistintamente:

$usuarioModel->orWhere('email', $email)->orWhere('usuario', $usuario)->first();

Condiciones like

El método like() se usa cuando necesitas búsquedas parciales, equivalente al LIKE '%valor%' de SQL. También dispones de orLike() para combinar condiciones parciales con OR, y de groupStart() / groupEnd() para agrupar condiciones complejas:

$peliculaModel->asObject()
    ->select('peliculas.*, categorias.titulo as categoria')
    ->join('categorias', 'categorias.id = peliculas.categoria_id')
    ->like('peliculas.titulo', $buscar)
    ->find()

Agrupación con groupBy

La función groupBy() agrupa los resultados por el campo indicado, equivalente al GROUP BY de SQL. Ten en cuenta que, dependiendo de la versión de MySQL que uses, las columnas del SELECT que no sean funciones de agregación pueden necesitar estar incluidas también en el GROUP BY:

$peliculaModel->asObject()
    ->select('peliculas.*, categorias.titulo as categoria')
    ->join('categorias', 'categorias.id = peliculas.categoria_id')
    ->join('imagenes', 'peliculas.id = pelicula_id', 'left')
    ->groupBy('peliculas.id')
    ->find()

Ver el Query Compilado

Cuando estás construyendo consultas encadenadas, muchas veces quieres ver el SQL generado antes de ejecutarlo para depurar o verificar que la consulta es correcta. Para ello, necesitas una instancia del Query Builder de CodeIgniter, que te permite conectarte a cualquier tabla sin necesidad de un modelo:

$db = \Config\Database::connect();
$builder = $db->table('peliculas');

Una vez que tienes el $builder, en lugar de usar los métodos finales como get() o first(), utilizas getCompiledSelect(), que retorna el SQL generado como un string sin ejecutarlo:

return $builder->limit(10, 20)->getCompiledSelect();

Y obtendrías algo como:

SELECT * FROM `peliculas` LIMIT 20, 10

Lo más importante es notar que existe una equivalencia directa entre las operaciones SQL y las funciones del Query Builder de CodeIgniter 4. Estas funciones se van aprendiendo con la práctica: saber qué parámetros aceptan, cuáles son opcionales y cómo encadenarlas es lo que te volverá productivo rápidamente.

Este material forma parte de mi curso y libro completo sobre CodeIgniter 4.

Configuración de Timestamps: created_at y updated_at en CodeIgniter 4

Tener columnas que se rellenen automáticamente con la fecha de creación y la fecha de última modificación es algo estándar en frameworks modernos como Laravel, y en CodeIgniter 4 también podemos configurarlo de forma igual de sencilla. Solo necesitas definir tres propiedades en tu modelo:

  1. $useTimestamps — Booleano (true/false) que activa el manejo automático de timestamps.
  2. $createdField — Nombre de la columna que almacenará la fecha de creación (por defecto 'created_at').
  3. $updatedField — Nombre de la columna que almacenará la fecha de última actualización (por defecto 'updated_at').

Un detalle importante: si tienes activados los timestamps ($useTimestamps = true), debes incluir los campos created_at y updated_at también en tu $allowedFields, de lo contrario el modelo los filtrará y los timestamps no se guardarán. Veamos un ejemplo completo:

<?php
namespace App\Models;
use CodeIgniter\Model;

class ProductsControlModel extends Model
{
    protected $table         = 'products_control';
    protected $primaryKey    = 'id';
    protected $allowedFields = ['product_id', 'type', 'count', 'created_at', 'updated_at'];
    protected $useTimestamps = true;
    protected $createdField  = 'created_at';
    protected $updatedField  = 'updated_at';
}

Cómo Agregar Foreign Keys (Claves Foráneas) en Migraciones de CodeIgniter 4

Relaciones de tipo foráneas en CodeIgniter 4

Las relaciones son el núcleo de cualquier base de datos relacional. Describen cómo las tablas se vinculan entre sí mediante un identificador único: la clave primaria (PK). Por ejemplo, si tenemos una tabla de películas (movies) y otra de categorías (categories), cada película pertenece a una categoría. Para expresar esa relación, almacenamos el identificador de la categoría en la tabla de películas.

Cuando esa clave primaria de una tabla se registra en otra tabla para crear la relación, se convierte en una Foreign Key (FK) o clave foránea. Las FK a nivel de base de datos garantizan la integridad referencial: no podrás insertar un category_id en movies si ese ID no existe en categories. Veamos cómo definirlas en CodeIgniter 4 desde las migraciones:

// Firma: $this->forge->addForeignKey(campo, tabla_relacionada, campo_relacionado, al_borrar, al_actualizar);
$this->forge->addForeignKey('category_id', 'categories', 'id', 'CASCADE', 'CASCADE');

Migración de la tabla padre (tabla a relacionar)

En CodeIgniter 4, igual que en Laravel, las relaciones entre tablas se definen en las migraciones. Siempre debemos crear primero la tabla padre (la que tiene la PK) antes de crear la tabla hija (la que tendrá la FK). En este ejemplo, categories es la tabla padre:

<?php namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class Categories extends Migration
{
    public function up() {
        $this->forge->addField([
            'id' => [
                'type'           => 'INT',
                'constraint'     => 5,
                'unsigned'       => TRUE,
                'auto_increment' => TRUE
            ],
            'title' => [
                'type'       => 'VARCHAR',
                'constraint' => '255',
            ],
        ]);
        $this->forge->addKey('id', TRUE);
        $this->forge->createTable('categories');
    }
    //--------------------------------------------------------------------
    public function down()
    {
        $this->forge->dropTable('categories');
    }
}

Lo estándar: un campo id con auto incremento y un campo title. En el método down() realizamos la operación inversa: si en up() creamos la tabla, en down() la eliminamos. Esto permite revertir la migración limpiamente con php spark migrate:rollback.

Migración de la tabla hija con la FK

Ahora definimos la tabla que tendrá la clave foránea. El campo category_id debe ser del mismo tipo y tamaño que la PK a la que referencia (en este caso, INT(5) UNSIGNED):

<?php namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class Movies extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'id' => [
                'type'           => 'INT',
                'constraint'     => 5,
                'unsigned'       => TRUE,
                'auto_increment' => TRUE
            ],
            'title' => [
                'type'       => 'VARCHAR',
                'constraint' => 255
            ],
            'category_id' => [
                'type'       => 'INT',
                'constraint' => 5,
                'unsigned'   => TRUE
            ],
        ]);
        $this->forge->addKey('id', TRUE);
        $this->forge->addForeignKey('category_id', 'categories', 'id', 'CASCADE', 'CASCADE');
        $this->forge->createTable('movies');
    }
    //--------------------------------------------------------------------
    public function down()
    {
        $this->forge->dropTable('movies');
    }
}

Lo clave aquí es la columna category_id y la llamada a addForeignKey(), que recibe los siguientes parámetros:

  1. El nombre de la columna en la tabla hija (movies) que actuará como FK: 'category_id'.
  2. El nombre de la tabla padre a relacionar: 'categories'.
  3. El nombre de la columna en la tabla padre que es la PK: 'id'.
  4. Acción al borrar un registro padre: 'CASCADE' (borra también los hijos), 'SET NULL', 'NO ACTION' o 'RESTRICT'.
  5. Acción al actualizar la PK del registro padre: mismas opciones que el parámetro anterior.

Modelos para las tablas con relación FK

Los modelos correspondientes a las migraciones anteriores son simples: en $allowedFields incluyes el campo de la FK (category_id) como cualquier otro campo permitido:

<?php namespace App\Models;
use CodeIgniter\Model;

class MovieModel extends Model
{
    protected $table         = 'movies';
    protected $primaryKey    = 'id';
    protected $allowedFields = ['title', 'category_id'];
}

class CategoryModel extends Model
{
    protected $table         = 'categories';
    protected $primaryKey    = 'id';
    protected $allowedFields = ['title'];

    public function get($id = null)
    {
        if ($id === null) {
            return $this->findAll();
        }
        return $this->asArray()
            ->where(['id' => $id])
            ->first();
    }
}

Extra: Crear un Registro con Relación Foránea

Para insertar un registro que tiene una FK no necesitas ninguna configuración adicional en el modelo; la FK es, a efectos prácticos, una columna de tipo entero con una restricción de integridad. Simplemente pasas el valor del ID relacionado en el array de datos. En este ejemplo, los datos vienen de un formulario:

$movie = new MovieModel();
if ($this->validate('movies')) {
    $id = $movie->insert([
        'title'       => $this->request->getPost('title'),
        'description' => $this->request->getPost('description'),
        'category_id' => $this->request->getPost('category_id'),
    ]);
    // Obtén el ID del registro recién insertado
    $insertedId = $movie->getInsertID();
}

Otros Datos Importantes sobre los Modelos en CodeIgniter 4

  • En los modelos va la lógica de negocio: funciones específicas de la aplicación, como obtener todos los registros activos, buscar por criterios compuestos o aplicar transformaciones a los datos antes de devolverlos al controlador. Cada proceso suele tener su propio método dentro del modelo.
  • El framework provee métodos heredados de forma gratuita: find(), findAll(), first(), insert(), save(), update() y delete(). No tienes que escribirlos; simplemente los usas.
  • Los modelos van de la mano con las migraciones, pero son componentes independientes. A diferencia de frameworks como Django o Flask (donde las migraciones se generan a partir del modelo), en CodeIgniter 4 ambos son archivos separados. Lo habitual es tener una correspondencia uno a uno: una migración y un modelo por cada tabla.

Consultas con JOIN en el Query Builder de CodeIgniter 4

Consultas joins para la base de datos en CodeIgniter 4

En SQL, un JOIN combina filas de dos o más tablas relacionadas a partir de una columna común, generalmente las columnas de tipo FK. Se utilizan para recuperar datos de múltiples tablas en una sola consulta, presentando la información de manera completa. Por ejemplo: si tienes tablas de posts y categories, un JOIN te permite obtener el post junto con el nombre de su categoría en una sola consulta, sin necesidad de hacer dos viajes a la base de datos.

Existen diferentes tipos de JOIN en SQL, y CodeIgniter 4 soporta los principales:

  • INNER JOIN: Devuelve únicamente las filas que tienen coincidencia en ambas tablas. Si un registro de la tabla izquierda no tiene correspondencia en la tabla derecha, es excluido del resultado.
  • LEFT JOIN: Devuelve todas las filas de la tabla izquierda, y las columnas de la tabla derecha solo cuando hay coincidencia; donde no la hay, aparecen como NULL. Es útil cuando quieres obtener todos los registros de la tabla principal aunque no tengan relación en la otra.
  • RIGHT JOIN: El opuesto al LEFT JOIN: devuelve todas las filas de la tabla derecha, con los valores de la izquierda como NULL donde no hay coincidencia.

El uso del método join() en CodeIgniter 4 es directo. Primero construyes el SELECT base con los campos que necesitas (usando alias para evitar ambigüedades entre tablas):

$query = $productModel->asObject()->select("pc.*, u.email, puc.description, puc.direction")

Y luego encadenas los join() necesarios. Puedes anidar tantos como tus relaciones lo permitan:

$query = $productModel->asObject()->select("pc.*, u.email, puc.description, puc.direction")
    ->join('products_control as pc', 'pc.product_id = products.id')
    ->join('users as u', 'pc.user_id = u.id')
    ->join('products_users_control as puc', 'pc.id = puc.product_control_id');

En este ejemplo, un producto se relaciona con products_control, este a su vez con users, y finalmente con products_users_control. Observa el uso de alias (as pc, as u, as puc) para escribir condiciones más concisas.

Parámetros del método join()

La función join() acepta dos parámetros obligatorios y un tercero opcional:

  1. Tabla a unir — El nombre de la tabla. Opcionalmente, puedes agregar un alias con AS para simplificar las referencias: 'products_control as pc'.
  2. Condición de unión — La igualdad que define la relación, generalmente basada en la FK: 'pc.product_id = products.id'.
  3. Tipo de join (opcional) — Puede ser 'inner', 'left', 'right', 'outer', 'left outer' o 'right outer'. Por defecto es INNER JOIN. Ejemplo: ->join('products_control as pc', 'pc.product_id = products.id', 'left').

A continuación, los modelos completos del ejemplo del curso, que ilustran cómo combinar $useTimestamps, $allowedFields y los joins en un contexto real:

class ProductModel extends Model
{
    protected $table         = 'products';
    protected $primaryKey    = 'id';
    protected $allowedFields = ['name', 'code', 'description', 'entry', 'exit', 'stock', 'price'];

    public function get($id = null)
    {
        if ($id === null) {
            return $this->findAll();
        }
        return $this->asArray()
            ->where(['id' => $id])
            ->first();
    }
}

class ProductControlModel extends Model
{
    protected $table         = 'products_control';
    protected $primaryKey    = 'id';
    protected $allowedFields = ['product_id', 'type', 'count', 'created_at', 'updated_at', 'user_id'];
    protected $useTimestamps = true;
    protected $createdField  = 'created_at';
    protected $updatedField  = 'updated_at';

    public function get($id = null)
    {
        if ($id === null) {
            return $this->findAll();
        }
        return $this->asArray()
            ->where(['id' => $id])
            ->first();
    }
}

class ProductUserControlModel extends Model
{
    protected $table         = 'products_users_control';
    protected $primaryKey    = 'id';
    protected $allowedFields = ['product_id', 'user_id', 'product_control_id', 'type', 'count', 'created_at', 'updated_at', 'description', 'direction'];
    protected $useTimestamps = true;
    protected $createdField  = 'created_at';
    protected $updatedField  = 'updated_at';

    public function get($id = null)
    {
        if ($id === null) {
            return $this->findAll();
        }
        return $this->asArray()
            ->where(['id' => $id])
            ->first();
    }
}

Los JOINs son la herramienta más poderosa del Query Builder cuando necesitas datos de múltiples tablas relacionadas. En CodeIgniter 4 su uso es muy directo: el método join() acepta la tabla, la condición y opcionalmente el tipo de unión. Lo importante es que el orden en que encadenas los join()s y la definición correcta de las condiciones FK determinan la calidad y precisión de tus resultados.

Siguiente paso: genera datos de prueba mediante los Seeders en CodeIgniter 4.

Domina los modelos en CodeIgniter 4: configura $allowedFields, $useTimestamps (created_at/updated_at), Foreign Keys y el Query Builder join. Ejemplos de código incluidos.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.