DEV Community

Cover image for Optimizando los Modelos de Laravel
Lito
Lito

Posted on

Optimizando los Modelos de Laravel

Laravel es un framework de PHP muy pesado a nivel computacional. Está pensado para falicitar la vida al desarrollador, lo cual se agradece mucho, pero de ningún modo está pensado para ser ni rápido ni óptimo en el consumo de procesador o memoria, y los modelos son un claro reflejo de esto.

Es por esto que los modelos es de lo que primero se prescinde cuando en un proyecto se presenta una alta carga de peticiones por segundo o bien una importante carga de datos en memoria para ciertos procesos.

En este artículo vamos a presentar ciertos cambios que aliviarán de manera notable el consumo de procesador y memoria en tu código.

Iterar sobre elementos usando cursor

Cuando realizas una selección de registros de base de datos, todos los registros se cargan directamente en memoria al mismo tiempo dentro de una colección de modelos. Cuando trabajas con miles de registros esto puede ser demoledor para el tiempos de ejecución del proceso.

Para evitar eso puedes iterar por cada uno de los registros mediante el método cursor, el cual te permitirá mantener sólo en memoria un registro en cada iteración.

foreach (UserModel::withRelations()->cursor() as $user) {
    actionWithEveryUser($user);
}

Desactivar los attributos mágicos

Eloquent aplica demasiada magia en los modelos, y una de esas son los Mutators:

class User extends Model
{
    /**
     * @return string
     */
    public function getFullNameAttribute(): string
    {
        return $this->first_name.' '.$this->last_name;
    }

    /**
     * @param string $value
     *
     * @return void
     */
    public function setFirstNameAttribute(string $value): void
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}

Esto implica que cada acceso al atributo $user->full_name suponga un montón de operaciones (a parte de lo confuso de acceder a propiedades que realmente no existen en el modelo de datos).

Con lo cual vamos a desactivar los métodos que utiliza Eloquent para la comprobación de estos métodos mágicos.

Para eso lo ideal es siempre disponer de un ModelAbstract propio al que extiendan todos los demás modelos:

<?php declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

abstract class ModelAbstract extends Model
{
}

Sobre este modelo podemos aplicar un trait que desactive el acceso a los mutators.

<?php declare(strict_types=1);

namespace App\Models\Traits;

trait MutatorDisabled
{
    /**
     * @param string $key
     *
     * @return bool
     */
    public function hasGetMutator($key)
    {
        return false;
    }

    /**
     * @param string $key
     *
     * @return bool
     */
    public function hasSetMutator($key)
    {
        return false;
    }
}

y este trait se lo aplicamos al modelo base:

<?php declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

abstract class ModelAbstract extends Model
{
    use Traits\MutatorDisabled;
}

Una vez desactivados los mutators, utilizamos métodos tradicionales para el acceso a estos valores:

<?php declare(strict_types=1);

namespace App\Models;

class User extends ModelAbstract
{
    /**
     * @return string
     */
    public function getFullName(): string
    {
        return $this->first_name.' '.$this->last_name;
    }

    /**
     * @param string $value
     *
     * @return void
     */
    public function setFirstName(string $value): void
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}
<h1>{{ $user->getFullName() }}</h1>

Desactivar la gestión de fechas

Por defecto Eloquent inicializa como objetos Carbon todas las fechas que están definidas en nuestros modelos (que por defecto son created_at y updated_at).

Carbon es una librería muy potente pero también bastante lenta, y esta inicialización supone una carga muy alta cuando se trabaja con cantidad de registros importante.

La idea sería desactivar las funcionalidad de cast de fechas desde el modelo, y realizarla según se necesite mediante un helper, y a poder ser, usando directamente DateTime, ya que la diferencia de rendimiento es notable.

Para eso creamos un trait que después añadimos al ModelAbstract:

<?php declare(strict_types=1);

namespace App\Models\Traits;

trait DateDisabled
{
    /**
     * @return array
     */
    public function getDates(): array
    {
        return [];
    }

    /**
     * @param string $key
     *
     * @return bool
     */
    public function isDateCastable($key)
    {
        return false;
    }
}

Esto anulará el cast de todas las fechas, incluidas las definidas en $casts.

<?php declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

abstract class ModelAbstract extends Model
{
    use Traits\MutatorDisabled;
    use Traits\DateDisabled;
}

Evitar la transformación de claves de atributos en la exportación a JSON

En la conversión de valores de un modelo a JSON, Laravel realiza un cambio a snake_case, esto supone, a demás de un problema de rendimiento, un problema en la referencia claves de datos en su exportación.

Para evitar esto y que mantenga la definición propia del modelo debemos añadir un parámetro en el ModelAbstract:

<?php declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

abstract class ModelAbstract extends Model
{
    use Traits\MutatorDisabled;
    use Traits\DateDisabled;

    /**
     * @var bool
     */
    public static $snakeAttributes = false;
}

Si deseas aplicar cierta conversión del modelo en la salida JSON (lo cual es SIEMPRE lo recomendable), puedes usar Presenters, Transformers o Fractals (aquí cada cual le pone un nombre diferente a la misma cosa).

Mover los scopes hacia Query Builders

Eloquent mezcla en los modelos la gestión de datos con la selección de los mismos, lo cual es un claro antipatrón en la S de SOLID (Single Responsibility Principle).

La idea es sacar todo lo que tiene que ver con selección de datos en la base de datos (excepto relaciones) a clases auxiliares para la separación de conceptos.

También nos ayudará a evitar métodos mágicos que se llaman de una manera pero se referencian de otra, evitando el uso de scope y dándole un respiro a nuestro IDE.

Necesitaremos dar de alta un Builder por cada modelo, con lo cual vamos a crear un BuilderAbstract que nos permitirá añadir algunas acciones genéricas:

<?php declare(strict_types=1);

namespace App\Models\Builder;

use Illuminate\Database\Eloquent\Builder;
use App\Models\User as UserModel;

abstract class BuilderAbstract extends Builder
{
    /**
     * @param int $id
     *
     * @return self
     */
    public function byId(int $id): self
    {
        $this->where('id', $id);

        return $this;
    }

    /**
     * @param \App\Models\User $user
     *
     * @return self
     */
    public function byUser(UserModel $user): self
    {
        $this->where('user_id', $user->id);

        return $this;
    }

    /**
     * @return self
     */
    public function enabled(): self
    {
        $this->where('enabled', 1);

        return $this;
    }
}

Una vez definido nuestro BuilderAbstract, generamos uno por cada modelo:

<?php declare(strict_types=1);

namespace App\Models\Builder;

class User extends BuilderAbstract
{
    /**
     * @param string $email
     *
     * @return self
     */
    public function byEmail(string $email): self
    {
        $this->where('email', $email);

        return $this;
    }

    /**
     * @return self
     */
    public function withRelations(): self
    {
        $this->with(['posts', 'comments']);

        return $this;
    }
}

y con esto ya podemos indicar a nuestro modelo que debe usar un Builder de Eloquent personalizado:

<?php declare(strict_types=1);

namespace App\Models;

use Illuminate\Auth\Authenticatable as AuthenticatableTrait;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends ModelAbstract implements Authenticatable
{
    use AuthenticatableTrait, SoftDeletes;

    /**
     * @var string
     */
    protected $table = 'user';

    /**
     * @var string
     */
    public static string $foreign = 'user_id';

    /**
     * @var array
     */
    protected $casts = [
        'enabled' => 'boolean',
    ];

    /**
     * @var array
     */
    protected $hidden = ['password'];

    /**
     * @param \Illuminate\Database\Query\Builder $builder
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newEloquentBuilder($builder)
    {
        return new Builder\User($builder);
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function comments(): Relations\HasMany
    {
        return $this->hasMany(Comment::class, static::$foreign);
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function posts(): Relations\HasMany
    {
        return $this->hasMany(Post::class, static::$foreign);
    }
}

Con esto conseguiremos unos modelos mucho más limpios, de rendimiento más óptimo y más cerca de SOLID.

Si te ha parecido interesante, compárteme!

Top comments (0)