Blade cơ bản (Laravel 13) — dành cho người mới
Blade là bộ tạo template (view engine) của Laravel. Bạn viết file HTML trộn với vài cú pháp gọn (`{{ }}`, `@if`, `@foreach`...), Laravel biên dịch thành PHP thuần rồi cache lại — nên vừa dễ viết vừa chạy nhanh.
Blade cơ bản (Laravel 13) — dành cho người mới
Blade là bộ tạo template (view engine) của Laravel. Bạn viết file HTML trộn với
vài cú pháp gọn ({{ }}, @if, @foreach...), Laravel biên dịch thành PHP thuần
rồi cache lại — nên vừa dễ viết vừa chạy nhanh.
- File view nằm ở
resources/views/, đuôi.blade.php. - File biên dịch nằm ở
storage/framework/views/(bạn không sửa tay). Đổi view mà thấy chưa cập nhật? Chạyphp artisan view:clear. - Trong dự án này mọi trang đều kế thừa layout chung
resources/views/layouts/app.blade.php.
1. In dữ liệu ra màn hình
{{ }} — in an toàn (dùng 99% trường hợp)
{{ }} tự động escape HTML (chống XSS). Đây là cách in mặc định.
<h1>{{ $post->title }}</h1>
<span>{{ $post->user?->name }}</span> {{-- ?-> : nếu user null thì không lỗi --}}
<p>{{ $post->description ?: 'Chưa có mô tả' }}</p> {{-- ?: giá trị mặc định --}}
<time>{{ $post->created_at->format('H:i d/m/Y') }}</time>
Bên trong {{ }} là PHP thuần, nên bạn dùng được toán tử, gọi hàm, ternary...
{!! !!} — in HTML thô (cẩn thận!)
{!! !!} không escape — trình duyệt sẽ hiểu chuỗi là HTML. Chỉ dùng khi nội dung
đã được làm sạch. Ví dụ thật trong dự án (bài viết Markdown đã lọc thẻ nguy hiểm):
{{-- resources/views/pages/posts/show.blade.php --}}
<div class="markdown">
{!! $post->renderedContent() !!}
</div>
⚠️ Không bao giờ đổ dữ liệu người dùng nhập thẳng vào
{!! !!}nếu chưa sanitize — đó là lỗ hổng XSS.
Ghi chú (comment) — {{-- --}}
{{-- Comment này chỉ có trong source, KHÔNG xuất ra HTML gửi về trình duyệt. --}}
2. Điều kiện (if / else)
@if ($post->thumbnail)
<img src="{{ $post->thumbnail }}" alt="">
@elseif ($post->description)
<p>{{ $post->description }}</p>
@else
<p>Không có gì để hiển thị.</p>
@endif
Các directive tiện lợi khác:
@unless (auth()->check()) {{-- = @if (! ...) --}}
<a href="{{ route('login') }}">Đăng nhập</a>
@endunless
@isset($post) ... @endisset {{-- biến tồn tại & không null --}}
@empty($posts) ... @endempty {{-- biến "rỗng" (null, [], '', 0) --}}
Kiểm tra đăng nhập nhanh (rất hay dùng trong partials/auth-menu.blade.php):
@auth
Xin chào, {{ auth()->user()->name }}
@endauth
@guest
<a href="{{ route('login') }}">Đăng nhập</a>
@endguest
3. Vòng lặp
@forelse — lặp mảng, có sẵn nhánh "rỗng" (mẫu dùng cho danh sách bài viết)
{{-- resources/views/pages/home/index.blade.php --}}
@forelse ($posts as $post)
<article>{{ $post->title }}</article>
@empty
<div>{{ __('LBL_POST_EMPTY') }}</div>
@endforelse
@foreach và biến $loop
@foreach ($items as $item)
<li>
{{ $loop->iteration }}. {{ $item }} {{-- đếm từ 1 --}}
@if ($loop->first) (đầu tiên) @endif
@if ($loop->last) (cuối cùng) @endif
</li>
@endforeach
$loop còn có: index (từ 0), count, even, odd, remaining... Ngoài ra có
@for, @while, @foreach ... @endforeach như PHP thường.
4. Layout: kế thừa & chèn nội dung
Trong dự án, mọi trang viết theo mẫu này:
{{-- resources/views/pages/home/index.blade.php --}}
@extends('layouts.app') {{-- kế thừa layout chung --}}
@section('page', 'home') {{-- tên page (để tự nạp CSS/JS riêng nếu có) --}}
@section('title', __('LBL_TITLE_HOME')) {{-- đổ vào @yield('title') của layout --}}
@section('meta_description', __('LBL_SEO_HOME'))
@section('content') {{-- khối nội dung chính --}}
<h1>{{ __('LBL_POSTS_TITLE') }}</h1>
...
@endsection
Bên trong layout (layouts/app.blade.php) là các "chỗ trống" chờ nội dung:
<title>@yield('title', config('app.name', 'The Vi Blog'))</title> {{-- có giá trị mặc định --}}
<main>
@yield('content') {{-- nội dung trang con đổ vào đây --}}
</main>
| Directive | Ý nghĩa |
|---|---|
@extends('layouts.app') |
Trang con kế thừa layout nào |
@section('x') ... @endsection |
Định nghĩa nội dung cho vùng x |
@section('title', 'văn bản') |
Dạng ngắn: gán 1 dòng |
@yield('x', 'mặc định') |
(Trong layout) đặt nội dung của section x vào đây |
@include('partials.nav') |
Chèn 1 view con vào đúng chỗ (dùng chung header/footer) |
@includeWhen($dk, 'view') |
Chỉ chèn khi điều kiện đúng |
5. @push / @stack / @once — thêm CSS/JS đúng chỗ
Layout khai báo "ngăn xếp" bằng @stack, trang/component "đẩy" nội dung vào bằng @push:
{{-- Trong layout <head>: --}} @stack('styles')
{{-- Cuối <body>: --}} @stack('scripts')
{{-- Một component nạp thư viện CDN của riêng nó — chỉ khi trang thực sự dùng: --}}
@once {{-- @once: chỉ chạy 1 lần dù component xuất hiện nhiều lần --}}
@push('styles')
<link rel="stylesheet" href="https://.../glightbox.min.css">
@endpush
@push('scripts')
<script src="https://.../glightbox.min.js"></script>
@endpush
@endonce
Đây chính là cách <x-gallery> và partials/tooltip tự nạp thư viện — bạn chỉ cần
dùng component, không phải nhớ thêm <script> vào layout.
6. Component <x-...> — mảnh giao diện tái sử dụng
Dự án dùng anonymous component: mỗi file trong resources/views/components/ tự động
thành một thẻ <x-tên-file>. Thư mục con → dùng dấu chấm (components/form/input.blade.php
→ <x-form.input>).
Cách gọi
<x-icon name="pencil" /> {{-- prop tĩnh: name="pencil" --}}
<x-icon name="gear" class="text-xl" /> {{-- class thêm sẽ được gộp vào (xem $attributes) --}}
<x-form.input name="title" :label="__('LBL_POST_TITLE')" required />
<x-gallery :src="$post->thumbnail" :alt="$post->title" />
name="..."→ truyền chuỗi tĩnh.:label="..."→ có dấu:nghĩa là vế phải là biểu thức PHP (biến, hàm...).required(không có giá trị) → prop boolean =true.
Cách khai báo bên trong component
{{-- resources/views/components/icon.blade.php --}}
@props(['name']) {{-- khai báo prop; có thể đặt mặc định: ['name' => 'house'] --}}
<i {{ $attributes->merge(['class' => 'bi bi-' . $name]) }} aria-hidden="true"></i>
@props([...])— liệt kê các prop component nhận.$attributes— mọi thuộc tính khác người gọi truyền vào (class, id, data-*...).$attributes->merge(['class' => '...'])— gộp class mặc định của component với class người gọi thêm (nhờ vậy<x-icon class="text-xl">vẫn giữ đượcbi bi-...).
Slot — nội dung nằm giữa thẻ
<x-tooltip content="Gợi ý"> {{-- $slot = phần bên trong --}}
<button>Di chuột vào tôi</button>
</x-tooltip>
Bên trong component in slot bằng {{ $slot }}.
7. Form, CSRF và hiển thị lỗi validation
{{-- resources/views/components/posts/form.blade.php (rút gọn) --}}
<form method="post" action="{{ $action }}" enctype="multipart/form-data">
@csrf {{-- BẮT BUỘC với mọi form POST — token chống CSRF --}}
@if ($method === 'PUT')
@method('PUT') {{-- giả lập PUT/PATCH/DELETE (HTML chỉ có GET/POST) --}}
@endif
<x-form.input name="title" :label="__('LBL_POST_TITLE')" :value="$post?->title" required />
<button type="submit">{{ __('LBL_POST_SAVE') }}</button>
</form>
Các "trợ thủ" cho form:
old('title', $mặcĐịnh)— giữ lại giá trị user vừa nhập khi form bị lỗi & load lại (componentx-form.inputdùng sẵn:value="{{ old($name, $value) }}").$errors— túi lỗi validation. Lấy lỗi 1 trường:$errors->first('title').@error('title') ... @enderror— chạy khi trường đó có lỗi:
<input name="title" value="{{ old('title') }}">
@error('title')
<p class="text-red-600">{{ $message }}</p> {{-- $message có sẵn trong @error --}}
@enderror
- Directive gán thuộc tính có điều kiện (rất gọn):
<input @required($post === null)> {{-- thêm "required" nếu đúng --}}
<input type="checkbox" @checked($post->is_active)>
<option @selected($city === 'HN')>Hà Nội</option>
<button @disabled($dangGui)>Gửi</button>
8. Phân quyền trong view: @can
Ẩn/hiện nút theo quyền (dựa trên Policy). Ví dụ thật ở trang chủ: chỉ tác giả/admin mới thấy nút Sửa/Xoá.
@can('create', \App\Models\Post::class)
<a href="{{ route('posts.create') }}">{{ __('LBL_POSTS_ADD') }}</a>
@endcan
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">{{ __('LBL_POST_EDIT') }}</a>
@endcan
@cannot('delete', $post)
{{-- người không có quyền xoá --}}
@endcannot
9. Các hàm hay dùng trong {{ }}
| Hàm | Công dụng | Ví dụ |
|---|---|---|
__('LBL_...') |
Dịch chữ theo ngôn ngữ hiện tại (Anh/Việt) | {{ __('LBL_POSTS_TITLE') }} |
route('tên', $tham_số) |
Sinh URL từ tên route (đừng viết URL cứng) | {{ route('posts.show', $post) }} |
url('/duong-dan') |
URL tuyệt đối từ đường dẫn | {{ url('/images/logo.png') }} |
asset('duong-dan') |
Link tới file tĩnh trong public/ |
{{ asset('favicon.png') }} |
config('app.name') |
Đọc cấu hình Laravel | {{ config('app.name') }} |
setting('features.x') |
Đọc config/settings.php (cấu hình app) |
@if (setting('features.show_language_switcher')) |
old('field', $default) |
Giá trị cũ sau khi validate lỗi | value="{{ old('email') }}" |
auth()->user() |
User đang đăng nhập (hoặc null) |
{{ auth()->user()?->name }} |
10. Chèn PHP thuần: @php
Thỉnh thoảng cần tính toán nhỏ ngay trong view:
@php
$id = $attributes->get('id', $name);
@endphp
Hạn chế nhồi logic vào view. Logic nghiệp vụ để ở Service/Controller (xem
KIEN_TRUC.md); view chỉ nên lo hiển thị.
11. Ghép lại — một trang hoàn chỉnh (rút gọn từ dự án)
@extends('layouts.app')
@section('page', 'home')
@section('title', __('LBL_TITLE_HOME'))
@section('content')
<div class="mb-6 flex items-center justify-between">
<h1>{{ __('LBL_POSTS_TITLE') }}</h1>
@can('create', \App\Models\Post::class)
<a href="{{ route('posts.create') }}">
<x-icon name="plus-lg" /> {{ __('LBL_POSTS_ADD') }}
</a>
@endcan
</div>
@forelse ($posts as $post)
<article>
@if ($post->thumbnail)
<x-gallery :src="$post->thumbnail" :alt="$post->title" />
@endif
<a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
<span>{{ $post->user?->name }} · {{ $post->created_at->format('d/m/Y') }}</span>
</article>
@empty
<p>{{ __('LBL_POST_EMPTY') }}</p>
@endforelse
{{ $posts->links() }} {{-- thanh phân trang tự động của Laravel --}}
@endsection
12. Mẹo cho người mới
- Đổi view không thấy cập nhật?
php artisan view:clear(xoá cache view đã biên dịch). - Ưu tiên component có sẵn (
<x-form.*>,<x-display.*>,<x-icon>,<x-gallery>) thay vì viết HTML thô — vừa nhất quán vừa đỡ lặp. - Chữ hiển thị luôn qua
__('LBL_*')và thêm vào cảlang/en.json+lang/vi.json. - URL luôn qua
route(), không viết/posts/1cứng — route đổi là view vẫn đúng. {{ }}là mặc định; chỉ dùng{!! !!}khi nội dung chắc chắn an toàn.- Xem "mặt thật" một component: mở file của nó trong
resources/views/components/.