diff --git a/.gitignore b/.gitignore index 99e14dd88ef..0d6ca048125 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ node_modules !/var/.htaccess /app/Plugin/* !/app/Plugin/.gitkeep +!/app/Plugin/README.html +!/app/Plugin/README.md /app/PluginData/* !/app/PluginData/.gitkeep /app/keystore/* diff --git a/AGENTS.md b/AGENTS.md index 3591ebc3fb1..549d67a03b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,8 @@ AI エージェント向けの情報は、この `AGENTS.md` を**正典(ハ | `GEMINI.md` | 薄いポインタ。Gemini CLI は Skill 非対応のため索引経由で `SKILL.md` へ誘導 | Gemini CLI | | `llms.txt` | 外部 LLM・クローラ向けの英語サマリ(llmstxt.org 準拠。公開 URL 前提) | LLM クローラ・外部 LLM | | `.claude/skills//SKILL.md` | レイヤ別の詳細規約(末端)。`.codex/skills`・`.agents/skills` は symlink 共有 | Skill 対応ツール(詳細は「Skill の配置と各ツールの読み込み」節) | +| `<機能ディレクトリ>/README.html` | コード近接の**人間向けの仕様書**(挙動・なぜ・図表)。`data-section`/`data-customer` で章立て(顧客提出フィルタ用) | 人間(開発者・新規参加者・顧客) | +| `<機能ディレクトリ>/README.md` | コード近接の**短い索引**。GitHub のディレクトリビューで自動表示され、`README.html`(人間向け仕様)と `SKILL.md`(AI 向け規約)への道標を兼ねる | GitHub 閲覧者・AI エージェント | ### 定義ファイルを増やすときの原則 @@ -29,6 +31,27 @@ AI エージェント向けの情報は、この `AGENTS.md` を**正典(ハ - 新しい定義ファイルを足すときは、上のインデックス表にも 1 行追加し、所在を本節で一元管理する。 - 設計思想・アーキテクチャの詳細文書(`DESIGN.md` / `ARCHITECTURE.md`)は現状未整備。整備する場合も本ファイルはハブに留め、詳細はそれらへリンクして本節の表に追記する。 +### コード近接ドキュメント(README.html / README.md) + +主要な機能ディレクトリ(`src/Eccube/` の各レイヤ根や高複雑サブシステム、一部 `app/`)には、コードと同じ場所へ 2 ファイルを置く。 +**読者別に 3 層へ分離し、同じ事柄を二度書かず相互にリンクする**(Issue #6906)。 + +| 成果物 | 主読者 | 役割 | +|---|---|---| +| `README.html` | 人間(開発者・新規参加者・顧客) | **真の仕様書**(挙動・なぜ・図表)。`
` で章立てし、顧客提出時は `data-customer="true"` の章だけ抽出できる | +| `README.md` | GitHub 閲覧者・AI エージェント | **短い索引**。要点+ `README.html`(人間向け仕様)と `SKILL.md`(AI 向け規約)へのリンク | +| `.claude/skills//SKILL.md` | AI エージェント | **実装の書き方ルール**(規約・DO/DON'T)。仕様説明は `README.html` に委ね相互リンク | + +- **棲み分け**: `README.html`=「機能の仕様(人間向け)」、`SKILL.md`=「コードの書き方(AI 向け)」。読者と目的が異なるため両立する。 +- **入口(TOP ページ)**: リポジトリルートの [`README.html`](./README.html) が全 `README.html` へのポータル(目次)。ビルド不要でブラウザで開ける。新しい `README.html` を配置したら、この目次にも 1 行追加する(漏れは CI `docs-check` が検出)。 +- **参照トポロジ**(既存の一方向ルールを維持): `README.md`(索引)→ `README.html`(仕様)/ `SKILL.md`(規約)→ `AGENTS.md`(正典)。 + 上流(`AGENTS.md`)から個別 README への下向き内容参照は足さない。 +- **ドメイン詳細は複製しない**: 受注 ER・ステートマシン・計算仕様など doc4 に既出のものは `README.html` から `https://doc4.ec-cube.net/` へリンクする。 +- **テンプレートと粒度**の基準はパイロット [`src/Eccube/Service/PurchaseFlow/README.html`](./src/Eccube/Service/PurchaseFlow/README.html)(+ 同ディレクトリの `README.md`)。新規配置時はこれを金型にする。 + - `README.html` は自己完結 HTML。共通 CSS を将来当てられるよう過度なインライン装飾を避け、`data-customer="true"`=顧客提出にも載る章 / `"false"`=開発者向け(拡張・内部注意点)で振り分ける。 + - Skill が無いディレクトリ(例 `Doctrine/` `DependencyInjection/` `Attribute/` `Service/AgentCommerce/`)は `README.md` の SKILL 行を省き `README.html` のみを指す。 +- **集約出力・鮮度維持**: `bin/console eccube:docs:export [--filter=customer]` で全 `README.html` を集約出力できる(`--filter=customer` は `data-customer="true"` の章だけ抽出)。鮮度は PR テンプレートのチェック項目と CI(`docs-check`)で維持する。静的サイト化・本番公開先(doc4 への統合を含む)は **Issue #6906** で検討(未確定)。 + ## プロジェクト概要 EC-CUBE は日本で広く使われる OSS の EC プラットフォームです。本ブランチ(4.4)は **Symfony 7.4 / PHP 8.2+** 上に構築されています。 diff --git a/README.html b/README.html new file mode 100644 index 00000000000..bf9975a5bef --- /dev/null +++ b/README.html @@ -0,0 +1,100 @@ + + + + + +EC-CUBE 仕様書ポータル + + + + +

EC-CUBE 仕様書ポータル

+ + +
+

このページについて

+

EC-CUBE の主要な機能ディレクトリには、コードと同じ場所へ README.html(人間向けの仕様書)と + README.md(GitHub 表示用の短い索引)を配置しています(Issue #6906・AGENTS.md §8.1)。 + このページは、それら各ディレクトリの README.html への入口(TOP ページ)です。

+

各 README.html は自己完結した HTML 文書なので、リポジトリを取得したあと + ビルド不要でそのままブラウザで開けます。下の表のディレクトリ名をクリックすると、そのディレクトリの仕様へ移動します。

+
実装の書き方ルール(AI・開発者向けの規約 / DO・DON'T)は各 README.html からリンクする + .claude/skills/<name>/SKILL.md が正典です。README.html=「機能の仕様」、SKILL.md=「コードの書き方」で棲み分けます。
+
+ +
+

コアレイヤ(src/Eccube/)

+ + + + + + + + + + + + + + + +
ディレクトリ役割
Controller/HTTP コントローラ(管理画面・フロント)
Entity/Doctrine エンティティ(#[ORM\…] 属性マッピング)
Repository/データアクセス(クエリ・検索)
Form/フォーム(FormType・拡張・バリデーション・値変換)
Service/ビジネスロジックの受け皿
EventListener/イベントリスナ
Security/認証・認可
Twig/Extension/Twig 拡張(Filter・Function)
Command/コンソールコマンド(バッチ)
Plugin/プラグイン管理
Attribute/PHP 属性定義(フロー宣言 等)
DependencyInjection/DI 拡張・コンパイラパス
Doctrine/Doctrine 関連(マッピング・ライフサイクル支援)
+
+ +
+

高複雑サブシステム

+ + + + + +
ディレクトリ役割
Service/PurchaseFlow/受注処理パイプライン(計算・検証・確定)
Controller/Admin/Order/管理画面の受注管理
Service/AgentCommerce/エージェントコマース(ACP / UCP)基盤
+
+ +
+

AgentCommerce サブドメイン(src/Eccube/Service/AgentCommerce/)

+ + + + + + + + + + +
サブドメイン役割
Catalog/カタログ(商品フィード)
CheckoutSession/チェックアウトセッション
Discovery/ディスカバリ(capability 提示)
Exception/例外
Fulfillment/フルフィルメント(出荷)
Idempotency/冪等性(重複実行の防止)
Payment/決済
Security/セキュリティ(認証・スコープ・メッセージ署名・鍵の保管)
+
+ +
+

プロジェクト領域(app/)

+ + + + + +
ディレクトリ役割
app/Customize/プロジェクト固有のカスタマイズ(アップグレード安全)
app/DoctrineMigrations/DB マイグレーション
app/Plugin/インストール済みプラグイン
+
+ + + diff --git a/README.md b/README.md index 4f7c8402f86..b2b73878f1d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ + 本体開発にあたって不明点などあれば[Issue](https://github.com/EC-CUBE/ec-cube/wiki/Issues%E3%81%AE%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95)をご利用下さい。 + EC-CUBE 3系の保守については、 [EC-CUBE/ec-cube3](https://github.com/EC-CUBE/ec-cube3/)にて開発を行っております。 + EC-CUBE 2系の保守については、 [EC-CUBE/ec-cube2](https://github.com/EC-CUBE/ec-cube2/)にて開発を行っております。 ++ コード近接の仕様書(各機能ディレクトリの `README.html`)の入口は [`README.html`(仕様書ポータル)](README.html) です。ビルド不要でブラウザで開けます。 ## インストール @@ -52,7 +53,7 @@ JavaScript のライブラリは esbuild でバンドル/minifyされます。 バンドルするライブラリを変更する場合は、テンプレートごとに以下の bundle.js を修正し、リビルドしてください。 - [html/template/admin/assets/js/bundle.js](html/template/admin/assets/js/bundle.js) - [html/template/default/assets/js/bundle.js](html/template/default/assets/js/bundle.js) -- [html/template/install/assets/js/bundle.js](html/template/default/install/js/bundle.js) +- [html/template/install/assets/js/bundle.js](html/template/install/assets/js/bundle.js) ```shell npm ci # 初回およびpackage-lock.jsonに変更があったとき diff --git a/app/Customize/README.html b/app/Customize/README.html new file mode 100644 index 00000000000..4c9e8cf4ecd --- /dev/null +++ b/app/Customize/README.html @@ -0,0 +1,91 @@ + + + + + +app/Customize — プロジェクト固有カスタマイズ 仕様 + + + + +

app/Customize — プロジェクト固有カスタマイズ

+ + +
+

概要

+

app/Customize/ は、プロジェクト固有の改変を置く場所です。コア(src/Eccube/)を + 直接書き換えず、ここで拡張・上書きすることで、コア改変を避けます。PSR-4 で + Customize\ = app/Customize/ にマッピングされ、autowire / autoconfigure 済みで登録されます。

+ +

置けるものはコントローラ・エンティティ拡張・フォーム拡張・リポジトリ・サービス・Twig など。 + リポジトリに実在する骨組みは次の通り(各レイヤは必要に応じて足す)。

+
app/Customize/ + ├── Controller/ # #[Route] を持つコントローラ(追加) + ├── Entity/ # コアエンティティ拡張トレイト(#[EntityExtension]) + └── Resource/ + ├── config/ # サービス定義等 + └── locale/ # 翻訳
+
+ +
+

Plugin との使い分け

+

プロジェクト固有・1 回限りの改変は app/Customize/、着脱・再配布できる機能は + app/Plugin/。作法(トレイト拡張・proxy 再生成等)は共通ですが、位置づけが異なります。

+ + + + + + +
app/Customize/(本ディレクトリ)app/Plugin/{Code}/
名前空間Customize\Plugin\{Code}\(独立)
想定プロジェクト固有・1 回限り着脱・再配布できる機能パッケージ
ライフサイクルなし(常時有効)install/enable/disable/uninstall あり
メタデータなしcomposer.json の extra.code 必須
+
「アップグレード安全」は限定的。効くのはパッチ更新まで。マイナー/メジャー更新ではコア変更で破綻し得ます。 + サービス/テンプレートを override すると、コアに当たったセキュリティパッチが自動反映されず個別再適用が必要になるため、override は最小限に。
+
+ +
+

拡張・上書きのパターン開発者向け

+

実装の書き方・DO/DON'T は eccube-customize/SKILL.md が正典。ここでは全体像だけ示します。

+ + + + + + + +
やりたいこと置き場所 / 方法
コアエンティティにカラム追加Entity/*Trait.php に #[EntityExtension(対象::class)]+#[ORM\Column](追加のみ。既存の差し替えは不可)
既存フォームに項目追加Form/Extension/ に AbstractTypeExtension+getExtendedTypes()
コアサービスの振る舞い変更Service/ + services.yaml の decorates/@.inner でデコレーション
コントローラ追加Controller/ に #[Route](customize_controllers が属性走査)
テンプレート上書きapp/template/ のコアと同じ相対パスに同名ファイル(フロント {テーマコード}/、管理画面 admin/)
+
エンティティ拡張トレイトを足したら bin/console eccube:generate:proxies で proxy 再生成。 + #[EntityExtension] を付け忘れると proxy に乗らず、カラムが認識されません。
+
+ +
+

注意点開発者向け

+
    +
  • コアを直接書き換えない。拡張・上書きはすべてここで行い、アップグレード安全(限定的)を保つ。
  • +
  • コアエンティティは置換不可。トレイト+#[EntityExtension] による追加のみ。
  • +
  • カラム追加に ALTER マイグレーションは不要(属性が源泉。schema:update --force が反映)。型変更・データ投入は app/DoctrineMigrations/。
  • +
  • サービス上書きは #[AsDecorator] ではなく services.yaml の decorates(コアの作法に合わせる)。
  • +
  • テンプレートはコア原本を編集しない。app/template/ に同じ相対パスで同名ファイルを置くと app 側が優先される。
  • +
  • カスタムのサービス/バンドル定義は app/Customize/Resource/config/ に置く。Kernel が services.(php|yaml)(+services_{env})を glob で自動ロードし(Kernel::configureContainer())、bundles.php があれば追加 Symfony バンドルも登録する(同 registerBundles())。decorates を書く services.yaml の置き場所はここ。コントローラのルートは属性走査(customize_controllers)だが、この Resource/config は Customize のルート読み込み対象ではない点に注意。
  • +
+
+ + + diff --git a/app/Customize/README.md b/app/Customize/README.md new file mode 100644 index 00000000000..d6ac62b6a7b --- /dev/null +++ b/app/Customize/README.md @@ -0,0 +1,22 @@ +# app/Customize — プロジェクト固有カスタマイズ + +コア(`src/Eccube/`)を直接書き換えず、プロジェクト固有の改変を拡張・上書きで行う場所。 +PSR-4 で `Customize\` = `app/Customize/` にマッピングされ、autowire / autoconfigure 済み。 +着脱・再配布できる機能は `app/Plugin/` を使う(こちらは 1 回限りの改変向け)。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-customize/SKILL.md`](../../.claude/skills/eccube-customize/SKILL.md) +- 🧩 着脱式の機能: [`app/Plugin/`](../Plugin/README.md) + +## 主要ディレクトリ + +- `Controller/` — `#[Route]` を持つコントローラの追加(`customize_controllers` が属性走査) +- `Entity/` — コアエンティティ拡張トレイト(`#[EntityExtension(対象::class)]` + `#[ORM\Column]`。追加のみ) +- `Resource/config/` — サービス定義等 +- `Resource/locale/` — 翻訳 +- テンプレート上書きは `app/Customize/` ではなく `app/template/`(コアと同じ相対パスに同名ファイル) + +## 注意 + +- 「アップグレード安全」はパッチ更新まで。override はコアのパッチ再適用が要るため最小限に。 +- カラム追加にマイグレーションは不要(属性+`schema:update`)。型変更・データ投入は `app/DoctrineMigrations/`。 diff --git a/app/DoctrineMigrations/README.html b/app/DoctrineMigrations/README.html new file mode 100644 index 00000000000..a6d9aaaf9a7 --- /dev/null +++ b/app/DoctrineMigrations/README.html @@ -0,0 +1,102 @@ + + + + + +app/DoctrineMigrations — マイグレーション 仕様 + + + + +

app/DoctrineMigrations — マイグレーション

+ + +
+

概要

+

このディレクトリは、コア本体の DB マイグレーション(VersionYYYYMMDDHHMMSS.php)を置く場所です。 + 名前空間は DoctrineMigrations、各クラスは final class VersionYYYYMMDDHHMMSS extends AbstractMigration。 + 適用状況は doctrine_migration_versions テーブルが管理します(doctrine-migrations-bundle 既定名。migration_versions は旧称で、EC-CUBE は table_name を上書きしていない)。

+ +
最重要: スキーマの源泉は Entity の属性(#[ORM\...])であって、マイグレーションではない。 + マイグレーションは「属性+schema:update では届かないもの」を補うためだけに書きます。
+
+ +
+

いつ書くか(=ほとんど書かない)

+

EC-CUBE のスキーマ更新は 2 段構えです。単純なカラム追加・変更は ① が自動反映するため、 + マイグレーションは不要です。

+
① doctrine:schema:update --force … Entity 属性から導けるカラム追加・変更を反映 +② doctrine:migrations:migrate … ① で扱えないもの(INSERT・型変更等)を適用
+ + + + + +
ケースマイグレーション
カラム追加(属性を足すだけ)不要 ① が反映する
マスタ/初期データの INSERT(mtb_* / dtb_block / dtb_mail_template 等)必要 ② で投入
型変更・リネーム・データ移行を伴う構造変更必要 ② で適用
+
マスタ/初期データは「CSV + マイグレーション」の両方が必要。 + src/Eccube/Resource/doctrine/import_csv/ はインストール時にしか読まれないため、既存環境へは INSERT マイグレーションで届ける。 + 新規インストールは CSV から入る。両方を同一 PR で行う。
+
+ +
+

作法開発者向け

+

実装の詳細は eccube-migration/SKILL.md が正典。要点のみ。

+
    +
  • 生成は bin/console doctrine:migrations:generate で空の雛形を作り、必要な SQL を手で書く。 + doctrine:migrations:diff は使わない(カラム差分まで ALTER 化し、原則と矛盾するため)。
  • +
  • up() と down() を対で実装する(確実に巻き戻せること)。ただし旧列の削除を伴うデータ移行など、元に戻せないものは + down() を空にする(例: Version20260924000000 は在庫数の旧列を削除するため巻き戻せない)。
  • +
  • ① と ② の実行順に依存させない。データ移行を伴う構造変更では、schema:update が先に旧列を消すとデータが失われる。 + 旧列をスキーマ生成の結果に残す仕組み(例: LegacyProductClassStockColumnSubscriber)と組み合わせ、どちらを先に実行しても同じ結果にする。
  • +
  • 冪等にする: $schema->hasTable() / $table->hasColumn() や SELECT COUNT(*) で存在確認し、適用済みなら早期 return。
  • +
  • PHP ファイル先頭に EC-CUBE ライセンスヘッダ。PostgreSQL / MySQL 双方で動く SQL を意識する。
  • +
+
final class Version20240101000000 extends AbstractMigration +{ + public function up(Schema $schema): void + { + $count = $this->connection->fetchOne("SELECT COUNT(*) FROM mtb_sale_type WHERE id = 3"); + if ($count > 0) { return; } // 冪等 + $this->addSql("INSERT INTO mtb_sale_type (id, name, sort_no, discriminator_type) + VALUES (3, '定期購入', 3, 'saletype')"); + } + public function down(Schema $schema): void + { + $this->addSql("DELETE FROM mtb_sale_type WHERE id = 3"); + } +}
+
+ +
+

注意点開発者向け

+
    +
  • ❌ カラムを足したので ALTER TABLE ... ADD COLUMN を書く → ✅ 属性を足すだけ。schema:update が反映する。
  • +
  • ❌ diff で ALTER を自動生成 → ✅ generate で空雛形+手書き。
  • +
  • ❌ マイグレーションでテーブルを"新規定義"して源泉にする → ✅ 源泉は Entity 属性。
  • +
  • ❌ INSERT・構造変更でガードなし → ✅ 存在チェックで冪等に。
  • +
  • プラグインのマイグレーションはここに置かない。各プラグインの DoctrineMigrations/(migration_{code} テーブル)で別管理。
  • +
  • STI テーブル(mtb_* / dtb_block 等)への INSERT は discriminator_type 列の指定が必須。
  • +
  • 「カラム追加=不要」は原則だが、コアに反例が実在する。Version20260316234241 は dtb_base_info に ALTER TABLE ... ADD option_guest_purchase でカラムを追加している(属性追加だけで済ませていない)。単純なカラム追加でも運用都合で ALTER を添えることはあり、「カラム追加なのに ALTER がある=規約違反」と機械的に断じない。
  • +
+
+ + + diff --git a/app/DoctrineMigrations/README.md b/app/DoctrineMigrations/README.md new file mode 100644 index 00000000000..2b4736260d3 --- /dev/null +++ b/app/DoctrineMigrations/README.md @@ -0,0 +1,22 @@ +# app/DoctrineMigrations — マイグレーション + +コア本体の DB マイグレーション(`VersionYYYYMMDDHHMMSS.php`、名前空間 `DoctrineMigrations`)を置く場所。 +**スキーマの源泉は Entity 属性(`#[ORM\...]`)であってマイグレーションではない**ため、 +単純なカラム追加は不要(`schema:update` が反映)。書くのは INSERT(マスタ/初期データ)と型変更等に限る。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-migration/SKILL.md`](../../.claude/skills/eccube-migration/SKILL.md) + +## 主要ファイル / 実例 + +- `Version20240312170000.php` — `dtb_block` に「新着商品」ブロックを INSERT(CSV 追記とセット運用の実例) +- `Version20220603074035.php` — `mtb_csv_type` のマスタ追加(INSERT マイグレーション) +- `Version20230515023836.php` — `dtb_mail_template` へのレコード投入 +- `Version20260316234241.php` — 構造変更の例(`hasColumn` ガード付きで `ALTER ... ADD`) +- `Version20260924000000.php` — データ移行を伴う構造変更の例(在庫数の旧列から補完して削除。巻き戻せないため `down()` は空) +- 命名は `VersionYYYYMMDDHHMMSS.php`。`up()`/`down()` を対で実装し(巻き戻せないものは `down()` を空に)、存在チェックで冪等にする。 + +## 注意 + +- 生成は `doctrine:migrations:generate`(空雛形+手書き)。`diff` は使わない。 +- プラグインのマイグレーションはここではなく各プラグインの `DoctrineMigrations/`(`migration_{code}` テーブル)。 diff --git a/app/Plugin/README.html b/app/Plugin/README.html new file mode 100644 index 00000000000..d6eb4c3158d --- /dev/null +++ b/app/Plugin/README.html @@ -0,0 +1,94 @@ + + + + + +app/Plugin — プラグイン設置先 仕様 + + + + +

app/Plugin — プラグイン設置先

+ + +
+

概要

+

app/Plugin/ は、インストール済みプラグインが置かれる場所です。 + プラグイン 1 個が app/Plugin/{PluginCode}/ の 1 ディレクトリに対応し、PSR-4 で + Plugin\{PluginCode}\ = app/Plugin/{PluginCode}/ にマッピングされます。

+ +

プラグインは eccube:plugin:install(コンソール)や管理画面から導入され、その結果としてここへ展開されます。 + ただし ECCUBE_RESTRICT_FILE_UPLOAD=1 の環境では管理画面からの導入・有効化・無効化・更新・削除は 403 になり、コンソールが唯一の導線です(Web サーバーが app/Plugin へ書けない構成向け)。 + したがって中身は環境ごとに異なります(何が置かれるかはインストール状況次第)。

+ +
Git 管理: 中身は .gitkeep を除き .gitignore 済み(/app/Plugin/*)。 + 素の状態では空で、実運用のプラグインは環境依存のためコミットされません。 + 例外として、リポジトリには PHPUnit のテスト用サンプルプラグイン(HogePlugin・EntityExtension・ + MigrationSample 等)が明示追跡でコミットされています。これらはテストの検証材料であり、実運用の雛形ではありません。
+
+ +
+

ライフサイクル(ここに置かれるまで/消えるまで)

+

このディレクトリの内容は、プラグインのライフサイクル操作によって出入りします。状態は dtb_plugin テーブルが持ちます。

+
install … Composer 経由で取得し app/Plugin/{code}/ に展開(直後は enabled=false) +enable … 有効化。マイグレーション適用・プロキシ再生成が走る +disable … 無効化(ファイルは残る) +update … 差分適用(PluginManager::update) +uninstall … 取り外し。app/Plugin/{code}/ ごと削除され得る
+
開発時の事故防止: app/Plugin/{code}/ 直下で直接開発すると、 + uninstall のテストをした瞬間にソースごと消えます。実開発は別ディレクトリで行い、シンボリックリンクで配置するのが安全 + (詳細は eccube-plugin/SKILL.md)。
+
+ +
+

プラグインの構成開発者向け

+

1 つのプラグインが持つ最小構成と、主な拡張点の置き場所です。実装の書き方は + eccube-plugin/SKILL.md が正典。ここでは配置だけ示します。

+
app/Plugin/{PluginCode}/ + ├── composer.json # 必須(version と extra.code が必須) + ├── PluginManager.php # 任意(ライフサイクル処理が要るときだけ) + ├── Controller/ # #[Route] でルーティング + ├── Entity/*Trait.php # #[EntityExtension(Target::class)] でコア拡張 + ├── Form/Extension/ # AbstractTypeExtension で既存フォーム拡張 + ├── Repository/ Service/ EventListener/ + ├── DoctrineMigrations/Version* # プラグイン専用(migration_{code} で管理) + └── Resource/template/ # プラグインの Twig
+ + + + + +
制約内容
PluginCode^\w+$(英数字・アンダースコアのみ。- 不可)。ディレクトリ名・名前空間・クラス名に使われる
composer.jsonversion と extra.code が必須。extra.code が無いと install で失敗
雛形生成手書きせず bin/console eccube:plugin:generate <name> <code> <ver> で骨組みを作る
+
+ +
+

注意点開発者向け

+
    +
  • 名前空間を混同しない: プラグインは Plugin\{Code}\(独立名前空間)。プロジェクト固有の 1 回限りの改変は Customize\(app/Customize/)。
  • +
  • install しただけでは動かない。eccube:plugin:enable --code=... で有効化する。
  • +
  • エンティティ拡張トレイトを足したら bin/console eccube:generate:proxies でプロキシ再生成(enable/disable/uninstall 時はコアが自動再生成)。
  • +
  • プラグインのマイグレーションは migration_{code} テーブルで管理され、コアの app/DoctrineMigrations/ とは別系統(正確には migration_+コードの小文字化。AbstractPluginManager::MIGRATION_TABLE_PREFIX / migration())。
  • +
  • enable/disable とロードの非対称に注意: Resource/config/services.(php|yaml) は物理的に存在する全プラグインが glob で無条件ロードされる(Kernel::configureContainer())が、ルーティングは有効プラグインのみ(eccube.plugins.enabled)読み込まれる(同 configureRoutes())。disable してもディレクトリが残る限りサービス定義はコンテナに載る(消えるのはルートだけ)。完全に外すには uninstall(ディレクトリ削除)が要る。
  • +
+
+ + + diff --git a/app/Plugin/README.md b/app/Plugin/README.md new file mode 100644 index 00000000000..25d0f1e817e --- /dev/null +++ b/app/Plugin/README.md @@ -0,0 +1,21 @@ +# app/Plugin — プラグイン設置先 + +インストール済みプラグインが置かれる場所。1 プラグイン = `app/Plugin/{PluginCode}/` の 1 ディレクトリで、 +PSR-4 で `Plugin\{PluginCode}\` にマッピングされる。中身は環境依存(インストール状況次第)で、 +`.gitkeep` を除き `.gitignore` 済み。素の状態では空。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-plugin/SKILL.md`](../../.claude/skills/eccube-plugin/SKILL.md) +- 🧩 ライフサイクル基盤(コア): [`src/Eccube/Plugin/`](../../src/Eccube/Plugin/README.md) + +## ここに置かれるもの + +- `{PluginCode}/composer.json` — 必須。`version` と `extra.code` が必須 +- `{PluginCode}/PluginManager.php` — 任意。ライフサイクル処理(`AbstractPluginManager` 継承) +- `{PluginCode}/DoctrineMigrations/Version*.php` — プラグイン専用マイグレーション(`migration_{code}` テーブルで管理) +- `{PluginCode}/Controller/` `Entity/` `Form/Extension/` `Resource/template/` — 各拡張点 + +## 注意 + +- 実運用プラグインはコミットされない(環境依存・gitignore 済み)。リポジトリ内の `HogePlugin` 等は **PHPUnit のテスト用サンプル**。 +- 雛形は手書きせず `bin/console eccube:plugin:generate ` で生成する。 diff --git a/src/Eccube/Attribute/README.html b/src/Eccube/Attribute/README.html new file mode 100644 index 00000000000..39146cde6d6 --- /dev/null +++ b/src/Eccube/Attribute/README.html @@ -0,0 +1,100 @@ + + + + + +Attribute — 仕様 + + + + +

Attribute — EC-CUBE 独自の PHP 属性

+ + +
+

概要

+

このディレクトリは、EC-CUBE が独自に定義する PHP8 属性(Attribute)を集めた場所です。 + いずれも クラス・プロパティ・メソッドに「印(マーカー)」を付けるためだけの宣言で、 + 属性そのものはロジックを持ちません。付けられた印を別の場所(CompilerPass・EventListener・拡張サービス)が + Reflection で拾って動作を変える、という使われ方をします。

+ +

用途で分けると 3 系統あります。

+ + + + + +
系統属性拾う側
受注処理フローへの登録マーカーCartFlow / ShoppingFlow / OrderFlowPurchaseFlowPass
拡張機構(エンティティ・フォーム)EntityExtension / FormAppendEntityProxyService / DoctrineOrmExtension
コントローラのアクセス制限ForwardOnlyForwardOnlyListener
+
+ +
+

属性一覧

+ + + + + + + + +
属性対象意味
#[CartFlow]クラスこの Processor/Validator を cart フローに登録する印。
#[ShoppingFlow]クラス同じく shopping フローへの登録マーカー。
#[OrderFlow]クラス同じく order フローへの登録マーカー。
#[EntityExtension('...')]クラス(繰り返し可)トレイトを既存エンティティへ合成する対象クラス名を指定。プロキシ生成の入力になる。
#[FormAppend]プロパティエンティティ拡張で追加したプロパティを、既存フォームへ差し込む設定(type / options / form_theme / auto_render / style_class)。
#[ForwardOnly]メソッドそのコントローラアクションを forward(内部転送)専用にし、直接 URL アクセスを拒否する印。
+
CartFlow / ShoppingFlow / OrderFlow は引数なしの純粋なマーカーです。 + 対象フローが名前で決まるだけで、優先度などの制御は持ちません(順序制御が必要なら YAML タグ側で行う)。
+
+ +
+

受注フロー登録マーカーの仕組み開発者向け

+

#[CartFlow] / #[ShoppingFlow] / #[OrderFlow] は、プラグインや app/Customize が + 受注処理の部品(Processor/Validator)をどのフローに載せるかを宣言するためのものです。

+
Processor/Validator クラスに #[ShoppingFlow] を付与 + ↓ 基底クラスの継承で自動タグ付け +PurchaseFlowPass が ReflectionAttribute::IS_INSTANCEOF で属性を検出 + ↓ +eccube.purchase.flow.shopping へ addMethodCall で配線(YAML 配線済みなら重複登録しない)
+

詳しい配線ロジックは DependencyInjection の PurchaseFlowPass、 + 受注処理そのものの全体像は PurchaseFlow を参照してください。

+
+ +
+

拡張機構マーカーの仕組み開発者向け

+

#[EntityExtension] と #[FormAppend] は、コアを改変せずにエンティティ・フォームを + 拡張するための印です(アップグレード安全なカスタマイズ)。

+ + + + +
属性拾う側動作
#[EntityExtension(Target::class)]EntityProxyServiceトレイトに付けると、value で指定した対象エンティティにそのトレイトを合成したプロキシを生成する(IS_INSTANCEOF で検出)。複数指定のため IS_REPEATABLE。
#[FormAppend]DoctrineOrmExtension(FormTypeExtension)拡張プロパティに付けると、その type・options 等を読み取り、対応する既存フォームへ自動でフィールドを追加する。
+

実運用のサンプルは app/Plugin/EntityExtension/・app/Plugin/EntityForm/ にあります。

+
+ +
+

注意点開発者向け

+
    +
  • 属性は宣言だけ・ロジックは持たない。挙動を追うときは「付けた側」ではなく「拾う側」(PurchaseFlowPass / EntityProxyService / DoctrineOrmExtension / ForwardOnlyListener)を読む。
  • +
  • #[EntityExtension] を足したらプロキシ再生成が必要(bin/console eccube:generate:proxies)。付けただけでは反映されない。
  • +
  • フロー登録マーカーは順序を持たない。実行順の制御が必要なときは purchaseflow.yaml の priority 付きタグを使う(属性とタグの併用はしない)。
  • +
  • #[ForwardOnly] は直接アクセス時に AccessDeniedHttpException を投げる。認可(ロール)ではなく内部転送か否かで弾く仕組みである点に注意。
  • +
+
+ + + diff --git a/src/Eccube/Attribute/README.md b/src/Eccube/Attribute/README.md new file mode 100644 index 00000000000..4c136bb6f53 --- /dev/null +++ b/src/Eccube/Attribute/README.md @@ -0,0 +1,15 @@ +# Attribute — EC-CUBE 独自の PHP 属性 + +クラス・プロパティ・メソッドに「印(マーカー)」を付けるための PHP8 属性群。 +属性自体はロジックを持たず、付けられた印を CompilerPass・EventListener・拡張サービスが +Reflection で拾って挙動を変える。受注フロー登録・エンティティ/フォーム拡張・アクセス制限の 3 系統。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🔗 関連: [PurchaseFlow](../Service/PurchaseFlow/README.md) ・ [DependencyInjection](../DependencyInjection/README.md) + +## 主要ファイル + +- `CartFlow.php` / `ShoppingFlow.php` / `OrderFlow.php` — 受注処理パイプラインへの登録マーカー(`PurchaseFlowPass` が拾う) +- `EntityExtension.php` — トレイトを既存エンティティへ合成する対象を指定(`IS_REPEATABLE`。`EntityProxyService` が拾う) +- `FormAppend.php` — 拡張プロパティを既存フォームへ差し込む設定(`DoctrineOrmExtension` が拾う) +- `ForwardOnly.php` — コントローラアクションを forward 専用にし直接アクセスを拒否(`ForwardOnlyListener` が拾う) diff --git a/src/Eccube/Command/README.html b/src/Eccube/Command/README.html new file mode 100644 index 00000000000..bd4bae8a3de --- /dev/null +++ b/src/Eccube/Command/README.html @@ -0,0 +1,124 @@ + + + + + +Command — 仕様 + + + + +

Command — コンソールコマンド/バッチ

+ + +
+

概要

+

この Command/ ディレクトリは、EC-CUBE の コンソールコマンド(bin/console から実行するコマンド/バッチ)が置かれる場所です。 + インストール・プロキシ生成・プラグイン管理・ダミーデータ生成・不要データの削除など、 + Web 画面を介さずに実行する運用・保守処理をここに集約します。

+ +

各コマンドの共通の性質:

+
    +
  • Symfony\Component\Console\Command\Command を継承し、クラスに #[AsCommand(name: ..., description: ...)] 属性を付ける。
  • +
  • コマンド名は eccube: 接頭辞のコロン区切り(eccube:install / eccube:plugin:enable 等)。
  • +
  • サービス登録は不要。services.yaml の autoconfigure: true で #[AsCommand] を付けるだけで console.command として自動登録される。
  • +
  • コマンドは「もう 1 つの入口」であって、業務ロジックの置き場所ではない。処理は Service/Repository へ委譲する。
  • +
+
プロジェクト固有のコマンドは app/Customize/Command/、プラグインは各プラグインの Command/ に置きます。 + 名前空間が Customize\ / Plugin\ でも autoconfigure の対象なので、置くだけで登録されます。
+
+ +
+

代表的なコマンド

+

コアが標準搭載する主なコマンド(全一覧は bin/console list eccube):

+ + + + + + + + + + + + + + +
コマンド名役割
eccube:installEC-CUBE の初期インストール(DB スキーマ作成・初期データ投入)。
eccube:generate:proxiesエンティティプロキシの再生成(トレイト拡張の反映)。
eccube:schema:updateエンティティ属性からのスキーマ更新。
eccube:fixtures:generate / eccube:fixtures:loadダミーデータの生成・フィクスチャの投入。
eccube:delete-carts不要になったカートデータの削除(バッチの手本)。
eccube:plugin:install|enable|disable|uninstall|updateプラグインのライフサイクル操作。
eccube:composer:install|require|update|removeプラグイン導入に伴う Composer 操作のラッパ。
eccube:cache:buildコンパイル済みコンテナ・ルーティング・テンプレートを var/build/{env} へ生成する。Web サーバーと CLI の権限を分けた構成では cache:clear の代わりにこれを使う。
eccube:doctor:permissionsディレクトリごとの書き込み権限(Web サーバー/CLI のどちらが書けるべきか)を検査する。
eccube:page|block|mail-template:list|show|apply|removeページ・ブロック・メールテンプレートを CLI から操作する(管理画面と同じ FormType で検証)。
eccube:asset:* / eccube:user-data:* / eccube:env:* / eccube:keystore:*CSS/JS・html/user_data・.env・署名鍵を CLI から操作する。
eccube:contents:export|importページ・ブロック・メールテンプレート・レイアウトの定義(DB 側)を app/contents/*.yaml と入出力する。
+
eccube:schema:update は Doctrine の doctrine:schema:update を alias で上書きしている(#[AsCommand(name: 'eccube:schema:update', aliases: ['doctrine:schema:update'])]、OrmUpdateCommand を継承)。 + 素の Doctrine コマンドと違い、プラグインのプロキシを一時生成してから差分を取るため、トレイト拡張分も含めてスキーマ更新できる(--no-proxy で素の挙動に戻る)。UpdateSchemaDoctrineCommand の #[AsCommand] と execute()。
+
+ +
+

コマンドの実行フロー

+

Symfony Console の実行順は決まっています。EC-CUBE のコマンドはこの型に沿って + 「引数の取得 → Service へ委譲 → 結果の出力 → 終了コード」に徹します。

+
#[AsCommand(name: 'eccube:...')] … コマンド名を宣言(autoconfigure で自動登録) + │ + ▼ +__construct(...依存...) … Service / Repository / EntityManager を DI + │ parent::__construct() が必須 + ▼ +configure() … addArgument() / addOption() で入力を宣言 + │ + ▼ +execute(InputInterface, OutputInterface): int + │ $io = new SymfonyStyle(...) … 出力は SymfonyStyle に寄せる + │ 業務処理は Service へ委譲 + ▼ +return Command::SUCCESS; … 正常 SUCCESS(0) / 異常 FAILURE(1)・INVALID(2)
+
大量データを扱うバッチは、ループ内で毎回 flush() せず + 一定件数ごとにまとめて flush() する(端数も最後に flush)。トランザクション境界が要るなら + beginTransaction()/commit()/失敗時 rollback() で囲む(DeleteCartsCommand が手本)。
+
+ +
+

コマンドの追加開発者向け

+

新しいコマンドは 1 クラス足すだけです。 + 実装の書き方・DO/DON'T は eccube-command/SKILL.md が正典です。ここでは要点だけ示します。

+ + + + + + +
やること作法
クラスを宣言するCommand を継承し #[AsCommand(name: 'eccube:...')] を付ける
依存を受けるコンストラクタインジェクション。parent::__construct() を必ず呼ぶ
入力を宣言するconfigure() で addArgument()/addOption()(VALUE_REQUIRED/VALUE_NONE を使い分け)
処理を書くexecute() で入力取得 → Service へ委譲 → SymfonyStyle で出力 → int を返す
+
cron/定期実行: EC-CUBE 4.4 のコアに独自スケジューラ(Symfony Scheduler 等)は無い。 + 定期実行は OS の cron 等から bin/console <コマンド名> を叩く前提で、冪等・安全に再実行できる設計にする。
+
+ +
+

注意点開発者向け

+
    +
  • parent::__construct() を呼び忘れない(呼ばないと実行時エラー)。
  • +
  • execute() は int を返す。void にしない。値はリテラルではなく Command::SUCCESS/FAILURE/INVALID を使う(Rector が書き換える)。
  • +
  • コア独自の終了コード 3 は「本処理は完了したが手動対応が要る」(例: build ディレクトリへ書けず eccube:cache:build の実行が必要)。2 は Symfony の INVALID と衝突するため使わない。
  • +
  • execute() に業務ロジックを直書きしない。複数 Repository 横断や計算・判定は Service へ委譲する。
  • +
  • #[AsCommand]+autoconfigure 任せにし、services.yaml に手書きで console.command タグを足さない。
  • +
  • コマンド名は独自命名にせず eccube: 接頭辞のコロン区切り(既存コマンドに倣う)。
  • +
  • 「Symfony Scheduler で定期実行」と推測で書かない。コアに機構は無い。
  • +
  • 登録できているかは bin/console list に自分のコマンドが現れるかで確認する。
  • +
+
+ + + diff --git a/src/Eccube/Command/README.md b/src/Eccube/Command/README.md new file mode 100644 index 00000000000..97c8cb3639a --- /dev/null +++ b/src/Eccube/Command/README.md @@ -0,0 +1,19 @@ +# Command — コンソールコマンド/バッチ + +`bin/console` から実行するコンソールコマンド(バッチ・cron 用途含む)の置き場所。 +インストール・プロキシ生成・プラグイン管理・ダミーデータ生成・不要データ削除など、 +Web 画面を介さない運用・保守処理を集約する。コマンドは「もう 1 つの入口」であり、 +業務ロジックは Service/Repository へ委譲して薄く保つ。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-command/SKILL.md`](../../../.claude/skills/eccube-command/SKILL.md) + +## 主要ファイル + +- `InstallerCommand.php` — `eccube:install`(初期インストール) +- `GenerateProxyCommand.php` — `eccube:generate:proxies`(エンティティプロキシ再生成) +- `DeleteCartsCommand.php` — `eccube:delete-carts`(トランザクション境界を持つバッチの手本) +- `GenerateDummyDataCommand.php` — `eccube:fixtures:generate`(オプション/バッチ flush の手本) +- `PluginEnableCommand.php` ほか `Plugin*Command.php` — プラグインのライフサイクル操作 +- `CacheBuildCommand.php` — `eccube:cache:build`(`var/build` の生成。権限を分けた構成での `cache:clear` の代替) +- `Content/` `Env/` `KeyStore/` — ページ・ブロック・メールテンプレート・`.env`・署名鍵などを CLI から操作するコマンド群 diff --git a/src/Eccube/Controller/Admin/Order/README.html b/src/Eccube/Controller/Admin/Order/README.html new file mode 100644 index 00000000000..cb62a63aa5f --- /dev/null +++ b/src/Eccube/Controller/Admin/Order/README.html @@ -0,0 +1,88 @@ + + + + + +Admin/Order — 仕様 + + + + +

Admin/Order — 管理画面の受注管理

+ + +
+

概要

+

src/Eccube/Controller/Admin/Order/ は、管理画面から受注(Order)を操作するコントローラ群です。 + 受注の一覧・検索・登録・編集、出荷(Shipping)の編集・出荷通知、受注メールの送信、出荷情報の CSV 取込などを担います。

+

受注ドメインの詳細(受注ステータスの遷移・明細種別・出荷単位など)は複製せず + doc4「受注」仕様 を参照してください。

+
+ +
+

主要な役割・分類

+ + + + + + + +
コントローラ役割
OrderController受注一覧・検索・一括削除・CSV/PDF 出力・出荷ステータス/伝票番号の更新。
EditController受注の新規登録(admin_order_new)と編集(admin_order_edit)。商品・会員・明細種別の検索補助も持つ。
ShippingController出荷(Shipping)の編集と、出荷完了通知メールのプレビュー/送信。
MailController受注に対する任意メールの作成・プレビュー・送信(MailHistory に記録)。
CsvImportController出荷情報 CSV の取込とテンプレート配布(AbstractCsvImportController 継承)。
+
+ +
+

代表的な処理の流れ

+

受注の登録・編集(EditController / ShippingController)は、店頭の購入と同じ受注処理パイプライン(PurchaseFlow)を + order フローで実行します。管理者が組んだ受注も、金額計算・在庫・採番の扱いを店頭購入と一貫させるためです。

+
① フォーム処理 受注編集フォームを handleRequest() → isValid() +② 検証・計算 PurchaseFlow.validate() … warning / error を収集して画面に表示 +③ 確定(登録時) PurchaseFlow.prepare() → commit() … 在庫引当・注文番号採番など +④ ステータス遷移 変更時は OrderStateMachine.apply() で受注ステータスを更新
+

詳細は PurchaseFlow の仕様 を参照してください。

+
+ +
+

拡張ポイント開発者向け

+
    +
  • 受注編集の途中に処理を差し込むには テンプレートイベント/コントローライベント(例: ADMIN_ORDER_EDIT_INDEX_PROGRESS)を購読する。コントローラ本体は改変しない。
  • +
  • 受注に対する検証・計算・確定ロジックの追加は PurchaseFlow の order フローに部品を登録する(Processor / Validator)。コントローラには書かない。
  • +
  • プロジェクト固有の振る舞いは app/Customize/ 側で拡張・上書きする。
  • +
+
+ +
+

注意点開発者向け

+
    +
  • 管理画面の操作が購入と同じ確定処理を呼ぶ点に注意。 EditController / ShippingController は管理画面の操作から、購入時と同じ PurchaseFlow.prepare()/commit() を実行する。そのため画面上は編集操作でも、在庫引当・注文番号採番・(出荷画面では)出荷通知メール送信といった副作用が同時に走る。確定系の呼び出しを安易に増やさない。
  • +
  • 受注ステータスは OrderStateMachine 経由で遷移させる。 直接 setOrderStatus() で書き換えず、遷移可否チェックを通す。
  • +
  • Fat コントローラを増やさない。 受注の計算・検証・確定は PurchaseFlow へ。コントローラは HTTP 入出力と画面遷移に徹する。
  • +
  • 認可・CSRF。 パスは %eccube_admin_route% 配下(admin ファイアウォールで保護)。一括削除・ステータス更新・伝票番号更新など GET 以外の状態変更は CSRF トークンを検証する。
  • +
  • ルートの {id} が指す対象に注意。 出荷編集 admin_shipping_edit(/shipping/{id}/edit)の {id} は Shipping ではなく Order の IDで、1 受注の全お届け先をまとめて編集する。一方で対応状況・送り状番号の更新(admin_shipping_update_order_status / admin_shipping_update_tracking_number)は ShippingController ではなく OrderController に置かれ、{id} は Shipping の ID を指す。
  • +
  • 発送済(DELIVERED)遷移は全出荷が揃って初めて受注に反映。 出荷 1 件を発送済にしても、受注ステータスが DELIVERED へ進むのは 受注内の全 Shipping が発送済のときだけ。未発送の出荷が残る間は当該 Shipping の shippingDate だけ設定される(部分出荷)。
  • +
  • ステータス変更が在庫・会員サマリを更新する。 IN_PROGRESS/CANCEL への遷移は商品在庫を増減し、受注に会員が紐づく場合はいずれの遷移でも updateOrderSummary() で購入回数・購入金額を再計算する(受注登録・編集でも同様)。
  • +
  • 一括削除は物理削除。 bulkDelete() は $em->remove($Order) による物理削除で、論理削除フラグを立てるのではなく受注そのものを消す。
  • +
+
+ + + diff --git a/src/Eccube/Controller/Admin/Order/README.md b/src/Eccube/Controller/Admin/Order/README.md new file mode 100644 index 00000000000..d07669993f4 --- /dev/null +++ b/src/Eccube/Controller/Admin/Order/README.md @@ -0,0 +1,17 @@ +# Admin/Order — 管理画面の受注管理 + +管理画面から受注(`Order`)を操作するコントローラ群。受注の一覧・検索・登録・編集、 +出荷(`Shipping`)の編集・出荷通知、受注メール送信、出荷 CSV 取込を担う。 +受注の登録・編集は店頭購入と同じ受注処理パイプライン(PurchaseFlow)を `order` フローで実行する。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-controller/SKILL.md`](../../../../../.claude/skills/eccube-controller/SKILL.md) +- 📚 ドメイン詳細: https://doc4.ec-cube.net/spec_order + +## 主要ファイル + +- `OrderController.php` — 受注一覧・検索・一括削除・CSV/PDF 出力・出荷ステータス/伝票番号更新 +- `EditController.php` — 受注の新規登録・編集。PurchaseFlow の `validate`/`prepare`/`commit` を管理操作から実行 +- `ShippingController.php` — 出荷編集と出荷完了通知メールのプレビュー/送信 +- `MailController.php` — 受注に対する任意メールの作成・送信(`MailHistory` に記録) +- `CsvImportController.php` — 出荷情報 CSV の取込とテンプレート配布(`AbstractCsvImportController` 継承) diff --git a/src/Eccube/Controller/README.html b/src/Eccube/Controller/README.html new file mode 100644 index 00000000000..d4ddfb24a28 --- /dev/null +++ b/src/Eccube/Controller/README.html @@ -0,0 +1,92 @@ + + + + + +Controller — 仕様 + + + + +

Controller — HTTP 入出力層

+ + +
+

概要

+

src/Eccube/Controller/ は、EC-CUBE の HTTP リクエストを受け取り、レスポンスを返す入口です。 + URL(ルーティング)ごとに 1 つのアクション(メソッド)が対応し、店頭(フロント)と管理画面の両方の画面・API を提供します。

+ +

コントローラの仕事は「HTTP の入出力変換」に徹することです。実際の業務処理(金額計算・在庫引当・メール送信など)は + Service や受注処理パイプライン(PurchaseFlow)へ委譲し、コントローラ自身は薄く保ちます。

+
+ +
+

主要な役割・分類

+

ディレクトリは「誰が使う画面か」で大きく分かれます。

+ + + + + + + + +
場所役割
直下(CartController / ShoppingController / ProductController / EntryController 等)店頭(一般利用者向け)の画面。商品閲覧・カート・購入手続き・会員登録・お問い合わせなど。
Mypage/会員(ログイン済み Customer)向けのマイページ。注文履歴・お気に入り・会員情報変更など。
Admin/管理画面。受注・商品・会員・コンテンツ・設定・店舗管理など機能単位でサブディレクトリに分割。
Block/ブロック(画面部品)の描画コントローラ。
Install/初期インストール用ウィザード。
AgentCommerce/エージェントコマース(外部エージェント連携)向けのエンドポイント。
+

共通の基底として AbstractController(フラッシュメッセージ・CSRF 検証・リダイレクトのヘルパを提供)と、 + 購入系で受注処理を扱う AbstractShoppingController があります。

+
+ +
+

代表的な処理の流れ

+

1 アクションは原則として次の 4 ステップで構成されます。業務ロジックは自分で書かず委譲するのが要点です。

+
① リクエスト受取 Request・ルートパラメータ($id 等)を受け取る +② フォーム処理 createForm() → handleRequest() → isValid()(CSRF 検証込み) +③ 委譲 Service / Repository を呼んで処理を任せる +④ 応答組み立て addSuccess() / redirectToRoute() / render() で結果を返す
+

購入手続き系(ShoppingController 等)は AbstractShoppingController::executePurchaseFlow() を通じて + 受注処理パイプライン(PurchaseFlow)に検証・計算を委譲します。 + コントローラは PurchaseFlow の結果(warning / error)を受け取り、画面遷移に変換するだけです。

+
+ +
+

拡張ポイント開発者向け

+
    +
  • プロジェクト固有の追加・上書きは app/Customize/Controller/ へ。コア(src/Eccube/Controller/)を直接改変しない(アップグレード安全のため)。
  • +
  • プラグインは自身のディレクトリ内にコントローラを持ち、ルーティング属性で URL を登録する。
  • +
  • ルーティングは #[Route] 属性(Symfony\Component\Routing\Attribute\Route)。管理画面のパスは %eccube_admin_route% プレフィックス配下に置く。
  • +
  • リクエスト/レスポンスのライフサイクルへの割り込みは EventSubscriber / EventListener で行う(コントローラを直接いじらない)。
  • +
+

実装の書き方・DO/DON'T は eccube-controller/SKILL.md が正典です。

+
+ +
+

注意点開発者向け

+
    +
  • Fat コントローラを増やさない。 金額・在庫・送料・ポイント等の計算、$em->persist()/$em->flush() を伴う業務的な永続化、複数 Repository をまたぐ処理・外部 API・メール送信・ファイル IO がアクションに混ざり始めたら Service(受注処理は PurchaseFlow)へ抽出する。行数で線引きしない。
  • +
  • 認可はファイアウォール+ロールで制御。 管理アクションは必ず %eccube_admin_route% 配下に置く(admin ファイアウォールで認証が要求される)。EC-CUBE コアは個別の #[IsGranted] ではなくファイアウォールで守る点に注意。
  • +
  • CSRF: GET 以外の状態変更はトークン検証。 フォーム経由(handleRequest()+isValid())は保護込み。フォームを介さない削除・Ajax は基底の $this->isTokenValid() を明示的に呼ぶ(無効時は AccessDeniedHttpException)。isTokenValid() はトークンをリクエストパラメータ Constant::TOKEN_NAME だけでなく ECCUBE-CSRF-TOKEN ヘッダからも受け付けるため、Ajax はヘッダ送信でよい。
  • +
  • フラッシュメッセージの名前空間はデフォルトが front。 addSuccess()/addError() 等の第 2 引数を省略すると eccube.front.* に積まれる。管理画面では addSuccess($msg, 'admin') のように 'admin' を明示しないと、メッセージが front 名前空間に入り管理画面テンプレートに表示されない。
  • +
  • AbstractController が #[Required] セッター注入で提供する依存(eventDispatcher・entityManager・translator・formFactory・session・router・eccubeConfig)はコンストラクタで再注入しない。
  • +
+
+ + + diff --git a/src/Eccube/Controller/README.md b/src/Eccube/Controller/README.md new file mode 100644 index 00000000000..c4b08afef48 --- /dev/null +++ b/src/Eccube/Controller/README.md @@ -0,0 +1,16 @@ +# Controller — HTTP 入出力層 + +EC-CUBE の HTTP リクエストを受け取りレスポンスを返す入口。URL(ルーティング)ごとに 1 アクションが対応し、 +店頭(フロント)と管理画面の画面・API を提供する。業務ロジックは自分で持たず、 +Service や受注処理パイプライン(PurchaseFlow)へ委譲して薄く保つ。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-controller/SKILL.md`](../../../.claude/skills/eccube-controller/SKILL.md) + +## 主要ファイル + +- `AbstractController.php` — 全コントローラの基底。フラッシュメッセージ(`addSuccess`/`addError` 等)・CSRF 検証(`isTokenValid()`)・`forwardToRoute()` を提供 +- `AbstractShoppingController.php` — 購入系の基底。`executePurchaseFlow()` で受注処理パイプラインへ検証・計算を委譲 +- `CartController.php` / `ShoppingController.php` — 店頭のカート・購入手続き +- `ProductController.php` / `EntryController.php` — 商品閲覧・会員登録 +- `Admin/` — 管理画面コントローラ群(受注・商品・会員・設定などを機能単位で分割)/`Mypage/` — 会員向けマイページ diff --git a/src/Eccube/DependencyInjection/README.html b/src/Eccube/DependencyInjection/README.html new file mode 100644 index 00000000000..fced668ee3f --- /dev/null +++ b/src/Eccube/DependencyInjection/README.html @@ -0,0 +1,126 @@ + + + + + +DependencyInjection — 仕様 + + + + +

DependencyInjection — DI 拡張とコンテナ組み立て

+ + +
+

概要

+

このディレクトリは、EC-CUBE を Symfony の DI コンテナに載せるための拡張ポイントを集めた場所です。 + 「アプリ起動時に、設定を読み込み、プラグインの有効/無効を判定し、各種サービスをタグで自動配線する」といった + コンテナのブート/組み立て処理をここが担います。実行時のリクエスト処理ではなく、 + コンテナのコンパイル(ビルド)フェーズで動くコードが中心です。

+ +

大きく 4 種類の部品で構成されます。

+ + + + + + +
部品役割
EccubeExtensionバンドル拡張(Extension)。eccube 設定の読み込みと、他バンドル設定への prepend(動的な差し込み)を行う。
Configurationeccube 名前空間の設定スキーマ定義。現状は rate_limiter(レートリミッタ)セクションを定義。
Compiler/コンテナのコンパイル時に走る CompilerPass 群。タグ収集・自動配線・定義の書き換えを行う。
Facade/DI コンテナの外(静的コンテキスト)から一部サービスへアクセスするための singleton ファサード。
+
+ +
+

Compiler パス一覧

+

各 CompilerPass は「特定タグの付いたサービスを集めて、別のサービスへ配線する/定義を書き換える」責務を持ちます。 + 登録は Kernel::build()(src/Eccube/Kernel.php)で行い、一部は実行順のため優先度を指定しています。

+ + + + + + + + + + + + + + + + +
パスやること
AutoConfigurationTagPassdoctrine.event_subscriber / rate_limiter / payment_method のタグを条件に応じて自動付与。優先度 11(PluginPass より先)。
PluginPass無効なプラグインのサービスタグをクリアし、拡張機構を無効化(doctrine.repository_service は除く)。優先度 10。
PurchaseFlowPass受注処理の 3 フロー(cart/shopping/order)へ Processor/Validator を配線。詳細は下記および PurchaseFlow。
QueryCustomizerPasseccube.query_customizer タグを Queries に addCustomizer() で登録。
NavCompilerPasseccube.nav タグの EccubeNav::getNav() を集約し、eccube_nav パラメータへマージ。
PaymentMethodPasseccube.payment.method タグのサービスを public 化。
TwigBlockPasseccube.twig_block タグの EccubeTwigBlock::getTwigBlock() を eccube_twig_block_templates へ集約。
TwigExtensionPass本番環境のみ、twig に IgnoreRoutingNotFoundExtension を追加(未定義ルートで例外を投げない)。
WebServerDocumentRootPassweb_server コマンドのドキュメントルートを public からプロジェクトルートへ変更。
RuntimeCacheDirPassリクエスト処理中に書き込まれるキャッシュ(設定で変えられないもの)の出力先を %eccube_runtime_dir%(var/runtime/{env})へ差し替える。
BuildDirCacheWarmerPassテンプレートの一括事前コンパイルを自動 warmup から外し、eccube:cache:build のときだけ実行させる。
CliFileLogHandlerPassファイルへ書く monolog ハンドラをラップし、ECCUBE_CLI_LOG_TO_FILE=0 のとき CLI 実行時だけファイル出力を止められるようにする。
RuntimeCachePoolFailsafePassファイルへ書く cache pool を、書き込めない場合は保存を諦める実装へ差し替える(CLI から Web サーバー所有の領域へ書けない構成向け)。
StripReportFieldsArgPassORM3 環境で AttributeDriver の余分な引数を paths 1 引数へ統一(DoctrineBundle #1844 回避)。優先度 -1000。
+
+ +
+

EccubeExtension の prepend と access_control 動的生成開発者向け

+

EccubeExtension は PrependExtensionInterface を実装し、prepend() で + 他バンドルの設定を起動時に動的に差し込みます。

+
prepend() + ├ configureFramework() … security の access_control を動的生成 + framework の rate_limiter を組み立て + └ configurePlugins() … DB に直接接続して dtb_plugin を読み、有効/無効プラグインを判定 + → 有効プラグインの twig paths・翻訳 paths を framework/twig に差し込む
+ +
非自明な挙動: /admin・/mypage の access_control は security.yaml ではなく、ここ(configureFramework())で動的生成されます。 + security.yaml だけを見て認可仕様を判断すると誤読するので注意(認可レビューで ^/mypage/ を「未保護」と誤検出しない)。
+ +

生成される access_control(この順序=上から評価。%eccube_admin_route% は管理画面プレフィックス):

+ + + + + + + + +
pathroles
^/{admin}/loginIS_AUTHENTICATED_ANONYMOUSLY
^/{admin}/ROLE_ADMIN
^/mypage/loginIS_AUTHENTICATED_ANONYMOUSLY
^/mypage/withdraw_completeIS_AUTHENTICATED_ANONYMOUSLY
^/mypage/changeIS_AUTHENTICATED_FULLY
^/mypage/ROLE_USER
+

環境変数 ECCUBE_FORCE_SSL が有効なときは、全エントリに requires_channel: https を付与し https に限定します。

+
+ +
+

PurchaseFlow の配線開発者向け

+

PurchaseFlowPass は受注処理パイプラインの 3 フロー(eccube.purchase.flow.cart / + .shopping / .order)へ、Processor / Validator を 2 経路で配線します。

+
    +
  • YAML タグ purchaseflow.yaml の flow_type + priority 指定。 + findAndSortTaggedServices() の並びを flow_type ごとに再ソートして登録する。
  • +
  • 属性 #[CartFlow] / #[ShoppingFlow] / #[OrderFlow](Attribute)を + ReflectionAttribute::IS_INSTANCEOF で拾って対象フローへ登録。YAML で配線済みなら重複登録しない(alreadyWired())。
  • +
+

タグ ↔ 追加メソッドの対応は getProcessorTags() に定義(eccube.item.validator → addItemValidator など 7 種)。

+
+ +
+

注意点開発者向け

+
    +
  • ここは基本的にコンパイル時のコード。CompilerPass や prepend() の変更はコンテナの再ビルドで反映される。 + コンパイル済みコンテナの出力先は var/build/{env}(Kernel::getBuildDir())で、再生成は bin/console eccube:cache:build。 + Web サーバーと CLI の権限を分けた構成では cache:clear ではなくこちらを使う。
  • +
  • Kernel::getCacheDir()(var/cache)と getBuildDir()(var/build)を同じパスに統合しない。別パスであることが twig の 3 層キャッシュが有効になる条件になっている。
  • +
  • configurePlugins() は prepend フェーズでコンテナが未完成のため、DBAL の接続を直接生成して dtb_plugin を読む。DB 未接続・テーブル未作成時は安全に空扱いで抜ける(インストール前を考慮)。
  • +
  • 認可仕様を確認するときは security.yaml だけでなく必ず EccubeExtension::configureFramework() を併読する(上記 access_control 動的生成のため)。
  • +
  • CompilerPass の実行順が重要なものは優先度で制御されている(AutoConfigurationTagPass=11 → PluginPass=10、StripReportFieldsArgPass=-1000)。順序前提を崩さないこと。
  • +
  • Facade/ の LoggerFacade / TranslatorFacade は、DI が使えない静的コンテキストで例外的にサービスへアクセスする手段。通常のサービスではコンストラクタ注入を優先する。
  • +
+
+ + + diff --git a/src/Eccube/DependencyInjection/README.md b/src/Eccube/DependencyInjection/README.md new file mode 100644 index 00000000000..d317e3b212f --- /dev/null +++ b/src/Eccube/DependencyInjection/README.md @@ -0,0 +1,17 @@ +# DependencyInjection — DI 拡張とコンテナ組み立て + +EC-CUBE を Symfony DI コンテナに載せるための拡張ポイント。設定読み込み・プラグイン有効/無効判定・ +サービスのタグ自動配線といった、コンテナのブート/コンパイル処理を担う。 +実行時のリクエスト処理ではなく、コンテナのビルドフェーズで動くコードが中心。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🔗 関連: [Attribute](../Attribute/README.md) ・ [PurchaseFlow](../Service/PurchaseFlow/README.md) + +## 主要ファイル + +- `EccubeExtension.php` — バンドル拡張。`prepend()` で `/admin`・`/mypage` の access_control を**動的生成**し、DB を直接読んでプラグインの有効/無効を判定 +- `Configuration.php` — `eccube` 設定スキーマ(`rate_limiter` セクション) +- `Compiler/PurchaseFlowPass.php` — 受注処理 3 フローへ Processor/Validator を配線(YAML タグ+属性の 2 経路) +- `Compiler/PluginPass.php` — 無効プラグインのサービスタグをクリアして拡張機構を無効化 +- `Compiler/AutoConfigurationTagPass.php` — doctrine.event_subscriber 等のタグを自動付与 +- `Facade/` — DI 外の静的コンテキスト向け singleton ファサード(`LoggerFacade` / `TranslatorFacade`) diff --git a/src/Eccube/Doctrine/README.html b/src/Eccube/Doctrine/README.html new file mode 100644 index 00000000000..cd29292e195 --- /dev/null +++ b/src/Eccube/Doctrine/README.html @@ -0,0 +1,139 @@ + + + + + +Doctrine — 仕様 + + + + +

Doctrine — EC-CUBE 固有の Doctrine 拡張

+ + +
+

概要

+

このディレクトリは、EC-CUBE が Doctrine ORM / DBAL に被せている独自拡張を収めます。 + エンティティ定義(src/Eccube/Entity/)そのものではなく、 + 「クエリの後付け拡張」「日時の UTC 保存」「日本語検索用の DQL 関数」「表示制御用のフィルタ」「Proxy を安全に読むマッピングドライバ」 + 「CSV からの初期データ投入」といった基盤側の仕組みが対象です。

+ +
区分の凡例(この機構は何のためにあるか): 拡張ポイント=プラグイン/Customize が介入するための仕組み / + ドメイン=EC 業務要件そのもの / portability=複数 DB・タイムゾーン吸収 / + 基盤=横断インフラ / 負債=歴史的経緯による簡素化候補。 + 「拡張ポイントの多さ=コアの複雑さの主因」を可視化するためのタグ。
+ + + + + + + + + +
サブ区分目的
Query/拡張ポイント既存クエリに WHERE / JOIN / ORDER BY を後付けで差し込むカスタマイズ機構(プラグイン拡張の要)
ORM/Mapping/Driver/拡張ポイントエンティティプロキシ機構(trait 拡張)を成立させる属性マッピングドライバ
DBAL/Types/portabilityDB へは常に UTC で保存し、PHP 側でアプリのタイムゾーンへ変換するカスタム型
ORM/Query/portability日本語検索・日時抽出のための独自 DQL 関数(NORMALIZE / EXTRACT。DB 別に SQL 出し分け)
Filter/ドメイン全クエリに横断的に効く SQL フィルタ(在庫なし非表示・仮受注除外)
EventSubscriber/基盤Doctrine ライフサイクルへのフック(作成/更新日時の自動セット・税込価格算出・在庫有無フラグの再計算・旧在庫列の保護)と、接続時 TZ 設定の DBAL ミドルウェア
Common/CsvDataFixtures/基盤インストール時に CSV からマスタ等を一括投入する仕組み
+

このディレクトリは 7 サブ中 2 つ(Query/・Mapping Driver/)が純粋な拡張ポイントで、行数の多くを占めます。 + 「Doctrine を薄くラップ」ではなく「Doctrine を拡張可能にするため厚く被せている」のがコアの複雑さの一因です。

+
+ +
+

クエリカスタマイズ機構(Query/)

+

EC-CUBE の一覧・検索クエリ(ProductRepository / OrderRepository / CustomerRepository 等)は、 + プラグインやカスタマイズが後から条件を足せるよう作られています。中心が QueryCustomizer インターフェイスです。

+
Repository が queryKey(例: "product_search")付きで + ↓ Queries::customize($queryKey, $qb, $params) +Queries が queryKey に登録された QueryCustomizer 群を順に適用 + ↓ +WhereCustomizer → createStatements() が返す WhereClause[] を andWhere で追加 +JoinCustomizer → createStatements() が返す JoinClause[] を join で追加 +OrderByCustomizer→ OrderByClause[] を addOrderBy で追加
+ + + + + + + +
クラス役割
QueryCustomizer(interface)customize() と getQueryKey() を持つ。対象クエリは queryKey で識別。
QueriesqueryKey ごとに Customizer を保持し、customize() で該当分をまとめて適用。
WhereCustomizer / JoinCustomizer / OrderByCustomizer(abstract)用途別の基底。createStatements() だけ実装すればよい。
WhereClauseWHERE 句のファクトリ(eq / like / in / between / gt …)。パラメータバインドまで自動で行う。
JoinClause / OrderByClauseJOIN / ORDER BY を組み立てる値オブジェクト。JoinClause は付随する WHERE / ORDER BY も持てる。
+

登録は DI タグ eccube.query_customizer(QueryCustomizerPass が収集)。実装が QueryCustomizer を継承していないとコンパイル時に弾かれます。

+
+ +
+

DBAL 型・DQL 関数・フィルタ

+ +

DBAL カスタム型(DBAL/Types/)

+

UTCDateTimeType / UTCDateTimeTzType が Doctrine の datetime / datetimetz を差し替えます(doctrine.yaml の types)。 + DB へは常に UTC で保存し、PHP 側で読むときにアプリのタイムゾーン(既定 Asia/Tokyo、setTimeZone())へ変換します。 + 接続時にも InitSubscriber がセッションのタイムゾーンを UTC に固定します。

+ +

独自 DQL 関数(ORM/Query/)

+ + + + +
関数用途
NORMALIZEひらがな→カタカナ変換+小文字化。DB 差異を吸収したあいまい検索(PostgreSQL は TRANSLATE、MySQL は照合順序、他は LOWER)。
EXTRACT日時から YEAR/MONTH/DAY 等を取り出す。UTC 保存分をアプリ TZ へ補正した上で抽出(DB 別に SQL を出し分け)。
+ +

SQL フィルタ(Filter/)

+

Doctrine の SQLFilter で、有効化すると対象エンティティの全クエリに WHERE 条件が自動付与されます。いずれも doctrine.yaml では既定 enabled: falseで、必要な箇所で明示的に有効化します。

+ + + + +
フィルタ効果
NoStockHiddenFilter(option_nostock_hidden)ProductClass を在庫あり(または無制限)だけに絞る。条件は在庫数ではなく非正規化列 in_stock = true。
OrderStatusFilter(incomplete_order_status_hidden)仮受注(PROCESSING / PENDING)の Order を除外する。
+
+ +
+

マッピングドライバとライフサイクル開発者向け

+ +

属性マッピングドライバ(ORM/Mapping/Driver/)

+

EC-CUBE のエンティティプロキシ機構(トレイト追加で既存エンティティを拡張する仕組み)を成立させるための Doctrine AttributeDriver 派生です。

+ + + + + +
ドライバ役割
TraitProxyAttributeDriverクラスをスキャンする際、対応する Proxy ファイルがあれば元エンティティの代わりにそれを読む。
ReloadSafeAttributeDriver同一プロセス内で新規生成した Proxy を再読込するときの Fatal を回避(一時的にクラス名を変えてロードしメタデータ抽出)。
NopAttributeDriver常に空を返す no-op ドライバ。
+ +

ライフサイクルフック(EventSubscriber/)

+ + + + + + + +
Subscriberフック
SaveEventSubscriberprePersist / preUpdate で createDate / updateDate / currencyCode / creator を自動セット(メソッドが在れば)。
TaxRuleEventSubscriberProductClass の読込・保存時に税込価格(price01IncTax 等)を TaxRuleService で算出。
ProductClassInStockSubscriberonFlush で ProductClass::in_stock を在庫数(ProductStock::stock)と在庫無制限フラグから再計算。
LegacyProductClassStockColumnSubscriberpostGenerateSchemaTable で旧列 dtb_product_class.stock をスキーマに残し、マイグレーション前の schema:update で在庫数が失われないようにする。
InitSubscriber(Doctrine イベントではなく DBAL の Middleware)接続時に DB セッションのタイムゾーンを UTC に固定。
+
+ +
+

注意点開発者向け

+
    +
  • 日時は必ず UTC で DB に入る。生 SQL で日時を条件に使うと TZ ずれを起こす。DQL の EXTRACT は補正込みだが、直書き SQL は自前で補正が要る。
  • +
  • SQL フィルタは既定で無効。「在庫なしを隠す」「仮受注を除く」は自動では効かない。フロント商品一覧や売上集計など、必要な経路で明示的に enableFilter() する(コアの Repository / Controller が手本)。
  • +
  • in_stock は ORM の flush を通ったときだけ同期される。DBAL や生 SQL で在庫数を更新すると in_stock がずれ、在庫なし非表示の結果を誤る。
  • +
  • クエリに条件を足したいときは Repository を書き換えず、QueryCustomizer を eccube.query_customizer タグで登録して差し込む(アップグレード安全)。
  • +
  • WhereClause は必ずパラメータバインドを通す。生値を DQL 文字列へ連結しない(SQL インジェクション回避)。
  • +
  • Mapping ドライバ/Proxy まわりは app/proxy/entity/ 生成物と密結合。トレイト追加後の再生成は bin/console eccube:generate:proxies。
  • +
  • CsvDataFixtures はインストール/初期データ投入専用の低レベル一括 INSERT(同階層の definition.yml で投入順を定義)。通常の CSV 入出力(商品・受注)は Service/Csv*Service 側で、これとは別物。
  • +
+
+ + + diff --git a/src/Eccube/Doctrine/README.md b/src/Eccube/Doctrine/README.md new file mode 100644 index 00000000000..836a78e4d17 --- /dev/null +++ b/src/Eccube/Doctrine/README.md @@ -0,0 +1,20 @@ +# Doctrine — EC-CUBE 固有の Doctrine 拡張 + +エンティティ定義そのものではなく、Doctrine ORM / DBAL に被せた EC-CUBE 独自の基盤拡張。 +クエリの後付けカスタマイズ機構、日時の UTC 保存型、日本語検索用の DQL 関数、 +表示制御用 SQL フィルタ、エンティティプロキシ用マッピングドライバ、 +CSV からの初期データ投入、ライフサイクルフックを収める。 + +- 📖 仕様(人間向け): [README.html](./README.html) + +## 主要ファイル + +- `Query/QueryCustomizer.php` / `Query/Queries.php` — 既存クエリに WHERE/JOIN/ORDER BY を後付けする機構。`WhereClause` / `JoinClause` で句を組み立て、DI タグ `eccube.query_customizer` で登録 +- `DBAL/Types/UTCDateTimeType.php` — DB へは UTC 保存、PHP 側でアプリ TZ へ変換するカスタム日時型(`doctrine.yaml` の `types` で差し替え) +- `ORM/Query/Normalize.php` / `ORM/Query/Extract.php` — 日本語あいまい検索・日時抽出の独自 DQL 関数(`NORMALIZE` / `EXTRACT`) +- `Filter/OrderStatusFilter.php` / `Filter/NoStockHiddenFilter.php` — 仮受注除外・在庫なし非表示の SQL フィルタ(既定は無効、必要な経路で有効化) +- `EventSubscriber/SaveEventSubscriber.php` — prePersist/preUpdate で作成・更新日時や creator を自動セット +- `EventSubscriber/ProductClassInStockSubscriber.php` — onFlush で `ProductClass::in_stock`(在庫なし非表示フィルタの条件)を再計算 + +> クエリに条件を足すときは Repository を直接書き換えず `QueryCustomizer` を登録して差し込む(アップグレード安全)。 +> SQL フィルタは既定で無効なので、必要な箇所で `enableFilter()` を明示的に呼ぶ点に注意。 diff --git a/src/Eccube/Entity/README.html b/src/Eccube/Entity/README.html new file mode 100644 index 00000000000..f61e7232bff --- /dev/null +++ b/src/Eccube/Entity/README.html @@ -0,0 +1,119 @@ + + + + + +Entity — 仕様 + + + + +

Entity — Doctrine エンティティ

+ + +
+

概要

+

このディレクトリは、EC-CUBE のデータ構造を表す Doctrine エンティティを置きます。 + 会員・商品・受注・カートといった業務データは、ここに定義したクラスがそのまま DB のテーブルに対応します。

+

マッピング(クラス/プロパティと DB テーブル/カラムの対応)は PHP8 属性 #[ORM\...] で + クラスに直接書きます(XML・アノテーションは使いません)。スキーマの源泉はこの属性であり、 + カラムを足したいときは属性を書き換えます。

+ + + + +
種類置き場所 / 継承例
データ系エンティティ(dtb_*)Eccube\Entity/AbstractEntity を継承Customer Product Order
マスタ系エンティティ(mtb_*)Eccube\Entity\Master/AbstractMasterEntity を継承OrderStatus SaleType Pref
+
+ +
+

主要エンティティと関連

+

受注ドメインの詳細(ER 図・受注ステータスの遷移)は複製せず + doc4「受注」仕様 を参照してください。ここでは代表的なクラスと関連だけを示します。

+ + + + + + + + + +
エンティティ役割主な関連
Customer会員複数の CustomerAddress / Order を持つ
Product / ProductClass商品とその規格(サイズ・カラー)1 商品は複数 ProductClass を持つ。在庫・価格は ProductClass 単位
Order / OrderItem受注と明細1 受注は複数 OrderItem・複数 Shipping を持つ
Shipping出荷単位1 受注に複数あり得る(配送先・お届け日違い)。送料は Shipping 単位
Cart / CartItemカートと明細確定前の一時データ。確定で Order になる
Member管理者ユーザー—
BaseInfo店舗設定(店名・住所・税設定)単一レコード
+

関連は #[ORM\OneToMany] / #[ORM\ManyToOne] 等の属性で定義し、 + コレクション(1 対多の「多」側)はコンストラクタで ArrayCollection を初期化します。

+
+ +
+

データの持ち方

+
区分の凡例(なぜそうなっているか): ドメイン=EC 業務要件 / + 基盤=ORM/横断インフラの都合 / 負債=歴史的経緯による簡素化候補。
+
    +
  • ドメイン 在庫・価格は Product ではなく ProductClass が持つ。 規格を持たない商品も内部的に + ProductClass を 1 つ持ちます(規格の有無は Product::hasProductClass() で判定)。
  • +
  • 基盤 在庫数の実体は ProductStock(dtb_product_stock.stock。ProductClass と 1:1)だけが持つ。 + ProductClass::getStock() / setStock() は ProductStock へ委譲する(無ければ setStock() が作る)。旧列 dtb_product_class.stock はマッピングから外れ、 + DQL で pc.stock を参照すると例外になる。在庫無制限フラグ stock_unlimited と、在庫の有無を表す非正規化列 in_stock は ProductClass 側にあり、 + in_stock は flush 時に ProductClassInStockSubscriber が再計算する(Doctrine)。 + ProductClass を複製すると __clone() が ProductStock も複製し、複製先の在庫変更が複製元へ及ばないようにしている。
  • +
  • 基盤 金額は文字列で持つ。 Types::DECIMAL のカラム(Order の total・subtotal 等)は + Doctrine ORM 3.x で ?string になります。計算は float ではなく bcmath(bcadd/bcmul/bccomp、スケール 2)で行います。
  • +
  • ドメイン 受注金額は OrderItem 明細の合算。 商品行だけでなく送料・手数料・値引き・税・ポイントもそれぞれ独立した OrderItem の 1 行として持ち、 + 明細種別(OrderItemType)で区別します(OrderItem::isProduct() / isDeliveryFee() / isCharge() / isDiscount() / isTax() / isPoint())。 + 「OrderItem = 購入商品」ではないため、金額を集計・加工するときは明細種別で絞り込みます。
  • +
  • 基盤 単一テーブル継承(STI)。 マスタ系や Order・Block 等は 1 テーブルに複数の型を格納し、 + discriminator_type 列で型を区別します(#[ORM\InheritanceType('SINGLE_TABLE')])。
  • +
  • 基盤 作成・更新日時は自動。 create_date/update_date/creator はコアの SaveEventSubscriber が + 永続化時に自動セットするため、setter を生やすだけでよく、自前の #[ORM\PrePersist] は書きません。
  • +
+
「Order が存在する=確定済み注文」ではありません。カート確定の入口で受注は PROCESSING(仮受注)で作られ、 + 購入完了で NEW に遷移します。受注ステータスの詳細は doc4「受注」仕様 を参照。
+
+ +
+

拡張ポイント開発者向け

+

拡張ポイント エンティティ拡張(trait+プロキシ生成)は、プラグイン/Customize が + コアを書き換えずにカラムを足すための仕組みで、コアの複雑さの一因です(=「拡張性のための複雑さ」)。

+

既存エンティティへのフィールド追加はコアを書き換えず trait で行い、app/Customize/Entity/ に置きます。 + 反映には bin/console eccube:generate:proxies でプロキシを再生成します(app/proxy/entity/ に拡張後のエンティティが生成される)。 + 実装の書き方・DO/DON'T は eccube-entity/SKILL.md が正典です。

+ + + + + +
やりたいことやり方
コアエンティティにカラムを足すtrait を作り app/Customize/Entity/ に置く → プロキシ再生成
新規テーブルを追加するEccube\Entity にクラスを作り #[ORM\...] でマッピング
スキーマへ反映するdoctrine:schema:update --force(単純なカラム追加に ALTER マイグレーションは不要)
+
+ +
+

注意点開発者向け

+
    +
  • if (!class_exists(X::class)) { ... } のクラスラッパは書かない。 trait 追加はプロキシ生成が担い、ラッパはコアから撤去済み(旧方式の名残)。
  • +
  • ID の getter が ?int(nullable)なのは意図的。 IDENTITY 採番は永続化まで ID が未採番(null)だからです。
  • +
  • 金額 getter の戻り値を int/float 扱いしない。 DECIMAL は ?string。型宣言・代入・計算をこれに合わせます。
  • +
  • 他エンティティへの関連は親削除時の挙動を決める。 FK は既定で削除を止めます(RESTRICT 相当)。未指定だと退会・商品削除が FK 違反で失敗し得るため、 + onDelete を指定するか Service 側で後始末します。
  • +
  • STI テーブルへの INSERT は discriminator_type の指定が必須(例: mtb_sale_type は 'saletype')。
  • +
+
+ + + diff --git a/src/Eccube/Entity/README.md b/src/Eccube/Entity/README.md new file mode 100644 index 00000000000..9b62135b80e --- /dev/null +++ b/src/Eccube/Entity/README.md @@ -0,0 +1,16 @@ +# Entity — Doctrine エンティティ + +EC-CUBE のデータ構造を表す Doctrine エンティティ。会員・商品・受注・カート等の業務データを、 +PHP8 属性 `#[ORM\...]` でマッピングしたクラスとして持つ。**スキーマの源泉はこの属性**。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-entity/SKILL.md`](../../../.claude/skills/eccube-entity/SKILL.md) +- 📚 ドメイン詳細: https://doc4.ec-cube.net/spec_order + +## 主要ファイル + +- `AbstractEntity.php` — 全データ系エンティティの基底 +- `Master/AbstractMasterEntity.php` — マスタ系(`mtb_*`)の基底 +- `Customer.php` / `Member.php` — 会員・管理者ユーザー +- `Product.php` / `ProductClass.php` — 商品と規格(在庫・価格は `ProductClass` 単位) +- `Order.php` / `OrderItem.php` / `Shipping.php` — 受注・明細・出荷(STI、`discriminator_type` で型区別) diff --git a/src/Eccube/EventListener/README.html b/src/Eccube/EventListener/README.html new file mode 100644 index 00000000000..e52439f7fbe --- /dev/null +++ b/src/Eccube/EventListener/README.html @@ -0,0 +1,131 @@ + + + + + +EventListener — 仕様 + + + + +

EventListener — イベント購読による拡張

+ + +
+

概要

+

EC-CUBE の拡張は「コアを直接書き換えるのではなくイベントを購読する」のが基本方針です。 + この EventListener/ ディレクトリには、コア自身が Symfony の EventDispatcher に登録する + イベントサブスクライバが置かれています。いずれも EventSubscriberInterface を実装し、 + リクエスト/レスポンス/認証などアプリケーションのライフサイクルの節目にフックして横断的な処理を行います。

+ +

各リスナーの共通の性質:

+
    +
  • Symfony\Component\EventDispatcher\EventSubscriberInterface を実装し、static な getSubscribedEvents() で購読するイベントを宣言する。
  • +
  • サービス登録は不要。services.yaml の autoconfigure: true で Eccube\ 配下は自動的に kernel.event_subscriber タグが付く。
  • +
  • 業務ロジックはリスナー本体に溜め込まず、注入した Service へ委譲する薄い層として作られている。
  • +
+
このディレクトリはコアが標準搭載するリスナーです。プロジェクト固有の購読は + app/Customize/EventListener/、プラグインは各プラグインの EventListener/ に置きます(配置場所が違うだけで作り方は同じ)。
+
+ +
+

イベントの種類と代表リスナー

+

EC-CUBE のイベントは大きく 4 種類あります。このディレクトリの中身はほぼ「Symfony Kernel/Security イベント」(一部 Console イベント)で、 + コントローラ前後にフックする「EC-CUBE 独自イベント」やテンプレートへの差し込みは、隣の Event/ が定義を提供し、主にプラグイン側のリスナーが購読します。

+ + + + + + +
種類購読キー用途
Symfony Kernel イベントKernelEvents::REQUEST 等リクエスト/レスポンスのライフサイクル
Security イベントLoginFailureEvent::class 等認証の成否にフック
EC-CUBE 独自イベントEccubeEvents::XXX 定数(../Event/)コントローラ処理の前後にフック
Doctrine イベント#[AsDoctrineListener(event: ...)](../Doctrine/EventSubscriber/)エンティティの永続化前後
+ +

コアの代表的な Kernel/Security リスナー:

+ + + + + + + + + + + + + +
リスナー役割
TransactionListenerリクエスト単位で DB トランザクションを開始し、正常終了で commit・例外で rollback する(REQUEST/EXCEPTION/TERMINATE)。
TwigInitializeListenerリクエスト時に Twig のグローバル変数(店舗情報・ログイン状態等)を初期化する。
MaintenanceListenerメンテナンスモード時にレスポンスを差し替える(RESPONSE)。
SecurityListener / LoginHistoryListener認証成功・失敗(LoginFailureEvent 等)にフックしてログイン履歴やロック処理を行う。
TwoFactorAuthListenerCONTROLLER_ARGUMENTS にフックし、二段階認証が必要なコントローラを制御する。
MobileTemplatePathListenerモバイル端末を判定し、テンプレート探索パスを切り替える。
ExceptionListener / LogListener例外時のエラーページ描画・アクセスログ/エラーログ出力。
RestrictFileUploadListenerECCUBE_RESTRICT_FILE_UPLOAD=1 のとき、eccube_restrict_file_upload_urls に載った管理画面を読み取り専用にする(REQUEST。GET/HEAD は通し、書き込みは 403)。ファイルへ書き込む管理画面のルートを足したら、この一覧にも載せる。
RuntimeCachePoolClearListenercache:clear の終了時(ConsoleEvents::TERMINATE)に、権限を分けた構成で残る後始末(eccube:cache:build の実行など)を案内する。
PasswordNormalizationListenerログイン送信パスワードを NFKC 正規化してから firewall に渡す(NIST SP 800-63B 対応)。REQUEST を優先度 16(RouterListener の 32 の後・firewall の 8 の前)で購読するのが要点で、この順序を外すと Unicode パスワードの表記ゆれで照合に失敗する。対象は admin_login / mypage_login ルートのみ。
ForwardOnlyListenerコントローラのアクションに #[ForwardOnly] 属性が付いていると、内部 forward 専用となり直接 HTTP アクセスは AccessDeniedHttpException(CONTROLLER でチェック)。URL を持たせず内部遷移だけで呼びたいアクションに使う。
+
+ +
+

隣接する定義(Event / Doctrine)との関係

+

「イベントの定義」と「イベントの購読」は場所が分かれています。全体像は次の通りです。

+
コントローラ + │ new EventArgs(['key' => $v], $request) を dispatch + ▼ +Event/EccubeEvents.php … EC-CUBE 独自イベント名の定数(命名: CONTEXT_CONTROLLER_ACTION_PHASE) +Event/EventArgs.php … 独自イベントのペイロード(getArgument / setArgument / setResponse) +Event/TemplateEvent.php … 画面差し込み(addSnippet / addAsset / setSource) + │ + ▼ ← これらを購読する側が… +EventListener/ … コア標準のリスナー(Kernel / Security 中心)※このディレクトリ +app/Customize/EventListener/ … プロジェクト固有の購読 +app/Plugin/*/EventListener/ … プラグインの購読(独自イベント購読が主に行われる場所) + +Doctrine/EventSubscriber/ … エンティティ永続化前後のフック(#[AsDoctrineListener]) + SaveEventSubscriber(作成日時/更新日時の自動設定)が手本
+

Doctrine 系(../Doctrine/EventSubscriber/)は Kernel/Security リスナーと別ディレクトリで、 + #[AsDoctrineListener(event: Events::prePersist)] のように属性でイベントを宣言します。 + SaveEventSubscriber(prePersist/preUpdate)・TaxRuleEventSubscriber・ProductClassInStockSubscriber(onFlush)が実例です(同じディレクトリの InitSubscriber は Doctrine イベントではなく DBAL のミドルウェア)。

+
+ +
+

拡張ポイント開発者向け

+

独自の処理をフックするには、購読したいイベントに応じてリスナーを 1 つ足すだけです。 + 実装の書き方・DO/DON'T は eccube-event-subscriber/SKILL.md が正典です。ここでは要点だけ示します。

+ + + + + + +
やりたいことフックするイベント
コントローラ処理の前後に割り込むEC-CUBE 独自イベント(EccubeEvents::XXX 定数)を購読し EventArgs で読み書き
リクエスト/レスポンスを横断的に変えるKernel イベント(KernelEvents::REQUEST 等)。if (!$event->isMainRequest()) return; でサブリクエストを除外
画面に HTML/アセットを差し込むテンプレートイベント(ファイル名がイベント名。Skill twig-template)
エンティティの永続化前後に処理を足すDoctrine イベント(#[AsDoctrineListener])
+

登録の実際:

+
    +
  • 共通 クラスに EventSubscriberInterface を実装し public static function getSubscribedEvents() を定義するだけ。Eccube\ / Customize\ / Plugin\ 配下は autoconfigure で自動登録される(services.yaml への手動登録は二重発火の元。スカラー引数を渡すために arguments だけを書くのは可)。
  • +
  • 優先度 同一イベントを複数が購読するとき、[メソッド名, 優先度] の 数値が大きいほど先に実行される。
  • +
+
+ +
+

注意点開発者向け

+
    +
  • getSubscribedEvents() は必ず static。非 static だと登録されず、リスナーが黙って動かない。
  • +
  • 独自イベント名は文字列直書きせず EccubeEvents 定数を使う(タイポの温床)。定義は ../Event/EccubeEvents.php。
  • +
  • autoconfigure 済みなのに services.yaml へタグ付きで手動登録しない(二重発火する)。引数だけの定義(例: RuntimeCachePoolClearListener)は問題ない。
  • +
  • Kernel イベントでは サブリクエストの除外(isMainRequest())を忘れない。TransactionListener のようにライフサイクル全体を跨ぐリスナーは特に注意。
  • +
  • リスナーに業務ロジックを溜め込まない。重い処理は Service へ委譲し、リスナーは「フック点で Service を呼ぶ」薄い層に保つ。
  • +
  • 登録状況・優先度は bin/console debug:event-dispatcher で確認できる。効かないときはまずここに出ているかを見る。
  • +
+
+ + + diff --git a/src/Eccube/EventListener/README.md b/src/Eccube/EventListener/README.md new file mode 100644 index 00000000000..cf7299576dc --- /dev/null +++ b/src/Eccube/EventListener/README.md @@ -0,0 +1,18 @@ +# EventListener — イベント購読による拡張 + +EC-CUBE の拡張は「コアを書き換えず、イベントを購読する」のが基本方針。 +このディレクトリには、コア自身が Symfony の `EventDispatcher` に登録する +イベントサブスクライバ(`EventSubscriberInterface` 実装)が置かれ、 +リクエスト/レスポンス/認証などライフサイクルの節目にフックして横断的な処理を行う。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-event-subscriber/SKILL.md`](../../../.claude/skills/eccube-event-subscriber/SKILL.md) +- 📚 関連: イベント定義 [`../Event/`](../Event/)(`EccubeEvents` / `EventArgs` / `TemplateEvent`)・Doctrine イベント [`../Doctrine/EventSubscriber/`](../Doctrine/EventSubscriber/) + +## 主要ファイル + +- `TransactionListener.php` — リクエスト単位の DB トランザクション(正常時 commit・例外時 rollback) +- `TwigInitializeListener.php` — Twig グローバル変数の初期化(店舗情報・ログイン状態) +- `MaintenanceListener.php` — メンテナンスモード時のレスポンス差し替え +- `SecurityListener.php` / `LoginHistoryListener.php` — 認証成否にフック(ログイン履歴・ロック) +- `TwoFactorAuthListener.php` — 二段階認証が必要なコントローラの制御 diff --git a/src/Eccube/Form/README.html b/src/Eccube/Form/README.html new file mode 100644 index 00000000000..b5300fa1420 --- /dev/null +++ b/src/Eccube/Form/README.html @@ -0,0 +1,121 @@ + + + + + +Form — 仕様 + + + + +

Form — フォーム

+ + +
+

概要

+

この Form/ ディレクトリは、EC-CUBE のフォーム機能一式を置くレイヤです。会員登録・お問い合わせ・ + 購入手続き(フロント)や、商品・受注・会員の編集/検索(管理画面)で使う入力フォームを、Symfony Form の仕組みで組み立てます。

+

フォーム「型」の本体(Type/)だけでなく、既存フォームへの横断拡張・独自バリデーション・送信値の前処理・値変換も + このレイヤに同居します。まずは下の構成表で全体像をつかんでください。

+
+ +
+

ディレクトリ構成(Form レイヤ全体像)

+
区分の凡例(この機構は何のためにあるか): 拡張ポイント=プラグイン/Customize が介入する仕組み / + ドメイン=EC 業務要件 / 基盤=フォーム機能の土台。 + Form レイヤは 拡張ポイントは Extension/ の 1 つだけで、他はフォームを作るための素の機構(Doctrine ほど拡張ポイントは多くない)。
+ + + + + + + +
サブディレクトリ区分役割代表クラス
Type/基盤フォーム型本体(1 型 = 1 クラス)。用途別にサブ(Admin/ Front/ Shopping/ Master/ Install/)へ分かれるAddressType / PriceType / Front/EntryType / Admin/ProductType
Extension/拡張ポイント既存 FormType への横断拡張(AbstractTypeExtension)。コアを書き換えず全フォームに機能を足すHelpTypeExtension / HTMLPurifierTextTypeExtension / DoctrineOrmExtension
Validator/ドメインフォーム用の独自バリデーション(Constraint + ConstraintValidator の対)Email / PasswordBlocklist / TwigLint
EventListener/基盤送信値の前処理(Symfony Form の PRE_SUBMIT で入力を整形)。※カーネルの EventListener レイヤとは別物ConvertKanaListener / TruncateHyphenListener / HTMLPurifierListener
DataTransformer/基盤フォーム値とモデルの相互変換(DataTransformerInterface)EntityToIdTransformer
+
Form/EventListener/ は「フォーム送信データの整形」用で、Symfony の FormEvents::PRE_SUBMIT にフックします。 + リクエスト/レスポンスやログインに絡むカーネルの EventListener(src/Eccube/EventListener/)とは別レイヤなので混同しないこと。
+
+ +
+

フォーム型(Type/)

+

各型は Symfony の AbstractType を継承し、主に次のメソッドで構成されます。

+ + + + + +
メソッド役割
buildForm()フィールド(add())とバリデーション制約(Assert\*)を組み立てる
configureOptions()既定オプションを設定。エンティティに紐づくフォームは data_class を指定する
getBlockPrefix(): stringテンプレートのブロック名・フィールド名の接頭辞。クラス名から導かれる既定値と異なる場合だけ定義する(既定値と同じものは Rector が削除する)。定義するなら戻り値型 : string は必須
+

用途ごとにサブディレクトリへ分かれ、直下には「どの画面からも使い回す部品型」を置きます。 + 同じ入力(住所・金額・都道府県 等)を毎回手書きせず、汎用型を add() で組み合わせるのが原則です。

+ + + + + + +
やりたいこと使う型
金額入力(通貨・桁区切り・上下限を EC-CUBE 設定から自動適用)PriceType(内部で MoneyType を親に持つ)
住所(都道府県 + 住所1 + 住所2 をまとめて)AddressType
マスタ選択肢(都道府県・性別・支払方法 等)Master/*Type(MasterType = EntityType ベース)
確認用にメール/パスワードを 2 回入力RepeatedEmailType / RepeatedPasswordType
+
入力値の動的制御(他フィールドの値でバリデーションを変える等)は $builder->addEventListener(FormEvents::PRE_SET_DATA, ...) / + POST_SUBMIT で行います。AddressType は required に応じて NotBlank を後付けする実例です。
+
+ +
+

バリデーション・値変換・送信前処理

+

フォーム型に付随する周辺部品です。いずれもコアが標準提供し、フォームから利用します。

+ + + + + + + + + +
部品役割
Validator/Email(+ EmailValidator)EC-CUBE 独自のメールアドレス検証制約。
Validator/PasswordBlocklist(+ Validator)脆弱・禁止パスワードのブロックリスト検証(NIST SP 800-63B 系のパスワード要件対応)。
Validator/TwigLint(+ Validator)入力された Twig テンプレート文字列の構文検証(メール・CMS 等ユーザー編集テンプレートの保存前チェック)。
DataTransformer/EntityToIdTransformerフォーム値の ID ⇔ エンティティ相互変換(transform()/reverseTransform())。
EventListener/ConvertKanaListenerPRE_SUBMIT でかなを変換する。
EventListener/TruncateHyphenListenerPRE_SUBMIT でハイフンを除去する(電話番号・郵便番号 等)。
EventListener/HTMLPurifierListenerPRE_SUBMIT を最優先(priority 1000001)で走らせ、送信値を HTML 無害化する。
+
+ +
+

拡張ポイント:既存フォームへ項目を足す開発者向け

+

既存フォームにフィールドを追加したいときは、コアの FormType を書き換えず FormTypeExtension を使うのが鉄則です。 + 詳しい DO/DON'T は eccube-formtype/SKILL.md が正典です。

+
    +
  • コア 横断的な拡張は src/Eccube/Form/Extension/ に AbstractTypeExtension を継承して置く。 + getExtendedTypes() で対象 Type を指定する(例: HelpTypeExtension は全 FormType に help を追加、HTMLPurifierTextTypeExtension は入力を無害化)。
  • +
  • プラグイン / Customize プロジェクト固有の項目追加は app/Customize/Form/Extension/ に置く(コアはアップグレードで上書きされるため)。
  • +
  • エンティティ属性 FormTypeExtension を書かずに、エンティティのプロパティへ #[FormAppend(type: ...)] 属性を付けるだけでも項目を追加できる。 + DoctrineOrmExtension が全 FormType の PRE_SET_DATA でメタデータを走査し、data_class のプロパティに #[FormAppend] があれば自動で add() する(EC-CUBE 独自機構。Symfony 標準ドキュメントには無い)。
  • +
+

新規フォームを一から作る場合は名前空間 Eccube\Form\Type・AbstractType 継承・ライセンスヘッダ・型宣言(PHPStan level 6)を守り、依存はコンストラクタ DI で受けます。

+
+ +
+

注意点開発者向け

+
    +
  • getBlockPrefix() は既定値(クラス名から導かれる接頭辞)と異なる場合だけ書く。書くなら戻り値型 : string は必須(Symfony 6+)。
  • +
  • 管理画面の検索フォームでも CSRF を無効化しない。csrf_protection => true(既定)を保ち、テンプレートでトークンを出力する。
  • +
  • バリデーションは制約クラス(Assert\NotBlank 等)で付ける。Range で min/max 両方指定時は notInRangeMessage を使う。
  • +
  • 既存の EC-CUBE 提供 Type(AddCartType / AddressType / PriceType 等)があれば再利用し、具象クラス直依存を避ける。
  • +
  • パスワード入力を扱う場合、対象エンティティに plain_password プロパティが必要。
  • +
  • TextType の HTML 自動無害化(HTMLPurifier)はフロントのリクエスト時だけ効く(HTMLPurifierTextTypeExtension は Context::isFront() のときのみ purify_html を有効化)。管理画面の TextType 入力は自動サニタイズされないので、出力側(Twig)でのエスケープに頼る。
  • +
+
+ + + diff --git a/src/Eccube/Form/README.md b/src/Eccube/Form/README.md new file mode 100644 index 00000000000..09ee4474c1d --- /dev/null +++ b/src/Eccube/Form/README.md @@ -0,0 +1,16 @@ +# Form — フォーム + +会員登録・お問い合わせ・購入手続き(フロント)や商品・受注・会員の編集/検索(管理画面)で使う +フォーム機能一式を置くレイヤ。フォーム型本体(`Type/`)に加え、既存フォームへの横断拡張・独自バリデーション・ +送信値の前処理・値変換も同居する。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-formtype/SKILL.md`](../../../.claude/skills/eccube-formtype/SKILL.md) + +## サブディレクトリ + +- `Type/` — フォーム型本体(1 型 = 1 クラス)。用途別に `Admin/` `Front/` `Shopping/` `Master/` `Install/` +- `Extension/` — 既存 FormType への横断拡張(`AbstractTypeExtension`)。例: `HTMLPurifierTextTypeExtension` / `DoctrineOrmExtension`(`#[FormAppend]` 自動追加) +- `Validator/` — フォーム用の独自バリデーション(`Constraint` + `ConstraintValidator`)。`Email` / `PasswordBlocklist` / `TwigLint` +- `EventListener/` — 送信値の前処理(`PRE_SUBMIT`)。かな変換・ハイフン除去・HTML 無害化。※カーネルの `src/Eccube/EventListener/` とは別 +- `DataTransformer/` — フォーム値とモデルの相互変換。`EntityToIdTransformer`(ID ⇔ Entity) diff --git a/src/Eccube/Plugin/README.html b/src/Eccube/Plugin/README.html new file mode 100644 index 00000000000..5cf92dcd317 --- /dev/null +++ b/src/Eccube/Plugin/README.html @@ -0,0 +1,105 @@ + + + + + +Plugin 基盤(コア) — 仕様 + + + + +

Plugin 基盤(コア)

+ + +
+

概要

+

このディレクトリ(src/Eccube/Plugin/)は、プラグインのライフサイクル基盤を提供します。 + 実体は AbstractPluginManager.php ただ 1 ファイルで、各プラグインの PluginManager はこれを継承します。

+
存在理由: 拡張ポイント(プラグイン拡張のための仕組み)。この階層に業務ロジックは無く、 + 「プラグインがコアの処理に介入するための基底クラス」だけを提供する ——「拡張性のためにコアが抱える複雑さ」の一部。 + 区分の凡例は他レイヤ README(例 Doctrine)と共通。
+ +

プラグインそのものの置き場所は app/Plugin/{PluginCode}/(別ディレクトリ。app/Plugin/README.md)です。 + ここはあくまでコア側の共通処理を持ち、プラグイン本体のコードは含みません。

+ +
プラグインの install/enable/disable/update/uninstall を駆動するコア機構本体は、この階層ではなく + src/Eccube/Service/PluginService.php・src/Eccube/Service/Composer/・src/Eccube/Command/Plugin*Command.php にあります。 + このディレクトリは「プラグインが継承する基底クラス」の置き場です。
+
+ +
+

ライフサイクルと AbstractPluginManager

+

AbstractPluginManager は 5 つのライフサイクルメソッドを持ち、いずれもデフォルト no-opです。 + プラグインは必要なものだけ override します(すべて任意)。呼び出しはコアの PluginService が行います。

+ + + + + + + +
メソッド呼ばれる契機用途の例
installインストール時初期データ投入
enable有効化時マイグレーション適用
disable無効化時マイグレーションを戻す
update更新時差分マイグレーション
uninstallアンインストール時クリーンアップ
+

シグネチャは全メソッド共通で (array $meta, ContainerInterface $container)。$meta['code'] は + プラグインの composer.json の extra.code(PluginCode)です。

+ +

migration() ヘルパ

+

AbstractPluginManager::migration() は、プラグイン専用のマイグレーションを実行するための共通処理です。

+
$this->migration($connection, $meta['code']); + → 対象ディレクトリ: app/Plugin/{code}/DoctrineMigrations/(省略時の既定) + → 名前空間 : Plugin\{code}\DoctrineMigrations + → 管理テーブル : migration_{code}(小文字。コア本体の migration_versions とは別)
+
PluginManager 実行時にはコアが Doctrine の schema:update を自動で行うため、 + このメソッドは主にデータの更新(INSERT 等)に使います(カラム追加は属性+schema:update で足りる)。
+

第 3 引数 $version でマイグレーション先を指定できます(既定 null)。 + null=最新まで適用、'0'=先頭まで巻き戻し(全ロールバック)。disable() で + 「マイグレーションを戻す」のはこの migration($conn, $code, '0') を指します。第 4 引数でディレクトリも差し替え可 + (AbstractPluginManager::migration())。

+
+ +
+

PluginManager の書き方開発者向け

+

ライフサイクル処理が必要なときだけ、プラグイン側に Plugin\{Code}\PluginManager(クラス名は固定)を作り、 + AbstractPluginManager を継承します。実装の書き方・DO/DON'T は + eccube-plugin/SKILL.md が正典です。

+
namespace Plugin\Example; + +class PluginManager extends AbstractPluginManager +{ + public function enable(array $meta, ContainerInterface $container): void + { + $conn = $container->get('doctrine')->getManager()->getConnection(); + $this->migration($conn, $meta['code']); // migration_example テーブルで管理 + } +}
+
+ +
+

注意点開発者向け

+
    +
  • ここにプラグイン本体を置かない。プラグインは app/Plugin/{PluginCode}/。このディレクトリは基底クラス専用。
  • +
  • install 直後はデフォルト無効(enabled=false)。有効化は eccube:plugin:enable --code=... か管理画面で行う。
  • +
  • プラグインのマイグレーションはコアの app/DoctrineMigrations/ とは別管理(migration_{code} テーブル)。混同しない。
  • +
  • コア機構(インストール・Composer 連携・コマンド)を追う場合は PluginService / src/Eccube/Command/Plugin*Command.php を見る。
  • +
+
+ + + diff --git a/src/Eccube/Plugin/README.md b/src/Eccube/Plugin/README.md new file mode 100644 index 00000000000..9356efb7ebf --- /dev/null +++ b/src/Eccube/Plugin/README.md @@ -0,0 +1,22 @@ +# Plugin 基盤(コア) + +プラグインの**ライフサイクル基盤**を提供する階層。実体は `AbstractPluginManager.php` のみで、 +各プラグインの `PluginManager` はこれを継承する。プラグイン本体は置かず(本体は `app/Plugin/{PluginCode}/`)、 +install/enable/disable/update/uninstall の共通処理と `migration()` ヘルパを持つ。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-plugin/SKILL.md`](../../../.claude/skills/eccube-plugin/SKILL.md) +- 📦 プラグイン本体の置き場: [`app/Plugin/`](../../../app/Plugin/README.md) + +## 主要ファイル + +- `AbstractPluginManager.php` — 全 `PluginManager` の基底。5 つのライフサイクルメソッド(すべて no-op 既定)と、 + プラグイン専用マイグレーションを回す `migration()`(既定で `app/Plugin/{code}/DoctrineMigrations`、管理テーブルは `migration_{code}`) + +## コア機構本体(別階層) + +ライフサイクルを駆動する実処理はこの階層ではなく次にある。 + +- `src/Eccube/Service/PluginService.php` — install/enable/disable/uninstall の中核 +- `src/Eccube/Service/Composer/` — Composer 連携(依存取得・削除) +- `src/Eccube/Command/Plugin*Command.php` — `eccube:plugin:generate|install|enable|disable|update|uninstall` diff --git a/src/Eccube/Repository/README.html b/src/Eccube/Repository/README.html new file mode 100644 index 00000000000..cbea58503fb --- /dev/null +++ b/src/Eccube/Repository/README.html @@ -0,0 +1,100 @@ + + + + + +Repository — 仕様 + + + + +

Repository — データアクセス

+ + +
+

概要

+

このディレクトリは、エンティティの取得・保存・削除といったデータアクセスを担う Doctrine リポジトリを置きます。 + 「商品を条件で検索する」「会員の受注一覧を引く」といった DB への問い合わせは、コントローラやサービスに直接書かず、 + 対応するリポジトリのメソッドに集約します。

+

各リポジトリは Eccube\Repository\AbstractRepository<T>(Symfony の ServiceEntityRepository を継承)を継承し、 + コンストラクタで担当エンティティのクラスを親へ渡します。1 エンティティにつき 1 リポジトリが対応します。

+
責務はデータアクセスのみ。 金額計算・状態遷移・在庫引当などの業務ロジックはここに書かず Service / PurchaseFlow へ寄せます。
+
+ +
+

主要リポジトリ

+ + + + + + + +
リポジトリ担当エンティティ主な役割
ProductRepositoryProduct店頭・管理画面の商品検索(規格の並び替え込み取得)
OrderRepositoryOrder受注検索・ステータス変更・会員別受注一覧
CustomerRepositoryCustomer会員検索・認証まわりの取得
CartRepositoryCartカートの取得・保存
マスタ系(Master/)OrderStatus 等マスタ(mtb_*)の取得
+

検索系メソッドの定石は getQueryBuilderBySearchData(array $searchData): QueryBuilder を返す形で、 + 管理画面用は getQueryBuilderBySearchDataForAdmin() のように分けます(ProductRepository / OrderRepository が実例)。

+
+ +
+

クエリの書き方

+
$qb = $this->createQueryBuilder('p'); // QueryBuilder / DQL を使う(生 SQL 連結はしない) +$qb->andWhere('p.name LIKE :name') // 検索条件を組み立て + ->setParameter('name', '%'.$word.'%'); // 値は必ず setParameter でバインド(SQL インジェクション対策) +return $qb->orderBy('p.id', 'DESC'); // 一覧はページング用に QueryBuilder を返すに留める
+
    +
  • createQueryBuilder() / DQL を使う。 生 SQL の文字列連結は避けます。
  • +
  • 値は必ず setParameter() でバインド。 検索語を文字列に直挿ししません。
  • +
  • N+1 を避ける。 必要に応じて addSelect() / JOIN で関連をまとめて取得します。
  • +
  • ページングはコントローラ/サービス側。 リポジトリは QueryBuilder を返すに留め、knp_paginator は呼び出し側で適用します。
  • +
+
+ +
+

永続化と拡張開発者向け

+

保存・削除は AbstractRepository が提供する save(AbstractEntity $entity) / delete(AbstractEntity $entity) を使います。 + これらをオーバーライドする場合は親クラスのシグネチャを厳守します。

+
save() / delete() は flush() しません。 実体は persist() / remove() の呼び出しのみで、 + DB への確定は行いません(AbstractRepository::save()/delete())。呼び出し側で EntityManager::flush() を明示的に実行するか、 + 一連の処理をまとめて 1 回 flush します。「save() したのに保存されない」の多くはこの flush 漏れです。 + ただしこれは AbstractRepository の既定動作で、オーバーライドして内部で flush() するリポジトリも多い + (MemberRepository / CategoryRepository / NewsRepository / TaxRuleRepository / FaqRepository 等。並び順の採番などを伴う)。 + トランザクションの途中で呼ぶ前に、対象リポジトリの実装を確認します。
+

検索クエリの拡張はサービス/メソッド差し替えではなくフックで行えます。 主要な検索系メソッドは組み立てた QueryBuilder を + Queries::customize(QueryKey::<対象>, $qb, $searchData) に通してから返します(ProductRepository / OrderRepository / CustomerRepository 等)。 + QueryCustomizer を実装して登録すると、対象の QueryKey(QueryKey.php に定義)に対して検索条件や JOIN を後付けで注入できます。 + プラグイン/カスタマイズで検索条件を足すときはこのフックを使い、リポジトリのコアメソッド自体は書き換えません。

+

プロジェクト固有の検索メソッド追加は app/Customize/Repository/ で行います。 + 実装の書き方・DO/DON'T は eccube-repository/SKILL.md が正典です。

+
+ +
+

注意点開発者向け

+
    +
  • 業務ロジックを持ち込まない。 金額計算・状態遷移・受注処理は Service / PurchaseFlow の責務です。
  • +
  • ServiceEntityRepository を直接継承しない。 必ず AbstractRepository<T> を継承します。
  • +
  • 全件取得を無制限にしない。 画面表示の一覧・関連取得は件数が際限なく増え得るため、ページング(QueryBuilder を返す)か上限を設けます。
  • +
  • 受注一覧は仮受注を除外する。 PROCESSING・PENDING は確定前の仮受注のため、売上集計や一覧では除外します(OrderStatusFilter)。
  • +
+
+ + + diff --git a/src/Eccube/Repository/README.md b/src/Eccube/Repository/README.md new file mode 100644 index 00000000000..eb36c6bae8a --- /dev/null +++ b/src/Eccube/Repository/README.md @@ -0,0 +1,17 @@ +# Repository — データアクセス + +エンティティの取得・保存・削除を担う Doctrine リポジトリ。DB への問い合わせ(検索・一覧の絞り込み)を +コントローラやサービスに散らさず、`AbstractRepository` を継承した各リポジトリに集約する。 +**責務はデータアクセスのみ**(業務ロジックは Service / PurchaseFlow へ)。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-repository/SKILL.md`](../../../.claude/skills/eccube-repository/SKILL.md) +- 📚 ドメイン詳細: https://doc4.ec-cube.net/spec_order + +## 主要ファイル + +- `AbstractRepository.php` — 全リポジトリの基底。`save()` / `delete()` を提供 +- `ProductRepository.php` — 商品検索(`getQueryBuilderBySearchData` / `...ForAdmin`) +- `OrderRepository.php` — 受注検索・ステータス変更・会員別受注一覧 +- `CustomerRepository.php` — 会員検索・認証まわりの取得 +- `Master/` — マスタ系(`mtb_*`)リポジトリ diff --git a/src/Eccube/Security/README.html b/src/Eccube/Security/README.html new file mode 100644 index 00000000000..83dcf6291e6 --- /dev/null +++ b/src/Eccube/Security/README.html @@ -0,0 +1,109 @@ + + + + + +Security — 仕様 + + + + +

Security — 認証・認可

+ + +
+

概要

+

このディレクトリは、EC-CUBE の認証(誰か)・認可(何を許すか)の中核部品を収めます。 + EC-CUBE のアクセス制御は「ファイアウォール+ロール+Voter」の 3 層モデルで、 + 個々のアクションに #[IsGranted] を付けるのではなく、パスのプレフィックス単位でまとめて守るのが基本です。

+ +

ファイアウォールと access_control の宣言は app/config/eccube/packages/security.yaml、 + および src/Eccube/DependencyInjection/EccubeExtension.php(後述の動的生成)にあります。 + このディレクトリはそこに差し込まれる実装(UserProvider / PasswordHasher / Voter / 各種ハンドラ)を提供します。

+ + + + + + +
ファイアウォール対象パス認証する主体
admin^/%eccube_admin_route%/Member(管理者)。CSRF 有効・ログイン試行制限あり
customer^/(サイト全体)Customer(会員)。remember_me あり
dev静的リソース等security: false(認証対象外)
+

認可判定は unanimous 戦略(allow_if_all_abstain: false)。 + 1 つでも Voter が DENY すれば拒否、全 Voter が棄権しても拒否(=明示的な許可が必要)です。

+
+ +
+

ディレクトリ構成と役割

+ + + + + + + + + + + + +
サブクラス役割
Core/UserCustomerProvider / MemberProviderログイン ID からユーザーを解決する UserProviderInterface。会員は email+本会員(REGULAR)、管理者は login_id+有効(Work::ACTIVE)で引く。PasswordUpgraderInterface 実装によりログイン成功時にハッシュを自動再計算・保存する。
PasswordHasherPasswordHasherパスワードのハッシュ・照合(hash_hmac)。旧バージョン(2.11 未満)からの移行照合も内包。
Core/EncoderPasswordEncoderハッシュ生成・照合・salt 生成のヘルパ。eccube_auth_magic 等の設定を参照する。
VoterAuthorityVoter管理画面内の URL 単位の権限制御。Member の Authority に紐づく AuthorityRole.deny_url にパスが一致したら ACCESS_DENIED。
Http/AuthenticationEccubeAuthenticationSuccessHandler / EccubeAuthenticationFailureHandler / EccubeLogoutSuccessHandlerログイン成功・失敗時のリダイレクト先を絶対 URL からパスへ正規化(オープンリダイレクト対策)。ログアウト時は管理画面のメンテナンス Cookie を破棄。
+
+ +
+

access_control はどこで決まるか

+

「どのパスにどのロールを要求するか」は security.yaml に直書きされておらず、 + EccubeExtension.php が動的に生成して差し込みます。security.yaml だけを見ても全体像は掴めません。

+
^/%eccube_admin_route%/login IS_AUTHENTICATED_ANONYMOUSLY … ログイン画面は誰でも +^/%eccube_admin_route%/ ROLE_ADMIN … 管理画面全体(プレフィックス単位で保護) +^/mypage/login IS_AUTHENTICATED_ANONYMOUSLY +^/mypage/withdraw_complete IS_AUTHENTICATED_ANONYMOUSLY +^/mypage/change IS_AUTHENTICATED_FULLY … 重要操作は FULLY(remember-me 除外) +^/mypage/ ROLE_USER … 会員ページ全体
+
最重要の含意: 新しい管理アクションは、認可の主たる仕組みがファイアウォール(パスのプレフィックス)なので、 + 必ず %eccube_admin_route% 配下のパスに置くこと。配下から外すと無認証で到達できてしまう(よくある重大な事故)。 +
また ^/mypage/ は ROLE_USER 前提なので、その配下のコントローラでは getUser() は非 null が保証される。
+
+ +
+

拡張ポイント開発者向け

+

実装の書き方・DO/DON'T は eccube-security/SKILL.md が正典です。ここでは全体像だけ示します。

+ + + + + + +
やりたいこと手段
特定の権限にこの管理画面を見せないコントローラ改変ではなく dtb_authority_role の deny_url 設定。AuthorityVoter が拾う。
独自の認可ルールを足すSymfony の Voter を継承(autoconfigure で security.voter タグは自動付与)。対象外は必ず ACCESS_ABSTAIN。
ログイン後の遷移・ハンドラを変えるHttp/Authentication の各ハンドラを継承/デコレート。
フォームを介さない状態変更(削除・Ajax)コントローラで $this->isTokenValid() を明示的に呼び CSRF を検証(GET 以外は必須)。
+
+ +
+

注意点開発者向け

+
    +
  • unanimous 戦略に注意。独自 Voter で「対象外」を ACCESS_DENIED で返すと全体が拒否になる。対象外は必ず ACCESS_ABSTAIN。
  • +
  • IDOR: フロントで {id} から取得したエンティティは getUser() と突き合わせ、他人のものなら AccessDeniedHttpException。
  • +
  • 重要操作は IS_AUTHENTICATED_FULLY(パスワード変更・退会・購入確定など)。remember-me(盗難 Cookie)を弾く。
  • +
  • パスワードは PasswordHasher / PasswordEncoder 任せにし、自前ハッシュ・平文比較を書かない。PasswordHasher::needsRehash() が常に true を返すため、ログインのたびに現行アルゴリズムへ再ハッシュされる。
  • +
  • AuthorityVoter はリクエストが取得できないとき(テスト等)ACCESS_ABSTAIN を返す。deny_url は正規表現として評価され、誤りがあれば preg_quote でエスケープしてリトライする。
  • +
  • ログイン成功/失敗ハンドラは、リダイレクト先が絶対 URL のときパス部分だけを取り出す(外部ドメインへのオープンリダイレクト対策)。
  • +
+
+ + + diff --git a/src/Eccube/Security/README.md b/src/Eccube/Security/README.md new file mode 100644 index 00000000000..ee8f3de9228 --- /dev/null +++ b/src/Eccube/Security/README.md @@ -0,0 +1,19 @@ +# Security — 認証・認可 + +EC-CUBE の認証(誰か)・認可(何を許すか)の中核部品。アクセス制御は +「ファイアウォール+ロール+Voter」の 3 層で、パスのプレフィックスで面的に守るのが基本。 +UserProvider(ユーザー解決)・PasswordHasher(照合)・AuthorityVoter(URL 単位の権限)・ +ログイン/ログアウトの各ハンドラを収める。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-security/SKILL.md`](../../../.claude/skills/eccube-security/SKILL.md) + +## 主要ファイル + +- `Core/User/CustomerProvider.php` / `Core/User/MemberProvider.php` — ログイン ID からユーザーを解決する UserProvider。ログイン成功時にパスワードを現行アルゴリズムへ自動再ハッシュ +- `PasswordHasher/PasswordHasher.php` — パスワードのハッシュ・照合(旧 2.11 未満からの移行照合を内包) +- `Voter/AuthorityVoter.php` — 管理画面内の URL 単位の権限制御(`AuthorityRole.deny_url` に一致で拒否) +- `Http/Authentication/EccubeAuthenticationSuccessHandler.php` — ログイン後リダイレクトを絶対 URL からパスへ正規化(オープンリダイレクト対策)。失敗・ログアウト用ハンドラも同ディレクトリ + +> access_control(どのパスにどのロールを要求するか)は security.yaml 直書きではなく +> `src/Eccube/DependencyInjection/EccubeExtension.php` が動的生成する点に注意。 diff --git a/src/Eccube/Service/AgentCommerce/Catalog/README.html b/src/Eccube/Service/AgentCommerce/Catalog/README.html new file mode 100644 index 00000000000..02310c5cab8 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Catalog/README.html @@ -0,0 +1,92 @@ + + + + + +AgentCommerce Catalog — 仕様 + + + + +

AgentCommerce Catalog — カタログ提供

+ + +
+

概要

+

EC-CUBE の Product / ProductClass をプロトコル非依存の中立 DTO に写し、 + そこから ACP Feed(push 型)と UCP Catalog(POST RPC)の両方へ出力するサブドメインです。 + addressable unit のマッピングは 親 Product = AgentCatalogItemDto / variant ProductClass = AgentCatalogVariantDto。

+

出力対象は公開中商品(ProductStatus::DISPLAY_SHOW)かつ visible な ProductClass に限ります。 + 金額は店頭と同じ税込価格を MinorUnitConverter で minor-unit 整数へ変換します(確定金額は CheckoutSession で再計算)。

+
+ +
+

中立 DTO / enum

+ + + + + + + + + +
クラス役割
AgentCatalogItemDtoカタログ商品(親 = Product)。variants[] に variant DTO を持つ。
AgentCatalogVariantDtoバリエーション(addressable unit = ProductClass)。金額は minor-unit 整数。
AgentCatalogOptionDtovariant option。EC-CUBE の規格分類(ClassName 名 / ClassCategory 名)に対応。
AgentCatalogMediaDtoメディア(画像等)。type / 絶対 URL。
AgentCatalogDescriptionDto商品説明。plain / html / markdown を任意保持(html は描画側でサニタイズ前提)。
AgentCatalogBarcodeDtoバーコード(GTIN/JAN 等)。標準では常に空(EC-CUBE に GTIN 項目が無いため Customize seam)。
AvailabilityStatus在庫状況 enum。標準出力は IN_STOCK / OUT_OF_STOCK の 2 値のみ(他は仕様準拠で定義のみ)。
+
+ +
+

マッピングと供給

+ + + + + +
クラス役割
CatalogMapperProduct/ProductClass → AgentCatalogItemDto への写像。出力対象判定と availability 判定の中核(対応表は同クラス docblock)。
CatalogProvider(CatalogProviderInterface)公開中 Product を id 昇順で逐次供給。大規模カタログでメモリ枯渇しないよう EntityManager を一定間隔で clear。
ProductReferenceResolver(ProductReferenceResolverInterface)エージェントの参照子(sku=ProductClass.code / product_class_id / barcode)を ProductClass へ解決。
+
+ +
+

プロトコル別の出力開発者向け

+

Acp/ と Ucp/ に、中立 DTO を各プロトコルの本文へ写すシリアライザ等を置きます。

+ + + + + + + + + +
クラス役割
ACP AcpFeedGenerator全置換成果物(products.jsonl / metadata.json)をストリーム生成(全件をメモリに蓄積しない)。
ACP AcpFeedProductSerializerDTO → ACP Feed Product 連想配列(schema.feed.json の $defs/Product に適合。null/空は出さない)。
ACP AcpFeedValidatorvendored schema.feed.json に対し push 前検証(不正データ送出を防ぐ必須ゲート)。
ACP AcpFeedClient(AcpFeedClientInterface)OpenAI ホストの Feed API へ outbound push。Bearer は env bind、ログ/例外に出さない。
UCP UcpCatalogResponseBuildersearch / lookup / product の各レスポンス本文(ucp ラッパー付き)を組み立て。
UCP UcpCatalogProductSerializerDTO → UCP Catalog Product/Variant 連想配列(price_range は variants から算出)。
UCP UcpCatalogCacheUCP Catalog レスポンスの FS キャッシュ(POST RPC のためボディ hash をキー、LockFactory でスタンピード防止)。
+

Exception/ に ACP Feed 用の例外(AcpFeedException 基底 / AcpFeedTransportException(HTTP 4xx/5xx)/ AcpFeedValidationException(schema 不適合))を置きます。

+
ACP Feed は push モデル(Agent=OpenAI がホスト、加盟店がクライアント)で、outbound Bearer で認証する。inbound OAuth2(eccube-api4)とは無関係な点に注意。
+
+ +
+

注意点開発者向け

+
    +
  • 画像 URL は RequestContext 由来。商品画像はルーティング対象でない静的ファイルのため、CatalogMapper は scheme/host/port/baseUrl を RequestContext から組み立てて絶対 URL 化する。Feed 生成を CLI/cron で回すと Request が無いため、router.request_context.*(scheme/host/base_url)を設定しないと既定値のホストで URL が出る。リバースプロキシ配下では TRUSTED_PROXIES と X-Forwarded-* に依存する。
  • +
  • list_price は割引時のみ。list_price(pre-discount 参照価格)は price01 > price02(実際に割引がある場合)のみ出力する。店頭 detail.twig は price01 設定時に常に通常価格を表示するのとセマンティクスが異なるので、店頭表示と機械的に一致させないこと。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/Catalog/README.md b/src/Eccube/Service/AgentCommerce/Catalog/README.md new file mode 100644 index 00000000000..594dcbb6e9f --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Catalog/README.md @@ -0,0 +1,19 @@ +# AgentCommerce Catalog — カタログ提供 + +EC-CUBE の `Product` / `ProductClass` をプロトコル非依存の中立 DTO に写し、 +ACP Feed(push 型)と UCP Catalog(POST RPC)の両方へ出力するサブドメイン。 +親 `Product` = `AgentCatalogItemDto` / variant `ProductClass` = `AgentCatalogVariantDto`。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) +- 📦 同梱リソースの出所: [`Resource/AgentCommerce/README.md`](../../../Resource/AgentCommerce/README.md) + +## 主要ファイル + +- `CatalogMapper.php` — `Product`/`ProductClass` → 中立 DTO への写像(出力対象・availability 判定の中核) +- `CatalogProvider.php`(`CatalogProviderInterface.php`)— 公開中 `Product` を逐次供給(大規模カタログ対応) +- `ProductReferenceResolver.php`(`ProductReferenceResolverInterface.php`)— sku / product_class_id / barcode を `ProductClass` へ解決 +- `AgentCatalog*Dto.php` / `AvailabilityStatus.php` — プロトコル非依存の中立 DTO・enum +- `Acp/` — ACP Feed の生成(`AcpFeedGenerator`)/ シリアライズ / 検証(`AcpFeedValidator`)/ push(`AcpFeedClient`) +- `Ucp/` — UCP Catalog のレスポンス組み立て(`UcpCatalogResponseBuilder`)/ シリアライズ / FS キャッシュ +- `Exception/` — ACP Feed 用例外(`AcpFeedException` / `AcpFeedTransportException` / `AcpFeedValidationException`) diff --git a/src/Eccube/Service/AgentCommerce/CheckoutSession/README.html b/src/Eccube/Service/AgentCommerce/CheckoutSession/README.html new file mode 100644 index 00000000000..5184517ff4e --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/CheckoutSession/README.html @@ -0,0 +1,71 @@ + + + + + +AgentCommerce CheckoutSession — 仕様 + + + + +

AgentCommerce CheckoutSession — チェックアウト

+ + +
+

概要

+

エージェントチェックアウトの見積〜確定(complete)を、プロトコル非依存の中立表現で束ねるサブドメインです。 + エージェントはブラウザセッションを持たないため、通常購入のセッションに代わって本サブドメインの DTO がチェックアウトの状態を運びます。

+

実際の金額再計算・在庫引当は、ルート直下の AgentCheckoutPurchaseFlowAdapter が + 通常購入と同一の shopping flow を使って行います(本サブドメインはその入出力と確定の段取りを担当)。

+
+ +
+

中立 DTO / enum

+ + + + + + + + +
クラス役割
AgentCheckoutRequest見積要求。明細(AgentCheckoutLineItem[])と購入者/配送先住所(AgentCheckoutAddress)を保持。
AgentCheckoutLineItem明細 1 行。プロトコル固有 Mapper が variant 識別子を ProductClass.id へ解決して組み立てる。
AgentCheckoutAddress購入者/配送先住所。prefId は mtb_pref.id(1-47)。
AgentCheckoutResult見積/確定の結果。再計算済み Order とビジネス系 messages[]。見積時は Cart も返す。
AgentCheckoutCompletionResultcomplete 状態機械の結果。status(正規化)/ actionData(continue_url 等の原資)/ messages。
AgentCheckoutMessage / AgentCheckoutMessageLevelビジネス系メッセージ(HTTP 200 + messages[])とそのレベル。
+
+ +
+

オーケストレーションと会員解決開発者向け

+ + + + + +
クラス役割
AgentCheckoutCompletionServicecomplete を「中断 → 再開」する状態機械として実行するオーケストレータ。追加認証(EMV-3DS)や外部ハンドオフ(escalation)はエラーでなく正常な中間状態として扱う。在庫引当の保持/回収・正規化ステータス遷移・トランザクション境界を core に集約。
CustomerResolverInterfaceセッションに紐づく会員(Customer)を解決する seam。OAuth2 トークン → Customer 解決(eccube-api4 依存)。
GuestCustomerResolver標準実装。会員 ID 連携が実装されるまで常に null(ゲスト購入)を返す。
+
complete は冪等な単発呼び出しではなく複数回呼ばれる状態機械である。prepare で物理引当した在庫は REQUIRES_ACTION/PENDING では rollback せず保持し、FAILED/期限切れで rollback する。StockReduceProcessor の悲観ロックに対応するため各 complete を明示トランザクションで囲む。
+
    +
  • 状態別の再入: COMPLETED は短絡し副作用を再実行せず既存 Order を返す(冪等)。REQUIRES_ACTION/IN_PROGRESS からの再開 complete は在庫引当が初回で済んでいるため prepare を再実行しない。CANCELED/EXPIRED は確定不能でプロトコル系例外。
  • +
  • 決済ハンドラの有無で分岐: ハンドラ未登録(代引・無償等)は与信不要で直ちに commit。ハンドラありは authorize → COMPLETED の後にのみ capture し、capture 失敗は rollback して失敗扱い。
  • +
  • 失敗は retryable で分岐: card declined 等 retryable=true は READY へ戻して再 complete を許し、fraud 等 retryable=false は CANCELED。いずれも在庫は rollback する。prepare 段の在庫不足等ビジネス系エラーは status 据置で messages[] を返す。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/CheckoutSession/README.md b/src/Eccube/Service/AgentCommerce/CheckoutSession/README.md new file mode 100644 index 00000000000..d768d0038f0 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/CheckoutSession/README.md @@ -0,0 +1,17 @@ +# AgentCommerce CheckoutSession — チェックアウト + +エージェントチェックアウトの見積〜確定(complete)を、プロトコル非依存の中立表現で束ねるサブドメイン。 +金額再計算・在庫引当はルート直下の `AgentCheckoutPurchaseFlowAdapter` が通常購入と同一の shopping flow で行い、 +本サブドメインはその入出力と確定(中断→再開する状態機械)の段取りを担う。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) +- 📚 受注処理の詳細: [PurchaseFlow README](../../PurchaseFlow/README.md) + +## 主要ファイル + +- `AgentCheckoutCompletionService.php` — complete を「中断→再開」する状態機械として実行するオーケストレータ +- `AgentCheckoutRequest.php` / `AgentCheckoutLineItem.php` / `AgentCheckoutAddress.php` — 見積要求の中立 DTO +- `AgentCheckoutResult.php` / `AgentCheckoutCompletionResult.php` — 見積/確定・complete の中立結果 +- `AgentCheckoutMessage.php` / `AgentCheckoutMessageLevel.php` — ビジネス系メッセージ(HTTP 200 + messages[]) +- `CustomerResolverInterface.php`(`GuestCustomerResolver.php`)— 会員解決 seam(標準はゲスト固定) diff --git a/src/Eccube/Service/AgentCommerce/Discovery/README.html b/src/Eccube/Service/AgentCommerce/Discovery/README.html new file mode 100644 index 00000000000..c668529a6a3 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Discovery/README.html @@ -0,0 +1,62 @@ + + + + + +AgentCommerce Discovery — 仕様 + + + + +

AgentCommerce Discovery — 機能発見

+ + +
+

概要

+

エージェントが店舗の対応機能を発見するための UCP discovery profile(/.well-known/ucp)を組み立てるサブドメインです。 + profile には対応サービス・決済ハンドラ・署名鍵(JWK)が含まれ、これを起点にエージェントはカタログ取得やチェックアウトを行います。

+
{ "ucp": { version, services, payment_handlers, capabilities }, "signing_keys": [JWK...] }
+

services / capabilities / payment_handlers のキーは reverse-domain 形式で、値は単一のオブジェクトではなくエントリのリスト(UCP の ucp.json スキーマが要求。単一オブジェクトだとスキーマ検証するクライアントに拒否される)。endpoint(絶対 URL)はパスをハードコードせず UrlGenerator(RequestContext)から動的生成します。

+
+ +
+

主要クラス

+ + + + + +
クラス役割
UcpProfileBuilderUCP discovery profile ドキュメントを組み立てる。signing_keys[] は EC 公開鍵 JWK のみ(秘密鍵パラメータ非混入、UcpMessageSigner の戻りをそのまま使う)。
PaymentHandlerRegistryInterfaceprofile の payment_handlers(reverse-domain キーのレジストリ)を寄与する口。
EmptyPaymentHandlerRegistry既定実装。tagged service で寄与された各レジストリをマージ。寄与が無ければ空オブジェクト {} を返す。
+
+ +
+

注意点開発者向け

+
    +
  • core 本体は payment_handlers をカラム化せず、PaymentHandlerRegistryInterface の実装からのみ収集する。決済ハンドラプラグイン(将来の Google Pay 等)が eccube.agent_commerce.payment_handler_registry タグ付きサービスで寄与する。
  • +
  • UCP profile schema 上 payment_handlers は必須のため、寄与が無い場合でも空オブジェクト {} を出す(空配列 [] ではない)。
  • +
  • signing_keys は署名鍵が無い(signer が空を返す)とキー自体を出力しない(常に空 {} を出す payment_handlers とは非対称)。
  • +
  • services の endpoint は base パスをハードコードせず agent_ucp_catalog_search ルートの絶対 URL から末尾 /search を除去して導出する(個別 RPC パスは capability/schema 側で定義)。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/Discovery/README.md b/src/Eccube/Service/AgentCommerce/Discovery/README.md new file mode 100644 index 00000000000..eb16a5ccd1b --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Discovery/README.md @@ -0,0 +1,15 @@ +# AgentCommerce Discovery — 機能発見 + +エージェントが店舗の対応機能を発見するための UCP discovery profile(`/.well-known/ucp`)を組み立てるサブドメイン。 +profile には対応サービス・決済ハンドラ・署名鍵(JWK)が含まれる。 +core は `payment_handlers` をカラム化せず、tagged service で寄与された内容のみを収集する。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) +- 📚 プロトコル仕様: [UCP profile.json](https://github.com/Universal-Commerce-Protocol/ucp/blob/main/schemas/profile.json) + +## 主要ファイル + +- `UcpProfileBuilder.php` — UCP discovery profile ドキュメントの組み立て(endpoint は UrlGenerator で動的生成) +- `PaymentHandlerRegistryInterface.php` — `payment_handlers`(reverse-domain キー)を寄与する口 +- `EmptyPaymentHandlerRegistry.php` — 既定実装。寄与が無ければ空オブジェクト `{}` を返す diff --git a/src/Eccube/Service/AgentCommerce/Exception/README.html b/src/Eccube/Service/AgentCommerce/Exception/README.html new file mode 100644 index 00000000000..0cd59b823b2 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Exception/README.html @@ -0,0 +1,61 @@ + + + + + +AgentCommerce Exception — 仕様 + + + + +

AgentCommerce Exception — 例外とエラー分類

+ + +
+

概要

+

エージェントチェックアウトのプロトコル系エラー(不正要求・処理不能)を分類・表現するサブドメインです。 + ACP/UCP 共通の「2 系統エラー」のうち、ここで扱うのは HTTP 4xx/5xx + Error に対応する側です。 + 在庫切れ・販売停止等のビジネス系エラー(HTTP 200 + messages[])は例外ではなく + CheckoutSession の AgentCheckoutMessage で表現します。

+
+ +
+

主要クラス / enum

+ + + + + +
クラス役割
AgentCheckoutErrorCodeプロトコル系エラーのコード分類 enum(空明細・商品未検出・住所不正・数量不正等)。
AgentCheckoutExceptionプロトコル系エラーの例外。不正要求・処理不能を表す。
IdempotencyConflictExceptionIdempotency-Key の競合(HTTP 409 相当)。Idempotency サブドメインから投げられる。
+
+ +
+

注意点開発者向け

+
    +
  • HTTP ステータスや messages[] への具体的な変換アダプタはプロトコル層(#6776 / #6574)が担う。本サブドメインは中立なエラー分類・例外型のみを提供する。
  • +
  • ビジネス系(在庫切れ等)をここで例外にしない。混同すると本来 HTTP 200 で返すべき結果を 4xx/5xx で返してしまう。
  • +
  • IdempotencyConflictException は AgentCheckoutException のサブクラスではなく、直接 \RuntimeException を継承する別系統(errorCode を持たない)。catch (AgentCheckoutException) では捕捉できない点に注意(409 と 4xx/5xx を別ハンドラで扱う)。
  • +
  • AgentCheckoutException はメッセージ未指定時、例外メッセージに errorCode->value(例 product_not_found)をそのまま用いる。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/Exception/README.md b/src/Eccube/Service/AgentCommerce/Exception/README.md new file mode 100644 index 00000000000..feae8c5e0b7 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Exception/README.md @@ -0,0 +1,16 @@ +# AgentCommerce Exception — 例外とエラー分類 + +エージェントチェックアウトのプロトコル系エラー(不正要求・処理不能=HTTP 4xx/5xx + Error)を分類・表現するサブドメイン。 +在庫切れ・販売停止等のビジネス系エラー(HTTP 200 + messages[])は例外ではなく +[CheckoutSession](../CheckoutSession/README.md) の `AgentCheckoutMessage` で表現する(混同しない)。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) + +## 主要ファイル + +- `AgentCheckoutErrorCode.php` — プロトコル系エラーのコード分類 enum +- `AgentCheckoutException.php` — プロトコル系エラーの例外(不正要求・処理不能) +- `IdempotencyConflictException.php` — Idempotency-Key の競合(HTTP 409 相当) + +HTTP ステータスや messages[] への変換はプロトコル層(#6776 / #6574)が担う。 diff --git a/src/Eccube/Service/AgentCommerce/Fulfillment/README.html b/src/Eccube/Service/AgentCommerce/Fulfillment/README.html new file mode 100644 index 00000000000..a317f0ca627 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Fulfillment/README.html @@ -0,0 +1,62 @@ + + + + + +AgentCommerce Fulfillment — 仕様 + + + + +

AgentCommerce Fulfillment — 配送方法の提示

+ + +
+

概要

+

エージェントへ提示する配送方法の選択肢を、EC-CUBE の配送マスタから中立表現へ組み立てるサブドメインです。 + 各選択肢は配送方法(Delivery)に対応し、配送先(Pref)に対する送料・配送日数・利用可能な支払方法を保持します。 + 金額はすべて minor-unit(整数)で表現します。

+
+ +
+

主要クラス

+ + + + + + +
クラス役割
FulfillmentOption配送方法の選択肢(Delivery 1 件)。送料(shippingFeeMinor)・配送日数・支払方法選択肢を保持。
FulfillmentPaymentOption配送方法に紐づく支払方法の選択肢。代引手数料等を chargeMinor(minor-unit)で保持。
FulfillmentOptionMapperInterface明細と配送先から選択肢を組み立てる seam(app/Customize で差し替え可能)。
StandardFulfillmentOptionMapper標準実装。Delivery/DeliveryFee/Payment/DeliveryDuration マスタから選択肢を組み立てる。
+
+ +
+

標準実装の判定開発者向け

+
    +
  • 利用可能な Delivery は明細の SaleType から DeliveryRepository::getDeliveries() で絞る。
  • +
  • 送料は配送先 Pref と Delivery の DeliveryFee から解決(該当行が無ければ配送不可として除外)。
  • +
  • 手数料(代引等)は Delivery の PaymentOption → Payment::getCharge() から解決。ただし Payment::isVisible() が false の支払方法は選択肢から除外する。
  • +
  • 配送日数は明細横断の DeliveryDuration::getDuration() の最大値(負数=お取り寄せが 1 件でもあれば未確定として null。配送日数の設定が皆無の場合も null)。
  • +
  • 複数 SaleType をまたぐ注文(複数配送分割)の厳密な表現は標準実装の対象外で、app/Customize での差し替えに委ねる(フラットな一覧を返す)。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/Fulfillment/README.md b/src/Eccube/Service/AgentCommerce/Fulfillment/README.md new file mode 100644 index 00000000000..a25ed433277 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Fulfillment/README.md @@ -0,0 +1,14 @@ +# AgentCommerce Fulfillment — 配送方法の提示 + +エージェントへ提示する配送方法の選択肢を、EC-CUBE の配送マスタ(`Delivery`/`DeliveryFee`/`Payment`/`DeliveryDuration`) +から中立表現へ組み立てるサブドメイン。各選択肢は送料・配送日数・利用可能な支払方法を minor-unit(整数)で保持する。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) + +## 主要ファイル + +- `StandardFulfillmentOptionMapper.php` — 配送マスタから選択肢を組み立てる標準実装 +- `FulfillmentOptionMapperInterface.php` — 差し替え用 seam +- `FulfillmentOption.php` — 配送方法の選択肢(送料・配送日数・支払方法) +- `FulfillmentPaymentOption.php` — 配送方法に紐づく支払方法の選択肢(代引手数料等) diff --git a/src/Eccube/Service/AgentCommerce/Idempotency/README.html b/src/Eccube/Service/AgentCommerce/Idempotency/README.html new file mode 100644 index 00000000000..be03258867d --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Idempotency/README.html @@ -0,0 +1,63 @@ + + + + + +AgentCommerce Idempotency — 仕様 + + + + +

AgentCommerce Idempotency — 冪等性

+ + +
+

概要

+

状態変更操作(complete 等)の二重実行を防ぐためのサブドメインです。 + リクエストに Idempotency-Key ヘッダが付与された場合、初回はハンドラを実行して結果を保存し、 + 同一キーの再送はハンドラを再実行せず保存済みレスポンスをリプレイします(副作用の再実行なし)。

+
+ +
+

主要クラス

+ + + +
クラス役割
AgentCheckoutIdempotencyStoreIdempotency-Key 処理の本体。dtb_agent_checkout_idempotency に結果を保存し、再送時にリプレイする。
+
(idempotency_key, subject) の DB 一意制約で直列化するため、Redis 等の共有キャッシュや分散ロックに依存せず、マルチインスタンス(AWS 等)でも単一の共有 DB だけで並行・越境の二重実行を防ぐ。subject は認証済みエージェント識別子で名前空間化する。
+
+ +
+

競合の扱い開発者向け

+

次の場合は IdempotencyConflictException(Exception サブドメイン)を投げ、プロトコル層で HTTP 409 Conflict へ変換する。

+
    +
  • 同一キーが異なるリクエスト内容で再利用された場合
  • +
  • 同一キーの処理がまだ進行中(並行リクエスト)の場合
  • +
+
    +
  • Idempotency-Key が null / 空のリクエストは重複排除されず毎回 compute を実行する。冪等保証はキー付与が前提。
  • +
  • 初回は compute の前に予約行を INSERT し、compute が例外を投げた場合は予約行を削除して同一キーでの再試行を許す(失敗時にキーを確定・固定しない)。
  • +
  • 並行 INSERT が一意制約違反になると flush() 失敗で EntityManager が閉じるため、以降は生 DBAL でリプレイ元を直接読む(ORM を使わない)。相手がロールバック等で行が消えていれば「処理中」扱いにして再試行を促す。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/Idempotency/README.md b/src/Eccube/Service/AgentCommerce/Idempotency/README.md new file mode 100644 index 00000000000..e8ad24e33ee --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Idempotency/README.md @@ -0,0 +1,14 @@ +# AgentCommerce Idempotency — 冪等性 + +状態変更操作(complete 等)の二重実行を防ぐサブドメイン。`Idempotency-Key` ヘッダの初回はハンドラを実行して結果を保存し、 +同一キーの再送は保存済みレスポンスをリプレイする(副作用の再実行なし)。 +`(idempotency_key, subject)` の DB 一意制約で直列化し、マルチインスタンスでも共有 DB だけで越境の二重実行を防ぐ。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) + +## 主要ファイル + +- `AgentCheckoutIdempotencyStore.php` — Idempotency-Key 処理(`dtb_agent_checkout_idempotency` へ保存・再送でリプレイ) + +競合時(内容不一致の再利用・処理進行中)は `Exception/IdempotencyConflictException`(HTTP 409 相当)を投げる。 diff --git a/src/Eccube/Service/AgentCommerce/Payment/README.html b/src/Eccube/Service/AgentCommerce/Payment/README.html new file mode 100644 index 00000000000..e8814b48a19 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Payment/README.html @@ -0,0 +1,64 @@ + + + + + +AgentCommerce Payment — 仕様 + + + + +

AgentCommerce Payment — 決済ハンドラ

+ + +
+

概要

+

エージェント注文の決済ハンドラの型と、支払方法の割当・解決を担うサブドメインです。 + 通常購入では買い手が画面で支払方法を選びますが、エージェントは画面を持たず discovery で広告された決済ハンドラを + handler_id で指定します。core は具象決済実装を持たず、決済プラグインがタグ付きサービスとして + ハンドラを寄与します(型のみ core が保持)。

+
+ +
+

主要クラス / enum

+ + + + + + + +
クラス役割
AgentCheckoutPaymentHandlerInterface決済ハンドラの共通基底。プロトコル固有のトークン償還(ACP Shared Payment Token / UCP Payment Token Exchange)は派生 IF が定義。
AgentCheckoutPaymentHandlerRegistryagent_commerce.payment_handler タグのハンドラを集約し、Order の支払方法から適切なハンドラを解決。
AgentPaymentMethodResolverInterfaceエージェント注文に割り当てる Payment を解決する seam。
DefaultAgentPaymentMethodResolver標準実装。PaymentRepository::findAllowedPayments() を、登録済みハンドラが扱える Payment へ絞り込む。
PaymentOutcome / PaymentOutcomeStatus決済ハンドラ(authorize/capture)の処理結果(Symfony Response 非依存の中立 DTO)とステータス enum。
+
+ +
+

注意点開発者向け

+
    +
  • sort_no の機械採用は禁止。利用可能な支払方法の先頭(sort_no 最小)を採ると、エージェント決済不可能な方法(銀行振込等)が割り当たり、ハンドラ未通過のまま検知されず確定してしまう。リゾルバは登録済みハンドラが扱える Payment のみを候補にし、sort_no は候補内のタイブレークとしてのみ使う。
  • +
  • DefaultAgentPaymentMethodResolver は methodClass 等のプロトコル固有判定をハンドラ側(supports())に委ね、特定の決済クラスに依存しない(将来 PSP が新 methodClass を出しても無改変で動く)。
  • +
  • PaymentOutcomeStatus は追加認証(EMV-3DS)や外部ハンドオフ(escalation)をエラーでなく正常な中間状態として表す。これを受けて CheckoutSession/AgentCheckoutCompletionService がステータス遷移・在庫引当の保持/回収を決める。
  • +
  • handler_id はプラグイン間で一意必須。同一 handler_id を複数のハンドラが宣言すると resolveByHandlerId() は null でなく RuntimeException を投げ、非決定的なハンドラ選択を fail-fast で防ぐ。
  • +
  • 候補 Payment は Shipping でなく販売種別(SaleType)起点で引く。エージェント注文は確定前に Shipping へ配送業者が割り当たらないことがあるため、DefaultAgentPaymentMethodResolver は SaleType → 配送業者 → 利用可能 Payment の順で解決する。既に割当済みの Payment がハンドラ適合ならそれを尊重し(create→complete の引き継ぎ・別リクエストでの再読込に対応)、不適合なら元の割当へ戻して注文の状態を変えない。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/Payment/README.md b/src/Eccube/Service/AgentCommerce/Payment/README.md new file mode 100644 index 00000000000..367f39ea4d9 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Payment/README.md @@ -0,0 +1,15 @@ +# AgentCommerce Payment — 決済ハンドラ + +エージェント注文の決済ハンドラの型と、支払方法の割当・解決を担うサブドメイン。 +エージェントは画面を持たず discovery で広告された決済ハンドラを `handler_id` で指定する。 +core は具象決済実装を持たず、決済プラグインがタグ付きサービスでハンドラを寄与する(型のみ core が保持)。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) + +## 主要ファイル + +- `AgentCheckoutPaymentHandlerInterface.php` — 決済ハンドラの共通基底(プロトコル固有 IF が派生) +- `AgentCheckoutPaymentHandlerRegistry.php` — タグ付きハンドラの集約と Order からの解決 +- `AgentPaymentMethodResolverInterface.php`(`DefaultAgentPaymentMethodResolver.php`)— `Payment` 割当リゾルバ(sort_no 機械採用を避ける) +- `PaymentOutcome.php` / `PaymentOutcomeStatus.php` — 決済結果の中立 DTO とステータス(追加認証/escalation を中間状態として表す) diff --git a/src/Eccube/Service/AgentCommerce/README.html b/src/Eccube/Service/AgentCommerce/README.html new file mode 100644 index 00000000000..821d2e1e0fb --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/README.html @@ -0,0 +1,112 @@ + + + + + +AgentCommerce — 仕様 + + + + +

AgentCommerce — エージェントコマース基盤

+ + +
+

概要

+

AgentCommerce は、AI エージェント(OpenAI 等)が EC-CUBE の商品を発見し購入できるようにするための + エージェントコマース対応の基盤サービス群です。ブラウザセッションを持たないエージェントからの + カタログ提供・チェックアウト・決済・署名検証を、コントローラや個別サービスに散らさずこのディレクトリに集約します。

+ +

対応プロトコルは 2 系統あり、プロトコル非依存の中立表現(DTO・中間結果)を核に置いて、 + 各プロトコルの入出力へ写す設計です。プロトコル固有のマッパー・署名は本ディレクトリの Acp/ / Ucp/、 + HTTP の入口は src/Eccube/Controller/AgentCommerce/ が担い、それ以外のサブドメインは共通基盤に徹します。

+ + + + + +
プロトコル意味
ACPAgentic Commerce Protocol(OpenAI ホスト)。カタログは push 型の Feed API。
UCPUniversal Commerce Protocol。カタログは POST RPC、discovery は /.well-known/ucp。
+ +
設計の核心(#6777): 新規の購入フローは作らない。 + エージェントチェックアウトも通常購入と同一の shopping flow(PurchaseFlow)を再利用し、 + 税計算・送料・手数料・ポイント・在庫引当・受注番号採番のロジックを完全共有します。 + 詳細は PurchaseFlow の README を参照。
+
+ +
+

用語

+ + + + + + + +
用語意味
中立表現(プロトコル非依存 DTO)ACP / UCP どちらにも写せる共通の入出力表現。AgentCatalog*Dto / AgentCheckout* 等。プロトコル固有の変換は各プロトコル層が行う。
addressable unitカタログで一意に指せる購入単位。EC-CUBE では親=Product、variant=ProductClass に対応。
minor-unit(最小通貨単位)ACP / UCP の金額表現。通貨の小数桁数(ISO 4217)だけ整数化した値。JPY は ×1、USD は ×100 と通貨ごとに桁数が変わる。MinorUnitConverter が変換。
2 系統エラープロトコル系(HTTP 4xx/5xx + Error=不正要求・処理不能)とビジネス系(HTTP 200 + messages[]=在庫切れ・販売停止等)を明確に分ける。
discoveryエージェントが店舗の対応機能・決済ハンドラ・署名鍵を発見する仕組み(UCP の /.well-known/ucp profile 等)。
+
+ +
+

サブドメイン索引

+

本ディレクトリはプロトコル非依存の 8 つのサブドメインと、プロトコル別の Acp/ / Ucp/ で構成されます。各サブドメインの詳細はそれぞれの README を参照してください。

+ + + + + + + + + + + +
サブドメイン担うこと
Catalog商品カタログの中立 DTO 化と ACP Feed / UCP Catalog への出力・検証。
CheckoutSession見積〜確定(complete)の中立表現と、中断→再開する状態機械オーケストレーション。
DiscoveryUCP discovery profile の組み立てと決済ハンドラの収集口。
Fulfillment配送方法・送料・支払方法の選択肢を EC-CUBE マスタから中立表現へ。
IdempotencyIdempotency-Key による状態変更操作の二重実行防止(DB 一意制約ベース)。
Paymentエージェント決済ハンドラのレジストリと、Order への支払方法割当リゾルバ。
Securityインバウンド OAuth2 検証・scope 照合・メッセージ署名(RFC 9421)・鍵ストア。
Exceptionプロトコル系エラーのコード分類と例外(ビジネス系は CheckoutSession の message で表現)。
Acp/ / Ucp/中立表現と各プロトコルの入出力の相互変換(マッパー)、メッセージ署名、UCP のリクエスト署名検証(Ucp/Signature/)。
+
+ +
+

ルート直下の共通ユーティリティ開発者向け

+

サブドメインをまたいで使われる、プロトコル非依存の共通部品です。

+ + + + + + +
クラス役割
AgentCheckoutPurchaseFlowAdapter中立 DTO → Cart/Order を構築し、通常購入と同一の shopping flow で税・送料・在庫を再計算。buildOrder(見積)/ prepare(在庫引当・採番)/ commit / rollback を提供。自身はトランザクションを開始しない(境界は決済オーケストレーション層の責務)。
AddressMappingService住所系エンティティ(Customer / CustomerAddress / Shipping)を ACP/UCP 住所 DTO へ写す。国コードは mtb_country_iso_code で numeric→alpha-2 変換、region は Pref 名。
MinorUnitConvertermajor-unit(表記)と minor-unit(整数)を相互変換。桁数は symfony/intl の ISO 4217 権威データから取得(一律 ×100 ではない)。負数(割引・返金)対応。金額計算は bcmath。
StorefrontUrlResolverdiscovery / checkout レスポンスに必要な店舗 URL(プライバシーポリシー・利用規約・注文 permalink)を UrlGenerator から絶対 URL で実行時生成。BaseInfo にカラム化しない。
+
+ +
+

注意点開発者向け

+
    +
  • 2 系統エラーを混同しない。在庫不足・販売停止・配送制限などのビジネス系は例外を投げず AgentCheckoutMessage(HTTP 200 + messages[])に写す。不正な商品参照・空明細などのプロトコル系のみ AgentCheckoutException を投げる。
  • +
  • 金額は minor-unit 整数で持ち回る。通貨ごとに桁数が違うため一律 ×100 で組まない(必ず MinorUnitConverter を通す)。EC-CUBE 本体の金額カラムは 2 桁までである点に注意。
  • +
  • 会員は既定でゲスト購入。会員 ID 連携は eccube-api4 の Customer authorization_code grant に依存し、標準実装(GuestCustomerResolver)は常に null(ゲスト)を返す。会員解決は CustomerResolverInterface の差し替えで有効化する。
  • +
  • ハンドラ非対応の支払方法が検知されず割り当たる問題に注意。利用可能な支払方法の先頭(sort_no 最小)を機械採用するとエージェント決済不可能な方法(銀行振込等)が割り当たる。AgentPaymentMethodResolverInterface は登録済みハンドラが扱える Payment のみを候補にする。
  • +
  • 拡張は各 Interface の差し替え(seam)で。ProductReferenceResolverInterface / CustomerResolverInterface / FulfillmentOptionMapperInterface / KeyStoreInterface 等を app/Customize で実装・alias 上書きする。core は具象決済実装を持たず、決済ハンドラは決済プラグインがタグ付きサービスで寄与する。
  • +
  • 本ディレクトリはプロトコル非依存に徹する。HTTP ステータス/各プロトコルのレスポンス形への変換はプロトコル層(Acp/ / Ucp/ とコントローラ)が担う。共通サブドメインに HTTP 依存を持ち込まない。
  • +
  • エージェント所有 Cart は Web カートと隔離。AgentCheckoutPurchaseFlowAdapter が組む Cart は setAgentOwned(true) を立て、Web ストアフロントの CartService の解決対象から外す。会員帰属は Order 側に持たせ Cart の customer_id は常に NULL に保つ(ログイン会員の Web カートへの混入・操作を防ぐため)。
  • +
  • MinorUnitConverter は不正入力を例外にせず既定値で処理する。未知の通貨コードは安全側に倒して 2 桁(×100)扱い、数値として解釈できない金額文字列(abc / 1,000 / 1e3 等)は例外でなく 0 を返す。呼び出し側で通貨・金額の妥当性を保証すること。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/README.md b/src/Eccube/Service/AgentCommerce/README.md new file mode 100644 index 00000000000..8ded122b560 --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/README.md @@ -0,0 +1,29 @@ +# AgentCommerce — エージェントコマース基盤 + +AI エージェント(OpenAI 等)が EC-CUBE の商品を発見し購入できるようにするための基盤サービス群。 +ACP(Agentic Commerce Protocol)/ UCP(Universal Commerce Protocol)の 2 系統に対し、 +プロトコル非依存の中立表現を核に置いて共通処理を提供する。 +チェックアウトは新規フローを作らず**通常購入と同一の shopping flow**([PurchaseFlow](../PurchaseFlow/README.md))を再利用する(#6777)。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 📦 同梱リソースの出所: [`Resource/AgentCommerce/README.md`](../../Resource/AgentCommerce/README.md) +- 📚 プロトコル仕様: [ACP](https://github.com/agentic-commerce-protocol/agentic-commerce-protocol) / [UCP](https://github.com/Universal-Commerce-Protocol/ucp) + +## サブドメイン + +- [Catalog](./Catalog/README.html) — カタログの中立 DTO 化と ACP Feed / UCP Catalog 出力・検証 +- [CheckoutSession](./CheckoutSession/README.html) — 見積〜確定(complete)の中立表現と状態機械 +- [Discovery](./Discovery/README.html) — UCP discovery profile 組み立てと決済ハンドラ収集口 +- [Fulfillment](./Fulfillment/README.html) — 配送方法・送料・支払方法の選択肢を中立表現へ +- [Idempotency](./Idempotency/README.html) — Idempotency-Key による二重実行防止(DB 一意制約) +- [Payment](./Payment/README.html) — 決済ハンドラレジストリと支払方法割当リゾルバ +- [Security](./Security/README.html) — OAuth2 検証・scope 照合・メッセージ署名・鍵ストア +- [Exception](./Exception/README.html) — プロトコル系エラーのコード分類と例外 +- `Acp/` / `Ucp/` — プロトコル別のマッパー・メッセージ署名・リクエスト署名検証(HTTP の入口は `Controller/AgentCommerce/`) + +## ルート直下の共通ユーティリティ + +- `AgentCheckoutPurchaseFlowAdapter.php` — 中立 DTO → `Cart`/`Order` を構築し shopping flow で再計算(`buildOrder`/`prepare`/`commit`/`rollback`) +- `AddressMappingService.php` — 住所系エンティティを ACP/UCP 住所 DTO へ写す(国コード numeric→alpha-2、region=`Pref` 名) +- `MinorUnitConverter.php` — major-unit ⇄ minor-unit 変換(ISO 4217 権威データ・bcmath・負数対応) +- `StorefrontUrlResolver.php` — 店舗 URL(規約・プライバシー・注文 permalink)を絶対 URL で実行時生成 diff --git a/src/Eccube/Service/AgentCommerce/Security/README.html b/src/Eccube/Service/AgentCommerce/Security/README.html new file mode 100644 index 00000000000..57d1247f36a --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Security/README.html @@ -0,0 +1,75 @@ + + + + + +AgentCommerce Security — 仕様 + + + + +

AgentCommerce Security — 認証と署名

+ + +
+

概要

+

エージェントコマースのインバウンド認証(OAuth2)・scope 照合・メッセージ署名(RFC 9421)・署名鍵の管理を担うサブドメインです。 + エージェントはブラウザセッション(form_login)を持たないため、認証は OAuth2 アクセストークン、 + 応答の真正性は HTTP Message Signatures で担保します。公開鍵は discovery の signing_keys[](JWK)として広告されます。

+
+ +
+

認証・scope

+ + + + +
クラス役割
AgentCommerceOAuth2Authenticatorインバウンド OAuth2 トークン検証 + scope×protocol 照合。トークン検証は Symfony 標準の AccessTokenHandlerInterface 経由(実体は eccube-api4 が提供)。
AgentCommerceScopeRegistryscope を "<protocol>:<capability>" 形式(例 "ucp:checkout")に正準化。protocol 越境を許可しない。
+
+ +
+

メッセージ署名・鍵ストア

+ + + + + + + + + +
クラス役割
AgentCommerceMessageSignerInterfaceメッセージ署名・検証の抽象。アルゴリズムはプロトコルごとに差し替え可能。
UcpMessageSignerUCP の RFC 9421 実装。EC P-256 / ES256、署名は raw R‖S(IEEE P1363, 64byte)。公開鍵を EC JWK として広告。
KeyStoreInterface署名鍵の永続化抽象(app/Customize で DB 保管・Vault 連携等に差し替え可能)。
FilesystemKeyStore標準実装。既定パス {projectDir}/app/keystore/agent-commerce/{purpose}.key。既定はディレクトリ 0755 / ファイル 0644(CLI が配置した鍵を Web サーバーが読めるようにするため)。ECCUBE_KEYSTORE_STRICT_PERMISSIONS=1 で 0700 / 0600。
KeyPurposeInterface / KeyPurposeRegistry鍵の用途(purpose)ごとの生成方法の定義と、その一覧(UcpSigningKeyPurpose / AcpWebhookKeyPurpose)。CLI の eccube:keystore:generate と実行時の自動生成が同じ実装を通る。実装を追加すると自動でタグ付けされ eccube:keystore:* の対象になる。
EcJwkFactoryEC 公開鍵を JWK と kid(RFC 7638 JWK Thumbprint)へ変換する。discovery と eccube:keystore:show が同じ kid を返すよう 1 箇所に置く。
KeyStoreInspectoreccube:keystore:* の出力源。鍵の有無・保管先・権限と、Web サーバーから読めるかを判定する。
+
+ +
+

注意点開発者向け

+
    +
  • OAuth2 の実体(リソースサーバー)は eccube-api4 が提供する想定([EC-CUBE/eccube-api4#188])。eccube-api4 未導入時は handler が null となり 503(ServiceUnavailable)を返す機能フラグのようなフォールバック。firewall への配線は checkout エンドポイントを持つ #6776/#6574 で行う。
  • +
  • 付与 scope は UserBadge の attributes に handler が載せる前提(scopes: array もしくは scope: 空白区切り string)。
  • +
  • signing_keys[] には EC 公開鍵 JWK のみを出す(秘密鍵パラメータ非混入)。秘密鍵が鍵ストアに無ければ生成して永続化するが、これは CLI を使えない環境向けのフォールバック。Web サーバーと CLI の権限を分けた構成では app/keystore へ Web サーバーが書けないため、bin/console eccube:keystore:generate で事前に配置する。kid は RFC 7638 の JWK Thumbprint。
  • +
  • UcpMessageSigner は grace period の旧公開鍵(gracePublicKeyPems)を verify と signing_keys[] 広告の両方に含めることで無停止の鍵ローテーションを可能にする。verify は現用+旧鍵を順に試し、署名長不正等の例外は捕捉して破棄し、次の鍵を試す。
  • +
  • 厳格モード(ECCUBE_KEYSTORE_STRICT_PERMISSIONS=1)では、FilesystemKeyStore::write() が書き込みの間だけ umask(0077) に切り替える。file_put_contents の作成直後~chmod(0600) までの間に秘密鍵が group/other から読める競合ウィンドウを塞ぐためで、単なる 0600 設定ではない(既定モードは 0644 なので切り替えない)。
  • +
  • purpose は既定パスへ直接連結されるため [a-z0-9_-] のみに制限し、パストラバーサル(../ 等)を防ぐ(違反時 InvalidArgumentException)。
  • +
+
+ + + diff --git a/src/Eccube/Service/AgentCommerce/Security/README.md b/src/Eccube/Service/AgentCommerce/Security/README.md new file mode 100644 index 00000000000..38a3bd9371d --- /dev/null +++ b/src/Eccube/Service/AgentCommerce/Security/README.md @@ -0,0 +1,16 @@ +# AgentCommerce Security — 認証と署名 + +エージェントコマースのインバウンド認証(OAuth2)・scope 照合・メッセージ署名(RFC 9421)・署名鍵の管理を担うサブドメイン。 +エージェントはブラウザセッションを持たないため、認証は OAuth2 アクセストークン、応答の真正性は HTTP Message Signatures で担保する。 +公開鍵は discovery の `signing_keys[]`(JWK)として広告される。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- ⬆ 親: [AgentCommerce README](../README.md) + +## 主要ファイル + +- `AgentCommerceOAuth2Authenticator.php` — インバウンド OAuth2 検証 + scope×protocol 照合(トークン検証は eccube-api4 依存、未導入時は 503) +- `AgentCommerceScopeRegistry.php` — scope を `:` 形式へ正準化(protocol 越境を禁止) +- `AgentCommerceMessageSignerInterface.php`(`UcpMessageSigner.php`)— メッセージ署名(RFC 9421 / EC P-256 / ES256) +- `KeyStoreInterface.php`(`FilesystemKeyStore.php`)— 署名鍵の永続化(標準は FS、既定 0644/0755・厳格モードで 0600/0700) +- `KeyPurposeInterface.php`(`KeyPurposeRegistry.php`)— 鍵の用途ごとの生成方法。`eccube:keystore:generate` と実行時の自動生成が共用 diff --git a/src/Eccube/Service/PurchaseFlow/README.html b/src/Eccube/Service/PurchaseFlow/README.html new file mode 100644 index 00000000000..bbe09a5e0c9 --- /dev/null +++ b/src/Eccube/Service/PurchaseFlow/README.html @@ -0,0 +1,124 @@ + + + + + +PurchaseFlow — 受注処理パイプライン 仕様 + + + + +

PurchaseFlow — 受注処理パイプライン

+ + +
+

概要

+

PurchaseFlow は、カート・注文確認・受注確定・受注編集の各場面で使われる + 受注処理の中核パイプラインです。送料・手数料・税・値引き・ポイント・在庫引当・注文番号採番といった + 「受注に関わる計算・検証・確定」を、コントローラや個別サービスに散らさず、この 1 本のパイプラインに集約します。

+ +

パイプラインは cart / shopping / order の 3 フローぶん存在し、それぞれ別々の + 処理部品(Processor / Validator)の組を持ちます。どのフローで実行中かは PurchaseContext が保持します。

+ + + + + + +
フロー使われる場面
cartカート投入・数量変更時の検証と金額再計算
shopping注文確認画面での検証・送料/手数料/税/ポイントの確定計算
order管理画面の受注編集など、確定済み受注に対する再計算・差分反映
+
+ +
+

用語

+

受注ドメインの詳細(ER 図・受注ステータスの遷移・税計算の仕様)は複製せず + doc4「受注」仕様 を参照してください。ここでは本パイプラインの理解に必要な語だけをまとめます。

+ + + + + + +
用語意味
明細(Item)OrderItem / CartItem。商品だけでなく送料・手数料・値引き・ポイント・税も「明細の 1 行」として表現される。
受注/カート全体(ItemHolder)Order / Cart。明細を束ねる入れ物。Order 固有の概念(出荷・ポイント)は Cart には無い。
出荷(Shipping)1 受注に複数あり得る出荷単位。送料は Shipping 単位で計算される。
販売種別(SaleType)注文を決済単位に分割する区分(通常購入/定期購入など)。詳細は doc4。
+
+ +
+

処理フロー

+

検証・計算は validate() が、受注の確定は prepare() → commit()(失敗時 rollback())が担います。 + この 2 系統は独立して呼び出されます。

+ +

検証・計算:validate() の実行順

+

各段階の合間で金額が再集計(calculateAll():送料 → 手数料 → 値引き → 小計 → 税 → 合計。計算は bcmath)されます。

+
① 明細検証 ItemValidator … 明細1行ごとの検証(在庫・価格変更 等) + ↓ calculateAll +② 受注検証 ItemHolderValidator … カート/受注全体の検証(空明細・在庫合算 等) + ↓ calculateAll +③ 明細前処理 ItemPreprocessor … 明細1行ごとの調整(コアでは未使用の拡張点) + ↓ calculateAll +④ 受注前処理 ItemHolderPreprocessor … 送料/手数料/税 明細の付与・調整 + ↓ calculateAll +⑤ 値引き(削除) DiscountProcessor … 既存の値引き明細をいったん全消し + ↓ calculateAll +⑥ 値引き(追加) DiscountProcessor … 値引き明細を積み直す(合計を超えない範囲で) + ↓ calculateAll +⑦ 最終検証 ItemHolderPostValidator … 全処理後の最終検証・確定値の確定(ポイント付与 等)
+
値引きは「⑤ いったん全消し → ⑥ 再計算で積み直す」のが大前提です。前処理も同様に、 + 自分が付けた明細を毎回削除してから付け直すことで何度実行しても結果が変わらない(冪等)ように作られています。
+ +

確定:prepare() / commit() / rollback()

+

確定系は PurchaseProcessor のみが担います。在庫引当や注文番号採番など、実際に世界を変える処理です。

+
prepare() 仮確定(在庫引当 等)… 失敗時は例外を投げてフロー中断 +commit() 確定 +rollback() 仮確定の取消(在庫戻し 等)… prepare の逆操作
+
+ +
+

拡張ポイント開発者向け

+

受注処理へ独自の検証・計算を差し込むには、対応する部品を拡張して対象フローへ登録します。 + 実装の書き方・DO/DON'T は eccube-purchase-flow/SKILL.md が正典です。ここでは全体像だけ示します。

+ + + + + + + +
やりたいこと拡張する部品(基底)
明細1行の検証ItemValidator(abstract。常に warning 化)
カート/受注全体の検証(購入を止めたい)ItemHolderValidator / ItemHolderPostValidator
送料・手数料・税など明細の付与/調整ItemHolderPreprocessor
値引きDiscountProcessor
在庫引当・採番・ポイント付与など確定処理PurchaseProcessor(AbstractPurchaseProcessor 継承)
+

登録方法は 2 通り(併用しない):

+
    +
  • コア app/config/eccube/packages/purchaseflow.yaml でタグ登録。flow_type で対象フロー、priority(降順=大きいほど先)で実行順を指定。
  • +
  • プラグイン / Customize #[CartFlow] / #[ShoppingFlow] / #[OrderFlow] 属性で対象フローを宣言(src/Eccube/Attribute/)。基底の継承で自動タグ付けされる。順序制御が必要なら YAML タグを使う。
  • +
+
+ +
+

注意点開発者向け

+
    +
  • 金額計算は bcmath(bcadd/bcsub/bcmul/bccomp)。float で組まない。合計の集計は calculateAll() が行うので、部品側は明細の足し引きに集中する。
  • +
  • ItemValidator は常に warning に変換される(購入は止まらない)。購入を中断したい検証は ItemHolderValidator / ItemHolderPostValidator で warning なしの error にする。
  • +
  • Cart と Order の違いに注意。Shipping・ポイント・Customer は Order 固有。instanceof Order でガードする。
  • +
  • PurchaseProcessor の rollback() 実装漏れに注意(prepare() の逆操作を必ず用意する)。
  • +
  • パイプラインに実際にどの部品がどの順で乗っているかは PurchaseFlow::dump()(__toString())で確認できる(確定系の PurchaseProcessor は dump に出ない点に注意)。
  • +
+
+ + + diff --git a/src/Eccube/Service/PurchaseFlow/README.md b/src/Eccube/Service/PurchaseFlow/README.md new file mode 100644 index 00000000000..c7b68316bcc --- /dev/null +++ b/src/Eccube/Service/PurchaseFlow/README.md @@ -0,0 +1,18 @@ +# PurchaseFlow — 受注処理パイプライン + +受注に関わる計算・検証・確定(送料/手数料/税/値引き/ポイント/在庫引当・採番)を、 +コントローラや汎用サービスに散らさず 1 本のパイプラインに集約する EC-CUBE 受注処理の中核。 +cart / shopping / order の 3 フローぶん存在する。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-purchase-flow/SKILL.md`](../../../../.claude/skills/eccube-purchase-flow/SKILL.md) +- 📚 ドメイン詳細: https://doc4.ec-cube.net/spec_order + +## 主要ファイル + +- `PurchaseFlow.php` — パイプライン本体。7 種の拡張点を保持し `validate()` / `prepare()` / `commit()` / `rollback()` を実行 +- `PurchaseContext.php` — 実行中コンテキスト(フロー種別・実行前の状態を保持) +- `ProcessResult.php` / `PurchaseFlowResult.php` — 各処理の結果(success/warn/error)とその集約 +- `ItemValidator.php` / `ItemHolderValidator.php` / `ItemHolderPostValidator.php` — 検証系の基底(abstract) +- `ItemPreprocessor.php` / `ItemHolderPreprocessor.php` / `DiscountProcessor.php` / `PurchaseProcessor.php` — 前処理・値引き・確定の拡張点(interface) +- `Processor/` — 標準実装(`StockValidator` `TaxProcessor` `PointProcessor` `StockReduceProcessor` 等) diff --git a/src/Eccube/Service/README.html b/src/Eccube/Service/README.html new file mode 100644 index 00000000000..117ec603067 --- /dev/null +++ b/src/Eccube/Service/README.html @@ -0,0 +1,104 @@ + + + + + +Service — 仕様 + + + + +

Service — 業務ロジックの置き場所

+ + +
+

概要

+

src/Eccube/Service/ は、EC-CUBE の業務ロジックの受け皿です。 + 税の算出、カートの操作、受注ステータスの遷移、プラグインの導入、メール送信・CSV 入出力・PDF 出力といった + 「アプリの仕事そのもの」を、コントローラ(画面・HTTP の担当)から切り離してここに集約します。

+ +

Service に集約する狙いは 2 つ。 + 複数の画面・コマンドから同じ処理を再利用できること、そして + 1 つの Service が 1 つの責務だけを持つ(単一責任)ことで見通しとテスト容易性を保つことです。 + コントローラは「入出力の交通整理」に徹し、実処理は Service に委ねる、という分担になっています。

+ +
受注に関わる計算・検証・確定は別枠です。 + 送料・手数料・税・値引き・ポイント・在庫引当・注文番号採番は、汎用 Service ではなく + PurchaseFlow パイプライン(同ディレクトリ配下・別途 README あり)が担います。 + この境界が EC-CUBE の受注処理の核心です。
+
+ +
+

サービスの役割と代表例

+

このディレクトリ直下にある主な Service は次の通りです(実装は各ファイル)。

+ + + + + + + + + + +
サービス役割
CartServiceカートの取得・商品の追加/削除・保存。セッション上のカートと永続化カートの統合も担う。
TaxRuleService税額の算出(getTax() / calcTax())。税込価格の計算と丸め処理。
OrderHelperカートから購入処理中(PROCESSING)の受注を組み立てる入口。会員情報の反映・仮注文 ID の発番など。
OrderStateMachine受注ステータスの遷移(apply() / can())。遷移に伴うポイント・在庫の確定/巻き戻しをイベントで実行。
PluginServiceプラグインの install/enable/disable/uninstall。アーカイブ展開・プロキシ生成・スキーマ更新まで。
OrderPdfService受注情報の PDF(納品書等)出力。
TwoFactorAuthService管理者の 2 要素認証(シークレット生成・コード検証・認証済み Cookie 発行)。
SystemServiceシステム情報の取得とメンテナンスモードの切り替え。
+

このほか、計算補助(Calculator/)・決済(Payment/)・Agentic Commerce(AgentCommerce/)など、 + 用途別のサブディレクトリを持ちます。

+
+ +
+

関連する仕様(別ディレクトリ・別 Skill)

+

Service 配下でも、次の領域はそれぞれ専用の実装規約(Skill)が正典です。独立配置はせず本 README から案内します。

+ + + + + +
領域対象クラス参照先(実装規約)
受注処理パイプラインPurchaseFlow/PurchaseFlow の README
CSV 入出力CsvImportService / CsvExportServiceeccube-csv/SKILL.md
メール送信MailServiceeccube-mail/SKILL.md
+
+ +
+

拡張ポイント開発者向け

+

プロジェクト固有の業務ロジックはコアを直接改変せず、app/Customize/Service/ に追加します。 + コアアップグレードの影響を避けるためです。既存 Service の振る舞いを差し替えたいときは 2 通り。

+ + + + +
やりたいこと手段
新しい業務ロジックを足すapp/Customize/Service/ に新規 Service を置き、コンストラクタ DI で利用する。
既存 Service の一部を差し替えるサービスデコレーション(#[AsDecorator])またはコンパイラパスで置換する。
+

実装の書き方・DO/DON'T は eccube-service/SKILL.md が正典です(app/Customize 全般は customize Skill)。

+
+ +
+

注意点開発者向け

+
    +
  • 依存は一方向(Controller → Service → Repository)。Service が Eccube\Controller\... を use / 型ヒントするのはレイヤ違反。
  • +
  • HTTP の知識を持ち込まない。Request / Response を引数で受けず、必要な値(プリミティブやエンティティ)だけを渡す。
  • +
  • 単一責任を保つ。1 つの Service に無関係な責務(例: 商品検索とメール送信)を同居させない。責務が増えたら分割する。
  • +
  • 受注の計算・検証・確定は Service に直書きしない。PurchaseFlow の Processor / Validator を拡張する。
  • +
  • 永続化(persist() / flush())は Service に置いてよい。ただし flush() をループ内で乱発せず、トランザクション境界を意識してまとめる。
  • +
  • CartService の「カート」は単一ではない。商品は販売種別(SaleType)ごとに別々の Cart へ振り分けられ(SaleTypeCartAllocator)、getCarts() は Cart[] を返す。getCart() は先頭(primary)の 1 つだけを返すので、複数販売種別が混在する場面で「カート=1 つ」と思い込まない(先頭は setPrimary() で入れ替え可能)。
  • +
  • ログイン時に非会員カートを会員カートへマージする(CartService::mergeFromPersistedCart())。マージで販売種別が混在してカートが分割されると SecurityListener が SESSION_CART_DIVIDE_FLAG を立て、OrderHelper::verifyCart() が false を返して購入手続きを止める。
  • +
+
+ + + diff --git a/src/Eccube/Service/README.md b/src/Eccube/Service/README.md new file mode 100644 index 00000000000..f3950eac47a --- /dev/null +++ b/src/Eccube/Service/README.md @@ -0,0 +1,18 @@ +# Service — 業務ロジックの置き場所 + +コントローラ(HTTP・画面)から切り離した業務ロジックの受け皿。税の算出・カート操作・受注ステータス遷移・ +プラグイン導入・メール送信・CSV 入出力・PDF 出力などを、単一責任・HTTP 非依存で集約する。 +受注に関わる計算・検証・確定は汎用 Service ではなく `PurchaseFlow/` が担う。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-service/SKILL.md`](../../../.claude/skills/eccube-service/SKILL.md) +- 📚 関連: 受注処理は [`PurchaseFlow/`](./PurchaseFlow/README.html) / CSV は [`eccube-csv/SKILL.md`](../../../.claude/skills/eccube-csv/SKILL.md) / メールは [`eccube-mail/SKILL.md`](../../../.claude/skills/eccube-mail/SKILL.md) + +## 主要ファイル + +- `CartService.php` — カートの取得・商品追加/削除・保存(セッション/永続化カートの統合) +- `TaxRuleService.php` — 税額算出(`getTax()` / `calcTax()`)と税込価格・丸め +- `OrderHelper.php` — カートから購入処理中(`PROCESSING`)受注を組み立てる入口 +- `OrderStateMachine.php` — 受注ステータス遷移(ポイント・在庫の確定/巻き戻しをイベントで実行) +- `PluginService.php` — プラグインの install/enable/disable/uninstall +- `PurchaseFlow/` — 受注処理パイプライン(別途 [README](./PurchaseFlow/README.html)) diff --git a/src/Eccube/Twig/Extension/README.html b/src/Eccube/Twig/Extension/README.html new file mode 100644 index 00000000000..3e65e04ded6 --- /dev/null +++ b/src/Eccube/Twig/Extension/README.html @@ -0,0 +1,119 @@ + + + + + +Twig/Extension — 仕様 + + + + +

Twig/Extension — Twig 拡張

+ + +
+

概要

+

ここは Twig テンプレートから使える独自のフィルタ・関数・テストを定義するディレクトリです。 + 価格の表示({{ price|price }})、日付整形({{ date|date_format }})、商品取得({{ product(id) }})といった、 + テンプレート側で頻出する処理を PHP 側にまとめて提供します。

+

登録方式は 2 つあり、どちらも services.yaml の autoconfigure: true で + 自動的に Twig 拡張として登録されます(手動タグ不要)。

+
    +
  • 属性方式 普通のクラスのメソッドに #[AsTwigFilter] / #[AsTwigFunction] を付ける。 + CsrfExtension / TaxExtension / IntlExtension / CartServiceExtension / CookieConsentExtension / TwigIncludeExtension はこちら(AbstractExtension を継承しない)。
  • +
  • 継承方式 Twig 標準の AbstractExtension を継承し、下表のメソッドで要素を返す。テストや NodeVisitor・エスケープ戦略も提供する EccubeExtension などはこちら。
  • +
+

継承方式で提供できる要素は 3 種類です。

+ + + + + +
提供メソッドテンプレートでの使い方
getFilters()フィルタ {{ value|filter }}
getFunctions()関数 {{ func(args) }}
getTests()テスト {% if value is test %}
+
+ +
+

主要な拡張とその役割

+ + + + + + + + +
クラス役割提供する主なフィルタ/関数
EccubeExtensionコアで最も使われる汎用拡張price / date_format / ellipsis / file_ext_icon(filter)、product() / has_errors() / active_menus() / class_categories_as_json() / currency_symbol()(function)
TaxExtension税に関する判定is_reduced_tax_rate()(軽減税率対象か)
CsrfExtensionCSRF トークンの出力csrf_token_for_anchor()
RepositoryExtensionテンプレートからリポジトリ参照repository()
IntlExtension日時の表示整形date_day / date_min / date_sec / date_day_with_weekday(filter)
TemplateEventExtensionテンプレートイベントの差し込み基盤(NodeVisitor)(フィルタ/関数ではなく描画フックを提供)
+
テンプレートの描画時に、ファイル名をイベント名として TemplateEvent が dispatch されます。 + プラグイン・カスタマイズはここにアセットやスニペットを差し込みます(見た目の調整に使い、業務ロジックは書かない)。 + 購読方法は Skill event-subscriber を参照。
+
+ +
+

使いどころ

+

テンプレートで表示・整形が必要になったら、まず既存のフィルタ/関数で足りるかを確認します。 + 同じ整形ロジックをテンプレートに散らさないのが原則です。

+ + + + + + + +
やりたいこと使うもの
金額を通貨記号・桁区切り付きで表示{{ value|price }}
日付を EC-CUBE 既定フォーマットで表示{{ value|date_format }}
長い文字列を省略(…){{ value|ellipsis(length) }}
ID から商品エンティティを取得{{ product(id) }}
グローバル値の参照BaseInfo / eccube_config / Layout / Page(TwigInitializeListener が注入)
+
+ +
+

独自フィルタ/関数の追加方法開発者向け

+

フィルタ・関数だけを足すなら、メソッドに #[AsTwigFilter] / #[AsTwigFunction] を付ける属性方式が簡単です + (4.4 のコアはこちらへ移行中)。テストや NodeVisitor も要るなら AbstractExtension を継承し、getFilters() / getFunctions() で登録します。 + どちらも依存はコンストラクタ DI で受けます。実装の DO/DON'T は eccube-twig-template/SKILL.md が正典です。

+
// 属性方式(CsrfExtension)。HTML を返すなら isSafe を明示する +#[AsTwigFunction(name: 'csrf_token_for_anchor', isSafe: ['all'])] +public function getCsrfTokenForAnchor(): string { ... } + +// 継承方式(EccubeExtension::getFilters() / getFunctions()) +new TwigFilter('price', $this->getPriceFilter(...)); +// HTML を返すフィルタ/関数は is_safe を明示する +new TwigFilter('file_ext_icon', $this->getExtensionIcon(...), ['is_safe' => ['html']]); +new TwigFunction('product', $this->getProduct(...));
+
    +
  • コア src/Eccube/Twig/Extension/ に置く(このディレクトリ)。
  • +
  • プラグイン / Customize 同じ 2 方式で追加でき、autoconfigure で登録される。
  • +
+
+ +
+

注意点(XSS・エスケープ)開発者向け

+
    +
  • Twig は HTML オートエスケープが既定で有効。{{ value }} は自動でエスケープされる。|raw を付けない限り安全、が大原則。
  • +
  • |raw はエスケープ無効化。ユーザー入力・DB 由来の値に付けると XSS。管理画面テンプレートも例外ではない。
  • +
  • コンテキストに応じたエスケープを使う。JavaScript の中に埋めるなら {{ value|escape('js') }}(HTML エスケープでは JS 文脈の XSS を防げない)。
  • +
  • HTML を返すフィルタ/関数には ['is_safe' => ['html']](属性方式なら isSafe: ['html'])を付ける(付けないと二重エスケープ)。ただし付ける=出力責任を負うということ。 + 中で外部入力を混ぜるなら htmlspecialchars($value, ENT_QUOTES, 'UTF-8') で自前エスケープしてから返す。
  • +
  • ユーザー編集可能なテンプレート文字列(CMS・フリーエリア・メール本文)は Twig サンドボックスを通す + (template_from_string(...) + sandboxed = true)。外すとテンプレートインジェクションになる(過去の脆弱性修正の中心領域)。
  • +
  • サンドボックス違反は本番では例外にならず素通りする。IgnoreTwigSandboxErrorExtension が Twig 標準の include を上書きし、SecurityError を APP_ENV=dev なら再スロー、それ以外はログ出力して null を返す。サンドボックスが効いているかは必ず dev 環境で検証すること(本番では例外が出ないため気づけない)。
  • +
  • メール本文用のエスケープ戦略は HTML とは別。SafeTextmailEscaperExtension が safe_textmail ストラテジを登録し、< > を 実体参照ではなく全角の < > に置換する(プレーンテキストメールに &lt; が出ないようにするため)。メールテンプレートで HTML 用エスケープを流用しない。
  • +
+
+ + + diff --git a/src/Eccube/Twig/Extension/README.md b/src/Eccube/Twig/Extension/README.md new file mode 100644 index 00000000000..b8376e9f260 --- /dev/null +++ b/src/Eccube/Twig/Extension/README.md @@ -0,0 +1,16 @@ +# Twig/Extension — Twig 拡張 + +Twig テンプレートから使える独自のフィルタ・関数・テストを定義するディレクトリ。 +価格表示 `{{ value|price }}`・日付整形 `{{ value|date_format }}`・商品取得 `{{ product(id) }}` など、 +テンプレート頻出の処理を PHP 側に集約する。`#[AsTwigFilter]`/`#[AsTwigFunction]` 属性、または `AbstractExtension` の継承で定義し、`autoconfigure` で自動登録される。 + +- 📖 仕様(人間向け): [README.html](./README.html) +- 🛠 実装規約(AI 向け): [`eccube-twig-template/SKILL.md`](../../../../.claude/skills/eccube-twig-template/SKILL.md) + +## 主要ファイル + +- `EccubeExtension.php` — コアで最も使う汎用拡張(`price` / `date_format` / `ellipsis` / `product()` / `has_errors()` 等) +- `TaxExtension.php` — 軽減税率対象かの判定 `is_reduced_tax_rate()` +- `CsrfExtension.php` — CSRF トークン出力 `csrf_token_for_anchor()`(属性方式と `isSafe` の付与例) +- `TemplateEventExtension.php` — テンプレートイベント差し込みの基盤(NodeVisitor)。プラグイン/カスタマイズの拡張点 +- `RepositoryExtension.php` — テンプレートからリポジトリを参照する `repository()`