added support for legacy tickets
This commit is contained in:
+461
@@ -0,0 +1,461 @@
|
||||
# Stop Treating Your Blade Files Like Trash Bins. Give Them Contracts And Structure
|
||||
|
||||
Are you a Laravel developer? Do you love Blade? It’s simple, expressive, and fits naturally with the framework. But
|
||||
let’s be honest: most projects eventually turn their Blade files into dumping grounds for random variables. The
|
||||
Controllers pass loose data, views end up receiving unstructured data, partials rely on implicit assumptions, and the
|
||||
whole setup becomes fragile and error-prone.
|
||||
|
||||
## The Result?
|
||||
|
||||
- No autocomplete.
|
||||
- Runtime errors from typos.
|
||||
- Partial views with zero contracts.
|
||||
- A mess that only grows as the app scales.
|
||||
|
||||
It doesn’t have to be this way. Blade files deserve the same contracts and structure we give to APIs, database models,
|
||||
or services. With a few clear practices, you can bring discipline, contracts, and structure to Blade.
|
||||
|
||||
This article will show you the complete approach:
|
||||
|
||||
1. The problem of passing data as arrays or variables in views — solved by introducing ViewModels as contracts.
|
||||
2. Enable Autocomplete in Blade so your IDE actually helps.
|
||||
3. Annotation to views and partials bound to their own ViewModels.
|
||||
4. Lock down data structures with DTO classes for strict typing.
|
||||
5. Establish Principles for sustainable Blade development.
|
||||
|
||||
**Remember, we are only targeting Blade for now.** Not Twig or Antlr.
|
||||
Grab a coffee — this is a long read. But by the end, you’ll never look at Blade the same way again.
|
||||
|
||||
---
|
||||
|
||||
## The Crime Scene: What’s Wrong With “Array Soup” in Blade
|
||||
|
||||
Here’s the standard Laravel approach we’ve all seen in 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') );
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or maybe:
|
||||
|
||||
```php
|
||||
return view('products.index', [
|
||||
'title' => 'Products',
|
||||
'products' => Product::all(),
|
||||
'categories' => Category::all(),
|
||||
'brand' => Brand::all(),
|
||||
]);
|
||||
```
|
||||
|
||||
Or a slightly cleaner:
|
||||
|
||||
```php
|
||||
$data = [
|
||||
'title' => 'Products',
|
||||
'products' => Product::all(),
|
||||
'categories' => Category::all(),
|
||||
'brand' => Brand::all(),
|
||||
];
|
||||
|
||||
return view('products.index', $data);
|
||||
```
|
||||
|
||||
And here is how we do it for partials:
|
||||
|
||||
```blade
|
||||
@foreach ($products as $product)
|
||||
@include('products.partials.product')
|
||||
@endforeach
|
||||
|
||||
// or pass some additional data
|
||||
@foreach ($products as $product)
|
||||
@include('products.partials.product', ['editable' => $editable])
|
||||
@endforeach
|
||||
```
|
||||
|
||||
And finally for components:
|
||||
|
||||
```blade
|
||||
@foreach ($products as $product)
|
||||
<x-product-row :name="$product->name" :price ="$product->price" />
|
||||
@endforeach
|
||||
```
|
||||
|
||||
### The Old Lovely Blade
|
||||
|
||||
Here’s how it works well:
|
||||
|
||||
```blade
|
||||
<h1>{{ $title }}</h1>
|
||||
|
||||
<ul>
|
||||
@foreach ($products as $product)
|
||||
<li>{{ $product->name }} ({{ $product->price }})</li>
|
||||
{{-- or partial --}}
|
||||
@include('products.partials.product')
|
||||
{{-- or component --}}
|
||||
<x-product-row :name="$product->name" :price ="$product->price" />
|
||||
@endforeach
|
||||
</ul>
|
||||
```
|
||||
|
||||
But ask yourself:
|
||||
|
||||
- Do you remember all the variables passed to your views?
|
||||
- Do you get autocomplete in Blade to help you catch typos?
|
||||
- What about partials and includes — are you sure the data they rely on is really there?
|
||||
- If a teammate edits the controller, how do you even know the view won’t silently break?
|
||||
- How many times have you opened a Blade file and had no clue what’s in scope?
|
||||
|
||||
---
|
||||
|
||||
## 1. Introducing ViewModels: Contracts for Your Views
|
||||
|
||||
Instead of throwing arrays around, define a ViewModel — a simple DTO class. It declares exactly what data is expected.
|
||||
|
||||
```php
|
||||
namespace App\ViewModels;
|
||||
|
||||
class ProductsViewModel
|
||||
{
|
||||
public string $title;
|
||||
public array $products;
|
||||
public array $categories;
|
||||
public array $brand;
|
||||
}
|
||||
```
|
||||
|
||||
And in your 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]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Suggestion: Always pass view‑model with name `model` for consistency. This will help in autocomplete.
|
||||
|
||||
Now the Blade file might look like:
|
||||
|
||||
```blade
|
||||
<h1>{{ $model->title }}</h1>
|
||||
<ul>
|
||||
@foreach ($model->products as $product)
|
||||
<li>{{ $product->name }} ({{ $product->price }})</li>
|
||||
{{-- or partial --}}
|
||||
@include('products.partials.product',['model' => $product])
|
||||
{{-- or component --}}
|
||||
<x-product-row :model="$product" />
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
@include('partials.brands', ['model' => $model->brands])
|
||||
@include('partials.categories', ['model' => $model->categories])
|
||||
```
|
||||
|
||||
Again, the suggestion: for partials and components, always pass data with name `$model` for consistency to aid
|
||||
autocomplete.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- The contract is explicit.
|
||||
- Adding or removing data requires updating the ViewModel.
|
||||
- Each view, partial, or component has a single data access point `$model`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Installing Extension for Autocomplete
|
||||
|
||||
This does not require code setup, but you’ll need to install a plugin/extension in your IDE. The author uses VS Code and
|
||||
recommends **PHP Tools by DevSense** (also available for other editors).
|
||||
|
||||
If you use a different editor, find a similar extension — support for Blade autocomplete is key.
|
||||
|
||||
---
|
||||
|
||||
## 3. Enabling Autocomplete in Blade
|
||||
|
||||
Even with ViewModels, Blade doesn’t automatically know the type of `$model`. The trick is to annotate it in Blade views,
|
||||
partials, and components:
|
||||
|
||||
```blade
|
||||
{{-- views/products/index.blade.php --}}
|
||||
@php
|
||||
/** @var \App\ViewModels\ProductsViewModel $model */
|
||||
@endphp
|
||||
<ul>
|
||||
@foreach ($model->products as $product)
|
||||
<li>{{ $product->name }} ({{ $product->price }})</li>
|
||||
{{-- or partial --}}
|
||||
@include('products.partials.product')
|
||||
{{-- or component --}}
|
||||
<x-product-row :name="$product->name" :price="$product->price" />
|
||||
@endforeach
|
||||
</ul>
|
||||
```
|
||||
|
||||
In partials or components:
|
||||
|
||||
```blade
|
||||
{{-- views/products/partials/product.blade.php --}}
|
||||
@php
|
||||
/** @var \App\Models\Product $model */
|
||||
@endphp
|
||||
|
||||
<span>
|
||||
{{ $model->name }}
|
||||
</span>
|
||||
```
|
||||
|
||||
And in components:
|
||||
|
||||
```blade
|
||||
{{-- views/components/product.blade.php --}}
|
||||
@php
|
||||
/** @var \App\View\Components\Product $model */
|
||||
@endphp
|
||||
|
||||
<span>
|
||||
{{ $model->name }}
|
||||
</span>
|
||||
```
|
||||
|
||||
Now, in an IDE like VS Code, typing `$model->` will give you real autocomplete suggestions.
|
||||
|
||||
This is a game-changer:
|
||||
|
||||
- No more guessing property names.
|
||||
- Mistyped variables get caught instantly.
|
||||
- New developers know exactly what’s available in the view.
|
||||
|
||||
---
|
||||
|
||||
## 4. Enforce Types at Runtime
|
||||
|
||||
Since now we pass ViewModels into views, partials, or components and define annotations, let’s enforce Blade to accept
|
||||
only specific types. The author adds:
|
||||
|
||||
1. **TypedViewFactory** – A custom view factory that parses `@var` declarations at the top of Blade files and enforces
|
||||
them.
|
||||
2. **TypedViewServiceProvider** – Wires everything into Laravel automatically.
|
||||
|
||||
If you declare a type in a Blade file, Laravel must pass the correct class, array, or collection; otherwise, it throws
|
||||
an exception.
|
||||
|
||||
Example:
|
||||
|
||||
```blade
|
||||
{{-- products/index.blade.php --}}
|
||||
@php /** @var App\ViewModels\ProductsViewModel $model */ @endphp
|
||||
<h1>{{ $model->title }}</h1>
|
||||
```
|
||||
|
||||
Only `ProductsViewModel` works—anything else throws an exception.
|
||||
|
||||
Similarly for partials and components, the wrong type is no longer silently accepted; it fails loudly.
|
||||
|
||||
You can do the same for arrays and collections:
|
||||
|
||||
```blade
|
||||
@php
|
||||
/** @var App\Models\Product[] $model */
|
||||
@endphp
|
||||
|
||||
{{-- or --}}
|
||||
@php
|
||||
/** @var \Illuminate\Support\Collection<App\Models\Product> $model */
|
||||
@endphp
|
||||
```
|
||||
|
||||
Wrong types cause errors rather than silent bugs.
|
||||
|
||||
---
|
||||
|
||||
## 5. Going Fully Typed With DTO Classes
|
||||
|
||||
So far, we’ve wrapped data in ViewModels. But what about the data itself? Passing raw Eloquent models to views can be
|
||||
risky — they carry too much baggage and autocomplete may expose a jungle of methods and properties.
|
||||
|
||||
Let’s create DTO classes to define exactly what your views need. (Note: components themselves can act as DTOs, so
|
||||
sometimes you don’t need separate DTOs.)
|
||||
|
||||
### BaseDTO
|
||||
|
||||
```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()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SimpleProduct DTO
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
In the controller:
|
||||
|
||||
```php
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$viewModel = new ProductsViewModel();
|
||||
|
||||
$viewModel->products = Product::select(SimpleProduct::columns())->get();
|
||||
// other assignments...
|
||||
return view('products.index', ['model' => $viewModel]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the partial:
|
||||
|
||||
```blade
|
||||
{{-- views/products/partials/product.blade.php --}}
|
||||
@php
|
||||
/** @var \App\DTO\Product\SimpleProduct $model */
|
||||
@endphp
|
||||
|
||||
<span>
|
||||
{{ $model->name }}
|
||||
</span>
|
||||
```
|
||||
|
||||
Autocomplete now shows only DTO properties.
|
||||
|
||||
#### Caveats & Tips
|
||||
|
||||
- If you only select `DTO::columns()` in your query and still pass raw models to Blade, IDE autocomplete works via
|
||||
annotations—but at runtime Blade can still access everything.
|
||||
- For stricter discipline, wrap results into DTO instances.
|
||||
- Schema Drift Protection: if a DTO includes a property not in the table, MySQL will error, keeping your DTOs in sync.
|
||||
- You lose Eloquent helpers, relations, and accessors. Do transformations before mapping to DTOs.
|
||||
- Mapping overhead is minimal: a base DTO + mapper makes it clean and efficient.
|
||||
|
||||
Example mapper:
|
||||
|
||||
```php
|
||||
class DTOMapper
|
||||
{
|
||||
public static function map(object $source, string $dtoClass): object
|
||||
{
|
||||
$dtoReflection = new ReflectionClass($dtoClass);
|
||||
$properties = $dtoReflection->getProperties();
|
||||
|
||||
$args = [];
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$name = $property->getName();
|
||||
if (isset($source->$name)) {
|
||||
$args[$name] = $source->$name;
|
||||
}
|
||||
}
|
||||
|
||||
return new $dtoClass(...$args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```php
|
||||
$dto = DTOMapper::map($product, SimpleProduct::class);
|
||||
$dtos = $products->map(fn($p) => DTOMapper::map($p, ProductDTO::class));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Principles of Structured Blade
|
||||
|
||||
This approach holds only if you enforce ground rules:
|
||||
|
||||
1. **No untyped arrays in views** — every view receives a single typed contract.
|
||||
2. **One ViewModel per Blade file** — keep mental load low.
|
||||
3. **Partials and components need contracts too** — no freeloaders.
|
||||
4. **Always annotate with `@var`** — autocomplete is non‑negotiable.
|
||||
5. **Always pass data as `model` for consistency** — every blade view has a single entry point.
|
||||
6. **Business logic stays out of Blade views** — data shaping and conditions belong in ViewModels.
|
||||
7. **Use DTOs to avoid Eloquent jungle** — views see pure, predictable DTOs.
|
||||
8. **Strict access, fail loudly** — undefined properties in ViewModel should throw, not silently fail.
|
||||
|
||||
---
|
||||
|
||||
## Final Word
|
||||
|
||||
Blade isn’t broken. The way most developers use it is.
|
||||
|
||||
Treating Blade files as unstructured trash bins creates brittle, unmaintainable code. Adding ViewModels, autocomplete,
|
||||
contracts for partials, and DTOs gives your views the same level of professionalism as the rest of your codebase.
|
||||
|
||||
This isn’t overkill. It’s discipline. And discipline is what separates fragile projects from sustainable ones.
|
||||
|
||||
So stop passing arrays. Start passing contracts. Give your Blade files the structure they deserve.
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user