Laravel Model Tips

Laravel provides a huge amount of cool features that help improve our development experience (DX). But with the regular releases, stresses of day-to-day work, and the vast amount of features available, it's easy to miss some of the lesser-known features that can help improve our code. In this article, I'm going to cover some of my favourite tips for working with Laravel models. Hopefully, these tips will help you write cleaner, more efficient code and help you avoid common pitfalls. Spotting and Preventing N+1 Issues The first tip we'll look at is how to spot and prevent N+1 queries. N+1 queries are a common issue that can occur when lazy loading relationships, where N is the number of queries that are run to fetch the related models. But what does this mean? Let's take a look at an example. Imagine we want to fetch every post from the database, loop through them, and access the user that created the post. Our code might lo

Laravel Model Tips

INCREASE YOUR SALES WITH NGN1,000 TODAY!

Advertise on doacWeb

WhatsApp: 09031633831

To reach more people from NGN1,000 now!

INCREASE YOUR SALES WITH NGN1,000 TODAY!

Advertise on doacWeb

WhatsApp: 09031633831

To reach more people from NGN1,000 now!

INCREASE YOUR SALES WITH NGN1,000 TODAY!

Advertise on doacWeb

WhatsApp: 09031633831

To reach more people from NGN1,000 now!

Laravel Model Tips

Laravel provides a huge amount of cool features that help improve our development experience (DX). But with the regular releases, stresses of day-to-day work, and the vast amount of features available, it's easy to miss some of the lesser-known features that can help improve our code.

In this article, I'm going to cover some of my favourite tips for working with Laravel models. Hopefully, these tips will help you write cleaner, more efficient code and help you avoid common pitfalls.

Spotting and Preventing N+1 Issues

The first tip we'll look at is how to spot and prevent N+1 queries.

N+1 queries are a common issue that can occur when lazy loading relationships, where N is the number of queries that are run to fetch the related models.

But what does this mean? Let's take a look at an example. Imagine we want to fetch every post from the database, loop through them, and access the user that created the post. Our code might look something like this:

$posts = Post::all();

foreach ($posts as $post) {
    // Do something with the post...

    // Try and access the post's user
    echo $post->user->name;
}

Although the code above looks fine, it's actually going to cause an N+1 issue. Say there are 100 posts in the database. On the first line, we'll run a single query to fetch all the posts. Then inside the foreach loop when we're accessing $post->user, this will trigger a new query to fetch the user for that post; resulting in an additional 100 queries. This means we'd run 101 queries in total. As you can imagine, this isn't great! It can slow down your application and put unnecessary strain on your database.

As your code becomes more complex and features grow, it can be hard to spot these issues unless you're actively looking out for them.

Thankfully, Laravel provides a handy Model::preventLazyLoading() method that you can use to help spot and prevent these N+1 issues. This method will instruct Laravel to throw an exception whenever a relationship is lazy-loaded, so you can be sure that you're always eager loading your relationships.

To use this method, you can add the Model::preventLazyLoading() method call to your App\Providers\AppServiceProvider class:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

Now, if we were to run our code from above to fetch every post and access the user that created the post, we'd see an Illuminate\Database\LazyLoadingViolationException exception thrown with the following message:

Attempted to lazy load [user] on model [App\Models\Post] but lazy loading is disabled.

To fix this issue, we can update our code to eager load the user relationship when fetching the posts. We can do this by using the with method:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // Do something with the post...

    // Try and access the post's user
    echo $post->user->name;
}

The code above will now successfully run and will only trigger two queries: one to fetch all the posts and one to fetch all the users for those posts.

Prevent Accessing Missing Attributes

How often have you tried to access a field that you thought existed on a model but didn't? You might have made a typo, or maybe you thought there was a full_name field when it was actually called name.

Imagine we have an App\Models\User model with the following fields:

  • id
  • name
  • email
  • password
  • created_at
  • updated_at

What would happen if we ran the following code?:

$user = User::query()->first();
    
$name = $user->full_name;

Assuming we don't have a full_name accessor on the model, the $name variable would be null. But we wouldn't know whether this is because the full_name field actually is null, because we haven't fetched the field from the database, or because the field doesn't exist on the model. As you can imagine, this can cause unexpected behaviour that can sometimes be difficult to spot.

Laravel provides a Model::preventAccessingMissingAttributes() method that you can use to help prevent this issue. This method will instruct Laravel to throw an exception whenever you try to access a field that doesn't exist on the current instance of the model.

To enable this, you can add the Model::preventAccessingMissingAttributes() method call to your App\Providers\AppServiceProvider class:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventAccessingMissingAttributes();
    }
}

Now if we were to run our example code and attempt to access the full_name field on the App\Models\User model, we'd see an Illuminate\Database\Eloquent\MissingAttributeException exception thrown with the following message:

The attribute [full_name] either does not exist or was not retrieved for model [App\Models\User].

An additional benefit of using preventAccessingMissingAttributes is that it can highlight when we're trying to read a field that exists on the model but that we might not have loaded. For example, let's imagine we have the following code:

$user = User::query()
    ->select(['id', 'name'])
    ->first();

$user->email;

If we have prevented missing attributes from being accessed, the following exception would be thrown:

The attribute [email] either does not exist or was not retrieved for model [App\Models\User].

This can be incredibly useful when updating existing queries. For example, in the past, you may have only needed a few fields from a model. But maybe you're now updating the feature in your application and need access to another field. Without having this method enabled, you might not realise that you're trying to access a field that hasn't been loaded.

It's worth noting that the preventAccessingMissingAttributes method has been removed from the Laravel documentation (commit), but it still works. I'm not sure of the reason for its removal, but it's something to be aware of. It might be an indication that it will be removed in the future.

Prevent Silently Discarding Attributes

Similar to preventAccessingMissingAttributes, Laravel provides a preventSilentlyDiscardingAttributes method that can help prevent unexpected behaviour when updating models.

Imagine you have an App\Models\User model class like so:

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
    
    // ...
}

As we can see, the name, email, and password fields are all fillable fields. But what would happen if we tried to update a non-existent field on the model (such as full_name) or a field that exists but isn't fillable (such as 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...
]);

If we were to run the code above, both the full_name and email_verified_at fields would be ignored because they haven't been defined as fillable fields. But no errors would be thrown, so we would be unaware that the fields have been silently discarded.

As you'd expect, this could lead to hard-to-spot bugs in your application, especially if any other in your "update" statement were in fact updated. So we can use the preventSilentlyDiscardingAttributes method which will throw an exception whenever you try to update a field that doesn't exist on the model or isn't fillable.

To use this method, you can add the Model::preventSilentlyDiscardingAttributes() method call to your App\Providers\AppServiceProvider class:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventSilentlyDiscardingAttributes();
    }
}

The above would force an error to be thrown.

Now if we were to try and run our example code from above and update the user's first_name and email_verified_at fields, an Illuminate\Database\Eloquent\MassAssignmentException exception would be thrown with the following message:

Add fillable property [full_name, email_verified_at] to allow mass assignment on [App\Models\User].

It's worth noting that the preventSilentlyDiscardingAttributes method will only highlight unfillable fields when you're using a method such as fill or update. If you're manually setting each property, it won't catch these errors. For example, let's take the following code:

$user = User::query()->first();

$user->full_name = 'Ash';
$user->email_verified_at = now();

$user->save();

In the code above, the full_name field doesn't exist in the database, so rather than Laravel catching it for us, it would be caught at the database level. If you were using a MySQL database, you'd see an error like this:

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)

Enable Strict Mode for Models

If you'd like to use the three methods that we've mentioned so far, you can enable them all at once using the Model::shouldBeStrict() method. This method will enable the preventLazyLoading, preventAccessingMissingAttributes, and preventSilentlyDiscardingAttributes settings.

To use this method, you can add the Model::shouldBeStrict() method call to your App\Providers\AppServiceProvider class:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::shouldBeStrict();
    }
}

This is the equivalent of:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
        Model::preventSilentlyDiscardingAttributes();
        Model::preventAccessingMissingAttributes();
    }
}

Similar to the preventAccessingMissingAttributes method, the shouldBeStrict method has been removed from the Laravel documentation (commit) but still works. This might be an indication that it will be removed in the future.

Using UUIDs

By default, Laravel models use auto-incrementing IDs as their primary key. But there may be times when you'd prefer to use universally unique identifiers (UUIDs).

UUIDs are 128-bit (or 36-character) alphanumeric strings that can be used to uniquely identify resources. Due to how they're generated, it's highly unlikely that they'll collide with another UUID. An example UUID is: 1fa24c18-39fd-4ff2-8f23-74ccd08462b0.

You may want to use a UUID as the primary key for a model. Or, you might want to keep your auto-incrementing IDs for defining relationships within your application and database, but use UUIDs for public-facings IDs. Using this approach can add an extra layer of security by making it harder for attackers to guess the IDs of other resources.

For example, say we are using auto-incrementing IDs in a route. We might have a route for accessing a user like so:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('/users/{user}', function (User $user) {
    dd($user->toArray());
});

An attacker could loop through the IDs (e.g. - /users/1, /users/2, /users/3, etc.) in an attempt to try and access other users' information if the routes aren't secure. Whereas if we used UUIDs, the URLs might look more like /users/1fa24c18-39fd-4ff2-8f23-74ccd08462b0, /users/b807d48d-0d01-47ae-8bbc-59b2acea6ed3, and /users/ec1dde93-c67a-4f14-8464-c0d29c95425f. As you can imagine, these are much harder to guess.

Of course, just using UUIDs isn't enough to protect your applications, they're just an additional step you can take to improve the security. You need to make sure you're also using other security measures like rate limiting, authentication, and authorization checks.

Use UUIDs as the Primary Key

We'll first start by looking at how to change the primary key to UUIDs.

To do this, we'll need to make sure our table has a column that's capable of storing UUIDs. Laravel provides a handy $table->uuid method that we can use in our migrations.

Imagine we have this basic migration that creates a comments table:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

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();
        });
    }

    // ...
}

As we can see in the migration, we've defined a UUID field. By default, this field will be called uuid, but you can change this by passing a column name to the uuid method if you'd like.

We then need to instruct Laravel to use the new uuid field as the primary key for our App\Models\Comment model. We also need to add a trait that will allow Laravel to generate UUIDs automatically for us. We can do this by overriding the $primaryKey property on our model and using the Illuminate\Database\Eloquent\Concerns\HasUuids trait:

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    use HasUuids;

    protected $primaryKey = 'uuid';
}

The model should now be configured and ready to use UUIDs as the primary key. Take this example code:

use App\Models\Comment;
use App\Models\Post;
use App\Models\User;

$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"
// ]

We can see in the dumped model that the uuid field has been populated with a UUID.

Add a UUID Field to a Model

If you'd prefer to keep your auto-incrementing IDs for internal relationships but use UUIDs for public-facing IDs, you can add a UUID field to your model.

We'll assume your table has id and uuid fields. Since we'll be using the id field as the primary key, we won't need to define the $primaryKey property on our model.

We can override the uniqueIds method that's made available via the Illuminate\Database\Eloquent\Concerns\HasUuids trait. This method should return an array of the fields that should have UUIDs generated for them.

Let's update our App\Models\Comment model to include the field that we've called uuid:

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    use HasUuids;

    public function uniqueIds(): array
    {
        return ['uuid'];
    }
}

Now if we were to dump a new App\Models\Comment model, we'd see that the uuid field has been populated with a 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"
// ]

We'll take a look later in this article at how you can update your models and routes to use these UUIDs as your public-facing IDs in your routes.

Using ULIDs

Similar to using UUIDs in your Laravel models, you may sometimes want to use universally unique lexicographically sortable identifiers (ULIDs).

ULIDs are 128-bit (or 26-character) alphanumeric strings that can be used to uniquely identify resources. An example ULID is: 01J4HEAEYYVH4N2AKZ8Y1736GD.

You can define ULID fields in the exact same as you would UUID fields. The only difference is that instead of updating your model to use the Illuminate\Database\Eloquent\Concerns\HasUuids trait, you should use the Illuminate\Database\Eloquent\Concerns\HasUlids trait.

For example, if we wanted to update our App\Models\Comment model to use ULIDs as the primary key, we could do so like this:

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    use HasUlids;
}

Changing the Field Used for Route Model Binding

You'll likely already know what route model binding is. But just in case you don't, let's quickly go over it.

Route model binding allows you to automatically fetch instances of models based on data passed to your Laravel app's routes.

By default, Laravel will use your model's primary key field (typically an id field) for route model binding. For example, you might have a route for displaying the information for a single user:

use App\Models\User;
use Illuminate\Support\Facades\Route;

Route::get('/users/{user}', function (User $user) {
    dd($user->toArray());
});

The route defined in the example above will attempt to find a user that exists in the database with the provided ID. For example, let's say a user with an ID of 1 exists in the database. When you visit the URL /users/1, Laravel will automatically fetch the user with an ID of 1 from the database and pass it to the closure function (or controller) for acting on. But if a model doesn't exist with the provided ID, Laravel will automatically return a 404 Not Found response.

But there may be times when you'd like to use a different field (other than your primary key) to define how a model is retrieved from the database.

For example, as we've previously mentioned, you may want to use auto-incrementing IDs as your model's primary keys for internal relationships. But you might want to use UUIDs for public-facing IDs. In this case, you might want to use the uuid field for route model binding instead of the id field.

Similarly, if you're building a blog, you may want to fetch your posts based on a slug field instead of the id field. This is because a slug field is more human-readable and SEO-friendly than an auto-incrementing ID.

Changing the Field for All Routes

If you'd like to define the field that should be used for all routes, you can do so by defining a getRouteKeyName method on your model. This method should return the name of the field you'd like to use for route model binding.

For example, imagine we want to change all route model binding for an App\Models\Post model to use the slug field instead of the id field. We can do this by adding a getRouteKeyName method to our Post model:

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    public function getRouteKeyName()
    {
        return 'slug';
    }

    // ...
}

This means we could now define our routes like so:

use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts/{post}', function (Post $post) {
    dd($post->toArray());
});

And when we visit the URL /posts/my-first-post, Laravel will automatically fetch the post with a slug of my-first-post from the database and pass it to the closure function (or controller) for acting on.

Changing the Field for Single Routes

However, there may be times when you only want to change the field used in a single route. For example, you might want to use the slug field for route model binding in one route, but the id field in all other routes.

We can do this by using the :field syntax in our route definition. For example, let's say we want to use the slug field for route model binding in a single route. We can define our route like so:

Route::get('/posts/{post:slug}', function (Post $post) {
    dd($post->toArray());
});

This now means in this specific route, Laravel will attempt to fetch the post with the provided slug field from the database.

Use Custom Model Collections

When you fetch multiple models from the database using a method such as App\Models\User::all(), Laravel will typically put them inside an instance of the Illuminate\Database\Eloquent\Collection class. This class provides a lot of useful methods for working with the returned models. However, there may be times when you want to return a custom collection class instead of the default one.

You might want to create a custom collection for a few reasons. For instance, you might want to add some helper methods that are specific to dealing with that type of model. Or, maybe you want to use it for improved type safety and be sure that the collection will only contain models of a specific type.

Laravel makes it really easy to override the type of collection that should be returned.

Let's take a look at an example. Let's imagine we have an App\Models\Post model and when we fetch them from the database, we want to return them inside an instance of a custom App\Collections\PostCollection class.

We can create a new app/Collections/PostCollection.php file and define our custom collection class like so:

declare(strict_types=1);

namespace App\Collections;

use App\Models\Post;
use Illuminate\Support\Collection;

/**
 * @extends Collection
 */
class PostCollection extends Collection
{
    // ...
}

In the example above, we've created a new App\Collections\PostCollection class that extends Laravel's Illuminate\Support\Collection class. We've also specified that this collection will only contain instances of the App\Models\Post class using the docblock. This is great for helping your IDE understand the type of data that will be inside the collection.

We can then update our App\Models\Post model to return an instance of our custom collection class by overriding the newCollection method like so:

namespace App\Models;

use App\Collections\PostCollection;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    // ...

    public function newCollection(array $models = []): PostCollection
    {
        return new PostCollection($models);
    }
}

In the example, we've taken the array of App\Models\Post models that are passed to the newCollection method and returned a new instance of our custom App\Collections\PostCollection class.

Now we can fetch our posts from the database using our custom collection class like so:

use App\Models\Post;

$posts = Post::all();

// $posts is an instance of App\Collections\PostCollection

Comparing Models

A common issue that I see when working on projects is how models are compared. This is usually within authorization checks when you want to check whether a user can access a resource.

Let's look at some of the common gotchas and why you should probably avoid using them.

You should avoid using === when checking if two models are the same. This is because the === check, when comparing objects, will check if they are the same instance of the object. This means that even if two models have the same data, they won't be considered the same if they are different instances. So you should avoid doing this, as it will likely return false.

Assuming a post relationship exists on an App\Models\Comment model and that the first comment in the database belongs to the first post, let's look at an example:

// ⚠️ Avoid using `===` to compare models

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::first();

$postsAreTheSame = $comment->post === $post;

// $postsAreTheSame will be false.

You should also avoid using == when checking if two models are the same. This is because the == check, when comparing objects, will check if they are an instance of the same class and if they have the same attributes and values. However, this can lead to unexpected behaviour.

Take this example:

// ⚠️ Avoid using `==` to compare models

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::first();

$postsAreTheSame = $comment->post == $post;

// $postsAreTheSame will be true.

In the example above, the == check will return true because $comment->post and $post are the same class and have the same attributes and values. But what would happen if we changed the attributes in the $post model so they were different?

Let's use the select method so we only grab the id and content fields from the posts table:

// ⚠️ Avoid using `==` to compare models

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();

$postsAreTheSame = $comment->post == $post;

// $postsAreTheSame will be false.

Even though $comment->post is the same model as $post, the == check will return false because the models have different attributes loaded. As you can imagine, this can lead to some unexpected behaviour that can be pretty difficult to track down, especially if you've retrospectively added the select method to a query and your tests start failing.

Instead, I like to use the is and isNot methods that Laravel provides. These methods will compare two models and check they belong to the same class, have the same primary key value, and have the same database connection. This is a much safer way to compare models and will help to reduce the likelihood of unexpected behaviour.

You can use the is method to check if two models are the same:

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();

$postsAreTheSame = $comment->post->is($post);

// $postsAreTheSame will be true.

Similarly, you can use the isNot method to check if two models are not the same:

$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();

$postsAreNotTheSame = $comment->post->isNot($post);

// $postsAreNotTheSame will be false.

Use whereBelongsTo When Building Queries

This last tip is more of a personal preference, but I find that it makes my queries easier to read and understand.

When attempting to fetch models from the database, you might find yourself writing queries that filter based on relationships. For example, you might want to fetch all comments that belong to a specific user and post:

$user = User::first();
$post = Post::first();

$comments = Comment::query()
    ->where('user_id', $user->id)
    ->where('post_id', $post->id)
    ->get();

Laravel provides a whereBelongsTo method that you can use to make your queries more readable (in my opinion). Using this method, we could rewrite the query above like so:

$user = User::first();
$post = Post::first();

$comments = Comment::query()
    ->whereBelongsTo($user)
    ->whereBelongsTo($post)
    ->get();

I like this syntactic sugar and feel like it makes the query more human-readable. It's also a great way to ensure that you're filtering based on the correct relationship and field.

You, or your team, may prefer to use the more explicit approach of writing out the where clauses. So this tip might not be for everyone. But I think as long as you're consistent with your approach, either way is perfectly fine.

Conclusion

Hopefully, this article should have shown you some new tips for working with Laravel models. You should now be able to spot and prevent N+1 issues, prevent accessing missing attributes, prevent silently discarding attributes, and change the primary key type to UUIDs or ULIDs. You should also know how to change the field used for route model binding, specify the type of collection returned, compare models, and use whereBelongsTo when building queries.


The post Laravel Model Tips appeared first on Laravel News.

Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.

What's Your Reaction?

like

dislike

love

funny

angry

sad

wow