「最速」PHPフレームワークPhalconのモデルについて、基本事項をまとめます(公式ドキュメントの翻訳+αです)。記事執筆時のPhalconのバージョンは1.3.1です。

バリデーションメッセージ

Phalcon\Mvc\Modelには、INSERT/UPDATE時のバリデーションメッセージの保持・出力を柔軟に行えるサブシステムがあります。

それぞれのメッセージは、Phalcon\Mvc\Model\Messageクラスのインスタンスになります。生成されたメッセージの集合は、getMessages()メソッドで取得することができます。それぞれのメッセージは、フィールド名やメッセージの種類のような、幅広い情報を提供します。

<?php

if ($robot->save() === false) {
    foreach ($robot->getMessages() as $message) {
        echo "Message: ", $message->getMessage();
        echo "Field: ", $message->getField();
        echo "Type: ", $message->getType();
    }
}

Phalcon\Mvc\Modelが生成可能なメッセージの一覧は以下です。

種類 説明
PresenceOf フィールドにNOT NULL制約が付与されているときに、null値をINSERT/UPDATEしようとした際に発生
ConstrainvViolation 仮想外部キー制約を設定しているフィールドに、参照しているモデルに存在しない値をINSERT/UPDATEしようとした際に発生
InvalidValue 無効な値によってバリデーションが失敗した際に発生
InvalidCreateAttempt 既に存在するレコードを新規作成しようとした際に発生
InvalidUpdateAttempt 更新しようとしたレコードがまだ存在しない際に発生

getMessages()メソッドをオーバーライドすることで、モデルが自動で生成するメッセージを置き換えることができます。

<?php

class Robots extends Phalcon\Mvc\Model
{
    public function getMessages()
    {
        $messages = array();
        foreach (parent::getMessages() as $message) {
            switch ($message->getType()) {
                case 'InvalidCreateAttempt':
                    $messages[] = 'The record cannot be created because it already exists';
                    break;
                case 'InvalidUpdateAttempt':
                    $messages[] = 'The record cannot be updated because it already exists';
                    break;
                case 'PresenceOf':
                    $messages[] = 'The field ' . $message->getField() . ' is mandatory';
                    break;
            }
        }
        return $messages;
    }
}

イベントとイベントマネージャ

モデルには、INSERT/UPDATE/DELETE時に実行されるイベントを実装することができるようになっています。これらは、モデルのビジネスルールを定義する助けになります。Phalcon\Mvc\Modelがサポートしているイベントの一覧は以下です(実行順)。

処理 イベント名 処理の中断可否 説明
INSERT/UPDATE beforeValidation フィールドのNOT NULL・空文字列・外部キー制約のバリデーション前に実行される
INSERT beforeValidationOnCreate INSERTの場合で、フィールドのNOT NULL・空文字列・外部キー制約のバリデーション前に実行される
UPDATE beforeValidationOnUpdate UPDATEの場合で、フィールドのNOT NULL・空文字列・外部キー制約のバリデーション前に実行される
INSERT/UPDATE onValidationFails ○(既に停止済み) バリデーションが失敗した際に実行される
INSERT afterValidationOnCreate INSERTの場合で、フィールドのバリデーション後に実行される
UPDATE afterValidationOnUpdate UPDATEの場合で、フィールドのバリデーション後に実行される
INSERT/UPDATE afterValidation フィールドのバリデーション後に実行される
INSERT/UPDATE beforeSave INSERT又はUPDATEの実行前に実行される
UPDATE beforeUpdate UPDATEの実行前に実行される
INSERT beforeCreate INSERTの実行前に実行される
UPDATE afterUpdate × UPDATEの実行後に実行される
INSERT afterCreate × INSERTの実行後に実行される
INSERT/UPDATE afterSave × INSERT/UPDATEの実行後に実行される

モデルのクラスにイベントを実装する

モデルをイベントに対応させるための最も簡単な方法は、イベント名と同じ名前のメソッドをモデルのクラスに実装することです。

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public function beforeValidationOnCreate()
    {
        echo "このメソッドはRobotの作成前に実行されます";
    }

}

イベントは、処理の実行前に値を代入するのに便利です。

<?php

class Products extends \Phalcon\Mvc\Model
{
    public function beforeCreate()
    {
        // 登録日時をセット
        $this->created_at = date('Y-m-d H:i:s');
    }

    public function beforeUpdate()
    {
        // 更新日時をセット
        $this->modified_in = date('Y-m-d H:i:s');
    }

}

イベントマネージャのカスタマイズ

モデルのイベントコンポーネントは、Phalcon\Events\Managerと統合されています。そのため、イベントが実行される際のリスナーを作ることもできます。

<?php

use Phalcon\Mvc\Model,
    Phalcon\Events\Manager as EventsManager;

class Robots extends Model
{

    public function initialize()
    {

        $eventsManager = new EventsManager();

        // "model"イベントに無名関数を登録する
        $eventsManager->attach('model', function($event, $robot) {
            if ($event->getType() === 'beforeSave') {
                if ($robot->name === 'Scooby Doo') {
                    echo "Scooby Dooはロボットではありません!";
                    return false;
                }
            }
            return true;
        });

        // イベントマネージャをイベントに登録
        $this->setEventsManager($eventsManager);
    }

}

上記サンプルコードでは、イベントマネージャはオブジェクトとリスナーの橋渡しを行います。イベントは、Robotsモデルのsave時に発火します。

<?php

$robot = new Robots();
$robot->name = 'Scooby Doo';
$robot->year = 1969;
$robot->save();

全てのオブジェクトに共通のEventsManagerを実装したい場合、EventsManagerをModelsManagerに登録します。

<?php

// modelsManagerサービスをDIコンテナに登録
$di->setShared('modelsManager', function() {

    $eventsManager = new \Phalcon\Events\Manager();

    // "model"イベントのリスナーとして無名関数を登録する
    $eventsManager->attach('model', function($event, $model){

        // Robotsモデルに寄って生成されるイベントをキャッチする
        if (get_class($model) == 'Robots') {

            if ($event->getType() == 'beforeSave') {
                if ($model->name == 'Scooby Doo') {
                    echo "Scooby Dooはロボットではありません!";
                    return false;
                }
            }

        }
        return true;
    });

    // EventsManagerを登録する
    $modelsManager = new ModelsManager();
    $modelsManager->setEventsManager($eventsManager);
    return $modelsManager;
});

リスナーがfalseを返すと、その処理の実行は中断されます。

ビジネスルールを実装する

INSERT/UPDATE/DELETEの実行時に、モデルはイベント一覧に挙げられた名前のメソッドが無いか確認します。

以下のコード例では、INSERT/UPDATE時のイベントを実装し、yearの値が0未満でないかバリデーションしています。

<?php

class Robots extends \Phalcon\Mvc\Model
{

    public function beforeSave()
    {
        if ($this->year < 0) {
            echo "year は0未満にできません!";
            return false;
        }
    }

}

イベントがfalseを返すと、処理は中断されます。イベントが何も返さない場合、Phalcon\Mvc\Modelはtrueが返されたとみなします。

データの完全性バリデーション

Phalcon\Mvc\Modelは、データをバリデーションし、ビジネスルールを実装するためのイベントを提供しています。「validation」イベントによって、組み込みのバリデーターを呼び出すことができます。Phalconは、いくつかの組み込みバリデーターを提供しています。

<?php

use Phalcon\Mvc\Model\Validator\InclusionIn,
    Phalcon\Mvc\Model\Validator\Uniqueness;

class Robots extends \Phalcon\Mvc\Model
{

    public function validation()
    {

        $this->validate(new InclusionIn(
            array(
                "field"  => "type",
                "domain" => array("Mechanical", "Virtual")
            )
        ));

        $this->validate(new Uniqueness(
            array(
                "field"   => "name",
                "message" => "ロボットの名前が重複してはいけません"
            )
        ));

        return $this->validationHasFailed() != true;
    }

}

上記コード例では、組み込みの「InclusionIn」バリデーターを使用しています。このバリデーターは、「type」フィールドの値がdomainのリストに含まれているかチェックしています。値が含まれていなかった場合は、バリデーションに失敗し、バリデーターはfalseを返します。

組み込みのバリデーターの一覧は以下です。

名前 説明 実装例
PresenceOf フィールドの値がnullではなく、空文字列でもないことをバリデーションする。NOT NULL制約が設定されているフィールドに対しては、このバリデーターが自動的に追加される。
Email フィールドの値が有効なEメールアドレスであるかバリデーションする
ExclusionIn フィールドの値が禁止リストに含まれないことをバリデーションする
InclusionIn フィールドの値が許可リストに含まれることをバリデーションする
Numericality フィールドの値が数値形式かバリデーションする
Regex フィールドの値が正規表現にマッチするかバリデーションする
Uniqueness フィールドの値が既存のレコードと重複しないかバリデーションする
StringLength 文字列の長さをバリデーションする
Url フィールドの値が有効なURLの形式化バリデーションする

組み込みのバリデーターに加えて、独自のバリデーターを作成することもできます。

<?php

use Phalcon\Mvc\Model\Validator,
    Phalcon\Mvc\Model\ValidatorInterface;

class MaxMinValidator extends Validator implements ValidatorInterface
{

    public function validate($model)
    {
        $field = $this->getOption('field');

        $min = $this->getOption('min');
        $max = $this->getOption('max');

        $value = $model->$field;

        if ($min <= $value && $value <= $max) {
            $this->appendMessage(
                "フィールドの値が有効範囲外です",
                $field,
                "MaxMinValidator"
            );
            return false;
        }
        return true;
    }

}

バリデーターをモデルに追加するには、以下のようにします。

<?php

class Customers extends \Phalcon\Mvc\Model
{

    public function validation()
    {
        $this->validate(new MaxMinValidator(
            array(
                "field"  => "price",
                "min" => 10,
                "max" => 100
            )
        ));
        if ($this->validationHasFailed() == true) {
            return false;
        }
    }

}

バリデーターを作成することで、バリデーションのロジックを複数のモデルで使い回すことができます。1箇所でしか使わないバリデーションであれば、以下のようにシンプルに実装することもできます。

<?php

use Phalcon\Mvc\Model,
    Phalcon\Mvc\Model\Message;

class Robots extends Model
{

    public function validation()
    {
        if ($this->type === "Old") {
            $message = new Message(
                "Sorry, old robots are not allowed anymore",
                "type",
                "MyType"
            );
            $this->appendMessage($message);
            return false;
        }
        return true;
    }

}

SQLインジェクションを避ける

モデルのプロパティに代入された値は全て、そのデータ型に応じたエスケープが施されます。開発者が、DBに入れる値のエスケープを実装する必要はありません。Phalconは内部でPDOのバインド機構を使用して、自動エスケープを行っています。

mysql> desc products;
+------------------+------------------+------+-----+---------+----------------+
| Field            | Type             | Null | Key | Default | Extra          |
+------------------+------------------+------+-----+---------+----------------+
| id               | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| product_types_id | int(10) unsigned | NO   | MUL | NULL    |                |
| name             | varchar(70)      | NO   |     | NULL    |                |
| price            | decimal(16,2)    | NO   |     | NULL    |                |
| active           | char(1)          | YES  |     | NULL    |                |
+------------------+------------------+------+-----+---------+----------------+

上記テーブルへのINSERTをする際、PDOを使用すると、以下のようなコードになります。

<?php

$productTypesId = 1;
$name = 'Artichoke';
$price = 10.5;
$active = 'Y';

$sql = 'INSERT INTO products VALUES (null, :productTypesId, :name, :price, :active)';
$sth = $dbh->prepare($sql);

$sth->bindParam(':productTypesId', $productTypesId, PDO::PARAM_INT);
$sth->bindParam(':name', $name, PDO::PARAM_STR, 70);
$sth->bindParam(':price', doubleval($price));
$sth->bindParam(':active', $active, PDO::PARAM_STR, 1);

$sth->execute();

Phalconのモデルを使用すると、以下のように書けます(エスケープは全て自動で行われます)。

<?php

$product = new Products();
$product->product_types_id = 1;
$product->name = 'Artichoke';
$product->price = 10.5;
$product->active = 'Y';
$product->create();

カラムをスキップする

何らかのトリガーや、デフォルト値の代入を行いたいため、バリデーションを行いたくない場合があります。モデルによるバリデーションを行わないようにするには、以下のように書きます。

<?php

class Robots extends \Phalcon\Mvc\Model
{

    public function initialize()
    {
        // INSERT/UPDATE時にフィールドのバリデーションをしない
        $this->skipAttributes(array('year', 'price'));

        // INSERT時にバリデーションしない
        $this->skipAttributesOnCreate(array('created_at'));

        // UPDATE時にバリデーションしない
        $this->skipAttributesOnUpdate(array('modified_in'));
    }

}

これによって、アプリケーション全体で、INSERT/UPDATE時のバリデーションが実行されなくなります。デフォルト値の代入は以下のように行えます。

<?php

$robot = new Robots();
$robot->name = 'Bender';
$robot->year = 1999;
$robot->created_at = new \Phalcon\Db\RawValue('default');
$robot->create();

イベントのコールバックによって、特定条件の場合にデフォルト値を設定することもできます。

<?php

use Phalcon\Mvc\Model,
    Phalcon\Db\RawValue;

class Robots extends Model
{
    public function beforeCreate()
    {
        if ($this->price > 10000) {
            $this->type = new RawValue('default');
        }
    }
}

注意:Phalcon\Db\RawValueは、ユーザー入力のような外部データや変数の値の代入に使用してはいけません。RawValueにはバインド機構が使用されないため、SQLインジェクション脆弱性を引き起こす危険性があります。

ダイナミックアップデート

SQLのUPDATE文は、デフォルトでは全てのカラムをUPDATEするように作成されます。ダイナミックアップデートを有効化すると、変更されたフィールドだけが更新されるUPDATE文が生成されます。

これによって、アプリケーションサーバ・DBサーバ間のトラフィックが減少し、パフォーマンスが改善されることがあります。特に、BLOB/TEXT型のような大きなデータを取り扱うカラムがある場合、ダイナミックアップデートは大きな助けになります。

<?php

class Robots extends Phalcon\Mvc\Model
{
    public function initialize()
    {
        $this->useDynamicUpdate(true);
    }
}

レコードの削除

Phalcon\Mvc\Model::delete()メソッドに寄って、レコードを削除できます。

<?php

$robot = Robots::findFirst(11);
if ($robot != false) {
    if ($robot->delete() == false) {
        echo "今はロボットを削除できないようです:\n";
        foreach ($robot->getMessages() as $message) {
            echo $message, "\n";
        }
    } else {
        echo "ロボットの削除に成功しました!";
    }
}

結果セットをforeachでまわすことで、複数のレコードを削除できます。

<?php

foreach (Robots::find("type='mechanical'") as $robot) {
    if ($robot->delete() == false) {
        echo "今はロボットを削除できないようです:\n";
        foreach ($robot->getMessages() as $message) {
            echo $message, "\n";
        }
    } else {
        echo "ロボットの削除に成功しました!";
    }
}

以下のイベントを使用することで、削除処理の際に実行されるビジネスルールを定義することができます。

処理 名前 処理の中断可否 説明
DELETE beforeDelete 削除処理の実行前に実行
DELETE afterDelete × 削除処理の実行後に実行

上記イベントを使用すると、以下のようにビジネス・ルールを定義できます。

<?php

class Robots extends Phalcon\Mvc\Model
{

    public function beforeDelete()
    {
        if ($this->status == 'A') {
            echo "The robot is active, it can't be deleted";
            return false;
        }
        return true;
    }

}

バリデーション失敗時のイベント

バリデーション失敗時のイベントもあります。

処理 名前 説明
INSERT/UPDATE notSave INSERT/UPDATEが何からの理由で失敗した際に発生
INSERT/UPDATE/DELETE onValidationFails データ処理が失敗した際に発生

今回はここまで

ここまで、Phalconのモデルを利用したバリデーションの実装方法をみてきました。次回は、Behaviorsから先をみていきます。