Laravel 模型提示
2024 年 8 月 22 日
Laravel 提供了大量炫酷的功能,有助于改善我们的开发体验 (DX)。但由于定期发布、日常工作压力以及大量可用功能,我们很容易忽略一些鲜为人知的、有助于改善我们代码的功能。
在本文中,我将介绍一些我最喜欢的使用 Laravel 模型的技巧。希望这些技巧能帮助您编写更简洁、更高效的代码,并帮助您避免常见的陷阱。
发现并预防 N+1 问题
我们要看的第一个技巧是如何发现和预防 N+1 查询 。
N+1 查询是延迟加载关系时可能出现的常见问题,其中 N 是运行以获取相关模型的查询数。
但这是什么意思呢?让我们看一个例子。假设我们想从数据库中获取每篇帖子,循环遍历它们,并访问创建帖子的用户。我们的代码可能看起来像这样:
$posts = Post::all();
foreach ($posts as $post) {
// Do something with the post...
// Try and access the post's user
echo $post->user->name;
}
虽然上面的代码看起来不错,但实际上会导致 N+1 问题。假设数据库中有 100 个帖子。在第一行,我们将运行一个查询来获取所有帖子。然后在
foreach
当我们访问时循环
$post->user
,这将触发一个新查询来获取该帖子的用户;从而产生额外的 100 个查询。这意味着我们总共运行了 101 个查询。你可以想象,这可不是什么好事!它会减慢你的应用程序速度并给你的数据库带来不必要的压力。
随着代码变得越来越复杂并且功能越来越多,除非您主动寻找这些问题,否则很难发现这些问题。
值得庆幸的是,Laravel 提供了一个方便的
Model::preventLazyLoading()
您可以使用此方法来帮助发现和预防这些 N+1 问题。此方法将指示 Laravel 在关系延迟加载时抛出异常,因此您可以确保始终急切加载关系。
要使用此方法,您可以添加
Model::preventLazyLoading()
方法调用到你的
App\Providers\AppServiceProvider
班级:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading();
}
}
现在,如果我们运行上面的代码来获取每篇帖子并访问创建帖子的用户,我们将看到
Illuminate\Database\LazyLoadingViolationException
抛出异常并显示以下消息:
Attempted to lazy load [user] on model [App\Models\Post] but lazy loading is disabled.
为了解决这个问题,我们可以更新代码,在获取帖子时预先加载用户关系。我们可以通过使用
with
方法:
$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;
}
上述代码现在可以成功运行,并且只会触发两个查询:一个用于获取所有帖子,另一个用于获取这些帖子的所有用户。
防止访问缺失的属性
您是否经常尝试访问您认为存在于模型中的字段,但实际上却不存在?您可能输入了错误,或者您认为存在
full_name
当它实际被调用时
name
。
假设我们有一个
App\Models\User
具有以下字段的模型:
id
name
email
password
created_at
updated_at
如果我们运行以下代码会发生什么?:
$user = User::query()->first();
$name = $user->full_name;
假设我们没有
full_name
模型上的访问器,
$name
变量将是
null
但我们不知道这是否是因为
full_name
领域实际上是
null
,因为我们还没有从数据库中获取该字段,或者因为该字段在模型中不存在。你可以想象,这可能会导致意外行为,有时很难发现。
Laravel 提供了
Model::preventAccessingMissingAttributes()
您可以使用此方法来帮助防止出现此问题。此方法将指示 Laravel 在您尝试访问模型当前实例上不存在的字段时抛出异常。
为了实现此功能,您可以添加
Model::preventAccessingMissingAttributes()
方法调用到你的
App\Providers\AppServiceProvider
班级:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventAccessingMissingAttributes();
}
}
现在,如果我们运行示例代码并尝试访问
full_name
字段上的
App\Models\User
模型,我们会看到
Illuminate\Database\Eloquent\MissingAttributeException
抛出异常并显示以下消息:
The attribute [full_name] either does not exist or was not retrieved for model [App\Models\User].
使用的另一个好处是
preventAccessingMissingAttributes
是它可以在我们尝试读取模型上存在但可能尚未加载的字段时突出显示。例如,假设我们有以下代码:
$user = User::query()
->select(['id', 'name'])
->first();
$user->email;
如果我们阻止访问缺失的属性,则会抛出以下异常:
The attribute [email] either does not exist or was not retrieved for model [App\Models\User].
这在更新现有查询时非常有用。例如,过去您可能只需要模型中的几个字段。但也许您现在正在更新应用程序中的功能并需要访问另一个字段。如果不启用此方法,您可能不会意识到您正在尝试访问尚未加载的字段。
值得注意的是
preventAccessingMissingAttributes
方法已从 Laravel 文档中删除(
犯罪
),但它仍然有效。我不确定它被删除的原因,但这是需要注意的事情。这可能预示着它将来会被删除。
防止默默丢弃属性
如同
preventAccessingMissingAttributes
,Laravel 提供了
preventSilentlyDiscardingAttributes
这种方法可以帮助防止更新模型时出现意外行为。
想象一下你有一个
App\Models\User
模型类如下:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
];
// ...
}
我们可以看到,
name
,
email
, 和
password
字段都是可填充字段。但是如果我们尝试更新模型中不存在的字段(例如
full_name
)或存在但无法填写的字段(例如
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...
]);
如果我们运行上面的代码,
full_name
和
email_verified_at
字段将被忽略,因为它们尚未被定义为可填充字段。但不会抛出任何错误,因此我们不会意识到这些字段已被默默丢弃。
正如您所预料的,这可能会导致您的应用程序中出现难以发现的错误,尤其是当您的“更新”语句中的任何其他语句实际上已更新时。因此,我们可以使用
preventSilentlyDiscardingAttributes
每当您尝试更新模型上不存在或不可填充的字段时,方法都会引发异常。
要使用此方法,您可以添加
Model::preventSilentlyDiscardingAttributes()
方法调用到你的
App\Providers\AppServiceProvider
班级:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes();
}
}
上述操作将强制引发错误。
现在,如果我们尝试运行上面的示例代码并更新用户的
first_name
和
email_verified_at
字段,
Illuminate\Database\Eloquent\MassAssignmentException
将引发异常并显示以下消息:
Add fillable property [full_name, email_verified_at] to allow mass assignment on [App\Models\User].
值得注意的是
preventSilentlyDiscardingAttributes
方法只会在您使用以下方法时突出显示未填写的字段
fill
或者
update
。如果您手动设置每个属性,它将不会捕获这些错误。例如,让我们以以下代码为例:
$user = User::query()->first();
$user->full_name = 'Ash';
$user->email_verified_at = now();
$user->save();
在上面的代码中,
full_name
字段在数据库中不存在,因此 Laravel 不会帮我们捕获该错误,而是会在数据库级别捕获该错误。如果您使用的是 MySQL 数据库,则会看到如下错误:
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)
为模型启用严格模式
如果您想使用我们到目前为止提到的三种方法,您可以使用
Model::shouldBeStrict()
方法。此方法将启用
preventLazyLoading
,
preventAccessingMissingAttributes
, 和
preventSilentlyDiscardingAttributes
设置。
要使用此方法,您可以添加
Model::shouldBeStrict()
方法调用到你的
App\Providers\AppServiceProvider
班级:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::shouldBeStrict();
}
}
这相当于:
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading();
Model::preventSilentlyDiscardingAttributes();
Model::preventAccessingMissingAttributes();
}
}
类似于
preventAccessingMissingAttributes
方法,
shouldBeStrict
方法已从 Laravel 文档中删除(
犯罪
),但仍有效。这可能表明它将来会被删除。
使用 UUID
默认情况下,Laravel 模型使用自动递增 ID 作为主键。但有时你可能更喜欢使用 通用唯一标识符 (UUID) 。
UUID 是 128 位(或 36 个字符)字母数字字符串,可用于唯一标识资源。由于生成方式的原因,它们不太可能与其他 UUID 冲突。UUID 示例如下:
1fa24c18-39fd-4ff2-8f23-74ccd08462b0
。
您可能希望使用 UUID 作为模型的主键。或者,您可能希望保留自动递增 ID 来定义应用程序和数据库内的关系,但使用 UUID 作为面向公众的 ID。使用这种方法可以增加一层额外的安全性,使攻击者更难猜测其他资源的 ID。
例如,假设我们在路由中使用自动递增 ID。我们可能有一个访问用户的路由,如下所示:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('/users/{user}', function (User $user) {
dd($user->toArray());
});
攻击者可以循环遍历 ID(例如 -
/users/1
,
/users/2
,
/users/3
等),如果路由不安全,则尝试访问其他用户的信息。而如果我们使用 UUID,URL 可能看起来更像
/users/1fa24c18-39fd-4ff2-8f23-74ccd08462b0
,
/users/b807d48d-0d01-47ae-8bbc-59b2acea6ed3
, 和
/users/ec1dde93-c67a-4f14-8464-c0d29c95425f
。正如你所想象的,这些是很难猜测的。
当然,仅使用 UUID 不足以保护您的应用程序,它们只是您可以采取的提高安全性的额外步骤。您需要确保还使用其他安全措施,例如速率限制、身份验证和授权检查。
使用 UUID 作为主键
我们首先看看如何将主键更改为 UUID。
为此,我们需要确保我们的表有一个能够存储 UUID 的列。Laravel 提供了一个方便的
$table->uuid
我们可以在迁移中使用的方法。
想象一下,我们有这个基本的迁移,它创建了一个
comments
桌子:
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();
});
}
// ...
}
正如我们在迁移中看到的,我们定义了一个 UUID 字段。默认情况下,此字段将被称为
uuid
,但您可以通过将列名称传递给
uuid
方法。
然后我们需要指示 Laravel 使用新的
uuid
字段作为我们的主键
App\Models\Comment
模型。我们还需要添加一个特征,让 Laravel 自动为我们生成 UUID。我们可以通过覆盖
$primaryKey
在我们的模型上使用
Illuminate\Database\Eloquent\Concerns\HasUuids
特征:
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
use HasUuids;
protected $primaryKey = 'uuid';
}
现在模型应该已配置完毕,可以使用 UUID 作为主键。以下是示例代码:
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"
// ]
我们可以在倾倒模型中看到
uuid
字段已填充 UUID。
向模型添加 UUID 字段
如果您希望保留自动递增 ID 用于内部关系,但使用 UUID 作为面向公众的 ID,则可以向模型添加 UUID 字段。
我们假设你的桌子上有
id
和
uuid
字段。因为我们将使用
id
字段作为主键,我们不需要定义
$primaryKey
我们的模型上的属性。
我们可以覆盖
uniqueIds
该方法可通过
Illuminate\Database\Eloquent\Concerns\HasUuids
特征。此方法应返回应为其生成 UUID 的字段数组。
让我们更新我们的
App\Models\Comment
模型包含我们调用的字段
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'];
}
}
现在如果我们要倾倒一个新的
App\Models\Comment
模型,我们会看到
uuid
字段已填充 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"
// ]
我们将在本文后面介绍如何更新模型和路线,以使用这些 UUID 作为路线中面向公众的 ID。
使用 ULID
与在 Laravel 模型中使用 UUID 类似,有时你可能想要使用 通用唯一按字典顺序排序标识符 (ULID) 。
ULID 是 128 位(或 26 个字符)字母数字字符串,可用于唯一标识资源。ULID 示例如下:
01J4HEAEYYVH4N2AKZ8Y1736GD
。
您可以像定义 UUID 字段一样定义 ULID 字段。唯一的区别是,您无需更新模型以使用
Illuminate\Database\Eloquent\Concerns\HasUuids
特征,你应该使用
Illuminate\Database\Eloquent\Concerns\HasUlids
特征。
例如,如果我们想更新我们的
App\Models\Comment
模型使用 ULID 作为主键,我们可以这样做:
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
use HasUlids;
}
更改用于路由模型绑定的字段
您可能已经知道什么是路由模型绑定。但以防万一您不知道,让我们快速回顾一下。
路由模型绑定允许您根据传递给 Laravel 应用程序路由的数据自动获取模型实例。
默认情况下,Laravel 将使用模型的主键字段(通常是
id
字段)进行路由模型绑定。例如,你可能有一个用于显示单个用户信息的路由:
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('/users/{user}', function (User $user) {
dd($user->toArray());
});
上面示例中定义的路由将尝试查找数据库中具有所提供 ID 的用户。例如,假设 ID 为
1
存在于数据库中。当您访问 URL 时
/users/1
,Laravel 将自动获取 ID 为
1
从数据库中获取数据并将其传递给闭包函数(或控制器)进行操作。但如果不存在具有所提供 ID 的模型,Laravel 将自动返回
404 Not Found
回复。
但有时您可能想使用不同的字段(除主键之外)来定义如何从数据库中检索模型。
例如,正如我们之前提到的,您可能希望使用自动递增 ID 作为模型的内部关系的主键。但您可能希望使用 UUID 作为面向公众的 ID。在这种情况下,您可能希望使用
uuid
路由模型绑定的字段,而不是
id
场地。
类似地,如果你正在创建一个博客,你可能希望基于以下方式获取你的帖子:
slug
字段,而不是
id
场。这是因为
slug
字段比自动递增的 ID 更易于人性化且更有利于 SEO。
更改所有路线的字段
如果你想定义应该用于所有路线的字段,你可以通过定义
getRouteKeyName
方法。此方法应返回您要用于路由模型绑定的字段的名称。
例如,假设我们想要更改所有路由模型绑定
App\Models\Post
模型使用
slug
字段,而不是
id
字段。我们可以通过添加
getRouteKeyName
方法
Post
模型:
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';
}
// ...
}
这意味着我们现在可以像这样定义我们的路线:
use App\Models\Post;
use Illuminate\Support\Facades\Route;
Route::get('/posts/{post}', function (Post $post) {
dd($post->toArray());
});
当我们访问 URL 时
/posts/my-first-post
,Laravel 将自动使用
slug
的
my-first-post
从数据库中获取数据并将其传递给闭包函数(或控制器)进行操作。
更改单程路线的字段
但是,有时您可能只想更改单个路由中使用的字段。例如,您可能想使用
slug
字段用于一条路线中的路线模型绑定,但
id
所有其他路线上的领域。
我们可以使用
:field
语法。例如,假设我们想使用
slug
字段用于在单个路由中绑定路由模型。我们可以像这样定义我们的路由:
Route::get('/posts/{post:slug}', function (Post $post) {
dd($post->toArray());
});
这意味着在这个特定的路由中,Laravel 将尝试使用提供的
slug
来自数据库的字段。
使用自定义模型集合
当你使用如下方法从数据库获取多个模型时
App\Models\User::all()
,Laravel 通常会将它们放在
Illuminate\Database\Eloquent\Collection
类。此类提供了很多有用的方法来处理返回的模型。但是,有时您可能希望返回自定义集合类,而不是默认集合类。
您可能出于某些原因想要创建自定义集合。例如,您可能想要添加一些专门用于处理该类型模型的辅助方法。或者,您可能想要使用它来提高类型安全性并确保集合仅包含特定类型的模型。
Laravel 使得覆盖应该返回的集合类型变得非常容易。
让我们看一个例子。假设我们有一个
App\Models\Post
模型,当我们从数据库获取它们时,我们希望将它们返回到自定义的
App\Collections\PostCollection
班级。
我们可以创造一个新的
app/Collections/PostCollection.php
文件并定义我们的自定义集合类,如下所示:
declare(strict_types=1);
namespace App\Collections;
use App\Models\Post;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, Post>
*/
class PostCollection extends Collection
{
// ...
}
在上面的例子中,我们创建了一个新的
App\Collections\PostCollection
扩展 Laravel 的
Illuminate\Support\Collection
类。我们还指定此集合将仅包含
App\Models\Post
使用 docblock 来定义类。这非常有助于您的 IDE 了解集合中的数据类型。
然后我们可以更新我们的
App\Models\Post
通过重写模型来返回我们自定义集合类的实例
newCollection
像这样的方法:
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);
}
}
在示例中,我们采用了
App\Models\Post
传递给
newCollection
方法并返回我们自定义的
App\Collections\PostCollection
班级。
现在我们可以使用自定义集合类从数据库中提取帖子,如下所示:
use App\Models\Post;
$posts = Post::all();
// $posts is an instance of App\Collections\PostCollection
比较模型
我在处理项目时经常遇到的一个问题是如何比较模型。这通常在授权检查中,当你想检查用户是否可以访问资源时。
让我们来看看一些常见的陷阱以及为什么应该避免使用它们。
你应该避免使用
===
检查两个模型是否相同时。这是因为
===
check 在比较对象时,将检查它们是否是对象的同一实例。这意味着即使两个模型具有相同的数据,如果它们是不同的实例,它们也不会被视为相同。因此您应该避免这样做,因为它可能会返回
false
。
假设
post
关系存在于
App\Models\Comment
模型,并且数据库中的第一条评论属于第一篇文章,让我们看一个例子:
// ⚠️ Avoid using `===` to compare models
$comment = \App\Models\Comment::first();
$post = \App\Models\Post::first();
$postsAreTheSame = $comment->post === $post;
// $postsAreTheSame will be false.
您还应避免使用
==
检查两个模型是否相同时。这是因为
==
check 在比较对象时,会检查它们是否是同一类的实例,以及它们是否具有相同的属性和值。然而,这可能会导致意外的行为。
请看这个例子:
// ⚠️ Avoid using `==` to compare models
$comment = \App\Models\Comment::first();
$post = \App\Models\Post::first();
$postsAreTheSame = $comment->post == $post;
// $postsAreTheSame will be true.
在上面的例子中,
==
检查将返回
true
因为
$comment->post
和
$post
是同一个类,具有相同的属性和值。但是如果我们改变
$post
模型,所以它们有区别?
让我们使用
select
方法,所以我们只抓取
id
和
content
来自的字段
posts
桌子:
// ⚠️ 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.
虽然
$comment->post
与
$post
, 这
==
检查将返回
false
因为模型加载了不同的属性。你可以想象,这可能会导致一些意想不到的行为,这些行为可能很难追踪,特别是如果你事后添加了
select
方法进行查询,您的测试就会开始失败。
相反,我喜欢使用
is
和
isNot
Laravel 提供的方法。这些方法将比较两个模型并检查它们属于同一类、具有相同的主键值以及具有相同的数据库连接。这是一种更安全的比较模型的方法,有助于降低发生意外行为的可能性。
您可以使用
is
检查两个模型是否相同的方法:
$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();
$postsAreTheSame = $comment->post->is($post);
// $postsAreTheSame will be true.
类似地,您可以使用
isNot
检查两个模型是否不同的方法:
$comment = \App\Models\Comment::first();
$post = \App\Models\Post::query()->select(['id', 'content'])->first();
$postsAreNotTheSame = $comment->post->isNot($post);
// $postsAreNotTheSame will be false.
使用
whereBelongsTo
构建查询时
这最后一条建议更多的是个人偏好,但我发现它使我的查询更容易阅读和理解。
当尝试从数据库获取模型时,你可能会发现自己编写了基于关系进行过滤的查询。例如,你可能想要获取属于特定用户和帖子的所有评论:
$user = User::first();
$post = Post::first();
$comments = Comment::query()
->where('user_id', $user->id)
->where('post_id', $post->id)
->get();
Laravel 提供了
whereBelongsTo
您可以使用该方法使查询更具可读性(在我看来)。使用此方法,我们可以重写上面的查询,如下所示:
$user = User::first();
$post = Post::first();
$comments = Comment::query()
->whereBelongsTo($user)
->whereBelongsTo($post)
->get();
我喜欢这个语法糖,感觉它让查询更易于理解。这也是确保您根据正确的关系和字段进行筛选的好方法。
您或您的团队可能更喜欢使用更明确的方式写出
where
条款。所以这个技巧可能并不适合所有人。但我认为只要你坚持自己的方法,无论哪种方式都是完全可以的。
结论
希望本文能向您展示一些使用 Laravel 模型的新技巧。您现在应该能够发现和预防 N+1 问题、防止访问缺失的属性、防止默默丢弃属性以及将主键类型更改为 UUID 或 ULID。您还应该知道如何更改用于路由模型绑定的字段、指定返回的集合类型、比较模型以及使用
whereBelongsTo
在建立查询时。
帖子 Laravel 模型提示 首先出现在 Laravel 新闻 。
加入 Laravel 时事通讯 获取最新信息 类似这样的 Laravel 文章将直接发送到您的收件箱。