Phalcon Model まとめ(5) ビヘイビア

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

ビヘイビア

ビヘイビアとは、複数のモデルに共有される振る舞いのことです。PhalconのORマッパーは、モデルでビヘイビアを実装するためのAPIを提供しています。また、イベントとコールバックを使用して、より自由度の高いビヘイビアを実装することもできます。

ビヘイビアは、モデルの初期化処理に加えなければなりません。モデルは複数のビヘイビアをもつことができます。

<?php

use Phalcon\Mvc\Model\Behavior\Timestampable;

class Users extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public $created_at;

    public function initialize()
    {
        $this->addBehavior(new Timestampable(
            array(
                'beforeCreate' => array(
                    'field' => 'created_at',
                    'format' => 'Y-m-d'
                )
            )
        ));
    }

}

フレームワークは、以下の組み込みビヘイビアを提供しています。

名前 説明
Timestampable レコードが作成・更新される際に、自動的にモデルのフィールドを更新する
SoftDelete 永続的な削除(物理削除)の代わりに、削除フラグを付与する(論理削除)

Timestampable

このビヘイビアは、配列をオプションとして受け取ります。最初のキーが、カラムへの代入が行われるイベント名になります。

<?php

public function initialize()
{
    $this->addBehavior(new Timestampable(
        array(
            'beforeCreate' => array(
                'field' => 'created_at',
                'format' => 'Y-m-d'
            )
        )
    ));
}

それぞれのイベントには独自のオプションがあり、fieldは更新されるカラム名になります。formatが文字列なら、PHPのdate()関数の引数として渡されます。formatには無名関数を使うこともでき、お好みの形式でタイムスタンプを作ることができます。

<?php

public function initialize()
{
    $this->addBehavior(new Timestampable(
        array(
            'beforeCreate' => array(
                'field' => 'created_at',
                'format' => function() {
                    $datetime = new Datetime(new DateTimeZone('Europe/Stockholm'));
                    return $datetime->format('Y-m-d H:i:sP');
                }
            )
        )
    ));
}

formatがオプションの配列に含まれない場合は、タイムスタンプはPHPのtime()関数を使用し、タイムスタンプの数値が代入されます。

SoftDelete

このビヘイビアは、以下のように使います。

<?php

use Phalcon\Mvc\Model\Behavior\SoftDelete;

class Users extends \Phalcon\Mvc\Model
{

    const DELETED = 'D';

    const NOT_DELETED = 'N';

    public $id;

    public $name;

    public $status;

    public function initialize()
    {
        $this->addBehavior(new SoftDelete(
            array(
                'field' => 'status',
                'value' => Users::DELETED
            )
        ));
    }

}

このビヘイビアは、2つのオプションを受け取ります。fieldは更新されるフィールド名を、valueは削除フラグの値を指定します。以下のようなデータをもつusersテーブルについて考えてみます。

mysql> select * from users;
+----+---------+--------+
| id | name    | status |
+----+---------+--------+
|  1 | Lana    | N      |
|  2 | Brandon | N      |
+----+---------+--------+
2 rows in set (0.00 sec)

もし、いずれかのレコードを削除したら、レコードが削除される代わりに、statusが更新されます。

<?php

Users::findFirst(2)->delete();

上記コードが実行されると、テーブルの中のデータは以下のようになります。

mysql> select * from users;
+----+---------+--------+
| id | name    | status |
+----+---------+--------+
|  1 | Lana    | N      |
|  2 | Brandon | D      |
+----+---------+--------+
2 rows in set (0.01 sec)

削除済みか否かのフラグをクエリで明示的に指定してやる必要がある点に注意してください。テーブルが上の状態の時、以下のコードを実行すると、削除されたはずのBrandonも取得されてしまいます。

$users = Users::find();

論理削除済みのレコードを除外するには、以下のようにします。

$users = Users::find('deleted = ' . Users::NOT_DELETED);

独自のビヘイビアを作る

ORマッパーは、独自のビヘイビアのためのAPIを提供しています。ビヘイビアは、Phalcon\Mvc\Model\BehaviorInterfaceを実装したクラスでなければなりません。また、Phacon\Mvc\Model\Behaviorが、ビヘイビアの実装に必要なほとんどのメソッドを提供しています。

以下のビヘイビアは、1つの実装例です。Blamableビヘイビアは、ユーザーがモデルに行った操作を特定します。

<?php

use Phalcon\Mvc\Model\Behavior;
use Phalcon\Mvc\Model\BehaviorInterface;

class Blameable extends Behavior implements BehaviorInterface
{

    public function notify($eventType, $model)
    {
        switch ($eventType) {

            case 'afterCreate':
            case 'afterDelete':
            case 'afterUpdate':

                // セッションからユーザー名取得
                $userName = $this->getDI()->get('session')->get('userName');

                // ユーザー名・イベント種別・主キーをログに記録
                file_put_contents(
                    'logs/blamable-log.txt',
                    $userName . ' ' . $eventType . ' ' . $model->id
                );

                break;

            default:
                /* 他のイベントは無視する */
        }
    }

}

次に、このビヘイビアをモデルに追加してみます。

<?php

class Profiles extends \Phalcon\Mvc\Model
{

    public function initialize()
    {
        $this->addBehavior(new Blamable());
    }

}

ビヘイビアは、不明なメソッド呼び出しに対して割り込みができます。

<?php

use Phalcon\Mvc\Model\Behavior,
    Phalcon\Mvc\Model\BehaviorInterface;

class Sluggable extends Behavior implements BehaviorInterface
{

    public function missingMethod($model, $method, $arguments=array())
    {
        // 「getSlug」メソッドが呼ばれたら、titleを変換する
        if ($method == 'getSlug') {
            return Phalcon\Tag::friendlyTitle($model->title);
        }
    }

}

Sluggableを実装しているモデルにgetSlug()メソッドの呼び出しを行うと、SEO対策のされたtitleが返ります。

<?php

$title = $post->getSlug();

トレイトをビヘイビアとして使う

PHP5.4以降では、トレイトを使うことでクラスのコードの再利用ができます。この機能を使って、独自のビヘイビアを実装することもできます。以下のトレイトは、TimeStampableビヘイビアのシンプルな実装例です。

<?php

trait MyTimestampable
{

    public function beforeCreate()
    {
        $this->created_at = date('r');
    }

    public function beforeUpdate()
    {
        $this->updated_at = date('r');
    }

}

モデルでは、以下のようにして利用します。

<?php

class Products extends \Phalcon\Mvc\Model
{
    use MyTimestampable;
}

今回はここまで

今回は、Phalconのモデルで利用可能なビヘイビアについて紹介しました。次回は、Transactionsから先をみていきます。

Phalconモデルまとめ(4) バリデーションとイベント

「最速」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から先をみていきます。

Phalconモデルまとめ(3) 計量・ハイドレーションモード・新規作成と更新

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

モデルの計量

計量(集約)はデータベースの操作を助ける機能で、COUNT/SUM/MAX/MIN/AVGなどがあります。Phalcon\Mvc\Modelでもこれらの機能を利用できます。

COUNTの例:

<?php

// 従業員の数は?
$rowcount = Employees::count();

// 従業員が割り当てられているエリアの数は?
$rowcount = Employees::count(array("distinct" => "area"));

// Testing部門には何人の従業員がいる?
$rowcount = Employees::count("area = 'Testing'");

// 従業員を、部門ごとにグループ分けして数える
$group = Employees::count(array("group" => "area"));
foreach ($group as $row) {
   echo "There are ", $row->rowcount, " in ", $row->area;
}

// 従業員を、部門ごとにグループ分けして数え、数の少ない順に並べる
$group = Employees::count(array(
    "group" => "area",
    "order" => "rowcount"
));

// バインド機構を使ってSQLインジェクションを防ぐ
$group = Employees::count(array(
    "type > ?0",
    "bind" => array($type)
));

SUMの例:

<?php

// 全ての従業員の給料の合計は?
$total = Employees::sum(array("column" => "salary"));

// セールス部門の従業員の給料の合計は?
$total = Employees::sum(array(
    "column"     => "salary",
    "conditions" => "area = 'Sales'"
));

// 部門ごとの従業員の給料の合計は?
$group = Employees::sum(array(
    "column" => "salary",
    "group"  => "area"
));
foreach ($group as $row) {
   echo "The sum of salaries of the ", $row->area, " is ", $row->sumatory;
}

// 部門ごとの従業員の給料の合計を算出し、合計額が多い順に並べる
$group = Employees::sum(array(
    "column" => "salary",
    "group"  => "area",
    "order"  => "sumatory DESC"
));

// バインド機構を使ってSQLインジェクションを防ぐ
$group = Employees::sum(array(
    "conditions" => "area > ?0",
    "bind" => array($area)
));

AVERAGEの例:

<?php

// 全従業員の平均給料は?
$average = Employees::average(array("column" => "salary"));

// セールス部門の従業員の平均給料は?
$average = Employees::average(array(
    "column" => "salary",
    "conditions" => "area = 'Sales'"
));

// バインド機構を使ってSQLインジェクションを防ぐ
$average = Employees::average(array(
    "column" => "age",
    "conditions" => "area > ?0",
    "bind" => array($area)
));

MAX/MINの例:

<?php

// 全従業員のうち、最高齢は?
$age = Employees::maximum(array("column" => "age"));

// セールス部門の最高齢は?
$age = Employees::maximum(array(
    "column" => "age",
    "conditions" => "area = 'Sales'"
));

// 全従業員で最も少ない給料は?
$salary = Employees::minimum(array("column" => "salary"));

ハイドレーションモード

Phalconのモデルの結果セットは、全てがオブジェクトです。DBの各行が単一のオブジェクトになっています。これらのオブジェクトに変更を加え、保存して永続化することができます。

<?php

// オブジェクトの結果セットを操作
foreach (Robots::find() as $robot) {
    $robot->year = 2000;
    $robot->save();
}

結果セットの形式を変更するモードのことを、「ハイドレーションモード」といいます。

<?php

use Phalcon\Mvc\Model\Resultset;

$robots = Robots::find();

// 全てのロボットを配列として返す
$robots->setHydrateMode(Resultset::HYDRATE_ARRAYS);

foreach ($robots as $robot) {
    echo $robot['year'], PHP_EOL;
}

// 全てのロボットをstdClassのインスタンスとして返す
$robots->setHydrateMode(Resultset::HYDRATE_OBJECTS);

foreach ($robots as $robot) {
    echo $robot->year, PHP_EOL;
}

// 全てのロボットをRobotsモデルのインスタンスとして返す
$robots->setHydrateMode(Resultset::HYDRATE_RECORDS);

foreach ($robots as $robot) {
    echo $robot->year, PHP_EOL;
}

ハイドレーションモードは、find()のパラメーターとして渡すこともできます。

<?php

use Phalcon\Mvc\Model\Resultset;

$robots = Robots::find(array(
    'hydration' => Resultset::HYDRATE_ARRAYS
));

foreach ($robots as $robot) {
    echo $robot['year'], PHP_EOL;
}

レコードの作成・更新

Phalcon\Mvc\Model::save() メソッドによって、レコードの作成・更新ができます。save()メソッドは内部でPhalcon\Mvc\Modelのcreate()又はupdate()を呼びます。いずれのメソッドを呼ぶかは、エンティティの主キーが定義済みか否かによって決まります。

また、このメソッドは、同時にバリデーションも実行します。

<?php

$robot       = new Robots();
$robot->type = "mechanical";
$robot->name = "Astro Boy";
$robot->year = 1952;
if ($robot->save() == false) {
    echo "今はロボットを保存できないようです: \n";
    foreach ($robot->getMessages() as $message) {
        echo $message, "\n";
    }
} else {
    echo "おめでとうございます、新しいロボットが作成されました!";
}

全てのカラムに手動で代入する代わりに、save()に配列を渡すことができます。Phalcon\Mvc\Model は各カラムに定義済みのsetterが無いか確認します。

<?php

$robot = new Robots();
$robot->save(array(
    "type" => "mechanical",
    "name" => "Astro Boy",
    "year" => 1952
));

確実に作成・更新する

アプリケーションが同時に多くの利用者に利用されている時、レコードを新規作成すると予想していたのに実際には更新がされてしまうことがあります。Phalcon\Mvc\Model::save()を使用してDBへの永続化を行うと、このような現象が発生する可能性があります。もし、絶対にレコードが新規作成又は更新されるようにしたい場合、save()の代わりにcreate()又はupdate()を呼びます。

<?php

$robot       = new Robots();
$robot->type = "mechanical";
$robot->name = "Astro Boy";
$robot->year = 1952;

// このレコードは絶対に新規作成されなければならない
if ($robot->create() === false) {
    echo "今はロボットを保存できないようです: \n";
    foreach ($robot->getMessages() as $message) {
        echo $message, "\n";
    }
} else {
    echo "おめでとうございます、新しいロボットが作成されました!";
}

これらのメソッドには、save()と同様、配列をパラメータとして渡すことができます。

自動採番されるid

モデルがidを示すカラムをもつことがあります。これらのカラムはふつう、テーブルの主キーとして使用されます。Phalcon\Mvc\Modelはidのカラムを認識することができます。そのため、Phalcon\Mvc\Modelが生成するSQLのINSERT文には、idが含まれません(DBMSが自動採番できるようにするため)。レコードを新規作成した際には、DBMSによって採番されたidがモデルに登録されます。

?php

$robot->save();

echo "生成されたid: ", $robot->id;

Phalcon\Mvc\Modelはidのカラムを認識することができます。DBMSの種類によりますが、PostgreSQLのようなSERIAL型のカラムであることもあれば、MySQLのようにauto_incrementが設定されたカラムである場合もあります。

関連テーブルの一括保存

マジックプロパティによって、あるレコードとその関連するプロパティを一度に保存することができます。

<?php

// アーティストを作成
$artist = new Artists();
$artist->name = 'Shinichi Osawa';
$artist->country = 'Japan';

// アルバムを作成
$album = new Albums();
$album->name = 'The One';
$album->artist = $artist; // アーティストを代入
$album->year = 2008;

// アルバムとアーティストの両方を保存
$album->save();

保存するレコードと、その関連レコードには、has-manyの関係があります。

<?php

// 既存のレコードの取得
$artist = Artists::findFirst('name = "Shinichi Osawa"');

// アルバムの新規作成
$album = new Albums();
$album->name = 'The One';
$album->artist = $artist;

$songs = array();

// 最初の曲の作成
$songs[0] = new Songs();
$songs[0]->name = 'Star Guitar';
$songs[0]->duration = '5:54';

// 2曲目の作成
$songs[1] = new Songs();
$songs[1]->name = 'Last Days';
$songs[1]->duration = '4:29';

// songs配列を代入
$album->songs = $songs;

// アルバムとアルバムに含まれる曲を一度に保存
$album->save();

アルバムとアーティストを同時に保存すると、暗黙的にトランザクションが使用されます。そのため、何らかの原因で関連レコードの保存に失敗した場合、親となるレコードも保存されません(内部的にエラーが発生します)。

注意点:以下のメソッドオーバーロードによって関連するエンティティを追加しても、効果はありません。

  • Phalcon\Mvc\Model::beforeSave()
  • Phalcon\Mvc\Model::beforeCreate()
  • Phalcon\Mvc\Model::beforeUpdate()

もし、保存時の挙動を変更したいなら、Phalcon\Mvc\Model::save()をオーバーロード(※)する必要があります。


※:原文では「You need to overload PhalconMvcModel::save()」なのでそのまま訳しました。overrideの間違い?

今回はここまで

次回は、Validation Messagesから先、Phalconのモデルによるバリデーションの方法を紹介します。

『GitHub実践入門』を読了



GitHubは、オンラインのGitリポジトリ共有サービス。Gitは、分散型のバージョン管理システムで、近年とても人気がある。

本書の書名を見た時は、GitHubで本1冊書くほどのネタあるの? と思ったど、本書はGitHubの画面の使い方といった表面的な情報だけでなく、Gitの入門からGitHubを利用したワークフローまで、GitHubを取り巻くトピックを的確に網羅している。

主な対象読者は、「Gitって名前は聞いたことがあるけど、使ったことはない」くらいの人から、「Gitは使ってるけどGitHubはあまり使いこなせてない」といった人あたり。GitHubでバリバリに開発している人でも、ショートカット等の細かなtipsなど、何かしら得る部分はあると思う(会社での導入の説得材料としても、本書は使いでがありそう)。

対象読者のレベルをしっかりとおさえた丁寧な解説なので、楽に読める。やや概念的に難しいのはワークフローの部分だが、ここも手を動かしながらのチュートリアルがあるため、非常に理解しやすい。

対象読者に該当する人にはおすすめ。

Conduitを削除する方法

WindowsChromeを使っていたら、Conduitというブラウザハイジャッカーがインストールされていました。ホームページを改ざんして、ブラウザ起動時にConduit版のbingページを表示するアドウェア(実質的にマルウェアといっても差し支えない)。

まず、Windows機からConduitを削除しました(「コントロールパネル」→「Search Protect」をアンインストール)。

しかし、Chrome起動時のタブが戻らない!

AdwCleaner等のソフトも試しましたが、効果なし。

最終的に解決に至った方法は、「Chromeの設定リセット」でした。(AdwCleaner等の働きのおかげもあるかもしれませんが、決め手になったのは「Chromeの設定リセット」です)

Chromeの設定リセット方法は、こちらのページ(Google公式)を参考に。

設定をリセットすると拡張やブックマークも消去されるのかな、と思ってたのですが、拡張やブックマークの情報はリセット後にも残りました。リセットされるのは、ホームページ・新しいタブ・検索エンジン等、Conduitに改ざんされて困っていた部分なので、ほとんどデメリット無しで、Conduitがホームページから消えました。

今回困ったのは、Conduit(Search Protection)をインストールしたのはWindows機だったんですが、Chromeの設定は別PCにも共有される、という点です。Chromeを利用している別のMac等でもいちいちブラウザ設定をリセットする必要があるのは結構面倒でした。設定の共有は良し悪しですね。

Phalconモデルまとめ(2)リレーション

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

リレーションの定義

Phalconでは、リレーションはモデルのinitialize()メソッドの中で定義する必要があります。リレーションを定義するメソッドには4種類あり、いずれも「自分自身のフィールド名(≒カラム名)」「参照するモデル名」「参照するフィールド名」の3つのパラメータをとります。

メソッド 状態
hasMany 1対多
hasOne 1対1
belongsTo 多対1
hasManyToNany 多対多

以下のようなテーブルの関係を考えてみます。

CREATE TABLE `robots` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(70) NOT NULL,
    `type` varchar(32) NOT NULL,
    `year` int(11) NOT NULL,
    PRIMARY KEY (`id`)
);

CREATE TABLE `robots_parts` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `robots_id` int(10) NOT NULL,
    `parts_id` int(10) NOT NULL,
    `created_at` DATE NOT NULL,
    PRIMARY KEY (`id`),
    KEY `robots_id` (`robots_id`),
    KEY `parts_id` (`parts_id`)
);

CREATE TABLE `parts` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(70) NOT NULL,
    PRIMARY KEY (`id`)
);
  • 1つのRobotsは、1つ以上のRobotsPartsをもつ
  • 1つのPartsは、1つ以上のRobotsPartsをもつ
  • 1つ以上のRobotsPartsが、Robotsに属する
  • 1つ以上のRobotsPartsが、Partsに属する
  • RobotsとPartsは、RobotsPartsを介して、多対多の関係になっている(RobotsPartsは中間テーブル)

ER:robots-robots_parts-parts

それぞれのモデルは以下のように実装できます(Phalcon DevToolsはv1.3.1現在、リレーションの自動生成には対応していません。リレーションの記述は手動で行う必要があります)。

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany("id", "RobotsParts", "robots_id");
    }

}
<?php

class Parts extends \Phalcon\Mvc\Model
{

    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany("id", "RobotsParts", "parts_id");
    }

}
<?php

class RobotsParts extends \Phalcon\Mvc\Model
{

    public $id;

    public $robots_id;

    public $parts_id;

    public function initialize()
    {
        $this->belongsTo("robots_id", "Robots", "id");
        $this->belongsTo("parts_id", "Parts", "id");
    }

}

リレーション定義メソッドの第1引数には、自分自身のフィールド名、第2引数には参照するモデル名、第3引数には参照するモデルのフィールド名を渡します。複数のフィールドのリレーションを指定したい場合、配列を使うこともできます。

3つのモデルからなる多対多の関係を単一のメソッドで記述すると、以下のようになります。

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasManyToMany(
            "id",
            "RobotsParts",
            "robots_id", "parts_id",
            "Parts",
            "id"
        );
    }
}

リレーションを活用する

モデルの関係が明示的に定義されると、関連するレコードを簡単に一括取得できます。

<?php

$robot = Robots::findFirst(2);
foreach ($robot->robotsParts as $robotPart) {
    echo $robotPart->parts->name, "\n";
}

Phalconは、関連するモデルへのデータの保存・取得にマジックメソッド(__set/__get/__call)を用います。

リレーションと同じ名前のプロパティにアクセスすると、関連するレコードが自動で取得されます。

<?php

$robot = Robots::findFirst();
$robotsParts = $robot->robotsParts; // RobotsPartsの関連レコード

getterもマジックメソッドで実装されています(明示的な定義が不要なgetter:マジックゲッター)。

<?php

$robot = Robots::findFirst();
$robotsParts = $robot->getRobotsParts(); // RobotsPartsの関連レコード
$robotsParts = $robot->getRobotsParts(array('limit' => 5)); // パラメータを渡す

Phalcon\Mvc\Modelは、「get」が頭についているメソッドの呼び出しがされると、findFirst()又はfind()の結果を返します。以下の例では、マジックゲッターを使った場合と使わない場合とを比較しています。

<?php

$robot = Robots::findFirst(2);

// Robotsと RobotsParts には1対多(hasMany)の関係がある
$robotsParts = $robot->robotsParts;

// 条件に合ったパーツだけを取得
$robotsParts = $robot->getRobotsParts("created_at = '2012-03-15'");

// パラメータをバインドする場合
$robotsParts = $robot->getRobotsParts(array(
    "created_at = :date:",
    "bind" => array("date" => "2012-03-15")
));

$robotPart = RobotsParts::findFirst(1);

// RobotsParts は Robots と多対1(belongsTo)の関係がある
$robot = $robotPart->robots;

関連するレコードを手動で取得する場合:

<?php

$robot = Robots::findFirst(2);

// Robotsと RobotsParts には1対多(hasMany)の関係がある
$robotsParts = RobotsParts::find("robots_id = '" . $robot->id . "'");

// 条件に合ったパーツだけを取得
$robotsParts = RobotsParts::find(
    "robots_id = '" . $robot->id . "' AND created_at = '2012-03-15'"
);

$robotPart = RobotsParts::findFirst(1);

// RobotsParts は Robots と多対1(belongsTo)の関係がある
$robot = Robots::findFirst("id = '" . $robotPart->robots_id . "'");

「get」という接頭辞は、find()/findFirst()する際に使用されます。どのメソッドが呼ばれるかは、リレーションの種類によります。

種類 説明 メソッド
belongsTo 関連するレコードのモデルを返す findFirst
hasOne 関連するレコードのモデルを返す findFirst
hasMany 関連するモデルの検索結果を返す find
hasManyToMany 関連するモデルの検索結果を返す(暗黙的にINNER JOINを行う) (複雑なクエリ)

「count」という接頭辞を使うことで、関連レコードの数を数えることもできます。

<?php

$robot = Robots::findFirst(2);
echo "ロボットのパーツ数:", $robot->countRobotsParts(), "\n";

リレーションのエイリアス

エイリアスの働きを説明するため、以下の例を使用します。

mysql> desc robots_similar;
+-------------------+------------------+------+-----+---------+----------------+
| Field             | Type             | Null | Key | Default | Extra          |
+-------------------+------------------+------+-----+---------+----------------+
| id                | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| robots_id         | int(10) unsigned | NO   | MUL | NULL    |                |
| similar_robots_id | int(10) unsigned | NO   |     | NULL    |                |
+-------------------+------------------+------+-----+---------+----------------+

「robots_similiar」は、あるロボットと別のロボットの類似性を定義しています。

ER:robots-robots_similar

このリレーションは以下のように実装できます。

<?php

class RobotsSimilar extends Phalcon\Mvc\Model
{

    public function initialize()
    {
        $this->belongsTo('robots_id', 'Robots', 'id');
        $this->belongsTo('similar_robots_id', 'Robots', 'id');
    }
}

いずれのリレーションも同じモデルを指しているため、どのレコードを取得するのか判然としません。

<?php

$robotsSimilar = RobotsSimilar::findFirst();

// robots_idに基いて関連するレコードを返す
// belongsTo(多対1)なので返却されるのは1レコードのみ
// しかし、「getRobots」は複数のレコードを返すようにも思える
$robot = $robotsSimilar->getRobots();

// では、similar_robots_idに基いて関連するレコードを取得するにはどうすれば?
// リレーションの名前はどちらも同じ。。。

エイリアスを使うことで、リレーションの名前を付け直すことができます。

<?php

class RobotsSimilar extends Phalcon\Mvc\Model
{

    public function initialize()
    {
        $this->belongsTo('robots_id', 'Robots', 'id', array(
            'alias' => 'Robot'
        ));
        $this->belongsTo('similar_robots_id', 'Robots', 'id', array(
            'alias' => 'SimilarRobot'
        ));
    }

}

エイリアスを使うことで、レコードの取得は簡単になります。

<?php

$robotsSimilar = RobotsSimilar::findFirst();

// robots_idに基いて関連レコードを返す
$robot = $robotsSimilar->getRobot();
$robot = $robotsSimilar->robot;

// similar_robots_idに基いて関連レコードを返す
$similarRobot = $robotsSimilar->getSimilarRobot();
$similarRobot = $robotsSimilar->similarRobot;

仮想外部キー制約

デフォルトでは、リレーションはRDBの外部キー制約のようには動作しません。外部キー制約によってエラーとされるような値をDBに入れようとした場合、Phalconは何のバリデーションエラーも発しません。リレーション定義時の第4引数にパラメータを設定することで、この挙動を変更できます。

<?php

class RobotsParts extends \Phalcon\Mvc\Model
{

    public $id;

    public $robots_id;

    public $parts_id;

    public function initialize()
    {
        $this->belongsTo("robots_id", "Robots", "id", array(
            "foreignKey" => true
        ));

        $this->belongsTo("parts_id", "Parts", "id", array(
            "foreignKey" => array(
                "message" => "partsに存在しないpart_idを指定しています"
            )
        ));
    }

}

belongsTo()リレーションが外部キー制約として振る舞うように変更すると、DBへの値の登録時にも、外部キー制約によるバリデーションが行われるようになります。同じように、hasMany()/hasOne()の振る舞いが変更されると、その値が別テーブルから参照されている限り、削除することができなくなります。

<?php

class Parts extends \Phalcon\Mvc\Model
{

    public function initialize()
    {
        $this->hasMany("id", "RobotsParts", "parts_id", array(
            "foreignKey" => array(
                "message" => "他のロボットが使用しているため、このパーツを削除できません"
            )
        ));
    }

}

CASCADEとRESTRICT

仮想外部キー制約として働くリレーションは、デフォルトでは作成・更新・削除に制限を加え、データの完全性を保ちます。

<?php

namespace Store\Models;

use Phalcon\Mvc\Model,
    Phalcon\Mvc\Model\Relation;

class Robots extends Model
{

    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany('id', 'Store\Models\Parts', 'robots_id', array(
            'foreignKey' => array(
                'action' => Relation::ACTION_CASCADE
            )
        ));
    }

}

上記コード例では、マスタとなるロボットのレコードが削除されると、それを参照しているパーツのレコードも全て削除されるように設定しています。

[補足]名前空間エイリアス

Phalcon公式ドキュメントには無いけれど、知っておいたほうが良いワザとして、「名前空間エイリアス設定」があります。名前空間を使用したモデルへのリレーション設定は1つ上のサンプルで、エイリアスも別のサンプルにはありますが、両方を一度に扱ったサンプルはありません。モデルに名前空間を使用している場合、エイリアスの使用は必須に近いと思うので、ここで紹介しておきます。

<?php

namespace Store\Models;

use Phalcon\Mvc\Model;

class Robots extends Model
{

    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany('id', 'Store\Models\Parts', 'robots_id');
    }

}

上記のような設定を行ったモデルでは、以下のようにしてStore\Models\Partsにアクセスすることができます。

$robot = new \Store\Models\Robots;
$parts = $robot->getRelations('Store\Models\Parts');

しかし、いちいち名前空間をフルパスで書くのはいかにも面倒臭い。この場合、エイリアスを利用すると幾分楽ができます。

<?php

namespace Store\Models;

use Phalcon\Mvc\Model;

class Robots extends Model
{

    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany('id', 'Store\Models\Parts', 'robots_id', ['alias' => 'Parts']);
    }

}
$robot = new \Store\Models\Robots;
$parts = $robot->Parts;

今回はここまで

ここまでで、リレーションの基本は押さえました。次回は、Generating Calculationsから先をみていきます。

Phalconモデルまとめ(1)データ取得の基本

「最速」PHPフレームワークPhalconのモデルについて、基本事項をまとめます(公式ドキュメントの翻訳+αです)。記事執筆時のPhalconのバージョンは1.3.1です。なお、サンプルコードを実行したい場合、環境構築を参考にしてください。

モデルの基本

Phlaconのモデルは、Phalcon\Mvc\Modelを継承したクラスです。モデルクラスは以下の条件を満たす必要があります。

  • modelsディレクトリに配置する
  • モデルファイルは1つのクラスだけを含む
  • クラス名はキャメルケース
<?php

class Robots extends \Phalcon\Mvc\Model
{

}

上記例が、Robotsモデルの実装例です。RobotsがPhalcon\Mvc\Modelを継承している点に注目してください。Phalcon\Mvc\Modelを継承することで、データベースにおいて基本的なCRUD処理から、データのバリデーション、複数のモデルの関係に基づいた検索など様々な機能を利用することができます。

デフォルトでは、「Robots」モデルは「robots」テーブルを参照します。参照するテーブルを手動で設定したい場合は、getSource()メソッドを使用します。

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public function getSource()
    {
        return "the_robots";
    }
}

上記コード例では、Robotsは「the_robots」テーブルを参照します。

initialize()メソッドは、1リクエストに対して1回だけ呼ばれます。モデルの振る舞いのカスタマイズに適しています。

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public function initialize()
    {
        $this->setSource("the_robots");
    }
}

initialize()は1回のリクエストを通して1回だけ呼ばれ、モデルの全てのインスタンスの初期化に使うことを目的としています。インスタンスが作られる度に呼ばれるメソッドが欲しい場合は、onConstruct()メソッドを使います。

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public function onConstruct()
    {
        //...
    }
}

publicプロパティとgetter/setter

モデルのプロパティは、publicに実装し、どこからでも読み取り・変更できるようにすることができます。(Phalcon DevToolsのデフォルトではpublicプロパティでモデルを自動生成します)

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public $price;
}

getterとsetterを使うことで、プロパティの操作を自由に行えるようにしつつ、モデルにセットされるデータの整形やバリデーションを行うことができます。(Phalcon DevToolsのモデル生成コマンドに--get-setオプションを付けると、getter/setterでモデルを自動生成します)

<?php

class Robots extends \Phalcon\Mvc\Model
{
    protected $id;

    protected $name;

    protected $price;

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        // 名前の長さをチェック
        if (strlen($name) < 10) {
            throw new \InvalidArgumentException('名前が短すぎます');
        }
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setPrice($price)
    {
        // 値段をチェック
        if ($price < 0) {
            throw new \InvalidArgumentException('負の数の値段をつけることはできません');
        }
        $this->price = $price;
    }

    public function getPrice()
    {
        // 値段を浮動小数点数に変換
        return (double) $this->price;
    }
}

publicなプロパティは、利用する際にシンプルなコードになるという利点があります。一方、getter/setterは、テストのしやすさや拡張性・メンテナンス性を向上させてくれます。自分の作ろうとしているアプリケーションに適した実装を選んでください。PhalconのORマッパーはいずれの実装にも対応しています。

モデルの名前空間

クラス名の衝突を避けるため、名前空間を使えます。参照するテーブルはクラス名に基づくため、以下の例では「robots」テーブルが参照されます。

<?php

namespace Store\Toys;

class Robots extends \Phalcon\Mvc\Model
{

}

レコードからオブジェクトへの変換

モデルの全てのインスタンスは、テーブルの1行を表します。オブジェクトのプロパティを取得することで、DBのレコードにアクセスすることができます。一例として、以下のrobotsテーブルを考えます。

mysql> select * from robots;
+----+------------+------------+------+
| id | name       | type       | year |
+----+------------+------------+------+
|  1 | Robotina   | mechanical | 1972 |
|  2 | Astro Boy  | mechanical | 1952 |
|  3 | Terminator | cyborg     | 2029 |
+----+------------+------------+------+

以下のコードで、プライマリーキーによる検索を行い、その結果を表示できます。

<?php

// id = 3 のレコードを検索
$robot = Robots::findFirst(3);

// "Terminator"と表示
echo $robot->name;

レコードを一度取得すれば、そのデータを変更して保存することもできます。

<?php

$robot = Robots::findFirst(3);
$robot->name = "RoboCop";
$robot->save();

Phalconのモデルを使う場合、生のSQLを書く必要はありません。Phalcon\Mvc\Modelはデータベースの抽象化を行ってくれます。

レコードの検索

Phalcon\Mvc\Modelはデータの検索のためにいくつかのメソッドを提供しています。以下はfind()メソッドの使用例です。

<?php

// ロボットは何体いる?
$robots = Robots::find();
echo "ロボットの数:", count($robots), "\n";

// typeがmechanicalであるロボットの数は?
$robots = Robots::find("type = 'mechanical'");
echo "mechanicalなロボットの数:", count($robots), "\n";

// typeがvirtualなロボットの一覧を取得して、name順にソート
$robots = Robots::find(array(
    "type = 'virtual'",
    "order" => "name"
));
foreach ($robots as $robot) {
    echo $robot->name, "\n";
}

// typeがvirtualなロボットの一覧を取得して、name順にソート(上限100件)
$robots = Robots::find(array(
    "type = 'virtual'",
    "order" => "name",
    "limit" => 100
));
foreach ($robots as $robot) {
   echo $robot->name, "\n";
}

findFirst()メソッドを使うことで、判定基準に合った最初のレコードを取得することができます。

<?php

// robotsテーブルの最初のロボットは?
$robot = Robots::findFirst();
echo "最初のロボットの名前:", $robot->name, "\n";

// typeがmechanicalな最初のロボットは?
$robot = Robots::findFirst("type = 'mechanical'");
echo "最初のmechanicalロボットの名前:", $robot->name, "\n";

// nameでソートした最初のvirtualロボットの名前
$robot = Robots::findFirst(array("type = 'virtual'", "order" => "name"));
echo "最初のvirtualロボットの名前:", $robot->name, "\n";

find()/findFirst()には、検索条件となる連想配列を渡すことができます。

<?php

$robot = Robots::findFirst(array(
    "type = 'virtual'",
    "order" => "name DESC",
    "limit" => 30
));

$robots = Robots::find(array(
    "conditions" => "type = ?1",
    "bind"       => array(1 => "virtual")
));

利用可能なクエリオプションは以下です。

パラメータ 説明
conditions 検索条件。Phalcon\Mvc\Modelは第1引数を検索条件とみなす。 "conditions" => "name LIKE ‘steve%’"
columns 指定したカラムだけを返すようにする。 "columns" => "id, name"
bind プレースホルダーをbindする値で置き換える。 "bind" => array("status" => "A", "type" => "some-time")
bindTypes バインド時にパラメーターを指定した型に変換する。 "bindTypes" => array(Column::BIND_TYPE_STR, Column::BIND_TYPE_INT)
order 検索結果をソートする。 "order" => "name DESC, status"
limit 検索結果の件数を制限する。 "limit" => 10 / "limit" => array("number" => 10, "offset" => 5)
group 複数のレコードからデータを集め、結果を1つ以上のグループにまとめる。 "group" => "name, status"
for_update 検索対象データに排他ロックをかける。 "for_update" => true
shared_lock 検索対象データに共用ロックをかける。 "shared_lock" => true
cache 結果をキャッシュし、DBアクセスを減らす "cache" => array("lifetime" => 3600, "key" => "my-find-key")
hydration 結果セットの型を指定する(オブジェクト/連想配列)。 "hydration" => Resultset::HYDRATE_OBJECTS

お好みであれば、パラメータではなくオブジェクト指向の構文でクエリを組み立てることもできます。

<?php

$robots = Robots::query()
    ->where("type = :type:")
    ->andWhere("year < 2000")
    ->bind(array("type" => "mechanical"))
    ->order("name")
    ->execute();

静的メソッドのquery()がPhalcon\Mvc\Model\Criteriaオブジェクトを返します。

全てのクエリは内部的にPHQLとして扱われます。PHQLは、Phalcon独自のクエリ言語です。

最後に、findFirstBy<プロパティ名>メソッドがあります。このメソッドは、先に紹介したfindFirst()の拡張です。

一例として、以下のようなモデルがあるとします。

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public $price;
}

ここで、nameを元に検索したい場合、findByName()メソッドを使って以下のように検索できます。

<?php

$name = "Terminator";
$robot = Robots::findFirstByName($name);

モデルの結果セット

findFirst()は、検索結果のモデルのインスタンスを直接返します。

一方、find()はPhalcon\Mvc\Model\Resultset\Simpleオブジェクトを返します。このオブジェクトは、データの順次取得や特定のデータの探索、レコード数のカウント等の機能をカプセル化します。

これらのオブジェクトは、通常の配列よりも機能が豊富です。Phalcon\Mvc\Model\Resultsetで最も素晴らしい機能の1つは、メモリ内に存在するレコードはどんな時でも1件だけである、という点です。このため、大量のデータを扱うときでもメモリの消費は最小限に抑えられます。

<?php

// 全てのロボットを取得
$robots = Robots::find();

// foreachでまわす
foreach ($robots as $robot) {
    echo $robot->name, "\n";
}

// whileでまわす
$robots->rewind();
while ($robots->valid()) {
    $robot = $robots->current();
    echo $robot->name, "\n";
    $robots->next();
}

// 結果数を数える
echo count($robots);

// 結果数を数える、もう1つの方法
echo $robots->count();

// 内部カーソルを3番めに進める
$robots->seek(2);
$robot = $robots->current();

// 結果セットの中での位置からロボットを取得
$robot = $robots[4];

// 特定の位置にレコードがあるかチェック
if (isset($robots[3])) {
   $robot = $robots[3];
}

// 結果セットの最初のレコードを取得
$robot = $robots->getFirst();

// 最後のレコードを取得
$robot = $robots->getLast();

Phalconの結果セットはDBMSのカーソルをエミュレートしているため、場所を指定してデータを取得することができます。DBMSによってはカーソルに対応していないものもあるため、注意してください(非対応のDBMSの場合、特定の位置のレコードを取得しようとする度にDBに問い合わせが行われます)。

巨大な問い合わせ結果をメモリに持っておくと、リソースを大きく消費します。そのため、結果セットは32行の固まりとしてDBから取得し、問い合わせ回数の減少とメモリの節約を行っています。

結果セットをキャッシュしておくことができます(Phaclon\Cacheなどが利用できます)。しかし、データのシリアライズを行うと全てのデータを取得して配列にするため、キャッシュの作成処理を行っている間はメモリ消費が多くなります。

<?php

// Partsモデルから全てのモデルを取得
$parts = Parts::find();

// 結果セットをファイルに保存
file_put_contents("cache.txt", serialize($parts));

// partsをファイルから取得
$parts = unserialize(file_get_contents("cache.txt"));

// partsをループでまわす
foreach ($parts as $part) {
   echo $part->id;
}

結果セットのフィルタング

最も効率的なデータのフィルタリング方法は、検索条件を指定することです。この場合、データベースは(利用可能であれば)インデックスを使用するため、高速にデータを取得できます。加えて、PHPでデータのフィルタリングを行うこともできます。

<?php

$customers = Customers::find()->filter(function($customer) {

    // 有効なEメールアドレスのあるcustomerだけを返す
    if (filter_var($customer->email, FILTER_VALIDATE_EMAIL)) {
        return $customer;
    }

});

パラメータのバインド

Phaclon\Mvc\Modelはパラメータのバインド機構をサポートしています。バインド機構を使うことによるパフォーマンスへの影響はわずかですが、この方法を使うことでSQLインジェクション攻撃を受ける可能性を減少させることができます。文字列と数字のプレースホルダーがサポートされています。

<?php

// 文字列のプレースホルダー(PDOと違って、「前後にコロン」なので注意!)
$conditions = "name = :name: AND type = :type:";
// $conditions = "name = :name AND type = :type"; //(「前にコロン」のPDO流だとNG)

// プレースホルダーと同じ文字列のキーの部分がパラメーターで置き換えられる
$parameters = array(
    "name" => "Robotina",
    "type" => "maid"
);

// クエリ実行
$robots = Robots::find(array(
    $conditions,
    "bind" => $parameters
));

// 数字のプレースホルダー(PDOと違って、?だけでなく、数字も必要)
$conditions = "name = ?0 AND type = ?1";
// $conditions = "name = ? AND type = ?"; // 「?だけ」のPDO流だとNG

$parameters = array("Robotina", "maid");
$robots     = Robots::find(array(
    $conditions,
    "bind" => $parameters
));

// プレースホルダーと同じ数字のキーの部分がパラメーターで置き換えられる
$conditions = "name = :name: AND type = ?1";

//Parameters whose keys are the same as placeholders
$parameters = array(
    "name" => "Robotina",
    1 => "maid"
);

// クエリ実行
$robots = Robots::find(array(
    $conditions,
    "bind" => $parameters
));

数字のプレースホルダーを使う場合、1, 2といったint型の値として定義する必要があります。'1', '2'といった定義は文字列とみなされるため、数字のプレースホルダーとしては機能しません。

文字列のプレースホルダーは、PDOによって自動的にエスケープされます。この機能は文字コードを考慮するため、データベース設定で適切な文字コードを指定することを推奨します。

パラメーターの型を指定することもできます。

<?php

use \Phalcon\Db\Column;

// バインドするパラメータ
$parameters = array(
    "name" => "Robotina",
    "year" => 2008
);

// パラメーターの型(この型に自動で変換される)
$types = array(
    "year" => Column::BIND_PARAM_INT
);

// クエリ実行
$robots = Robots::find(array(
    "name = :name: AND year = :year:",
    "bind" => $parameters,
    "bindTypes" => $types
));

bindTypesのデフォルトはPhalcon\Db\Column::BIND_PARAM_STR(文字列)です。文字列型のパラメータの型を指定する必要はありません。

バインド機構はfind()/findFirst()/findFirstBy<プロパティ名>のいずれでも利用できます。一方、count()、sum()、average()といったメソッドには利用できません。

取得したレコードの初期化

DBからデータを取得した後に、何かしらの初期化処理が必要な場合があります。このようなメソッドは、「afterFetch」メソッドとして定義できます。このイベントはインスタンスの作成とデータの代入時に実行されます。

<?php

class Robots extends Phalcon\Mvc\Model
{

    public $id;

    public $name;

    public $status;

    // 保存前
    public function beforeSave()
    {
        // 配列を文字列に変換
        $this->status = join(',', $this->status);
    }

    // データ取得後
    public function afterFetch()
    {
        // 文字列を配列に変換
        $this->status = explode(',', $this->status);
    }
}

もしgetter/setterを使っていれば、アクセス時に初期化することもできます。

<?php

class Robots extends Phalcon\Mvc\Model
{
    protected $status;

    public function getStatus()
    {
        return explode(',', $this->status);
    }
}

今回はここまで

以上で、データ取得の基本は終わりです(公式ドキュメントのInitializing/Preparing fetched recordsまで)。次回は、Relationships between Modelsから先、複数テーブルの扱いをみていきます。