Consejos sobre los modelos en Laravel
- Publicado el 27 agosto, 2024
- Palabras: 4262
Laravel ofrece una gran cantidad de funciones interesantes que ayudan a mejorar nuestra experiencia de desarrollo (DX). Pero con los lanzamientos regulares, el estrés del trabajo diario y la gran cantidad de funciones disponibles, es fácil pasar por alto algunas de las funciones menos conocidas que pueden ayudar a mejorar nuestro código. En este artículo, voy a cubrir algunos de mis consejos favoritos para trabajar con modelos de Laravel. Con suerte, estos consejos te ayudarán a escribir un código más limpio y eficiente y te ayudarán a evitar errores comunes.
#Detección y prevención de problemas N+1
El primer consejo que veremos es cómo detectar y evitar las consultas N+1.
Las consultas N+1 son un problema común que puede ocurrir cuando se cargan relaciones de forma diferida, donde N es la cantidad de consultas que se ejecutan para obtener los modelos relacionados.
Pero, ¿qué significa esto? Veamos un ejemplo. Imaginemos que queremos obtener todas las publicaciones de la base de datos, recorrerlas en bucle y acceder al usuario que creó la publicación. Nuestro código podría verse así:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
}
Aunque el código anterior parece correcto, en realidad va a causar un problema N+1. Digamos que hay 100 publicaciones en la base de datos. En la primera línea, ejecutaremos una única consulta para obtener todas las publicaciones. Luego, dentro del bucle foreach, cuando accedamos a $post→user, esto activará una nueva consulta para obtener el usuario para esa publicación; lo que dará como resultado 100 consultas adicionales. Esto significa que ejecutaríamos 101 consultas en total. Como puedes imaginar, ¡esto no es del todo óptimo! Puede ralentizar tu aplicación y poner una tensión innecesaria en tu base de datos.
A medida que tu código se vuelve más complejo y las funciones crecen, puede ser difícil detectar estos problemas a menos que estés buscándolos activamente.
Afortunadamente, Laravel proporciona un método Model::preventLazyLoading() útil que puedes usar para ayudar a detectar y prevenir estos problemas N+1. Este método le indicará a Laravel que lance una excepción cada vez que se cargue de forma diferida una relación, de modo que pueda estar seguro de que siempre cargará con anticipación sus relaciones.
Para usar este método, puede agregar la llamada al método Model::preventLazyLoading() a su clase AppProvidersAppServiceProvider:
namespace AppProviders;
use IlluminateDatabaseEloquentModel;
use IlluminateSupportServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading();
}
}
Ahora, si ejecutáramos nuestro código anterior para obtener cada publicación y acceder al usuario que creó la publicación, veríamos una excepción ‘IlluminateDatabaseLazyLoadingViolationException’ con el siguiente mensaje:
Attempted to lazy load [user] on model [AppModelsPost] but lazy loading is disabled.
Para solucionar este problema, podemos actualizar nuestro código para cargar con avidez la relación de usuario al obtener las publicaciones. Podemos hacerlo usando el método with:
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name;
}
El código anterior ahora se ejecutará correctamente y solo activará dos consultas: una para obtener todas las publicaciones y otra para obtener todos los usuarios de esas publicaciones.
#Evitar el acceso a atributos que faltan
¿Con qué frecuencia has intentado acceder a un campo que creías que existía en un modelo pero no era así? Es posible que hayas cometido un error tipográfico o tal vez hayas pensado que existía un campo full_name cuando en realidad se llamaba name.
Imagínese que tenemos un modelo ‘AppModelsUser’ con los siguientes campos:
- id
- name
- password
- created_at
- updated_at
¿Qué pasaría si ejecutamos el siguiente código?
$user = User::query()->first();
$name = $user->full_name;
Suponiendo que no tenemos un accessor full_name en el modelo, la variable $name sería nula. Pero no sabríamos si esto se debe a que el campo full_name en realidad es nulo, a que no hemos obtenido el campo de la base de datos o a que el campo no existe en el modelo. Como puedes imaginar, esto puede provocar un comportamiento inesperado que a veces puede ser difícil de detectar.
Laravel proporciona un método Model::preventAccessingMissingAttributes() que puedes usar para ayudar a prevenir este problema. Este método le indicará a Laravel que genere una excepción cada vez que intentes acceder a un campo que no existe en la instancia actual del modelo.
Para habilitar esto, puedes agregar la llamada al método Model::preventAccessingMissingAttributes() a tu clase AppProvidersAppServiceProvider:
namespace AppProviders;
use IlluminateDatabaseEloquentModel;
use IlluminateSupportServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventAccessingMissingAttributes();
}
}
Ahora, si ejecutamos nuestro código de ejemplo e intentamos acceder al campo full_name en el modelo AppModelsUser, veríamos una excepción IlluminateDatabaseEloquentMissingAttributeException con el siguiente mensaje:
The attribute [full_name] either does not exist or was not retrieved for model [AppModelsUser].
Un beneficio adicional de usar preventAccessingMissingAttributes es que puede indicar cuándo estamos intentando leer un campo que existe en el modelo pero que quizás no hayamos cargado. Por ejemplo, imaginemos que tenemos el siguiente código:
$user = User::query()
->select(['id', 'name'])
->first();
$user->email;
Si hemos impedido que se acceda a los atributos que faltan, se lanzará la siguiente excepción:
The attribute [email] either does not exist or was not retrieved for model [AppModelsUser].
Esto puede resultar increíblemente útil al actualizar consultas existentes. Por ejemplo, en el pasado, es posible que solo haya necesitado algunos campos de un modelo. Pero tal vez ahora esté actualizando la función en su aplicación y necesite acceder a otro campo. Sin tener este método habilitado, es posible que no se dé cuenta de que está intentando acceder a un campo que no se ha cargado.
Vale la pena señalar que el método preventAccessingMissingAttributes se ha eliminado de la documentación de Laravel, pero aún funciona. No estoy seguro del motivo de su eliminación, pero es algo que debe tener en cuenta. Puede ser una indicación de que se eliminará en el futuro.
#Evitar el descarte silencioso de atributos
De manera similar a preventAccessingMissingAttributes, Laravel ofrece un método preventSilentlyDiscardingAttributes que puede ayudar a prevenir un comportamiento inesperado al actualizar los modelos.
Imagina que tienes una clase de modelo AppModelsUser como la siguiente:
namespace AppModels;
use IlluminateFoundationAuthUser as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
];
// ...
}
Como podemos ver, los campos de name, email y password son campos que se pueden completar. Pero, ¿qué sucedería si intentáramos actualizar un campo inexistente en el modelo (como full_name) o un campo que existe pero no se puede completar (como email_verified_at)?:
$user = User::query()->first();
$user->update([
'full_name' => 'Ash', // Field does not exist
'email_verified_at' => now(), // Field exists but isn't fillable
// Update other fields here too...
]);
Si tuviéramos que ejecutar el código anterior, tanto el campo full_name como el campo email_verified_at se ignorarían porque no se han definido como campos rellenables. Pero no se generarían errores, por lo que no sabríamos que los campos se han descartado de forma silenciosa.
Como es de esperar, esto podría generar errores difíciles de detectar en su aplicación, especialmente si cualquier otro campo en su declaración "update" se actualizara de hecho. Por lo tanto, podemos usar el método preventSilentlyDiscardingAttributes que generará una excepción cada vez que intente actualizar un campo que no existe en el modelo o que no se puede rellenar.
Para usar este método, puede agregar la llamada al método Model::preventSilentlyDiscardingAttributes() a su clase AppProvidersAppServiceProvider:
namespace AppProviders;
use IlluminateDatabaseEloquentModel;
use IlluminateSupportServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes();
}
}
Lo anterior obligaría a que se genere un error.
Ahora, si intentáramos ejecutar nuestro código de ejemplo anterior y actualizar los campos first_name y email_verified_at del usuario, se generaría una excepción IlluminateDatabaseEloquentMassAssignmentException con el siguiente mensaje:
Add fillable property [full_name, email_verified_at] to allow mass assignment on [AppModelsUser].
Vale la pena señalar que el método preventSilentlyDiscardingAttributes solo resaltará los campos que no se pueden completar cuando se utiliza un método como fill o update. Si se configura manualmente cada propiedad, no se detectarán estos errores. Por ejemplo, tomemos el siguiente código:
$user = User::query()->first();
$user->full_name = 'Ash';
$user->email_verified_at = now();
$user->save();
En el código anterior, el campo full_name no existe en la base de datos, por lo que, en lugar de que Laravel lo detecte por nosotros, lo detectaría en el nivel de la base de datos. Si estuvieras usando una base de datos MySQL, verías un error como este:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'full_name' in 'field list' (Connection: mysql, SQL: update ‘users’ set ‘email_verified_at’ = 2024-08-02 16:04:08, ‘full_name’ = Ash, ‘users’.'updated_at' = 2024-08-02 16:04:08'where ‘id '= 1)
#Habilitar el modo estricto para los modelos
Si desea utilizar los tres métodos que hemos mencionado hasta ahora, puede habilitarlos todos a la vez mediante el método Model::shouldBeStrict(). Este método habilitará las configuraciones preventLazyLoading, preventAccessingMissingAttributes y preventSilentlyDiscardingAttributes.
Para utilizar este método, puede agregar la llamada al método Model::shouldBeStrict() a su clase AppProvidersAppServiceProvider:
namespace AppProviders;
use IlluminateDatabaseEloquentModel;
use IlluminateSupportServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::shouldBeStrict();
}
}
Esto es el equivalente de:
namespace AppProviders;
use IlluminateDatabaseEloquentModel;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading();
Model::preventSilentlyDiscardingAttributes();
Model::preventAccessingMissingAttributes();
}
}
De manera similar al método preventAccessingMissingAttributes, el método shouldBeStrict se ha eliminado de la documentación de Laravel (commit), pero aún funciona. Esto podría ser una indicación de que se eliminará en el futuro.
#Usando UUIDs
De manera predeterminada, los modelos de Laravel usan identificadores de incremento automático como su clave principal. Pero puede haber ocasiones en las que prefieras usar identificadores únicos universales (UUID).
Los UUID son cadenas alfanuméricas de 128 bits (o 36 caracteres) que se pueden usar para identificar recursos de manera única. Debido a cómo se generan, es muy poco probable que entren en conflicto con otro UUID. Un ejemplo de UUID es: 1fa24c18-39fd-4ff2-8f23-74ccd08462b0.
Es posible que desees usar un UUID como la clave principal para un modelo. O bien, es posible que desees mantener tus identificadores de incremento automático para definir relaciones dentro de tu aplicación y base de datos, pero usar UUID para identificadores públicos. El uso de este enfoque puede agregar una capa adicional de seguridad al dificultar que los atacantes adivinen los identificadores de otros recursos.
Por ejemplo, supongamos que estamos usando identificadores de incremento automático en una ruta. Podríamos tener una ruta para acceder a un usuario como la siguiente:
use AppModelsUser;
use IlluminateSupportFacadesRoute;
Route::get('/users/{user}', function (User $user) {
dd($user->toArray());
});
Un atacante podría recorrer los identificadores (p. ej., /users/1, /users/2, /users/3, etc.) en un intento de acceder a la información de otros usuarios si las rutas no son seguras. Mientras que si usáramos UUID, las URL podrían parecerse más a /users/1fa24c18-39fd-4ff2-8f23-74ccd08462b0, /users/b807d48d-0d01-47ae-8bbc-59b2acea6ed3 y /users/ec1dde93-c67a-4f14-8464-c0d29c95425f. Como puedes imaginar, son mucho más difíciles de adivinar.
Por supuesto, el uso de UUID no es suficiente para proteger tus aplicaciones, son solo un paso adicional que puedes tomar para mejorar la seguridad. Debes asegurarte de utilizar también otras medidas de seguridad, como limitación de velocidad, autenticación y comprobaciones de autorización.
#Usar UUID como clave principal
Primero, comenzaremos por ver cómo cambiar la clave principal a UUID.
Para ello, debemos asegurarnos de que nuestra tabla tenga una columna que sea capaz de almacenar UUID. Laravel ofrece un método $table->uuid muy útil que podemos usar en nuestras migraciones.
Imaginemos que tenemos esta migración básica que crea una tabla de comentarios:
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->uuid();
$table->foreignId('user_id');
$table->foreignId('post_id');
$table->string('content');
$table->timestamps();
});
}
// ...
}
Como podemos ver en la migración, hemos definido un campo UUID. De manera predeterminada, este campo se llamará uuid, pero puede cambiarlo pasando un nombre de columna al método uuid si lo desea.
Luego, debemos indicarle a Laravel que use el nuevo campo uuid como clave principal para nuestro modelo AppModelsComment. También debemos agregar un Trait que le permitirá a Laravel generar UUID automáticamente para nosotros. Podemos hacer esto anulando la propiedad $primaryKey en nuestro modelo y usando el Trait IlluminateDatabaseEloquentConcernsHasUuids:
namespace AppModels;
use IlluminateDatabaseEloquentConcernsHasUuids;
use IlluminateDatabaseEloquentModel;
class Comment extends Model
{
use HasUuids;
protected $primaryKey = 'uuid';
}
El modelo ya debería estar configurado y listo para usar UUID como clave principal. Veamos este código de ejemplo:
use AppModelsComment;
use AppModelsPost;
use AppModelsUser;
$user = User::first();
$post = Post::first();
$comment = new Comment();
$comment->content = 'The comment content goes here.';
$comment->user_id = $user->id;
$comment->post_id = $post->id;
$comment->save();
dd($comment->toArray());
// [
// "content" => "The comment content goes here."
// "user_id" => 1
// "post_id" => 1
// "uuid" => "9cb16a60-8c56-46f9-89d9-d5d118108bc5"
// "updated_at" => "2024-08-05T11:40:16.000000Z"
// "created_at" => "2024-08-05T11:40:16.000000Z"
// ]
Podemos ver en el modelo volcado que el campo uuid se ha rellenado con un UUID.
#Agregar un campo UUID a un modelo
Si prefieres mantener tus identificadores de incremento automático para las relaciones internas pero usar UUID para los identificadores públicos, puedes agregar un campo UUID a tu modelo.
Supondremos que tu tabla tiene campos id y uuid. Dado que usaremos el campo id como clave principal, no necesitaremos definir la propiedad $primaryKey en nuestro modelo.
Podemos anular el método uniqueIds que está disponible a través del atributo IlluminateDatabaseEloquentConcernsHasUuids. Este método debería devolver una matriz de los campos para los que se deberían generar UUID.
Actualicemos nuestro modelo AppModelsComment para incluir el campo que hemos llamado uuid:
namespace AppModels;
use IlluminateDatabaseEloquentConcernsHasUuids;
use IlluminateDatabaseEloquentModel;
class Comment extends Model
{
use HasUuids;
public function uniqueIds(): array
{
return ['uuid'];
}
}
Ahora, si tuviéramos que volcar un nuevo modelo AppModelsComment, veríamos que el campo uuid se ha rellenado con un UUID:
// [
// "id" => 1
// "content" => "The comment content goes here."
// "user_id" => 1
// "post_id" => 1
// "uuid" => "9cb16a60-8c56-46f9-89d9-d5d118108bc5"
// "updated_at" => "2024-08-05T11:40:16.000000Z"
// "created_at" => "2024-08-05T11:40:16.000000Z"
// ]
Más adelante en este artículo veremos cómo puedes actualizar tus modelos y rutas para usar estos UUID como tus ID públicos en tus rutas.
#Usando ULIDs
De manera similar al uso de UUID en sus modelos de Laravel, es posible que a veces desee utilizar identificadores universalmente únicos y ordenables lexicográficamente (ULID).
Los ULID son cadenas alfanuméricas de 128 bits (o 26 caracteres) que se pueden utilizar para identificar recursos de forma única. Un ejemplo de ULID es: 01J4HEAEYYVH4N2AKZ8Y1736GD.
Puede definir los campos ULID exactamente de la misma manera que definiría los campos UUID. La única diferencia es que, en lugar de actualizar su modelo para utilizar el Trait IlluminateDatabaseEloquentConcernsHasUuids, debería utilizar el Trait IlluminateDatabaseEloquentConcernsHasUlids.
Por ejemplo, si quisiéramos actualizar nuestro modelo AppModelsComment para utilizar ULID como clave principal, podríamos hacerlo de esta manera:
namespace AppModels;
use IlluminateDatabaseEloquentConcernsHasUlids;
use IlluminateDatabaseEloquentModel;
class Comment extends Model
{
use HasUlids;
}
#Cambiar el campo utilizado para el route model binding
Probablemente ya sepas qué es el route model binding. Pero, por si acaso no lo sabes, vamos a repasarlo rápidamente.
El route model binding te permite obtener automáticamente instancias de modelos en función de los datos que se pasan a las rutas de tu aplicación Laravel.
De forma predeterminada, Laravel utilizará el campo de clave principal de tu modelo (normalmente un campo id) para el route model binding. Por ejemplo, podrías tener una ruta para mostrar la información de un solo usuario:
use AppModelsUser;
use IlluminateSupportFacadesRoute;
Route::get('/users/{user}', function (User $user) {
dd($user->toArray());
});
La ruta definida en el ejemplo anterior intentará encontrar un usuario que exista en la base de datos con el ID proporcionado. Por ejemplo, digamos que existe un usuario con un ID de 1 en la base de datos. Cuando visite la URL /users/1, Laravel buscará automáticamente el usuario con un ID de 1 de la base de datos y lo pasará a un clousure (o controlador) para que actúe en consecuencia. Pero si no existe un modelo con el ID proporcionado, Laravel devolverá automáticamente una respuesta 404 No encontrado.
Pero puede haber ocasiones en las que desee utilizar un campo diferente (que no sea su clave principal) para definir cómo se recupera un modelo de la base de datos.
Por ejemplo, como hemos mencionado anteriormente, es posible que desee utilizar ID de incremento automático como las claves principales de su modelo para las relaciones internas. Pero es posible que desee utilizar UUID para ID de cara al público. En este caso, es posible que desee utilizar el campo uuid para el route model binding en lugar del campo id.
De manera similar, si estás creando un blog, es posible que quieras obtener tus publicaciones en función de un campo slug en lugar del campo id. Esto se debe a que un campo slug es más legible para los humanos y más compatible con SEO que un ID de incremento automático.
#Cambiar el campo para todas las rutas
Si desea definir el campo que se debe utilizar para todas las rutas, puede hacerlo definiendo un método getRouteKeyName en su modelo. Este método debe devolver el nombre del campo que desea utilizar para la vinculación del modelo de ruta.
Por ejemplo, imagine que desea cambiar todas las vinculaciones del modelo de ruta para un modelo AppModelsPost para utilizar el campo slug en lugar del campo id. Podemos hacer esto agregando un método getRouteKeyName a nuestro modelo Post:
namespace AppModels;
use IlluminateContractsDatabaseEloquentBuilder;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
class Post extends Model
{
use HasFactory;
public function getRouteKeyName()
{
return 'slug';
}
// ...
}
Esto significa que ahora podríamos definir nuestras rutas de la siguiente manera:
use AppModelsPost;
use IlluminateSupportFacadesRoute;
Route::get('/posts/{post}', function (Post $post) {
dd($post->toArray());
});
Y cuando visitamos la URL /posts/my-first-post, Laravel obtendrá automáticamente la publicación con un slug de my-first-post de la base de datos y la pasará a al clousure (o controlador) para actuar en consecuencia.
#Cambiar el campo para rutas individuales
Sin embargo, puede haber ocasiones en las que solo desee cambiar el campo utilizado en una única ruta. Por ejemplo, es posible que desee utilizar el campo slug para la vinculación del modelo de ruta en una ruta, pero el campo id en todas las demás rutas.
Podemos hacer esto utilizando la sintaxis :field en nuestra definición de ruta. Por ejemplo, supongamos que queremos utilizar el campo slug para la vinculación del modelo de ruta en una única ruta. Podemos definir nuestra ruta de la siguiente manera:
Route::get('/posts/{post:slug}', function (Post $post) {
dd($post->toArray());
});
Esto ahora significa que en esta ruta específica, Laravel intentará obtener la publicación con el campo slug proporcionado de la base de datos.
#Utilice colecciones de modelos personalizados
Cuando obtienes varios modelos de la base de datos usando un método como AppModelsUser::all(), Laravel normalmente los colocará dentro de una instancia de la clase IlluminateDatabaseEloquentCollection. Esta clase proporciona muchos métodos útiles para trabajar con los modelos devueltos. Sin embargo, puede haber ocasiones en las que desees devolver una clase de colección personalizada en lugar de la predeterminada.
Es posible que desees crear una colección personalizada por algunas razones. Por ejemplo, es posible que desees agregar algunos métodos auxiliares que sean específicos para trabajar con ese tipo de modelo. O tal vez quieras usarlo para mejorar la seguridad de tipos y asegurarte de que la colección solo contenga modelos de un tipo específico.
Laravel hace que sea muy fácil anular el tipo de colección que se debe devolver.
Echemos un vistazo a un ejemplo. Imaginemos que tenemos un modelo AppModelsPost y cuando los recuperamos de la base de datos, queremos devolverlos dentro de una instancia de una clase AppCollectionsPostCollection personalizada.
Podemos crear un nuevo archivo app/Collections/PostCollection.php y definir nuestra clase de colección personalizada de la siguiente manera:
declare(strict_types=1);
namespace AppCollections;
use AppModelsPost;
use IlluminateSupportCollection;
/**
* @extends Collection<int, Post>
*/
class PostCollection extends Collection
{
// ...
}
En el ejemplo anterior, hemos creado una nueva clase AppCollectionsPostCollection que extiende la clase IlluminateSupportCollection de Laravel. También hemos especificado que esta colección solo contendrá instancias de la clase AppModelsPost mediante el uso del docblock. Esto es excelente para ayudar a su IDE a comprender el tipo de datos que estarán dentro de la colección.
Luego podemos actualizar nuestro modelo AppModelsPost para que devuelva una instancia de nuestra clase de colección personalizada anulando el método newCollection de la siguiente manera:
namespace AppModels;
use AppCollectionsPostCollection;
use IlluminateDatabaseEloquentModel;
class Post extends Model
{
// ...
public function newCollection(array $models = []): PostCollection
{
return new PostCollection($models);
}
}
En el ejemplo, hemos tomado la matriz de modelos AppModelsPost que se pasan al método newCollection y hemos devuelto una nueva instancia de nuestra clase AppCollectionsPostCollection personalizada.
Ahora podemos obtener nuestras publicaciones de la base de datos usando nuestra clase de colección personalizada de la siguiente manera:
use AppModelsPost;
$posts = Post::all();
// $posts es uan instancia de AppCollectionsPostCollection
#Comparación de modelos
Un problema común que veo cuando trabajo en proyectos es cómo se comparan los modelos. Esto suele ocurrir en las comprobaciones de autorización cuando se desea comprobar si un usuario puede acceder a un recurso.
Veamos algunos de los problemas más comunes y por qué probablemente debería evitar su uso.
Debe evitar el uso de === al comprobar si dos modelos son iguales. Esto se debe a que la comprobación ===, al comparar objetos, comprobará si son la misma instancia del objeto. Esto significa que incluso si dos modelos tienen los mismos datos, no se considerarán iguales si son instancias diferentes. Por lo tanto, debe evitar hacer esto, ya que es probable que devuelva falso.
Suponiendo que existe una relación de publicación en un modelo AppModelsComment y que el primer comentario en la base de datos pertenece a la primera publicación, veamos un ejemplo:
// ⚠️ Evite usar '===' para comparar modelos
$comment = AppModelsComment::first();
$post = AppModelsPost::first();
$postsAreTheSame = $comment->post === $post;
// $postsAreTheSame será falso.
También debe evitar usar == al verificar si dos modelos son iguales. Esto se debe a que la verificación ==, al comparar objetos, verificará si son una instancia de la misma clase y si tienen los mismos atributos y valores. Sin embargo, esto puede generar un comportamiento inesperado.
Tome este ejemplo:
// ⚠️ Evite usar '==' para comparar modelos
$comment = AppModelsComment::first();
$post = AppModelsPost::first();
$postsAreTheSame = $comment->post == $post;
// $postsAreTheSame será verdadero.
En el ejemplo anterior, la comprobación == devolverá verdadero porque $comment->post y $post son la misma clase y tienen los mismos atributos y valores. Pero, ¿qué sucedería si cambiáramos los atributos en el modelo $post para que fueran diferentes?
Usemos el método select para que solo tomemos los campos id y content de la tabla posts:
// ⚠️ Evite usar '==' para comparar modelos
$comment = AppModelsComment::first();
$post = AppModelsPost::query()->select(['id', 'content'])->first();
$postsAreTheSame = $comment->post == $post;
// $postsAreTheSame será falso.
Aunque $comment->post es el mismo modelo que $post, la comprobación == devolverá falso porque los modelos tienen diferentes atributos cargados. Como puedes imaginar, esto puede provocar un comportamiento inesperado que puede ser bastante difícil de rastrear, especialmente si has añadido retrospectivamente el método select a una consulta y tus pruebas empiezan a fallar.
En cambio, me gusta utilizar los métodos is y isNot que proporciona Laravel. Estos métodos compararán dos modelos y comprobarán que pertenecen a la misma clase, tienen el mismo valor de clave principal y tienen la misma conexión a la base de datos. Esta es una forma mucho más segura de comparar modelos y ayudará a reducir la probabilidad de un comportamiento inesperado.
Puedes utilizar el método is para comprobar si dos modelos son iguales:
$comment = AppModelsComment::first();
$post = AppModelsPost::query()->select(['id', 'content'])->first();
$postsAreTheSame = $comment->post->is($post);
// $postsAreTheSame será verdadero.
De manera similar, puedes usar el método isNot para comprobar si dos modelos no son iguales:
$comment = AppModelsComment::first();
$post = AppModelsPost::query()->select(['id', 'content'])->first();
$postsAreNotTheSame = $comment->post->isNot($post);
// $postsAreNotTheSame será falso.
#Utilice whereBelongsTo al crear consultas
Este último consejo es más una cuestión de gusto personal, pero considero que hace que mis consultas sean más fáciles de leer y comprender.
Cuando intentas obtener modelos de la base de datos, es posible que tenga que escribir consultas que filtren en función de las relaciones. Por ejemplo, es posible que desee obtener todos los comentarios que pertenecen a un usuario y una publicación específicos:
$user = User::first();
$post = Post::first();
$comments = Comment::query()
->where('user_id', $user->id)
->where('post_id', $post->id)
->get();
Laravel ofrece un método whereBelongsTo que puedes usar para que tus consultas sean más legibles (en mi opinión). Con este método, podríamos reescribir la consulta anterior de la siguiente manera:
$user = User::first();
$post = Post::first();
$comments = Comment::query()
->whereBelongsTo($user)
->whereBelongsTo($post)
->get();
Me gusta esta sintaxis más sencilla y creo que hace que la consulta sea más legible para los humanos. También es una excelente manera de garantizar que se está filtrando en función de la relación y el campo correctos.
Es posible que usted o su equipo prefieran utilizar el enfoque más explícito de escribir las cláusulas where. Por lo tanto, este consejo puede no ser para todos. Pero creo que, siempre que sea coherente con su enfoque, cualquiera de las dos opciones está perfectamente bien.
#Conclusion
Con suerte, este artículo debería haberle mostrado algunos consejos nuevos para trabajar con modelos de Laravel. Ahora debería poder detectar y prevenir problemas N+1, evitar el acceso a atributos faltantes, evitar el descarte silencioso de atributos y cambiar el tipo de clave principal a UUID o ULID. También debería saber cómo cambiar el campo utilizado para la vinculación del modelo de ruta, especificar el tipo de colección devuelta, comparar modelos y usar whereBelongsTo al crear consultas.
Antonio Jenaro
Web Developer
Fuente: Laravel news
Inicia la conversación
Hazte miembro de Antonio Jenaro para comenzar a comentar.
Regístrate ahora¿Ya estás registrado? Inicia sesión