Skip to content

Commit 30976e7

Browse files
nanasessclaude
andcommitted
fix: マスタEntityをtraitで拡張するとマスタデータ管理から消える問題を修正
Entity/Master 配下のマスタEntityをプラグインのtraitで拡張すると、 マスタデータ管理ページのプルダウンから該当テーブルが消え、さらに プラグインインストール時のschema updateがカラムを自動追加しない問題を修正する。 原因は TraitProxyAttributeDriver / ReloadSafeAttributeDriver の getAllClassNames() で、Proxyパス生成時に basename() を用いて Entity/Master 等のサブディレクトリ情報を落としていたため。 $sourceFile のパス構造ごと proxies ディレクトリへ変換するよう修正した。 - 親ドライバ TraitProxyAttributeDriver: プルダウン表示 (#5400) - 子ドライバ ReloadSafeAttributeDriver: install時の自動schema update (#6273) の両方を修正 (annotation時代の #5401 + #6274 を4.4で統合的に解決) 回帰テストを Codeception から Playwright へ移植: - MasterEntityExtension プラグイン (テストデータ) - MasterDataManagePage / MasterEntityExtensionLocal - plugin-extend.spec.ts に test_master_entity_extension を追加 - plugin-test.yml の plugin-extend マトリクスに追加 Closes #5400 Closes #6273 Refs #5401 #6274 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a387a99 commit 30976e7

8 files changed

Lines changed: 151 additions & 6 deletions

File tree

.github/workflows/plugin-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ jobs:
371371
- test_extend_same_table_disabled_remove_local
372372
- test_extend_same_table_crossed_store
373373
- test_extend_same_table_crossed_local
374+
- test_master_entity_extension
374375
include:
375376
- db: pgsql
376377
database_url: postgres://postgres:password@127.0.0.1:5432/eccube_db
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of EC-CUBE
5+
*
6+
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
7+
*
8+
* http://www.ec-cube.co.jp/
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Plugin\MasterEntityExtension\Entity;
15+
16+
use Doctrine\DBAL\Types\Types;
17+
use Doctrine\ORM\Mapping as ORM;
18+
use Eccube\Attribute\EntityExtension;
19+
use Eccube\Entity\Master\DeviceType;
20+
21+
/**
22+
* マスタデータのEntity(Entity/Master配下)をtraitで拡張するサンプル.
23+
* Issue #5400 / #6273 の回帰テスト用.
24+
*/
25+
#[EntityExtension(DeviceType::class)]
26+
trait DeviceTypeTrait
27+
{
28+
#[ORM\Column(name: 'notes', type: Types::STRING, nullable: true)]
29+
public ?string $notes = null;
30+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"ec-cube/masterentityextension","description":"エンティティ拡張機能(Master)のサンプル","version":"1.0.0","type":"eccube-plugin","require":{"ec-cube/plugin-installer":"^2.0@dev"},"extra":{"code":"MasterEntityExtension","id":10000}}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { type Page } from '@playwright/test';
2+
import { type DbClient } from '../../helpers/db-client';
3+
import { type PluginTestConfig } from '../../fixtures/plugin-test';
4+
import { LocalPlugin } from '../local-plugin';
5+
6+
/**
7+
* マスタデータのEntity(Entity/Master配下)をtraitで拡張するローカルプラグイン。
8+
* Issue #5400 / #6273 の回帰テスト用。
9+
* Codeception の MasterEntityExtension_Local に相当。
10+
*/
11+
export class MasterEntityExtensionLocal extends LocalPlugin {
12+
constructor(page: Page, db: DbClient, config: PluginTestConfig) {
13+
super(page, db, config, 'MasterEntityExtension');
14+
// Entity/Master 配下のEntityを拡張するtrait。
15+
// 有効化でこのtraitがproxyに注入され、mtb_device_type がプルダウンに残ることを検証する。
16+
this.traits.set('Plugin\\MasterEntityExtension\\Entity\\DeviceTypeTrait', 'src/Eccube/Entity/Master/DeviceType');
17+
}
18+
19+
static async start(page: Page, db: DbClient, config: PluginTestConfig): Promise<MasterEntityExtensionLocal> {
20+
return new MasterEntityExtensionLocal(page, db, config);
21+
}
22+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { type Page, expect } from '@playwright/test';
2+
3+
/**
4+
* マスタデータ管理ページ
5+
* Codeception の Page\Admin\MasterDataManagePage に相当。
6+
* Issue #5400 / #6273: Entity/Master 配下のマスタEntityをtraitで拡張しても
7+
* プルダウンから消えないことを検証する。
8+
*/
9+
export class MasterDataManagePage {
10+
/** マスタデータ選択プルダウン (form1) */
11+
static readonly SELECT_SELECTOR = '#admin_system_masterdata_masterdata';
12+
/** 更新完了フラッシュ */
13+
static readonly ALERT_SUCCESS_SELECTOR = '.c-contentsArea .alert-success';
14+
15+
constructor(private readonly page: Page) {}
16+
17+
static async go(page: Page, adminRoute = 'admin'): Promise<MasterDataManagePage> {
18+
await page.goto(`/${adminRoute}/setting/system/masterdata`);
19+
await expect(page.locator(MasterDataManagePage.SELECT_SELECTOR)).toBeVisible({ timeout: 30_000 });
20+
return new MasterDataManagePage(page);
21+
}
22+
23+
/**
24+
* プルダウンに指定ラベル(テーブル名)の選択肢が存在するか。
25+
* バグがある場合は拡張したマスタのテーブル名が選択肢から消えるため false になる。
26+
*/
27+
async 選択肢が存在する(label: string): Promise<boolean> {
28+
const count = await this.page
29+
.locator(MasterDataManagePage.SELECT_SELECTOR)
30+
.locator('option', { hasText: label })
31+
.count();
32+
return count > 0;
33+
}
34+
35+
/**
36+
* マスタデータを選択して form1 を submit し、編集フォーム(form2)を表示する。
37+
* 選択肢が存在しない場合は selectOption がタイムアウトして回帰を検知する。
38+
*/
39+
async 選択(label: string): Promise<this> {
40+
await expect(
41+
this.page.locator(MasterDataManagePage.SELECT_SELECTOR).locator('option', { hasText: label }),
42+
`マスタデータのプルダウンに ${label} が存在する`,
43+
).toHaveCount(1);
44+
45+
await this.page.locator(MasterDataManagePage.SELECT_SELECTOR).selectOption({ label });
46+
await this.page.locator('#form1 button[type="submit"]').click();
47+
await this.page.waitForLoadState('load');
48+
49+
// 選択後は編集フォーム(form2)が表示される
50+
await expect(this.page.locator('#form2')).toBeVisible({ timeout: 30_000 });
51+
52+
return this;
53+
}
54+
55+
/**
56+
* 編集フォーム(form2)を保存し、更新完了メッセージを確認する。
57+
*/
58+
async 保存(): Promise<this> {
59+
await this.page.locator('#form2 .c-conversionArea button[type="submit"]').click();
60+
await this.page.waitForLoadState('load');
61+
await expect(this.page.locator(MasterDataManagePage.ALERT_SUCCESS_SELECTOR)).toContainText('保存しました', {
62+
timeout: 30_000,
63+
});
64+
65+
return this;
66+
}
67+
}

e2e/tests/plugin-extend.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { HorizonStore } from '../models/plugins/horizon-store';
33
import { HorizonLocal } from '../models/plugins/horizon-local';
44
import { BoomerangStore } from '../models/plugins/boomerang-store';
55
import { BoomerangLocal } from '../models/plugins/boomerang-local';
6+
import { MasterEntityExtensionLocal } from '../models/plugins/master-entity-extension-local';
7+
import { MasterDataManagePage } from '../pages/master-data-manage.page';
68
import { emptyDir } from '../helpers/tar-helper';
79

810
test.describe('Plugin Extend', () => {
@@ -111,4 +113,22 @@ test.describe('Plugin Extend', () => {
111113
await boomerang.無効化();
112114
await boomerang.削除();
113115
});
116+
117+
// Issue #5400 / #6273: Entity/Master 配下のマスタEntityをtraitで拡張すると、
118+
// マスタデータ管理ページのプルダウンから拡張したテーブルが消えてしまう回帰の防止。
119+
test('test_master_entity_extension', async ({ page, db, config }) => {
120+
const plugin = await MasterEntityExtensionLocal.start(page, db, config);
121+
122+
await plugin.インストール();
123+
await plugin.有効化();
124+
125+
// 拡張したマスタデータ(mtb_device_type)がプルダウンに残っており、選択・保存できること
126+
const masterPage = await MasterDataManagePage.go(page, config.adminRoute);
127+
expect(await masterPage.選択肢が存在する('mtb_device_type'), 'mtb_device_type がプルダウンに存在する').toBe(true);
128+
await masterPage.選択('mtb_device_type');
129+
await masterPage.保存();
130+
131+
await plugin.無効化();
132+
await plugin.削除();
133+
});
114134
});

src/Eccube/Doctrine/ORM/Mapping/Driver/ReloadSafeAttributeDriver.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,11 @@ public function getAllClassNames(): ?array
9999
$projectDir = str_replace('\\', '/', $projectDir);
100100
}
101101

102-
// Replace /path/to/ec-cube to proxies path
103-
$proxyFile = str_replace($projectDir, $this->trait_proxies_directory, $path).'/'.basename((string) $sourceFile);
104-
if (file_exists($proxyFile)) {
102+
// Replace /path/to/ec-cube to proxies path.
103+
// Entity/Master 等のサブディレクトリ配下のクラスでもProxyパスが正しく解決されるよう、
104+
// basename()でファイル名だけに丸めず$sourceFileのパス構造ごと変換する(Issue #5400 / #6273).
105+
$proxyFile = str_replace($projectDir, $this->trait_proxies_directory, (string) $sourceFile);
106+
if ($proxyFile !== $sourceFile && file_exists($proxyFile)) {
105107
$sourceFile = $proxyFile;
106108
}
107109

src/Eccube/Doctrine/ORM/Mapping/Driver/TraitProxyAttributeDriver.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ public function getAllClassNames(): ?array
7878
$sourceFile = str_replace('\\', '/', $sourceFile);
7979
$projectDir = str_replace('\\', '/', $projectDir);
8080
}
81-
// Replace /path/to/ec-cube to proxies path
82-
$proxyFile = str_replace($projectDir, $this->trait_proxies_directory, $path).'/'.basename((string) $sourceFile);
83-
if (file_exists($proxyFile)) {
81+
// Replace /path/to/ec-cube to proxies path.
82+
// Entity/Master 等のサブディレクトリ配下のクラスでもProxyパスが正しく解決されるよう、
83+
// basename()でファイル名だけに丸めず$sourceFileのパス構造ごと変換する(Issue #5400 / #6273).
84+
$proxyFile = str_replace($projectDir, $this->trait_proxies_directory, (string) $sourceFile);
85+
if ($proxyFile !== $sourceFile && file_exists($proxyFile)) {
8486
require_once $proxyFile;
8587

8688
$sourceFile = $proxyFile;

0 commit comments

Comments
 (0)