ModelのCakePHP3移行

kanazawa|2019年10月15日
CakePHP

SREチームの金澤です。

前回、ランサーズのCakePHP2→3へのバージョンアップを開始した記事を書きました。

今回は、ModelのCakePHP3移行について書きたいと思います。

CakePHP2からCakePHP3を読み込む

CakePHP2とCakePHP3を共存する方法を前回のブログで紹介しました。

Modelの移行については、@kunitさんの記事

CakePHP2 から CakePHP3 ORMを使ってみる

がとても参考になりました。

この方法をベースにModelのCakePHP3移行にトライしてみました。

まずは、cake28/Config/bootstrap.php に以下の設定を追加します。

define('CONFIG_CAKEPHP3', __DIR__ . '/../../config/');

if (!env('DB_MASTER_HOST') && file_exists(CONFIG_CAKEPHP3 . '.env')) {
    $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG_CAKEPHP3 . '.env']);
    $dotenv->parse()
        ->putenv()
        ->toEnv()
        ->toServer();
}

try {
    \Cake\Core\Configure::config(
        'default',
        new \Cake\Core\Configure\Engine\PhpConfig(CONFIG_CAKEPHP3)
    );
    \Cake\Core\Configure::load('app', 'default', false);
} catch (\Exception $e) {
    exit($e->getMessage() . "\n");
}

\Cake\Datasource\ConnectionManager::setConfig(
    'default',
    \Cake\Core\Configure::read('Datasources.default')
);
\Cake\Datasource\ConnectionManager::setConfig(
    'slave',
    \Cake\Core\Configure::read('Datasources.slave')
);
\Cake\Datasource\ConnectionManager::setConfig(
    'test',
    \Cake\Core\Configure::read('Datasources.test')
);

\Cake\Cache\Cache::setConfig(
    '_cake_model_',
    \Cake\Core\Configure::read('Cache._cake_model_')
);

\Cake\I18n\I18n::setLocale(\Cake\Core\Configure::read('App.defaultLocale'));

そして、CakePHP2のUT実行時にCakePHP3側の処理もテスト用DBにアクセスするようにします。

これは、@okinakaさんの記事

CakePHP2からCakePHP3 ORMを使ったテスト

を参考にさせていただきました。

cake28/Test/bootstrap.php に以下の設定を追加します。

\Cake\Datasource\ConnectionManager::alias('test', 'default');
\Cake\Datasource\ConnectionManager::alias('test', 'slave');

test.phpで実行したときのパターンも想定し、
cake28/Config/bootstrap.php にも以下の設定を追加しておきます。

if (preg_match("|/test.php$|", $_SERVER['SCRIPT_FILENAME'])) {
    \Cake\Datasource\ConnectionManager::alias('test', 'default');
    \Cake\Datasource\ConnectionManager::alias('test', 'slave');
}

CakePHP3 ORM変換の挫折

続いて、CakePHP2からCakePHP3 ORMを呼び出す処理を実装します。

結論を言うと、この試みは挫折しました。

@kunitさんの記事を参考にさせていただき、加えて
既存のCakePHP2のControllerのソースを変更せずにModelのみCakePHP3に移行したかったため、
CakePHP2のfind関数をオーバーライドし、そこからCakePHP3のORMを呼び出して変換する方針で
実装していました。

現在のランサーズのソースコードで利用されているパターンを全て網羅するためには
例えば以下のパターンを考慮する必要があります。

  • InflectorでTable名→Model名変換
  • CakePHP3のEntityをCakePHP2の配列に変換
  • CakePHP3のDateTime型をフォーマット変換
  • find(‘list’)、find(‘all’)の対応
  • ORDER BYの対応

↑ここまでは対応していたのですが、

  • GROUP BYの対応
  • fieledのワイルドカード
  • バーチャルフィールドなしの集計関数 
  • HAVING区
  • CASE WHENなどの分岐
  • その他いろいろ

↑ここら辺も対応する必要があることがわかってきました。

ランサーズ内に出てくる、これらすべてのクエリを全てを網羅した変換処理を書き続けるよりも、
別なアプローチの方が良いと思いました。

※この挫折の詳細については、別途Qiitaに書きたいと思います。

CakePHP2からCakePHP3のModel関数を呼び出す

CakePHP3のORMをCakePHP2に変換するのではなく、
CakePHP2のModelの関数をCakePHP3に移行する方針に変更しました。

citiesテーブルにアクセスするCity ModelをCakePHP3に移行してみます。

<?php
class City extends AppModel
{
    public $name = 'City';
    public $validate = [
        'id' => ['numeric'],
    ];

    /**
     * 市区町村名を返す
     *
     * @param int|null $cityId 市区町村ID
     * @return string|null 市区町村名
     * @access public
     */
    public function getName($cityId)
    {
        $prefectural = $this->findById($prefecturalId);
        return isset($prefectural['Prefectural']['name']) ? $prefectural['Prefectural']['name'] : null;
    }
}

bakeでCakePHP3のModelを生成

CakePHP3のソースをbakeで生成します。

CakePHP3のbakeコマンドを実行すると

$ bin/cake bake model Cities

以下のファイルが生成されます。
src/Model/Entity/City.php
src/Model/Table/CitiesTable.php
tests/Fixture/CitesFixture.php
tests/TestCase/Model/CitiesTableTest.php

CakePHP3のTableにCakePHP2の関数を移植

CakePHP2のgetName関数をCakePHP3のTableに移植します。

src/Model/Table/CitiesTable.php


    public function __construct($id = false, $table = null, $ds = null)
    {
        parent::__construct($id, $table, $ds);
        $this->cake3Table = Cake\ORM\TableRegistry::getTableLocator()->get('Cities');
    }

    /**
     * 市区町村名を返す
     *
     * @param int|null $cityId 市区町村ID
     * @return string|null 市区町村名
     * @access public
     */
    public function getName(?int $cityId) : ?string
    {
        if (!isset($cityId)) {
            return null;
        }
        $result = $this->find()->select(['name'])->where(['id' => $cityId])->first();
        return isset($result) ? $result->name : null;
    }

CakePHP2のModelからCakePHP3の関数を呼び出す

cake28/Model/City.php

    /**
     * 市区町村名を返す
     *
     * @param int|null $cityId 市区町村ID
     * @return string|null 市区町村名
     * @access public
     */
    public function getName($cityId)
    {
        return $this->cake3Table->getName($cityId);
    }

この状態で、CakePHP2のUTが正常に終了すれば移行完了になります。

Validationの移行について

この手順で移行すれば、
CakePHP2のController

CakePHP2のModel

CakePHP3のTable
の順番で呼び出されるため、一応、ModelのみCakePHP3に移行できます。

そして、ControllerをCakePHP3に移行したタイミングで
CakePHP3のModelが直接呼び出されるようになっても
CakePHP3に実装した関数は同様に動作するはずです。

しかし、Validationについては、まだCakePHP2のModelで動作している状態です。

CakePHP3のValidationは、CakePHP2から大幅に変更されています。
Validationクラスやモデルのないフォーム等、
新しいValidationの仕組みに対応する必要があるため、
単純に関数を移行すればOKというわけではありません。

Validation絡みの処理に関しては、bakeで生成されるもの以外は
Controller移行時にまとめて行うことにしました。

CakePHP3用のUT実行

現状、UTはCakePHP2側に寄せていますが、
CakePHP3のbakeで生成したValidationについても、CakePHP2との互換性を確認しておきたいところです。
そのため、CakePHP3のValidationの確認をCakePHP3のUTで行うことにしました。

現状、CakePHP2のUT実行は以下のコマンドで行っています。
(phpunit.xmlでcake28/Test/bootstrap.phpを読み込んでいる)

$ ./cake28/Console/cake l_test cake28/Test/Case/Model/CityTest.php

CakePHP3のUTは以下のコマンドで実行できます。
(CakePHP3のbootstrap.phpを読み込んでphpunitを実行)

$ ./bin/phpunit --bootstrap config/bootstrap.php tests/TestCase/Model/Table/CitiesTableTest.php

CakePHP3のValidationのテストは以下のように実装しました。
(getErrorを呼び出し、内容を確認)

tests/TestCase/Model/Table/CitiesTableTest.php

    public function testValidate()
    {
        // 正常系
        $city = $this->Cities->newEntity([
            'id' => 1,
            'prefectural_id' => 1,
            'name' => '札幌市',
            'created' => '2019-09-04 12:27:03',
            'modified' => '2019-09-04 12:27:03'
        ]);
        $this->assertSame([], $city->getErrors());

        // nameが空
        $city = $this->Cities->newEntity([
            'id' => 1,
            'prefectural_id' => 1,
            'name' => '',
            'created' => '2019-09-04 12:27:03',
            'modified' => '2019-09-04 12:27:03'
        ]);
        $this->assertSame(["name" => ["_empty" => "入力してください", ]], $city->getErrors());
    }

今後の予定

まずは一部の機能に絞って、Model、Lib、Controller、viewを一通り移行したいと考えています。

今回の方針も含め、まだ手探りな部分が多いため、
一通りModel、Lib、Controller、viewのCakePHP3移行を行い、この手法が有効かどうかを検証し、
問題なさそうであれば、本格的にすべてのModel移行を開始したいと思います。

CakeFest2019に登壇します

2019/11/7 – 2019/11/10にCakeFestが東京で開催されます。

CakeFestはCakePHPの世界的なイベントで、2019年は日本で開催されることになりました。
※ランサーズは前回の2017年に続き、2019年もスポンサーになっています。

「CakePHP3への滑らかな移行を考える」というテーマが採択されました。
本記事の内容も踏まえて発表する予定です。