+2

Symfony components: OptionsResolver

Mình là một PHP programmer nhưng bài viết viết PHP lại khá ít (03/19 bài). Sau một khoảng thời gian viết về các ngôn ngữ linh tinh, hôm nay mình sẽ quay về với ngôn ngữ mà có lẽ là mình thạo nhất nhé. Chẳng là vừa rồi mình có viết một PHP package làm việc với OneSignal API. Nên mình có tìm hiểu và sử dụng đến một component của Symfony để validate các tham số truyền vào, đó là Symfony OptionsResolver. Mình thấy nó khá hay và tiện nên viết bài giới thiệu tới mọi người. Hy vọng sẽ giúp cho mọi người có một sự lựa chọn khi có chung một mục đích giống mình. Chúng ta cùng tìm hiểu xem Symfony OptionsResolver là gì, làm việc như thế nào và được ứng dụng ra sao nhé ^^

Symfony OptionsResolver

Symfony OptionResolver component giống như hàm array_replace. Nó cho phép bạn tạo một hệ thống các tùy chọn với các tùy chọn bắt buộc, các giá trị mặc định, kiểm tra (kiểu dữ liệu, giá trị dữ liệu), tạo chuẩn cho dữ liệu đầu ra, ...

Cài đặt

Để cài đặt package này, bạn có thể làm theo 2 cách:

Sau đó, bạn chỉ cần require file vendor/autoload.php để sử dụng.

Sử dụng

Bây giờ, chúng ta cùng sử dụng ví dụ ở trang chủ của Symfony để tìm hiểu cách hoạt động và tính ứng dụng của component này nhé. Chúng ta có một class sử dụng để email. Class này có các tham số cài đặt là host, username, passwordport để cài đặt.

class Mailer
{
    protected $options;

    public function __construct(array $options = array())
    {
        $this->options = $options;
    }
}

Khi sử dụng, chúng ta cần truy cập các thông số cài đặt. Chúng ta cần phải xử lý rất nhiều nghiệp vụ logic để kiểm tra các cài đặt đó. Xem nó có tồn tại và hợp lệ hay không:

class Mailer
{
    // ...
    public function sendMail($from, $to)
    {
        $mail = ...;

        $mail->setHost(isset($this->options['host'])
            ? $this->options['host']
            : 'smtp.example.org');

        $mail->setUsername(isset($this->options['username'])
            ? $this->options['username']
            : 'user');

        $mail->setPassword(isset($this->options['password'])
            ? $this->options['password']
            : 'pa$$word');

        $mail->setPort(isset($this->options['port'])
            ? $this->options['port']
            : 25);

        // ...
    }
}

Với đoạn code trên, bạn có thể thấy nó rất khó để đọc, lặp đi lặp lại các câu lệnh logic để kiểm tra? Giống như lời giới thiệu ban đầu, là chúng ta có thể sử dụng method array_replace của PHP để xử lý việc này:

class Mailer
{
    // ...

    public function __construct(array $options = array())
    {
        $this->options = array_replace(array(
            'host'     => 'smtp.example.org',
            'username' => 'user',
            'password' => 'pa$$word',
            'port'     => 25,
        ), $options);
    }
}

Đoạn trên thì code của chúng ta đã sáng sủa hơn, đúng không ạ. Nhưng sẽ ra sao khi chúng ta viết nhầm tên của một tùy chọn như:

$mailer = new Mailer(array(
    'usernme' => 'johndoe',  // usernAme misspelled
));

Đoạn code trên không gây ra lỗi. Nhưng nó sẽ phát sinh ra bug. Và chúng ta sẽ mất kha khá thời gian để tìm hiểu nguyên nhân để xử lý (do lỗi typo, và lỗi này khá là khó tìm đấy nhể :v?). Vậy nếu sử dụng OptionsResolver, những bất cập mà chúng ta đưa ra từ nãy đến giờ có giải quyết được không? Có dễ dàng và đơn giản để sử dụng hay không? Chúng ta cùng thử nhé 😄!

use Symfony\Component\OptionsResolver\OptionsResolver;

class Mailer
{
    private $mailerOptions;

    public function __construct(array $options = array())
    {
        $optionsResolver = new OptionsResolver;

        $optionsResolver->setDefaults(array(
            'host'     => 'smtp.example.org',
            'username' => 'user',
            'password' => 'pa$$word',
            'port'     => 25,
        ));

        $this->mailerOptions = $optionsResolver->resolve($options);
    }
}

Chúng ta thử viết sai một option xem sao:

$mailer = new Mailer(array(
    "usernme" => "lorem"
));

Khi chạy thử code, chúng ta sẽ nhận được một exception ngay:

PHP Fatal error:  Uncaught exception 'Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException' with message 'The option "usernme" does not exist. Defined options are: "host", "password", "port", "username".' in vendor/symfony/options-resolver/OptionsResolver.php:685

Thật là tiện lợi, phải không? OK, bây giờ chúng ta sẽ đi qua một số tùy chọn khác nữa nhé 😄

Option required

Nếu như trong class của chúng ta cần một tùy chọn bắt buộn phải truyền vào. Ví dụ như host chẳng hạn. Chúng ta có thể sử dụng method setRequired() để khai báo. Method này nhận kiểu dữ liệu là string hoặc array:

$optionResolver->setRequired('host');

// or

$optionResolver->setRequired(array('host', 'port'));

Khi bạn khởi tạo mà không truyền các tham số được yêu cầu, bạn sẽ nhận được một ngoại lệ:

PHP Fatal error:  Uncaught exception 'Symfony\Component\OptionsResolver\Exception\MissingOptionsException' with message 'The required options "host", "port" are missing.

Để kiểm tra một option là có yêu cầu hay không, bạn có thể sử dụng method isRequired(). Bạn cũng có thể sử dụng getRequiredOptions() để lấy tất cả các option được yêu cầu. Ngoài ra, chúng ta có thể sử dụng method isMissing() để kiểm tra xem tùy chọn đó đã được truyền vào hay chưa. Khác với isRequired() là kiểm tra xem tùy chọn đó có bắt buộc hay không. Bạn cũng có thể sử dụng getMissingOptions() để lấy ra tất cả các options chưa được truyền vào.

Type validation

Ngoài việc kiểm tra xem tùy chọn đó có được truyền vào hay không là chưa đủ để đảm bảo dữ liệu của chúng ta đúng để chạy. Options Resolver hỗ trợ chúng ta kiểm tra kiểu của dữ liệu. Để thực hiện, chúng ta gọi method setAllowedTypes().

$optionsResolver->setAllowedTypes('port', 'int');
// ...
$mailer  = new Mailer(array(
    'port' => '25'
));

Bạn sẽ nhận được một exception như sau:

PHP Fatal error:  Uncaught exception 'Symfony\Component\OptionsResolver\Exception\InvalidOptionsException' with message 'The option "port" with value "25" is expected to be of type "int", but is of type "string".'

Các kiểu dữ liệu có thể kiểm tra là những function có prefix is_<type>() được định nghĩa bởi PHP. Chúng ta có thể điểm qua những function quen thuộc như: is_int(), is_bool(), is_array(), ... Ngoài ra, bạn có thể sử dụng addAllowedTypes() để định nghĩa thêm kiểu dữ liệu mà bạn chấp nhận.

Value validation

Một vài tùy chọn chỉ chấp nhận một số kiểu giá trị nhất định, ví dụ như trong class email có thêm tùy chọn transport định nghĩa kiểu kết nối (ví dụ như: SMTP, IMAP, ...) thì bạn có thể giới hạn nó bằng function setAllowedValues(). Function này có hai tham số. Tham số đầu tiên là tên tùy chọn, tham số thứ 2 là kiểu giá trị được chấp nhận. Tham số thứ hai có thể là chuỗi, mảng hoặc một callback function để bạn có thể thực hiện việc kiểm tra dữ liệu:

$optionsResolver->setAllowedValues('transport', array('imap', 'smtp', 'sendmail'));

$mailer = new Mailer(array(
    'transport' => 'send-mail'
));

Nếu bạn truyền vào không đúng, bạn sẽ nhận được một ngoại lện InvalidOptionsException như sau:

PHP Fatal error:  Uncaught exception 'Symfony\Component\OptionsResolver\Exception\InvalidOptionsException' with message 'The option "transport" with value "send-mail" is invalid. Accepted values are: "smtp", "mail", "sendmail".'

Chúng ta thử validate giá trị của host phải là một địa chỉ IP nhé:

$optionsResolver->setAllowedValues('host', function($hostValue) {
    return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $hostValue);
});
// ...
$mailer = new Mailer({
     'host' => '12.34'
});

Chúng ta sẽ nhận được exception như sau:

PHP Fatal error:  Uncaught exception 'Symfony\Component\OptionsResolver\Exception\InvalidOptionsException' with message 'The option "host" with value "12.34" is invalid.'

Option Normalization

Đôi khi, chúng ta muốn chuẩn hóa dữ liệu để đảm bảo chương trình chạy đúng. Ví dụ với tùy chọn host chúng ta cần phải có thêm http ở đầu để đảm việc thực hiện luôn được diễn ra tốt đẹp, chúng ta sử dụng function setNormalizer() để chuẩn hóa dữ liệu:

use Symfony\Component\OptionsResolver\Options;

$optionsResolver->setNormalizer('host', function(Options $options, $hostValue) {
    if (!preg_match('/^https?\:\/\//', $hostValue)) {
        $hostValue = "http://" . $hostValue;
    }

    return $hostValue;
});
// ...
public function getMailerOptions()
{
    return $this->mailerOptions;
}

// ...
$mailer = new Mailer(array(
    'host' => 'https://gmail.com',
));
var_dump($mailer->getMailerOptions());

Trong ví dụ trên, với biến $options của Options instance, bạn có thể sử dụng nó để kiểm tra các tùy chọn khác trước khi thực hiện chuẩn hóa dữ liệu. Ví dụ như kiểm tra xem tùy chọn ssl có true hay không, nếu có thì thêm s sau http, còn không thì thôi chẳng hạn 😄!

Options without Default Values

Nãy giờ mình toàn làm việc với các ví dụng có sử dụng giá trị mặc định cho các tùy chọn (default values). Làm như vậy nó không thật sự an toàn vì chúng ta không biết rằng người khác có truyền các tùy chọn đó vào hay không. Vậy chúng ta cần tới sự trợ giúp của method setDefined() để định nghĩa các tùy chọn sẽ có mà không cần phải khai báo trong setDefaults(). Function này nhận tham số có thể là chuỗi đơn hoặc một mảng các tùy chọn bạn cần định nghĩa:

$optionsResolver->setDefaults(array(
    'port' => 25
))->setDefined(array('host', 'username', 'password'));
// ...
$mailer = new Mailer(array(
    'transport' => 'smpt'
));

Khi bạn truyền một tùy chọn không có trong danh sách được định nghĩa, bạn sẽ nhận được UndefinedOptionsException exception:

PHP Fatal error:  Uncaught exception 'Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException' with message 'The option "transport" does not exist. Defined options are: "host", "password", "port", "username".'

Bạn có thể sử dụng hai function isDefined() hoặc getDefinedOptions() để kiểm tra một tùy chọn hay lấy ra danh sách các tùy chọn đã được định nghĩa.

Lời kết

Vậy là mình đã giới thiệu xong một component mình nghĩ khá là tiện ích của Symfony. Hy vọng nó sẽ giúp bạn giải quyết các vấn đề tương tự khi cần phải giải quyết. Bài viết của mình đến đây là kết thúc. Hẹn mọi người ở bài viết sau, có lẽ cũng sẽ là một PHP component hay ho nào đó của Symfony hoặc lại là một ngôn ngữ khác không phải là PHP (yaoming)(seeyou)!!!!

Link các ví dụ trong bài: https://github.com/namnv609/viblo-options-resolver-demo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí