Engineer as a Lifestyle @tenkoma

What We Find Changes Who We Become -- Peter Morville著『アンビエント・ファインダビリティ 』

CakePHP 3 のチュートリアルにユニットテストを追加する (2)

これは CakePHP Advent Calendar 2017 19日目の記事です。遅れてごめんなさい。 18日目は @neeton_iwasakiさんのCakePHP SocialAuth Pluginプラグイン使用例 | ハックノートでした。

前回の記事 CakePHP 3 のチュートリアルにユニットテストを追加する (1) では、構築したアプリケーションに対して、ユニットテストを書くための準備を行い、テーブル、エンティティー、ルーティングのテストを書きました。

今回は残りのコントローラークラスにテストコードを書いていきましょう。

どんなテストを書くか

チュートリアルでは、 ArticlesController, TagsController, UsersController を作成しました。 すべてのアクションについてテストを書くと説明が重複してしまいます。 そこで、 bake 後のカスタマイズで手を加えた ArticlesController のすべてのアクションと UsersControllerlogin(), logout() アクションにテストを追加します。

  1. ArticlesTable のテスト (1-5, 1-6, 2-5, 2-7) (前回の記事)
  2. User のテスト (2-1) (前回の記事)
  3. Article のテスト (2-6) (前回の記事)
  4. ルーティングのテスト (2-3) (前回の記事)
  5. ログイン・ログアウトのテスト (3-1, 3-2) (この記事でテストを書きます)
  6. ArticlesController のテスト (上記以外全部) (この記事でテストを書きます)

UsersController のテスト

(実装は 認証機能のテストPull Request #17 で確認できます)

ログイン login() のテストを書く

ログインのテストでは以下のテストを書いてみます。

  • ログインページが表示されること
  • 認証失敗したらエラーメッセージを表示する
  • 認証成功したら認証セッションを書き込み、元のページにリダイレクトする

ログインのテストをするためには ユーザー情報の準備が必要です。UsersFixture はダミーのテキストが入ったままなので、テストできるデータに変更します。 ここでは次の init() メソッドを追加してテストデータを生成します。

<?php
// tests/Fixture/UserFixture.php
    public function init()
    {
        $hasher = new DefaultPasswordHasher();
        $this->records = [
            [
                'id' => 1,
                'email' => 'myname@example.com',
                'password' => $hasher->hash('password'),
                'created' => '2017-11-18 11:45:40',
                'modified' => '2017-11-18 11:45:40'
            ],
        ];
        parent::init();
    }

init() メソッドを定義するとプロパティ初期代入より自由にテストデータを生成できます。 UserFixture 全体は以下の通りです。

<?php
// tests/Fixture/UserFixture.php
namespace App\Test\Fixture;

use Cake\Auth\DefaultPasswordHasher;
use Cake\TestSuite\Fixture\TestFixture;

/**
 * UsersFixture
 *
 */
class UsersFixture extends TestFixture
{

    /**
     * Fields
     *
     * @var array
     */
    // @codingStandardsIgnoreStart
    public $fields = [
        'id' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null],
        'email' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null, 'fixed' => null],
        'password' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null, 'fixed' => null],
        'created' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
        'modified' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
        '_constraints' => [
            'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
        ],
        '_options' => [
            'engine' => 'InnoDB',
            'collation' => 'utf8mb4_general_ci'
        ],
    ];
    // @codingStandardsIgnoreEnd

    /**
     * Records
     *
     * @var array
     */
    public $records = [];

    public function init()
    {
        $hasher = new DefaultPasswordHasher();
        $this->records = [
            [
                'id' => 1,
                'email' => 'myname@example.com',
                'password' => $hasher->hash('password'),
                'created' => '2017-11-18 11:45:40',
                'modified' => '2017-11-18 11:45:40'
            ],
        ];
        parent::init();
    }
}

UsersController のテストを書きましょう。 ログイン機能のテストを3つのテストメソッドに定義しました。

<?php
// tests/TestCase/Controller/UsersController.php
namespace App\Test\TestCase\Controller;

use App\Controller\UsersController;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\IntegrationTestCase;

/**
 * App\Controller\UsersController Test Case
 */
class UsersControllerTest extends IntegrationTestCase
{

    /**
     * Fixtures
     *
     * @var array
     */
    public $fixtures = [
        'app.users',
        'app.articles'
    ];

    /**
     * ログインページが表示される
     */
    public function testLoginShow()
    {
        $this->get('/users/login');
        $this->assertResponseOk();
        $this->assertResponseContains('ログイン');
    }

    /**
     * ログイン失敗
     */
    public function testLoginFailed()
    {
        $this->post('/users/login', [
            'email' => 'myname@example.com',
            'password' => 'wrongpassword',
        ]);
        $this->assertResponseOk();
        $this->assertResponseContains('ユーザー名またはパスワードが不正です。');
    }

    /**
     * ログイン成功
     */
    public function testLoginSucceed()
    {
        $this->post('/users/login?redirect=%2Farticles%2Fadd', [
            'email' => 'myname@example.com',
            'password' => 'password',
        ]);
        $this->assertRedirect('/articles/add');
        $this->assertSession(1, 'Auth.User.id');
    }
}

get($url)post($url, $data) を使ってアクションメソッドを実行し、 assertResponseOk()assertResponseContains(...) で結果(応答ヘッダやボディ)を検証しています。

コントローラーのテストについて

このテストコードはコントローラーの統合テストを使って実装しました。いままで実装したテーブルやエンティティーのユニットテストと比べると、テスト対象のオブジェクト生成や、変数への代入がありません。それらはすべてコントローラーの統合テストのために用意されたメソッド( get()post() 、各種アサーション)でラップされています。

このような仕組みは、テストコードを読みやすく簡潔にしやすい利点があります。一方で、処理やデータが隠されているため、テストコードを書きにくく感じることがあるかもしれません。その場合、 IntegrationTestCase のコードを読むと、軽減できるかと思います。

ログアウト logout() のテストを書く

<?php
// tests/TestCase/Controller/UsersController.php
// ... 略
    /**
     * ログアウト
     */
    public function testLogout()
    {
        $this->session(['Auth.User.id' => 1]);

        $this->get('/users/logout');

        $this->assertSession([], 'Auth');
        $this->assertRedirect('/users/login');
    }
// ... 略

$this->session(...) で認証済み時のセッションデータを作り、 logout アクションを実行後の振る舞いを検証します。

ArticlesController のテスト

ArticlesController には6つのアクションメソッドがあります。まずはbakeでテストケースクラスを生成します。

$ bin/cake bake test Controller ArticlesController

記事一覧 index()

(実装は テスト追加: 記事一覧を表示 Pull Request #18 で確認できます)

  • 各記事のタイトルが表示されること

をテストします。元々あった testIndex() メソッドを以下に置き換えてください。

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php

    public function test記事一覧を表示()
    {
        $this->get('/articles');
        $this->assertResponseOk();
        $this->assertResponseContains('CakePHP3 チュートリアル');
        $this->assertResponseContains('Happy new year');
    }

記事詳細 view()

(実装は テスト追加: 記事詳細を表示 Pull Request #19 で確認できます)

  • 詳細ページが表示されること
  • 詳細ページが存在しないとき404になること

をテストします。 testView() を以下でいれかえてください。

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php
    public function test記事詳細ページを表示()
    {
        $this->get('/articles/view/CakePHP3-chutoriaru');

        $this->assertResponseOk();
        $this->assertResponseContains('CakePHP3 チュートリアル'); // title
        $this->assertResponseContains('このチュートリアルは簡単な ' .
            'CMS アプリケーションを作ります。'); // body
    }

    public function test記事詳細ページが存在しない()
    {
        $this->get('/articles/view/not-found-articles');

        $this->assertResponseCode(404);
    }

ちなみに、 articles テーブルには published カラムがありますが、チュートリアルではフィルタを実装していません。 published = 1 の記事だけ表示するロジックを追加したら、それに対するテストを書いてもいいでしょう。

記事追加 add()

(実装は テスト追加: 記事追加 Pull Request #20 で確認できます)

  • 記事追加ページにアクセスできる
  • 記事が追加されると、記事一覧にリダイレクトする
  • バリデーションエラーだと追加できず、エラーメッセージが表示される

をテストします。

記事を追加するには、 users テーブルが必要なため、フィクスチャーに追加しておきます。

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php
    public $fixtures = [
        'app.users', // この行を追加
        'app.articles',
        'app.tags',
        'app.articles_tags'
    ];

準備できたので、 testAdd() を以下でいれかえてください。

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php
    public function test記事追加ページにアクセスできる()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->get('/articles/add');

        $this->assertResponseOk();
        $this->assertResponseContains('記事の追加');
    }

    public function test記事が追加されると記事一覧にリダイレクトする()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->post('/articles/add', [
            'title' => 'Nintendo Switch を購入!',
            'body' => 'クリスマスプレゼントとして買った',
            'tag_string' => 'game,2017',
        ]);

        $this->assertSession('Your article has been saved.', 'Flash.flash.0.message');
        $this->assertRedirect('/articles');

        $this->get('/articles');
        $this->assertResponseContains('Nintendo Switch を購入!');
    }

    public function testバリデーションエラーだと追加できずエラーメッセージが表示される()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->post('/articles/add', [
            'title' => 'Nintendo Switch を購入!',
            'body' => '',
            'tag_string' => '',
        ]);

        $this->assertResponseOk();
        $this->assertResponseContains('Unable to add your article.');

        $this->get('/articles');
        $this->assertResponseNotContains('Nintendo Switch を購入!');
    }

記事の追加はログインしないとできません。 $this->session(['Auth.User.id' => 1]); で認証済みの状態にします。

記事編集 edit()

(実装は テスト追加: 記事編集 Pull Request #21 で確認できます)

  • 記事編集ページにアクセスできる
  • 記事を更新し、その後、記事一覧にリダイレクトする
  • バリデーションエラーだと更新できず、エラーメッセージが表示される

をテストします。testEdit() を以下でいれかえてください。

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php
    public function test記事編集ページにアクセスできる()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->get('/articles/edit/CakePHP3-chutoriaru');

        $this->assertResponseContains('記事の編集');
        $this->assertResponseContains('CakePHP3 チュートリアル');
    }

    public function test記事を更新し記事一覧にリダイレクトする()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->post('/articles/edit/CakePHP3-chutoriaru', [
            // タイトルを変更する
            'title' => '1時間で分かるCakePHP3 チュートリアル',
            'body' => 'このチュートリアルは簡単な CMS アプリケーションを作ります。 はじめに CakePHP のインストールを行い、データベースの作成、 そしてアプリケーションを素早く仕上げるための CakePHP が提供するツールを使います。',
            'tag_string' => 'PHP,CakePHP',
        ]);
        $this->assertRedirect('/articles');
        $this->assertSession('Your article has been updated.', 'Flash.flash.0.message');

        $this->get('/articles');
        $this->assertResponseContains('1時間で分かるCakePHP3 チュートリアル');
    }

    public function testバリデーションエラーだと更新できずエラーメッセージが表示される()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->post('/articles/edit/CakePHP3-chutoriaru', [
            // タイトルを変更する
            'title' => '1時間で分かるCakePHP3 チュートリアル',
            'body' => '',
        ]);
        $this->assertResponseOk();
        $this->assertResponseContains('Unable to update your article.');

        $this->get('/articles');
        $this->assertResponseNotContains('1時間で分かるCakePHP3 チュートリアル');
    }

ほとんど記事の追加と一緒です。

記事削除 remove()

(実装は テスト追加: 記事削除 Pull Request #22 で確認できます)

  • 記事を削除する。その後、記事一覧にリダイレクトする
  • getリクエストの場合削除しない

をテストします。testDelete() を以下でいれかえてください。

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php
    public function test記事を削除してその後記事一覧にリダイレクトする()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->post('/articles/delete/CakePHP3-chutoriaru');

        $this->assertRedirect('/articles');

        $this->get('/articles');
        $this->assertResponseNotContains('CakePHP3 チュートリアル');
    }

    public function testGetリクエストの場合削除しない()
    {
        $this->session(['Auth.User.id' => 1]);
        $this->get('/articles/delete/CakePHP3-chutoriaru');

        $this->assertResponseCode(405);
        $this->get('/articles');
        $this->assertResponseContains('CakePHP3 チュートリアル');
    }

タグ検索 tags()

(実装は テスト追加: タグ検索 Pull Request #23 で確認できます)

  • 複数タグを指定してアクセス
  • 存在しないタグを指定してアクセス

をテストします。testTags() を以下でいれかえてください。

<?php
// tests/TestCase/Controller/ArticlesControllerTest.php
    public function test複数タグを指定してアクセス()
    {
        $this->get('/articles/tagged/php/cakephp');

        $this->assertResponseOk();
        $this->assertResponseRegExp('/Articles tagged with\s+php or cakephp/m');
        $this->assertResponseContains('CakePHP3 チュートリアル');
    }

    public function test存在しないタグを指定してアクセス()
    {
        $this->get('/articles/tagged/undefined-tag');

        $this->assertResponseOk();
        $this->assertResponseRegExp('/Articles tagged with\s+undefined-tag/m');
        $this->assertResponseContains('記事が見つかりませんでした。');
    }

この記事では基本的にプロダクションコードを変更してません。タグ検索の場合、存在しないタグを指定した場合に、記事一覧の部分に何も表示されないので、「記事が見つかりませんでした。」と表示して、その表示をテストしました。テンプレートを以下のように修正してください。

<?php
// src/Template/Articles/tags.ctp
/**
 * @var array $tags
 * @var App\Model\Entity\Article[] $articles
 */
?>
<h1>
    Articles tagged with
    <?= $this->Text->toList(h($tags), 'or') ?>
</h1>

<section>
    <?php if ($articles->count() === 0): ?>
    <p>記事が見つかりませんでした。</p>
    <?php endif; ?>
    <?php foreach ($articles as $article): ?>
        <article>
            <!-- リンクの作成に HtmlHelper を使用 -->
            <h4><?= $this->Html->link(
                    $article->title,
                    ['controller' => 'Articles', 'action' => 'view', $article->slug]
                ) ?></h4>
            <span><?= h($article->created) ?>
        </article>
    <?php endforeach; ?>
</section>

まとめ

CMSチュートリアルで実装したコントローラーにテストを書く方法を紹介しました。

CakePHP Advent Calendar 2017 19日目の記事でした。

付録・CircleCI で継続的インテグレーションする設定

(実装は CircleCI セットアップ Pull Request #11 で確認できます)

今回、テスト実装はメソッドごとにPull Requestで分けていました。(Pull Requests · tenkoma/cakephp_cms)そして、テストを実装し始める前にCircleCI でPush毎に全テストを実行する準備をしました。

CircleCIのセットアップ手順は省略しますが、CakePHP3プロジェクトをCircleCIでテストするための設定ファイルはcakephp_cms/config.yml at masterで確認できます。

テストコードが増えていくと、毎回ローカルで全テストを実行するのは大変です。CircleCIやTravisCIなどを使って自動化する方法をとると、楽になるでしょう。

CakePHP 3 のチュートリアルにユニットテストを追加する (1)

これは CakePHP Advent Calendar 2017 2日目の記事です。 1日目は @kunitさんのCakePHPの過去、現在、そして未来 - Qiitaでした。

1ヶ月ほど前に CakePHP 3.x ドキュメントCMS チュートリアルというページが追加されました。以前ブログチュートリアルとブックマークチュートリアルというコンテンツがありましたが、説明に重複する部分があるので統合されたもののようです。 CakePHP 3の機能をざっくり把握するためによい資料かと思います。 ところでこのチュートリアルではユニットテストに触れていませんが、高品質なアプリケーション開発のためには必須です。 自分の知識を整理するために、CMSチュートリアルにユニットテストを書いてみました。 今回はコントローラーの統合テスト以外について説明します。

(現在でもナビゲーションからは辿れませんが、見ることは可能です: ブックマークチュートリアル - 3.x, ブログチュートリアル - 3.x)

ユニットテストを実行する準備

PHPUnit をインストールする

$ composer require --dev "phpunit/phpunit"

composer.phar がある場合は、 php composer.phar require --dev "phpunit/phpunit" でインストールします

ユニットテスト環境向けデータベース作成

開発環境と同じMySQLサーバーにテスト環境向けデータベースを作成します。

CREATE DATABASE test_cake_cms CHARACTER SET utf8mb4;
GRANT ALL  ON test_cake_cms.* TO cakephp@localhost IDENTIFIED BY "AngelF00dC4k3~";
FLUSH PRIVILEGES;

app.php 変更

作成したデータベースと権限を app.php に反映します。

<?php
// config/app.php
// 略
    'Datasources' => [
        // 'default' => [ ... は省略

        /**
         * The test connection is used during the test suite.
         */
        'test' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'cakephp',  // ここを変更
            'password' => 'AngelF00dC4k3~',  // ここを変更
            'database' => 'test_cake_cms',  // ここを変更
            'encoding' => 'utf8mb4',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
            'quoteIdentifiers' => false,
            'log' => false,
            //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
            'url' => env('DATABASE_TEST_URL', null),
        ],
    ],

どんなテストを書くか

CMS チュートリアルで実装した機能を箇条書きにしました。(番号は独自で付けたものです)

1 Articles コントローラーの作成

  • 1-1 コントローラー ArticlesController:index
  • 1-2 コントローラー ArticlesController:view
  • 1-3 コントローラー ArticlesController:add (2-2, 3-3, 3-4 で拡張)
  • 1-4 コントローラー ArticlesController:edit (2-2, 3-3, 3-4 で拡張)
  • 1-5 テーブル ArticlesTable save でスラグ生成
  • 1-6 テーブル ArticlesTable バリデーション
  • 1-7 コントローラー ArticlesController:delete

2 タグとユーザー

  • 2-1 エンティティー User のパスワードをハッシュ化
  • 2-2 コントローラー ArticlesController:add/edit 記事追加・編集時にタグ付け
  • 2-3 ルーティング tags アクションのためのカスタムルーティング
  • 2-4 コントローラー ArticlesController:tags タグによる記事の検索
  • 2-5 テーブル ArticlesTable:findTagged(ファインダーメソッド)
  • 2-6 エンティティー Article:_getTagString(計算フィールド)
  • 2-7 テーブル Article:_buildTags(タグ文字列の永続化)

3 認証

  • 3-1 コントローラー UsersController:login ログイン
  • 3-2 コントローラー UsersController:logout ログアウト
  • 3-3 コントローラー ArticlesController::isAuthorized ... add, tags, edit, などでの認可ロジック
  • 3-4 コントローラー ArticlesController::add と edit アクションで固定値だった user_id を実際のユーザーIDに

上のようにまとめたところ、記事の一覧・詳細・追加・編集・削除、記事に関連するタグ、認証(ログイン・ログアウト)、認可の機能を実装していますので、それに伴うユニットテストを以下の順で実装していくことにします。

  1. テーブル ArticlesTable のテスト (1-5, 1-6, 2-5, 2-7)
  2. エンティティー User のテスト (2-1)
  3. エンティティー Article のテスト (2-6)
  4. ルーティングのテスト (2-3)
  5. ログイン・ログアウトのテスト (3-1, 3-2)
  6. ArticlesController のテスト (上記以外全部)

不足するFixture 作成

そのままテストを実行すると、フィクスチャーファイルが無くてエラーになるので、生成しておきます。

$ bin/cake bake fixture Articles
$ bin/cake bake fixture ArticlesTags

vendor/bin/phpunit でテスト実行して、以下のように表示されたら準備完了です。

$ vendor/bin/phpunit
PHPUnit 6.4.4 by Sebastian Bergmann and contributors.

......IIIIIIIIIIIIIIII                                            22 / 22 (100%)

Time: 1.8 seconds, Memory: 14.00MB

OK, but incomplete, skipped, or risky tests!
Tests: 22, Assertions: 34, Incomplete: 16.

f:id:tenkoma:20171202153727p:plain

実行ログで I と表示されているのは、実装していないテストがあることを示しています。テストケースクラスを bake で生成したときは、実装済みのメソッドに対してテストメソッドが生成されますが、内容が以下のようになっています。

<?php
// 省略
$this->markTestIncomplete('Not implemented yet.');

テスト失敗ではないのでこのチュートリアルでは無視します。 この記事で書いたコードについてはすべてGitHub tenkoma/cakephp_cmsで公開しています。

テスト実装

ArticlesTable のテスト (1-5, 1-6, 2-5, 2-7)

(テスト実装はArticlesTable のテスト Pull Request #13 で確認できます)

バリデーションのテスト

ArticlesTable にバリデーションルールを定義した(1-6)のでテストします。

<?php
// 省略
    public function validationDefault(Validator $validator)
    {
        $validator
            ->notEmpty('title')
            ->minLength('title', 10)
            ->maxLength('title', 255)

            ->notEmpty('body')
            ->minLength('body', 10);

        return $validator;
    }

src/Model/Table/ArticlesTable.php のテストは慣習として tests/TestCase/Model/Table/ArticlesTableTest.php に書きます。 UsersTableTest.php がすでにありますが、 ArticlesTableTest.php はありません。これは ArticlesTable.php を bake コマンドを使わずに作成したからです。 テストケースクラスだけを生成することもできるので、生成します。

$ bin/cake bake test Table ArticlesTable

生成されたテストケースクラスを見ると、setUp()ArticlesTable が初期化されています。

<?php
// 省略
    /**
     * setUp method
     *
     * @return void
     */
    public function setUp()
    {
        parent::setUp();
        $config = TableRegistry::exists('Articles') ? [] : ['className' => ArticlesTable::class];
        $this->ArticlesTable = TableRegistry::get('Articles', $config);
    }

この $this->ArticlesTable を使ってバリデーションのテストを実装していきます。 ArticlesTableTest にこのままテストを追加してもよいですが、テスト結果がずっと黄色なのも面白くないので、中身が markTestIncomplete(...) なメソッドはすべて削除して以下のようにします。(実際は、テストに置き換えることで未実装のテストを無くす方がよいです)

<?php
// tests/TestCase/Model/Table/ArticlesTableTest.php
namespace App\Test\TestCase\Model\Table;

use App\Model\Table\ArticlesTable;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;

/**
 * App\Model\Table\ArticlesTable Test Case
 */
class ArticlesTableTest extends TestCase
{

    /**
     * Test subject
     *
     * @var \App\Model\Table\ArticlesTable
     */
    public $ArticlesTable;

    /**
     * Fixtures
     *
     * @var array
     */
    public $fixtures = [
        'app.articles',
        'app.tags',
        'app.articles_tags'
    ];

    /**
     * setUp method
     *
     * @return void
     */
    public function setUp()
    {
        parent::setUp();
        $config = TableRegistry::exists('Articles') ? [] : ['className' => ArticlesTable::class];
        $this->ArticlesTable = TableRegistry::get('Articles', $config);
    }

    /**
     * tearDown method
     *
     * @return void
     */
    public function tearDown()
    {
        unset($this->ArticlesTable);

        parent::tearDown();
    }
}

準備できたのでバリデーションのためのテストを実装します。

<?php
// tests/TestCase/Model/Table/ArticlesTableTest.php
// 省略
    public function testValidationDefault()
    {
        // エラーが無いとき
        $article = $this->ArticlesTable->newEntity([
            'title' => str_repeat('a', 10),
            'body' => str_repeat('b', 256),
        ]);
        $expected = [];
        $this->assertSame($expected, $article->getErrors());

        // 必須項目が空のとき
        $emptyArticle = $this->ArticlesTable->newEntity([
            'title' => '',
            'body' => '',
        ]);
        $expected = [
            'title' => ['_empty' => 'This field cannot be left empty'],
            'body' => ['_empty' => 'This field cannot be left empty'],
        ];
        $this->assertSame($expected, $emptyArticle->getErrors());

        // 文字数が少ないとき
        $lessArticle = $this->ArticlesTable->newEntity([
            'title' => str_repeat('a', 9),
            'body' => str_repeat('b', 9),
        ]);
        $expected = [
            'title' => ['minLength' => 'The provided value is invalid'],
            'body' => ['minLength' => 'The provided value is invalid'],
        ];
        $this->assertSame($expected, $lessArticle->getErrors());

        // 文字数が多いとき
        $moreArticle = $this->ArticlesTable->newEntity([
            'title' => str_repeat('a', 256),
            'body' => str_repeat('b', 256),
        ]);
        $expected = [
            'title' => ['maxLength' => 'The provided value is invalid'],
        ];
        $this->assertSame($expected, $moreArticle->getErrors());
    }

バリデーション結果は newEntitypatchEntity を呼び出して取得したエンティティーから getErrors() で取り出します。 バリデーションにパスする場合としない場合3パターンの計4パターンをテストしています。 テストを実行した結果は以下になります。

f:id:tenkoma:20171202154242p:plain

グリーンバーが表示されたらテスト成功です。

記事保存のテスト

記事 (Article エンティティー)を保存するときは以下の処理を行っています

  • タグ文字列をエンティティーに変換
  • 記事スラグ生成

記事更新時はスラグが変更されない、という違いがあるので、testSaveInserttestSaveUpdate に分けて実装します。

<?php
// tests/TestCase/Model/Table/ArticlesTableTest.php
// 省略
    /**
     * articles 追加
     */
    public function testSaveInsert()
    {
        $newArticle = $this->ArticlesTable->newEntity([
            'user_id' => 1,
            'title' => 'CakePHP テスト',
            'body' => str_repeat('🍺', 10),
            'tag_string' => 'PHP',
        ]);
        $this->ArticlesTable->save($newArticle);

        $article = $this->ArticlesTable->get($newArticle->id, [
            'contain' => ['tags'],
        ]);

        // スラグ
        $this->assertSame('CakePHP-tesuto', $article->slug);

        // タグに変換
        $this->assertSame('PHP', $article->tags[0]->title);
    }

testSaveInsert() を追加してテスト実行すると以下のようにエラーなります。

There was 1 error:

1) App\Test\TestCase\Model\Table\ArticlesTableTest::testSaveInsert
PDOException: SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`test_cake_cms`.`articles`, CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)

/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/Database/Statement/MysqlStatement.php:39
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/Database/Connection.php:314
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/Database/Query.php:214
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/ORM/Table.php:1925
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/ORM/Table.php:1819
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/ORM/Table.php:1732
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/ORM/Table.php:1455
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/Database/Connection.php:681
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/ORM/Table.php:1456
/Users/tenkoma/projects/sample_projects/cms/vendor/cakephp/cakephp/src/ORM/Table.php:1733
/Users/tenkoma/projects/sample_projects/cms/tests/TestCase/Model/Table/ArticlesTableTest.php:108

ERRORS!
Tests: 2, Assertions: 4, Errors: 1.

articles テーブルには外部キー制約があり、指定の users レコードがないのでレコード追加ができない、というエラーです。users テーブルと、users.id = 1 のレコードがあればよいので、 $fixtures'app.users' を追加します。 フィクスチャーとはユニットテストのためにテーブルとテストデータを用意するための仕組みです。

<?php
// tests/TestCase/Model/Table/ArticlesTableTest.php
// 省略
    public $fixtures = [
        'app.articles',
        'app.tags',
        'app.articles_tags',
        'app.users', // この行を追加
    ];

これでテストは成功します。 更新時にスラグが変更されないことを以下のように実装します。 現時点ではフィクスチャーのデータにはデフォルトのダミー文字列が入っているので、アプリケーション向きのデータに変更します。

<?php
// tests/Fixture/ArticlesFixture.php
// 省略
    public $records = [
        [
            'id' => 1,
            'user_id' => 1,
            'title' => 'CakePHP3 チュートリアル',
            'slug' => 'CakePHP3-chutoriaru',
            'body' => 'このチュートリアルは簡単な CMS アプリケーションを作ります。 はじめに CakePHP のインストールを行い、データベースの作成、 そしてアプリケーションを素早く仕上げるための CakePHP が提供するツールを使います。',
            'published' => 1,
            'created' => '2017-11-19 11:04:25',
            'modified' => '2017-11-19 11:04:25'
        ],
    ];
    // 省略
<?php
// tests/TestCase/Model/Table/ArticlesTableTest.php
// 省略
    public function testSaveUpdate()
    {
        $article = $this->ArticlesTable->get(1);
        $this->assertSame('CakePHP3-chutoriaru', $article->slug);
        $article = $this->ArticlesTable->patchEntity($article, [
            'title' => 'CakePHP3 Tutorial',
        ]);
        $this->ArticlesTable->save($article);

        $newArticle = $this->ArticlesTable->get(1);

        // title が変わってもスラグは変化しない
        $this->assertSame('CakePHP3 Tutorial', $newArticle->title);
        $this->assertSame('CakePHP3-chutoriaru', $newArticle->slug);
    }

カスタムファインダーのテスト

2-5 で作成した findTagged メソッドは find('tagged', [...]) という感じで呼び出して利用します。 これをテストするにはテストデータの準備ができていないので、フィクスチャーを以下のようにカスタマイズします。

<?php
// tests/Fixture/ArticlesFixture.php
// 省略
    public $records = [
        [
            // タグあり
            'id' => 1,
            'user_id' => 1,
            'title' => 'CakePHP3 チュートリアル',
            'slug' => 'CakePHP3-chutoriaru',
            'body' => 'このチュートリアルは簡単な CMS アプリケーションを作ります。 はじめに CakePHP のインストールを行い、データベースの作成、 そしてアプリケーションを素早く仕上げるための CakePHP が提供するツールを使います。',
            'published' => 1,
            'created' => '2017-11-19 11:04:25',
            'modified' => '2017-11-19 11:04:25'
        ],
        [
            // タグなし
            'id' => 2,
            'user_id' => 1,
            'title' => 'Happy new year',
            'slug' => 'Happy-new-year',
            'body' => '2018🍺🍺🍺🍺🍺',
            'published' => 1,
            'created' => '2017-11-19 11:04:25',
            'modified' => '2017-11-19 11:04:25'
        ],
    ];
    // 省略
<?php
// tests/Fixture/TagsFixture.php
// 省略
    public $records = [
        [
            'id' => 1,
            'title' => 'PHP',
            'created' => '2017-11-18 12:15:34',
            'modified' => '2017-11-18 12:15:34'
        ],
        [
            'id' => 2,
            'title' => 'CakePHP',
            'created' => '2017-11-18 12:15:34',
            'modified' => '2017-11-18 12:15:34'
        ],
        [
            'id' => 3,
            'title' => 'Bakery',
            'created' => '2017-11-18 12:15:34',
            'modified' => '2017-11-18 12:15:34'
        ],
    ];
    // 省略
<?php
// tests/Fixture/ArticlesTagsFixture.php
// 省略
    public $records = [
        [
            'article_id' => 1,
            'tag_id' => 1
        ],
        [
            'article_id' => 1,
            'tag_id' => 2
        ],
    ];
    // 省略

タグありの記事検索とタグなしの記事検索を以下のようにテストします。

<?php
// tests/TestCase/Model/Table/ArticlesTableTest.php
// 省略
    public function testFindTagged()
    {
        // タグなし
        $notTaggedArticle = $this->ArticlesTable
            ->find('tagged', ['tags' => []])
            ->contain(['Tags'])
            ->first();
        $this->assertEmpty($notTaggedArticle->tags);

        // タグあり
        $taggedArticle = $this->ArticlesTable
            ->find('tagged', ['tags' => ['PHP']])
            ->contain(['Tags'])
            ->first();
        $tags = new \Cake\Collection\Collection($taggedArticle->tags);
        $this->assertNotEmpty($tags->filter(function($tag) {
            return $tag->title === 'PHP';
        }));
    }

以上で ArticlesTable に実装したコードのテストは終わります。

User エンティティーのテスト (2-1)

(テスト実装はUser エンティティーのテスト Pull Request #14 で確認できます)

ユーザー追加時に、パスワードハッシュ化の追加の処理を User エンティティーに実装しましたのでこちらをテストします。

User エンティティーのテストケースクラスを以下のコマンドで生成します。

$ bin/cake bake test Entity User

テストコードを実装します。

<?php
// tests/TestCase/Model/Entity/UserTest.php
// 省略
    public function testSetPassword()
    {
        $rawPassword = 'secret';
        $this->User->password = $rawPassword;
        $hashedPassword = $this->User->password;

        // ハッシュ化済み
        $this->assertNotSame($rawPassword, $hashedPassword);

        $hasher = new DefaultPasswordHasher();
        $this->assertTrue($hasher->check($rawPassword, $hashedPassword));
    }

User::_setPassword() では DefaultPasswordHasher::hash() でハッシュ化文字列を求めていますが、このハッシュ化文字列は毎回変化するため、テストコードでは使えません。代わりに DefaultPasswordHasher::check() を使って検証します。ログイン機能を実装するときは AuthComponent がデフォルトでやってくれています。

Article エンティティーのテスト (2-6)

(テスト実装はArticle エンティティーのテスト Pull Request #15で確認できます)

計算フィールドの追加で、Article に関連付いたタグ文字列を直接取得できるようにしましたので、テストを追加します。まず bake します。

$ bin/cake bake test Entity Article

生成された ArticleTest.php から testInitialize を削除して以下を追加します。

<?php
// tests/TestCase/Model/Entity/ArticleTest.php
// 省略
    /**
     * @dataProvider dataTestTagString
     */
    public function testTagString($tags, $expected)
    {
        $tagEntities = [];
        foreach ($tags as $tagTitle) {
            $tagEntities[] = new Tag(['title' => $tagTitle]);
        }
        $article = new Article(['tags' => $tagEntities]);
        $this->assertSame($expected, $article->tag_string);
    }

    public function dataTestTagString()
    {
        return [
            [[''], ''],
            [['Torte'], 'Torte'],
            [['Torte', 'Financier', 'Macaron'], 'Torte, Financier, Macaron'],
        ];
    }

タグが0個、1個、複数個の場合をテストするためにPHPUnitのデータプロバイダ機能で、テストデータをまとめてみました。

ルーティングのテスト (2-3)

(テスト実装はルーティングのテスト Pull Request #16 で確認できます)

タグによる記事の検索では、タグ付けされた記事をURLから検索できるようにするために config/routes.php でカスタマイズしています。このファイルはクラスではありませんがテストできます。チュートリアルではまだ個数が少ないのでバグになりにくいですが、書いておくと、記述を増やすとき、既存のルーティングを壊さないかの確認ができます。

<?php
// tests/TestCase/Routing/RoutingTest.php
// 省略
<?php
namespace App\Test\TestCase\Routing;

use Cake\Http\ServerRequest;
use Cake\Routing\Router;
use Cake\TestSuite\TestCase;

class RoutingTest extends TestCase
{
    /**
     * 正引き ('/url' => 配列)
     * @dataProvider dataTestRouting
     * @param string $url
     * @param array $expected
     * @param array $expectedPass
     */
    public function testRoute($url, $expected, $expectedPass=[])
    {
        $expected['pass'] = $expectedPass;
        $actual = Router::parseRequest(new ServerRequest($url));
        $this->assertSame($actual['controller'], $expected['controller']);
        $this->assertSame($actual['action'], $expected['action']);
        $this->assertSame($actual['pass'], $expected['pass']);
    }

    /**
     * 逆引き (配列 => '/url')
     * @dataProvider dataTestRouting
     * @param string $expected
     * @param array $parsedArray
     */
    public function testReverseRoute($expected, $parsedArray)
    {
        $this->assertSame($expected, Router::url($parsedArray));
    }

    public function dataTestRouting()
    {
        return [
            [
                '/articles/tagged',
                ['controller' => 'Articles', 'action' => 'tags'],
            ],
            [
                '/articles/tagged/funny/cat/gifs',
                ['controller' => 'Articles', 'action' => 'tags', 'funny', 'cat', 'gifs'],
                ['funny', 'cat', 'gifs'],
            ],
            [
                '/articles',
                ['controller' => 'Articles', 'action' => 'index'],
            ],
            [
                '/articles/add',
                ['controller' => 'Articles', 'action' => 'add'],
            ],
            [
                '/',
                ['controller' => 'Pages', 'action' => 'display', 'home'],
                ['home'],
            ],
        ];
    }
}

config/routes.php をテストするときは Router クラスを使ってテストします。 testRoute() でURLから配列への変換を、 testReverseRoute() で配列からURLへの変換をテストしています。 一度仕組みを作れば、その後はテストデータを追加するだけなので、おすすめです。

参考文献: CakePHP routes.phpの確認はユニットテストで - Shin x blog

もし、プラグインルーティングを使ったり、GETとPOSTでルートを分ける場合は、追加のテストコードが必要になるでしょう。

まとめ

最近改訂されたCakePHP 3.x チュートリアルに、テーブル、エンティティー、ルーティングのテストを追加する手順をまとめました。どのようなユニットテストを重視するかは、開発者それぞれ異なると思うので、コメントもらえるとありがたいです。

コントローラーの統合テストについてもまとめる予定です。→CakePHP 3 のチュートリアルにユニットテストを追加する (2) - Engineer as a Lifestyle @tenkomaにまとめました。

CakePHP Advent Calendar 2017 2日目の記事でした。

ssh設定アップデート(ed25519鍵を使う)

新しい環境を作る機会もあるので、sshの設定も見直すことにしました。 いままで2048bit RSA鍵を使っていましたがed25519鍵を使うことにします。 ローカルPCには.ssh/id_rsa, .ssh/id_rsa.pub が作成されていることとします。

$ ls -al .ssh
total 64
drwx------   9 tenkoma  staff   306  2 18  2017 .
drwxr-xr-x+ 91 tenkoma  staff  3094  8 26 11:04 ..
-rw-r--r--   1 tenkoma  staff   117  1 14  2017 config
-rw-------   1 tenkoma  staff  3326 11 19  2016 id_rsa
-rw-r--r--   1 tenkoma  staff   752 11 19  2016 id_rsa.pub
-rw-r--r--   1 tenkoma  staff  6622  4  2 20:23 known_hosts

次のコマンドでed25519鍵を生成します。

$ ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/Users/tenkoma/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/tenkoma/.ssh/id_ed25519.
Your public key has been saved in /Users/tenkoma/.ssh/id_ed25519.pub.
The key fingerprint is:
(略)
The key's randomart image is:
(略)

.ssh/id_ed25519, .ssh/id_ed25519.pub が作成されました。

https://github.com/settings/keys にアクセスして、既存の鍵を削除し、.ssh/id_ed25519.pubの中身のテキストを登録します。

登録したらGitHubに接続テストします。ssh-add でsshエージェントに登録し、毎回パスフレーズを聞かれないようにします。

$ ssh-add -D
All identities removed.
$ ssh-add .ssh/id_ed25519
Enter passphrase for .ssh/id_ed25519:
Identity added: .ssh/id_ed25519 (tenkoma@KojinoMacBook-Pro.local)
$ ssh -T git@github.com
Hi tenkoma! You've successfully authenticated, but GitHub does not provide shell access.

メッセージが表示されたら成功です。

古い鍵を順次アップデートするために

ssh 接続するとき、自動的に.ssh/id_rsaを利用するため、古い設定が残り続けていても気づかない恐れがあります。.ssh/id_rsa, .ssh/id_rsa.pubをリネームして、接続できなかった場合に ssh -i ~/.ssh/id_rsa_deprecated username@example.com でログインしてリモートの鍵を更新します。やむをえず古い鍵を使う場合は、.ssh/config に設定しておきます。

Host deprecated-ssh-key-host.example.com
    IdentityFile ~/.ssh/id_rsa_deprecated

brew upgradeしたらphp実行できなくなって、php-buildも失敗するようになった【たぶん解決】

とりあえず現状を載せておきます。 brew upgrade を実行したら、php-buildでインストールしたphpの実行に失敗するようになりました。 php-buildは最新版であることを確認して、(コミットハッシュ: e2969a6) install.sh を実行しました。 結論としては CXXFLAGS='-std=c++11' を追加してビルドすると、phpが実行できるようになりました。

追記 (2017/08/28 14:52)

Fix default temporary directory for php-build on macOS by hnw · Pull Request #467 · php-build/php-build でこの問題が修正され、ビルド時に環境変数CXXFLAGS='-std=c++11'を指定しなくてもよくなりました。PHP5.3.29もビルドできるようになりました。

環境

  • MacBook Pro 2016Mid
  • macOS Sierra 10.12.6
  • direnv でphpのパスを設定
  • ghq で php-build をインストール

現象

PHP実行

$ php -v
dyld: Library not loaded: /usr/local/opt/jpeg/lib/libjpeg.8.dylib
  Referenced from: /Users/tenkoma/local/php/7.1.8/bin/php
  Reason: image not found
Abort trap: 6

php-build

$ ghq look php-build
        cd /Users/tenkoma/src/github.com/php-build/php-build
bash-3.2$ time YACC=$(brew --prefix bison)/bin/bison PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j7 php-build -i development 7.1.8 ~/local/php/7.1.8/
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
[Info]: Loaded github Plugin.
[Info]: Loaded uprofiler Plugin.
[Info]: Loaded xdebug Plugin.
[Info]: Loaded xhprof Plugin.
[Info]: Loaded zendopcache Plugin.
[Info]: php.ini-development gets used as php.ini
[Info]: Building 7.1.8 into /Users/tenkoma/local/php/7.1.8/
[Downloading]: https://secure.php.net/distributions/php-7.1.8.tar.bz2
[Preparing]: /var/folders/pc/vq2zc_sn05v190vzzvz99bpc0000gn/T//php-build/source/7.1.8
^@[Compiling]: /var/folders/pc/vq2zc_sn05v190vzzvz99bpc0000gn/T//php-build/source/7.1.8
^@^@
-----------------
|  BUILD ERROR  |
-----------------

Here are the last 10 lines from the log:

-----------------------------------------
                                                   ^
                                                    _Nullable
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/signal.h:106:48: note: insert '_Nonnull' if the pointer should never be null
int     sigvec(int, struct sigvec *, struct sigvec *);
                                                   ^
                                                    _Nonnull
352 warnings generated.
352 warnings generated.
352 warnings generated.
331 warnings generated.
-----------------------------------------

The full Log is available at '/tmp/php-build.7.1.8.20170814220930.log'.
[Warn]: Aborting build.

real    3m15.342s
user    6m5.609s
sys 1m21.761s

ビルド成功しました(追記 2017/08/16 1:22)

下の、「libjpegのエラーについては、ビルドしなおせば、上記のエラーは発生しなくなります」も確認ください。 解決策情報をいただきました。

やってみます。まず、libjpeg のバージョンを切り替え

$ brew info libjpeg
jpeg: stable 9b (bottled)
Image manipulation library
http://www.ijg.org
/usr/local/Cellar/jpeg/8d (19 files, 708.3KB)
  Poured from bottle on 2016-11-19 at 12:49:23
/usr/local/Cellar/jpeg/9b (20 files, 724KB) *
  Poured from bottle on 2017-08-14 at 00:35:36
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/jpeg.rb
$ brew switch libjpeg 8d
Cleaning /usr/local/Cellar/jpeg/8d
Cleaning /usr/local/Cellar/jpeg/9b
17 links created for /usr/local/Cellar/jpeg/8d

次に CXXFLAGS を指定してビルド

$ ghq look php-build
        cd /Users/tenkoma/src/github.com/php-build/php-build
bash-3.2$ time YACC=$(brew --prefix bison)/bin/bison CXXFLAGS='-std=c++11' PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j7 php-build -i development 7.1.8 ~/local/php/7.1.8/
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
[Info]: Loaded github Plugin.
[Info]: Loaded uprofiler Plugin.
[Info]: Loaded xdebug Plugin.
[Info]: Loaded xhprof Plugin.
[Info]: Loaded zendopcache Plugin.
[Info]: php.ini-development gets used as php.ini
[Info]: Building 7.1.8 into /Users/tenkoma/local/php/7.1.8/
[Downloading]: https://secure.php.net/distributions/php-7.1.8.tar.bz2
[Preparing]: /var/folders/pc/vq2zc_sn05v190vzzvz99bpc0000gn/T//php-build/source/7.1.8
^@[Compiling]: /var/folders/pc/vq2zc_sn05v190vzzvz99bpc0000gn/T//php-build/source/7.1.8
^@^@^@^@[xdebug]: Installing version 2.5.5
[xdebug]: Compiling xdebug in /var/folders/pc/vq2zc_sn05v190vzzvz99bpc0000gn/T//php-build/source/xdebug-2.5.5
[xdebug]: Cleaning up.
[Info]: Enabling Opcache...
[Info]: Done
[Info]: The Log File is not empty, but the Build did not fail. Maybe just warnings got logged. You can review the log in /tmp/php-build.7.1.8.20170816011335.log
[Success]: Built 7.1.8 successfully.

real    6m12.645s
user    16m37.504s
sys 5m20.081s
bash-3.2$ php -v
PHP 7.1.8 (cli) (built: Aug 16 2017 01:18:24) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.1.8, Copyright (c) 1999-2017, by Zend Technologies
    with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

ビルド時刻が更新されているので、ビルド成功です。

libjpegのエラーについては、ビルドしなおせば、上記のエラーは発生しなくなります(さらに追記 2017/08/18 18:27)

さらに情報をいただきました。ありがとうございます。

libjpeg を元に戻してビルドし直しました。

$ brew switch libjpeg 9b
Cleaning /usr/local/Cellar/jpeg/8d
Cleaning /usr/local/Cellar/jpeg/9b
17 links created for /usr/local/Cellar/jpeg/9b
$ ghq look php-build
$ time YACC=$(brew --prefix bison)/bin/bison CXXFLAGS='-std=c++11' PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j7 php-build -i development 7.1.8 ~/local/php/7.1.8/
$ ~/local/php/7.1.8/bin/php -i | grep JPEG
Supported filetypes => JPEG,TIFF
JPEG Support => enabled
libJPEG Version => 9 compatible

いただいた情報の通り、 php 実行できました。

5.4.45, 5.5.38, 5.6.31, 7.0.22, 7.1.8, 7.2beta2 までビルド成功しました。5.3.29では以下の通りビルド失敗しました。

$ time YACC=$(brew --prefix bison)/bin/bison CXXFLAGS='-std=c++11' PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j7 php-build -i development 5.3.29 ~/local/php/5.3.29/
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
[Info]: Loaded github Plugin.
[Info]: Loaded uprofiler Plugin.
[Info]: Loaded xdebug Plugin.
[Info]: Loaded xhprof Plugin.
[Info]: Loaded zendopcache Plugin.
[Info]: php.ini-development gets used as php.ini
[Info]: Building 5.3.29 into /Users/tenkoma/local/php/5.3.29/
[Downloading]: https://secure.php.net/distributions/php-5.3.29.tar.bz2
[Info]: Applying patches: /usr/local/bin/../share/php-build/patches/php-5.3.29-64bit-intl.patch
[Preparing]: /var/folders/pc/vq2zc_sn05v190vzzvz99bpc0000gn/T//php-build/source/5.3.29
[Compiling]: /var/folders/pc/vq2zc_sn05v190vzzvz99bpc0000gn/T//php-build/source/5.3.29
^@
-----------------
|  BUILD ERROR  |
-----------------

Here are the last 10 lines from the log:

-----------------------------------------
        ^
          _Nullable
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/stdio.h:475:6: note: insert '_Nonnull' if the pointer should never be null
FILE    *funopen(const void *,
        ^
          _Nonnull
330 warnings generated.
330 warnings generated.
330 warnings generated.
331 warnings generated.
-----------------------------------------

The full Log is available at '/tmp/php-build.5.3.29.20170818191216.log'.
[Warn]: Aborting build.

real    2m24.479s
user    3m19.952s
sys 1m30.081s

今後5.3を使う必要に迫られない限り、放置でいこうかと思います。

CircleCI 2.0でCakePHPアプリをビルドする話を PHP BLT #8でしてきました

PHP BLT #8 - connpass

8月8日、株式会社メルカリ 東京支社オフィスで開催されたPHP BLT #8 に参加してLTしました。

CircleCI 2.0 で CakePHP3 アプリのビルド // Speaker Deck

CircleCI 2.0を検証してみて

参考文献

余談

見てくださるかたの反応を見ながら話すのが苦手です…

あと、LTでコードを見せて理解してもらうのは困難、という学びがありました。

ワークフロー機能が実装される前は、副ジョブのAPIをcurlで叩く、というやり方だったので(参考: CircleCI 2.0 でNode.jsのマルチバージョンビルド - teppeis blog)、CircleCI に「お前は何を言ってるんだ」と言いたくなりましたが、ワークフロー機能は便利です。

PHPカンファレンス福岡2017 に参加しました #phpconfuk

2017年6月10日に福岡で開催されたPHPカンファレンス福岡2017に行ってきました。

関西は2回ほど行ってますが福岡は初参加でした。

前日入りしてFusicさんに初訪問して雑談したりコード書いたりしてましたが後で述べます。

当日の感想など

ひとりLT大会

途中から参加。早口すぎてわからないところが多かったのですが、LTは勢いが大事、ということで… クイズを支える技術についてはY8 2017 spring in Shibuya懇親会で聴いてました。

新卒2年目がサービス開発の際に乗り越えた課題とその解法など

スポンサードセッション3つのうちの1つでした。私はこれ1つだけ聴いて、あとはスポンサーブースを回ったのですが(すみません!)題目だけ見るとまったく宣伝くさくないですね。開発ノウハウの共有自体が最高の宣伝ということなのでしょう。

  • PHP5.6で書いてたコード、ほぼPHP7でも動いたとのこと
  • デプロイを自動化する、コマンドを使わず、エンジニア以外でもデプロイできるようにする仕組みについては僕もとりくんでいるので、参考になるかも…と思います。
  • 僕はDeployer — Deployment Tool for PHP使ってます。CakePHP3なら、デプロイ時はここに書き込み権限が必要でORMキャッシュクリア必要だよねー、みたいな部分をプリセットで用意してくれる(他にもいろんなレシピがあります)ので、おすすめです。

実践Action Domain Responder

「Action Domain Responder」という用語自体知らなかったので参加

スライド未アップロードのようですが、Lumen/Laravel Action-Domain-Responder(ADR)アプローチ - Qiitaにyuuki takezawaさんの資料がありました。

CakePHPでコードを書いていると、Controllerのコードが太りやすくなったり、ビューロジック(Responseの操作)が混ざったりするので、分けられないかな〜と思うことはあるので、後ほどチェックしたいと思います。

faultline / faultline-php によるサーバ管理不要なエラートラッキングとその効果

少し前にツール自体試してないのにプルリクを送ったことがあり、興味があったので参加しました。

小規模なプロジェクトが数多くある現場だとSaaSのエラートラッキングツールは選びにくいので、そういった現場では非常に有用ですね! S3にログを蓄積していて、WEBUIを使った場合、それをHTTPで取得しているので、検索機能は実装しにくい(もしAmazon Athenaを使うとなると金が発生しそう…?)そうですが、GitHub Issueに登録する機能があるので、簡易的な検索はそちらで出来る気がしました。

次の日もFusicさんにお邪魔してfaultlineを実際に試してみることができました。

Progressive Web Apps + AMP = PWAMP for PHPer

Progressive Web Apps はHTML5 Conferenceだったかで聴いたことがあり参加。

Progressive Web Apps や Service Worker といった新しめの技術はlocalhost以外でHTTPSが必須となっていて、僕も新規にサイト構築するときはhttps前提に考えているので、HTTPS化をすすめるためにも勉強していきたい。

PHPerに覚えて欲しい日本語の重要性

エラーメッセージってどうかくか、よく迷いますね。 ユーザーにとっての「具体的な解決策」を書くようにしよう!

CakePHP 1.3 + PHP 5.3 → CakePHP 3 + PHP 7 移行を決めた話

開催前から注目してたセッション。 バージョンアップ条件の整理(20ページ)など、バージョンアップ時に注意していることの情報がたくさんで貴重です。 CakePHPコアに直接手を加えている箇所、多いですね…

[LT]カンファレンスのあちら側とこちら側

勉強会・カンファレンスで発表したいけど一歩踏み出せない方に超おすすめLTでした。

スポンサーブースまわり

ホールから1度出た別の部屋でスポンサーコーナーとAsk The Speakerコーナーがありました。 BaserCMS のブースがあり、要望を書くとくじが引けたのでCakePHP3対応を要望しました(笑)

全体的な感想

最近、福岡にはASCII.jp:髙島市長も熱弁!さくら、アカツキ、ピクシブ、メルカリが福岡拠点開所へというニュースもあり、その盛り上がりを見たいというのがあり、初めて参加しました。

カンファレンスの会場はとても快適で、3つあるホール/ルームはすべて座って聴くことができました。前の方の席にテーブルが用意されていてセッション開始後でもすいていたので助かりました。ルームの移動もしやすかったです。

セッション内容も興味のあるものが多く、カンファレンス後もPHPerの皆さんと飲みながら長く話すことができて充実できました。

来年も開催されるなら是非参加したいです。

(おまけ)前日と翌日

カンファレンスの前日と翌日、地下鉄天神駅の近くにある株式会社Fusicさんにお邪魔して、雑談したりコード書いたりしてました。

前日は8. 現場の座談会(cakephper / tomzoh / tadsan) | PHPの現場の収録も行われ、生で内容を聞いてました。その後、前夜祭まで少し時間があったので、会場で袋詰めのお手伝いをしました。

翌日は主にfaultlineのハンズオンに参加していて、ちょっとしたバグ修正のPRをその場で送ったりしてました。