| name | csv |
|---|---|
| description | EC-CUBE 4.4 の CSV 入出力(CsvImportService・CsvExportService・CSV 定義)を実装・改修するときの規約。「CSVインポートを実装して」「CSVエクスポートを追加して」「商品/受注のCSV出力を作って」「CSVの項目を増やして」などと言われたとき、または src/Eccube/Service/Csv*Service・CSV 定義を作成・編集するときに使用する。 |
対象: src/Eccube/Service/CsvImportService.php, src/Eccube/Service/CsvExportService.php,
CSV 項目定義(dtb_csv = Eccube\Entity\Csv / mtb_csv_type = Eccube\Entity\Master\CsvType),
および CSV 入出力を行う管理画面コントローラ(src/Eccube/Controller/Admin/**)。
前提: Symfony 7.4 / PHP 8.2+ / Doctrine ORM 3.x
目的: EC-CUBE の CSV 入出力は「出力項目をマスタ(
dtb_csv)で定義し、CsvExportServiceが エンティティから値を引く」「入力はCsvImportService(Iterator)で 1 行ずつ読む」という二つの確立した仕組みに乗る。 自前でfgetcsv/fputcsvを書き散らさず、既存サービスとマスタ定義の枠組みに従う。
- エクスポート:
CsvExportService(Eccube\Service)。dtb_csvの定義に従ってヘッダ・データ行をphp://outputへ流す。 コントローラ側はStreamedResponseでラップして返す。 - インポート:
CsvImportService(Eccube\Service)。\SplFileObjectを包む\Iterator/\SeekableIterator/\Countable。 1 行を連想配列(ヘッダ名 => 値)として返す。 - 項目定義: 何のエンティティのどのカラムを CSV のどの列に出すかは
dtb_csv(Csvエンティティ)で持つ。 CSV 種別(商品・会員・受注・配送…)はmtb_csv_type(CsvTypeマスタ、CSV_TYPE_*定数)で区別する。 - 文字コード・区切り文字は コントローラやサービスにハードコードせず
EccubeConfig(eccube.yaml)の設定値を使う。
- 新規に CSV 入出力ロジックを書くときも、まず既存サービスに乗れないか確認する。
fputcsv/fgetcsvの直書きはCsvExportService::fputcsv()/CsvImportServiceで吸収されている。 - 出力項目はコードに埋め込まず
dtb_csv定義で表現する。項目の追加・並び替え・有効無効はCsv(field_name/reference_field_name/disp_name/sort_no/enabled)で制御する。 - 文字コード・区切り文字は設定値を使う(
src/Eccube/Service/CsvExportService.php/eccube.yaml):- 出力エンコーディング:
eccube_csv_export_encoding(既定SJIS-win) - 出力区切り文字:
eccube_csv_export_separator(既定,) - 出力日付フォーマット:
eccube_csv_export_date_format(既定Y-m-d H:i:s) - 複数データ(one-to-many)の区切り:
eccube_csv_export_multidata_separator(既定,) - 入力エンコーディング候補:
eccube_csv_import_encoding、入力区切り/囲み:eccube_csv_import_delimiter/eccube_csv_import_enclosure
- 出力エンコーディング:
- エクスポートは必ず
StreamedResponse。メモリに全件貯めず、ストリームへ逐次出力する (件数が膨大になり得るため)。レスポンスはContent-Type: application/octet-stream+Content-Disposition: attachment。 - ストアド項目の追加・拡張はイベントで行う。コア改変ではなく、
EccubeEventsの CSV エクスポートイベント (ADMIN_*_CSV_EXPORT*)を購読してExportCsvRowに列を足す(後述)。 - データアクセス・業務ロジックの分担は Skill
service/repositoryに従う(検索条件の組み立ては Repository のgetQueryBuilderBySearchData*()、値のバインドはsetParameter())。
src/Eccube/Controller/Admin/Order/OrderController.php::exportCsv() が定石。
CsvExportService を StreamedResponse のコールバック内で駆動する:
protected function exportCsv(Request $request, int $csvTypeId, string $fileName): StreamedResponse
{
set_time_limit(0);
// 大量出力時は SQL Logger を無効化
$this->entityManager->getConfiguration()->setSQLLogger();
$response = new StreamedResponse();
$response->setCallback(function () use ($request, $csvTypeId): void {
// 1. CSV 種別で初期化(dtb_csv から有効・sort_no 順の定義を読み込む)
$this->csvExportService->initCsvType($csvTypeId);
// 2. 検索条件のクエリビルダを取得(Repository 由来)
$qb = $this->csvExportService->getOrderQueryBuilder($request);
// 3. ヘッダ行(dtb_csv.disp_name)
$this->csvExportService->exportHeader();
// 4. データ行(100 件ずつページングし em->clear() しながら出力)
$this->csvExportService->setExportQueryBuilder($qb);
$this->csvExportService->exportData(function ($entity, $csvService): void {
$Csvs = $csvService->getCsvs();
foreach ($entity->getOrderItems() as $OrderItem) {
$ExportCsvRow = new ExportCsvRow();
foreach ($Csvs as $Csv) {
// getData() が「定義エンティティと一致するか」を判定して値を返す
$ExportCsvRow->setData($csvService->getData($Csv, $entity));
if ($ExportCsvRow->isDataNull()) {
$ExportCsvRow->setData($csvService->getData($Csv, $OrderItem));
}
// ...(必要なら Shipping 等もフォールバック探索)
$ExportCsvRow->pushData();
}
$csvService->fputcsv($ExportCsvRow->getRow());
}
});
});
$response->headers->set('Content-Type', 'application/octet-stream');
$response->headers->set('Content-Disposition', 'attachment; filename='.$fileName);
return $response;
}ポイント:
initCsvType()はdtb_csvをenabled = trueかつsort_no ASCで読む(CsvExportService::initCsvType())。getData(Csv $Csv, AbstractEntity $entity)が値の取り出しを一手に担う:Csv::getEntityName()と実エンティティのクラスが一致しなければnull(複数エンティティを順に当てて探す前提)。- one-to-one は
reference_field_nameの値、one-to-many はeccube_csv_export_multidata_separatorで連結、\DateTimeはeccube_csv_export_date_format、bool は'1'/'0'に変換。
fputcsv()はgetConvertEncodingCallback()を通して UTF-8 → 出力エンコーディングへ変換してから書き出す。
AbstractCsvImportController(src/Eccube/Controller/Admin/AbstractCsvImportController.php)を継承し、
getImportData() で CsvImportService を得る。これが定石(商品/会員/受注インポート各コントローラが踏襲):
$data = $this->getImportData($formFile); // CsvImportService|false
if ($data === false) {
$this->addErrors(trans('admin.common.csv_invalid_format'));
return $this->renderWithError($form, $headers, false);
}
// 必須ヘッダの充足チェック
$columnHeaders = $data->getColumnHeaders();
if (count(array_diff($requireHeader, $columnHeaders)) > 0) { /* エラー */ }
if (count($data) < 1) { /* データ無しエラー */ }
$this->entityManager->getConnection()->beginTransaction();
try {
foreach ($data as $row) { // $row はヘッダ名 => 値 の連想配列
$line = $data->key() + 1; // 行番号
if ($headerSize != count($row)) { /* 列数不一致エラー */ }
// ... エンティティへマッピングし persist
}
if ($this->hasErrors()) { // 途中で addErrors されていたら
$this->entityManager->getConnection()->rollback();
} else {
$this->entityManager->flush();
$this->entityManager->getConnection()->commit();
}
} finally {
$this->removeUploadedFile(); // 一時ファイルを必ず削除
}ポイント(AbstractCsvImportController 由来):
getImportData()がアップロードファイルをeccube_csv_temp_realdirに退避し、eccube_csv_import_delimiter/eccube_csv_import_enclosureを使ってCsvImportServiceを生成、setHeaderRowNumber(0)する。CsvImportServiceは 先頭行を見て UTF-8 でなければ SJIS-win → UTF-8 の stream filter を自動適用する (SjisToUtf8EncodingFilter/ConvertLineFeedFilter)。エンコーディング判定を自前で書かない。count($data)で行数、$data->key()で現在行番号、foreachで 1 行ずつ取得(メモリに全展開しない)。- 取り込みはトランザクションで囲み、エラー時は rollback。終了時に一時ファイルを削除する。
- 管理画面で増やせる出力項目は
dtb_csvのレコード追加(CSV 設定画面)で完結する。コード変更は不要。 - コードで列を足したい場合は、コア改変ではなく CSV エクスポートイベントを購読する。
EccubeEventsに種別ごとのイベントがある:ADMIN_ORDER_CSV_EXPORT_ORDER(受注・出荷 CSV 共通。OrderController::exportCsv()は受注/出荷どちらのエクスポートでもこの1イベントを dispatch する)ADMIN_PRODUCT_CSV_EXPORT/ADMIN_CUSTOMER_CSV_EXPORTADMIN_PRODUCT_CATEGORY_CSV_EXPORT/ADMIN_PRODUCT_CLASS_NAME_CSV_EXPORT/ADMIN_PRODUCT_CLASS_CATEGORY_CSV_EXPORT購読側でEventArgsからExportCsvRowを受け取り、setData()/pushData()で列を追加する (OrderController::exportCsv()のイベント dispatch 箇所を参照)。イベント実装の作法は Skillevent-subscriber。
- 新しいエンティティに紐づくマスタ種別を足すなら
mtb_csv_typeへの INSERT(STI なのでdiscriminator_type必須、Skillmigration)。
- ❌ コントローラ/サービスで
fgetcsv/fputcsvを直書きする → ✅CsvImportService/CsvExportService::fputcsv()に乗る - ❌ 文字コード・区切り文字をハードコードする(
'SJIS-win',','直書き) → ✅EccubeConfig(eccube_csv_export_*/eccube_csv_import_*)の設定値を使う - ❌ エクスポートで全件を配列に貯めて一括出力する → ✅
StreamedResponse+exportData()のページング(100 件ずつem->clear())で逐次出力 - ❌ 出力項目をコントローラに
ifで羅列する → ✅dtb_csv定義(field_name/sort_no/enabled)で表現しgetData()に引かせる - ❌ インポートで自前エンコーディング判定や全行読み込みをする → ✅
CsvImportService(stream filter 自動適用・Iterator)に任せ 1 行ずつ処理 - ❌ インポートをトランザクション無しで
flush()/エラー時も一時ファイルを残す → ✅beginTransaction〜commit/rollbackで囲み、終了時にremoveUploadedFile() - ❌ 出力項目追加のためにコアの export 処理を改変する → ✅
ADMIN_*_CSV_EXPORT*イベントを購読してExportCsvRowに列追加 - ❌
mtb_csv_typeへdiscriminator_typeを指定せず INSERT する(STI のため壊れる) → ✅ 種別追加時は discriminator を必ず指定(Skillmigration) - ❌ 素の
fputcsv($fp, $row)で escape 引数を省略(PHP 8.4 で deprecation:the $escape parameter must be provided)→ ✅ 第5引数まで明示(コアはfputcsv(..., ',', '"', '\\'))。そもそもCsvExportService::fputcsv()に乗れば吸収される
- 実装後の整形・型・静的解析・テストは AGENTS.md「開発コマンド」 に従って実行する (PHP-CS-Fixer / PHPStan level 6 / PHPUnit)。
- 関連レイヤの規約も参照: Skill
service(サービス責務)/repository(クエリビルダ)/event-subscriber(エクスポートイベント購読)/migration(mtb_csv_type追加)。 - 実装・改修後は Skill
review-responsibilityで責務分離・セキュリティを点検すること。