Strings of Life

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

タグ:PHP

「最速」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から先、ルーティングをまとめて管理する方法等をみていきます。


「Web技術オーバービュー」と「Webアプリケーション実践入門/PHP編」の感想はこちらの記事に書いています。

「Webアプリケーション実践入門/Ruby編」は、RubyによるWebアプリケーションの構築方法を解説しています。Webアプリケーションフレームワークには、定番のRuby On Railsではなく、マイクロフレームワークのSinatraを使用しています。

PHP編とRuby編のいずれも、マイクロフレームワークを使用して簡単なCRUDのWebアプリケーションを作る実習になっています。この2編を両方やると、PHPとRubyそれぞれの感触がつかめると思います。

両方をやって感じたPHPとRubyの比較は以下のような感じです。

PHPの良いところ
*  環境構築が簡単。ピュアPHPなライブラリが多く、ネイティブ拡張も大抵はOSのパッケージマネージャーからインストールできる。

PHPの悪いところ
* レガシーな環境が残りがち(PHPそれ自体というよりは、外部要因)。

Rubyの良いところ
* エレガントな文法。かゆいところに手が届くクラス。
* ActiveRecordが非常に良い。

Rubyの悪いところ
* gemライブラリは依存関係が激しく、ビルドも失敗しやすい。動かすまでの環境構築が大変。

また、文法については、Rubyは結構自由度が高いのに対して、PHPはあまり融通が効きません。この点、PHPは初心者にやさしく、Rubyは上級者ほどたのしく書ける言語といえるでしょう。


「サーバ環境の作り方」「サービス運用の基礎知識」については、あっさりめですが、Webアプリ制作の初心者が見落としがちな部分でもあるので、結構ためになります。


『Webアプリケーションエンジニア養成読本』全体としては、良い本だと思います。さらに学びを深めるための推薦書籍/WebサイトのリストがあるのもGood。第2章の「Webアプリケーション実践入門」ではレイアウトが見づらく実習を進めるにはストレスが溜まる、という難点がありますが、対象読者に当てはまる人なら、買って損はしません。

OhLifeというWebサービスがある。メールの送受信をフックとしたログ記録サービスで、以下のような特徴がある。

  • 1日1回、決まった時間にメールを送ってくる
  • このメールに返信すると、返信内容が記録される
  • OhLifeが送ってくるメールの本文には、過去のログが含まれる

OhLifeの主な機能には満足しているのだけど、一点だけ不満がある。それは、過去のログの選ばれる規則を設定できないこと。個人的な用途としては、前日からの進捗確認に使いたいので、必ず昨日のログを表示してほしい。しかし、OhLifeの過去ログ抽出規則は、「前日・1週間前・1ヶ月前」等があり、古いものが優先されているように思える。

類似サービスも探したのだが、どうも無いみたい。

無いなら作ろう。

ということで、作ってみた(現在、一般公開はしていない)。


サーバはさくらVPSで、OSはDebian Wheezy。メールサーバとしてはPostfixを使用。今回一番ハマったのはPostfixの設定だったり。

Postfixで受け取ったメールをPHPスクリプトに渡す方法としては、 この記事が参考になる。

また、メールをパースする方法としては、ライブラリを使用することにした。最初は illuminate/mailなども検討したのだけど、サンプルコードが探しやすかったので PEAR::Mail_MimeDecodeにした(サンプルコードは この記事を参考にした)。

インストールはComposerで。

{
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear.php.net/"
        }
    ],
    "require": {
        "pear-pear.php.net/Mail_mimeDecode" : "*"
    }
}

メールを受信するプログラムはこんな感じ(受け取ったメールの本文をDBに保存する)。

<?php

require_once __DIR__ . '/vendor/autoload.php';

$raw_mail = file_get_contents('php://stdin'); // メール本文を標準入力から受け取る

$params = [];
$params['include_bodies'] = true;
$params['decode_bodies']  = true;
$params['decode_headers'] = true;
$params['input'] = $raw_mail;
$params['crlf'] = '\r\n';

$mail_data    = Mail_mimeDecode::decode($params);
$mail_address = $mail_data->headers['from'];
$mail_address = trim(substr($mail_address, strpos($mail_address, '<')), '<>');
$charset      = $mail_data->ctype_parameters['charset'];
$mail_body    = mb_convert_encoding($mail_data->body, 'UTF-8', $charset);

try {
    $dbh = require_once __DIR__ . '/db_connection.php'; // PDOオブジェクトを取得

    // メールアドレスからユーザーを取得
    $sql = 'SELECT * FROM users WHERE mail = :mail';
    $stmt = $dbh->prepare($sql);
    $result = $stmt->execute([':mail' => $mail_address]);

    // DBにメール本文を保存
    if (true === $result) {
        $row = $stmt->fetchAll(PDO::FETCH_ASSOC)[0];
        $sql = 'INSERT INTO logs (users_id, body, created_at) VALUES (:users_id, :body, :created_at)';
        $stmt = $dbh->prepare($sql);
        $stmt->bindParam('users_id', $row['id']);
        $stmt->bindParam('body', $mail_body);
        $stmt->bindParam('created_at', date('Y-m-d H:i:s'));
        $stmt->execute();
    }
} catch (Exception $e) {
    error_log($e->getMessage() . PHP_EOL, 3, __DIR__ . '/log/receiver.log');
    exit(1);
}

メール送信機能はmb_send_mail()使えば簡単。あとは、cronにメール送信スクリプトを登録して、定期実行すればOK。






本書の想定読者は以下のような人です。
  • Webアプリ開発を業務として初めて手がけようとするエンジニアの方
  • Webアプリ開発の全体像を知りたい方
  • Web開発の全体的な知識を初学者に学ばせたい方
  • スタートアップ系企業の方からWeb系企業の新人エンジニアの方まで
こういった初心者の人はもちろんですが、「実業務の経験はあるけど、現場がいまいちモダンじゃない…」というPHPerの人にもピッタリな内容だと思います。


第1章「Web技術オーバービュー」は、初心者でなければ読み飛ばしても良いと思います。逆に初心者は、分からないことがあっても気にせず進みましょう。


第2章「Webアプリケーション実践入門」は、本書のハイライト。

PHP編は「初心者向けだから名前空間は使わないように」といった妥協がなく、モダンなスタイルを貫いている点には好感が持てます(遅延静的束縛やトレイトといった機能をさらっと使ってるのもポイントが高い)。

ただ、対象読者がイマイチわからないですね。PHPのオブジェクト指向構文と名前空間を理解していないとついていけない内容なので、PHP初心者向けとは言いがたい。『パーフェクトPHP』のPart2くらいまでは読んでいないと、「動かす」ことはできても、「なぜそう書くのか理解する」ことはできないでしょう。

初心者よりはむしろ、実務でPHP5.3以前のフレームワーク(CakePHP2/CodeIgniter/symfony等)やSmarty+生PHPなどと格闘している人が、PHP5.5時代のモダンな開発スタイルにキャッチアップするのに向いている、という印象です。

PHP編について、誤記と思われる箇所をリストしておきます(書籍内容に関するお問い合わせでも報告済み)。

p.34 12行目
× # apt-get php5-cli
○ # apt-get install php5-cli

p.40
×リスト4 showメソッド(lib/Base/Controller/TimeLine.php)
○リスト4 showメソッド(lib/Controller/TimeLine.php)

p. 47
×リスト22では、クラス定義の前にuse演算子を用いて〜
○リスト6では、クラス定義の前にuse演算子を用いて〜


ついでに、TinitterをMySQLで動かしたい人向けの設定(Ubuntu12.04/MySQL5.5を想定):

sudo apt-get install mysql-server でインストール。

/etc/mysql/my.cnf を編集して以下の設定を追加。
# [client]の下
default-character-set = utf8

# [mysqld]の下
character-set-server = utf8

設定できたら sudo /etc/init.d/mysql restart でMySQL再起動。

あとは適当にテーブルを作って、サンプルコードに含まれるスキーマファイルを読み込み

mysql -u root -p -e 'CREATE DATABASE tinitter;'
mysql -u root -p tinitter < schema.mysql.sql

config.phpの$db_settingsは以下のような感じで。
$db_settings = [
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'tinitter',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci'
];

PHP編を最後までやった感想としては、レイアウトが酷い。これにつきます。ある部分で説明しているコードの実体が次のページにある、といった現象が頻繁に発生しており、読みづらいことこの上ない。Ruby編はまともなレイアウトです。何故PHP編はこんなに混乱したレイアウトなんでしょう。。。

PHP編までで、結構長くなってしまったので、Ruby編とインフラ編の感想はまた今度。

http://madroom-project.blogspot.jp/2014/03/arrayreduce.html
array_reduce()ってどんな時に使うと便利なのかなー。

演算方法をカスタマイズしたい時に便利。

<?php
$a = array_reduce(range(1, 100), function($result, $item) {
    if ($item % 2 === 0) { // 偶数だけ加算する
        return $result += $item;
    }
    return $result;
});
echo $a, PHP_EOL; // 2550

あと、文字列の操作もできたり

<?php
$arr = ['a', 'b', 'c', 'A', 'B', 'C'];
$a = array_reduce($arr, function($result, $item) {
    if (preg_match('/[a-z]/', $item)) { // 小文字のみ連結
        return $result .= $item;
    }
    return $result;
});
echo $a, PHP_EOL; // abc

連想配列のkeyを判定基準にして足し算したり:http://d.hatena.ne.jp/maeharin/20121108/p1

このページのトップヘ