ECCUBE3カスタマイズについていくつかのこと:拡張エンティティをぶっこむためには既存の処理を置き換えねばならぬ

さて問題はここからである。ここからなはずなのだが、いささか中身としては地味かもしれぬ。

まあさておき。

背景

前回までで、商品情報や注文情報など既存のテーブル、エンティティに項目を追加したいときは直接テーブルやPHPコードに手を入れたりせず、プラグイン内に新規エンティティとして生成して運用せよというガイドラインを確認し、それに従ってテーブルとPHPコードを生成するところまでを進めた。

今回はそのエンティティを使ってサイト表示できるところまでもっていきたいと思う。

ダミーデータの挿入

さて、現時点ではテーブルはあっても中身がなく空なので拡張エンティティといっても無いも同然である。テストのやりようがない。というわけでダミーデータを入れておきたいと思う。

マイグレーション | EC-CUBE 開発ドキュメント

こちらの内容に沿って、ダミーデータ生成用のマイグレーションファイルを用意する。

<?php

namespace DoctrineMigrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20180524215000 extends AbstractMigration
{
    const NAME = 'plg_sampleplugin_productreview';

    /**
     * @param Schema $schema
     */
    public function up(Schema $schema)
    {
        if ($schema->hasTable(self::NAME)) {
            $this->addSql('INSERT INTO ' . self::NAME . ' (product_id, comment) VALUES (1, \'aaa\')');
        }
    }

    /**
     * @param Schema $schema
     */
    public function down(Schema $schema)
    {
    }
}

ダミーデータ登録のために一旦プラグインを再インストール、再有効化の必要あり。

$ php app/console plugin:develop uninstall --code SamplePlugin
$ php app/console plugin:develop install --code SamplePlugin
$ php app/console plugin:develop enable --code SamplePlugin

DB内容を確認。

MariaDB [eccube3]> SELECT * FROM plg_sampleplugin_productreview;
+------------+---------+
| product_id | comment |
+------------+---------+
|          1 | aaa     |
+------------+---------+
1 row in set (0.00 sec)

## 処理の差し込み: コントローラを挿げ替える

拡張エンティティを処理に連結するために必要な処理を追加したいが、ここに一つ大きな問題がある。それは、「差し込みたい場所にかならず差し込めるとは限らない」ということだ。

ECCUBE3になって、本体の処理をイベントフックという形でプラグイン側が受け取ることができるようになっているのだが、これを通して処理を受け取るためには当然のことながら本体側でイベントフックが実装されてないといけない。つまり、「欲しい場所に必ずイベントフックが仕込まれてないといけないが、だいたいにおいてそうとは限らない」という悲しい現実が待っている。

そうするとやはり本体のコードに手を入れるということが不可避になるのだが、やはりどうしてもそれは避けたいとなった場合、本体コントローラの処理をまるごと乗っ取るという判断になる。その時につくったプラグイン内のServiceProviderがこちら。

class SamplePluginServiceProvider implements ServiceProviderInterface
{
    public function register(BaseApplication $app)
    {
        // 管理画面定義
        $admin = $app['controllers_factory'];
        //Frontend
        $front = $app['controllers_factory'];
        // 強制SSL
        if ($app['config']['force_ssl'] == Constant::ENABLED) {
            $admin->requireHttps();
            $front->requireHttps();
        }

        $admin->match('/product/product/new', '\Plugin\SamplePlugin\Controller\Admin\Product\ProductController::edit')->bind('admin_product_product_new');
        $admin->match('/product/product/{id}/edit', '\Plugin\SamplePlugin\Controller\Admin\Product\ProductController::edit')->assert('id', '\d+')->bind('admin_product_product_edit');
        $app->mount('/'.trim($app['config']['admin_route'], '/').'/', $admin);

        $front->match('/products/detail/{id}', '\Plugin\SamplePlugin\Controller\ProductController::detail')->bind('product_detail')->assert('id', '\d+');
        $app->mount('', $front);

        //...
    }
}

だいたいこんな感じ。

プラグイン内に同じ名前でよいのでControllerを新造し、該当するアクションメソッドを複製する。そこに必要な処理を追加してルーティングに乗せると新造したコントローラが該当URLの処理を受け持ってくれるようになる。まったくもって力技である。なお置き換えないメソッド・URLはそのままでよく、新造するクラスは置き換える必要のあるメソッドだけ実装すればよいのは助かる。

次にServiceクラスを挿げ替える

これだけで済めばよいがそうとも限らない。RailsでいうところのHelperクラスに近いととらえればよいのだろうか、Service系のクラスに結構処理が並んでいてそこで処理の追加や差し替えがどうしても必要なケースというのが出てくることがある。こういうのがほんと困るんだ。どうしようか?

これも挿げ替えである。

        $app['eccube.service.cart'] = $app->share(function () use ($app) {
            return new \Plugin\SamplePlugin\Service\CartServiceEx($app);
        });
        $app['eccube.service.shopping'] = $app->share(function () use ($app) {
            return new \Plugin\SamplePlugin\Service\ShoppingServiceEx($app, $app['eccube.service.cart'], $app['eccube.service.order']);
        });
        $app['eccube.service.order'] = $app->share(function () use ($app) {
            return new \Plugin\SamplePlugin\Service\OrderServiceEx($app);
        });

前項でルーティングを置き換えたServiceProviderのregister()メソッドにつなげて、今度はServiceインスタンスの登録をカスタム化したクラスで置き換える。ECCUBE3では幸い?なことに、すべてのServiceクラスは直接クラス名を指定して起動しておらず、こうして置き換える…というか乗っ取ることができる。すごいのか何なのか。なおControllerと違ってServiceの場合はクラス単位で置き換えてしまうのですべてのメソッドを複製する必要がある点に注意。というか置き換え前のインスタンスを拡張側のServiceで参照して拡張の必要のない処理はその置き換え前インスタンスに委譲する形で実装してもよいかも。

あとは置き換えたプラグイン側の実装に必要な処理を追加すればよい。今回のProductReviewエンティティは元となるProductエンティティへのカラム追加と同じという体で実装するので、ProductへのSELECTやUPDATEが発生する箇所すべてに横付けする形になる。

最後にテンプレート側で

ここまでやってもカバーできないケースもある。というか、いざ表示するのにControllerで拡張エンティティを拾わせてそれをrender()メソッドに渡してテンプレートに…ってやるの、MVC分離としては正しいんだけども引数が増える一方で妙に相互依存が増してるしどうなんだろう?という気がする瞬間があるわけです。サイト側では更新もなく表示するだけとかだとなおさら。

qiita.com

これの、「Twig拡張関数を使わない場合」に沿って拡張エンティティを拾って表示するっていう実装で割とほぼ用が足りる。確かにテンプレートにDBアクセス相当の処理を埋め込んでしまうのはかなり筋悪な実相でここまで必死に守ってきたMVC分離に対する強烈なちゃぶ台返しと言えなくもない。が、ここまで大規模に加えてきた改変がいろいろとモラルハザードを引き起こしてしまっている自分に気づく。

MVC分離は確かに大事だしこの期に及んでも「守りましょうね」と言い切るんですけど、それを守るために投じられるコストがあまりにECCUBE3(のカスタマイズ)においては大きすぎるので、ものすごく悩ましい。イベントフックとかもう少し何とかならんだろうか。うーむ。