Laravel view models [翻译]
视图模型是简化 controller
和 model
代码的抽象。视图模型负责向视图提供数据,否则这些数据将直接来自 controller
或 model
。它们可以更好地分离关注点,并为开发人员提供更多灵活性。
从本质上讲,视图模型是一些简单的类,可以获取一些数据,并将其转换为可用于视图的内容。在这篇文章中,我将向您展示该模式的基本原理,我们将看看它们如何集成到 Laravel 项目中,最后我将向您展示在 Spatie 公司,项目中使用该模式。
让我们开始吧。假设您有一个表单来创建一个带有 category
[类别] 的博客文章。您需要一种方法来填充视图中的选择框以及 category
选项。controller
必须提供这些。
public function create()
{
return view('blog.form', [
'categories' => Category::all(),
]);
}
以上示例适用于 create
方法,但不要忘记我们也应该能够编辑现有帖子。
public function edit(Post $post)
{
return view('blog.form', [
'post' => $post,
'categories' => Category::all(),
]);
}
接下来是一项新的业务要求:应限制用户允许发布的类别。换句话说:应根据用户限制类别选择。
return view('blog.form', [
'categories' => Category::allowedForUser(
current_user()
)->get(),
]);
这种方法不能扩展。您必须在 create
和 edit
方法中更改代码。你能想象当你需要在帖子中添加标签时会发生什么吗?或者,如果有另一个特殊的管理表单来创建和编辑帖子?
下一个解决方案是让 post
模型本身提供类别,如下所示:
class Post extends Model
{
public static function allowedCategories(): Collection
{
return Category::query()
->allowedForUser(current_user())
->get();
}
}
虽然这种情况经常发生在 Laravel 项目中,但有很多原因可以解释为什么这是一个坏主意。让我们关注我们案例中相关的问题:它仍然允许重复。
说有一个新的模型 News
也需要相同的类别选择。这会导致重复,但在模型级别而不是在控制器中。
另一种选择是将方法放在 User
模型上。这最有意义,但也使维护更难。想象一下,我们正在使用前面提到的标签。他们不依赖于用户。现在我们需要从用户模型中获取类别,并从其他地方获取标签。
我希望使用模型很清楚,做为视图的数据提供者这并不是银弹。
总之,无论您何时尝试从中获取类别,总会出现一些代码重复。这使得维护和推理代码变得更加困难。
这是视图模型发挥作用的地方。它们封装了所有这些逻辑,以便可以在不同的地方重用它们。他们只能一对一:为视图提供正确的数据。
class PostFormViewModel
{
public function __construct(
User $user,
Post $post = null
) {
$this->user = $user;
$this->post = $post;
}
public function post(): Post
{
return $this->post ?? new Post();
}
public function categories(): Collection
{
return Category::allowedForUser($this->user)->get();
}
}
让我们说出这样一个类的几个关键特征:
- 注入所有依赖项,这为外部提供了最大的灵活性。
- 视图模型公开了视图可以使用的一些方法。
post
方法会提供新帖子或现有帖子,具体取决于您是创建还是编辑帖子。
这就是控制器的样子:
class PostsController
{
public function create()
{
$viewModel = new PostFormViewModel(
current_user()
);
return view('blog.form', compact('viewModel'));
}
public function edit(Post $post)
{
$viewModel = new PostFormViewModel(
current_user(),
$post
);
return view('blog.form', compact('viewModel'));
}
}
最后视图可以像这样使用它:
<input value="{{ $viewModel->post()->title }}" />
<input value="{{ $viewModel->post()->body }}" />
<select>
@foreach ($viewModel->categories() as $category)
<option value="{{ $category->id }}">
{{ $category->name }}
</option>
@endforeach
</select>
这些是使用视图模型的两个好处:
- 它们封装了逻辑
- 它们可以在多种环境中重用
在 Laravel 中查看模型
前面的示例显示了一个带有一些方法的简单类。这足以使用该模式,但在 Laravel 项目中,我们可以添加一些细节。
例如,如果视图模型实现 Arrayable
,则可以将视图模型直接传递给 view
函数。
public function create()
{
$viewModel = new PostFormViewModel(
current_user()
);
return view('blog.form', $viewModel);
}
该视图现在可以直接使用视图模型的属性,如 $post
和 $categories
。上一个示例现在看起来像这样:
<input value="{{ $post->title }}" />
<input value="{{ $post->body }}" />
<select>
@foreach ($categories as $category)
<option value="{{ $categroy->id}}">
{{ $category->name }}
</option>
@endforeach
</select>
您还可以通过实现 Responsable
将视图模型本身作为 JSON 数据返回。当您通过 AJAX 调用保存表单并希望在调用完成后使用最新数据重新填充表单时,这非常有用。
public function update(Request $request, Post $post)
{
// Update the post...
return new PostFormViewModel(
current_user(),
$post
);
}
您可能会在视图模型和 Laravel 资源之间看到相似之处。请记住,当视图模型可以提供他们想要的任何数据时,资源会在模型上一对一映射。
在我们的一个项目中,我们实际上在视图模型中使用资源!
class PostViewModel
{
// ...
public function values(): array
{
return PostResource::make(
$this->post ?? new Post()
)->resolve();
}
}
最后,在这个项目中,我们正在使用需要 JSON 数据的 Vue 表单组件。在调用魔术 getter 时,我们创建了一个抽象来提供这个 JSON 数据而不是对象或数组:
abstract class ViewModel
{
// ...
public function __get($name): ?string
{
$name = Str::camel($name);
// Some validation...
$values = $this->{$name}();
if (! is_string($values)) {
return json_encode($values);
}
return $values;
}
}
我们可以调用它们的属性并获取 JSON,而不是调用视图模型方法。
<select-field
label="{{ __('Post category') }}"
name="post_category_id"
:options="{{ $postViewModel->post_catrgories}}"
></select-field>
等等,view composers 怎么样?
我听到了!关于该主题,有一篇完整的博客文章。你可以在这里 阅读它。
总之,视图模型可以是在控制器中处理数据的可行替代方案。它们允许更好的可重用性并封装不属于控制器的逻辑。
使用它们时,您也不仅仅限于表格。在 Spatie,我们还使用它们来填充 facet 过滤器选项,基于用户当前正在使用的复杂上下文。
我建议尝试这种模式。顺便说一句,你不需要任何东西。上面列出的所有 Laravel 花招都是可选的,可以根据你的使用情况添加。
如果你想使用 Laravel 花招,我们有一个扩展包:spatie/laravel-view-models :grinning:。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: