Laravel 最佳实践
Laravel 是一个旨在简化现代 Web 应用开发的强大框架。与所有框架一样,它在核心中融入了最佳实践。遵循这些指南,你能够编写更简洁的代码、减少技术债务、提升团队协作效率,并确保你的代码库符合 Laravel 的开发规范。
本文将探讨这些至关重要的 Laravel 最佳实践,涵盖代码结构设计到数据库操作优化,确保你的项目保持高效且便于开发。
无论你是经验丰富的 Laravel 开发者还是初学者,这些实践都能助你提升开发技能,打造出高质量的应用程序。
那么让我们开始吧。👇
大模型,小控制器
将数据库逻辑移到 Eloquent 模型,以维持控制器的整洁以及代码可复用。
不良示范:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($query) {
$query->where('created_at', '>', now()->subDays(7));
}])
->get();
return view('index', compact('clients'));
}优秀示例:
public function index(Client $client)
{
return view('index', ['clients' => $client->getVerifiedWithRecentOrders()]);
}
class Client extends Model
{
public function getVerifiedWithRecentOrders(): Collection
{
return $this->verified()
->with(['orders' => fn($query) => $query->recent()])
->get();
}
public function scopeVerified($query)
{
return $query->where('is_verified', true);
}
}
class Order extends Model
{
public function scopeRecent($query)
{
return $query->where('created_at', '>', now()->subDays(7));
}
}单一职责原则
一个类应该只有一个责任。这意味着一个类应该专注于单个功能。违反这一原则会使你的代码更难阅读、测试和维护,因为它混合了应该分开的关注点。
通过遵守单一职责原则,你可以创建更容易理解和重构的代码。每个类别或服务都有明确的目的,使整个系统更加模块化和灵活。
不良示范:
public function update(Request $request): string
{
$validated = $request->validate([
'name' => 'required|max:255',
'tasks' => 'required|array:due_date,status'
]);
foreach ($request->tasks as $task) {
$formattedDate = $this->carbon->parse($task['due_date'])->toDateTimeString();
$this->logger->info('Task updated: ' . $formattedDate . ' - ' . $task['status']);
}
$this->project->updateTasks($request->validated());
return redirect()->route('projects.index');
}优秀示例:
public function update(UpdateProjectRequest $request): string
{
$this->taskLogger->logTasks($request->tasks);
$this->projectService->updateTasks($request->validated());
return redirect()->route('projects.index');
}
class TaskLogger
{
public function logTasks(array $tasks): void
{
// Logic to log tasks
}
}
class ProjectService
{
public function updateTasks(array $data): void
{
// Logic to update project tasks
}
}方法只做一件事
一个函数应该只有一个目的,并能很好地执行它。当一个方法做不止一件事时,它就变得更难理解、测试和维护。将职责划分为更小、更专注的方法,可以使代码更具可读性,更容易调试。
不良示范:
public function getFullNameAttribute(): string
{
if (auth()->user() && auth()->user()->hasRole('admin') && auth()->user()->isVerified()) {
return 'Admin ' . $this->first_name . ' ' . $this->last_name;
} else {
return $this->first_name[0] . '. ' . $this->last_name;
}
}优秀示例:
public function getFullNameAttribute(): string
{
return $this->isVerifiedAdmin() ? $this->formatFullName() : $this->formatShortName();
}
private function isVerifiedAdmin(): bool
{
$user = auth()->user();
return $user && $user->hasRole('admin') && $user->isVerified();
}
private function formatFullName(): string
{
return 'Admin ' . $this->first_name . ' ' . $this->last_name;
}
private function formatShortName(): string
{
return strtoupper($this->first_name[0]) . '. ' . ucfirst($this->last_name);
}
将业务逻辑保留在服务类中
控制器只处理 HTTP 请求和响应,将复杂的逻辑委派给服务类。这能够确保代码整洁、可重用并且易于测试。
不良示范:
public function store(Request $request)
{
if ($request->hasFile('image')) {
$image = $request->file('image');
$image->storeAs('temp', $image->getClientOriginalName(), 'public');
}
// Other unrelated logic...
}
优秀示例:
public function store(Request $request, ArticleService $articleService)
{
$articleService->uploadImage($request->file('image'));
// Other unrelated logic...
}
class ArticleService
{
public function uploadImage(?UploadedFile $image): void
{
if ($image) {
$image->storeAs('uploads/temp', uniqid() . '_' . $image->getClientOriginalName(), 'public');
}
}
}避免将业务逻辑写入到路由中
路由只处理 HTTP 请求,而非业务逻辑。这能够保证代码的整洁和可维护性。
错误示范:
// Business logic in the route
Route::post('/article', function (Request $request) {
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->save();
});优秀示例
// Route delegates logic to the controller
Route::post('/article', [ArticleController::class, 'store']);
// In ArticleController
public function store(Request $request)
{
// logic to create article
}
使用关联以确保代码整洁
使用 Eloquent 关联来简化和声明关联模型的交互方式。这避免了重复的任务,使代码更容易维护,更不容易出错。
错误示范:
$article = new Article;
$article->title = $request->input('title');
$article->content = $request->input('content');
$article->verified = $request->boolean('verified');
$article->category_id = $category->id;
$article->save();优秀示例:
$category->articles()->create($request->safe()->only(['title', 'content', 'verified']));原子化业务操作使用数据库事务
事务确保所有数据库的操作要么全部成功,要么全部失败,维持数据库的一致性。
错误示范:
public function placeOrder(Request $request)
{
$order = new Order;
$order->user_id = $request->user_id;
$order->save();
$payment = new Payment;
$payment->order_id = $order->id;
$payment->save();
}优秀示例:
use DB;
public function placeOrder(Request $request)
{
DB::beginTransaction();
try {
$order = Order::create($request->validated());
$payment = Payment::create(['order_id' => $order->id]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}避免在 Blade 中使用查询:使用热加载
在 Blade 模板内执行查询会导致数据库调用效率低下,尤其是在循环中。热加载在单个查询中获取相关数据,提高了性能并避免了 N+1 查询问题。
错误示范:
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach如果你有 100 个用户,这段代码将会触发 101 个查询:一个用于查询所有用户,然后每个用户的 profile 各自有一个查询。
优秀示例:
// in a service class or model passed back to controller which shares that with the blade file
$users = User::with('profile')->get();
// in blade file
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach这个用户只有两个查询:一个用来查询用户,一个用来查询它们的 Profile。
数据分块(Chunk)以提升性能
对于涉及大量数据集的任务,通过限制一次性加载到内存的数据量按分块(chunk)处理将会减少内存用量并提升性能。
不良示范
$users = User::all();
foreach ($users as $user) {
// Process each user
}优秀示例:
User::chunk(500, function ($users) {
foreach ($users as $user) {
// Process each user
}
});使用常量替代硬编码值
使用常量将帮助你找到使用其值所在的位置,以防需要更改、重构它,并将帮助你进行调试。
Bad example
public function isAdmin(User $user): bool
{
return $user->type === 'admin';
}优秀示例:
public function isAdmin(User $user)
{
return $user->type === UserType::ADMIN;
}翻译字符串
如果你在一开始就考虑将字符串进行翻译,随着将来业务增长,你可能会感谢自己。你只需将字符串传入到 __() 函数
Bad example
return back()->with('message', 'Your article has been added!');优秀示例:
return back()->with('message', __('Your article has been added!')); // notice the call to __()注入依赖
使用 new 创建实例会耦合类,不利于测试或修改。使用 IoC 容器让依赖注入更为简便以及更好的可测试性。
Bad example
public function store(Request $request)
{
$user = new User;
$user->create($request->validated());
}
优秀示例:
public function __construct(protected UserService $userService) {}
public function store(Request $request)
{
$this->userService->create($request->validated());
}
避免在代码中直接使用 .env
在应用中直接访问 .env 中的数据,会让你的代码难以维护和测试。因此,请将值存入到配置文件中并使用 config() 方法访问。
Bad example
$apiKey = env('API_KEY');优秀示例:
// config/services.php
'api_key' => env('API_KEY'),
// Retrieve the value
$apiKey = config('services.api_key');将日期存储为对象,而非字符串
将日期存储为字符串可能会导致格式不一致和解析错误。最好将它们存储为 Carbon 实例,这样可以提供强大的日期处理。仅当显示层需要时,才使用访问器和修改器来格式化日期。
Bad example
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}优秀示例:
// In Model
protected $casts = [
'ordered_at' => 'datetime',
];
// In Blade View
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->format('m-d') }}保持代码文档的简约性和意义
过多的文档往往会使代码变得混乱,使维护变得更加困难。相反,要为变量、函数和类使用清晰、描述性的名称。仅在解释复杂逻辑绝对必要时使用注释。
Bad example
/**
* The function checks if the given string has any white spaces
*
* @param string $string String received from frontend which might contain
* space characters. Returns True if the string
* is valid.
*
* @return bool
*
* @license GPL
*/
public function checkString($string)
{
}优秀示例:
public function hasWhiteSpaces(string $string): bool
{
}与团队对齐代码规范
一致的代码提高了可读性和可维护性,使协作更容易。
使用 Laravel Pint 自动格式化和执行编码标准。它与你的开发工作流程集成在一起,因此你可以在每次提交之前使用 Git Hooks 运行它。
composer require --dev laravel/pint
vendor/bin/pint
Test, Test and Test最后,为了确保代码的可靠性、可维护性和可扩展性,最重要的事情之一就是编写自动化测试。
你不需要 100% 覆盖你的功能(我认为这会适得其反),但至少你需要确保覆盖所有的 GET 路由,而且你可以添加的越多越好。
为什么呢?:
- 今早发现漏洞:测试有助于在代码投入生产之前发现问题。在开发阶段捕获漏洞比部署后再修复要便宜得多、容易得多。
- 代码信心:有了足够的测试覆盖率,你可以自信地修改代码库。测试可以确保新的更改不会破坏现有功能。
- 文档:编写良好的测试可以作为代码的实时文档。它们描述了系统应该如何运行,并可用于理解代码的意图。
- 安全重构:在重构或改进现有代码时,测试提供了一个安全保障,确保在更改过程中不会丢失任何功能。
- 改进设计:编写测试通常会促进更好的软件设计。为了编写可测试的代码,您通常会创建更小、更专注的方法和类,从而更容易维护。
- 协作:测试使团队更容易在大型代码库上协同工作。它们为代码行为定义了明确的预期,减少了误解并改善了协作。
- 持续集成:测试对于实施持续集成和交付工作流程至关重要。自动化测试可以在每次代码推送时运行,确保只部署稳定的代码。
- 长期维护:在大型项目中,测试有助于随着时间的推移保持稳定性,尤其是在团队成员发生变化时。新的开发人员可以依靠测试来理解代码库的行为,并确保未来的更改不会破坏功能。