檢視模型是簡化 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 協議》,轉載必須註明作者和本文連結