Model

Model là phần trong mô hình MVC. Là đối tượng đại diện cho phần dữ liệu, phương thức xử lý và nghiệp vụ logic.

Bạn có thể tạo mới các lớp model bằng việc kế thừa từ lớp yii\base\Model hoặc các lớp con của nó. Lớp cơ sở yii\base\Model hỗ trợ nhiều tính năng như:

Lớp Model thường dựa trên lớp để thực hiện chức năng nâng cao, chẳng hạn Active Record. Vui lòng tham khảo thêm tài liệu để biết thêm thông tin.

Lưu ý: Model của bạn không phải bắt buộc kế thừa từ lớp yii\base\Model. Tuy nhiên, vì Yii chứa nhiều thành phần dựng lên và hỗ trợ cho yii\base\Model, vì thế nó là lớp cơ sở cho các lớp Model.

Thuộc tính (Attribute)

Model đại diện cho tầng xử lý nghiệp vụ và chứa các thuộc tính. Mỗi thuộc tính được truy cập toàn cục như phần tử của model. Phương thức yii\base\Model::attributes() sẽ mô tả các thuộc tính trong lớp model hiện có.

Bạn có thể truy cập vào thuộc tính như các phần tử của các đối tượng:

$model = new \app\models\ContactForm;

// "name" là tên thuộc tính của ContactForm
$model->name = 'example';
echo $model->name;

Bạn có thể truy cập các thuộc tính như truy cập mảng các phần tử, nhờ sự hỗ trợ từ lớp ArrayAccessArrayIterator bởi yii\base\Model:

$model = new \app\models\ContactForm;

// truy cập các thuộc tính như mảng các phần tử
$model['name'] = 'example';
echo $model['name'];

// iterate attributes
foreach ($model as $name => $value) {
    echo "$name: $value\n";
}

Định nghĩa các thuộc tính

Mặc định, nếu Model của bạn được kế thừa từ lớp yii\base\Model, và tất cả các biến có phạm vi toàn cục trong lớp . Ví dụ, Model ContactForm sau có bốn thuộc tính là: name, email, subjectbody. Model ContactForm dùng để nhận dữ liệu từ form HTML.

namespace app\models;

use yii\base\Model;

class ContactForm extends Model
{
    public $name;
    public $email;
    public $subject;
    public $body;
}

Bạn có thể ghi đè phương thức yii\base\Model::attributes() để định nghĩa các thuộc tính theo các cách khác. Phương thức nên được trả tên của thuộc tính trong Model. Ví dụ, lớp yii\db\ActiveRecord trả về danh sách tên của các cột liên quan tới các bảng trong CSDL như tên các thuộc tính. Bạn có thể ghi đè các phương thức như __get(), __set() để có thể truy cập các thuộc tính như các đối tượng thông thường.

Nhãn của thuộc tính

Mỗi khi cần hiển thị giá trị hoặc nhận dữ liệu cho thuộc tính, bạn cần hiển thị nhãn tương ứng với các thuộc tính . Ví dụ, với thuộc tính firstName, bạn cần hiển thị nhãn First Name nhãn này sẽ thân thiện hơn khi hiển thị tới người dùng với việc nhập dữ liệu và hiện thông báo.

Bạn có thể lấy tên nhãn các thuộc tính quan việc gọi phương thức yii\base\Model::getAttributeLabel(). Ví dụ,

$model = new \app\models\ContactForm;

// hiển thị "Name"
echo $model->getAttributeLabel('name');

Mặc định, nhãn thuộc tính sẽ tự động tạo từ tên của thuộc tính. Phương thức yii\base\Model::generateAttributeLabel() sẽ tạo mới các nhãn cho các thuộc tính. Nó sẽ chuyển tên các biến thành các từ mới qua việc chuyển ký tự đầu tiên thành ký tự in hoa. Ví dụ, username thành Username, và firstName thành First Name.

Nếu bạn không muốn việc tạo các nhản bằng cách tự động, bạn cần ghi đè phương thức yii\base\Model::attributeLabels() để mô tả các thuộc tính. Chẳng hạn,

namespace app\models;

use yii\base\Model;

class ContactForm extends Model
{
    public $name;
    public $email;
    public $subject;
    public $body;

    public function attributeLabels()
    {
        return [
            'name' => 'Tên liên hệ',
            'email' => 'Địa chỉ email',
            'subject' => 'Tiêu đề',
            'body' => 'Nội dung',
        ];
    }
}

Với ứng dụng cần hỗ trợ đa ngôn ngữ, bạn cần dịch lại nhãn của các thuộc tính. Xem trong phương thức attributeLabels() , như sau:

public function attributeLabels()
{
    return [
        'name' => \Yii::t('app', 'Tên liên hệ'),
        'email' => \Yii::t('app', 'Địa chỉ email'),
        'subject' => \Yii::t('app', 'Tiêu đề'),
        'body' => \Yii::t('app', 'Nội dung'),
    ];
}

Bạn có thể gán nhãn cho các thuộc tính. Chẳng hạn, dựa vào scenariocủa Model đã được sử dụng , bạn có thể trả về các nhãn khác nhau cho các thuộc tính khác nhau.

Lưu ý: Chính xác rằng, nhãn của thuộc tính là một phần của views. Tuy nhiên việc khai báo các nhãn vào Model thường rất tiện lợi, code dễ nhìn và tái sử dụng.

Kịch bản (Scenarios)

Model thường được sử dụng ở các kịch bản khác nhau . Ví dụ, Model User dùng để xử lý việc đăng nhập, nhưng cũng có thể được dùng ở mục đăng ký. Ở các kịch bản khác nhau, Model có thể được dùng trong các nghiệp vụ và xử lý logic khác nhau. Ví dụ,thuộc tính email có thể được yêu cầu trong mục đăng ký tài khoản mới, nhưng không được yêu cầu khi xử lý đăng nhập.

Mỗi Model sử dụng thuộc tính yii\base\Model::scenario để xử lý tuỳ theo kịch bản cần đợc dùng. Mặc định, Model sẽ hỗ trợ kịch bản là default. Xem đoạn mã sau để hiểu 2 cách thiết lập kịch bản cho Model. setting the scenario of a model:

// kịch bản được thiết lập qua thuộc tính
$model = new User;
$model->scenario = User::SCENARIO_LOGIN;

// kịch bản được thiết lập qua việc cấu hình khởi tạo
$model = new User(['scenario' => User::SCENARIO_LOGIN]);

Mặc định, các kịch bản được hỗ trợ bởi model được xác định qua các nguyên tắc xác minh được mô tả ở Model. Tuy nhiên, bạn có thê tuỳ biến bằng cách ghi đè phương thức yii\base\Model::scenarios(), như sau:

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        return [
            self::SCENARIO_LOGIN => ['username', 'password'],
            self::SCENARIO_REGISTER => ['username', 'email', 'password'],
        ];
    }
}

Lưu ý: Như phần trên và ví dụ vừa rồi, lớp Model được kế thừa từ lớp yii\db\ActiveRecord bởi vì lớp Active Record thường được sử dụng nhiều kịch bản.

Phương thức scenarios() trả về một mảng có chứa các khóa là tên các kịch bản và các giá trị tương ứng là các
danh sách thuộc tính được chọn. An active attribute can be massively assigned và là đối tượng sẽ được dùng để xác thực (validation). Chẳng hạn ở ví dụ trên, thuộc tính usernamepassword sẽ được chọn ở kịch bản login; còn ở kịch bản register, sẽ có thêm thuộc tính email ngoài 2 thuộc tính usernamepassword.

Việc triển khai phương thức scenarios() mặc định sẻ trả về các kịch bản tìm thấy trong phương thức yii\base\Model::rules(). Khi khi đè phương thức scenarios(), nếu bạn muốn khai báo các kịch bản mới, ngoài các kịch bản mặc định in addition to the default ones, bạn có thể viết mã như sau:

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        $scenarios = parent::scenarios();
        $scenarios[self::SCENARIO_LOGIN] = ['username', 'password'];
        $scenarios[self::SCENARIO_REGISTER] = ['username', 'email', 'password'];
        return $scenarios;
    }
}

Xây dựng các kịch bản được dùng vào việc xác thựcmassive attribute assignment. Tuy nhiên, bạn có thể dùng vào mục đích khác. Chẳng hạn, bạn có thể khai báo các nhãn thuộc tính khác nhau được dựa trên kịch bản hiện tại.

Các quy tắc xác nhận (Validation Rules)

Khi dữ liệu cho model được chuyển lên từ người dùng cuối, dữ liệu này cần được xác thực để chắc chắn rằng dữ liệu này là hợp lệ (được gọi là quy tắc xác nhận, có thể gọi business rules). Ví dụ, cho model ContactForm, bạn muốn tất cả các thuộc tính không được để trống và thuộc tính email phải là địa chỉ email hợp lệ. Nếu các giá trị cho các thuộc tính không được thỏa mãn với các quy tắc xác nhận, các thông báo lỗi sẽ được được hiển thị để giúp người dùng sửa lỗi.

Bạn có thể gọi phương thức yii\base\Model::validate() để xác thực các dữ liệu đã nhận. Phương thức sẽ dùng các quy tắc xác nhận được khai báo ở phương thức yii\base\Model::rules() để xác thực mọi thuộc tính liên quan. Nếu không có lỗi nào tìm thấy , sẽ trả về giá trị true. Nếu không thì, phương thức sẽ giữ các thông báo lỗi tại thuộc tính yii\base\Model::errors và trả kết quảfalse. Ví dụ,

$model = new \app\models\ContactForm;

// gán các thuộc tính của model từ dữ liệu người dùng
$model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
    // tất cả các dữ liệu nhập vào hợp lệ
} else {
    // xác nhận lỗi: biến $errors chứa mảng các nội dung thông báo lỗi
    $errors = $model->errors;
}

Các quy tắc xác nhận được gắn vào model, việc ghi đè phương thức yii\base\Model::rules() cùng với việc trả về có chứa các thuộc tính an toàn cần được xác thực. Ví dụ sau đây sẽ cho thấy các quy tắc xác nhận được khai báo cho model ContactForm:

public function rules()
{
    return [
        // the name, email, subject and body attributes are required
        [['name', 'email', 'subject', 'body'], 'required'],

        // the email attribute should be a valid email address
        ['email', 'email'],
    ];
}

Mỗi quy tắc được dùng để xác nhận một hoặc nhiều các thuộc tính, và một thuộc tính có thể được xác nhận một hoặc nhiều quy tắc. Vui lòng tham khảo mục Xác nhận đầu vào để biết thêm chi tiết về cách khai báo các quy tắc xác nhận.

Đôi khi, bạn muốn các quy tắc chỉ được áp dụng chỉ trong một số kịch bản. Để làm như vậy, bạn có thể thêm thông tin thuộc tính on ở mỗi quy tắc, giống như sau:

public function rules()
{
    return [
        // thuộc tính username, email và password cần được nhập ở kịch bản "register"
        [['username', 'email', 'password'], 'required', 'on' => self::SCENARIO_REGISTER],

        // username và password cần được nhập ở kịch bản "login"
        [['username', 'password'], 'required', 'on' => self::SCENARIO_LOGIN],
    ];
}

Nếu bạn không chỉ định thuộc tính on, quy tắc sẽ áp dụng trong tất cả các kịch bản. Một quy tắc được gọi một quy tắc hoạt động nếu nó được áp dụng với kịch bản hiện tại yii\base\Model::scenario.

Một thuộc tính được xác nhận nếu và chỉ nếu nó là thuộc tính được kích hoạt với khai báo tại phương thức scenarios() và được liên kết với một hoặc nhiều quy tắc được khai báo ở phương thức rules().

Gán nhanh (Massive Assignment)

Gán nhanh là cách tiện lợi cho việc nhập dữ liệu vào model từ người dùng với một dòng mã. Nó nhập vào các thuộc tính của model bằng việc gán dữ liệu nhập vào qua thuộc tính yii\base\Model::$attributes . 2 đoạn mã sau hoạt động giống nhau , cả 2 đều lấy dữ liệu trong form gửi lên từ người dùng vào các thuộc tính của model ContactForm. Nhanh gọn, cách trên, sẽ dùng gán nhanh, mã của bạn trông sạch và ít lỗi hơn cách sau đó:

$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');
$model = new \app\models\ContactForm;
$data = \Yii::$app->request->post('ContactForm', []);
$model->name = isset($data['name']) ? $data['name'] : null;
$model->email = isset($data['email']) ? $data['email'] : null;
$model->subject = isset($data['subject']) ? $data['subject'] : null;
$model->body = isset($data['body']) ? $data['body'] : null;

Thuộc tính an toàn (Safe Attributes)

Gán nhanh chỉ gán dữ liệu cho những thuộc tính gọi là thuộc tính an toàn (safe attributes) đó là các thuộc tính được liệt kê trong phương thức yii\base\Model::scenarios() cho thuộc tính yii\base\Model::scenario của model. Chẳng hạn, nếu model User có các kịch bản mô tả như sau, tiếp đến kịch bản login đang được chọn, thì chỉ thuộc tính usernamepassword có thể được gán nhanh. Bất kỳ các thuộc tính khác sẽ được giữ nguyên.

public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password'],
        self::SCENARIO_REGISTER => ['username', 'email', 'password'],
    ];
}

Thông tin: Lý do việc gán nhanh chỉ gán dữ liệu cho các thuộc tính an toàn là bởi vì bạn muốn kiểm soát những thuộc tính có thể được thay đổi bởi người dùng. Chẳng hạn, nếu model User có thuộc tính permission nhằm xác định các quyền hạn của người dùng, bạn chỉ muốn thuộc tính này chỉ được thay đổi bởi quản trị viên thông qua giao diện phụ trợ.

Bởi vì mặc định phương thức yii\base\Model::scenarios() sẽ trả về tất cả các kịch bản và thuộc tính nằm trong phương thức yii\base\Model::rules(), nếu bạn không ghi đè phương thức này, có nghĩa là một thuộc tính là an toàn miễn là có khai báo ở một trong các quy tắc xác nhận.

Vì lý do này, bí danh safe được đưa ra bạn có thể khai báo các thuộc tính an toàn mà không thực sự xác nhận nó. Chẳng hạn, các quy tắc sau đây khai báo thuộc tính titledescription là thuộc tính an toàn.

public function rules()
{
    return [
        [['title', 'description'], 'safe'],
    ];
}

Thuộc tính không an toàn (Unsafe Attributes)

Như mô tả trên, khai báo phương thức yii\base\Model::scenarios() có 2 mục đích: liệt kê thuộc tính cần được xác nhận , và xác định các thuộc tính là an toàn. Trong một số trường hợp khác, bạn muốn xác nhận thuộc tính nhưng không muốn đánh dấu là an toàn. bạn có thể thực hiện bằng việc đặt dấu chấm than ! vào tên thuộc tính khi khai báo tại phương thức scenarios(), giốn như thuộc tính secret như sau:

public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password', '!secret'],
    ];
}

Khi model đang ở kịch bản login, cả 3 thuộc tính sẽ được xác nhận. Tuy nhiên, chỉ có thuộc tính usernamepassword được gán nhanh. Để gán giá trị cho thuộc tính secret, bạn cần được gán trực tiếp như sau,

$model->secret = $secret;

Điều tương tự có thể được thực hiện trong phương thức rules():

public function rules()
{
    return [
        [['username', 'password', '!secret'], 'required', 'on' => 'login']
    ];
}

Trong trường hợp này các thuộc tính username, passwordsecret là yêu cầu nhập, nhưng thuộc tính secret phải cần được gán trực tiếp.

Xuất dữ liệu (Data Exporting)

Các model thường được cần trích xuất ra các định dạng khác nhau. Chẳng hạn, bạn cần chuyển dữ liệu sang của models sang định dạng JSON hoặc Excel. Quá trình xuất có thể được chia nhỏ thành hai bước độc lập:

  • models cần được chuyển sang định dạng mảng;
  • các mảng cần được chuyển đổi thành các định dạng cần chuyển.

Bạn chỉ cần tập trung vào bước đầu tiên, bởi vì bước thứ 2 có thể được thực hiện bởi các trình định dạng dữ liệu , chẳng hạn như yii\web\JsonResponseFormatter.

Các đơn giản nhất để chuyển đổi model sang dạng mảng là sử dụng thuộc tính yii\base\Model::$attributes. For example,

$post = \app\models\Post::findOne(100);
$array = $post->attributes;

Bởi mặc định, thuộc tính yii\base\Model::$attributes sẽ trả về các giá trị của tất cả các thuộc tính được khai báo trong phương thức yii\base\Model::attributes().

Còn một cách linh hoạt và tiện lợi hơn trong việc chuyển đổi model sang định dạng mảng là sử dụng phương thức yii\base\Model::toArray() . Cách chuyển đổi cũng tương tự như trong cách của thuộc tính yii\base\Model::$attributes. Tuy nhiên, nó cho phép bạn chọn các dữ liệu , được gọi là fields, được đặt trong mảng kết quả và chúng được định dạng thế nào. Trong thực tế, đó là cách trích xuất mặc định của các model ở việc phát triển các dịch vụ RESTful Web, như được mô tả trong mục Response Formatting.

Các trường (Fields)

Một trường đơn giản là tên của thành phần thu được nằm trong mảng khi gọi phương thức yii\base\Model::toArray() của model.

Mặc định, tên trường sẽ tương đương với tên thuộc tính. Tuy nhiên, bạn có thể thay đổi bằng việc ghi đè qua phương thức fields() và/hoặc phương thức extraFields(). Cả 2 phương thức trả về danh sách các khai báo trường. Các trường được định nghĩa bởi phương thức fields() là các trường mặc định, nghĩa là phương thức toArray() sẽ trả về những trường mặc định. Phương thức extraFields() sẽ khai báo thêm các trường bổ sung có thể được trả về bởi phương thức toArray() miễn là bạn chỉ định chugns qua tham số $expand. Chẳng hạn, đoạn mã sau sẽ trả về các trường được định nghĩa trong phương thức fields() và 2 trường prettyNamefullAddress nếu chúng được định nghĩa trong phương thức extraFields().

$array = $model->toArray([], ['prettyName', 'fullAddress']);

Bạn có thể ghi đè phương thức fields() để thêm, xóa, cập nhật hoặc định nghĩa lại các trường. Phương thức fields() sẽ trả về dữ liệu dạng mảng. Mảng này có các khóa là tên các trường, và các giá trị của mảng tương ứng với các trường đã định nghĩa giá trị có thể là tên các thuộc tính/biến hoặc một hàm trả về các giá trị trường tương ứng . Trong trường hợp đặc biệt khi tên trường giống với tên thuộc tính xác định của nó, bạn có thể bỏ qua khóa mảng. Ví dụ,

// liệt kê rõ ràng các trường, sử dụng tốt nhất khi bạn nắm được các thay đổi
// trong bản CSDL hoặc các thuộc tính của model, không gây ra sự thay đổi của trường (để giữ tương thích với API).
public function fields()
{
    return [
        // tên trường giống với tên thuộc tính
        'id',

        // tên trường là "email", tương ứng với tên thuộc tính là "email_address"
        'email' => 'email_address',

        // tên trường là "name", giá trị được định nghĩa bởi hàm
        'name' => function () {
            return $this->first_name . ' ' . $this->last_name;
        },
    ];
}

// lọc ra một số trường, nên sử dụng khi bạn muốn kế thừa các trường
// thêm vào blacklist một số trường không cần thiết.
public function fields()
{
    $fields = parent::fields();

    // remove fields that contain sensitive information
    unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);

    return $fields;
}

Cảnh báo: Bởi vì theo mặc định tất cả các thuộc tính của model sẽ liệt kê trong mảng trích xuất, bạn nên kiểm tra dữ liệu của bạn để chắc chắn rằng chúng không chứa các thông tin không cần thiết. Nếu có thông tin như vậy, bạn nên ghi đè phương thức fields() để lọc chúng ra. Tại ví dụ trên, chúng ta chọn các trường để lọc ra là auth_key, password_hashpassword_reset_token.

Bài thực hành

Các model là phần trung tâm đại diện cho tầng dữ liệu, chứa các quy tắc và logic. Model thường được tái sử dụng tại một số nơi khác nhau. Với một ứng dụng được thiết kế tốt, thông thường các model được chú trọng hơn controllers.

Tổng hợp mục, models

  • có thể chứa các thuộc tính đại diện cho tầng dữ liệu (business data);
  • có thể chứa các quy tắc xác nhận để đảm bảo tính hợp lệ và tính toàn vẹn của dữ liệu;;
  • có thể chứa các phương thức tại tầng logic (business logic);
  • không nên trực tiếp xử lý các yêu cầu, session, hoặc bất cứ dữ liệu môi trường. Những dữ liệu này nên được tiến hành xử lý bởi controllers vào model;
  • tránh việc nhúng mã HTML hoặc các dữ liệu hiển thị - mã này nên được đặt tại views;
  • tránh có quá nhiều kịch bản scenarios trong một model.

Bạn cần có sự xem xét các đề nghị trên mỗi khi bạn triển khai hệ thống lớn và phức tạp. Trong các hệ thống này, cácmodel cần được chú trọng bởi vì chúng được sử dụng ở nhiều nơi và có thể chứa nhiều các quy tắc và các xử lý nghiệp vụ. Điều này có sự ảnh hưởng tại tiến trình bảo trì mỗi thay đổi mã của bạn có thể ảnh hưởng tới nhiều vị trí khác nhau. Để mã code của bạn dễ được bảo trì hơn, bạn có thể được thực hiện các chiến lược sau:

  • Định nghĩa tập các lớp model cơ sở (base model) lớp này được chia sẻ qua các ứng dụng hoặc modules khác nhau. Các model này có thể chứa tập các quy tắc và logic có thể được dùng rộng rãi ở các lớp cần được sử dụng.
  • Tại mỗi ứng dụng hoặc module có dùng model, ta định nghĩa lớp khung model bằng việc kế thừa từ lớp model cơ sở. Lớp khung model này có thể chứa các quy tắc logic được mô tả cụ thể chi ứng dụng hoặc module này.

Ví dụ, với Mẫu dự án Advanced, bạn có thể định nghĩa lớp cơ sở model là common\models\Post. Tiếp đến tại ứng dụng front end, bạn định nghĩa lớp khung là frontend\models\Post lớp này kế thừa từ lớp common\models\Post. Và tương tự cho ứng dụng back end, bạn định nghĩa model backend\models\Post. Với cách giải quyết này, bạn sẽ chắc chắn rằng mã của bạn tại model frontend\models\Post chỉ dùng cho ứng dụng front end, và nếu bạn thực hiện với bất kỳ thay đổi nào, bạn không cần lo lắng về việc thay đổi này có ảnh hưởng tới ứng dụng back end.