EC-CUBE4 独自プラグイン開発　②独自プラグイン開発Tips

# 概要
EC-CUBE4で独自プラグインを開発するのに必要だった知識をまとめた。

ドキュメントにはたったの3ページしか[プラグイン開発に関する記述](https://doc4.ec-cube.net/plugin_spec)がない。情報が少なすぎる、と当初は思っていたけど読み返せばちゃんとドキュメントに情報が書いてあったりするのでよく読んだほうがいい。

### ただしがき
- この記事の内容はこうやって開発した、というだけであって正解とは限らない
- 「EC-CUBE標準」という単語が何回か出てくるが、これは開発環境構築直後の初期状態で実装されている機能のこと
- オーナーズストアで販売・配布されているプラグインを「市場プラグイン」、オーナーズストアには公開せず自分だけで使うプラグインを「独自プラグイン」と呼ぶことにする。今回説明するのは後者を作ったときに得た情報
- Symfonyの作法とEC-CUBE独自の作法が混ざってる

# 目次
[EC-CUBE4 独自プラグイン開発　①環境構築手順](https://qiita.com/haruna-nagayoshi/items/948b196cf4f5186641e9)
EC-CUBE4 独自プラグイン開発　②独自プラグイン開発Tips　(本記事)
[EC-CUBE4 独自プラグイン開発　③今後も使えそうなTips](https://qiita.com/haruna-nagayoshi/items/3527ee12c552a339bc6b)


# プラグイン開発Tips

## 開発中に画面上で開発中プラグインのアンインストール(削除)をしない

理由：
これを実行すると、app/Plugin配下のソースがすべてシステム的に削除される(=ゴミ箱内にも存在しない)ため。つまり、.gitディレクトリが復帰できなくなり、ローカルブランチやstashした内容が全て消え、それまでの作業が水の泡になる。

同様に、**eccube:plugin:uninstall --forceコマンドを叩かない**。このコマンドは、画面上でのアンインストールと同じ動作をすると思われる。(わざわざ叩く用がないので試してはいない)

`--force`オプションを付けない場合は、`dtb_plugin` というテーブルから削除するだけで、ソースは消えないと思われる。(これも正確に試してはいない。)

[クーポンプラグイン]()や[定休日プラグイン](https://www.ec-cube.net/products/detail.php?product_id=1848)のような市場のプラグインは、画面上でアンインストールしたり、`--forceオプション`をつけて削除しても問題ない。たとえソースを含めてアンインストールしても、再度インストールすればよいから。

### 補足 `dtb_plugin`テーブルについて
プラグインをインストールすると、このテーブルにプラグインの情報を含んだレコードが追加される。このレコードの内容が「プラグインがインストールされた状態なのか、有効化された状態なのか、インストールされていない状態(=アンインストールした状態)なのか」を表現している。

## 開発サイクル

開発のサイクルは、[EC-CUBE4 プラグイン開発　①環境構築手順](https://qiita.com/haruna-nagayoshi/items/948b196cf4f5186641e9)でも簡単に書いた。
独自プラグインを制作するにあたり、テーブルを拡張することが多かったため、ここでは、テーブルを初期化する際のコマンドを書く。

当時のプロジェクトでは、最終的にmakeコマンドを使いテーブルを初期化していた。
~~EC-CUBE4では、Laravelのようにマイグレーションを再実行したり、リセットして最新の状態に戻すためのコマンドが存在しないため。~~
**(2020/10/13 追記)**
Laravelと同様にマイグレーションを実行できます。デフォルトのDB定義を変更したいとき(Not Null制約を外すとか、Collateを変更するとか)に利用します。

```bash
#マイグレーションの作成(Pluginディレクトリ配下ではなく、app/DoctrineMigrations配下に生成される)
php bin/console doctrine:migrations:generate

#マイグレーションの実行
php bin/console doctrine:migrations:migrate

#マイグレーションの実行(指定したファイルのみ実行したい場合)
php bin/console doctrine:migrations:execute 20201010094313 –-up
```
詳しくは下記記事を参照。
[EC-CUBE4カスタマイズ - マイグレーションの作成・実行方法](https://umebius.com/eccube/eccube4-howto-migration/)

makeコマンドは、local環境でのみ利用していました。
**(2020/10/13 追記ここまで)**

デザインを修正しただけとか、テーブルを初期化しなくてもよい場合は初期化しない。

※このmakefileは、開発するプラグインのディレクトリ直下に置いている

```
reset:
	# データベースごと全テーブルを削除
	docker-compose exec ec-cube bin/console doctrine:schema:drop --force --full-database
	
	# データベース、テーブルを作成
	docker-compose exec ec-cube bin/console doctrine:schema:create
	
	# 開発中のプラグインをインストール
	docker-compose exec ec-cube bin/console eccube:plugin:install --code=Xxx
	
	# マスタデータやテストデータを追加するためのcsvを、プラグイン側からEC-CUBE本体のソースへコピー
	cp -r Resource/doctrine/import_csv/ja/ ../../../src/Eccube/Resource/doctrine/import_csv/ -f
	docker-compose exec ec-cube bin/console eccube:fixtures:load
	
	# 任意のプラグインをインストール・有効化
	docker-compose exec ec-cube bin/console eccube:plugin:install --code=Holiday
	docker-compose exec ec-cube bin/console eccube:plugin:enable --code=Holiday
	docker-compose exec ec-cube bin/console eccube:plugin:install --code=Coupon4
	docker-compose exec ec-cube bin/console eccube:plugin:enable --code=Coupon4
	
	# 開発中のプラグインを有効化
	docker-compose exec ec-cube bin/console eccube:plugin:enable --code=Xxx
	
	# キャッシュクリア
	docker-compose exec ec-cube bin/console cache:clear --no-warmup
.PHONY: reset
```

この`make reset`を叩いてテーブルを初期化
↓
ブランチを切って機能実装・修正
↓
git push
↓
ブランチをdevelopに戻し、pull
↓
以降繰り返し

makeコマンドについては下記を参照
[Windowsでmakeコマンドを使う](https://qiita.com/tokikaze0604/items/e13c04192762f8d4ec85)

## 管理画面サイドメニューに項目を追加、または、並び順を変更する
管理画面のサイドメニューの内容に手を加えるには、次の2つの方法がある。
1つ目の方法は本番環境ではやるべきではない。2つ目を推奨する。

### 方法その1(非推奨)
`ec-cube/app/config/eccube/packages/eccube_nav.yaml`を直接書き換える

### 方法その2
`eccube_nav.yaml`をコピー
↓
アルファベット順で`eccube_nav.yaml`よりも後にくるファイル名(`eccube_nav2.yaml`とか`eccube_nav_added.yaml`とか何でもいい)をつけ、独自プラグイン内に配置する。
後にくるファイルが優先されるので、サイドメニューの項目を追加したい・並び方を変更したい場合は`eccube_nav2.yaml`の内容を書き換えればよい。
↓
これをプラグイン有効化時やプラグイン更新時に`ec-cube/app/config/eccube/packages`にコピーする処理をPluginManagerに書く。手動コピーは面倒だし、ミスにつながるため。

```
   public function update(array $meta, ContainerInterface $container)
   {
  		$fileSystem->copy(__DIR__ . '/config/eccube/packages/eccube_nav2.yaml', __DIR__ . '/../../../app/config/eccube/packages/eccube_nav2.yaml');
  }
```

こうしておけば、プラグイン更新コマンドを叩くことでサイドメニューの項目を更新できる。

**※プラグイン更新コマンドについて**
プラングイン有効化コマンドなどと異なり、プラグインのコード名の渡し方が異なるため注意。

```
# プラングイン有効化
docker-compose exec ec-cube bin/console eccube:plugin:enable --code=Xxx

# プラグイン更新
docker-compose exec ec-cube bin/console eccube:plugin:update Xxx
```

## ECCUBE標準機能のページを修正する
これも、個人的には方法2のほうを推奨したい。

### 方法その1　jQueryでECCUBE標準機能のページに介入できる

ボタンや入力フォームを追加する方法として、[jQueryで画面に介入する](https://github.com/EC-CUBE/sample-payment-plugin#画面への介入について)という方法があるが、この方法ではページが描画されてから表示が切り替えられるため、当然ちらつく。
これでは美しくなく、可読性が悪いので、標準のtwigテンプレートを置き換える方法を選んだ。

### 方法その2　ECCUBE標準機能のページのtwigを差し替える
具体的には、twigテンプレートの呼び出し順序を利用する。
twigテンプレートの呼び出し順序については、下記を参照。

```
 1. app/template/admin/Product/index.twig
 2. src/Eccube/Resource/template/admin/Product/index.twig
 3. app/Plugin/[plugin_code]/Resource/template/admin/Product/index.twig
```

この「2.」が標準のテンプレートで、それよりも優先される「1.」を作ることによって、標準テンプレートを差し替えることができる。
[開発者向けドキュメント テンプレートの読み出し順序 呼び出し例](https://doc4.ec-cube.net/design_template)

>管理画面の例
>@Template("@admin/Product/index.twig") 商品マスターのテンプレートをapp/template/admin配下に配置した場合、以下の順序で呼び出されます。

#### 実際の手順

- プラグイン有効化時・更新時に、プラグインのテンプレート群`app/Plugin/xxxx/Resource/template`を`app/template`にコピーするようにPluginManagerに処理を書く。

```
     public function update(array $meta, ContainerInterface $container)
    {
 		$from = __DIR__ .'/Resource/template';
        $to = __DIR__ .'/../../';
        $command = 'cp -r '.$from.' '.$to;

        exec($command);
    }
```

- `app/Plugin/xxxx/Resource/template`に`src/Eccube/Resource/template`と同じディレクトリ構成でtwigテンプレートを配置する。
- この状態でプラグイン更新コマンドを叩けば、`app/Plugin/xxxx/Resource/template`配下のtwigテンプレートが`app/template`にコピーされる。
- こうすることで、各Controllerのtwigテンプレートを指定する部分`@Template("@Xxx/admin/Product/xxx.twig")`はそのままに、標準のテンプレートを差し替えることができる。

#### ちなみに

ページを新しく追加する場合は、`app/Plugin/Xxx/Resource/template`内にtwigテンプレートを置き、Controllerのtwigテンプレート指定部分では頭にプラグイン名をつける(下記のA)という方法でもプラグイン側のtwigテンプレートを返すことができる。

```
@Template("@Xxx/admin/Product/zzz.twig")　・・・A
@Template("@admin/Product/zzz.twig")　　　　・・・B
```

しかし、私が関わったプロジェクトでは、ルーティング名やtwigテンプレート名がプラグインと標準機能で重複するような事態を考慮する必要はなく、A・Bを区別する意味がなかった。そのため、差し替えたい・新規追加したいすべてのtwigを`app/Plugin/xxxx/Resource/template`に置き、`app/template`へコピーし、Controllerでは常にBを書くようにした。

## Entityをテーブルから逆生成する

新規追加するテーブルのEntityを作る方法を説明する。EC-CUBE標準のテーブルに対してカラムを追加する場合は次の項を参照。

Symfonyでは、カラム定義をEntityのannotationで定義するという実装方法がある。
EC-CUBE4では、プラグインインストール時にSQLを流して新規テーブルを作成するのではなく、このEntityの定義をもとにテーブルを作成する。
なので、テーブルを追加するには正しい内容のannotationが記述されたEntityを作る必要がある。

下記の例は、EC-CUBE標準のCustomerテーブル

```
    /**
     * Customer
     *
     * @ORM\Table(name="dtb_customer", uniqueConstraints={@ORM\UniqueConstraint(name="secret_key", columns={"secret_key"})}, indexes={@ORM\Index(name="dtb_customer_buy_times_idx", columns={"buy_times"}), @ORM\Index(name="dtb_customer_buy_total_idx", columns={"buy_total"}), @ORM\Index(name="dtb_customer_create_date_idx", columns={"create_date"}), @ORM\Index(name="dtb_customer_update_date_idx", columns={"update_date"}), @ORM\Index(name="dtb_customer_last_buy_date_idx", columns={"last_buy_date"}), @ORM\Index(name="dtb_customer_email_idx", columns={"email"})})
     * @ORM\InheritanceType("SINGLE_TABLE")
     * @ORM\DiscriminatorColumn(name="discriminator_type", type="string", length=255)
     * @ORM\HasLifecycleCallbacks()
     * @ORM\Entity(repositoryClass="Eccube\Repository\CustomerRepository")
     */
    class Customer extends \Eccube\Entity\AbstractEntity implements UserInterface
    {
        /**
         * @var int
         *
         * @ORM\Column(name="id", type="integer", options={"unsigned":true})
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="IDENTITY")
         */
        private $id;

        /**
         * @var string
         *
         * @ORM\Column(name="name01", type="string", length=255)
         */
        private $name01
    以下省略
```

このannotationを書くのが面倒だったので、テーブルからEntityを逆生成するという方法をとった。やり方はこの記事の通りに実行するだけ。
[symfonyで既存テーブルからEntityやCRUDを自動生成する。](https://qiita.com/yutachaos/items/c91c886ddb2f51147e53)

DB定義やテーブル生成時のSQLは既に完成されていたので、SQLを流して次のコマンドを叩く。

```
# SQLを流した後にこれを打つ。任意のパス、任意のテーブル名に置き換える
# --filterオプションを付けないと全テーブルのEntityが作られるので注意
docker-compose exec ec-cube bin/console doctrine:mapping:convert annotation ./app/Plugin/{plugin_name}/Entity/{path} --from-database --filter={table_name}
```

記事によるとnamespace、getter/setterやFormTypeもコマンドで生成できるみたいだけど、PhpStormですぐ書けるので私は使わなかった。あと、コマンドでgetter/setterを使うと変数の命名規則とかキャメルケースorスネイクケースがちょっとPSRと異なる状態で生成されるっぽい。(未検証)

### Entity逆生成は万能ではない

annotationに正確にDB定義が反映されない場合(照合順序など)があるため。
Entityを逆生成したら一度テーブルを削除して、Entityをもとにテーブルを生成したときに想定通りのテーブルが生成されれば作成完了、とすれば確実だと思う。(実際は、そんなことは面倒くさくていちいち確認していられなかった)

## EC-CUBE標準のEntity拡張方法

新しいテーブルを作るのではなく、標準のテーブルにカラムを拡張したい場合もある。
その場合は[公式ドキュメント](https://doc4.ec-cube.net/customize_entity)にも説明があるように、カラムを拡張したいテーブルに対してTraitを作成する。
プラグイン有効化コマンドまたはプラグイン更新コマンドを叩くと、Traitを存在する場合は対応するproxyが生成される。proxyがTraitをuseすることによってEntityを拡張する・・・という仕組みになっている。

新しいテーブルを追加するときと同様に、カラムを拡張する場合もテーブルからEntityを逆生成する。traitにしないとECCUBE本体のEntityを拡張できないので、生成されたEntityから拡張したい部分だけを残してtraitに変更する。annotationが足りていない部分を適宜修正して完成。

## テストデータやマスタデータを追加・変更する

- `src/Eccube/Resource/doctrine/import_csv/ja配下`に、テーブルごとにテストデータを記述したcsvファイルとそのcsvのファイル名を記述した`definition.yml`を置く。
- 既存のテストデータを上書きしたい場合は、同名のcsvファイルを作成して上書きコピーする。
- プラグイン更新コマンドを叩いたときに、これらのファイルが配置・コピーされるようにPluginManagerに処理を加えておく。
- csvに記載されたデータを各テーブルに反映したいときは、 `eccube:fixtures:load`をたたく



という処理を行った。(「開発サイクル」のmakeコマンドの内容を参照)

本番でも利用するマスタデータと開発環境でのみ利用するテストデータが混ざり合った状態になってしまうという問題があるが、これ以外にテストデータを生成し開発者間で共有する方法を見つけられなかった。

今のところ、テストデータのcsvを削除して本番環境に反映するしかない。

## 独自プラグインが依存するパッケージをcomposerによって依存解決するには？

### オーナーズストアの市場プラグインで依存解決する場合

[プラグイン仕様](https://doc4.ec-cube.net/plugin_spec#composerjson)には、こう記述がある。

>- require: 依存パッケージ  
>  - プラグインが利用するパッケージがあれば追記します。

しかし、これはオーナーズストアのプラグインにのみ適当される模様。

### 独自プラグインは依存解決できない

https://github.com/EC-CUBE/ec-cube/pull/3884

>プラグインの依存パッケージを直接インストールすると整合性が取れないため、独自プラグインインストールでは依存パッケージをインストールしないように修正

上記の理由により、
**独自プラグインをインストールしたときはcomposerによる依存解決ができないようにする修正が入っている。**

これでは独自プラグインで外部ライブラリが使えず困る、ということで
[独自プラグインをオーナーズストアのプラグインと同様に、プラグインが利用するライブラリの依存関係を解決をできるようにする](https://github.com/EC-CUBE/ec-cube/issues/4257)
というissueが立てられてはいるが、2020年4月3日時点では放置されている。


#### 回避策

今回の案件では導入する市場のプラグインが決まっていたため、影響がないことを確認しつつ
localならlocalで、検証環境ならサーバに入って `composer require` でライブラリを落としてくるようにしました。

※この方法を実行する場合は他のプラグインへの影響などは自己責任でお願いします

## twig上でDBをfindしてデータを取得する
twig上でDBをfindしてデータを取得したい場合、`repository()`というメソッドが使えます。

```twig
{% set categories = repository('Eccube\\Entity\\Category').findBy({'Parent' : 1}) %}
```

引数にEntityを渡し、どのRepositoryか指定します。
ドットでつなげてそのRepositoryのメソッドが呼べます。 ex: `findBy()` 

※getList()をtwigでやるくらいならControllerから渡したほうがいいと思います

参考
[twig上でレポジトリを呼び出しデータを取得する方法](https://umebius.com/eccube/use-repository-from-twig/)

## xdebugを有効にしていると市場のプラグインをインストールできない
xdebugが有効な状態でプラグインをインストールしようとすると、このようなエラーが出ます。
(エラーとは言っても `*****{Plugin名}*****` だけで意味不明)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/b09496d4-e61c-871b-c684-a61fd825f41c.png)

一度Dockerコンテナをdownして、xdebugを無効化してからインストールしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253650/e4a36816-9bd7-20ea-2fdf-2627cd942e6b.png)

参考
[ECオープンプラットフォーム「EC-CUBE」開発コミュニティ プラグインがインストールできない](https://xoops.ec-cube.net/modules/newbb/viewtopic.php?topic_id=21057&forum=19)

# 遭遇したエラーと解決方法

## エラー1

```
Unable to generate a URL for the named route "xxx_yyy_zzz" as such route does not exist.
```
`eccube_nav2.yaml`を更新していないことが原因のエラー。

ルーティングを追加するために`eccube_nav.yaml`とは別のファイル(仮に`eccube_nav2.yaml`と呼ぶ)を作成し、`ec-cube/app/config/eccube/packages配下`に置くということをした。さらに、e:p:eコマンドやe:p:updateコマンドを叩いたときに`eccube_nav2.yaml`を上書き更新するようにした。

作業ブランチで新しいルーティングを追加するような内容をpushしたあと、developに戻したりpullすると、プログラム側のソースと、サイドメニューのルーティングに差異ができる。
`eccube_nav2.yaml`を更新せずに管理画面を開こうとすると、サイドメニューにルーティングのURLだけが存在していることになり、このエラーが発生する。

## エラー2

```
Neither the property "tag_code" nor one of the methods "tag_code()", "gettag_code()"/"istag_code()"/"hastag_code()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView" in
```

templateまでは処理が進んだけど、いずれのプロパティも存在しないから取得できないみたいなことを言っている。

一番多かった原因は、拡張対象のFormTypeを間違えていたこと。Controllerのソースをちゃんと読む。
× TagType 〇 ProductTag　←使われていないTypeだった！削除したい！という例
× PageType 〇MainEditType　←名前で判断したら関係ないTypeだった！という例

また、 **updateコマンドを叩いてproxyファイルを生成する必要がある。** 生成し忘れてこのエラーが出ていたこともあった。

## エラー3

```
$ docker-compose exec ec-cube bin/console cache:clear --no-warmup // Clearing the cache for the dev environment with debug trueIn Filesystem.php line 57:  Failed to copy "/var/www/html/var/cache/dev/pools/x2YpzAU4yg/A/E/+qSFh13+ICKVa1bYz9mA" to "/var/www/html/var/cache/de~/pools/x2YpzAU4yg/A/E/+qSFh13+ICKVa1bYz9mA" because source file could not be opened for reading.
```
`cache:clear`コマンドを叩いたらこういうエラーが出たことがあった。
その上クリアしようとしても一部のファイルが残っていて、rmコマンドでvarディレクトリを丸ごと削除しようとしても消えなかった。

しばらく作業していたらいつのまにか消えていたし、cache:clearコマンドも正常に動くようになったので、このエラーに関しては解決方法がわからないまま。Dockerコンテナにvarディレクトリを同期することをやめた後は発生していない気がする・・・。

## エラー4

```
[RuntimeException]                                                             
  You can not install the EC-CUBE plugin via `composer` command.                 
  Please use the `bin/console eccube:composer:require ec-cube/coupon4` instead.  
```
Coupon4プラグインを入れている状態で、`composer install`や`docker-compose -d --build`すると発生する。
Coupon4のcomposer.jsonの書き方についてcomposerがよく怒っているので、Coupon4側の問題と思われる。

### 回避策
①だけで済む場合と、①-④が必要な場合がある。

①eccubeディレクトリ直下のcomposer.json、composer.lockからCoupon4プラグインに関する記述を消す
②エクスプローラでeccubeディレクトリを開いて、`ec-cube\app\Plugin\Coupon4` を切り取りでどこかに一時的に退避する
③ ①②でeccube上からcouponプラグインに関する情報が無くなったことになる。この状態でcomposer install
④`ec-cube\app\Plugin\Coupon4` を元に戻す

## エラー5 Failed opening required
ときどき、Git管理しつつDBをリセットしたりプラグインを有効化したりしていると、以下のようなエラーが出ることがあります。

```
Compile Error: ContainerHtk6t\EccubeDevDebugProjectContainer::load(): Failed opening required '/var/www/html/var/cache/dev/ContainerHxtk6vt/getConsole_Command_CacheClearService.php' (include_path='.:/usr/local/lib/php')
```

キャッシュに残ったnamespaceが古いままであるため発生するようです。以下のコマンドを叩いてcomposerのclass mapを更新しましょう。

```
docker-compose exec ec-cube composer dumpautoload
```

### 参考記事
- [Failed opening requiredエラー発生](https://umebius.com/eccube/compile-error-symfonycomponentdebugdebugclassloaderloadclass-failed-opening-required/)
- [EC-CUBE4のプラグイン作成中にCompile Error: Symfony\Component\Debug\DebugClassLoader::loadClass()エラー](http://coelacanth.jp.net/ec-cube4_debugclassloader_error/)

# 次
[EC-CUBE4 独自プラグイン開発　③今後も使えそうなTips](https://qiita.com/haruna-nagayoshi/items/3527ee12c552a339bc6b)