编程

Laravel 最佳实践

12 2026-02-28 23:43:00

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 路由,而且你可以添加的越多越好。 

为什么呢?:

  • 今早发现漏洞:测试有助于在代码投入生产之前发现问题。在开发阶段捕获漏洞比部署后再修复要便宜得多、容易得多。
  • 代码信心:有了足够的测试覆盖率,你可以自信地修改代码库。测试可以确保新的更改不会破坏现有功能。
  • 文档:编写良好的测试可以作为代码的实时文档。它们描述了系统应该如何运行,并可用于理解代码的意图。
  • 安全重构:在重构或改进现有代码时,测试提供了一个安全保障,确保在更改过程中不会丢失任何功能。
  • 改进设计:编写测试通常会促进更好的软件设计。为了编写可测试的代码,您通常会创建更小、更专注的方法和类,从而更容易维护。
  • 协作:测试使团队更容易在大型代码库上协同工作。它们为代码行为定义了明确的预期,减少了误解并改善了协作。
  • 持续集成:测试对于实施持续集成和交付工作流程至关重要。自动化测试可以在每次代码推送时运行,确保只部署稳定的代码。
  • 长期维护:在大型项目中,测试有助于随着时间的推移保持稳定性,尤其是在团队成员发生变化时。新的开发人员可以依靠测试来理解代码库的行为,并确保未来的更改不会破坏功能。