Laravel offers a lot of powerful features that help improve our development experience (DX). But with regular releases, the stress of day-to-day work, and the advent of a large number of available features, it’s easy to miss out on some lesser-known features that can help improve our code.
This article will introduce some of my favorite Laravel model usage tips. Hopefully these tips will help you write cleaner, more efficient code and help you avoid common pitfalls.
We will first introduce how to discover and prevent N 1 query problems.
When the association is delayed loading, common N 1 query problems may occur, where N is the number of queries run to get the related model.
What does this mean? Let's look at an example. Suppose we want to get all posts from the database, iterate through them, and access the user who created the post. Our code might look like this:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
Although the code above looks good, it actually causes N 1 problems. Suppose there are 100 posts in the database. On the first line, we will run a single query to get all posts. Then, in the $post->user
loop of accessing foreach
, this will trigger a new query to get the user of the post; resulting in an additional 100 queries. This means we will run a total of 101 queries. As you might imagine, this is not good! It slows down the application and puts unnecessary pressure on the database.
As the code becomes more and more complex and features become more and more difficult to spot unless you are actively looking for these problems.
Thankfully, Laravel provides a convenient Model::preventLazyLoading()
method that you can use to help discover and prevent these N 1 problems. This method will instruct Laravel to throw an exception when lazy loading the relationship, so you can make sure that your relationship is always loaded eagerly.
To use this method, add the Model::preventLazyLoading()
method call to your AppProvidersAppServiceProvider
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 want to run the code above to get each post and access the user who created that post, we will see a IlluminateDatabaseLazyLoadingViolationException
exception thrown with the following message:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
To resolve this issue, we can update the code to eagerly load user relationships when getting posts. We can use the with
method to achieve:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
The above code will now run successfully and will only trigger two queries: one for getting all posts and the other for all users who get those posts.
How often do you try to access fields that you think exist on the model but do not exist? You might have entered an error, or you might think that there is a full_name
field, when in fact it is called name
.
Suppose we have a AppModelsUser
model with the following fields:
id
name
email
password
created_at
updated_at
What happens if we run the following code? :
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
Suppose we do not have a full_name
accessor on the model, the $name
variable will be null
. But we don't know if this is because the full_name
field is actually null
, or because we didn't get the field from the database, or because the field does not exist in the model. As you can imagine, this can lead to unexpected behavior and can sometimes be difficult to detect.
Laravel provides a Model::preventAccessingMissingAttributes()
method that you can use to help prevent this problem. This method will instruct Laravel to throw an exception when you try to access a field that does not exist on the current instance of the model.
To enable this feature, add the Model::preventAccessingMissingAttributes()
method call to your AppProvidersAppServiceProvider
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 want to run our sample code and try to access the AppModelsUser
field on the full_name
model, we will see a IlluminateDatabaseEloquentMissingAttributeException
exception thrown with the following message:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
Another benefit of using preventAccessingMissingAttributes
is that it highlights the situation where we try to read fields that exist but may not be loaded on the model. For example, suppose we have the following code:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
If we block access to missing properties, the following exception will be thrown:
$user = User::query()->first(); $name = $user->full_name;
This is very useful when updating existing queries. For example, in the past, you might have only needed a few fields in the model. However, you may be updating the functionality in the application right now and need to access another field. If this method is not enabled, you may not realize that you are trying to access fields that are not loaded yet.
It is worth noting that the preventAccessingMissingAttributes
method has been removed from the Laravel documentation (commit), but it still works. I'm not sure why it was removed, but it's a matter of attention. This may indicate that it will be deleted in the future.
(The following content is the same as the original text. In order to maintain consistency, I will keep the original text and will not rewrite it anymore)
Similar to preventAccessingMissingAttributes
, Laravel provides a preventSilentlyDiscardingAttributes
method that can help prevent unexpected behavior when updating models.
Suppose you have a AppModelsUser
model class as follows:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
As we can see, the name
, email
and password
fields are all fillable fields. But what happens if we try to update a field that does not exist on the model (e.g. full_name
) or a field that exists but is not fillable (e.g. email_verified_at
)? :
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
If we run the above code, both the full_name
and email_verified_at
fields will be ignored because they are not defined as fillable fields. But no error is thrown, so we will not know that these fields have been silently discarded.
As you expected, this can cause hard-to-find errors in the application, especially if anything else in your "update" statement has actually been updated. Therefore, we can use the preventSilentlyDiscardingAttributes
method, which will throw an exception when you try to update a field that does not exist or is not fillable on the model.
To use this method, add the Model::preventSilentlyDiscardingAttributes()
method call to your AppProvidersAppServiceProvider
class:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
The above code will force an error to be thrown.
Now, if we try to run the above sample code and update the user's first_name
and email_verified_at
fields, a IlluminateDatabaseEloquentMassAssignmentException
exception is thrown with the following message:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
It is worth noting that the preventSilentlyDiscardingAttributes
method will only highlight the unfilled fields when you use methods such as fill
or update
. If you set each property manually, it will not catch these errors. For example, let's look at the following code:
$user = User::query()->first(); $name = $user->full_name;
In the above code, the full_name
field does not exist in the database, so Laravel does not capture it for us, but at the database level. If you are using a MySQL database, you will see an error like this:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventAccessingMissingAttributes(); } }
If you want to use the three methods we mentioned earlier, you can enable them at once using the Model::shouldBeStrict()
method. This method enables the preventLazyLoading
, preventAccessingMissingAttributes
and preventSilentlyDiscardingAttributes
settings.
To use this method, add the Model::shouldBeStrict()
method call to your AppProvidersAppServiceProvider
class:
<code>属性 [full_name] 不存在或未为模型 [App\Models\User] 获取。</code>
This is equivalent to:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
Similar to the preventAccessingMissingAttributes
method, the shouldBeStrict
method has been removed from the Laravel document (commit), but it still works. This may indicate that it will be deleted in the future.
By default, the Laravel model uses an auto-incremental ID as its primary key. But sometimes you may prefer to use a universal unique identifier (UUID).
UUID is an alphanumeric string of 128 bits (or 36 characters) that can be used to uniquely identify resources. Because of how they are generated, the chances of them clashing with another UUID are extremely low. An example of a UUID is: 1fa24c18-39fd-4ff2-8f23-74ccd08462b0
.
You may want to use UUID as the primary key of your model. Alternatively, you might want to keep the auto-incremented ID to define the relationships in the application and database, but use the UUID for public-facing IDs. Using this approach can add an additional layer of security by making it harder for an attacker to guess the IDs of other resources.
For example, suppose we use an auto-incremental ID in our routing. We may have a route for accessing users, as shown below:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
If the route is not secure, an attacker can loop over the ID (e.g. - /users/1
, /users/2
, /users/3
, etc.) to try to access other users' profiles. And if we use UUID, the URL may be more like /users/1fa24c18-39fd-4ff2-8f23-74ccd08462b0
, /users/b807d48d-0d01-47ae-8bbc-59b2acea6ed3
and /users/ec1dde93-c67a-4f14-8464-c0d29c95425f
. As you might imagine, these are harder to guess.
Of course, just using UUIDs doesn't protect your applications, they're just an extra step you can take to improve security. You need to make sure you also use other security measures such as rate limiting, authentication and authorization checking.
Let's first look at how to change the primary key to a UUID.
To do this, we need to make sure our table has a column that can store UUIDs. Laravel provides a convenient $table->uuid
method that we can use in migrations.
Suppose we have this created the basic migration of the comments
table:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
As we saw in the migration, we defined a UUID field. By default, this field will be called uuid
, but you can change it by passing the column name to the uuid
method if you wish.
We then need to instruct Laravel to use the new uuid
field as the primary key of our AppModelsComment
model. We also need to add a feature that will allow Laravel to automatically generate UUIDs for us. We can do this by overwriting the $primaryKey
attribute on the model and using the IlluminateDatabaseEloquentConcernsHasUuids
attribute:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
Now you should configure the model and be ready to use UUID as the primary key. Let's take a look at this sample code:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
We can see in the dumped model that the uuid
field is populated with the UUID.
If you prefer to use auto-incremented ID for internal relationships, but use UUID for public-facing IDs, you can add a UUID field to the model.
We assume that your table has id
and uuid
fields. Since we will use the id
field as the primary key, we do not need to define the $primaryKey
attribute on the model.
We can override the IlluminateDatabaseEloquentConcernsHasUuids
method provided by the uniqueIds
feature. This method should return an array of fields for which the UUID should be generated.
Let's update our AppModelsComment
model to contain fields we call uuid
:
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 want to dump a new AppModelsComment
model, we will see that the uuid
field is populated with UUID:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
We will later explain how to update your models and routes in this article so that these UUIDs are used as your public-facing IDs in your routes.
Similar to using UUID in Laravel models, sometimes you may want to use a universal unique dictionary sort identifier (ULID).
ULID is an alphanumeric string of 128 bits (or 26 characters) that can be used to uniquely identify resources. An example of ULID is: 01J4HEAEYYVH4N2AKZ8Y1736GD
.
You can define the ULID field as you would define the UUID field. The only difference is that you should use the IlluminateDatabaseEloquentConcernsHasUlids
feature instead of updating your model to use the IlluminateDatabaseEloquentConcernsHasUuids
feature.
For example, if we want to update our AppModelsComment
model to use ULID as the primary key, we can do this:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
You may already know what routing model binding is. But just in case you don't know, let's take a quick look back.
Routing model binding allows you to automatically obtain model instances based on data passed to the Laravel application route.
By default, Laravel will route model binding using the model's primary key field (usually the id
field). For example, you might have a route for displaying individual user information:
$user = User::query()->first(); $name = $user->full_name;
The route defined in the above example will try to find users present in the database and have the provided ID. For example, suppose there is a user in the database with an ID of 1
. When you access the URL /users/1
, Laravel will automatically obtain the user with ID 1
from the database and pass it to the closure function (or controller) for operations. However, if there is no model with the provided ID in the database, Laravel will automatically return a 404 Not Found
response.
However, sometimes you may want to use different fields (rather than primary keys) to define how to retrieve a model from a database.
For example, as we mentioned earlier, you might want to use the auto-incremented ID as the primary key of the model for internal relationships. But you may want to use UUID for public-facing IDs. In this case, you may want to use the uuid
field for routing model binding, rather than the id
field.
Similarly, if you are building a blog, you may want to get your post based on the slug
field instead of the id
field. This is because the slug
field is easier to read and more SEO-friendly than the auto-incremented ID.
If you want to define fields that are applied to all routes, you can do this by defining the getRouteKeyName
method on the model. This method should return the name of the field you want to use for routing model binding.
For example, suppose we want to change all routing model bindings for the AppModelsPost
model to use the slug
field instead of the id
field. We can do this by adding a Post
method to our getRouteKeyName
model:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
This means that we can now define our routes like this:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
When we access the URL /posts/my-first-post
, Laravel will automatically get the post slug
as my-first-post
from the database and pass it to the closure function (or controller) for operations.
However, sometimes you may want to change only the fields used in a single route. For example, you might want to use the slug
field in one route for routing model binding, but use the id
field in all other routes.
We can do this by using the :field
syntax in our routing definition. For example, suppose we want to use the slug
field in a route for routing model binding. We can define our route like this:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
This now means that in this particular route, Laravel will try to get a post from the database with the provided slug
field.
When you use methods such as AppModelsUser::all()
to get multiple models from a database, Laravel will usually put them in an instance of the IlluminateDatabaseEloquentCollection
class. This class provides many useful methods for processing returned models. However, sometimes you may want to return a custom collection class instead of a default collection class.
You may want to create a custom collection for several reasons. For example, you might want to add some helper methods specific to handling models of that type. Alternatively, you might want to use it for improved type safety and make sure that the collection contains only specific types of models.
Laravel makes it very easy to override the collection type that should be returned.
Let's look at an example. Suppose we have a AppModelsPost
model, and when we get them from the database, we want to return them to an instance of the custom AppCollectionsPostCollection
class.
We can create a new app/Collections/PostCollection.php
file and define our custom collection class like this:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
In the example above, we created a new AppCollectionsPostCollection
class that extends Laravel's IlluminateSupportCollection
class. We also specified that this collection will only contain instances of the AppModelsPost
class, using docblock. This is useful for helping your IDE understand the types of data that will be included in the collection.
We can then update our AppModelsPost
model to return an instance of the custom collection class by overriding the newCollection
method as follows:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
In this example, we get the newCollection
model array passed to the AppModelsPost
method and return a new instance of the custom AppCollectionsPostCollection
class.
Now we can use the custom collection class to get our posts from the database, as shown below:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
A common problem I have when working on a project is how to compare models. This is usually in authorization checks, when you want to check if the user has access to the resource.
Let's look at some common pitfalls and why you should probably avoid them.
You should avoid using ===
when checking whether the two models are the same. This is because ===
Checks when comparing objects, checks whether they are instances of the same object. This means that even if the two models have the same data, they will not be considered the same if they are different instances. Therefore, you should avoid doing this as it will most likely return false
.
Suppose that a AppModelsComment
relationship exists on the model, and the first comment in the database belongs to the first post, let's look at an example: post
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
when checking whether the two models are the same. This is because ==
checks when comparing objects will check whether they are instances of the same class and whether they have the same properties and values. However, this can lead to unexpected behavior. ==
$user = User::query()->first(); $name = $user->full_name;
check will return ==
because true
and $comment->post
are the same class and have the same properties and values. But what happens if we change the properties in the $post
model to make them different? $post
method so that we only get the select
and posts
fields from the id
table: content
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
Even if $comment->post
is the same model as $post
, the ==
check will return false
because the model has different loaded properties. As you can imagine, this can lead to some unintelligible behavior that is difficult to trace, especially if you have retroactively added the select
method to the query and your tests start to fail.
Instead, I like to use the is
and isNot
methods provided by Laravel. These methods compare the two models and check if they belong to the same class, have the same primary key value, and have the same database connection. This is a safer way to compare models and will help reduce the likelihood of unexpected behavior.
You can use the is
method to check if the two models are the same:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
Similarly, you can use the isNot
method to check if the two models are different:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
whereBelongsTo
The last trick is more like a personal preference, but I found it makes my queries easier to read and understand.
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
whereBelongsTo
$user = User::query()->first(); $name = $user->full_name;
I like this syntactic sugar and feel it makes the query easier to read. This is also a great way to ensure you filter based on the correct relationships and fields. where
ConclusionwhereBelongsTo
The above is the detailed content of Laravel Model Tips. For more information, please follow other related articles on the PHP Chinese website!