Tập 13: Blade template Laravel

Rất vui được gặp lại tất cả các bạn. Như đã nói ở tập trước thì ngày hôm nay, nội dung sẽ nói về "Blade template" trong Laravel. Tập ngày hôm nay chủ yếu chỉ học cú pháp và lợi ích của từng cú pháp đó, tuy dài nhưng không khó. Không luyên thuyên nữa, chúng ta vào bài thôi.

Vì editor của Viblo không hỗ trợ Blade template Laravel nên đôi khi màu sắc code có chỗ khó nhìn, mong các bạn thông cảm.

I. Giới thiệu (Introduction)

Blade là một template engine được cung cấp bởi Laravel. Đây là một engine khá mạnh mẽ, không như những các template engine khác, Blade không hạn chế việc sử dụng mã code PHP thuần túy trong template của bạn ngoài những cú pháp riêng biệt của nó. Thực tế thì Blade template được biên dịch thành các file PHP và được lưu vào cache. Blade template có đuôi mở rộng là .blade.php và được lưu trữ trong thư mục resources/views.

II. Layout template

1. Xác định bố cục (Defining layout)

Có hai lợi ích chính trong việc sử dụng Blade template là layout và section. Để bắt đầu chúng ta sẽ đi vào một ví dụ đơn giản. Như các bạn đã biết, trong một ứng dụng thì đa phần các trang con đều được thừa hưởng một cấu trúc layout chung. Blade template cho phép chúng ta định nghĩa một layout cho ứng dụng với duy nhất một blade view.

<html>
    <head>
        <title>Laravel</title>
    </head>
    <body>
        @section('sidebar')
            <h1>Sidebar</h1>
        @show
        
        @yield('content')
    </body>
</html>

Đây chỉ là nội dung của một đoạn mã HTML bình thường, nhưng có chút đặt biệt là nó có hai cú pháp lạ: @section, @yield. Đây chính là một trong những cú pháp riêng biệt của Blade template dùng cho mục đích kế thừa qua lại giữa parent-child.

  • @section: dùng để xác định một phần nội dung
  • @yield: dùng để hiển thị nội dung của một phần đã được xác định

Hiện tại khó mà để hiểu theo giải thích bằng văn được. Để các bạn có thể hiểu chi tiết công dụng của hai cú pháp này, mình sẽ làm sáng tỏ chúng qua ví dụ.

2. Kế thừa layout (Extending layout)

Như đã nói ở trên, công dụng chung của hai thằng @section@yied là dùng cho mục đích kế thừa. Blade view app là parent, vì nó là layout cho ứng dụng, vì vậy mình sẽ tạo một blade view child đóng vai trò là child kế thừa parent app.

@extends('app')

@section('content')
    <h1>My content</h1>
@endsection

Mình đã sử dụng cú pháp @extends, điều này giúp blade view child có thể kế thừa app. Tiếp theo mình đã xác định nội dung cho section content thông qua thẻ @section. Bây giờ đăng ký route và xem thử nó hoạt động như thế nào.

Route::get('/', function() {
    return view('child');
});

Ở đây chúng ta render view child chứ không phải app nhé. Kết qua thu được như thế này:

Chữ "My content" xuất hiện thì khá dễ hiểu, vì chúng đã xác định nội dung cho section content, nên lệnh @yield sẽ thực hiện công việc render. Nhưng sao section sidebar vẫn được hiển thị? Mình dùng @section để xác định nội dung sidebar như content thôi mà. Hãy quan sát so sánh sau:

Cú pháp @section cho section content thì như sau:

@section('content')
    <h1>My content</h1>
@endsection

Nó có thẻ mở @section và thẻ đóng @endsection. Trong khi đó, lệnh @section cho section sidebar thì khác một chút:

@section('sidebar')
    <h1>Sidebar</h1>
@show

Thẻ đóng lúc này là @show, chính vì thể cú pháp này không chỉ là xác định nội dung cho sidebar mà đồng với với @show nó sẽ render section này luôn. Đó là lý do vì sao mà chữ "Sidebar" lại xuất hiện.

Nhưng tại sao lại không dùng @yield luôn cho tiện mà phải dùng @section rồi đóng bằng thẻ @show để render? Cái gì cũng có nguyên nhân của nó cả, vì khi sử dụng @section ... @show trong layout thì bạn có thể làm một điều thú vị trong các trang con. Mình sẽ dẫn chứng ngay và luôn, các bạn mở file resources/views/child.blade.php và thêm đoạn code này vào:

@section('sidebar')
    @parent

    <ul>
        <li>Item 1</li>
        <li>Item 1</li>
    </ul>
@endsection

Giờ cứ thử xem kết quả trước đã rồi mình sẽ giải thích.

Như các bạn thấy, section sidebar không chỉ không mất chữ "Sidebar" mà còn có thêm cái list mình mới vừa tạo. Bây giờ hãy bắt đầu quan sát lại đoạn code vừa thêm ở trên, mình đã có thêm thẻ @parent. Với thẻ này thì các section được định nghĩa lại trong trang con sẽ append vào section ở blade view cha. Trong trường hợp này blade view child đã extend blade view app nên chính vì thế khi khai báo lại section sidebar thì nó đã append thêm cái list ở dưới chữ "Sidebar".

Vậy thử đoán xem điều gì sẽ xảy ra nếu mình bỏ đi thẻ @parent? Vâng, lúc này section sidebar được khai báo trong blade view child sẽ ghi đè hoàn toàn lên section sidebar ở blade view app.

Nói một chút về @section thì ngoài cách khai báo xác định nội dung trên thì ta còn có một cú pháp khác để xác định nội dung của một phần nào đó như sau:

@section('title', 'Laravel')

Thông thường với cách này thường áp dụng cho các section chứa nội dung ngắn gọn hoặc không chứa các thẻ HTML.

Ngoài ra, @yield còn có thể nhận tham số thứ hai làm giá trị mặc định nếu các trang con extend với nó không xác định nội dung cho phần đã khai báo.

// Nhận giá trị mặc định là view
@yield('content', View::make('view.name'))

// Nhận giá trị mặc định là một chuỗi
@yield('content', 'default')

Lưu ý: Kết thúc mỗi thẻ trong cú pháp Blade template không có dấu ;.

III. Component & Slot

Component và slot cung cấp tính năng khá giống với section và layout, nhưng có vài chỗ thì component và slot dễ hiểu hơn nhiều. Hiểu đơn giản thì component là các thành phần, còn slot là nơi chứa nội dung sẽ thay đổi nhiều lần. Hãy tưởng tượng nếu ứng dụng của bạn có nhiều xử lý cần hiện modal để thông báo, nhưng bạn lại muốn có thể sử dụng lại mà không cần phải khai báo nhiều lần. Lúc này component chính là modal, và nội dung thông báo chính là slot.

Thông thường các component được khai báo trong view, bạn có thể tự tổ chức cấu trúc thư mục để lưu trữ các component, hoặc là có thể tham khảo theo cấu trúc của mình.

resources/
├── views/
|   ├── components/
|   |   |   ...

1. Khởi tạo component & slot (Creating component & slot)

Bây giờ mình sẽ tạo component modal đặt trong file resources/views/components/modal.blade.php với nội dung sau:

<div class="modal">
    {{ $slot }}
</div>

Cú pháp {{ $slot }} sẽ chứa nội dung thông báo thay đổi liên tục trong suốt ứng dụng.

Lưu ý:: $slot không thể thay đổi tên biến khác.

Mọi thiết lập đã hoàn tất, giờ chỉ cần lấy ra sử dụng thôi. Mình sẽ sử dụng lại blade view child hồi nãy để test luôn.

@section('content')
    <h1>My content</h1>

    @component('components.modal')
        Đã có lỗi xảy ra
    @endcomponent
@endsection

Chúng ta sử dụng cặp thẻ @component ... @endcomponent để gọi component components.modal, đồng thời khai báo slot. Tham số trong () là tên blade view chứa component, nội dung trong cặp thẻ chính là giá trị mà $slot nhận được. Nạp lại server và chạy đường dẫn http://localhost:8000 ta sẽ được kết quả như hình:

Bạn có thể gọi một component nhiều lần như thế này:

@section('content')
    <h1>My content</h1>

    @component('components.modal')
        Đã có lỗi xảy ra
    @endcomponent

    @component('components.modal')
        Đã hết lỗi xảy ra
    @endcomponent
@endsection

2. The first available component

Bạn cũng có thể tùy chỉnh các giao diện của component nào đó với @componentFirst, về tính năng thì nó cũng tương tự như View::first.

@componentFirst(['custom.modal', 'modal'])
    Đã có lỗi xảy ra
@endcomponent

3. Truyền dữ liệu cho component (Passing data to component)

Đôi khi với một $slot thì bị hạn chế cho việc tùy chỉnh nội dung một component, chính vì thế Laravel cho phép truyền slot khác ngoài $slot mặc định để ứng dụng của bạn linh hoạt hơn.

@component('components.modal')
    @slot('title')
        Lỗi!
    @endslot

    Đã có lỗi xảy ra
@endcomponent

Sử dụng @slot với tham số là tên biến nhận dữ liệu trong blade view component để khai báo một giá trị khác mà component có thể nhận.

Tại component modal, bạn chỉnh sửa nội dung như sau:

<div class="modal">
    <h3>{{ $title }}</h3>
    
    {{ $slot }}
</div>

Laravel sẽ hiểu phần nội dung bạn khai báo ở @slot sẽ truyền vào $title, còn phần chỉ nằm trong cặp thẻ @component thì sẽ truyền vào $slot mặc định. Mình xin tản mạn một chút, cú pháp {{ $varible }} là dùng để hiển thị dữ liệu của biến.

Và đây là kết quả sau khi tải lại trang:

Ngoài ra bạn có thể thay thế cách truyền dữ liệu cho component trên bằng cách:

@component('components.alert', ['title' => 'Lỗi!'])
    Đã có lỗi xảy ra
@endcomponent

Tham số thứ hai trong () sẽ nhận một mảng chứa dữ liệu và lấy tên key làm tên biến trong component. Với cách này thông thường dùng để truyền các dữ liệu ngắn, không chứa thẻ HTML.

4. Aliasing component

Bạn có nghĩ mình sẽ có thể tự đặt cú pháp cho component để trông code dễ hiểu, dễ hình dung hơn không, chẳng hạn:

@modal
    Đã có lỗi xảy ra
@endmodal

Laravel hiểu mong muốn của bạn, vì thế đã định nghĩa method component trong Blade facade để ta có thể dễ dàng alias cú pháp cho bất kì component nào. Laravel khuyên nên thực hiện việc này tại AppServiceProvider, cụ thể làm method boot:

use Illuminate\Support\Facades\Blade;

public function boot() 
{
    Blade::component('components.modal', 'modal');
}

Ở đây bắt buộc chúng ta phải use Blade facade để có thể gọi method component ra. Method này sẽ nhận hai tham số:

  • Tham số thứ nhất là tên component
  • Tham số thứ hai là tên cú pháp thay thế

Bây giờ bạn có thể khai báo component modal theo cú pháp sau:

// Mặc định
@modal
    Đã có lỗi xảy ra
@endmodal

// Nếu có truyền dữ liệu thêm
@modal(['title' => 'Lỗi!'])
    Đã có lỗi xảy ra
@endmodal

Bạn có thể test để so sánh kết quả, đảm bảo vẫn không thay đổi gì.

IV. Hiển thị dữ liệu (Displaying data)

Bạn có thể hiện thị dữ liệu được truyền đến blade view bằng cách đặt biến đó giữa cặp {{ }}, ví dụ mình đăng ký một route:

Route::get('/', function() {
    return view('home', ['name' => 'Lê Chí Huy']);
});

Tiếp đó các bạn tạo blade view home và code nó như sau:

Welcome, {{ $name }}

Đây là kết quả chúng ta thu được:

Như bạn thấy cú pháp này vô cùng ngắn gọn để hiển thị $name thay vì phải code dài như thế này:

Welcome, <?php echo $name; ?>

Lưu ý: Bạn chỉ có thể sử dụng các cú pháp của Blade template khi đuôi mở rộng có dạng .blade.php.

1. Displaying unescaped data

Giờ các bạn thử chèn cặp thẻ <b></b> cho giá trị $name như thế này:

Route::get('/', function() {
    return view('home', ['name' => '<b>Lê Chí Huy</b>']);
});

Đoán xem chữ "Lê Chí Huy" có được bôi đen không? Đáp án là "Không". Vì cú pháp {{ }} trước khi hiển thị dữ liệu thì nó đã đưa qua hàm htmlspecialchars, việc này sẽ tránh các cuộc tất công XSS. Nói một cách đơn giản thì kiểu tấn công này lợi dụng việc có thể hiển thị các thẻ HTML thông qua chức năng viết bài, comment... để chèn mã script độc, tấn công hệ thống.

Vậy nếu giờ bạn muốn chèn HTML vào dữ liệu của mình thì sao? Laravel không tuyệt đường bất kỳ mong muốn nào của coder cả, họ cung cấp cho chúng ta cú pháp {!! ... !!} để có thể hiển thị cả mã HTML

Welcome, {!! $name !!}

Một kết quả đầy mong đợi:

Lưu ý: Để tránh bị tân công XSS, bạn nên dùng cú pháp này show các dữ liệu do bạn xác định, admin cpanel xác định chứ không nên show các dữ liệu do người dùng nhập như viết bài, comment... để tránh chèn mã độc.

Cú pháp {{ }} không giới hạn việc hiển thị dữ liệu của biến truyền vào blade view. Bạn có thể echo kết quả bất kì hàm PHP nào, chẳng hạn như:

The current UNIX timestamp is {{ time() }}.

2. Rendering JSON

Nếu bạn truyền một mảng đến blade view và muốn render nó như một JSON để gán giá trị cho biến nào đó trong đoạn mã script. Thông thường với PHP, bạn sẽ:

<?php

echo '
    <script>
        var app = ' . json_encode($array) . ';
    </script>
';

Nhưng đối với Blade template Laravel thì cú pháp sẽ gọn đi rất nhiều. Thay vì gọi thủ công như thế thì ta có thể sử dụng cú pháp @json, nó sẽ nhận các tham số tương tự như json_encode.

<script>
    var app = @json($array);

    var app = @json($array, JSON_PRETTY_PRINT);
</script>

Nếu như bạn truyền @json trong một thuộc tính của thẻ thì nên để trong nó trong cặp dấu ''.

<tag attr='@json($array)'></tag>

3. Blade & JavaScript Frameworks

Có một số Javascript framework sử dụng cặp {} làm cú pháp của mình, chính vì thế đôi khi Blade template sẽ hiểu nhầm là cú pháp của nó và compile, có thể gây lỗi. Để tránh điều đó, bạn chỉ cần thêm ký tự @ trước câu lệnh thì Blade template sẽ hiểu nó không phải là cú pháp cần compile.

Welcome, @{{ $name }}

Đây là kết quả:

Nếu trong trường hợp có nhiều cú pháp Javascript framework có chứa cặp {} mà bạn không muốn phải cất công thêm tiền tố @ trước từng câu lệnh. Laravel cung cấp cho chúng ta một giải pháp đó chính là đưa tất cả các cú pháp Javascript framework ấy vào trong cặp thẻ @verbatim ... @endverbatim như thế này:

@verbatim
{{ $variable }}
//...
@endverbatim

V. Cấu trúc điều khiển (Control structure)

Ngoài các tính năng lợi ích trên, Laravel còn cung cấp cho chúng ta các cú pháp ngắn gọn cho các cấu trúc điều khiển của PHP như if, for, foreach... Những cú pháp này rất thuận tiện, nhanh gọn nhưng lại không làm mất đi bản chất vốn có của các cấu trúc điều khiển.

1. Câu lệnh "if" ("if" statement)

Bạn có thể khởi tạo câu lệnh if bằng các thẻ @if, @elseif, @else@endif.

@if (count($records) === 1)1 
@elseif (count($records) > 1)
   Có nhiều
@else
    Không có
@endif

Như bạn thấy, chỉ là cú pháp khác, cách khai báo các điều kiện và hoạt động vẫn không khác gì cấu trúc điều kiện thuần túy.

Blade template cung cấp cho chúng ta thẻ @unless:

@unless (Auth::check())
    Chưa đăng nhập
@endunless

Phương thức Auth::check là dùng để kiểm tra xem user có đăng nhập hay chưa. Hiện tại chúng ta chưa học tới nên chắn chắc method này sẽ trả về giá trị là false. Nếu các bạn đã học qua chương trình tiếng Anh phổ cập thì sẽ biết unless đồng nghĩa với if not.

Ngoài ra còn có @isset@empty, hai thẻ này đại diện cho hai hàm quen thuộc trong PHP là issetempty.

@isset($records)
    // 
@endisset

@empty($records)
    // 
@endempty

a. Authentication

@auth@guest và hai thẻ dùng để kiểm tra xem user hiện tại có đăng nhập hay chưa.

@auth
    Đã đăng nhập
@endauth

@guest
    Chưa đăng nhập
@endguest

Ngoài ra bạn có thể truyền tham số cho hai thẻ này để kiểm tra trạng thái đăng nhập của user với tư cách là ai. Chẳng hạn một tài khoản có vai trò là "user" thì sau khi đăng nhập vẫn không thể truy cập vào admin cpanel được. Tính năng này được gọi là "Authentication Guard", chúng ta sẽ tìm hiểu sau.

@auth('admin')
    Đã đăn nhập với tư cách là "admin"
@endauth

@guest('admin')
    Chưa đăng nhập với tư cách là "admin"
@endguest

b. Section

Bạn có thể kiểm tra sự tồn tại của một section bằng thẻ @hasSection với tham số truyền vào là tên của section cần kiểm tra.

@hasSection('navigation')
    // 
@endif

2. Câu lệnh "switch" ("switch" statement)

Lệnh switch được khởi tạo bằng các thẻ @switch, @case, @break, @default@endswitch.

@switch($i)
    @case(1)
        First case...
        @break

    @case(2)
        Second case...
        @break

    @default
        Default case...
@endswitch

3. Vòng lặp (Loop)

Cũng giống như các cấu trúc điều khiển khác, các câu lệnh loop trong Blade template vẫn được giữ nguyên cách thức hoạt động.

// For statement
@for ($i = 0; $i < 10; $i++)
    {{ $i }} </br>
@endfor

// Foreach statement
@foreach ($array as $row)
    {{ $row['attr'] }} </br>
@endforeach

// While statement
@while (true)
    <p>I'm looping forever.</p>
@endwhile

Laravel cung cấp cho chúng ta thẻ @forelse, thẻ này hoạt động giống như là @foreach nhưng ta có thể kiểm tra nhanh xem object tham chiếu trong loop có rỗng hay không, nếu có thì sẽ thực thi lệnh gì đó thông qua thẻ @empty.

Bạn có thể test đoạn code này thì sẽ hiểu rõ thẻ @forelse ngay.

@forelse ([] as $user)
    <li>{{ $user['name'] }}</li>
@empty
    <p>No users</p>
@endforelse

Như quan sát, mình đã truyền một mảng rỗng vào vòng lặp, lặp tức thì nó sẽ được kiểm tra qua thẻ @empty. Kết qua ta thu được sẽ là:

Nói về vòng lặp thì không thể thiếu continuebreak được, đương nhiên hai lệnh này vẫn được Blade template chuyển cú pháp thành @continue@break.

@for ($i = 1; $i <= 10; $i++)
    @if ($i == 1)
        @continue
    @endif

    {{ $i }}

    @if ($i == 5)
        @break
    @endif
@endforeach

Nếu bạn thấy mỗi lần muốn break hoặc continue phải lồng trong một câu lệnh if thì hơi dài và khá rối mắc. Chính vì thế Laravel cho phép bạn truyền điều kiện vào hai thẻ @break@continue để rút ngắn thời gian cho bạn.

@for ($i = 1; $i <= 10; $i++)
    @continue($i == 1)

    {{ $i }}

    @break($i == 5)
@endforeach

4. Biến vòng lặp (The loop variable)

Khi sử dụng các lệnh vòng lặp foreach, forelse mặc định sẽ có sẵn $loop bên trong vòng lặp. Biến này cho phép ta lấy các thông tin hay sử dụng như index hiện tại, index đầu, index cuối vòng lặp...

@foreach ($users as $user)
    @if ($loop->first)
        This is the first iteration.
    @endif

    @if ($loop->last)
        This is the last iteration.
    @endif

    <p>This is user {{ $user->id }}</p>
@endforeach

Nếu bạn có các vòng lặp lồng nhau thì có thể tham chiếu $loop của vòng lặp cha bằng thuộc tính parent trong vòng lặp con.

@foreach ($users as $user)
    @foreach ($user->posts as $post)
        @if ($loop->parent->first)
            This is first iteration of the parent loop.
        @endif
    @endforeach
@endforeach

Dưới đây là toàn bộ các thông tin là $loop có thể cung cấp:

Thuộc tính Mô tả
$loop->index Lấy index hiện tại (bắt đầu từ 0)
$loop->iteration Lấy số lần đã lặp hiện tại (bắt đầu từ 1)
$loop->remaining Lấy số lần lặp còn lại
$loop->count Lấy tổng số vòng lặp sẽ lặp
$loop->first Nếu tại vòng lặp đầu tiên thì trả về true
$loop->first Nếu tại vòng lặp cuối cùng thì trả về true
$loop->even Nếu index vòng lặp hiện tại chẵn thì trả về true
$loop->odd Nếu index vòng lặp hiện tại lẻ thì trả về true
$loop->depth Lấy độ sâu của vòng lặp hiện tại
$loop->parent Lấy $loop của vòng lặp cha trong vòng lặp con

5. Comment

Blade template cũng cho phép chúng ta comment để code dễ hiểu, dễ quản lý hơn với cú pháp:

{{-- This comment will not be present in the rendered HTML --}}

6. PHP

Bạn cũng có thể chèn code PHP vào trong Blade template trong cặp thẻ @php ... @endphp.

@php
    //
@endphp

Lưu ý: Bạn không nên qua lạm dụng thể này để thực thi một số xử lý logic phức tạp, điều này sẽ làm mất bản chất của view. Với các xử lý logic phức tạp, bạn nên thực thi ở view composer hoặc controller action, sau đó mới truyền đến view đển render.

VI. Form

1. Trường CSRF (CSRF field)

Bạn còn nhớ vấn đề ở tập Routing chứ, chúng ta không thể truy cập route với phương thức POST, cũng như là PUT, DELETE... vì thiếu CSRF field này. Vì vậy khi khởi tạo một HTML form thì chúng ta phải khái báo CSRF field thông qua thẻ @csrf.

Giờ mình sẽ tạo blade view home với nội dung sau:

<form method="POST" action="/post">
    @csrf
    
    <input type="submit" value="Send post">
</form>

Form này sẽ gửi một request với method POST với URI /post khi submit.

Sau đó các bạn đăng ký hai route này trong routes/web.php:

Route::get('/', function () {
    return view('home');
});

Route::post('/post', function() {
    return 'Posted';
});

Với route đầu tiên thì sẽ render view chứa form của chúng ta. Còn route thứ hai mang phương thức POST sẽ được gọi khi chúng ta click vào nút "Send post".

Bây giờ ta hãy thử truy cập đường dẫn http://localhost:8000 và F12 lên để xem source HTML của form vừa tạo.

@csrf đã tạo một input hidden với name là _token và value chứa CSRF token, có field này chắc chắn ta sẽ gửi được request với method POST. Việc cuối cùng là click vào nút "Send post" thôi.

2. Trường phương thức (Method field)

Đối với những phương thức khác như PUT, DELETEPATCH ta không thể khai báo trong thuộc tính method của thẻ form được, chính vì vậy Laravel cung cấp cho ta thẻ @method để khai báo một Unsafe HTTP method khác ngoài POST.

Các bạn thêm thẻ @method với tham số PUT này như sau:

<form method="POST" action="/post">
    @method('PUT')
    @csrf
    
    <input type="submit" value="Send post">
</form>

Sau đó thử click nút "Send post" lại xem, chắc chắn xuất hiện lỗi:

Vì chúng ta đã thay đổi HTTP request thành PUT nên ở route đăng ký cũng phải thay đổi.

Route::put('/post', function() {
    return 'Posted';
});

3. Validation error

Với cặp thẻ @error ... @enderror ta có thể dễ dàng bắt bất kỳ thông báo nào khi request trả về lỗi. Về "Validation error message" ta sẽ tìm hiểu ở những tập sau nên phần này mình chỉ nó qua về cú pháp để kiểm tra có tồn tại lỗi được trả về tại blade template hay không.

// kiểm tra mặc định
@error('title')
    {{ $message }}
@enderror

// Kiểm tra trong thuộc tính
<input id="title" type="text" class="@error('title') is-invalid @enderror">

@error sẽ nhận tham số là tên lỗi mà chúng ta xử lý trước khi trả về cho blade view. Nếu true thì nó sẽ thực thị lệnh trong cặp thẻ.

VII. Liên kết sub-view (Including sub-view)

@include trong Blade template cho phép bạn có thể liên kết một blade view với một view khác. Tất cả các biến dữ liệu ở parent view đều được truyền đến view include.

Chẳng hạn mình có cấu trúc thư mục như sau:

resources/
├── views/
|   ├── includes/
|   |   |   room.blade.php
|   |   home.blade.php

Ở blade view home ta code:

@php
    $name = 'Lê Chí Huy';
@endphp

@include('includes.room')

Như các bạn thấy, mình đã sử dụng cú pháp @include với tham số là tên view liên kết cần đưa vào blade view home này. Ngoài ra mình có khai báo biến $name để thử xem là blade view includes.room có nhận được hay không.

Tại blade view includes.room mình code như sau:

{{ $name }}

Cuối cùng là đăng ký route để render blade view home ra:

Route::get('/', function () {
    return view('home');
});

Kết quả này chứng minh cho việc ta có thể lấy tất cả các dữ liệu tại view liên kết từ parent view. Ngoài ra bạn cũng có thể truyền một dữ liệu bất kì cho view liên kết thông qua tham số thứ hai của thẻ @include.

@include('view.name', ['some' => 'data'])

1. Một số cú pháp khác hỗ trợ cho include (Some other syntax support for including)

Trong trường hợp nếu bạn @include một view không tồn tại thì framework sẽ báo lỗi. Chính vì vậy khi include một view không chắc chắn sẽ tồn tại thì ta sử dụng thẻ @includeIf thay cho @include.

@includeIf('view.name', ['some' => 'data'])

Bạn cũng có thể include một view khi kiểm tra một điều kiện nào đó trả về boolean true.

@includeWhen($boolean, 'view.name', ['some' => 'data'])

Laravel còn cung cấp thẻ @includeFirst, thẻ này cho phép ta include view đầu tiên trong mảng view khai báo. Nhiều bạn sẽ hiểu ngay công dụng của thẻ này nếu theo dõi các tập trước của mình.

@includeFirst(['custom.admin', 'admin'], ['some' => 'data'])

Như dòng code này, nếu view custom.admin tồn tại thì nó sẽ include view này và bỏ qua view admin. Còn nếu view custom.admin không tồn tại thì nó sẽ bỏ qua và tiếp tục kiểm tra view admin.

Lưu ý: Bạn nên tránh sử dụng __DIR____FILE__ trong blade view vì chúng chỉ trả kết quả đường dẫn của các file cache hay compile view, không cung cấp thông tin cần thiết.

2. Aliasing include

Nếu blade view include một view nằm trong sub-directory, mà phải tham chiếu một tên rất dài. Chúng ta có thể alias cú pháp include một view nào đó giống như là component. Ta cũng sẽ làm việc này tại AppServiceProvider, ngay tại method boot.

use Illuminate\Support\Facades\Blade;

public function boot()
{
    Blade::include('includes.room', 'room');
}

Rồi bây giờ ta chỉ cần include view includes.room theo cú pháp đã đăng ký:

@room(['name' => 'Lê Chí Huy'])

3. Rendering views for collections

Nếu bạn có một mảng dữ liệu và muốn truyền từng dữ liệu vào một view nào đó thì bạn có thể sử dụng @each thay vì @foreach@include.

Chẳng hạn mình có cấu trúc thư mục như sau:

resources/
├── views/
|   ├── includes/
|   |   |   item.blade.php
|   |   list.blade.php

Đầu tiên mình sẽ thử sử dụng cách thông thường đó là dùng @foreach@include. Tại blade view list.blade.php mình sẽ code nội dung như sau:

@php
    $list = ['Item #1', 'Item #2', 'Item #3'];
@endphp

<ul>
    @foreach ($list as $item)
        @include('includes.item')
    @endforeach
</ul>

Và blade view includes.item:

<li>{{ $item }}</li>

Sau đó chúng ta đăng ký route và chạy thử:

Route::get('/', function () {
    return view('list');
});

Bây giờ chúng ta sẽ dùng @each để thay thế cách trên. Ta chỉ cần thay đổi đoạn vòng lặp trong blade view list.

<ul>
   @each('includes.item', $list, 'item')
</ul>

@each này sẽ nhận:

  • Tham số đầu tiên là tên view include
  • Tham số thứ hai là biến chứa mảng dữ liệu
  • Tham số thứ ba là tên biến nhận giá trị trong view include.

Ngoài ra trong trường hợp nếu mảng dữ liệu rỗng thì ta có thể include view hiện thông báo gì đó bằng cách khai báo thêm tham số thứ tư ở @each với giá trị là tên view chứa nội dung thông báo.

@each('view.name', $list, 'item', 'view.empty')

Lưu ý: Các view được render thông qua @each không thể nhận các biến dữ liệu được truyền đến parent view. Nên bạn cần cần nhắc sử dụng @foreach@include để thay thế nếu các view include cần những biến dữ liệu này.

VIII. Stacks

Nói về thuật ngữ này bằng văn bản thì rất khó hình dung nên mình sẽ đi qua ví dụ để các bạn thấy công dụng, từ đó có thể hiểu được nó. Chẳng hạn trong ứng dụng của bạn có rất nhiều trang con, tất cả các trang con đều được include với một file JS chung. Một số trang con đặc biệt sẽ có file JS riêng để xử lý.

Đây là cấu trúc thư mục cho ví dụ:

resources/
├── views/
|   ├── pages/
|   |   |   login.blade.php
|   |   | ...
|   |   layout.blade.php

Ở blade view layout, ta có nội dung sau:

<html>
    <head>
        <script src="main.js"></script>// File JS chung
    </head>
    </body>
        @yield('page')
    </body>
</html>

Còn blade view login thì:

@extends('layout')

@section('page')
    <form name="login">
        //
    </form>
@endsection

Bây giờ mình muốn include file JS login.js thì cách nhanh nhất là bỏ dòng code include JS vào trong section page ở blade view pages.login như thế này:

@section('page')
    <form name="login">
        //
    </form>
    
    <script src="login.js"></script>
@endsection

Cách trên thì có thể code vẫn hoạt động bình thường nhưng sẽ cảm thấy một chút gì đó rất khó chịu, có thể gây một số phiền phức sau này như không tốt cho SEO hoặc bất đồng bộ trong code Javascript.

Laravel cung cấp cho ta một giải pháp để giải quyết tình huống trên dựa vào hai thẻ @stack@push. Bạn hãy tưởng tưởng @stack giống như một kệ sách, còn @push giống như những cuốn sách, dù bạn đặt những cuốn sách ở đâu thì sau quá trình dọn dẹp (biên dịch) thì những cuốn sách sẽ được đưa vào kệ sách.

Tức là bây giờ mình muốn đưa cuốn sách <script src="login.js"></script> vào một kệ sách nào đó trong <head> thì mình phải khai báo kệ sách đó trước đã.

<head>
    <script src="main.js"></script>// File JS chung
    
    @stack('scripts')
</head>

Như các bạn thấy mình đã khởi tạo một kệ sách với tên là scripts rồi, việc cần làm bây giờ là đưa cuốn sách <script src="login.js"></script> về kệ sách scripts thôi.

Tại blade view pages.login, mình chỉnh sửa nội dung lại như sau:

@extends('layout')

@push('scripts')
    <script src="login.js"></script>
@endpush

@section('page')
    <form name="login">
        //
    </form>
@endsection

Giờ chắc các bạn đã hiểu cách hoạt động của @stack@push rồi đúng không nào. Ngoài ra, Laravel còn cung cấp thẻ @prepend, thẻ này giúp ta sẽ thêm cuốn sách "yêu thích" nào đó vào đầu ngăn sách.

@push('scripts')
    <script src="script_2.js"></script>
@endpush

// Later...

@prepend('scripts')
    <script src="script_1.js"></script>
@endprepend

IX. Service injection

Cái này chắc đã quá quen thuộc rồi, Laravel cho phép chúng ta lấy một service bất kỳ có trong service container thông qua @inject.

@inject('metrics', 'App\Services\MetricsService')

<div>
    Monthly Revenue: {{ $metrics->monthlyRevenue() }}.
</div>

X. Mở rộng Blade (Extending Blade)

Laravel cho phép chúng ta tự định nghĩa các thẻ bằng phương thức directive có trong Blade facade. Việc này đương nhiên chúng ta cũng sẽ code trong boot của AppServiceProvider.

use Illuminate\Support\Facades\Blade;

public function boot()
{
    Blade::directive('print', function($expression) {
        return "<?php echo 'Printed ' . $expression; ?>";
    });
}

Quan sát đoạn code trên, tại method Blade::directive:

  • Tham số thứ nhất là tên thẻ
  • Tham số thứ hai là một Closure object, nó sẽ chứa biến dữ liệu nhận được qua các tham số của thẻ trong blade view. Trong Closure này, ta sẽ return mã PHP được viết dưới dạng chuỗi.

Sau khi đăng ký xong, ta có thể test thẻ @print như sau:

@print('Lê Chí Huy')

Nếu bạn muốn truyền nhiều tham số cho thẻ của mình thì bạn có thể xử lý chuỗi $expression để lấy các giá trị tham số.

Để mình test cho các bạn xem, tại Closure object mình sẽ dump $expression.

use Illuminate\Support\Facades\Blade;

public function boot()
{
    Blade::directive('print', function($expression) {
        dd($expression);
        
        return "<?php echo 'Printed ' . $expression; ?>";
    });
}

Sau đó mình thử gọi @print và truyền nhiều tham số vào nó:

@print('Lê', 'Chí', 'Huy')

Đây là kết quả:

Như các bạn thấy, $expression trả về chuỗi với dạng:

'value1', 'value2', 'value3', ...

Các bạn có thể tùy biến để có thể lấy các giá trị tham số từ $expression.

Lưu ý: Trong khi phát triển ứng dụng, nếu có sử dụng thẻ riêng trong blade view thì sau mỗi lần sửa đổi code xử lý trong mỗi thẻ riêng thì phải chạy lệnh Artisan view:clear để xóa cache và compile lại các view.

Ngoài ra bạn cũng có thể tự tạo cho mình lệnh điều kiện riêng có thể gọi trong blade view bằng method Blade::if được đăng ký tại boot của AppServiceProvider.

use Illuminate\Support\Facades\Blade;

public function boot()
{
    Blade::if('env', function ($environment) {
        return app()->environment($environment);
    });
}

Với Blade::if thì:

  • Tham số thứ nhất là tên lệnh điều kiện,
  • Tham số thứ hai là Closure nhận biến giá trị truyền vào và trả về boolean

Như đăng ký trên, nó sẽ kiểm tra môi trường hiện tại của ứng dụng, nếu trùng khớp thì sẽ trả về true, còn không thì trả về false.

Ta có thể sử dụng trong blade view như sau:

@env('local')
    // The application is in the local environment...
@elseenv('testing')
    // The application is in the testing environment...
@else
    // The application is not in the local or testing environment...
@endenv

Cảm ơn các bạn đã quan tâm theo dõi. Cùng đồng hành với mình qua những tập tiếp theo tại series "Hành trình chinh phục Laravel Framework" nhé! Chúc may mắn và hẹn gặp lại.