Laravel, Lối đi đúng đắn!

** Giới thiệu **

PHP đã ra đời từ lâu, có rất nhiều tài liệu nói về PHP. Có nói PHP ở mọi nơi thì cũng không có gì là sai. Nhưng điều đáng nói là những tài liệu PHP đó đều đã lỗi thời, có lẽ chỉ phù hợp cho những năm của thập kỷ trước. PHP đã thực sự chuyển mình và bắt kịp các ngôn ngữ hiện đại khác khi ra đời PHP 5.4 và PHP Unit từ năm 2012.

Bài viết này chỉ giúp các bạn định hướng và làm quan với PHP cụ thể là Laravel.

** Bắt đầu **

Điều đâu tiên là các bạn hay luôn update những phiên bản mới nhất của PHP để bạn có thể hưởng lợi từ các bug fix và các chức năng cải tiến.

Với môi trường phát triển cụ thể. Nếu bạn đang sử dụng Mac, bạn có thể cài đặt MAMP hay XAMPP. Nhưng mình sẽ giới thiệu với các bạn một cách hay hơn nhiều, đó là dùng Vagrant.

Việc chạy ứng dụng web của bạn trên các môi trường development và production khác nhau sẽ dẫn đến những lỗi rất nghiêm trọng khi bạn đưa ứng dụng ra chạy lâu dài ở production. Một “cái chết bất thình lình” sẽ đến với bạn mà thậm chí bạn chẳng hiểu vì sao. Yên tâm, code của bạn rất ổn vì nó đã chạy được trên development nhưng không có nghĩa nó chạy tốt như vậy trên production.

Bạn nên dành thời gian tìm hiểu về Virtual Machine (VM) ngay những ngày đầu phát triển web của mình. Mình khuyên dùng VirtualBox, đơn giản, dễ dùng và cực kì mạnh mẽ

VM mang đến cho bạn lợi ích rất lớn về tính đồng bộ môi trường phát triển trong team hay trong các phase của quá trình phát triển. Nhưng có một sự khó chịu khi sử dụng VM khi bạn muốn phân phối VM này đến các thành viên trong team hay bạn muốn sử dụng VM này trên những máy tính khác. Bạn phải copy nguyên một file VM Image có thể nặng đến chục GB. Chúng ta phải cảm ơn Vagrant, một giải pháp tuyệt vời cho việc phân phối cũng như khởi tạo các VM. Chỉ với một file Vagrantfile nặng vài KB, bạn có thể dễ dàng phân phối VM này đi bất kì đâu để chạy. Tất cả công việc bạn cần làm khi đã có Vagrantfile đó là chạy vagrant up và VM đã sẵn sàng cho bạn.

** Tiêu chuẩn Code **

Framework Interop Group đã đưa ra một loạt các khuyến cáo về Code Style. Chúng bao gồm PSR-0, PSR-1, PSR-2, PSR-4 và thêm một chuẩn về logging nữa là PSR-3. Bạn nên đọc kĩ về những chuẩn này và tuân theo chúng, ngoài ra còn có một số chuẩn khác ít thông dụng hơn (ngày trước thông dụng) như PEAR Coding Standards và Zend Coding Standards tuy nhiên về cá nhân mình khuyên dùng PSR vì tính thông dụng cũng như ưu điểm của nó.

Bạn không nhất thiết phải tuân thủ hoàn toàn Code Style của PSR nhưng khi bạn làm việc trong một team, hãy tuân thủ theo Coding Standards của team đó, hầu hết Coding Standards của team được chỉnh sửa đôi chút từ PSR hay các standard ở trên nhưng cũng có team giữ nguyên.

** Dependency Management **

Không cần tải file zip, không cần giải nén một package mà vẫn có thể sử dụng pakage đó trong code của bạn. Mình giới thiệu với các bạn một thứ tuyệt vời: Composer.

  • Composer tuyệt vời ở 3 yếu tố chính: autoloading system, packagist, portable.

  • Composer tích hợp sẵn một hệ thống autoloading cho phép làm việc với hai chuẩn autoload rất thông dụng đó là PSR-0 và PSR-4. Tất cả mọi việc bạn cần làm là thêm dòng require 'vendor/autoload.php'; vào run file của ứng dụng.

  • Composer sử dụng package repository chính là Packagist (bạn vẫn có thể custom repository được). 99% các package bạn cần đều nằm ở Packagist. Nếu package đã tồn tại trên Packagist, tất cả những việc bạn cần làm đó là thêm package và version của nó vào composer.json, chạy composer update và bắt đầu sử dụng ngay mà không gặp trở ngại nào.

** Repository **

Hãy thử xem ví dụ với việc lấy thông tin của một người dùng như sau:

class UsersController extends BaseController
{
    // ... Something before
    public function index()
    {
        $users = User::with('groups', 'posts', 'comments')
                     ->where('group_id', '=', 2)
                     ->orWhere(function($query) {
                            $query->where('votes', '>', 100)
                                  ->where('title', '<>', 'Admin');
                     })
                     ->groupBy('group_id');
                     ->limit('10')
                     ->get();

        return View::make('users.index')->with('users', $users);
    }
    // ... Something after
}

class GroupsController extends BaseController
{
    // ... Something before
    public function index()
    {
        $groups = Group::all();

        $users = User::with('groups', 'posts', 'comments')
                     ->where('group_id', '=', 2)
                     ->orWhere(function($query) {
                            $query->where('votes', '>', 100)
                                  ->where('title', '<>', 'Admin');
                     })
                     ->groupBy('group_id');
                     ->limit('10')
                     ->get();

        return View::make('users.index')->with(['users' => $users, 'groups' => $groups]);
    }
    // ... Something after
}

Trong vị dụ trên, Cả UsersController và GroupsController đều cần lấy thông tin của người dùng. Chắc các bạn đều nhận ra rằng có quá nhiều đoạn code trùng lặp. Hơn nữa, với code như vậy thì không làm giảm hiện năng nhưng nó lại làm cho bạn khó khăn hơn rất nhiều lần trong việc debug. Thay vào đó chúng ra sẽ swer dụng Repository.

class UserController extends BaseController
{
    protected $users;

    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    // ... Something before
    public function index()
    {
        $users = $this->users->all();

        return View::make('users.index')->with('users', $users);
    }
    // ... Something after
}

Và bạn có thêm 2 file khác nữa là UserRepository.php làm contract và DbUserRepository.php là concret class, bạn nên đặt chúng trong thư mục riêng ví dụ thư mục app/repositories và thêm thư mục này vào autoload.classmap array:

interface UserRepository
{
    public function all();
    public function getUserById($id);
    public function getBannedUsers();
    public function getUserByStatus($status);
    public function getPaidUser();
}
class DbUserRepository implements UserRepository
{
    public function all()
    {
        return User::with('groups', 'posts', 'comments')
                     ->groupBy('group_id');
                     ->limit('10')
                     ->get();
    }

    public function getUserById($id)
    {
        return User::with('groups', 'posts', 'comments')
                     ->where('id', '=', $id)
                     ->get();
    }

    public function getBannedUsers()
    {
        return User::with('groups', 'posts', 'comments')
                     ->where('banned', '=', true)
                     ->groupBy('group_id');
                     ->limit('10')
                     ->get();
    }

    public function getUserByStatus($status)
    {
        return User::with('groups', 'posts', 'comments')
                     ->where('status', '=', $status)
                     ->groupBy('group_id');
                     ->limit('10')
                     ->get();
    }

    public function getPaidUser()
    {
        return User::with('groups', 'posts', 'comments')
                     ->where('is_paid', '=', true)
                     ->groupBy('group_id');
                     ->limit('10')
                     ->get();
    }
}

Tất nhiên là với Laravel 5.1 thì đã có sự thay đổi. Chúng ta sẽ tìm hiểu trong một bài khác.

Điều cuối cùng bạn cần làm để cho code này chạy được đó là bind interface với concret class:

App::bind('UserRepository', 'DbUserRepository');

** Exceptions for Errors **

Mình có một ví dụ

class DbUserRepository
{
    public function getUserById($id)
    {
        // Check $id is integer
        if (!is_int($id) && $id > 0) {
            return false;
        }

        // Check role
        // Auth::user()->can() is my example method to check current can do something or not
        if (!Auth::user()->can('users.get_user')) {
            return false;
        }

        // Another checks

        // Else: get user and returns
    }
}

Ví dụ trên nảy sinh vài vấn đề như sau:

  • Bạn không thể kiểm soát được loại lỗi trả về trong quá trình làm việc của hàm này. Làm sao để bạn biết khi nào thì lỗi do $id không hợp lệ, khi nào là lỗi do user không có quyền hạn get thông user khác.
  • Rất khó để mở rộng hàm này, cơ chế điều khiển lỗi quá nghèo nàn và thiếu thông tin.
  • Có thể có bạn sử dụng mẹo là thêm một mảng $errors vào thân hàm rồi return $errors đó khi có lỗi. Nhưng cũng hơi “căng” vì nếu kết quả user trả về cùng là một arrray thì móm à, làm thế nào phân biệt đc kết quả nào là fail/success?

Cách đúng là bạn cần dùng class Exception. Khi bạn throw exception, bạn sẽ truyền tất cả các thông tin liên quan đến lỗi vào một class do bạn tạo ra thừa kế từ class Excetion này. Ví dụ luôn:

class NotAuthorizeException extends \Exception
{
    protected $role;

    public function __construct($message, $delinquentRole = '')
    {
        $this->role = $delinquentRole;

        parent::__construct($message);
    }

    public function getRole()
    {
        return $this->role;
    }
}

Và tiếp theo là ví dụ khi sử dụng DbUserRepository trong controller hoặc nơi nào đó khác.

class UsersController
{
    protected $users;

    public funciton __construct(UserRepository $userRepo)
    {
        $this->users = $userRepo;
    }

    public function show($id)
    {
        try {
            $user = $this->users->getUserById($id);
        } catch (InvalidArgumentException $e) {
            // When $id is invalid
        } catch (NotAuthorizeException $e) {
            // When user can't get another user info
        }
    }
}

Bài viết sẽ được tiếp tục trong số ra tiếp theo. 😃


All Rights Reserved