テスト環境で EntityManager::lock() が TransactionRequiredException になり、キャンセル遷移を含む受注編集の Web テストが書けない
- Dominant language
- PHP
- Stars
- 788
- Forks
- 719
- Avg merge
- 3d 20h
- Merged PRs (30d)
- 45
Description
## 概要
テスト環境(`APP_ENV=test` + `dama/doctrine-test-bundle`)では、受注ステータスを「キャンセル」等へ遷移させる Web テストが `Doctrine\ORM\TransactionRequiredException` で必ず失敗します。
`StockReduceProcessor` が在庫を戻す際の悲観ロック(`EntityManager::lock()`)が「開いたトランザクション」を要求しますが、テスト環境ではそれが満たされないためです。本番の実リクエストでは発生しません。
そのため、**キャンセル遷移を含む受注編集の Web テストが本体・プラグインとも書けない**状態になっています。
## 再現環境
EC-CUBE 4.4(`ghcr.io/ec-cube/ec-cube-php:8.2-apache-4.4`)
| パッケージ | バージョン |
|---|---|
| PHP | 8.2.31 |
| symfony/framework-bundle | v7.4.8 |
| doctrine/orm | 3.6.2 |
| doctrine/dbal | 3.10.5 |
| dama/doctrine-test-bundle | v8.6.0 |
| phpunit/phpunit | 11.5.55 |
## 再現手順
`APP_ENV=test` で、管理画面の受注編集(`admin_order_edit`)に受注ステータス「キャンセル」を含むフォームを POST する Web テストを実行する。
(クーポンプラグイン側の実測ケース: `Plugin\Coupon44\Tests\Web\Admin\OrderControllerTest::testOrderEditWithCouponCancel`。当該ケースはこの問題のため現在スキップしています → EC-CUBE/coupon-plugin#197)
## 実測結果
```
Doctrine\ORM\TransactionRequiredException: An open transaction is required for this operation.
/var/www/html/vendor/doctrine/orm/src/TransactionRequiredException.php:19
/var/www/html/vendor/doctrine/orm/src/UnitOfWork.php:2261
/var/www/html/vendor/doctrine/orm/src/EntityManager.php:485
/var/www/html/src/Eccube/Service/PurchaseFlow/Processor/StockReduceProcessor.php:82
/var/www/html/src/Eccube/Service/PurchaseFlow/Processor/StockReduceProcessor.php:56
/var/www/html/src/Eccube/Service/OrderStateMachine.php:155
...
/var/www/html/src/Eccube/Service/OrderStateMachine.php:48
/var/www/html/src/Eccube/Controller/Admin/Order/EditController.php:182
```
(例外をキャッチさせない設定で採取。既定の Web テストでは 500 レスポンス =「システムエラーが発生しました。」となり、リダイレクトのアサーションが失敗します)
## 原因
1. `src/Eccube/Service/PurchaseFlow/Processor/StockReduceProcessor.php:82` が在庫を戻すため悲観ロックを掛ける
```php
$this->entityManager->lock($productStock, LockMode::PESSIMISTIC_WRITE);
```
2. ORM 3 の `UnitOfWork::lock()` は、`PESSIMISTIC_READ` / `PESSIMISTIC_WRITE` の場合に **DBAL `Connection` レベル**でトランザクションが開いていることを要求する
```php
case $lockMode === LockMode::PESSIMISTIC_READ:
case $lockMode === LockMode::PESSIMISTIC_WRITE:
if (! $this->em->getConnection()->isTransactionActive()) {
throw TransactionRequiredException::transactionRequired();
}
```
3. `dama/doctrine-test-bundle` は **ドライバ層の接続**でトランザクションを開始する(`src/Doctrine/DBAL/StaticDriver.php`)。DBAL `Connection` 自身のトランザクション入れ子カウンタは 0 のままなので、`isTransactionActive()` は `false` を返す
```php
if (!isset(self::$connections[$key])) {
self::$connections[$key] = parent::connect($params);
self::$connections[$key]->beginTransaction();
}
```
4. さらに `app/config/eccube/services_test.yaml:12-16` でテスト時は `TransactionListener` が無効化されているため、リクエスト単位で DBAL レベルのトランザクションが開かれることもない
```yaml
# テスト時はTransactionListenerを無効にする
Eccube\EventListener\TransactionListener:
arguments:
- '@doctrine.orm.default_entity_manager'
- false
```
結果として、テスト環境でのみ `lock()` が例外になります。
## 影響
- 受注ステータスをキャンセル(在庫を戻す遷移)へ変更する Web テストが書けない
- 本体側にも同種のテストがある場合は同じ問題に当たると思われます
- 本番の実リクエストでは `TransactionListener` が有効なため発生しません(機能不具合ではなく、テストハーネスの制約)
## 考えられる対応案
いずれが妥当かご判断いただきたく、案として挙げます。
1. テスト環境でも DBAL レベルのトランザクションが開くようにする(例: テスト用に `TransactionListener` 相当を有効化する、あるいはテスト基盤側で `beginTransaction()` を DBAL `Connection` に対して行う)
2. `StockReduceProcessor` の悲観ロックを、テスト環境では省略できる形にする(設定・環境変数などで切り替え)
3. テストハーネスの制約として明文化し、キャンセル遷移の検証は Web テストではなくユニットテスト(Processor を直接呼ぶ)で行う方針をドキュメント化する
クーポンプラグイン側(EC-CUBE/coupon-plugin#197)では暫定的に当該 Web テストをスキップし、`CouponStateProcessor` を直接呼ぶユニットテストで代替しています。
Contributor guide
Research direction
Start by reproducing the failure in the named Coupon44 Web test and read StockReduceProcessor.php, OrderStateMachine.php, EditController.php, services_test.yaml, and the test bundle's StaticDriver.php. Compare transaction handling in test and production, then define and verify a chosen approach that lets the cancellation Web test complete without breaking transaction isolation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php, symfony
- Domain
- backend, database, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 45/100