Strings of Life

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

タグ:Phalcon

「最速」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も。

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

基本形

<?php

// ルーターオブジェクトを作成
$router = new \Phalcon\Mvc\Router();

// ルーティング定義
$router->add(
    "/admin/users/my-profile",
    array(
        "controller" => "users",
        "action"     => "profile",
    )
);

\Phalcon\Mvc\Router::add()でルーティングを定義します。第1引数にURL、第2引数にコントローラーやアクションを定義します。なお、ルーティングの優先順位は、後にadd()したものが優先されます。

ルーティングの動作確認

Phalconがインストールされ有効になっている環境であれば、以下のようなスクリプトでルーティングの動作確認を行えます。

<?php

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

$router->add(
    "/admin/users/my-profile",
    array(
        "controller" => "users",
        "action"     => "profile",
    )
);

// テストしたいURLを指定
$router->handle("/admin/users/my-profile");

// コントローラ名とアクション名を表示
printf("Controller: %s \n", $router->getControllerName());
printf("Action    : %s \n", $router->getActionName());

// ルーティングオブジェクトの中身全部表示
$route = $router->getMatchedRoute();
var_dump($route);

このスクリプトを実行すると、handle()で指定したURLに対して、ルーターに定義したルートがマッチしたかどうか、マッチした場合はどのコントローラーとアクションが実行されるか、が分かります。

柔軟なルーティング

プレースホルダーを使うことで、柔軟なルートを定義できます。

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

このルートが定義されているとき、/admin/users/a/delete/dave/301 にアクセスすると、以下のように解釈されます。

Controllerusers
Actiondelete
Parameterdave
Parameter301

プレースホルダー

\Phalcon\Mvc\Routerで使用可能なプレースホルダーについてまとめた表が以下になります。

プレースホルダー 正規表現 用途
/:module /([a-zA-Z0-9_-]+) モジュール名
/:controller /([a-zA-Z0-9_-]+) コントローラ名
/:action /([a-zA-Z0-9_]+) アクション名
/:params (/.*)* パラメータ。スラッシュ区切りで複数渡せる。このプレースホルダーはルートの末尾にのみ使用できる。
/:namespace /([a-zA-Z0-9_-]+) 名前空間名
/:int /([0-9]+) 自然数

なお、コントローラー名は大文字化が行われます。この際、_と-は取り除かれます。例えば、some_controller は、 SomeController に変換されます。

名前付きパラメータ

$router->add(
    "/news/([0-9]{4})/([0-9]{2})/([0-9]{2})/:params",
    array(
        "controller" => "posts",
        "action"     => "show",
        "year"       => 1, // ([0-9]{4})
        "month"      => 2, // ([0-9]{2})
        "day"        => 3, // ([0-9]{2})
        "params"     => 4, // :params
    )
);

上のようにルーティングを定義すると、下のような形でURLからパラメータを受け取れます。

<?php

class PostsController extends \Phalcon\Mvc\Controller
{
    public function showAction()
    {
        $year  = $this->dispatcher->getParam("year");
        $month = $this->dispatcher->getParam("month");
        $day   = $this->dispatcher->getParam("day");
    }
}

以下のような書き方もできます。

$router->add(
    "/documentation/{chapter}/{name}.{type:[a-z]+}",
    array(
        "controller" => "documentation",
        "action"     => "show"
    )
);
<?php

class DocumentationController extends \Phalcon\Mvc\Controller
{
    public function showAction()
    {
        $name = $this->dispatcher->getParam("name");
        $type = $this->dispatcher->getParam("type");
    }
}

短縮形

\Phalcon\Mvc\Router::add()の第2引数には、配列ではなく文字列を渡すこともできます(短縮形)。この場合、コントローラ名とメソッド名を指定します。

// 短縮形
$router->add("/posts/{year:[0-9]+}/{title:[a-z\-]+}", "Posts::show");

// 配列
$router->add(
    "/posts/([0-9]+)/([a-z\-]+)",
    array(
       "controller" => "posts",
       "action"     => "show",
       "year"       => 1,
       "title"      => 2,
    )
);

モジュールへのルーティング

URLにモジュール名が含まれている場合は以下のようなルーティングを行います(複数モジュール構成の場合に使用します)。

$router->add('/:module/:controller/:action/:params', array(
    'module' => 1,
    'controller' => 2,
    'action' => 3,
    'params' => 4
));

このルートが定義されている時、/admin/users/edit/sonny にアクセスすると、以下のように解釈されます。

Moduleadmin
Controllerusers
Actionedit
Parametersonny

モジュール名を明示的に指定することもできます。

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

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

名前空間を指定することもできます。

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

名前空間とクラス名の指定もできます。

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

HTTPメソッドの制限

add()でルートを定義した場合、全てのHTTPメソッドが許可されます。特定のメソッドに限定したい場合は、addGet()/addPost()等を使用します。また、複数のHTTPメソッドを指定する場合は、via()を使用します。

// GETのみ
$router->addGet("/products/edit/{id}", "Products::edit");

// POSTのみ
$router->addPost("/products/save", "Products::save");

// POST又はPUT
$router->add("/products/update")->via(array("POST", "PUT"));

パラメータの加工

$router
    ->add('/search/{postcode:[a-z\-]+}', array(
        'controller' => 'search',
        'action'     => 'show'
    ))
    ->convert('postcode', function($slug) {
        // - を取り除く
        return str_replace('-', '', $slug);
    });

この例では、/search/100-6010 にアクセスすると、searchコントローラーのshowメソッドに、「1006010」というパラメーターが渡されます。

続きは(2)で

今回は、基本的なルーティングの定義方法について紹介しました(公式ドキュメントの、Using convertionsまで)。 次回は、Groups of Routesから先、ルーティングをまとめて管理する方法等をみていきます。

このページのトップヘ