Skip to content
Open
4 changes: 4 additions & 0 deletions app/config/eccube/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ services:
lazy: true
public: true

Eccube\Service\EnvFileService:
arguments:
$projectDir: '%kernel.project_dir%'

Eccube\Service\Composer\ComposerProcessService:
lazy: true

Expand Down
33 changes: 26 additions & 7 deletions src/Eccube/Controller/Admin/Setting/System/SecurityController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Eccube\Controller\AbstractController;
use Eccube\Form\Type\Admin\SecurityType;
use Eccube\Service\EnvFileService;
use Eccube\Util\CacheUtil;
use Eccube\Util\StringUtil;
use Symfony\Bridge\Twig\Attribute\Template;
Expand All @@ -25,11 +26,27 @@

class SecurityController extends AbstractController
{
/**
* この画面が .env へ書き込む環境変数キー(OS 環境変数によるオーバーライド判定に使用).
*/
private const ENV_KEYS = [
'ECCUBE_FRONT_ALLOW_HOSTS',
'ECCUBE_FRONT_DENY_HOSTS',
'ECCUBE_ADMIN_ALLOW_HOSTS',
'ECCUBE_ADMIN_DENY_HOSTS',
'ECCUBE_FORCE_SSL',
'TRUSTED_HOSTS',
'ECCUBE_ADMIN_ROUTE',
];
Comment on lines +32 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[高] 7 キーの OR 判定なので、1 キーが OS 環境変数として設定されているだけで、反映されるはずの残り 6 キーまで保存できなくなります。

EnvFileService::getIneffectiveReasons() は対象キーのどれか 1 つでも該当すれば REASON_OVERRIDDEN を返して break します(EnvFileService.php:69-74)。その結果、isEffective(self::ENV_KEYS)SecurityController.php:65)はセキュリティ設定画面全体をブロックします。

ECCUBE_ADMIN_ROUTE だけを OS 環境変数で設定する構成は現実的で、docker-compose.yml:50-59 も各キーを個別にコメントアウトして例示しています(ECCUBE_ADMIN_ROUTE は 54 行目)。この構成では:

  • ECCUBE_ADMIN_ROUTE の変更 → 確かに反映されない(ブロックは妥当)
  • TRUSTED_HOSTS / ECCUBE_FORCE_SSL / IP 制限 4 キー → .env に書けば反映される

にもかかわらず、後者も含めて登録ボタンが無効化され、POST も拒否されます。index.php:41-46 のコメント(「OS 環境変数を保護しつつ、.env 側の非 OS 変数は反映される」)が示すとおり、Docker 環境でも .env 側のキーは生きているので、これは今まで管理画面から変更できていた設定が変更できなくなる退行になります。PR 本文の「既存機能の仕様変更はありません」とも齟齬があると思います。

getIneffectiveReasons() が「どのキーが上書きされているか」まで返すようにして、

  • 上書きされているキーを名指しで警告する
  • 該当キーに対応するフォーム項目だけ無効化する(あるいは書き込み時に該当キーだけスキップする)
  • 画面全体のブロックは not_found / not_writable / local_php(=キーによらず全滅するケース)に限定する

という切り分けはいかがでしょうか。少なくとも「1 キーの上書きで画面全体を止める」のは過剰だと思います。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
過剰ブロックの退行、ご指摘のとおりでした。切り分けを入れました。

  • EnvFileService::getOverriddenKeys(array $keys) を新設し、上書きされているキーの部分集合を返すようにしました。
  • getIneffectiveReasons() はファイル単位で全滅する理由(not_found / not_writable / local_php)だけに限定。
  • セキュリティ設定は、上書きされているキーだけを書き込み対象から除外し、そのキーを名指しで警告します。反映できる残りのキーは保存でき、登録ボタンも有効のままです。
  • テンプレート設定は対象キーが ECCUBE_TEMPLATE_CODE の 1 つだけなので、それが上書きされていれば実質全滅として従来どおり保存拒否+無効化にしています。

「1 キーの上書きで画面全体を止める」挙動は解消しました。


/**
* SecurityController constructor.
*/
public function __construct(protected TokenStorageInterface $tokenStorage, private readonly CacheUtil $cacheUtil)
{
public function __construct(
protected TokenStorageInterface $tokenStorage,
private readonly CacheUtil $cacheUtil,
private readonly EnvFileService $envFileService,
) {
}

/**
Expand All @@ -44,8 +61,8 @@ public function index(Request $request): RedirectResponse|array
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
// .envファイルが存在しないときに設定は失敗する
if (file_exists($this->getParameter('kernel.project_dir').'/.env') === false) {
// .env への書き込みが反映されない状況では保存を拒否する
if (!$this->envFileService->isEffective(self::ENV_KEYS)) {
$this->addError('admin.common.save_error', 'admin');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] POST 拒否時のメッセージが汎用の admin.common.save_error だけなので、理由が分かりません。

GET 時は admin.system.env.ineffective.<reason> で理由を出しているので、POST 拒否時も同じ理由メッセージを併せて出すと、ボタン無効化を回避して直接 POST が飛んできたケース(あるいは表示後に環境が変わったケース)でも原因が伝わります。

$reasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS);
if ([] !== $reasons) {
    $this->addError('admin.common.save_error', 'admin');
    foreach ($reasons as $reason) {
        $this->addError('admin.system.env.ineffective.'.$reason, 'admin');
    }

    return $this->redirectToRoute('admin_setting_system_security');
}

TemplateController.php:72-76 も同様です。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご提案どおり、POST 拒否時に admin.common.save_error に加えて理由メッセージ(admin.system.env.ineffective.<reason> と、上書きキーがある場合は %keys% 入りの文言)を併記するようにしました。TemplateController 側も同様です。ボタン無効化を回避した直接 POST でも原因が伝わります。


return $this->redirectToRoute('admin_setting_system_security');
Expand Down Expand Up @@ -115,13 +132,15 @@ public function index(Request $request): RedirectResponse|array
$this->addWarning('admin.setting.system.security.admin_url_warning', 'admin');
}

// .envファイルが存在しない場合警告を出す。
if (file_exists($this->getParameter('kernel.project_dir').'/.env') === false) {
$this->addWarning('admin.setting.system.security.not_found_env_file', 'admin');
// .env への書き込みが反映されない状況では警告を出し、登録ボタンを無効化する。
$ineffectiveReasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS);
foreach ($ineffectiveReasons as $reason) {
$this->addWarning('admin.system.env.ineffective.'.$reason, 'admin');
}

return [
'form' => $form->createView(),
'envWritable' => [] === $ineffectiveReasons,
];
}
}
42 changes: 27 additions & 15 deletions src/Eccube/Controller/Admin/Store/TemplateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Eccube\Form\Type\Admin\TemplateType;
use Eccube\Repository\Master\DeviceTypeRepository;
use Eccube\Repository\TemplateRepository;
use Eccube\Service\EnvFileService;
use Eccube\Util\CacheUtil;
use Eccube\Util\StringUtil;
use Symfony\Bridge\Twig\Attribute\Template;
Expand All @@ -33,10 +34,15 @@

class TemplateController extends AbstractController
{
/**
* この画面が .env へ書き込む環境変数キー(OS 環境変数によるオーバーライド判定に使用).
*/
private const ENV_KEYS = ['ECCUBE_TEMPLATE_CODE'];

/**
* TemplateController constructor.
*/
public function __construct(protected TemplateRepository $templateRepository, protected DeviceTypeRepository $deviceTypeRepository, private readonly CacheUtil $cacheUtil)
public function __construct(protected TemplateRepository $templateRepository, protected DeviceTypeRepository $deviceTypeRepository, private readonly CacheUtil $cacheUtil, private readonly EnvFileService $envFileService)
{
}

Expand All @@ -59,10 +65,20 @@ public function index(Request $request): array|RedirectResponse
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
// .env への書き込みが反映されない状況では保存を拒否する
// - .env が存在しない / 書き込み不可
// - .env.local.php(dump-env の最適化済みスナップショット)が .env より優先される
// - ECCUBE_TEMPLATE_CODE が OS のプロセス環境変数として設定され .env を上書きしている
if (!$this->envFileService->isEffective(self::ENV_KEYS)) {
$this->addError('admin.common.save_error', 'admin');

return $this->redirectToRoute('admin_store_template');
}

$Template = $this->templateRepository->find($form['selected']->getData());

$envFile = $this->getParameter('kernel.project_dir').'/.env';
$env = file_exists($envFile) ? file_get_contents($envFile) : '';
$env = file_get_contents($envFile);

$env = StringUtil::replaceOrAddEnv($env, [
'ECCUBE_TEMPLATE_CODE' => $Template->getCode(),
Expand All @@ -72,33 +88,29 @@ public function index(Request $request): array|RedirectResponse

$this->addSuccess('admin.common.save_complete', 'admin');

// 次のいずれかの場合、.env への書き込みが起動時のロードで反映されない:
// 1. ECCUBE_TEMPLATE_CODE がプロセス環境変数として設定されている(Docker などで明示的に設定した場合)。
// bootEnv は OS 環境変数を上書きしないため .env の値が使われない。
// 2. .env.local.php(dump-env の最適化済みスナップショット)が存在する。
// bootEnv は .env より .env.local.php を優先するため、再度 `composer symfony:dump-env` を
// 実行するまで .env の変更が反映されない。
$envLocalPhp = $this->getParameter('kernel.project_dir').'/.env.local.php';
if (false !== getenv('ECCUBE_TEMPLATE_CODE') || file_exists($envLocalPhp)) {
$this->addWarning('admin.store.template.env_override_warning', 'admin');
}

$this->cacheUtil->clearCache();

return $this->redirectToRoute('admin_store_template');
}

// .env への書き込みが反映されない状況では警告を出し、登録ボタンを無効化する。
$ineffectiveReasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS);
foreach ($ineffectiveReasons as $reason) {
$this->addWarning('admin.system.env.ineffective.'.$reason, 'admin');
}

return [
'form' => $form->createView(),
'Templates' => $Templates,
'envWritable' => [] === $ineffectiveReasons,
];
}

/**
* テンプレート一覧からのダウンロード
*/
#[Route(path: '/%eccube_admin_route%/store/template/{id}/download', name: 'admin_store_template_download', requirements: ['id' => '\d+'], methods: ['GET'])]
public function download(Request $request, \Eccube\Entity\Template $Template): BinaryFileResponse
public function download(\Eccube\Entity\Template $Template): BinaryFileResponse

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] download() / delete() からの Request $request 引数削除は、本 PR の目的と無関係な public シグネチャ変更です。

download()(113 行目)と delete()(170 行目)から Request $request が削除されていますが、.env の反映可否検出とは無関係な変更です。app/Customize やプラグインでこのコントローラを継承してメソッドをオーバーライドしている場合、シグネチャ不一致で fatal になります。

PR 本文の互換性チェックリストは Service クラスについてしか言及していませんが、コントローラの public メソッドも同じく拡張点なので、

  • 別 PR に分ける(未使用引数の整理としてまとめて実施する)
  • あるいは本 PR に含めるなら、PR 本文に破壊的変更として明記する

のどちらかが良いと思いますが、いかがでしょうか。4.4 はメジャー更新なので削除自体が不可能とは思いませんが、レビュー単位としては分けたほうが追いやすいと感じました。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご指摘ありがとうございます。本 PR の目的と無関係な public シグネチャ変更、という点はそのとおりです。ひとつ判断を仰ぎたい点があります。

この引数削除は、実は CI の Rector ジョブが要求しています。SymfonySetList::SYMFONY_CODE_QUALITY に含まれる RemoveUnusedRequestParamRector が、download() / delete() の未使用 Request $request を検出し削除を求めます(本 PR で同ファイルを触ったことで Rector のファイル単位キャッシュが無効化され、既存の未使用引数が再解析されて表面化しました)。そのため引数を元に戻すと Rector ジョブが赤になります。

対応として 2 案あります。どちらが望ましいでしょうか。

  • (A) 引数を残す(BC 維持)+ rector.phpwithSkip()RemoveUnusedRequestParamRector × TemplateController を理由コメント付きで追加し、CI を通す。
  • (B) Rector に従い削除したままとし、PR 本文に「public シグネチャの互換性に関わる変更」として明記する。

個人的には、拡張点である public メソッドの互換を優先するなら (A) が筋だと思いますが、プロジェクトの Rector 運用方針に合わせたく、ご意見をいただけますか。合意後に反映します。

{
// 該当テンプレートのディレクトリ
$templateCode = $Template->getCode();
Expand Down Expand Up @@ -155,7 +167,7 @@ public function download(Request $request, \Eccube\Entity\Template $Template): B
}

#[Route(path: '/%eccube_admin_route%/store/template/{id}/delete', name: 'admin_store_template_delete', requirements: ['id' => '\d+'], methods: ['DELETE'])]
public function delete(Request $request, \Eccube\Entity\Template $Template): RedirectResponse
public function delete(\Eccube\Entity\Template $Template): RedirectResponse
{
$this->isTokenValid();

Expand Down
4 changes: 4 additions & 0 deletions src/Eccube/Resource/locale/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,10 @@ admin.setting.system.security.ip_limit_invalid_ipv4: "%ip% is not an IPv4 addres
admin.setting.system.security.ip_limit_invalid_https: "http is not allowed to do this setting."
admin.setting.system.security.admin_url_warning: Please set the Admin Console URL that is hard to guess for security.
admin.setting.system.security.not_found_env_file: .env file not found. If you do not use the .env file you can not change security config in Admin Console.
admin.system.env.ineffective.not_found: 'The .env file does not exist, so this setting cannot be changed from the admin screen. Please set the environment variables on the server directly.'
admin.system.env.ineffective.not_writable: 'The .env file is not writable, so this setting cannot be changed from the admin screen. Please check the write permission of the .env file.'
admin.system.env.ineffective.local_php: 'Because .env.local.php exists, changes to .env are not applied and cannot be made from the admin screen. Update .env on the server and re-run composer dump-env, or remove .env.local.php to re-enable changes from the admin screen.'
admin.system.env.ineffective.overridden: 'The environment variable for this setting is set as an OS process environment variable, so changes to .env are not applied. Please change the environment variable on the server directly.'
admin.setting.system.security.trusted_hosts: Trusted hosts
admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$
admin.setting.system.security.trusted_hosts_description: |
Expand Down
4 changes: 4 additions & 0 deletions src/Eccube/Resource/locale/messages.ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,10 @@ admin.setting.system.security.ip_limit_invalid_ip_and_submask: "%ip%はIPv4/ビ
admin.setting.system.security.ip_limit_invalid_https: "httpの場合には設定できません。"
admin.setting.system.security.admin_url_warning: 管理画面URLは、セキュリティのため推測されにくいものを設定してください。
admin.setting.system.security.not_found_env_file: .envファイルが見つかりません。.envを利用していない場合はセキュリティ設定を管理画面から変更できません。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] 参照されなくなった翻訳キーが残っています。

本 PR で SecurityController / TemplateController の警告が admin.system.env.ineffective.* に置き換わった結果、以下の 2 キーは参照ゼロになっています(rg で確認)。

  • admin.setting.system.security.not_found_env_file(この行 / en は messages.en.yaml:1431
  • admin.store.template.env_override_warningmessages.ja.yaml:1522 / messages.en.yaml:1522

削除するか、あるいは env_override_warning のほうは「どのキーが原因か名指しする」という良い先例なので、admin.system.env.ineffective.overridden の文言改善(別コメント参照)に取り込んで整理するのが良いと思います。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご指摘の 2 キーを削除しました。

  • admin.setting.system.security.not_found_env_file(ja / en)
  • admin.store.template.env_override_warning(ja / en)

後者が持っていた「原因キーを名指しする」案内は、overridden 文言(%keys%)に取り込みました。

admin.system.env.ineffective.not_found: '.env ファイルが存在しないため、この設定は管理画面から変更できません。サーバーの環境変数を直接設定してください。'
admin.system.env.ineffective.not_writable: '.env ファイルに書き込み権限がないため、この設定は管理画面から変更できません。.env ファイルの書き込み権限を確認してください。'
admin.system.env.ineffective.local_php: '.env.local.php が存在するため、.env への変更は反映されず、管理画面からは変更できません。サーバー側で .env を更新してから composer dump-env を再実行するか、.env.local.php を削除して管理画面からの変更を再開してください。'
admin.system.env.ineffective.overridden: 'この設定の環境変数が OS のプロセス環境変数として設定されているため、.env への変更は反映されません。サーバーの環境変数を直接変更してください。'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] どの環境変数が原因なのかがメッセージから分かりません。

セキュリティ設定は 7 キーを対象にしているため(SecurityController.php:32-40)、この文言だけでは管理者がサーバ側のどの環境変数を直せばよいか判断できません。置き換え前の admin.store.template.env_override_warningmessages.ja.yaml:1522)は ECCUBE_TEMPLATE_CODE を名指ししていたので、テンプレート設定に関しては案内内容が後退しています。

getIneffectiveReasons() が上書きされているキーを返すようにしたうえで、プレースホルダで埋め込む形はいかがでしょうか(en 側も同様)。

admin.system.env.ineffective.overridden: 'この設定の環境変数(%keys%)が OS のプロセス環境変数として設定されているため、.env への変更は反映されません。サーバーの環境変数を直接変更してください。'

上書きされているキーを名指しできれば、セキュリティ設定画面の過剰ブロック(SecurityController.php:32-40 のコメント参照)の解消にもそのまま使えると思います。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご提案どおり admin.system.env.ineffective.overridden%keys% 入りに変更し、getOverriddenKeys() が返したキー名を埋め込むようにしました(en 側も同様)。名指しできるようになったので、セキュリティ設定の警告でも同じ文言で原因キーを表示しています。

admin.setting.system.security.trusted_hosts: 信頼できるホスト名
admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$
admin.setting.system.security.trusted_hosts_description: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ file that was distributed with this source code.
<div class="col-6">
<div id="ex-conversion-action" class="row align-items-center justify-content-end">
<div class="col-auto">
<button type="submit" class="btn btn-ec-conversion px-5">{{ 'admin.common.registration'|trans }}</button>
<button type="submit" class="btn btn-ec-conversion px-5"{% if envWritable is defined and not envWritable %} disabled{% endif %}>{{ 'admin.common.registration'|trans }}</button>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/Eccube/Resource/template/admin/Store/template.twig
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ file that was distributed with this source code.
<div class="col-6">
<div id="ex-conversion-action" class="row align-items-center justify-content-end">
<div class="col-auto">
<button class="btn btn-ec-conversion px-5" type="submit">{{ 'admin.common.registration'|trans }}</button>
<button class="btn btn-ec-conversion px-5" type="submit"{% if envWritable is defined and not envWritable %} disabled{% endif %}>{{ 'admin.common.registration'|trans }}</button>
</div>
</div>
</div>
Expand Down
88 changes: 88 additions & 0 deletions src/Eccube/Service/EnvFileService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Eccube\Service;

/**
* .env ファイルへの書き込みが実行時に反映されるかを判定するサービス.
*
* セキュリティ設定・テンプレート設定などは .env を書き換えて機能を実現するが,
* docker-compose の環境変数や本番環境では .env が使われず, 書き換えても反映されない.
* ユーザーが変更できないことに気付けるよう, 反映されない理由を検出する.
*
* @see https://github.com/EC-CUBE/ec-cube/issues/6130
*/
class EnvFileService
{
/** .env ファイルが存在しない(.env を利用していない) */
public const REASON_NOT_FOUND = 'not_found';

/** .env ファイルに書き込み権限がない */
public const REASON_NOT_WRITABLE = 'not_writable';

/** .env.local.php(dump-env の最適化済みスナップショット)が存在し, .env より優先される */
public const REASON_LOCAL_PHP = 'local_php';

/** 対象の環境変数が OS のプロセス環境変数として設定されており, .env の値を上書きしている */
public const REASON_OVERRIDDEN = 'overridden';

public function __construct(private readonly string $projectDir)
{
}

/**
* .env への書き込みが実行時のロードに反映されない理由を返す.
*
* 空配列であれば .env への書き込みが有効に反映される.
*
* @param string[] $keys 対象の環境変数キー(OS 環境変数によるオーバーライド判定に使用)
*
* @return string[] REASON_* 定数の配列
*/
public function getIneffectiveReasons(array $keys = []): array
{
$reasons = [];

$envFile = $this->projectDir.'/.env';
if (!file_exists($envFile)) {
$reasons[] = self::REASON_NOT_FOUND;
} elseif (!is_writable($envFile)) {
$reasons[] = self::REASON_NOT_WRITABLE;
}

// .env.local.php があると bootEnv は .env より優先するため, .env の変更は反映されない
if (file_exists($this->projectDir.'/.env.local.php')) {
$reasons[] = self::REASON_LOCAL_PHP;
}
Comment on lines +72 to +75

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] .env.local / .env.$APP_ENV / .env.$APP_ENV.local.env を上書きしますが、検出されていません。

Dotenv::loadEnv().env.env.local.env.$env.env.$env.local の順にカスケード読み込みし、後から読んだファイルが .env の値を上書きします(doLoad()populate() は、既に dotenv が読んだキーであれば SYMFONY_DOTENV_VARS 経由で上書きを許可するため)。この挙動は本リポジトリの src/Eccube/Resource/functions/env.php:19-20 の docblock 自身にも書かれています。

また index.php:23 / bin/console:19.env.local$dotenvExists の判定に含めており、EC-CUBE として正式にサポートされている配置です。

同じ検証スクリプトでの実測結果です(.envECCUBE_ADMIN_ROUTE=fromdotenv):

構成 env()(実際に効く値) isEffective(['ECCUBE_ADMIN_ROUTE'])
.env のみ fromdotenv true(正しい)
.env + .env.local fromlocal true ← 検出漏れ
.env + .env.prod fromprod true ← 検出漏れ

.env.local.php と同じ「書いても反映されないのに気付けない」ケースなので、本 PR の目的(#6130)からすると取りこぼしたくないところだと思います。

ただし .env.local.php(dump-env のスナップショットなので全キーを含む)と違い、これらは特定のキーだけを定義していることが多いので、ファイルの存在だけで一律ブロックすると逆に過剰になります。Dotenv::parse() で該当ファイルを読み、対象キーが定義されている場合だけ理由に加える、という形が実態に合うと思いますが、いかがでしょうか。

$appEnv = $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null;
$cascades = array_filter([
    '.env.local',
    $appEnv ? ".env.{$appEnv}" : null,
    $appEnv ? ".env.{$appEnv}.local" : null,
]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご指摘ありがとうございます。.env.local / .env.$APP_ENV / .env.$APP_ENV.localDotenv::parse() で読み、対象キーが定義されているファイルがある場合だけ上書き扱いに加えるようにしました(ファイルの存在だけで一律ブロックはしていません)。$APP_ENV$_SERVER / $_ENV から取得しています。解析できないファイルは判定対象から外し、設定画面が落ちないようにしています。


// bootEnv は OS のプロセス環境変数を上書きしないため, getenv が値を返すキーは .env の変更が反映されない
foreach ($keys as $key) {
if (false !== getenv($key)) {
$reasons[] = self::REASON_OVERRIDDEN;
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[高] getenv() だけの判定では、.env が勝つ経路を「上書きされている」と誤検知し、反映される保存をブロックします。

index.php は OS 環境変数 APP_ENV の有無で読み込み方を変えています。

  • index.php:35APP_ENV 未設定 = 非 Docker の一般的な構成): boot_env(__DIR__.'/.env', true)overrideExistingVars = true なので .env の値が OS のプロセス環境変数を上書きする
  • index.php:47APP_ENV 設定済み = Docker 等): boot_env(__DIR__.'/.env', false) → OS 環境変数が勝つ

一方 Symfony Dotenv は putenv() を使わない(usePutenv=false)ため、getenv() は前者でも OS 側の値を返し続けます。手元で index.php の 2 経路を再現した実測結果です(.envECCUBE_ADMIN_ROUTE=fromdotenv、OS 環境変数に ECCUBE_ADMIN_ROUTE=osvalue):

経路 getenv() env()(実際に効く値) SYMFONY_DOTENV_VARS に含まれるか
A: APP_ENV 未設定 → boot_env(.., true) osvalue fromdotenv yes
B: APP_ENV 設定済み → boot_env(.., false) osvalue osvalue no

A では .env の値が実際に効いている(=管理画面からの変更は反映される)にもかかわらず、本サービスは REASON_OVERRIDDEN を返すため、SecurityController / TemplateController は保存を拒否し、登録ボタンも無効化されます。これは今まで動いていた操作ができなくなる退行になります。

なお、ブート後は bootEnv()$_SERVER['APP_ENV'] を必ずセットするため、コントローラ実行時点で「どちらの経路だったか」を isset($_SERVER['APP_ENV']) で判別することはできません。上表のとおり、実際の勝敗と一致するのは SYMFONY_DOTENV_VARS(Dotenv が実際に populate したキーの一覧)です。Dotenv::populate() 自身も getenv() を避けて(実装中のコメント: "don't check existence with getenv() because of thread safety issues")このリストで判断しています。コア側の env()src/Eccube/Resource/functions/env.php:55-64)も $_ENV$_SERVERgetenv() の順で、getenv() は互換のためのフォールバック扱いです。本サービスだけ解決順が逆になっています。

// Dotenv が実際に .env 系から populate したキーは SYMFONY_DOTENV_VARS に載る
$dotenvVars = array_flip(array_filter(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '')));

foreach ($keys as $key) {
    if (isset($dotenvVars[$key])) {
        continue; // .env 系の値が実際に適用されている = 書き込みは反映される
    }
    if (isset($_ENV[$key]) || isset($_SERVER[$key]) || false !== getenv($key)) {
        $reasons[] = self::REASON_OVERRIDDEN;
        break;
    }
}

この形にすると、A/B の判別に加えて、nginx の fastcgi_param や Apache の SetEnv のように $_SERVER 側にだけ入ってくる値(Dotenv::populate()isset($_ENV[$name]) を見てスキップするので、この場合も .env は負けます)も拾えるようになります。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご指摘ありがとうございます。実測まで添えていただき助かりました。index.phpboot_env(.., true) 経路で .env が勝つのに getenv() が OS 値を返す、という誤検知を確認しました。ご提案どおり判定を SYMFONY_DOTENV_VARS(Dotenv が実際に populate したキー一覧)ベースに変更しました。

  • .env 系から populate されたキー(SYMFONY_DOTENV_VARS に載る)は「反映される」として上書き扱いから除外。
  • それ以外で $_ENV / $_SERVER / getenv() に値があるキーのみ上書きと判定($_SERVER だけに入る fastcgi_param / SetEnv も拾えます)。

コアの env() と解決順が揃いました。


return $reasons;
}

/**
* 対象の環境変数について, .env への書き込みが有効に反映されるか.
*
* @param string[] $keys 対象の環境変数キー
*/
public function isEffective(array $keys = []): bool
{
return [] === $this->getIneffectiveReasons($keys);
}
}
Loading
Loading