Phalconモデルまとめ(0)環境構築

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

Phalconルーティングまとめ(2)ルーティングを使いこなす

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

Phalconルーティングまとめ(1)ルーティングの定義

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

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

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

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

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

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

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

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

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


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


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

Mac OS X 10.9(Marvericks)でactiverecordのインストールに失敗する(atomicのビルド失敗)

『Webアプリエンジニア養成読本』のRuby編をやっていたら、Activerecordのインストールでこけました。

環境は、OS X 10.9.2/Xcode5.1。

どうやら、atomicというRuby拡張のビルドに失敗している模様。

色々と原因を探ってみたところ、gcc(clang)のバージョンが原因でした。なお、私の環境のgccのバージョンは以下の通り。
$ which gcc
/usr/bin/gcc
$ gcc -v
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.1 (clang-503.0.38) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.1.0
Thread model: posix

OS Xユーザーが使用しているgccは、XCodeコマンドラインツールとしてインストールしたgccであることが多いと思います。このgcc、コマンドはgccなのですが、gcc -vしてみれば分かるように、実態はclangなのです。

(そもそもgccって何? って人のために説明すると、gccGNU Compiler Collectionの略で、C言語等の各種言語に対応したコンパイラプログラミング言語で書いたソースコードをコンピュータが実行可能なバイナリコードに変換する翻訳機)のこと。clangは、これまたC言語等のコンパイラなのだけど、割と最近出てきたコンパイラで、gccのライバル)

そして、XCode5.1に付属するclangでは、未知のビルドオプションを指定した場合にエラーを吐くようになりました。エラーなので、発生した時点でビルドが強制終了されます。

Xcode 5.1 Release Notes

The Apple LLVM compiler in Xcode 5.1 treats unrecognized command-line options as errors. This issue has been seen when building both Python native extensions and Ruby Gems, where some invalid compiler options are currently specified.

Projects using invalid compiler options will need to be changed to remove those options. To help ease that transition, the compiler will temporarily accept an option to downgrade the error to a warning:

-Wno-error=unused-command-line-argument-hard-error-in-future

このエラーを抑止する方法は、上記引用文に紹介されている「-Wno-error=unused-command-line-argument-hard-error-in-future」オプションをつけてビルドすることです。

このオプションを使用しつつ、ビルドを実行するには、以下のように環境変数をセットしつつbundle installすればOKです。
$ sudo env ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future bundle install

2013-04-06 追記

Nokogiriのインストールの際にも、mini_portileというネイティブ拡張のビルドでこけました。上記と同様、ビルドオプション付きのbundle installしたらOK。OS X 10.9でRubyのネイティブ拡張ビルドに失敗したら、まずは上記オプション付きのビルドを試すのが良さそう。

PHPでメールを送受信するアプリを作った

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。

『実践 Vagrant』を読了






『実践 Vagrant』は、Vagrantの作者であるMitchell Hashimoto氏が自ら著した、Vagrantの解説書。

Vagrantとは何かというと、VirtualBoxを便利に使うためのコマンドラインツール――というのが最初の頃のVagrantだったと思うのだけど、今ではVirtualBoxだけでなくVMWareなどもいけるし、さらにはAmazon EC2インスタンスVagrantで操作することもできる。

要は、仮想マシンの設定と操作を便利に行えるツール、と考えると良いと思う。

仮想マシンを簡単に作っては破棄してが出来るようになって、何がうれしいのかというと、環境構築のコストが非常に小さくなること。VMの設定ファイルであるVagrantfileと、VMのセットアップを行うプロビジョニングツールスクリプトさえ共有すれば、チームで全く同じ環境のVMを簡単に共有できるし、本番環境と同様に動作する開発環境を構築することもできる。

また、チームで開発する場合だけでなく、学習用の環境としても使いやすい。プレーンな状態のOSをさくっと立ち上げて、必要最低限のツールだけをインストールして学習用に使用し、仕事が終わったら破棄しておしまい、みたいなことが簡単にできる。

私の場合、最近は、学習用にはVagrantで作ったVMを使うことがほとんど。学習用の環境はしばらく使ったら捨てるのが前提なのだから、ローカルのOSに入れて環境を汚すより、使い捨ての環境を構築してしまったほうが良い。

共有フォルダやネットワーク関係の設定も、VirtualBoxGUIで行うより、Vagrantfileで行ったほうがわかりやすい。


本書『実践Vagrant』には、私がVagrant関係で知りたいと思っていたことはほとんど載っていた(設定ファイル、プロビジョニング、ネットワーク、ボックスの作り方、プラグイン等)。ただ、一点、「ボックスはどこで入手すべきか」という情報だけは、得ることができなかった。

この疑問に関しては、最近リリースされたVagrant Cloudが一つの答えなのだろう。


『実践Vagrant』は『Vagrant: Up and Running』の翻訳書だが、原著の刊行から訳書の刊行までに古くなってしまった部分は、訳者が適切に補っている。それだけでなく、原著にはない解説も追加されており、今Vagrantの解説書を買うなら、これしかない、という決定版といえる。Vagrantのバージョンアップの早さを考えると、賞味期限は短い(1年程度?)と思われるため、早めに読むのが吉。