Laravel

Laravel

Tutorial Laravel: Consultas avanzadas

Domina Eloquent ORM con scopes locales y globales, accesores, mutadores y eventos en Laravel para un código más organizado y eficaz en PHP para bases de datos.

Aprende Laravel GRATIS y certifícate

Scopes locales y globales

En Eloquent ORM, los scopes (alcances) permiten reutilizar partes de consultas de manera flexible y organizada. Los scopes facilitan la definición de condiciones comunes en los modelos, mejorando la legibilidad y mantenimiento del código.

Un scope local es una función que se define en el modelo y que se puede encadenar en las consultas. Para crear un scope local, se define un método público en el modelo que comienza con scope seguido del nombre del alcance. Por ejemplo:

class User extends Model
{
    public function scopeActive($query)
    {
        return $query->where('active', true);
    }
}

En este caso, el scope active filtra los usuarios que están activos. Para utilizar este scope en una consulta, simplemente se llama al método sin el prefijo scope:

$usuariosActivos = User::active()->get();

Los scopes locales pueden aceptar parámetros para personalizar aún más las consultas. Por ejemplo:

public function scopeOfType($query, $type)
{
    return $query->where('type', $type);
}

Y se utilizan de la siguiente manera:

$usuariosAdmin = User::ofType('admin')->get();

Los scopes globales se aplican automáticamente a todas las consultas de un modelo. Son útiles cuando se desea que ciertas condiciones se apliquen siempre, como filtrar registros eliminados lógicamente. Para definir un scope global, se utiliza el método addGlobalScope en el modelo:

class User extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('activo', function (Builder $builder) {
            $builder->where('active', true);
        });
    }
}

Con este scope global, todas las consultas sobre el modelo User incluirán automáticamente la condición where('active', true).

Si es necesario desactivar un scope global en una consulta específica, se puede utilizar el método withoutGlobalScope:

$usuariosInactivos = User::withoutGlobalScope('activo')->where('active', false)->get();

Los scopes globales también pueden definirse mediante clases que implementen la interfaz Scope de Eloquent, lo que permite una mayor separación y organización:

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class ActivoScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('active', true);
    }
}

class User extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new ActivoScope);
    }
}

Al utilizar scopes locales y globales, se logra un código más limpio y reutilizable. Además, se mejora la consistencia en las consultas y se reduce la posibilidad de errores al repetir condiciones comunes.

Es importante destacar que los scopes pueden combinarse y encadenarse en las consultas. Por ejemplo:

$usuariosActivosAdmin = User::active()->ofType('admin')->get();

En este ejemplo, se obtienen los usuarios que están activos y cuyo tipo es 'admin', aprovechando dos scopes locales definidos en el modelo.

El uso adecuado de scopes en Eloquent es una práctica recomendada que contribuye a mantener el código organizado y fácil de mantener en aplicaciones Laravel.

Accesores y mutadores

En Eloquent ORM, los accesores y mutadores son métodos que permiten manipular los valores de los atributos de un modelo al obtenerlos o establecerlos. Estos métodos proporcionan una forma eficiente de modificar la presentación o almacenamiento de los datos sin alterar directamente las propiedades del modelo.

Un accesor es un método que se ejecuta automáticamente al obtener el valor de un atributo. Para definir un accesor, se crea un método en el modelo con el prefijo get, seguido del nombre del atributo en CamelCase, y el sufijo Attribute. Por ejemplo, para el atributo nombre_completo:

class Usuario extends Model
{
    public function getNombreCompletoAttribute()
    {
        return "{$this->nombre} {$this->apellido}";
    }
}

En este ejemplo, el accesor getNombreCompletoAttribute combina los campos nombre y apellido para formar el nombre completo del usuario. Al acceder a $usuario->nombre_completo, se invoca este accesor de forma transparente.

Los mutadores permiten modificar el valor de un atributo antes de que se almacene en la base de datos. Se definen con el prefijo set, seguido del nombre del atributo en CamelCase, y el sufijo Attribute. Por ejemplo, para encriptar contraseñas:

class Usuario extends Model
{
    public function setPasswordAttribute($valor)
    {
        $this->attributes['password'] = bcrypt($valor);
    }
}

Aquí, el mutador setPasswordAttribute utiliza la función bcrypt para encriptar la contraseña antes de guardarla. Al asignar un valor a $usuario->password, se aplica este mutador automáticamente.

Los accesores y mutadores facilitan la aplicación de lógica al manejo de datos. Por ejemplo, para asegurar que los correos electrónicos se almacenen en minúsculas:

public function setEmailAttribute($valor)
{
    $this->attributes['email'] = strtolower($valor);
}

De esta manera, se garantiza que el atributo email siempre se almacene en un formato consistente en la base de datos.

También es posible utilizar accesores para formatear fechas u otros tipos de datos. Si se desea presentar la fecha de creación en un formato específico:

public function getCreatedAtAttribute($valor)
{
    return Carbon::parse($valor)->format('d-m-Y H:i');
}

Al acceder a $usuario->created_at, se obtiene la fecha formateada según el patrón definido, gracias al accesor implementado.

Para añadir atributos derivados que no existen en las tablas de la base de datos, se utiliza el array $appends en el modelo. Por ejemplo:

protected $appends = ['nombre_completo'];

Así, al serializar el modelo a un array o JSON, el atributo nombre_completo se incluye automáticamente, permitiendo que los accesores contribuyan a la representación externa de los datos.

Es importante seguir las convenciones de nomenclatura al definir los accesores y mutadores, utilizando el formato CamelCase y agregando los sufijos Attribute. Esto asegura que Laravel los reconozca y aplique correctamente.

Desde Laravel 9, se introdujo una nueva forma de definir accesores y mutadores utilizando la clase Attribute. Este enfoque mejora la legibilidad y organización del código. Por ejemplo:

use Illuminate\Database\Eloquent\Casts\Attribute;

public function nombreCompleto(): Attribute
{
    return Attribute::make(
        get: fn() => "{$this->nombre} {$this->apellido}"
    );
}

Con esta sintaxis, se define el accesor nombreCompleto de manera más concisa y explícita, aprovechando las características modernas de PHP.

Los mutadores también pueden manejarse con la clase Attribute. Por ejemplo, para asegurar que los nombres se almacenen con la primera letra en mayúscula:

public function nombre(): Attribute
{
    return Attribute::make(
        set: fn($valor) => ucfirst(strtolower($valor))
    );
}

Este mutador modifica el valor del atributo nombre antes de guardarlo, aplicando las funciones strtolower y ucfirst.

Además, es posible combinar accesores y mutadores en el mismo método Attribute. Por ejemplo:

public function saldo(): Attribute
{
    return Attribute::make(
        get: fn($valor) => $valor / 100,
        set: fn($valor) => $valor * 100
    );
}

En este caso, al obtener el atributo saldo, se divide el valor entre 100, y al establecerlo, se multiplica por 100, permitiendo manejar decimales almacenados como enteros en la base de datos.

Serialización y conversión a arrays/JSON

En Eloquent ORM, la serialización es el proceso de convertir modelos y sus relaciones a arrays o JSON para su consumo en vistas, APIs u otros sistemas. Laravel proporciona métodos integrados que facilitan esta conversión de manera eficiente y personalizable.

Para convertir un modelo a un array, se utiliza el método toArray(). Este método transforma el modelo y sus relaciones cargadas en una estructura de array nativa de PHP:

$usuario = User::find(1);
$datosUsuario = $usuario->toArray();

Al llamar a $usuario->toArray(), se obtiene un array con todos los atributos visibles del modelo, incluyendo las relaciones que se hayan cargado mediante eager loading.

Para convertir un modelo a JSON, se emplea el método toJson(). Este método serializa el modelo y sus relaciones a una cadena JSON:

$jsonUsuario = $usuario->toJson();

Este JSON puede utilizarse directamente en respuestas de APIs o enviarse a vistas para su uso con JavaScript.

Es posible controlar qué atributos se incluyen en la serialización utilizando la propiedad $hidden en el modelo. Los atributos especificados en $hidden se excluirán de los resultados de toArray() y toJson():

class User extends Model
{
    protected $hidden = ['password', 'remember_token'];
}

Al definir $hidden, se garantiza que datos sensibles como la contraseña no se expongan al serializar el modelo.

Si se prefiere especificar explícitamente los atributos a incluir, se puede utilizar la propiedad $visible:

protected $visible = ['id', 'name', 'email'];

Esta propiedad define una lista de atributos que se incluirán en la serialización, ignorando los demás.

Para añadir atributos personalizados a la serialización, se utiliza la propiedad $appends. Esto es útil cuando se han definido accesores en el modelo y se desea que sus resultados se incluyan al serializar:

protected $appends = ['nombre_completo'];

public function getNombreCompletoAttribute()
{
    return "{$this->nombre} {$this->apellido}";
}

Con $appends, el atributo nombre_completo aparecerá en el array o JSON resultante, aunque no exista físicamente en la base de datos.

Las relaciones cargadas se incluyen automáticamente en la serialización. Si se desea ocultar una relación específica, se puede utilizar el método makeHidden():

$usuario->makeHidden(['posts']);

$datosUsuario = $usuario->toArray();

En este caso, la relación posts no se incluirá en el array resultante. Este método es útil para ajustar la serialización de una instancia específica sin afectar al modelo en general.

Para modificar temporalmente los atributos visibles u ocultos en una instancia, se pueden utilizar los métodos setVisible() y setHidden():

$usuario->setVisible(['id', 'name']);

Esto permite ajustar los atributos incluidos en la serialización según las necesidades del contexto.

Cuando se trabaja con colecciones de modelos, como el resultado de all() o get(), es posible convertir toda la colección a un array o JSON:

$usuarios = User::all();
$datosUsuarios = $usuarios->toArray();

Las colecciones de Eloquent implementan la interfaz JsonSerializable, lo que facilita su conversión y manipulación.

Para personalizar el formato de las fechas al serializar, se puede definir la propiedad $dateFormat en el modelo:

protected $dateFormat = 'Y-m-d H:i:s';

De esta forma, todas las fechas del modelo se serializarán utilizando el formato especificado, lo que es útil para mantener la consistencia en las respuestas de APIs.

Además, Eloquent permite definir conversión de tipos mediante la propiedad $casts. Esto asegura que los atributos se serialicen con el tipo de dato correcto:

protected $casts = [
    'email_verified_at' => 'datetime',
    'is_admin' => 'boolean',
];

Los casts garantizan que, al serializar el modelo, los atributos como is_admin se representen como booleanos, en lugar de valores enteros o cadenas.

En ocasiones, es necesario personalizar por completo cómo se serializa un modelo. Esto se logra sobreescribiendo el método toArray() en la clase del modelo:

public function toArray()
{
    return [
        'identificador' => $this->id,
        'nombreUsuario' => $this->name,
        'correoElectronico' => $this->email,
        'fechaCreacion' => $this->created_at->format('d-m-Y H:i:s'),
    ];
}

Al hacerlo, se tiene un control total sobre la estructura y los datos incluidos en la serialización del modelo.

Es importante tener en cuenta que al utilizar toJson(), Laravel utiliza internamente toArray() antes de convertir el resultado a JSON. Por lo tanto, cualquier personalización realizada en toArray() afectará también a la serialización a JSON.

Cuando se retornan datos desde un controlador, se puede aprovechar la serialización automática que ofrece Laravel. Al retornar directamente un modelo o una colección, el framework se encarga de convertirlo a JSON:

public function show($id)
{
    $usuario = User::findOrFail($id);
    return $usuario;
}

En este caso, Laravel serializa el modelo User a JSON y establece correctamente los encabezados de la respuesta HTTP.

Para optimizar las respuestas en APIs, es recomendable utilizar recursos con JSON Resource de Laravel, que proporcionan una capa adicional para formatear y serializar datos de forma más precisa.

Finalmente, es posible convertir un modelo a JSON de forma legible utilizando el método toJson(JSON_PRETTY_PRINT):

echo $usuario->toJson(JSON_PRETTY_PRINT);

Esto genera una cadena JSON con formato, facilitando su lectura durante el desarrollo y depuración.

La serialización y conversión a arrays o JSON en Eloquent es una herramienta poderosa que permite preparar los datos de los modelos para su uso en diversas partes de la aplicación. Al aprovechar las funcionalidades de Eloquent, se logra una representación de los datos coherente y adaptable a las necesidades específicas de cada proyecto.

Eloquent Events y Observers

En Eloquent ORM, los eventos permiten interceptar y reaccionar a distintas etapas del ciclo de vida de un modelo. Los eventos proporcionan una forma eficaz de ejecutar lógica personalizada en momentos clave, como antes de guardar un modelo o después de eliminarlo.

Eventos de Eloquent

Laravel dispara varios eventos durante las operaciones de un modelo. Los eventos más comunes incluyen:

  • retrieved: cuando se obtiene un modelo de la base de datos.
  • creating: antes de crear un modelo nuevo.
  • created: después de crear un modelo nuevo.
  • updating: antes de actualizar un modelo.
  • updated: después de actualizar un modelo.
  • saving: antes de guardar un modelo, ya sea al crear o actualizar.
  • saved: después de guardar un modelo, ya sea al crear o actualizar.
  • deleting: antes de eliminar un modelo.
  • deleted: después de eliminar un modelo.
  • restoring: antes de restaurar un modelo con soft deletes.
  • restored: después de restaurar un modelo con soft deletes.

Para aprovechar estos eventos, se pueden definir closures en el método booted del modelo. Por ejemplo, para el evento creating:

class Usuario extends Model
{
    protected static function booted()
    {
        static::creating(function ($usuario) {
            // Lógica a ejecutar antes de crear el usuario
        });
    }
}

En este ejemplo, el closure asociado al evento creating se ejecuta antes de que el modelo Usuario se guarde en la base de datos por primera vez. Es una oportunidad para modificar atributos o realizar validaciones adicionales.

Observers

Los Observers son clases que agrupan los manejadores de eventos de un modelo, permitiendo organizar mejor el código y mantenerlo limpio. Un Observer puede contener métodos para uno o más eventos de un modelo específico.

Creación de un Observer

Para crear un Observer, se puede utilizar el comando de Artisan:

php artisan make:observer UsuarioObserver --model=Usuario

Este comando genera una clase UsuarioObserver en el directorio app/Observers y la asocia al modelo Usuario. La clase generada incluye métodos para cada evento disponible:

namespace App\Observers;

use App\Models\Usuario;

class UsuarioObserver
{
    public function creating(Usuario $usuario)
    {
        // Lógica antes de crear un usuario
    }

    public function updating(Usuario $usuario)
    {
        // Lógica antes de actualizar un usuario
    }

    // Otros métodos para los eventos disponibles
}

Cada método en el Observer corresponde a un evento y recibe una instancia del modelo como parámetro. Dentro de estos métodos, se puede implementar la lógica necesaria para cada caso.

Registro de un Observer

Para que Laravel utilice el Observer, es necesario registrarlo. Esto se hace típicamente en el método boot de la clase AppServiceProvider:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Models\Usuario;
use App\Observers\UsuarioObserver;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Usuario::observe(UsuarioObserver::class);
    }
}

Al llamar a observe(), se asocia el Observer UsuarioObserver al modelo Usuario. A partir de ese momento, Laravel llamará automáticamente a los métodos correspondientes en el Observer cuando ocurran los eventos del modelo.

Uso de Observers para Lógica de Negocio

Los Observers son ideales para mantener la lógica de negocio relacionada con los modelos en un lugar centralizado. Por ejemplo, se pueden usar para:

  • Generar un UUID único antes de crear un registro:
  public function creating(Usuario $usuario)
  {
      $usuario->uuid = (string) \Illuminate\Support\Str::uuid();
  }
  • Enviar un correo electrónico después de registrar un usuario:
public function created(Usuario $usuario)
{
    \Mail::to($usuario->email)->send(new BienvenidoMail($usuario));
}
  • Registrar cambios en los atributos antes de actualizar un modelo:
public function updating(Usuario $usuario)
{
    $cambios = $usuario->getDirty();
    // Registrar o procesar los cambios
}

Eventos de modelos y eventos personalizados

Es importante distinguir entre los eventos de modelos y los eventos personalizados que se manejan mediante la clase Event de Laravel. Los eventos de Eloquent están directamente relacionados con el ciclo de vida del modelo y se manejan a través de Observers o mediante la definición de closures en el modelo.

Por otro lado, los eventos personalizados se utilizan para comunicar diferentes partes de la aplicación y se manejan mediante Listeners y Subscribers. Estos eventos son ideales para disparar acciones que no están directamente relacionadas con un modelo específico.

Ejemplo completo con Observer

Supongamos que queremos mantener un registro de todas las eliminaciones de usuarios. Podemos crear un Observer que maneje el evento deleted:

namespace App\Observers;

use App\Models\Usuario;
use App\Models\HistorialEliminaciones;

class UsuarioObserver
{
    public function deleted(Usuario $usuario)
    {
        HistorialEliminaciones::create([
            'usuario_id' => $usuario->id,
            'email' => $usuario->email,
            'eliminado_el' => now(),
        ]);
    }
}

En este ejemplo, al eliminar un Usuario, se crea un registro en la tabla historial_eliminaciones usando el modelo HistorialEliminaciones. De esta manera, se lleva un historial de usuarios eliminados.

Validaciones y modificaciones con eventos

Los eventos pueden utilizarse para realizar validaciones o modificaciones antes de que se completen ciertas acciones. Por ejemplo, para asegurar que el correo electrónico de un usuario siempre se almacene en minúsculas:

public function saving(Usuario $usuario)
{
    $usuario->email = strtolower($usuario->email);
}

Al utilizar el evento saving, garantizamos que esta lógica se aplique tanto al crear como al actualizar un usuario.

Registro automático de observers

Si se desea registrar múltiples Observers de manera automática, se puede crear un proveedor de servicios dedicado. Por ejemplo, un ObserverServiceProvider:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Models\Usuario;
use App\Observers\UsuarioObserver;

class ObserverServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Usuario::observe(UsuarioObserver::class);
        // Registrar otros observers...
    }
}

Luego, se añade este proveedor al array providers en config/app.php:

'providers' => [
    // Otros proveedores...
    App\Providers\ObserverServiceProvider::class,
],

De esta forma, se mantiene organizado el registro de Observers en la aplicación.

Consideraciones de rendimiento

Es importante utilizar los eventos y Observers con precaución para evitar problemas de rendimiento. Ejecutar tareas intensivas dentro de los eventos puede ralentizar las operaciones del modelo. En estos casos, es recomendable delegar las tareas a Jobs o utilizar la cola de tareas de Laravel:

public function created(Usuario $usuario)
{
    EnviarCorreoBienvenida::dispatch($usuario);
}

Al utilizar dispatch(), la tarea se envía a la cola y se procesa de manera asíncrona, mejorando la eficiencia de la aplicación.

Desactivar eventos temporalmente

En ocasiones, puede ser necesario desactivar los eventos para una operación específica. Laravel permite suspender la ejecución de eventos utilizando el método withoutEvents:

Usuario::withoutEvents(function () {
    // Operaciones sobre modelos sin disparar eventos
    Usuario::create([...]);
});

Esta funcionalidad es útil para evitar bucles o acciones no deseadas durante procesos masivos o migraciones de datos.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Laravel GRATIS online

Todas las lecciones de Laravel

Accede a todas las lecciones de Laravel y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Laravel y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el uso de scopes locales y globales en Eloquent.
  • Aprender a crear y utilizar accesores y mutadores.
  • Diferenciar entre eventos de Eloquent y eventos personalizados.
  • Implementar Observers para manejar eventos de modelos.