Strings of Life

PHP/Phalcon/MySQL/JavaScript/RegExp/Ruby/Perl/ActionScript

タグ:PHP

「最速」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のモデルによるバリデーションの方法を紹介します。

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

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

「最速」PHPフレームワークPhalconのモデルについて、基本事項をまとめます。この記事では、Phalconのモデルの動作確認を行うための環境構築方法について紹介します。記事執筆時のPhalconのバージョンは1.3.1です。

前提条件:VirtualBox及びVagrantをインストール済みとします(私の手元の環境はVirtualBox 4.3.10、Vagrant 1.5.2です)。

(0) Phalconセットアップ済みのVagrant Boxを導入

vagrant init phalconbox https://s3-eu-west-1.amazonaws.com/phalcon/phalcon125-apache2-php54-mysql55.box
vagrant up
vagrant ssh

なお、このboxのPhalconのバージョンは1.2.5。最新版の1.3.1を使いたい場合は、以下の手順を実行。

git clone git://github.com/phalcon/cphalcon.git
cd cphalcon/build
sudo ./install

(1) Phalcon DevToolsの導入

mkdir ~/bin
cd ~/bin
sudo apt-get install curl
curl -s http://getcomposer.org/installer | php
php composer.phar require phalcon/devtools dev-master
sudo ln -s /home/vagrant/bin/vendor/bin/phalcon.php /usr/bin/phalcon
sudo chmod ugo+x /usr/bin/phalcon

以下の一文の入ったファイルを作成して、/etc/php5/mods-available にphalcon.iniとして保存。

extension=phalcon.so

「sudo php5enmod phalcon」でPhalconを有効化。

「phalcon」コマンドを実行して、以下のように表示されればOK。

$ phalcon

Phalcon DevTools (1.3.1)

Available commands:
  commands (alias of: list, enumerate)
  controller (alias of: create-controller)
  model (alias of: create-model)
  all-models (alias of: create-all-models)
  project (alias of: create-project)
  scaffold
  migration
  webtools

(2) テスト用のデータベースとテーブルを作成

mysql -u root -ppass -e 'CREATE DATABASE model_practice'

以下のSQL文を create_table_robots.sql として保存し、「mysql -u root -ppass model_practice < create_table_robots.sql」を実行。

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

(3) テストデータをINSERT

以下のSQL文を insert_into_robots.sql として保存し、「mysql -u root -ppass model_practice < insert_into_robots.sql」を実行。

INSERT INTO robots (name, type, year) VALUES ('Robotina', 'mecanical', 1972);
INSERT INTO robots (name, type, year) VALUES ('Astro Boy', 'mecanical', 1952);
INSERT INTO robots (name, type, year) VALUES ('Terminator', 'cyborg', 2029);
INSERT INTO robots (name, type, year) VALUES ('VT', 'virtual', 2002);
INSERT INTO robots (name, type, year) VALUES ('AK-966', 'virtual', 1999);

「mysql -u root -ppass model_practice -e 'SELECT * FROM robots'」を実行して、上でINSERTしたデータが表示されればOK。

(4) テスト用プロジェクトを作成

cd /var/www
phalcon project model_practice
cd model_practice

(5) テスト用プロジェクトのDB設定

app/config/config.php を編集。
<?php

return new \Phalcon\Config(array(
    'database' => array(
        'adapter'     => 'Mysql',
        'host'        => 'localhost',
        'username'    => 'root',
        'password'    => 'pass',
        'dbname'      => 'model_practice',
    ),
    'application' => array(
        'controllersDir' => __DIR__ . '/../../app/controllers/',
        'modelsDir'      => __DIR__ . '/../../app/models/',
        'viewsDir'       => __DIR__ . '/../../app/views/',
        'pluginsDir'     => __DIR__ . '/../../app/plugins/',
        'libraryDir'     => __DIR__ . '/../../app/library/',
        'cacheDir'       => __DIR__ . '/../../app/cache/',
        'baseUri'        => '/model_practice/',
    )
));

(6) モデルの生成

phalcon model robots

app/models の下に以下のような内容の Robots.php が生成されるはず。

<?php

class Robots extends \Phalcon\Mvc\Model
{

    /**
     *
     * @var integer
     */
    public $id;

    /**
     *
     * @var string
     */
    public $name;

    /**
     *
     * @var string
     */
    public $type;

    /**
     *
     * @var integer
     */
    public $year;

    /**
     * Independent Column Mapping.
     */
    public function columnMap()
    {
        return array(
            'id' => 'id',
            'name' => 'name',
            'type' => 'type',
            'year' => 'year'
        );
    }

}

(7) 動作確認

public/index.phpの27行目(echoの行)をコメントアウト。これで、ブラウザ向けの出力をしなくなる。

<?php

error_reporting(E_ALL);

try {

    /**
     * Read the configuration
     */
    $config = include __DIR__ . "/../app/config/config.php";

    /**
     * Read auto-loader
     */
    include __DIR__ . "/../app/config/loader.php";

    /**
     * Read services
     */
    include __DIR__ . "/../app/config/services.php";

    /**
     * Handle the request
     */
    $application = new \Phalcon\Mvc\Application($di);

//    echo $application->handle()->getContent();

} catch (\Exception $e) {
    echo $e->getMessage();
}

以下の内容でmodel_practice.phpを作成

<?php

require_once __DIR__ . '/public/index.php';

$robots = Robots::find();
foreach ($robots as $robot) {
    echo $robot->name, PHP_EOL;
}

「php model_practice.php」を実行して、ロボットの名前が表示されればOK。

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

なお、ルーティングの定義方法等の基本事項については、(1)を参照してください。

ルートのグループ化

ルートが共通のパスを持っている場合、それらをひとまとめにすることができます。

<?php

$router = new \Phalcon\Mvc\Router();

// 共通のモジュールとコントローラーを持つグループを作成
$blog = new \Phalcon\Mvc\Router\Group(array(
    'module'     => 'blog',
    'controller' => 'index'
));

// 全てのルートが /blog で始まると設定する
$blog->setPrefix('/blog');

// グループにルートを追加
$blog->add('/save', array(
    'action' => 'save'
));

// 別のルートを追加
$blog->add('/edit/{id}', array(
    'action' => 'edit'
));

// デフォルトとは異なるルートの追加
$blog->add('/blog', array(
    'controller' => 'blog',
    'action' => 'index'
));

// グループをルーターに追加
$router->mount($blog);

ルートグループを別ファイルに分けることもできます。

<?php

class BlogRoutes extends Phalcon\Mvc\Router\Group
{
    public function initialize()
    {
        // デフォルトのパス
        $this->setPaths(array(
            'module'    => 'blog',
            'namespace' => 'Blog\Controllers'
        ));

        // ルートは全て /blog から始まる
        $this->setPrefix('/blog');

        // ルートを追加
        $this->add('/save', array(
            'action' => 'save'
        ));

        // 別のルートを追加
        $this->add('/edit/{id}', array(
            'action' => 'edit'
        ));

        // デフォルトとは異なるルートの追加
        $this->add('/blog', array(
            'controller' => 'blog',
            'action' => 'index'
        ));

    }
}

あとは、このグループをルーターにmount()するだけです。

$router->mount(new BlogRoutes());

mod_rewrite

ルーターに有効なURLが渡されると、ルーターは与えられたURLがルートにマッチするかチェックします。デフォルトでは、mod_rewriteによって $GET['url'] という値が作られるようになっており、この値がルーターに渡されます。Phalconで一般的に使用されるmod_rewrite設定は以下のようになります。

RewriteEngine On
RewriteCond   %{REQUEST_FILENAME} !-d
RewriteCond   %{REQUEST_FILENAME} !-f
RewriteRule   ^(.*)$ index.php?_url=/$1 [QSA,L]

名前付きルート

ルーターに追加されたルートは、Phalcon\Mvc\Router\Route オブジェクトとして保持されます。このクラスは、それぞれのルートの詳細をカプセル化します。したがって、以下のような使い方もできます。

$route = $router->add("/posts/{year}/{title}", "Posts::show"); // add()の返り値はRouteオブジェクト
$route->setName("show-posts"); // Routeオブジェクトに名前を設定

// 上記処理をメソッドチェインでまとめる
$router->add("/posts/{year}/{title}", "Posts::show")->setName("show-posts");

名前付きルートは以下のように利用します(Phalcon\Mvc\Urlを使用)。

// /posts/2012/phalcon-1-0-released と表示
echo $url->get(array(
    "for" => "show-posts",
    "year" => "2012",
    "title" => "phalcon-1-0-released"
));

使用例

以下は、ルーティングのカスタマイズ例です。

// matches "/system/admin/a/edit/7001"
$router->add(
    "/system/:controller/a/:action/:params",
    array(
        "controller" => 1,
        "action"     => 2,
        "params"     => 3
    )
);

// matches "/es/news"
$router->add(
    "/([a-z]{2})/:controller",
    array(
        "controller" => 2,
        "action"     => "index",
        "language"   => 1
    )
);

// matches "/es/news"
$router->add(
    "/{language:[a-z]{2}}/:controller",
    array(
        "controller" => 2,
        "action"     => "index"
    )
);

// matches "/admin/posts/edit/100"
$router->add(
    "/admin/:controller/:action/:int",
    array(
        "controller" => 1,
        "action"     => 2,
        "id"         => 3
    )
);

// matches "/posts/2010/02/some-cool-content"
$router->add(
    "/posts/([0-9]{4})/([0-9]{2})/([a-z\-]+)",
    array(
        "controller" => "posts",
        "action"     => "show",
        "year"       => 1,
        "month"      => 2,
        "title"      => 4
    )
);

// matches "/manual/en/translate.adapter.html"
$router->add(
    "/manual/([a-z]{2})/([a-z\.]+)\.html",
    array(
        "controller" => "manual",
        "action"     => "show",
        "language"   => 1,
        "file"       => 2
    )
);

// matches /feed/fr/le-robots-hot-news.atom
$router->add(
    "/feed/{lang:[a-z]+}/{blog:[a-z\-]+}\.{type:[a-z\-]+}",
    "Feed::get"
);

// matches /api/v1/users/peter.json
$router->add('/api/(v1|v2)/{method:[a-z]+}/{param:[a-z]+}\.(json|xml)',
    array(
        'controller' => 'api',
        'version' => 1,
        'format' => 4
    )
);

コントローラーと名前空間として許可する文字列の正規表現には、注意する必要があります。これらの文字列はファイルを探す際に使われるため、許可しないファイルへのアクセスを許してしまう恐れがあります。安全な正規表現は「 /([a-zA-Z0-9_-]+)」です。

デフォルトルーティング

Phalcon\Mvc\Router にはデフォルトのルーティングが設定されています。そのパターンは、「/:controller/:action/:params」です。

例えば、http://phalconphp.com/documentation/show/about.html というURLは以下のように変換されます。

Controller documentation
Action show
Parameter about.html

このルートを使いたくない場合、ルーターの初期化時にfalseを渡します。

$router = new \Phalcon\Mvc\Router(false);

デフォルトルートの設定

アプリケーションがいずれのルートにもマッチしないURLでアクセスされた場合、「/」ルートが使用されます。

$router->add("/", array(
    'controller' => 'index',
    'action' => 'index'
));

デフォルトパスの設定

モジュールや名前空間のデフォルトをあらかじめ設定しておくこともできます。

$router->setDefaultModule('backend');
$router->setDefaultNamespace('Backend\Controllers');
$router->setDefaultController('index');
$router->setDefaultAction('index');

// 配列で設定する場合
$router->setDefaults(array(
    'controller' => 'index',
    'action' => 'index'
));

Not Found設定

いずれのルートにもマッチしなかった場合のルーティングを設定することができます。

$router->notFound(array(
    "controller" => "index",
    "action" => "route404"
));

余分なスラッシュの扱い

余分なスラッシュを伴ってアクセスされた場合("/login"に対して"/login/"でアクセス等)、デフォルトではルートはマッチせず、Not Foundになります。余分なスラッシュを無視したい場合は、以下のメソッドを使用します。

$router->removeExtraSlashes(true);

あるいは、自前で末尾スラッシュを無視するルートを定義することもできます。

$router->add(
    '/{language:[a-z]{2}}/:controller[/]{0,1}',
    array(
        'controller' => 2,
        'action'     => 'index'
    )
);

マッチのコールバック関数

beforMatchメソッドにコールバック関数を渡すことで、より複雑な条件によるマッチングを行えます。

$router->add('/login', array(
    'module' => 'admin',
    'controller' => 'session'
))->beforeMatch(function($uri, $route) {
    // リクエストがAjaxかチェック
    if ($_SERVER['X_REQUESTED_WITH'] === 'xmlhttprequest') {
        return false;
    }
    return true;
});

マッチング条件のコールバックは、クラスにすることで再利用できるようになります。

<?php

class AjaxFilter
{
    public function check()
    {
        return $_SERVER['X_REQUESTED_WITH'] == 'xmlhttprequest';
    }
}

この場合、無名関数の代わりにフィルタークラスのインスタンスと、実行したいメソッド名を渡します。

$router->add('/get/info/{id}', array(
    'controller' => 'products',
    'action' => 'info'
))->beforeMatch(array(new AjaxFilter(), 'check'));

ホストネーム制約

ルートに、ホストネームによる制約を付けることができます。

$router->add('/login', array(
    'module' => 'admin',
    'controller' => 'session',
    'action' => 'login'
))->setHostName('admin.company.com');

ホストネーム制約では、正規表現も使えます。

$router->add('/login', array(
    'module' => 'admin',
    'controller' => 'session',
    'action' => 'login'
))->setHostName('([a-z+]).company.com');

特定のグループに対してホストネーム制約を設定することもできます。

// 共通のモジュールとコントローラーをもつグループを作成
$blog = new \Phalcon\Mvc\Router\Group(array(
    'module' => 'blog',
    'controller' => 'posts'
));

// ホストネーム制約
$blog->setHostName('blog.mycompany.com');

$blog->setPrefix('/blog');

$blog->add('/', array(
    'action' => 'index'
));

$blog->add('/save', array(
    'action' => 'save'
));

$router->mount($blog);

URIの取得方法を変更する

Phalconのデフォルトでは、ルーターに渡されるURLは $GET['url'] から取得します。Router::setUriSource()にRouter::URI_SOURCE_SERVER_REQUEST_URI を渡すことで、 $GET['url']からではなく、$_SERVER['REQUEST_URI']からURLを取得するように変更できます。

// $router->setUriSource(Router::URI_SOURCE_GET_URL); // $_GET['_url'] を使う(デフォルト)
$router->setUriSource(Router::URI_SOURCE_SERVER_REQUEST_URI); // $_SERVER['REQUEST_URI'] を使う

ルーティングの動作テスト

以下のスクリプトで、ルーティングの動作テストを行えます。

<?php

// テスト用URL
$testRoutes = array(
    '/',
    '/index',
    '/index/index',
    '/index/test',
    '/products',
    '/products/index/',
    '/products/show/101',
);

$router = new Phalcon\Mvc\Router();

// ルートを定義

// テスト用URLを1つずつテスト
foreach ($testRoutes as $testRoute) {

    $router->handle($testRoute);

    echo 'Testing ', $testRoute, PHP_EOL;

    // ルートがマッチしたかテスト
    if ($router->wasMatched()) {
        echo 'Controller: ', $router->getControllerName(), PHP_EOL;
        echo 'Action: ', $router->getActionName(), PHP_EOL;
    } else {
        echo 'ルートはマッチしませんでした', PHP_EOL;
    }
    echo PHP_EOL;

}

ルーターインスタンスを登録する

DIコンテナにルーターのインスタンスを登録することで、ルーターが利用可能になります。

<?php

$router = new \Phalcon\Mvc\Router();

$router->add("/login", array(
    'controller' => 'login',
    'action' => 'index',
));

$router->add("/products/:action", array(
    'controller' => 'products',
    'action' => 1,
));

return $router;

上記phpファイルの末尾では、ルーターインスタンスをreturnしています。このphpファイルをrequireすると、returnされた値をrequireの返り値として受け取ることができます。

<?php

/**
* ルーティングを利用できるようにする
*/
$di->set('router', function(){
    $router = require __DIR__.'/../app/config/routes.php';
    return $router;
});

独自のルーターを実装する

Phalcon\Mvc\RouterInterfaceを実装したルーターを作れば、自前のルーターを用意することができます。

まとめ

今回は、Testing your routesまでを紹介しました。ここまでで、Phalconのルーターに関しては、ほとんどの事項を学び終えたことになります。

残りは、Symfony風のAnnotations Routerのような若干マニアックな機能であったり、APIリファレンスだったりなので、記事にするかは未定です。

APIリファレンスについては、Phalcon\Mvc\RouterPhalcon\Mvc\Routeあたりは見ておくと良いと思います。グループ機能を使うなら、Phalcon\Mvc\Route\Groupも。

このページのトップヘ