引言
Laravel是一个非常棒的框架。我们凭借其提供的所有功能和用户体验,可以快速构建产品。在Laravel中,通常有多种方法可以完成同一件事,没有一种方法绝对正确,有时候更多取决于我们个人如何构建我们的应用程序。
操作类和可调用控制器是当今的热门话题。我看到很多人在使用和谈论它们。我也使用并实验了这些想法,在这篇文章中,我将解释我认为可调用控制器是糟糕的主意,以及我创建并使用的一种架构模式,我将其命名为AaaS - Action As a Service(操作即服务)。
再次,正如我之前所说的,在Laravel中有很多做事的方式,我将向你展示其中之一。我喜欢它,并且我认为以这种方式组织我的应用程序是有意义的,但如果它对你来说没有意义,你可以自由地以你的方式组织你的应用程序。
操作类
操作类是旨在执行单个动作的类。这些类通常只有一个(公共)方法。一个真正简单的操作类示例是用于创建一个新的用户的。
namespace App\Actions\Users\CreateUser;
use App\Models\User;
class CreateUser
{
/**
* @param string $name
* @param string $email
* @return User
*/
function handle(string $name, string $email): User
{
User::query()->create([
'name' => $name,
'email' => $email,
]);
}
}
正如您在上面的(简化版)实现中看到的,类CreateUser
仅用于创建新的用户。在实际代码中,您可能在这个类中有更多逻辑,它甚至可以拥有其他私有方法,以更可读和可维护的方式分割逻辑,但关键是拥有一个具有单一目的的类。
可调用控制器
通常,动作类的概念被广泛用于创建具有执行单一动作目的的可调用控制器。因此,控制器中不需要有多个方法,它只会定义一个__invoke()
方法,并执行此方法。让我们看一下将上面提到的动作示例实施为可调用控制器。
namespace App\Http\Controllers\Users;
use App\Http\Controllers\Controller;
use App\Http\Requests\Users\CreateUserRequest;
use App\Models\User;
class CreateUserController extends Controller
{
/**
* @param CreateUserRequest $request
* @return User
*/
function __invoke(CreateUserRequest $request): User
{
$data = $request->validated();
User::query()->create([
'name' => $data['name'],
'email' => $data['email'],
]);
}
}
为什么可调用控制器是糟糕的
可调用控制器目前在:Laravel中是一个非常热门的话题,许多开发者都在大量使用这个概念。我个人认为它很差。我不是批评使用它的人,如果这对您的应用程序有效,请继续这样做,但我将解释为什么我不使用它。
据我所知,使用可调用控制器的开发者使用它来避免让控制器变得非常庞大。我完全理解这一点,但在我看来,即使控制器中有多个方法,控制器也不应该是庞大的。事实上,我在过去5年里每天都在为著名的产品产品:Laravel的< sottolineato>API工作,我的所有控制器方法最多只包含5行代码,这是因为我只将Controller用作强< 通信层
- 获取
请求
- 映射输入属性
- 将映射后的输入发送到
服务层
- 从
服务层
获取结果,并发送响应
AaaS模型
我真的很喜欢动作类(Action)的概念,但我上面提到,对我来说,将其实现为可调用控制器还没有意义。因此,我一直以不同的方式使用动作类,将它们用作我的服务层
。这正是我将这种模式命名为AaaS-动作作为服务的意义。
这就像MVC这样的一个架构模式
。尽管我创建这种模式是为了Laravel
,但如果您愿意,您也可以将其应用于其他框架和其他编程语言。这个模式有四个原则,以下将解释。
瘦通信层
通信层是接收用户输入的应用程序层。在Web
应用程序中,这是我们的控制器
,在CLI
中进行应用程序中,是命令(commands)
。这层应该只
- 接收用户输入
- 映射输入属性
- 将映射后的输入发送到服务层
- 从服务层获取结果并将其发送给用户
分离验证
数据验证不应与通信层
相关联。这意味着数据验证应该与数据本身或与之相关的动作本身关联。在Laravel应用程序中,这意味着不应该使用请求表单等方式进行数据验证,而是在DTO或动作本身中进行。
映射输入
执行操作所需的输入应映射到时需要的多个输入属性的DTO(数据传输对象)中,以提高应用程序中操作的代码质量和可维护性。
单一目的操作
应用程序的所有业务逻辑应位于操作类中,并且每个操作类应负责一个单独的操作。如果有必要,您可以在一个操作中调用另一个操作,以避免代码重复。
如何实现AaaS模式
既然你已经知道了什么是AaaS模式,让我们看一个简单的例子,说明如何在Laravel应用中应用它。让我们想象一个简单的API CRUD实现,用于我们应用程序的用户。
首先,让我们看一下UserController
类。
namespace App\Http\Controllers\Users;
use App\Actions\Users\DeleteUser;
use App\Actions\Users\FetchUsers;
use App\Actions\Users\SaveUser;
use App\Actions\Users\ShowUser;
use App\DTOs\Users\FetchUsersDTO;
use App\DTOs\Users\SaveUserDTO;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class UserController extends Controller
{
/**
* @param Request $request
* @param FetchUsers $action
* @return JsonResponse
*/
public function index(Request $request, FetchUsers $action): JsonResponse
{
return response()->json($action->handle(FetchUsersDTO::fromRequest($request)));
}
/**
* @param int $userId
* @param ShowUser $action
* @return JsonResponse
*/
public function show(int $userId, ShowUser $action): JsonResponse
{
return response()->json($action->handle($userId));
}
/**
* @param Request $request
* @param SaveUser $action
* @return JsonResponse
*/
public function store(Request $request, SaveUser $action): JsonResponse
{
return response()->json($action->handle(SaveUserDTO::fromRequest($request)), Response::HTTP_CREATED);
}
/**
* @param Request $request
* @param int $userId
* @param SaveUser $action
* @return JsonResponse
*/
public function update(Request $request, int $userId, SaveUser $action): JsonResponse
{
return response()->json($action->handle(SaveUserDTO::fromRequest($request), $userId), Response::HTTP_OK);
}
/**
* @param int $userId
* @param DeleteUser $action
* @return JsonResponse
*/
public function destroy(int $userId, DeleteUser $action): JsonResponse
{
$action->handle($userId);
return response()->noContent();
}
}
正如你所看到的,控制器中的所有方法都非常简单,遵循了薄通信层原则。
对于分离验证和映射输入原则,我将使用我创建的一个包::Validated DTO来简化DTO的使用,但你也可以使用你选择的任何包,甚至完全不使用任何包。
让我们看看FetchUsersDTO
和SaveUserDTO
类。
namespace App\DTOs\Users;
use WendellAdriel\ValidatedDTO\Casting\BooleanCast;
use WendellAdriel\ValidatedDTO\Casting\IntegerCast;
use WendellAdriel\ValidatedDTO\ValidatedDTO;
class FetchUsersDTO extends ValidatedDTO
{
public int $page;
public int $per_page;
public bool $active_only;
/**
* @return array
*/
protected function rules(): array
{
return [
'page' => ['sometimes', 'integer'],
'per_page' => ['sometimes', 'integer'],
'active_only' => ['sometimes', 'boolean'],
];
}
/**
* @return array
*/
protected function defaults(): array
{
return [
'page' => 1,
'per_page' => 20,
'active_only' => true,
];
}
/**
* @return array
*/
protected function casts(): array
{
return [
'page' => new IntegerCast(),
'per_page' => new IntegerCast(),
'active_only' => new BooleanCast(),
];
}
}
namespace App\DTOs\Users;
use WendellAdriel\ValidatedDTO\Casting\StringCast;
use WendellAdriel\ValidatedDTO\ValidatedDTO;
class SaveUserDTO extends ValidatedDTO
{
public string $name;
public string $email;
/**
* @return array
*/
protected function rules(): array
{
return [
'name' => ['required', 'string'],
'email' => ['required', 'email'],
];
}
/**
* @return array
*/
protected function defaults(): array
{
return [];
}
/**
* @return array
*/
protected function casts(): array
{
return [
'name' => new StringCast(),
'email' => new StringCast(),
];
}
}
正如你所看到的,现在数据验证是在DTO中进行的,与数据本身相关联,而不是与通信层相关联。如果我从CLI命令或另一个操作调用需要使用DTO的动作,如果验证是与通信层绑定的,例如使用
现在,对于最后一个原则——单一目的操作——让我们看看我们的FetchUsers
、ShowUser
、SaveUser
和DeleteUser
操作类。
namespace App\Actions\Users;
use App\DTOs\Users\FetchUsersDTO;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
class FetchUsers
{
/**
* @param FetchUsersDTO $dto
* @return Collection
*/
public function handle(FetchUsersDTO $dto): Collection
{
$query = User::query();
if ($dto->active_only) {
$query->where('is_active', true);
}
return $query->skip(($dto->page - 1) * $dto->per_page)
->take($dto->per_page)
->get();
}
}
namespace App\Actions\Users;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class ShowUser
{
/**
* @param int $userId
* @return User
*
* @throws ModelNotFoundException
*/
public function handle(int $userId): User
{
return User::query()->findOrFail($userId);
}
}
namespace App\Actions\Users;
use App\DTOs\Users\SaveUserDTO;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class SaveUser
{
public __construct(
private ShowUser $showAction
) {}
/**
* @param SaveUserDTO $dto
* @param int|null $userId
* @return User
*
* @throws ModelNotFoundException
*/
public function handle(SaveUserDTO $dto, ?int $userId = null): User
{
$user = is_null($userId)
? new User()
: $this->showAction->handle($userId);
$user->fill($dto->toArray());
$user->save();
return $user;
}
}
namespace App\Actions\Users;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class DeleteUser
{
public __construct(
private ShowUser $showAction
) {}
/**
* @param int $userId
* @return void
*
* @throws ModelNotFoundException
*/
public function handle(int $userId): void
{
$user = $this->showAction->handle($userId);
$user->delete();
}
}
正如你所看到的,每个操作类只有一个单个目的。对于更复杂的情况,你甚至可以将救操动和/或DTO
分割成两个不同的::startup
和代码:UpdateUser
动作操作和startup_updateUserDTO
和更新updateUserDTO数据传输对象。在这个简化的示例中不需要,这就是为什么它被合并到一个
Save动作:
中。
结论
正如我在文章开始时所说的那样,有各种各样的方法来接近使用
我还介绍了我创建的一种模式,即AaaS模式,你可以将其应用于你所从事的任何项目,并且这是一种使代码库易于工作、维护和更新的方法。
我希望你喜欢这篇文章,如果你喜欢,别忘了与你的朋友分享这篇文章!!!再见!
driesvints, thinkverse, sabotazh, sairahcaz, vasotelvi, elkdev, ruben-vb, roquib, sirony, innomatrix 等人喜欢了这篇文章