Laravel 项目结构:将代码移出控制器

141 阅读8分钟

您可以在此处重新定位控制器逻辑:

表单请求:简化数据验证。

在数据库交互期间调整数据。

响应特定的模型事件。

服务类:对您的业务逻辑进行分组。

操作类:处理单独的任务。

作业:管理后台任务。

事件和侦听器:协调应用程序事件和反应。

全局助手:随处可用的便捷功能。

Traits :可以在多个类中使用的代码片段。

基本控制器:具有共享功能的控制器。

存储库类:集中与数据库的交互。

简化您的控制器代码

让我们深入探讨如何组织一个粗壮的控制器并使其更加简洁。这是起始代码:


public function store(Request $request)  
公共函数存储(请求$请求)  
{  
$this->authorize('user_create');  
$this->authorize('user_create');  
$userData = $request->validate([  
$userData = $request->验证([  
'name' => 'required',  
'姓名' => '必填',  
'email' => 'required|unique:users',  
'电子邮件' => '必需|唯一:用户',  
'password' => 'required',  
'密码' => '必填',  
]);  
$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d');  
$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Ym-d');  
$userData['password'] = bcrypt($request->password);  
$userData['password'] = bcrypt($request->password);  
$user = User::create($userData);  
$user->roles()->sync($request->input('roles', []));  
$user->roles()->sync($request->input('roles', []));  
Project::create(['user_id' => $user->id, 'name' => 'Demo project 1']);  
Project::create(['user_id' => $user->id, 'name' => '演示项目 1']);  
Category::create(['user_id' => $user->id, 'name' => 'Demo category 1']);  
Category::create(['user_id' => $user->id, 'name' => '演示类别 1']);  
Category::create(['user_id' => $user->id, 'name' => 'Demo category 2']);  
Category::create(['user_id' => $user->id, 'name' => '演示类别 2']);  
MonthlyReport::where('month', now()->format('Y-m'))->increment('users_count');  
MonthlyReport::where('month', now()->format('Y-m'))->increment('users_count');  
$user->sendEmailVerificationNotification();  
$用户->sendEmailVerificationNotification();  
$admins = User::where('is_admin', 1)->get();  
$admins = User::where('is_admin', 1)->get();  
Notification::send($admins, new AdminNewUserNotification($user));  
通知::发送($admins, new AdminNewUserNotification($user));  
return response()->json(['result' => 'success', 'data' => $user], 200);  
返回响应()->json(['结果' => '成功', '数据' => $user], 200);  
}

注意:请记住,这是关于您感觉正确的事情。以下选项只是建议,您可以自由选择最适合您需求的选项。

使用表单请求简化 Laravel 验证

验证是任何 Laravel 应用程序的关键部分,确保正在处理的数据正确且有用。特别是在处理大量字段时,验证会使您的代码变得混乱。让我们简化一下。

首先,我们将验证规则转移到表单请求中。这不仅涉及数据验证,还涉及权限。

php artisan make:request StoreUserRequest

这会创建一个新文件 app\Http\Requests\StoreUserRequest.php 有两个关键方法:用于权限的authorize()和用于数据验证的rules() 。

您的表单请求应如下所示:

namespace App\Http\Requests;  
use Illuminate\Foundation\Http\FormRequest;  
class StoreUserRequest extends FormRequest  
{  
public function authorize()  
{  
return Gate::allows('user_create');  
}  
public function rules()  
{  
return [  
'name' => 'required',  
'email' => 'required|unique:users',  
'password' => 'required',  
];  
}  
}

在您的控制器中,使用StoreUserRequest而不是默认的Request 。然后,您可以通过从请求中调用validated()方法来直接访问经过验证的数据。

例如,您的控制器可能如下所示:

public function store(StoreUserRequest $request)  
{  
$userData = $request->validated();  
$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d');  
$userData['password'] = bcrypt($request->password);  
$user = User::create($userData);  
$user->roles()->sync($request->input('roles', []));  
// ...  
}

这个小调整大大简化了控制器,使您的代码更有组织性和可管理性。

使用 Laravel 的 Eloquent 简化数据修改:Mutators 与 Observers

想象一下,您需要在将某些数据保存到数据库之前对其进行调整,例如日期格式或密码加密。 Laravel 的 Eloquent 不是在控制器中执行此操作,而是提供了高效的工具:Mutators 和 Observers。让我们看看它们是如何工作的。

变异器允许您在将模型的属性值保存到数据库之前对其进行处理。根据 Laravel 版本定义 Mutators 有两种主要方法:

从 Laravel 9 开始:


protected function startAt(): Attribute  
{  
return Attribute::make(  
set: fn ($value) => Carbon::createFromFormat('m/d/Y', $value)->format('Y-m-d')  
);  
}  
protected function password(): Attribute  
{  
return Attribute::make(  
set: fn ($value) => bcrypt($value)  
);  
}

Observers: 观察员:

观察者为您提供在模型生命周期期间触发的方法,例如创建或更新条目时。

要为用户模型生成观察者,请使用:

php artisan make:observer UserObserver --model=User

以下是如何设置观察者以在创建记录之前修改数据的示例:

namespace App\Observers;  
class UserObserver  
{  
public function creating(User $user)  
{  
$user->start_at = Carbon::createFromFormat('m/d/Y', $user->start_at)->format('Y-m-d');  
$user->password = bcrypt($user->password);  
}  
}

请注意,某些方法(例如creating()可能没有正式记录,这表明它们并不总是推荐的方法。

考虑到易用性和 Laravel 官方指南,Mutators 通常是此类数据修改的首选。 现在,通过在控制器外部处理这些数据修改,代码变得更清晰:


public function store(StoreUserRequest $request)  
{  
$user = User::create($request->validated());  
$user->roles()->sync($request->input('roles', []));  
// ...  
}

这说明了 Laravel 如何使用正确的工具帮助生成简洁且可维护的代码。

使用 Laravel 中的服务类简化控制器

接下来,在整理控制器的过程中,我们将深入研究负责在数据库中保存数据的核心逻辑。一个专门的服务类将成为我们完成这项任务的盟友。 服务类没有用于创建的特定 artisan 命令,因此我们将手动创建一个。让我们在app/Services目录中开发一个UserService.php文件,其中包含创建用户的逻辑:

namespace App\Services;  
use App\Models\User;  
class UserService  
{  
public function create(array $userData): User  
{  
$user = User::create($userData);  
$user->roles()->sync($userData['roles']);  
  
return $user;  
}  
}

UserService中,我们引入了一个create()方法,该方法将经过验证的数据作为输入,编排用户创建、同步角色,然后返回新用户。

现在,为了在我们的控制器中利用此服务,我们遇到了多种路径。让我们揭开它们的面纱:

立即在控制器中启动服务,将经过验证的数据传递给我们的create()方法。

$user = (new UserService())->create($request->validated()); 使用依赖注入将服务引入到我们的 Controller 方法中,从而实现一种简洁有效的方式来访问我们的服务功能。

public function store(StoreUserRequest $request, UserService $userService)  
{  
$user = $userService->create($request->validated());  
// ...  
}

选择服务类方法可以提升代码的组织性,通过保持控制器的精简和专注来提高可读性和可维护性。

Laravel 中的服务类和操作类:了解差异

在 Laravel 庞大的生态系统中,Service 和 Action 类都提供了有组织的方式来管理逻辑。它们看起来很相似,但根据您更喜欢构建应用程序逻辑的方式来使用。让我们简单地阐明这些概念。

Action 类是精确的,通常专用于单个操作。与 Service 类不同,没有内置的 Artisan 命令来生成 Action 类。因此,您将手动创建一个。

考虑一个Action类来管理用户创建:


namespace App\Actions;  
use App\Models\User;  
class CreateUserAction  
{  
public function execute(array $userData): User  
{  
$user = User::create($userData);  
$user->roles()->sync($userData['roles']);  
  
return $user;  
}  
}

要在控制器中实现此Action ,您需要实例化该类并执行其方法,传递必要的数据:

public function store(StoreUserRequest $request)  
{  
$user = (new CreateUserAction())->execute($request->validated());  
// ...  
}

比较服务类和操作类,人们可能会问:“是什么让它们与众不同?”它很大程度上取决于您喜欢如何分割应用程序的逻辑:

服务类:这些倾向于包含与特定模型或实体相关的操作,例如UserServiceTaskService 。他们可能有多种方法来反映与该实体相关的各种任务。

操作类:它们封装了各个操作或动作,例如CreateUserActionUpdateTaskAction 。通常,它们有一个主要方法,但它们也可以包含其他私有方法以实现更复杂的逻辑。

总而言之,Service 类和 Action 类之间的选择并不是为了哪个更优越,而是为了哪个更高级。这是关于您希望如何在应用程序中构建和传达操作的问题。这两种范式都旨在促进更干净、更有组织的代码。因此,请选择符合您的编码理念和项目具体要求的一种。

了解 Laravel 中的作业和后台任务

作业:在 Laravel 中,作业是可以在后台运行的任务。它们就像操作,但有一个额外的优点 - 它们可以排队。这意味着您可以延迟它们的执行,以避免应用程序立即加载。

感谢 Laravel 的 Artisan,创建工作非常简单:


php artisan make:job NewUserDataJob  
php artisan make :job NewUserDataJob

基本作业如下所示:


use App\Models\Project;  
use App\Models\Category;  
class NewUserDataJob implements ShouldQueue  
{  
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;  
public $user;  
public function __construct(User $user)  
{  
$this->user = $user;  
}  
public function handle()  
{  
Project::create([  
'user_id' => $this->user->id,  
'name' => 'Sample Project',  
]);  
Category::create([  
'user_id' => $this->user->id,  
'name' => 'Sample Category 1',  
]);  
Category::create([  
'user_id' => $this->user->id,  
'name' => 'Sample Category 2',  
]);  
}  
}

要在控制器中使用此作业,您只需分派它:


public function store(StoreUserRequest $request)  
{  
$user = (new CreateUserAction())->execute($request->validated());  
NewUserDataJob::dispatch($user);  
// ... further code  
}

队列:调度作业后,Laravel 通过队列系统处理后台任务。请记住,为了使队列高效工作,您必须设置和管理队列工作线程,这是一个单独的进程。

利用 Laravel 中事件和侦听器的力量

首先创建一个用于注册新用户的事件:

php artisan make:event NewUserRegistered

接下来,创建一个等待NewUserRegistered事件的监听器: php artisan make:listener MonthlyReportUpdateListener

在您的控制器中,像处理作业一样调度事件:


public function register(RegistrationRequest $request)  
{  
$user = (new RegisterUserAction())->execute($request->validated());  
NewUserDataJob::dispatch($user);  
NewUserRegistered::dispatch($user);  
//...  
}

NewUserRegistered事件接受用户作为参数,因此我们的侦听器可以访问该用户:

class NewUserRegistered  
{  
public function __construct(public User $user) { }  
}

EventServiceProvider中,将事件链接到其侦听器:


class EventServiceProvider extends ServiceProvider  
{  
protected $listen = [  
NewUserRegistered::class => [  
MonthlyReportUpdateListener::class,  
// other listeners...  
],  
];  
}

MonthlyReportUpdateListener内,增加每月用户数:

class MonthlyReportUpdateListener  
{  
public function handle(NewUserRegistered $event)  
{  
MonthlyReport::where('month', now()->format('Y-m'))->increment('users_count');  
}  
}

Laravel 本身使用事件和监听器。例如,电子邮件验证通知由Registered事件自动触发。

您可以进一步扩展它。也许您想在新用户加入时通知管理员:

php artisan make:listener NotifyAdminsOfNewUser

然后,在这个新的侦听器中,通知管理员用户:


class NotifyAdminsOfNewUser  
{  
public function handle(NewUserRegistered $event)  
{  
$admins = User::where('is_admin', 1)->get();  
Notification::send($admins, new NewUserAdminNotification($event->user));  
}  
}

您的控制器现在变得精简了:

public function register(RegistrationRequest $request)  
{  
$user = (new RegisterUserAction())->execute($request->validated());  
NewUserDataJob::dispatch($user);  
NewUserRegistered::dispatch($user);  
return response()->json(['status' => 'success', 'user' => $user], 200);  
}

仅通过这些步骤,您就大大减小了控制器的大小,并使您的应用程序更加模块化和事件驱动。

在 Laravel 中使用全局助手

Laravel 中的辅助类非常方便。将它们视为一个工具箱,里面装满了用于特定任务的各种工具(方法)。例如, DateHelper可以帮助进行日期操作,而CurrencyHelper可以帮助进行货币转换。

让我们分解一个实际用例以更好地理解它:

假设您的 User 模型中有一个名为startAt的属性。该属性具有日期格式操作


protected function startAt(): Attribute  
{  
return Attribute::make(  
set: fn ($value) => Carbon::createFromFormat('m/d/Y', $value)->format('Y-m-d');  
);  
}

为了使此代码可重用,让我们将日期转换逻辑转移给助手。

app/Helpers/DateHelper.php创建一个新文件。 (Laravel 没有内置命令,因此您需要手动创建它。)

在这个新的DateHelper类中,我们创建一个名为convertToDB的方法:


namespace App\Helpers;  
use Carbon\Carbon;  
class DateHelper  
{  
public static function convertToDB($date)  
{  
return Carbon::createFromFormat('m/d/Y', $date)->format('Y-m-d');  
}  
}

现在,返回您的 User 模型并更新startAt属性以使用这个新的帮助器:

protected function startAt(): Attribute  
{  
return Attribute::make(  
set: fn ($value) => DateHelper::convertToDB($value);  
);  
}

现在,您已经在一个集中位置组织了日期操作逻辑。每当您需要将这种格式的日期转换为数据库时,只需调用此帮助程序即可。它使您的代码更整洁、更具可读性且易于维护。

在 Laravel 中管理重复部分:traits 还是 Base Controller?

在使用 Laravel 时,有效地处理重复任务至关重要,尤其是在响应一致性是关键的 API 控制器中。您可以通过使用 Base Controller 或 Traits 来管理重复的部分,如响应,从而简化流程。

一种简单的方法是将 logic 直接合并到 Base Controller ( app/Http/Controllers/Controller.php ) 中。

以下是构建它的方法:


class Controller extends BaseController  
{  
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;  
public function respondOk($data)  
{  
return response()->json([  
'result' => 'success',  
'data' => $data,  
], 200);  
}  
}

然后,在典型的控制器中,你可以毫不费力地调用这个 response 方法:

public function store(StoreUserRequest $request)  
{  
$user = (new CreateUserAction())->execute($request->validated());  
NewUserDataJob::dispatch($user);  
NewUserRegistered::dispatch($user);  
return $this->respondOk($user);  
}

特征提供了另一种优雅的解决方案。您可以创建一个特征来容纳您的响应逻辑。

Laravel 中是否需要 Repository 类?

在 Laravel 4 和 5 的早期,Repository 模式风靡一时。它充当 Laravel 的 Eloquent ORM(对象关系映射)和控制器之间的中间层。然而,随着时间的推移和 Laravel 的发展,这种方法变得不那么普遍。

为了理解这一点,我们来分解一下 Repository 模式的用途。在一般的编程中,Repository 充当 Controller 和 Database 之间的桥梁。这个概念在没有像 Laravel 的 Eloquent 这样的 ORM 机制的情况下特别有用

Laravel 的 Eloquent ORM 在设计上已经起到了这座桥的作用。它在控制器和数据库之间提供了一个抽象层,将我们的命令无缝地转换为数据库查询。例如:

而不是直接编写 SQL 查询,例如:

使用 Eloquent,我们可以简单地使用:

User::all();

鉴于此,将 Repository 作为 Eloquent 之上的附加层引入可以被视为多余。从本质上讲,您将在已经表现为该图层的内容之上添加一个图层。 总之,虽然 Repository 模式在特定上下文中有其优点,但在 Laravel 的世界中,它可能是一个额外的层,不会增加显着优势。在决定架构模式之前,考虑项目的具体需求和结构始终是必不可少的。