「最速」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から先、複数テーブルの扱いをみていきます。