Technote

by sizuhiko

CakePHP3 もくもく会#3 に参加して Bake してきた

2013/4/4(金) に コワーキングスペース茅場町 Co-Edo(コエド) で行われた「CakePHP3 もくもく会 #3」に参加しました。

CakePHP3もdev preview2、せっかくの機会なので初Cake3を体験しようということです。

環境構築する

イベントページからリンクされていたCo-Edo謹製Vagrantを使っても良かったのですが、私はCakePHPのコアデベロッパも参加するコミュニティ Friends Of CakeVagrant Chef を使いました。

事前に用意するものは

です。概ね最新版で大丈夫でしょう。 ダウンロード&インストールしたら、任意のディレクトリに先程のVagrant Chefをクローン(またはダウンロード)します。後は

cd vagrant-chef
vagrant up

を実行してしばらく待ちましょう。Chefでインストールされるものは Vagrant Chef のREADMEに書いてあるので、そちらを参照してください。

INFO: Chef Run complete in 541.542816 seconds
INFO: Running report handlers
INFO: Report handlers complete

なメッセージが出て、コンソールが戻ってきたらインストールの完了です。 動作確認として、http://192.168.13.37/ にアクセスしてみてください。

デフォルト画面が出たら環境構築の完了です。

CakePHP3をインストールする

Vagrant ChefにはCakePHPをインストールするレシピも入っているのですが、Friends Of CakeにCakePHPの環境を作るスケルトンコマンドが別途用意されているので、レシピの編集は必要ないです。 生成したVagrant環境にログインします。

vagrant ssh

ログイン直後

vagrant@precise64:/vagrant$ ls -la
total 20
drwxr-xr-x  1 vagrant vagrant  306 Apr 19 05:37 .
drwxr-xr-x 25 root    root    4096 Apr 19 05:37 ..
drwxr-xr-x  1 vagrant vagrant  102 Apr 19 05:37 app
drwxr-xr-x  1 vagrant vagrant  510 Apr 19 05:30 cookbooks
drwxr-xr-x  1 vagrant vagrant  442 Apr 19 05:30 .git
-rw-r--r--  1 vagrant vagrant   12 Apr 19 05:30 .gitignore
-rw-r--r--  1 vagrant vagrant 5489 Apr 19 05:30 README.markdown
drwxr-xr-x  1 vagrant vagrant  102 Apr 19 05:31 .vagrant
-rw-r--r--  1 vagrant vagrant 1179 Apr 19 05:30 Vagrantfile

のようになっていて、すでにappディレクトリが存在します。ここには先程のデフォルト画面が入っているのですが、今回は新しくアプリを作成するのでappディレクトリは削除します。

rm -rf app

環境構築でも利用した Friends Of Cake のリポジトリにはCakePHPのアプリケーションスケルトンを作成する app-template というリポジトリがあります。 すでにCakePHP3用のスケルトンも(cake3ブランチとして)用意されているので、GitHubのREADMEに書いてあるとおりのコマンドでCakePHPのアプリケーション環境を構築します。

composer -sdev create-project friendsofcake/app-template app dev-cake3

app の部分がアプリケーション名です。Vagrant Chefを使った場合はappを指定してください。

Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]? 

最後に上記のような確認メッセージが出てくるので、履歴が気にならなければそのままEnterを押して終了します。 インストールの確認として、http://192.168.13.37/ にアクセスしてみてください。

おなじみのCakePHP画面が表示されます。tmpディレクトリが書き込みできないと警告されます。これはwebサーバのデフォルトuserがvagrantでなくwww-dataであるためですが、開発環境用なの気にせず

sudo chmod -R 777 app/tmp

などのコマンドで権限を書き換えてください。

データベースを設定する

vagrant-chefを使うとデフォルトで databasename, testdatabase_name というデータベースが生成されています。 ただ文字コードの指定などの問題もあると思うので、ここは自分でデータベースを作成します。

mysql -u root -p

vagrant-chefのMySQL rootパスワードは、vagrant-chefのREADMEを参照ください(現時点ではbananasです)。

mysql> CREATE DATABASE cake3_mokumoku CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
mysql> CREATE DATABASE test_cake3_mokumoku CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;

続けておなじみのpostsテーブルを作成します。

mysql> use cake3_mokumoku;
mysql> CREATE TABLE `posts` (
          `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
          `title` varchar(50) DEFAULT NULL,
          `body` text,
          `created` datetime DEFAULT NULL,
          `modified` datetime DEFAULT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

app-templateで作成したCakePHPアプリケーションは、php-dotenvというライブラリを使って環境設定できるようになっています。 app/App/Config/.env.default ファイルをコピーして app/App/Config/.envファイルを作成します。

// app/App/Config/.env
export DATABASE_URL="mysql://my_app:secret@localhost/my_app?encoding=utf8"
export DATABASE_TEST_URL="mysql://my_app:secret@localhost/test_myapp?encoding=utf8"

の部分を

// mysql://ユーザ名:パスワード@URL/データベース名
export CAKEDATABASE_URL="mysql://root:bananas@localhost/cake3_mokumoku?encoding=utf8"
export CAKEDATABASE_TEST_URL="mysql://root:bananas@localhost/test_cake3_mokumoku?encoding=utf8"

のように書き換えます。データベースの詳しい設定方法は AD7six/php-dsnを参照ください。 exportの名前を変更しているのは、nginxのfastcgiパラメータとしてvagrantインストール時に

fastcgi_param DATABASE_URL          "mysql://root:<%= node[:mysql][:server_root_password] %>@localhost/database_name?encoding=utf8";
fastcgi_param DATABASE_TEST_URL     "mysql://root:<%= node[:mysql][:server_root_password] %>@localhost/test_database_name?encoding=utf8";

のような指定がされているため、設定値が重複してしまいfastcgi側が優先される仕組みとなっています。 このため異なる名前に変更しておきます。 合わせて、app.phpでDATABASEURLを利用している箇所もCAKEDATABASEURLに変更します。

// app/Config/app.php#L211-
/**
 * Connection information used by the ORM to connect
 * to your application's datastores.
 */
    'Datasources' => [
        'default' => DbDsn::parse(env('CAKEDATABASE_URL')),

        /**
         * The test connection is used during the test suite.
         */
        'test' => DbDsn::parse(env('CAKEDATABASE_TEST_URL'))
    ],

Cakeシェルを利用する

コンソールから

cd app/App
Console/cake -h

とヘルプを表示してみたいのですが、もし

Exception: Shell class for "-working" could not be found. in [/vagrant/app/vendor/cakephp/cakephp/src/Console/ShellDispatcher.php, line 178]

というエラーが表示されたら、app/App/Console/cake を編集して

exec php -q "$CONSOLE"/cake.php -working "$APP" "$@"

の行を

exec php -q "$CONSOLE"/cake.php "$@"

のように変更してください。現時点では -working オプションがあるとうまく動作しないためです。 うまく動作した場合は

$ Console/cake -h

Welcome to CakePHP v3.0.0-dev2 Console
---------------------------------------------------------------
App : App
Path: /vagrant/app/App/
---------------------------------------------------------------
Current Paths:

* app: App
* root: /vagrant/app
* core: /vagrant/app/vendor/cakephp/cakephp

Available Shells:

[CORE] bake, i18n, server, test

[app] console

To run an app or core command, type cake shell_name [args]
To run a plugin command, type cake Plugin.shell_name [args]
To get help on a specific command, type cake shell_name --help

のように表示されるはずです。

Bakeする

では早速Bakeしましょう

$ Console/cake bake model Posts

Welcome to CakePHP v3.0.0-dev2 Console
---------------------------------------------------------------
App : App
Path: /vagrant/app/App/
---------------------------------------------------------------
One moment while associations are detected.

Baking table class for Posts...

Creating file /vagrant/app/App/Model/Table/PostsTable.php
Wrote `/vagrant/app/App/Model/Table/PostsTable.php`

Baking entity class for Post...

Creating file /vagrant/app/App/Model/Entity/Post.php
Wrote `/vagrant/app/App/Model/Entity/Post.php`

Baking test fixture for Posts...

Creating file /vagrant/app/Test/Fixture/PostFixture.php
Wrote `/vagrant/app/Test/Fixture/PostFixture.php`

Baking test case for App\Model\Table\PostsTable ...

Creating file /vagrant/app/Test/TestCase/Model/Table/PostsTableTest.php
Wrote `/vagrant/app/Test/TestCase/Model/Table/PostsTableTest.php`

うまく動きます!!

続けてコントローラとビューも

$ Console/cake bake controller Posts
$ Console/cake bake view Posts

で生成します。

一通りbakeしたら、http://192.168.13.37/posts/ にアクセスしてみます。

おなじみのBake画面が表示されます。追加、変更、削除などもうまく動作するようです(執筆時においては)。

最後に

このブログはもくもく会から2週間ぐらい経過してしまいましたが、今回再度新しい環境を作って試しています。 もくもく会のときも同様の手順で進めていたのですが、いくらかbake周辺でコードの変更があったようで、うまく動いていた箇所が動かなくなっていたり、動かなかった箇所が動いていたりします。 開発者用のプレビュー2ということで、今後もコードは変わって行くと思いますが、Bakeが動く事で取り急ぎ動作するプログラムのベースは作れるようになっています。 実際にアプリケーションを作ってみてCakePHP3の新機能について感想を書いてみたり、要望を出してみたりすることも実リリースまでの期間としては大切なことだと思うので、今回簡単ではありますがブログとして公開してみました。

Co-Edo では今後も CakePHP3もくもく会が開かれると思うので、イベントページをチェックしておくと良いと思います。私も可能な範囲で参加していこうと思っています。

Composerのautoloadを使いこなす

Composerにはautoloadを自動生成する機能があり、これを利用するとrequire_onceなどを使わなくとも自動的にソースコードがロードされます。言葉のとおりautoloadですね。

例えばCakePHPではApp::uses()という記述で利用するクラスがどこにあるのか識別して、クラスをロードできるようにするのですが、これを使わなくても Composer のautoload機能を使うとクラスが利用可能になります。

app/composer.json に以下のような定義を記述してみましょう。

{
    "autoload": {
        "classmap": ["Model", "Controller"]
    },
}

ここで autoloadだけ を更新するコマンドを実行します。

composer dumpautoload

コマンド名からはちょっと想像がつきにくいのですが、これを実行すると Generating autoload files という結果が表示されてautoload定義が更新されます。

// app/Vendor/composer/autoload_classmap.php
<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'AppController' => $baseDir . '/Controller/AppController.php',
    'AppModel' => $baseDir . '/Model/AppModel.php',
    'PagesController' => $baseDir . '/Controller/PagesController.php',
    'Post' => $baseDir . '/Model/Post.php',
    'PostsController' => $baseDir . '/Controller/PostsController.php',
);

今回は予めpostsテーブルからbakeしてPostモデルとPostsコントローラを生成しておきました。 こうすると、App::uses()を記述しなくても、composerのautoloadがクラス名からPHPファイルを自動解決してくれます。App::uses()の記述が多過ぎてコードの見通しが悪い場合などに利用を検討されてはいかがでしょうか?

この他にもComposerのautoload機能は

  • PSR-4
  • PSR-0
  • Files(ファイルを直接指定できる)
  • include-path(どうしてもphp.ini的なincludeパスで解決しなくてはならない時のためにある)

といったパス解決方法を用意していますので、namespaceが入ったものなどのサポートも万全です。

詳しくは Composerの公式ドキュメントのautoloadを参照ください。

テスト駆動web開発勉強会 Vol.1で発表しました

2014/3/18にコワーキングスペース茅場町Co-Edoで開催された「テスト駆動web開発勉強会 Vol.1」で「Webアプリケーションテスト手法2014」の発表をしました。

第一回目ということと、1時間枠と限られた時間の中でCakePHPとJavaScriptについてのテストの話をしたので、ざっくりした内容となっています。

今後ハンズオンなども計画しているようなので、DoorKeeperのページをチェックしておくと良いかなーと思います。 また当日は「Java,PHPエンジニアの派遣、求人を探すならJapheego(ジャフィーゴ)」さんに懇親会スポンサーになっていただき、美味いビールとSUBWAYのサンドイッチをいただきました。感謝! 勉強会の懇親会はピザが多いのですが、連続すると飽きるのでたまにはSUBWAYのデリバリーとか良いと思いますよ!(SUBWAY大好きです)

To see files inline you need to enable JavaScript.
Yahoo has some instructions for enabling JavaScript if you’re unsure how to do it.

Download this file

最後に、当日紹介したサンプルのCakePHPテストコードを掲載しておきます。

モデルのテスト例:
<?php
class PostTest extends CakeTestCase {
    public $fixture = ['app.Post', 'app.Comment'];

    public function setUp() {
        $this->model = ClassRegistry::init('Post');
        $this->model = $this->getMockForModel('Post');
        $this->model = $this->getMockForModel('Post', ['save']);
    }

    public function testタイトルがない場合はバリデーションエラーになること() {

    }
    public function testデータベースへの保存に失敗した場合はfalseが戻ること() {

    }
    public function testデータベースへの保存に成功した場合はtrueが戻ること() {

    }
    public function testデータベースに新規登録できること() {

    }
}

モデルの初期化方法には3つのやり方があり、通常はClassRegistry::init()で良いのですが、モデルをモックしたい場合はgetMockForModel()というCakeTestCaseが用意してくれているモデル専用のモックジェネレータを使います。第二引数にメソッドの配列を指定するとパーシャルモックとなり、特定のメソッドのみモックします。

コントローラのテスト例:
<?php
class PostsControllerTest extends ControllerTestCase {
    public function setUp() {
        $this->controller = $this->generate('Posts',[
            'components' => [
                'Session',
                'RequestHandler'=>['isPost'],
            ],
            'models' => [
                'Post' => ['findById','delete'],
            ],
            'methods' => [
                'redirect', 'render',
            ],
        ]);
    }

    /**
     * @expectedException NotFoundException
     */
    public function test存在しない記事を表示するとNotFound例外になること() {
        $this->testAction('/posts/view/999');
    }
    /**
     * @expectedException BadRequestException
     */
    public function test保存がPOST以外で呼び出されたらBadResuest例外になること() {
        $this->testAction('/posts/save/1', ['method' => 'get']);
    }
    public function test削除が成功したらindexにリダイレクトする() {
        $this->controller->Post->expects($this->once())->method('delete')
                ->with($this->equalTo(1))->will($this->returnValue(true));
        $this->controller->expects($this->once())->method('redirect')
                ->with($this->equalTo('index'));
        $this->testAction('/posts/delete/1', ['method' => 'delete']);
    }
    public function test新しいトピックを追加できたら画面にメッセージが戻ること() {

    }
}

コントローラのテストをするとき、testActionメソッドを呼び出すわけですが、そのときコントローラ自体をモックするにはControllerTestCaseが用意してくれているコントローラ専用のモックジェネレータであるgenerate()を利用します。generateで作成したインスタンスをテストケースクラスのcontrollerメンバ変数にセットすることでControllerTestCaseがURLからディスパッチするときに、このモックオブジェクトを利用してくれます。

コントローラから依存関係にある、コンポーネント、モデルについてそれぞれサンプルのようにモックでき、コントローラ自体のメソッドもmethodsに列挙することでモックできます。例えばredirect()render()などがよく使われると思います。

CakePHPで動的にプレフィックスルーティングを追加したときに気をつけること

あるURLや特定のパラメータ、ユーザでアクセスされたとき、プレフィックスルーティングを付けたかったので、動的に付与したらハマったので小ネタを書いておきます。

動的にプレフィックスルーティングを追加するには

公式ドキュメントの解説のとおり、

// routes.php
Router::connect(
    "/site-proxy/:controller",
    array('action' => 'index', 'prefix' => 'site-proxy', 'site-proxy' => true)
);

// core.phpなど
Configure::write('Routing.prefixes', array('site-proxy'));

のように記述します。

RouterTest.phpのテストコードを見たら

$request = new CakeRequest();
$request->addParams(array(
    'controller' => 'registrations', 'action' => 'admin_index',
    'plugin' => null, 'prefix' => 'admin', 'admin' => true,
    'ext' => 'html'
));

みたいなコードがあったので、CakeRequest::addParams() を使って追加できることがわかります。

実際コントローラで

$this->request->addParams(['prefix' => 'site-proxy', 'site-proxy' => true]);

みたいに書くと、View上で $this->html->link() などで出力されるURLにプレフィックスルーティングが追加されます。

ハマりポイント

View上でフォームを作成するときに

echo $this->Form->create('Post');

のように書くと思うのですが、このようにモデル名だけが指定されているときCakePHPではどのようにURLが生成されるか、というと

// FormHelper::create() L367付近
if ($options['action'] === null && $options['url'] === null) {
    $options['action'] = $this->request->here(false);
} elseif (empty($options['url']) || is_array($options['url'])) {

URLというオプションを指定しない場合は、 action は CakeRequest::here() の値になることがわかります。

で、CakeRequest::here() とはどんなコードか見てみると

public function here($base = true) {
    $url = $this->here;
    if (!empty($this->query)) {
        $url .= '?' . http_build_query($this->query, null, '&');
    }
    if (!$base) {
        $url = preg_replace('/^' . preg_quote($this->base, '/') . '/', '', $url, 1);
    }
    return $url;
}

のようになっていて、要は現在のURL($this->query)から再生成されるだけです。リクエストに入っているプレフィックスルーティングの設定は参照されず、プレフィックスなしのURLがformタグのactionに入ってしまいます。

まとめ

解決方法としては

  • Form::create() で url オプションを追加する
  • addParams するときに here を変更する

    // 例)
    $request->addParams(array(
        'admin' => true,
        'prefix' => 'admin',
    ))->addPaths(array(
        'here' => '/admin/this/interesting/index',
    ))
    

あたりが良さそうです。まぁ動的にプレフィックス付けるなんてあんまりやらないのかもしれないですが、何かの参考になれば…

VagrantでComposerからPHPUnitをインストールするときに注意したいこと

現在以下のような環境を設置して開発をしています。

  • ホストOS: Windows7
  • 仮想環境: Vagrant + VirtualBox
  • ゲストOS: CentOS 6.4
  • 開発環境: Apache, PHP, MySQL, git, composer, CakePHP, Bdd, …

事件その1: Bddプラグインがインストールできなくなった

ある日、新しくcomposer installで環境を作っていた人が「Bddプラグインがうまくインストールできないんですけど…」 という事で調べていたら Bddプラグインから依存関係にあったライブラリが行方不明になっていました。

それは phpunit/Object_Freezer です。以前はpearにあって、あるときからgithubに移行していたはずなのに、どこにも痕跡を残さず消滅していました。 で、どこで使っているかというと、Bddプラグインの単体テストモジュールにあたるSpec-PHPです。 現在は Spec-PHP に同梱する形で解決しているのですが、以前はcomposer.jsonに

"pear-phpunit/Object_Freezer": "*"

のように指定していました。ちょうど金曜日だったので、週末にこれらを解決してBddプラグインがうまくイントールできるようになった(私の自宅Mac OSX環境+BddExampleAppで確認)と安心して月曜日出社したのです。

事件その2: PHPUnitがインストールできない!

で、Vagrant環境で更新してみたら….

composer update sizuhiko/Bdd

失敗….

しかも全然関係ないところ PHPUnit のインストールで失敗するよ! ちょうどその週末にPHPUnit4.0のコードがGitHubのデフォルトになりはじめたあたりでした。

でcomposer.jsonにはPHPUnitの記述が

"phpunit/phpunit": "3.7.*"

のように入っています。一見PHPUnit4をインストールするわけではないので関係なさそうですが、 composer install を実行すると

.....
  - Installing phpunit/php-token-stream (dev-master 292f4d5)
    Cloning 292f4d5772dad5a12775be69f4a8dd663b20f103

  - Installing phpunit/php-file-iterator (dev-master acd6903)
    Cloning acd690379117b042d1c8af1fafd61bde001bf6bb

  - Installing phpunit/php-code-coverage (1.2.x-dev 3a60a66)
    Cloning 3a60a660998e8d41d5ea81ff8d96ead546bce150

[RuntimeException]
Failed to execute git checkout '3a60a660998e8d41d5ea81ff8d96ead546bce150' &
& git reset -- hard '3a60a660998e8d41d5ea81ff8d96ead546bce150'

error: Untracked working tree file 'Tests/PHP/CodeCoverage/FilterTest,php'
would be overwritten by merge.

のようにエラーになります。

事件その3: Issueが光の速さで取り消される

ともかく事は重大そうなので、PHPUnitにIssue登録した方が良いかなと思ったので、投稿したところ、ものの数秒でそれはcomposerとか環境の問題でPHPUnitじゃないよ、と返ってきました。

Can’t install 3.7.x to Linux environment by composer

ちょっと慌てていたので、あまり確認しなかったのも良くないなと思い、ここから検証作業の開始です。

原因を調べてみた

まず、その週末にPHPUnitのgithubを見たところで、デフォルトブランチ(master)が4.0系になっているよ、という事に気がついていました。

ここで composer がどのようにライブラリを取得するか考えてみたところ、

  • packagist からリポジトリ情報をダウンロード
  • phpunit/phpunit と一致するソースコードリポジトリからダウンロード(この場合はgithub)
  • git clone git@github.com:sebastianbergmann/phpunit.git が実行される
  • リリースを指定しているので一致するタグがチェックアウトされる git checkout ....

この最後のステップで「Untracked working tree file」になり失敗するのです。

PHPUnitのリポジトリを見てみると、3系まではディレクトリ名が Tests だったのですが、 4系では tests のように小文字に変更となっています。大文字、小文字問題か!と意気たったのが、先程の Issue なわけですが。

自分の環境だけかなーと思って、隣の人にちょっとインストールしてみてもらえないか頼んでみたところ、成功…. えぇぇー!

その環境は Vagrant上のCentOSでなく、素のWindowsでした。 何かがおかしい….

そこで偶然なぜか思ったのが、とりあえずディレクトリ変えてみるか、という事。とりわけ何か理由があった訳でなくたまたまそう思っただけなのです。

composer update (この日は何度実行したろう….) …… 成功した!!!!でも、なんで?!

違いに悩むと…

成功したところ:

  • 素のWindows
  • Vagrant仮想環境内の /tmp/work/app

失敗したところ:

  • Vagrantのsyncfolderである /var/www/html/yourapp/app (CentOSから見えるディレクトリ) windows上だと c:¥develop¥your_app¥app みたいなところ

もしかしてVagrantでマウントしているWindowsディスク上だと大文字、小文字の変更をトレースできなくなるのでは!!

という結論に至り、他の人で同様にVagrantのsync_folder環境で試したらダメでした。

まとめ

ホストがwindowsでゲストがLinuxということはあると思うのですが、ディスク同期してそこにソースコードを置いてエディタはホストOS上で実行は仮想環境上みたいな開発するのは今後も増えてくると思います。 その中で今回のような事象に陥ることは composer + PHPUnit に限らず起きそうだなと思いました。

上記のような Failed to execute git checkout ... エラーが出た時に、あぁそういう事かと心構えができていれば慌てず、一旦ホストOS側で作業するとか、仮想環境上のsync_folder以外で作業するなどやりようがあるかな、と思います。

まぁ慌てず騒がず、落ち着いて対応しましょうという良い教訓になりました。twitterなどでお騒がせしてすみませんでした…