07/09/2018, 10:46

Giải Thích Về Dependency Injection Pattern Sử Dụng Laravel 5

Lưu ý 0.0: Câu chữ của bài viết và source code minh họa kèm theo được tinh gọn tới mức tối đa để ngay cả khi độc giả không biết về Laravel framework vẫn có thể theo dõi và hiểu được về Dependency Injection. Lưu ý 0.1: Độc giả cần biết về ít nhất một trong các ngôn ngữ lập trình OOP. Nếu bạn ...

Lưu ý 0.0: Câu chữ của bài viết và source code minh họa kèm theo được tinh gọn tới mức tối đa để ngay cả khi độc giả không biết về Laravel framework vẫn có thể theo dõi và hiểu được về Dependency Injection. Lưu ý 0.1: Độc giả cần biết về ít nhất một trong các ngôn ngữ lập trình OOP. Nếu bạn chưa biết OOP nghĩa là gì thì bạn nên tìm hiểu về nó ở khóa học giới thiệu về OOP trong PHP rồi sau đó quay lại đọc bài viết này.

Dependency Injection hay (DI) đang là một trong những design pattern đang được sử dụng ngày càng phổ biến trong hầu hết các framework hiện đại. Nếu bạn từng tìm hiểu về phiên bản 5.4 của Laravel framework, thì có nhiều khả năng bạn đã áp dụng design pattern này trong lúc lập trình mà có thể bạn không để ý. Bài viết này sẽ giúp bạn tìm hiểu về Dependency Injection thông qua các ví dụ minh hoạ sử dụng Laravel framework phiên bản 5.4.

Lưu ý 0.2: Bài viết này dành cho người bắt đầu và không đòi hỏi bạn cần hiểu về Dependency Pattern. Hãy tiếp tục đọc ngay cả khi bạn chưa từng đọc về Dependency Pattern.

Dependency Injection trong Laravel

Trước tiên dành cho những ai muốn một định nghĩa trước khi bắt đầu...

Dependency Injection Là Gì

Dependency Injection là một design pattern hay một kiểu mẫu thiết kế phần mềm được áp dụng trong trường hợp mà ở đó một object A phụ thuộc vào object B.

Trường hợp một object này phụ thuộc vào object khác là khá phổ biến trong lập trình. Một ví dụ đơn giản có thể kể đến như method registerAccount() trong object User thực hiện việc đăng ký tài khoản sẽ cần tới object Mailer dùng để gửi email kích hoạt tài khoản.

Khi một object A phụ thuộc vào một Object B thì một hệ lụy có nhiều khả năng xảy ra đó là sự thay đổi trong B có thể khiến Object A cũng phải thay đổi theo. Sử dụng Dependency Injection chúng ta có thể giảm thiểu sự thay đổi hoặc giữ nguyên source code của Object A ngay cả khi Object B thay đổi mà vẫn đảm bảo ứng hoạt động như dự kiến.

Tiếp theo chúng ta sẽ nhảy vào code một dự án sử dụng Laravel 5.4 để xem Dependency Injection có thể được áp dụng như thế nào trong framework này.

Ứng Dụng DI trong Laravel 5

Ở phần này chúng ta sẽ tạo một ứng dụng web nhỏ sử dụng Laravel framework. Ứng dụng này sẽ có một một chức năng duy nhất là hiển thị thông tin của người dùng lấy từ Database. Để đơn giản hóa dữ liệu người dùng được seed vào bảng users trên database thủ công.

Lưu ý 1.0: Bạn có thể tải source code của dự án từ Github sử dụng link này.

Đầu tiên nếu bạn đã cài PHP, composer và MySQL trên máy thì tôi khuyên bạn nên cài framework này về máy để có thể vừa theo dõi vừa chạy source code của ứng dụng trong bài viết này.

Bước tiếp theo chúng ta sẽ seed dữ liệu người dùng vào database. Laravel cung cấp tiện ích giúp chúng ta thực hiện công việc này một cách nhanh chóng. Mở tập tin appdatabaseseedsDatabaseSeeder.php và cập nhật method run() thành như sau:

<?php

use IlluminateDatabaseSeeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UsersTableSeeder::class);
        AppUser::insert([
            ['name' => 'User One', 'email' => 'user1@example.net', 'password' => 'abc123'],
            ['name' => 'User Two', 'email' => 'user2@example.net', 'password' => '123456']
        ]);
    }
}

Đoạn code trên sẽ thêm hai record vào bảng users trên database. Tuy nhiên thì chúng ta cần thực hiện thao tác migrate database để hoàn tất việc thêm dữ liệu vào DB.

Lưu ý: Bạn cần cập nhật các thông tin thiết lập cho MySQL database sử dụng trong ứng dụng trong file .env.php trước khi chạy migration.

Sau khi thiết lập thông tin database sử dụng trong ứng dụng bạn chạy câu lệnh sau:

$ php artisan migrate

Sau đó bạn có thể kiểm tra lại dữ liệu được thêm vào trong database sử dụng MySQL client hoặc sử dụng PhpMyAdmin (Nếu bạn am hiểu về Laravel bạn có thể sử dụng Tinker để xem dữ liệu trong DB).

Tiếp theo chúng ta sẽ tạo một controller cho ứng dụng để xử lý request lấy dữ liệu của người dùng. Laravel framework hỗ trợ chúng ta có thể tạo controller thông qua chạy lệnh trên terminal (hoặc command prompt trên Windows). Mở terminal và chạy câu lệnh sau:

$ php make:controller UsersController

Kết thúc câu lệnh bạn sẽ thầy tập tin UsersController.php được tạo ra trong thư mục app/Http/Controllers của dự án.

Tiếp theo trong controller mới tạo ra bạn thêm một action index với nội dung như sau.

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;

class UsersController extends Controller
{
    public function profile()
    {
        return "Hiển thị profile của user đăng nhập!";
    }
}

Và định nghĩa route cho *actiontrên bằng cách thêm vào dòng lệnh sau vào cuối tập tinapp outesweb.php` như sau:

Route::get('/users/profile', 'UsersController@profile');

Khi truy cập vào địa chỉ web chạy ứng dụng ví dụ http://laravel54-di.dev/users/profile bạn sẽ thấy trình duyệt hiển thị dòng văn bản trả về trong method profile() phía trên.

Tới đây có thể bạn sẽ thắc mắc tại sao không sử dụng một RESTfull URL cho route trên như /users/{id}/profile? Câu trả lời đó là vì chúng ta muốn route trên sẽ hiển thị các thông tin riêng tư của người dùng đang đăng nhập vào hệ thống và chỉ giới hạn cho người dùng này. Chính vì vậy chúng ta sẽ không dùng { id}` trong URL.

Việc xác thực người dùng có thể được thực hiện trong controller, tuy nhiên trong MVC controller nên được thiết kế với mục đích duy nhất là trung gian tiếp nhận request gửi tới và chuyển qua Model xử lý việc lấy data hoặc các business class khác để xử lý các business logic liên quan. Do đó chúng ta sẽ tọ một class UserService để thực hiện việc xác thực người dùng. Trong thư mục app/ bạn tạo một file UserService.php với nội dung sau:

<?php
/**
 * Created by PhpStorm.
 * User: codehub.vn
 * Date: 19/07/2017
 * Time: 19:57
 */

namespace App;

class UserService
{
    public function getLoggedInUser($email, $password)
    {
        $user = AppUser::where('email', '=', $email)
            ->where('password', '=', $password)->first();

        return $user;
    }
}

Method getLoggedInUser trong đoạn code trên sẽ tìm kiếm trong bảng users để lấy ra một record có trường email và password bằng với giá trị của hai biến $email và $password truyền vào hàm này. Nếu không có dữ liệu nào trả về từ DB thì method này sẽ trả về giá trị là null.

Bước tiếp theo là chúng ta sẽ sử dụng UserService trên trong UsersController. Sửa code trong method profile của class UserController thành như sau:

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;

class UsersController extends Controller
{
    public function profile()
    {
        $email = isset($_GET['email']) ? $_GET['email'] : null;
        $password = isset($_GET['password']) ? $_GET['password'] : null;

        $userService = new AppUserService();

        $loggedInUser = $userService->getLoggedInUser($email, $password);

        if ($loggedInUser) {
            return "Chào " . $loggedInUser->name . "!";
        }
        return "Email hoặc pass không hợp lệ!";
    }
}

Ở trên chúng ta thực hiện việc xác thực người dùng sử dụng giá trị các query parameter là email và password trong request URL làm đối số truyền vào cho method getLoggedInUser của UserService.

Tới đây nếu bạn truy cập vào URL http://laravel54-di.dev/users/profile?email=user1@example.net&password=abc123 bạn sẽ thấy trình duyệt hiển thị dòng thông báo:

Xin chào User One

Quay trở lại source code trong method profile() của UsersController bạn sẽ thấy sự hoạt động của method này phụ thuộc vào class UserService. Hay nói cách khác class UsersController có một dependency là UserService.

Laravel framework hỗ trợ việc quản lý dependency của object. Để xem chúng ta có thể áp dụng DI như thế nào thì bạn update lại đoạn code trong profile() thành như sau:

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;

class UsersController extends Controller
{
    public function profile(AppUserService $userService)
    {
        $email = isset($_GET['email']) ? $_GET['email'] : null;
        $password = isset($_GET['password']) ? $_GET['password'] : null;

        $loggedInUser = $userService->getLoggedInUser($email, $password);

        if ($loggedInUser) {
            return "Chào " . $loggedInUser->name . "!";
        }
        return "Email hoặc pass không hợp lệ!";
    }
}

Có gì thay đổi trong đoạn code trên so với phiên bản trước đó? Sự khác biệt đó là chúng ta không khởi tạo object từ UserService như trước đó mà thay vào đó chúng ta quy định method profile cần một đối số truyền vào $userService là một object instance của class UserService.

Bây giờ thử quay trở lại trình duyệt và tải lại trang bạn sẽ thấy kết quả hiển thị không thay đổi.

Điều đáng chú ý ở đây là Laravel đã tự thực hiện việc khởi tạo object $userService sử dụng class UserService thay cho chúng ta!

Làm sao Laravel hiểu được phải khởi tạo instance object từ UserService class như ở trên? Đó là nhờ tính năng Auto Resolving dependency cung cấp bởi framework này. Khi scan method profile(), Laravel nhận thấy method này phụ thuộc vào class AppUserService và do đó nó sẽ tự động khởi tạo object từ class này cho bạn.

Tương tự bạn có thể áp dụng cách làm trên cho tất cả những method nào phụ thuộc vào UserService (nói chi tiết hơn là cần một object khởi tạo từ class này).

Tuy nhiên nếu chúng ta chỉ dừng lại ở đây thôi thì chưa có nhiều điều hữu ích từ việc triển khai Dependency Injection pattern. Hãy tiếp tục!

Bây giờ đặt trường hợp ứng dụng của chúng ta có một số lượng lớn người dùng và để tăng tốc độ tải trang techical architech quyết định sử dụng Redis để cache dữ liệu từ MySQL và do đó chúng ta cần cập nhật UserService để lấy dữ liệu từ Redis thay vì truy vấn trực tiếp MySQL.

Và với yêu cầu như trên chúng ta sẽ refactor code trong method getLoggedInUser trong UserService thành như sau:

<?php
/**
 * Created by PhpStorm.
 * User: codehub.vn
 * Date: 19/07/2017
 * Time: 19:57
 */

namespace App;

class UserService
{
    private $_dbService;

    public function __construct(AppRedisService $dbService)
    {
        $this->_dbService = $dbService;
    }

    public function getLoggedInUser($email, $password)
    {
        $user = $this->_dbService->getLoggedInUser($email, $password);
        return $user;
    }
}

Với cách triển khai mới ở trên việc truy vấn dữ liệu để xác thực người dùng sẽ được ủy thác (hay delegate) cho object $dbService khởi tạo từ class AppDbService. Class này chưa có và chúng ta sẽ cần tạo nó:

<?php
/**
 * Created by PhpStorm.
 * User: codehub.vn
 * Date: 19/07/2017
 * Time: 20:28
 */

namespace App;

class RedisService
{
    public function getLoggedInUser($email, $password) {
        $user = AppUser::where('email', '=', $email)
            ->where('password', '=', $password)->first();
        return $user;
    }
}

Ở trên để đơn giản hóa thì method getLoggedInUser trong class RedisService vẫn sẽ tạm thời truy vấn dữ liệu từ MySQL thay vì sử dụng một thư viện của Redis để truy vấn dữ liệu từ Redis thực sự. Việc này giúp chúng ta tránh phải cài đặt Redis server và đấy dữ liệu vào DB trên Redis server. Tất nhiên trong thực tế bạn sẽ cần triển khai để truy vấn dữ liệu từ Redis server.

Lúc này nếu tải lại trang từ địa chỉ URL lúc trước bạn thấy kết quả vẫn không thay đổi.

Có gì cần chú ý trong source code của cách triển khai mới ở trên? Nếu bạn không sử dụng tính năng Autowiring trong Laravel bạn sẽ cần viết code trong method profile() của UsersController như sau:

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;

class UsersController extends Controller
{
    public function profile()
    {
        $email = isset($_GET['email']) ? $_GET['email'] : null;
        $password = isset($_GET['password']) ? $_GET['password'] : null;

        $redisService = new AppRedisService();
        $userService = new AppUserService($redisService);

        $loggedInUser = $userService->getLoggedInUser($email, $password);

        if ($loggedInUser) {
            return "Chào " . $loggedInUser->name . "!";
        }
        return "Email hoặc pass không hợp lệ!";
    }
}

Với cách viết như trên thì method profile() (và tất cả những object nào phụ thuộc vào UserService) sẽ phải thay đổi nếu như có sự thay đổi trong UserService hoặc RedisService.

Thử tưởng tượng có 200 class khác nhau phụ thuộc vào class UserService.

Nếu ví dụ ở trên vẫn chưa đủ thuyết phục bạn nên sử dụng autowiring (một phương thức triển khai Dependency Injection pattern) thì hãy tiếp tục đọc thêm xíu.

Bây giờ chúng ta cần thêm cấu hình thông tin kết nối với Redis server khi khởi tạo object từ class RedisService:

<?php
/**
 * Created by PhpStorm.
 * User: codehub.vn
 * Date: 19/07/2017
 * Time: 20:28
 */

namespace App;

class RedisService
{
    private $_username;
    private $_password;
    private $_database;

    public function __construct($username, $password, $database)
    {
        $this->_username = $username;
        $this->_password = $password;
        $this->_database = $database;
    }

    public function getLoggedInUser($email, $password) {
        $user = AppUser::where('email', '=', $email)
            ->where('password', '=', $password)->first();
        return $user;
    }
}

Tất nhiên trên thực tế chúng ta cũng cần thêm method connect() để kết nối tới Redis Server để sử dụng các private property mới thêm vào trong class này như $_username, $_password và $_database. Tuy nhiên một lần nữa để đơn giản hóa tôi sẽ bỏ qua phần này.

Lúc này quay lại method profile() của UsersController nếu không sử dụng autowiring bạn sẽ code như sau:

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;

class UsersController extends Controller
{
    public function profile()
    {
        $email = isset($_GET['email']) ? $_GET['email'] : null;
        $password = isset($_GET['password']) ? $_GET['password'] : null;

        $redisService = new AppRedisService(getenv('REDIS_USERNAME'), getenv('REDIS_PASSWORD'), getenv('REDIS_DATABASE'));
        $userService = new AppUserService($redisService);
        ...
    }
}

Chỉ một sự thay đổi đơn giản như trên bạn sẽ cần tìm tất cả các đoạn code sử dụng UserService (hoặc RedisService) và update lại!

Quay trở lại trường hợp source code của chúng ta sử dụng autowiring. Nếu như tải lại trang trên trình duyệt bạn sẽ gặp phải lỗi như sau:

(1/1) BindingResolutionException
Unresolvable dependency resolving [Parameter #0 [ <required> $username ]] in class AppRedisService

Điều này là bởi vì chúng ta đã thay đổi RedisService để yêu cầu 3 đối số khi khởi tạo object từ class này. Với cách triển khai hiện tại (sử dụng *autowiring) thì Laravel không biết được 3 đối số này có giá trị như thế nào.

Chúng ta sẽ tìm câu trả lời cho câu hỏi trên ở một bài viết khác về tính năng service container và class binding trong Laravel.

0