Giới Thiệu
Nếu bạn là một lập trình viên Laravel và yêu thích Blade, bạn sẽ hiểu rằng nó đơn giản, biểu cảm và kết hợp tự nhiên với framework. Tuy nhiên, nhiều dự án thường biến các file Blade thành nơi chứa đựng các biến ngẫu nhiên mà không có cấu trúc. Điều này dẫn đến việc Controllers truyền dữ liệu lỏng lẻo, views nhận dữ liệu không có cấu trúc, các partials phụ thuộc vào những giả định ngầm, và toàn bộ hệ thống trở nên mong manh và dễ gặp lỗi.
Kết Quả Là Gì?
- Không có tính năng tự động hoàn thành.
- Lỗi thời gian chạy do sai chính tả.
- Các partial views không có hợp đồng.
- Một mớ hỗn độn ngày càng lớn khi ứng dụng phát triển.
Nhưng mọi thứ không cần phải như vậy. Các file Blade xứng đáng nhận được các hợp đồng và cấu trúc giống như chúng ta dành cho APIs, các mô hình cơ sở dữ liệu hoặc dịch vụ. Với một vài thực hành rõ ràng, bạn có thể mang lại kỷ luật, hợp đồng và cấu trúc cho Blade.
Bài viết này sẽ chỉ cho bạn cách tiếp cận hoàn chỉnh:
- Vấn đề truyền dữ liệu dưới dạng mảng hoặc biến trong view — được giải quyết bằng cách giới thiệu ViewModels như hợp đồng.
- Kích hoạt Autocomplete trong Blade để IDE của bạn thực sự hỗ trợ.
- Chú thích cho views và partials gắn liền với ViewModels của chúng.
- Khóa chặt cấu trúc dữ liệu với DTO classes cho kiểu dữ liệu nghiêm ngặt.
- Thiết lập Nguyên Tắc cho phát triển Blade bền vững.
Nhớ rằng, chúng ta chỉ đang nhắm đến Blade ở thời điểm này. Không phải Twig hay Antlr.
Hãy làm một cốc cà phê — đây là một bài đọc dài. Nhưng đến cuối bài, bạn sẽ không bao giờ nhìn Blade theo cách cũ nữa.
Tình Huống Vấn Đề: Vấn Đề Với “Soup Mảng” Trong Blade
Dưới đây là cách tiếp cận tiêu chuẩn trong Laravel mà chúng ta đã thấy trong các controllers.
php
class ProductController extends Controller
{
public function index()
{
$products = Product::all();
$categories = Category::all();
$brand = Brand::all();
$title = 'Products';
return view('products.index', compact('products','categories','brands','title') );
}
}
Và đây là cách chúng ta làm với các partials.
php
@foreach ($products as $product)
@include('products.partials.product')
@endforeach
Cuối cùng là cho các components.
php
@foreach ($products as $product)
<x-product-row :name="$product->name" :price ="$product->price" />
@endforeach
Blade Cũ Yêu Thích
Dưới đây là cách mà nó hoạt động tốt.
php
<h1>{{ $title }}</h1>
<ul>
@foreach ($products as $product)
<li>{{ $product->name }} ({{ $product->price }})</li>
@include('products.partials.product')
<x-product-row :name="$product->name" :price ="$product->price" />
@endforeach
</ul>
Điều này hoạt động nhưng hãy tự hỏi:
- Bạn có thực sự nhớ tất cả các biến được truyền vào views không?
- Bạn có nhận được tính năng tự động hoàn thành trong Blade để giúp bạn phát hiện lỗi chính tả không?
- Các partials và includes — bạn chắc chắn rằng dữ liệu mà chúng phụ thuộc thực sự có ở đó?
- Nếu một đồng nghiệp chỉnh sửa controller, bạn làm cách nào để biết view không bị lỗi một cách âm thầm?
- Có bao nhiêu lần bạn mở một file Blade mà không có ý tưởng gì về nội dung trong phạm vi?
1. Giới Thiệu ViewModels: Hợp Đồng Cho Views Của Bạn
Thay vì ném mảng quanh, hãy định nghĩa một ViewModel, một DTO class đơn giản. Nó khai báo chính xác dữ liệu nào được mong đợi.
php
<?php
namespace App\ViewModels;
class ProductsViewModel
{
public string $title;
public array $products;
public array $categories;
public array $brand;
}
Và controller.
php
class ProductController extends Controller
{
public function index()
{
$viewModel = new ProductsViewModel();
$viewModel->title = 'Products';
$viewModel->products = Product::all();
$viewModel->categories = Category::all();
$viewModel->brands = Brand::all();
return view('products.index', ['model' => $viewModel]);
}
}
Gợi ý: Luôn truyền view-model với tên model để nhất quán. Điều này sẽ giúp trong việc tự động hoàn thành.
Bây giờ file blade sẽ trông như thế này.
php
<h1>{{ $model->title }}</h1>
<ul>
@foreach ($model->products as $product)
<li>{{ $product->name }} ({{ $product->price }})</li>
@include('products.partials.product',['model' => $product])
<x-product-row :model="$product" />
@endforeach
</ul>
@include('partials.brands', ['model' => $model->brands])
@include('partials.categories', ['model' => $model->categories])
Gợi ý: Đối với các partials và components, luôn truyền dữ liệu với tên model để nhất quán. Điều này sẽ giúp trong việc tự động hoàn thành.
Lợi ích:
- Hợp đồng rõ ràng.
- Thêm hoặc xóa dữ liệu yêu cầu cập nhật ViewModel.
- Mỗi view, partial hoặc component có một điểm truy cập dữ liệu duy nhất $model để bắt đầu.
2. Cài Đặt Tiện Ích Để Tự Động Hoàn Thành
Bước này không yêu cầu bất kỳ thiết lập mã nào nhưng cần cài đặt plugin/tiện ích trong IDE của bạn. Tôi sử dụng VS Code cho phát triển Laravel và đã tìm thấy một tiện ích tuyệt vời là PHP Tools của DevSense cho VS Code mà tôi đã sử dụng trong Visual Studio khi làm việc với .Net framework. Tiện ích này có sẵn cho Cursor, Zed, Visual Studio và VS Code. Nhưng nếu bạn đang sử dụng một trình soạn thảo khác, bạn cần tìm tiện ích tương tự.
Sau khi cài đặt, chúng ta cần chuyển sang bước tiếp theo. Hãy xem danh sách tính năng để tìm những gì có sẵn.
Lưu ý: Khi cài đặt PHP Tools, tôi đã phải bỏ Intellisense và Intelliphence nhưng tôi vẫn nhận được tất cả những gì mà họ cung cấp với PHP Tools.
3. Kích Hoạt Tự Động Hoàn Thành Trong Blade
Ngay cả với ViewModels, Blade không tự động biết loại của $model. Mẹo là bạn cần chú thích nó trong các view blade, partials và components:
php
// views/products/index.blade.php
@php
/** @var \App\ViewModels\ProductsViewModel $model */
@endphp
<ul>
@foreach ($model->products as $product)
<li>{{ $product->name }} ({{ $product->price }})</li>
@include('products.partials.product')
<x-product-row :name="$product->name" :price ="$product->price" />
@endforeach
</ul>
Bạn cần làm tương tự trong partial hoặc component. Dưới đây là partial.
php
// views/products/partials/product.blade.php
@php
/** @var \App\Models\Product $model */
@endphp
<span>
{{ $model->name }}
</span>
Cuối cùng, là một component.
php
// views/components/product.blade.php
@php
/** @var \App\View\Components\Product $model */
@endphp
<span>
{{ $model->name }}
</span>
Bây giờ, trong VS Code, gõ `$model->`
sẽ hiện lên những gợi ý tự động hoàn thành thực sự.
Đây là một bước ngoặt:
- Không còn phải đoán tên thuộc tính nữa.
- Các biến sai chính tả được phát hiện ngay lập tức.
- Các lập trình viên mới biết chính xác những gì có sẵn trong view.
4. Thực Thi Kiểu Dữ Liệu Tại Thời Điểm Chạy
Kể từ bây giờ chúng ta đang truyền ViewModels vào views, partials hoặc components và định nghĩa các chú thích trong mỗi file blade, hãy buộc Blade chấp nhận một loại lớp cụ thể. Để làm điều này, tôi đã thêm hai file:
- TypedViewFactory – Một factory view tùy chỉnh phân tích các khai báo
`@var`
ở đầu các file Blade và thực thi chúng. - TypedViewServiceProvider – Kết nối mọi thứ vào Laravel tự động.
Kiểm tra mã ở đây.
Kết Quả: nếu bạn khai báo một loại trong một file Blade, Laravel phải truyền lớp chính xác, mảng hoặc collection. Nếu không, nó sẽ ném một ngoại lệ.
Khai báo các loại trong Blade làm cho các view trở nên dự đoán được và nghiêm ngặt.
View Chính
php
{{-- products/index.blade.php --}}
@php /** @var App\ViewModels\ProductsViewModel $model */ @endphp
<h1>{{ $model->title }}</h1>
Chỉ `ProductsViewModel`
hoạt động — bất cứ điều gì khác sẽ ném một ngoại lệ.
Partial
php
{{-- products/partials/product.blade.php --}}
@php
/** @var App\Models\Product $model */
@endphp
<div>{{ $model->title }}</div>
Component
php
// views/components/product.blade.php
@php
/** @var \App\View\Components\Product $model */
@endphp
<span>
{{ $model->name }}
</span>
Bây giờ nếu dữ liệu sai loại được truyền vào, nó sẽ ném lỗi.
plaintext
View [products] expects $model of type App\ViewModels\ProductsViewModel,
but got string.
5. Đi Tới Việc Định Kiểu Hoàn Toàn Với DTO Classes
Cho đến nay, chúng ta đã gói dữ liệu trong ViewModels, nhưng dữ liệu thực sự thì sao? Việc truyền các mô hình Eloquent thô vào views có thể rủi ro — chúng mang theo quá nhiều hành lý và tự động hoàn thành sẽ hiển thị cho bạn một khu rừng các phương thức eloquent cùng với các thuộc tính mô hình và chúng ta sẽ phải đấu tranh để tìm ra các thuộc tính lần nữa. Để khắc phục vấn đề này, hãy tạo ra DTO Classes để mô tả chính xác những gì cần thiết.
Lưu ý: Vì
component
có thể hoạt động nhưDTO class
và mang theo các thuộc tính cụ thể, nó không cần thêm lớp DTO nào khác.
Hãy tạo một lớp BaseDTO để định nghĩa một số phương thức chung.
php
<?php
namespace App\DTO;
use InvalidArgumentException;
abstract class BaseDTO
{
public function __construct(array $data = [])
{
foreach ($data as $key => $value) {
if (!property_exists($this, $key)) {
throw new InvalidArgumentException(
"Property '{$key}' is not defined in " . static::class
);
}
$this->$key = $value;
}
}
public function __get($name)
{
throw new InvalidArgumentException(
"Tried to access undefined property '{$name}' on " . static::class
);
}
public function __set($name, $value)
{
throw new InvalidArgumentException(
"Tried to set undefined property '{$name}' on " . static::class
);
}
public static function columns(): array
{
return array_map(
fn($prop) => $prop->getName(),
(new \ReflectionClass(static::class))->getProperties()
);
}
}
Bây giờ lớp SimpleProduct.php của chúng ta sẽ chỉ định nghĩa các thuộc tính cụ thể cần thiết cho view hoặc partial Blade.
php
<?php
namespace App\DTO\Product;
use App\DTO\BaseDTO;
class SimpleProduct extends BaseDTO
{
public string $name;
public string $image;
public float $price;
public int $stock;
}
Và đây là controller của chúng ta bây giờ.
php
class ProductController extends Controller
{
public function index()
{
$viewModel = new ProductsViewModel();
$viewModel->products = Product::select(SimpleProduct::columns())->get();
return view('products.index', ['model' => $viewModel]);
}
}
Và trong partial, thay vì định nghĩa mô hình Eloquent như một chú thích, hãy định nghĩa lớp DTO như một chú thích.
php
// views/products/partials/product.blade.php
@php
/** @var \App\DTO\Product\SimpleProduct $model */
@endphp
<span>
{{ $model->name }}
</span>
Bây giờ tự động hoàn thành sẽ chỉ hiển thị cho bạn các thuộc tính DTO.
Nhưng đợi đã. Chúng ta đã truyền mô hình Eloquent nhưng chú thích lớp DTO. Điều này có vẻ như thế nào?
Chú ý rằng chúng ta chỉ chọn các cột cụ thể đúng không? Vì vậy mặc dù chúng ta đã truyền Eloquent, do chú thích, chúng ta sẽ thấy chỉ các thuộc tính DTO. Nhưng chúng ta phải tuân theo một điều kiện.
Gợi ý: Các thuộc tính của lớp DTO phải giống với các cột của mô hình Eloquent.
Kết Luận
Blade không bị hỏng. Cách mà hầu hết các lập trình viên sử dụng nó thì có.
Việc đối xử với các file Blade như những thùng rác không có cấu trúc tạo ra mã dễ gặp lỗi và không thể bảo trì. Việc thêm ViewModels, tự động hoàn thành, các hợp đồng partial và DTOs sẽ mang lại cho các view của bạn cùng một mức độ chuyên nghiệp như phần còn lại của codebase của bạn.
Đây không phải là quá mức. Đây là kỷ luật. Và kỷ luật chính là điều phân biệt giữa các dự án mong manh và bền vững.
Vì vậy, hãy ngừng truyền mảng. Hãy bắt đầu truyền hợp đồng. Đem lại cho các file Blade của bạn cấu trúc mà chúng xứng đáng.
Nếu bạn thấy bài viết này hữu ích, hãy xem xét việc ủng hộ công việc của tôi — điều đó có ý nghĩa rất nhiều với tôi.