Optimizando los Modelos de Laravel
Tags: php, laravel, castellano
Published: 2020-08-31 22:25:41

---
title: Optimizando los Modelos de Laravel
published: true
description: Optimizaciones pensadas para proyectos con una alta carga de datos (Eloquent Models) en memoria. Con estos "sencillos" cambios notarás una gran diferencia en el rendimiento de tu aplicación.
tags: php, laravel, castellano
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/7b8xoup0099w1c34r13n.jpg
---

[Laravel](https://laravel.com/) 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.

```php
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](https://laravel.com/docs/7.x/eloquent-mutators):

```php
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
<?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
<?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
<?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
<?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);
    }
}
```

```php
<h1>{{ $user->getFullName() }}</h1>
```

## Desactivar la gestión de fechas

Por defecto `Eloquent` inicializa como objetos [Carbon](https://github.com/briannesbitt/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](https://stackoverflow.com/a/43116788/49114).

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

```php
<?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
<?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
<?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
<?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
<?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
<?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!