RailsアプリをCakePHP3とMigrationsプラグインで移植する

photo

順調に開発が進むCakePHP3、最新のベータ版も公開され正式公開が近づいています。 今回はEngine Yardが公開しているRailsのサンプルアプリをCakePHP3に移植する形で実際に利用するポイントを確認してみます。

行った作業の流れを元にポイントを紹介します。

事前条件

  • アプリケーションは Engine Yardにワンクリックデプロイし、スケールアウト、スケールアップが可能にします。
  • 画像やCSS、JavaScriptは元のアプリのものを流用します。
  • データベーススキーマはRailsアプリと共通とします。

プロジェクト初期化

開発はVagrant上で行いました。CakePHP3からはcomposerを使いプロジェクトを初期化して作業を始めるのが通例です。 またフレームワーク本体やライブラリ、プラグインも全てComposer経由でインストールされます。 これによりリポジトリにはcomposer.jsonとcomposer.lockをコミットし、インストールされたライブラリの実体はコミットしないという形になります。

composer create-project cakephp/app cakephp3_todo

またcomposerはフレームワークのインストール後に初期設定スクリプトを実行します。 これによりconfing/app.default.phpを元にしたconfig/app.phpが生成されます。 app.phpはローカル用などの設定を上書きするファイルとして使い、これもリポジトリにはコミットしない形にしました。 例えばデータベースの接続設定はEngine Yard用の設定を app.default.php に記述し、ローカル環境では app.phpを書き換えてデータベースへ接続するという形です。

  • config/app.default.php(抜粋)
/**
 * Connection information used by the ORM to connect
 * to your application's datastores.
 */
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => $_SERVER['DB_HOST'],
            'username' => $_SERVER['DB_USER'],
            'password' => $_SERVER['DB_PASS'],
            'database' => $_SERVER['DB_NAME'],
            'prefix' => false,
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,

上記のファイルがクラウド環境では app.php として複製され自動的にデータベースへ接続できます。一方でローカル環境ではcomposerが生成した app.php を編集して接続設定をします。

  • config/app.php (コミットしない)
/**
 * Connection information used by the ORM to connect
 * to your application's datastores.
 */
    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'root',
            'password' => '',
            'database' => 'myapp',
            'prefix' => false,
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,

データベーススキーマは再度、Migrationを使って設定しますが一旦、SQLを流し込んで設定します。

コード生成

コード生成にはお馴染みのbakeを利用します。従来はbakeしないでもscaffoldを使う事もできましたが、CakePHP3ではscaffoldはコア機能ではなくなったのでbakeを使ってみることにします。 bakeはデータベースの接続設定が終わっていれば対話的に実行できます。 ./bin/cake bake controller をSSHログインして実行します。

vagrant@precise64:/vagrant_data/cakephp3_todo$ ./bin/cake bake controller

Welcome to CakePHP v3.0.0-beta2 Console
---------------------------------------------------------------
App : src
Path: /vagrant_data/cakephp3_todo/src/
---------------------------------------------------------------
Possible controllers based on your current database:
- Lists
- Tasks

作成済のテーブルにしたがってListsとTasksがサジェストされているのでそれぞれを実行します。

./bin/cake bake controller Lists
./bin/cake bake controller Tasks

同様にビューも生成します。

./bin/cake bake view Lists
./bin/cake bake view Tasks

ここまで実行すると画面からも確認ができるようになります。

photo

モデルの生成とコード変更

次にモデルを生成しますが、ここで問題があります。 今回、データベースにはlistsというテーブルがあり、このテーブルに対してモデルを生成するとListクラスとListsTableクラスが生成されます。 しかしListはPHPの予約語とバッティングしているのでこのままでは実行できなくなります。

今回は生成されたListクラスをListObjectクラスにリネームし、ListsTableクラスに設定を追加する事で任意のクラスをオブジェクトとして使うようにしました。

cakephp3_todo/ListsTable.php at master · yandod/cakephp3_todo

class ListsTable extends Table {
/**
 * Initialize method
 *
 * @param array $config The configuration for the Table.
 * @return void
 */
    public function initialize(array $config) {
        $this->table('lists');
        $this->displayField('name');
        $this->primaryKey('id');
        $this->hasMany('Tasks', [
            'foreignKey' => 'list_id',
        ]);
        $this->entityClass('App\Model\Entity\ListObject');
    }

ロジックの実装

あとはRubyで書かれた元の実装を参考にしてロジックを実装します。 CakePHP3は以前にもましてコードがすくなりなり作業自体は数時間で終了しました。 例としていくつかの箇所を見てみます。

Rubyでは次のようになっていたコントローラーの処理の部分は

class TasksController < ApplicationController
  def index
    @todo   = Task.where(:done => false)
    @task   = Task.new
    @lists  = List.all
    @list   = List.new

    respond_to do |format|
      format.html
      format.json do
        render :json => {:tasks => Task.all.map(&:to_json) }
      end
    end
  end

CakePHP3ではこのようになりました。

<?php
namespace App\Controller;
use App\Controller\AppController;

class TasksController extends AppController {

    public function index() {
        $this->loadModel('Lists');
        $this->set('todo', $this->Tasks->find());
        $this->set('new_task', $this->Tasks->newEntity($this->request->data));
        $this->set('lists', $this->Lists->find());
    }

またListObjectには対応するモデルのデータを遅延取得する為のメソッドを追加しました。

class ListObject extends Entity {

    protected function _getTasks() {
        $tasks = TableRegistry::get('Tasks');
        return $tasks->find('all')
            ->where(['list_id' => $this->id])
            ->all();
    }

    protected function _getDoneTasks() {
        $tasks = TableRegistry::get('Tasks');
        return $tasks->find('all')
            ->where(['list_id' => $this->id, 'done' => true])
            ->all();
    }

}

これらのメソッドはモデルの処理ですが、実際にはビューから実行される形になります。 これはモデルの結果が配列からオブジェクトになったことによるわかりやすい例です。

ビューについてはPHPタグの書式が若干変わっただけど、ほとんど従来と変化はありませんでした。 ブラウザでの表示確認などを行いながらの書き換えは分量は多いものの、単純な作業でした。

データベースマイグレーション

クラウド環境にスキーマを反映させる為には cakephp/migrations プラグインを使います。従来であればSchemaシェルを使うこともできましたが、Migrationsプラグインはより高機能です。

導入にはComposerを使います。また実行後に config/bootstrap.php にプラグインのロード処理を追加します。

composer require cakephp/migrations

MigrationsプラグインはPHPの汎用的なマイグレーションツールであるPhinxのフロントエンドになっています。 実際に記述するマイグレーションファイルもPhinxの書式となります。

まずは./bin/cake migrations create initialとしてマイグレーションファイルを生成し、ひな形の中にコードを記述します。

    /**
     * Migrate Up.
     */
    public function up()
    {
        $lists = $this->table('lists');
        $lists->addColumn('name','string', ['limit' => 20])
            ->addColumn('created_at', 'datetime')
            ->addColumn('updated_at', 'datetime')
            ->save();
        $this->execute("INSERT INTO `lists` VALUES (1,'Welcome','2014-10-27 08:50:02','2014-10-27 08:50:02');");
        $tasks = $this->table('tasks');
        $tasks->addColumn('name','string', ['limit' => 255])
            ->addColumn('done','boolean')
            ->addColumn('created_at', 'datetime')
            ->addColumn('updated_at', 'datetime')
            ->addColumn('list_id', 'integer')
            ->save();
        $this->execute("INSERT INTO `tasks` VALUES (1,'Check out our docs https://support.cloud.engineyard.com/forums',NULL,'2014-10-27 08:50:02','2014-10-27 08:50:02',1),(2,'Follow @EngineYard http://twitter.com/#!/engineyard',NULL,'2014-10-27 08:50:02','2014-10-27 08:50:02',1),(3,'Follow @eycloud http://twitter.com/#!/eycloud',NULL,'2014-10-27 08:50:02','2014-10-27 08:50:02',1),(4,'We blog http://www.engineyard.com/blog/',NULL,'2014-10-27 08:50:02','2014-10-27 08:50:02',1),(5,'Rock on!',NULL,'2014-10-27 08:50:02','2014-10-27 08:50:02',1);");
    }

デプロイ

最後にマイグレーションをクラウド上で自動的に実行する為の処理を追加します。 CLI時に接続を行う為の設定をconfig/bootstrap_cli.phpに記述します。

use Symfony\Component\Yaml\Yaml;
$ey_config = '../../shared/config/database.yml';
if (file_exists($ey_config)) {
    $yaml = Yaml::parse($ey_config);
    $config = $yaml[getenv('PHP_ENV')];
    Configure::write('Datasources.default.host', $config['host']);
    Configure::write('Datasources.default.database', $config['database']);
    Configure::write('Datasources.default.username', $config['username']);
    Configure::write('Datasources.default.password', $config['password']);
}

あとはデプロイフックから./bin/cake migrations migrateを実行するようにデプロイフックを作成します。

on_app_master {
  run! "cd #{config.release_path} && ./bin/cake migrations migrate"
}

ローカルの開発環境ではsshでログインした後にマイグレーションコマンドを自動で実行してください。

まとめ

今回はさらに完成度の上がったCakePHP3の機能を使ってアプリケーションを実装しました。 ドキュメントなども揃っているので十分に必要な挙動を実装できる状況だと感じました。 また新しく導入されたORMやデータベースマイグレーションについてもスムーズに動作させる事ができ、早くも従来の手法を忘れそうなほどです。

今回のサンプルは数クリックでEngine Yardにデプロイできますし、Vagrant上でも動きますのでぜひとも試してみてください。