Limited-Time Special Use Code to Get 30% Off Your First Buying EduBank. Find out more!

Laravel Model Tips

img
Author

Emily Harrison

Student
  • Posts: 17
  • Member since: 2024-08-26
  • Location: West Barminghum, Nyc

img 2024-08-27 - 02:01 AM

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.

Reply to the topic

Choose an attachment

Eager to Receive Special Offers & Updates on Courses?