使用 Services, Events, Jobs, Actions, 等重构 Laravel 控制器(二)
4. 代金券创建: 放在同一个服务类还是不同的服务类?
接下来, 在控制器方法中,还有三个操作:
Auth::login($user);
$voucher = Voucher::create([
'code' => Str::random(8),
'discount_percent' => 10,
'user_id' => $user->id
]);
$user->notify(new NewUserWelcomeNotification($voucher->code));
控制器中的登录操作会保留不变,因为它已经像服务类一样调用外部的 Auth 类了,我们并不清楚它的底层实现。
不过对于此例中的代金券,控制器包含了代金券如何创建及发送给用户的逻辑代码。
首先,我们需要将代金券的创建移到一个单独的类中:我在犹豫新建一个代金券服务类是 VoucherService
还是将其一同放到 UserService
中。这更像是理念之争:此方法是代金券系统相关还是用户系统相关,亦或兼而有之?
由于服务的一个特性就是包含多种方法,我决定不为一个方法单独创建 VoucherService
。我们可以在 UserService
中实现:
use App\Models\Voucher;
use Illuminate\Support\Str;
class UserService
{
// public function uploadAvatar() ...
// public function createUser() ...
public function createVoucherForUser(int $userId): string
{
$voucher = Voucher::create([
'code' => Str::random(8),
'discount_percent' => 10,
'user_id' => $userId
]);
return $voucher->code;
}
}
然后,在控制器中,我们这样调用它:
public function store(StoreUserRequest $request, UserService $userService)
{
// ...
Auth::login($user);
$voucherCode = $userService->createVoucherForUser($user->id);
$user->notify(new NewUserWelcomeNotification($voucherCode));
另外此处还可以考虑:或许我们应该将这两行代码放到 UserService 的一个单独方法中,用以负责发送欢迎邮件,再转去调用代金券方法。
像这样:
class UserService
{
public function sendWelcomeEmail(User $user)
{
$voucherCode = $this->createVoucherForUser($user->id);
$user->notify(new NewUserWelcomeNotification($voucherCode));
}
然后,控制器将只有一行代码:
$userService->sendWelcomeEmail($user);
5. 通知管理员:队列任务
最后,我们还有这么一段代码
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($user));
}
它用来发送多封邮件,可能需要消耗一些时间,因此我们将其放到队列之中,在后台运行。这里就用到了队列任务。
Laravel 通知类是 queueable
的,我们可以想象它相比于单纯地发送通知邮件更为复杂。因此我们为其创建了一个队列。
这种情况下,Laravel 为我们提供了 Artisan 命令:
php artisan make:job NewUserNotifyAdminsJob
app/Jobs/NewUserNotifyAdminsJob.php:
class NewUserNotifyAdminsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private User $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function handle()
{
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($this->user));
}
}
}
然后,在控制器中,我们就可以传入参数调用队列:
use App\Jobs\NewUserNotifyAdminsJob;
class RegisteredUserController extends Controller
{
public function store(StoreUserRequest $request, UserService $userService)
{
// ...
NewUserNotifyAdminsJob::dispatch($user);
至此,我们将所有的业务逻辑从控制器移到了其他地方,我们简要回顾一下:
public function store(StoreUserRequest $request, UserService $userService)
{
$avatar = $userService->uploadAvatar($request);
$user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
Auth::login($user);
$userService->sendWelcomeEmail($user);
NewUserNotifyAdminsJob::dispatch($user);
return redirect(RouteServiceProvider::HOME);
}
代码更精短,被分成了几个文件,不过依然可读性强,对吧。再次重申,这只是完成任务的其中一种实现方法,你可以自行决定用其他方法组织实现。
这还没完。我们也来探讨一下"被动"方式。
6. 事件/监听
理论上讲,我们可以将这个控制器方法里面的操作分成两种:主动的和被动的。
- 我们主动地创建用户并让他们登录
- 然后在后台用户可能(也可能没有)涉及一些事项。因此,我们被动地等待这些其他操作:发送欢迎邮件及通知管理员。
因此,其中一种分离解耦代码的方式是,不在控制器中调用它,而是应该在有事件发生的时候自动触发。
你可以这样联合使用事件和监听器:
php artisan make:event NewUserRegistered
php artisan make:listener NewUserWelcomeEmailListener --event=NewUserRegistered
php artisan make:listener NewUserNotifyAdminsListener --event=NewUserRegistered
事件类应该接收一个用户模型,之后将其传入事件对应的监听器中。
app/Events/NewUserRegistered.php
use App\Models\User;
class NewUserRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public User $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
然后,事件像这样在控制器中分发:
public function store(StoreUserRequest $request, UserService $userService)
{
$avatar = $userService->uploadAvatar($request);
$user = $userService->createUser($request->validated() + ['avatar' => $avatar]);
Auth::login($user);
NewUserRegistered::dispatch($user);
return redirect(RouteServiceProvider::HOME);
}
在监听器类中,我们重复相同的业务逻辑:
use App\Events\NewUserRegistered;
use App\Services\UserService;
class NewUserWelcomeEmailListener
{
public function handle(NewUserRegistered $event, UserService $userService)
{
$userService->sendWelcomeEmail($event->user);
}
}
还有,另一个:
use App\Events\NewUserRegistered;
use App\Notifications\NewUserAdminNotification;
use Illuminate\Support\Facades\Notification;
class NewUserNotifyAdminsListener
{
public function handle(NewUserRegistered $event)
{
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($event->user));
}
}
}
此方式使用了事件/监听,它的优势是什么?在代码中它们像"钩子"一样被使用,其他人在将来有需要时你可以使用这个钩子。换句话说,你在告诉后来的开发者:"用户注册事件触发了。如果你需要添加其他一些操作,只需要为其创建监听器便可"。
7.观察者:"沉默的" 事件/监听器
此例中,一个类似的"被动"实现方式是通过模型观察者实现。
php artisan make:observer UserObserver --model=User
app/Observers/UserObserver.php:
use App\Models\User;
use App\Notifications\NewUserAdminNotification;
use App\Services\UserService;
use Illuminate\Support\Facades\Notification;
class UserObserver
{
public function created(User $user, UserService $userService)
{
$userService->sendWelcomeEmail($event->user);
foreach (config('app.admin_emails') as $adminEmail) {
Notification::route('mail', $adminEmail)
->notify(new NewUserAdminNotification($event->user));
}
}
}
这样,你就不需要在控制器中派发任何事件,模型观察者会在 Eloquent 模型创建之后立即触发观察者。
很方便,是吧?
不过,个人觉得,这种模式有点危险。不只是因为在控制器中隐藏了业务逻辑的实现,同时这些操作的存在并不清晰。想象一下,一个新加入团队的开发者,他们会在维护用户注册的时候去检查所有可能的观察者方法吗?
当然,可能还是可以搞定,不过它仍然是不明显的。我们的目的是让代码具有更高的可维护性,因此少点"惊喜"会更好。因此,我也不是太喜欢观察者。
结语
回看这篇文章,我发现我只是非常肤浅地说明了代码分离的可能,用了非常简单的例子。
事实上,在这个简单地例子中,看起来更像是我们通过新建许多不同的PHP类,使应用变得更加复杂。不过,例子中这些分离的代码都比较短小。真实的开发中,可能远比此复杂,通过分离代码,我们可以更好的管理代码,每一部分可能会有由不同的开发者处理实现。我最后重申一次:你自己的应用最终还是要由你自己决定将其放在何处。目的在于让你和你的团队在将来还能理解这些代码,并且在添加新特性、维护代码修复漏洞时不会产生疑惑。