Laravel là một framework tuyệt vời, có khi trên cả tuyệt vời, với thiết kế đơn giản mà đầy mạnh mẽ, khả năng mở rộng vô biên cùng cú pháp luôn toát lên vẻ thanh lịch...

Tuy nhiên nó không phải là một cô gái hoàn hảo (lẽ dĩ nhiên là vậy khi mà PHP còn nhiều hạn chế), vẫn có nhiều điểm hạn chế khi ta phát triển và mở rộng ứng dụng.

Trong bài viết này mình xin nói một hạn chế thường gặp: Xuất và định dạng dữ liệu từ Model ra View, và cách dùng Presenter để khắc phục nó.

Vấn đề phát sinh

Trước khi đi vào vấn đề chính, hãy xem một ví dụ sau đây:

Bảng users có cấu trúc đơn giản như sau:

<?php

Schema::create('users', function(Blueprint $table)
{
    $table->increments('id');

    $table->string('firstname', 25);
    $table->string('lastname', 25);
    $table->string('email', 100)->unique();
    $table->string('password', 64);

    $table->timestamps();
});

Một Model User cũng khá đơn giản:

<?php

class User extends Eloquent {

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

}

Và giờ là khi chúng ta xuất dữ liệu của User ra view.

@foreach (User::all() as $user)
    <p>
        <img src="{{ Gravatar::src($user->email)) }}">
        <span>{{ $user->first_name . $user->last_name }}</span>
        <time>{{ $user->created_at->format('d/m/Y - h:m') }}</time>
    </p>
@endforeach

Nếu chỉ có thế thì sẽ không ra chuyện, chuyện sẽ xảy ra khi bạn không chỉ xuất dữ liệu User một lần mà nhiều lần. Và mỗi lần lại xuất fullname lại chấm một cái giữa fist_namelast_name hay format ngày tháng lại thì thật không hay chút nào.

Bằng sự tìm tòi sáng dạ của mình, tôi đã tìm được một giải pháp tình thế hay hơn ^1 :)): định dạng chúng ở trong Model, khi dùng chỉ cần lấy ra:

<?php

class User extends Eloquent {

    // ...

    public function fullname()
    {
        return $this->first_name . $this->last_name;
    }

    public function avatar()
    {
        return HTML::image(Gravatar::src($this->email));
    }

    public function getCreatedAtAttribute($value)
    {
        return \Carbon\Carbon::parse($value)->format('d/m/Y H:i');
    }

}

Và giờ trong view, dữ liệu xuất ra trở nên trong sáng và thống nhất hơn:

@foreach (User::all() as $user)
    <p>
        {{ $user->avatar() }}
        <span>{{ $user->fullname() }}</span>
        <time>{{ $user->created_at }}</time>
    </p>
@endforeach

Cười hả hê một lúc thì nhận thấy:

  • Model là nơi ta thao tác với CSDL, cớ sao ta đút định dạng ra view vào trong đó?
  • Và quả thật, nếu Model có nhiều scope ^2 thì thật tạp nham, không ra thể thống gì!
  • Nếu những thuộc tính xuất ra View cần nhiều sự logic, mã cứng html... thì đúng là không thể chấp nhận được!
  • ...

Đó chính là lúc cần sử dụng đến Presenter để tách những phương thức xuất ra View ra khỏi Model mà vẫn giữ được cách gọi nhẹ nhàng khi ở Model.

Presenter

Presenter là một kiểu thiết kế khá mới mẻ (dường như nó chỉ sinh ra cho Laravel?!), mình chưa tìm thấy định nghĩa chính thức từ wiki hay nơi nào khác?! Tuy nhiên mục đích của rõ ràng nhất là để khắc phục nhược điểm nêu ở trên: Làm cho Model, View được sạch sẽ và nhất quán.

Thư viện Presenter được dùng nhiều tại thời điểm hiện tại là: robclancy/presenter, nó được thiết kế nhỏ nhẹ mà khá mạnh mẽ cho mục đích sử dụng.

robclancy/presenter được lưu trữ tại địa chỉ: https://github.com/robclancy/presenter, trong đó có một tài liệu cài đặt, giới thiệu và sử dụng rõ ràng. Tuy nhiên, cho mục đích của bài viết này, mình xin được nêu cách sử dụng nó ra đây.

Cài đặt

Thêm robclancy/presenter trong phần "require" file composer.json của bạn.

"robclancy/presenter": "1.1.*"

Chạy composer update để cài đặt (hoặc cập nhật) lại source.

Cuối cùng thêm Provider của nó vào app/config/app.php của bạn:

'Robbo\Presenter\PresenterServiceProvider',

Sử dụng

Presenter rất dễ dàng sử dụng và nếu bạn có để mã trong như trong ví dụ đầu thì bạn cũng dễ dàng sửa đổi lại mà không làm vỡ cấu trúc method.

Giờ tôi sẽ viết lại ví dụ trên đầu theo kiểu thiết kế Presenter:

Một lớp UserPresenter kế thừa lại Robbo\Presenter\Presenter. Ném lại các phương thức trong User cũ vào lớp UserPresenter mới.

<?php

class UserPresenter extends Robbo\Presenter\Presenter {

    public function fullname()
    {
        return $this->first_name . $this->last_name;
    }

    public function avatar()
    {
        return HTML::image(Gravatar::src($this->email));
    }

    public function presentCreatedAt()
    {
        return $this->created_at->format('d/m/Y H:i');
    }

}

User đã trở về với sự đơn giản của nó, bạn cần một phương thức getPresenter để trả về lớp Presenter của Model.

<?php

use Robbo\Presenter\PresentableInterface;

class User extends Eloquent implements PresentableInterface {

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';


    /**
     * Return a created presenter.
     *
     * @return Robbo\Presenter\Presenter
     */
    public function getPresenter()
    {
        return new UserPresenter($this);
    }
}

UserPresenter có mục đích là tải lại các phương thức, biến... từ User khi bạn gọi đến. Và ở bạn có thể tự do thêm các phương thức logic để thao tác với View.

Nếu muốn ghi đè biến của Model, bạn có thể sử dụng prefix present trước tên biến. (Như presentCreatedAt() ở trên)

Giờ thì pass biến qua view:

<?php

$users = User::all();
return View::make('view-template', compact('users'))

Ở đây, bạn cần hiểu cách làm việc của Presenter: Khi bạn truy xuất dữ liệu từ User trong controller, bạn sẽ thao tác với User như thông thường. Khi pass biến qua View, dữ liệu sẽ được tự động chuyển thành UserPresenter thông qua phương thức getPresenter().

Vì vậy, trong View bạn có thể gọi bất cứ phương thức nào có trong User hay UserPresenter. Có thể gọi biến thông qua ArrayAccess, xem thêm tại đây

@foreach ($users as $user)
    <p>
        {{ $user->avatar() }}

        {{ $user['first_name'] }}
        {{ $user->first_name }}

        <span>{{ $user->fullname() }}</span>
        <time>{{ $user->created_at }}</time>
    </p>
@endforeach

Giờ thì app của bạn đã được phân định rõ ràng giữa Model, và Presenter Model. Với Model bạn chỉ nên giữ nó với những phương thức thao tác CSDL, hoặc self-validation. Còn Presenter Model bạn có thể thoải mái định dạng lại dữ liệu Model, hay thậm chí chèn cứng mã HTML vào mà không bị cảm thấy lộn xộn như khi để chúng ở Model.


Để cho được rõ ràng, lớp UserPresenter mình khuyến nghị các bạn đặt trong app/presenters/UserPresenter.php, và sử dụng autoload-class trong composer để tự động load chúng.

"classmap": [
    "app/presenters",
]

Chạy composer dump-autoload để nạp lại file autoload.

Tổng kết

Trong bài viết này bạn đã có thêm một cách thức đẹp nhất cho việc tuyền và định dạng lại dữ liệu từ Model sang View trong Laravel. Một cách thiết kế và tổ chức các lớp, thư mục sao cho gọn gàng và sạch sẽ...

Nếu có bất kỳ thắc mắc hoặc ý kiến, vui lòng để lại comment bên dưới!