EC-CUBE / EC-CUBE/ec-cube

複数プラグインをまとめてインストールした後の有効化が MappingException で失敗する

Open
#6,984 0 comments 0 reactions 0 assignees View on GitHub
bug bug:Low
Dominant language
PHP
Stars
788
Forks
719
Avg merge
3d 20h
Merged PRs (30d)
45

Description

## 概要

複数のプラグインを `bin/console eccube:composer:require` でまとめてインストールした後、`bin/console eccube:plugin:enable` で順に有効化すると、**まだ有効化していない別プラグインの Entity を参照して `MappingException` が発生し、有効化に失敗する**ことがあります。

```
In MappingException.php line 26:

The class 'Plugin\RelatedProduct44\Entity\RelatedProduct' was not found in
the chain configured namespaces Eccube\Entity, Customize\Entity,
Plugin\EntityExtension\Entity, Plugin\EntityForm\Entity,
Plugin\MigrationSample\Entity, Plugin\ProductReview44\Entity
```

## 発生条件

次の 3 つが揃ったときに発生します。

1. Doctrine の trait でコアエンティティを拡張するプラグイン(例: RelatedProduct の `ProductTrait`)が **インストール済みかつ未有効** で存在する
2. `app/Plugin/` にプラグインが配置される **前** にビルドされたコンテナキャッシュ(`var/cache/`)が残っている
(`composer require` はプラグインを配置するがキャッシュを再生成しないため、**一括インストール直後は通常この状態になります**)
3. その状態で、別のプラグインを **初回** 有効化する(`Plugin::isInitialized()` が false)

## 再現手順

```bash
# 1. 複数のプラグインをまとめてインストール(有効化はしない)
bin/console eccube:composer:require ec-cube/productreview44
bin/console eccube:composer:require ec-cube/relatedproduct44
# ... 他のプラグインも同様に

# 2. キャッシュを再生成せずに順に有効化する
bin/console eccube:plugin:enable --code=ProductReview44
# => MappingException で失敗する
```

RelatedProduct を先に有効化してから ProductReview を有効化すると成功するため、**有効化の順序に依存**します。

## 原因

1. `Kernel::addEntityExtensionPass()` は `app/Plugin/` をディレクトリスキャンし、**有効・無効を問わず** `Entity` ディレクトリを持つプラグインを Doctrine の mapping に登録します。これは**コンテナのコンパイル時**に実行されるため、キャッシュがプラグイン配置前のものだと、新しく入れたプラグインの namespace は mapping chain に載りません。

https://github.com/EC-CUBE/ec-cube/blob/4.4/src/Eccube/Kernel.php#L310-L326

2. 初回有効化時、`PluginEnableCommand` は `!$plugin->isInitialized()` の分岐で `PluginService::installWithCode()` を呼びます。

https://github.com/EC-CUBE/ec-cube/blob/4.4/src/Eccube/Command/PluginEnableCommand.php#L55-L57

3. `PluginService::generateProxyAndCallback()` は、未有効プラグインの場合に **自分の namespace だけ** を mapping chain へ `addDriver()` します。

https://github.com/EC-CUBE/ec-cube/blob/4.4/src/Eccube/Service/PluginService.php#L323-L332

4. ところが直後に呼ばれる `PluginService::regenerateProxy()` は、`$temporary === true` のとき `pluginRepository->findAll()` を使うため、**未有効で mapping chain にも登録されていない他プラグインの Entity ディレクトリまで proxy 生成対象**になります。

https://github.com/EC-CUBE/ec-cube/blob/4.4/src/Eccube/Service/PluginService.php#L666-L670

5. 結果、一時ディレクトリに生成される `Product.php` に未有効プラグインの trait が混入します(生成中の一時ファイルを退避して実際に確認しました)。

```php
class Product extends AbstractEntity implements \Stringable
{
use \Plugin\RelatedProduct44\Entity\ProductTrait; // ← 未有効プラグインの trait
```

この trait は `#[ORM\OneToMany(targetEntity: RelatedProduct::class, ...)]` を持ちます。

6. `SchemaService::executeCallback()` の `getAllMetadata()` が `targetEntity` を解決しようとしますが、当該 namespace は mapping chain に無いため `MappingException` になります。

https://github.com/EC-CUBE/ec-cube/blob/4.4/src/Eccube/Service/SchemaService.php#L97-L98

つまり **「proxy 生成の対象範囲(`findAll()`)」と「Doctrine mapping chain の登録範囲」がずれている**ことが原因です。

### 補足: `findAll()` は意図的な実装

`$temporary` のときに `findAll()` を使う実装は、2018-09-12 のコミット `94319e8eb3`「プラグインのインストール順によって拡張カラムが消える問題を修正」で導入されたものです。単純に `findAllEnabled()` へ戻すと当該バグが再発すると思われるため、修正する場合は proxy 生成対象と mapping chain の登録対象を揃える方向が適切と考えます(例: `regenerateProxy()` が対象にしたプラグインの namespace も `addDriver()` する)。

## 影響バージョン

`4.0` / `4.1` / `4.2` / `4.3` / `4.4` に同じ構造が存在します(4.4 固有ではありません)。

`upstream/4.3` と `4.4` を比較したところ、上記 1〜6 の要素はすべて同一構造で、差分は Doctrine のドライバ実装のみでした(4.3 = `AnnotationDriver` / 4.4 = `TraitProxyAttributeDriver`)。またコミット `94319e8eb3` は `4.0`〜`4.4` のすべてのブランチに含まれています。

なお、**実機で再現を確認したのは 4.4 のみ**です。4.3 以前についてはコードの同一性からの判断です。

## 回避策

- プラグインを 1 本ずつ `require` → `enable` する
- 一括インストール後、有効化する前に `bin/console cache:clear` を実行する
- コアエンティティを trait で拡張するプラグインを先に有効化する

キャッシュを再生成した後は、無効なプラグインの Entity も mapping chain に載る(`bin/console doctrine:mapping:info` で確認できます)ため、この問題は発生しません。

## 検証環境

- EC-CUBE 4.4(`89dec55c49`)
- PHP 8.5.4 / Symfony 7.4.13 / SQLite
- 検証に使用したプラグイン: ProductReview44, Recommend44, RelatedProduct44, SalesReport44, Securitychecker44, MailMagazine44, Coupon44(いずれも 4.4 対応 PR のブランチ)

### 再現の確度について

DB の `enabled` / `initialized` フラグを戻すだけでは再現せず、**コンテナキャッシュの状態を「プラグイン配置前」に揃えたときのみ**発生しました(`app/Plugin/` から対象プラグインを退避 → `cache:clear` → 復元 → `enable` で、初回と同一のエラーメッセージを再現)。切り分けの際はキャッシュ状態にご注意ください。

Contributor guide

Open the contributing guide

Research direction

Start with src/Eccube/Kernel.php, src/Eccube/Command/PluginEnableCommand.php, and the generateProxyAndCallback() and regenerateProxy() paths in src/Eccube/Service/PluginService.php. Reproduce the sequence with multiple plugin installs and first-time enables, then inspect the mapping drivers and proxy generation targets. Done means the enable command succeeds without MappingException while preserving the existing proxy-generation behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
php, symfony
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.