支持Laravel.io的持续发展 →

Laravel AaaS - 行为即服务

2023年1月23日 阅读时间 9分钟

引言

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用作强< 通信层,正如我在这篇文章中提到的:https://wendelladriel.com/blog/structuring-a-server-side-application。因此,对我来说,创建一个只有几行代码的文件是没有意义的。在我看来,控制器应该只用于

  1. 获取请求
  2. 映射输入属性
  3. 将映射后的输入发送到服务层
  4. 服务层获取结果,并发送响应

AaaS模型

我真的很喜欢动作类(Action)的概念,但我上面提到,对我来说,将其实现为可调用控制器还没有意义。因此,我一直以不同的方式使用动作类,将它们用作我的服务层。这正是我将这种模式命名为AaaS-动作作为服务的意义。

这就像MVC这样的一个架构模式。尽管我创建这种模式是为了Laravel,但如果您愿意,您也可以将其应用于其他框架和其他编程语言。这个模式有四个原则,以下将解释。

瘦通信层

通信层是接收用户输入的应用程序层。在Web应用程序中,这是我们的控制器,在CLI中进行应用程序中,是命令(commands)。这层应该只

  1. 接收用户输入
  2. 映射输入属性
  3. 将映射后的输入发送到服务层
  4. 从服务层获取结果并将其发送给用户

分离验证

数据验证不应与通信层相关联。这意味着数据验证应该与数据本身或与之相关的动作本身关联。在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的使用,但你也可以使用你选择的任何包,甚至完全不使用任何包。

让我们看看FetchUsersDTOSaveUserDTO类。

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的动作,如果验证是与通信层绑定的,例如使用表单请求,我就不需要手动验证数据。

现在,对于最后一个原则——单一目的操作——让我们看看我们的FetchUsersShowUserSaveUserDeleteUser操作类。

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模式,你可以将其应用于你所从事的任何项目,并且这是一种使代码库易于工作、维护和更新的方法。

我希望你喜欢这篇文章,如果你喜欢,别忘了与你的朋友分享这篇文章!!!再见!

最后更新1年前。

driesvints, thinkverse, sabotazh, sairahcaz, vasotelvi, elkdev, ruben-vb, roquib, sirony, innomatrix 等人喜欢了这篇文章

13
喜欢这篇文章吗? 让作者知道,并给他们鼓掌!
wendell_adriel(Wendell Adriel) PHP/Laravel专业Web工匠 😎开源爱好者 🔥 帮助你提高技能 💪 13+ 年网络开发经验 🤘 指导了数十名开发者 🎓

你可能还喜欢这些文章

如何使用 Larastan 将您的 Laravel 应用从0升级到9

在 Laravel 应用执行之前就 发现错误是可能的,感谢 Larastan...

阅读文章

无需 traits 标准化 API 响应

我发现大多数用于 API 响应的库都是使用 traits 实现的...

阅读文章

在 Laravel 项目中通过 Discord 通知收集反馈

如何创建一个反馈模块并在接收 Discord 通知时...

阅读文章

我们想感谢这些 令人难以置信的公司 支持我们

您的标志在这里?

Laravel.io

Laravel 问题解决、知识共享和社区建设的门户。

© 2024 Laravel.io - 版权所有。