0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

共有レンタルサーバーで.envの一部が効かない|phpdotenv v5 は putenv しない

0
Last updated at Posted at 2026-07-22

共有レンタルサーバーの罠 #5/全5本(最終回)。症状→原因→対策の索引は 🔗 罠ハブ から。

はじめに

phpdotenv を使いつつ getenv() も併用していて、共有レンタルサーバーに .env で設置している方へ。

共有レンタルサーバー向けの配布物(.env だけを置く設置形態)を用意する過程で、この設置を手元のコンテナで再現したところ、一部の設定(APP_BASE_PATH / BASE_DOMAIN / ORG_SLUG / TENANT_RESOLUTION / MEDIA_STORAGE_DRIVER)だけがそろって既定値に落ちました.env の中身は正しく、APP_ENV や DB 接続は効いているのに、です。Docker 本番では実環境変数で値を渡していたため表面化していなかった潜在バグです。

犯人は、vlucas/phpdotenvv4→v5 へ上げたときに変わっていた「環境変数を載せる場所」。v5 の既定は putenv() しないため、getenv() 直読みのコードだけが静かに壊れていました。この記事では、原因を実装で確かめ、getenv() 直読みを1箇所で救う env ブリッジで直すまでを追います(解決済み)。

この記事で分かること:

  • phpdotenv v5 の既定アダプタが $_ENV / $_SERVER のみで putenv() しないこと、その事実確認の仕方
  • 同じ .env でも「読み口が $_ENVgetenv() か」で結果が割れる仕組み
  • getenv() 直読みを各所に散らしたまま、エントリポイント1箇所で吸収する env ブリッジ(実環境変数優先)

なぜ一部の設定だけ既定値に落ちるのか

v5 は .env$_ENV / $_SERVER にだけ載せ、putenv() しません。だから getenv() 直読みの設定だけが既定値に落ちます(下図)。

phpdotenv v5 の既定は「$_ENV / $_SERVER のみ」

まず事実確認から。今回のプロジェクトは vlucas/phpdotenv: ^5.6(実体は v5.6.3)を使っています。エントリポイントでの読み込みはごく普通の形です。

Dotenv\Dotenv::createImmutable(dirname(__DIR__))->safeLoad();

ここがポイントで、v5 の createImmutable() / createMutable() が使う「既定アダプタ」には putenv() を書くアダプタが含まれていません。ライブラリのソースを見ると一目瞭然です。

// vendor/vlucas/phpdotenv/src/Repository/RepositoryBuilder.php
private const DEFAULT_ADAPTERS = [
    ServerConstAdapter::class,  // $_SERVER に書く
    EnvConstAdapter::class,     // $_ENV に書く
];

既定アダプタは $_SERVER$_ENV の2つだけ。getenv() / putenv() に相当する PutenvAdapter は入っていません。

PutenvAdapter が有効になるのは、名前に Unsafe が付いた明示的なファクトリを呼んだときだけです。

// vendor/vlucas/phpdotenv/src/Dotenv.php
public static function createUnsafeImmutable($paths, ...): self
{
    $repository = RepositoryBuilder::createWithDefaultAdapters()
        ->addAdapter(PutenvAdapter::class)   // ここで初めて putenv される
        ->immutable()
        ->make();
    // ...
}

つまり getenv() を経由して .env の値を読みたいなら、明示的に createUnsafeImmutable()(または ->addAdapter(PutenvAdapter::class))を選ぶ必要がある ということです。v4 までは既定で putenv() していたので、素朴に v5 へ上げると getenv() 直読みのコードが静かに壊れます。「危険(unsafe)」という名前が付いているとおり、getenv() はスレッドセーフでない等の理由で v5 では既定から外されました。

「効く設定」と「落ちる設定」を分けたのは読み口の違い

なぜ 全滅ではなく一部だけ 落ちたのか。それは、設定を読む口が2系統あったからです。

DB 接続などを組み立てる設定ローダは、$_SERVER / $_ENV を直接見ていました。

// ConfigLoader::readEnvironmentValues()
$value = $_SERVER[$key] ?? $_ENV[$key] ?? null;

こちらは v5 の既定アダプタが書き込む場所そのものなので、.env しか無くても正しく効きます。DB 接続情報が生きていたのはこのためです。

一方、あとから足したプロバイダ設定は getenv() を直読みしていました。

// src/Http/BasePath.php
$raw = getenv('APP_BASE_PATH');

// src/Http/RuntimeServiceProvider.php
$resolvedSlug   = (string) (getenv('ORG_SLUG') ?: '');
$resolvedDomain = (string) (getenv('BASE_DOMAIN') ?: 'localhost');
$resolvedMode   = $sysConfig->get('tenant_resolution_mode')
    ?: (string) (getenv('TENANT_RESOLUTION') ?: 'single');

// src/Media/MediaServiceProvider.php
$driver = getenv('MEDIA_STORAGE_DRIVER') ?: 'local';

.env$_ENV / $_SERVER にしか載っていないので、これらの getenv() は軒並み false を返し、?: の右側(既定値)に落ちます。同じ .env を読んでいるのに、読み口が $_ENVgetenv() かで結果が割れた ——これが「一部だけ既定値化」の正体でした。

Docker 本番で問題が出なかったのは、そこでは .env ファイルではなくコンテナの実環境変数として値を渡していたからです。実環境変数なら getenv() からも $_ENV からも読めるので、どちらの読み口でも効いていて、罠が露見しませんでした。

対策:env ブリッジで getenv() 直読みを救う

正攻法は「読み口を $_ENV に統一する」か「createUnsafeImmutable() に切り替える」かのどちらかです。ただ既存の getenv() 直読みが各所に散っており、一気に置換するのはリスクが高い。そこで、エントリポイントで一度だけ $_ENVgetenv() へ橋渡しするブリッジを1箇所に置いて吸収しました。実際に入れたコードがこれです。

// public_html/index.php
// 共有ホスティングブリッジ: phpdotenv v5 は .env を $_ENV/$_SERVER にしか
// 書かず putenv しないため、getenv() 直読みのプロバイダ設定(APP_BASE_PATH /
// BASE_DOMAIN / ORG_SLUG / TENANT_RESOLUTION / MEDIA_STORAGE_DRIVER)が「.env しか
// 無い」設置で全て既定値に落ちる。ここで .env を一度読み、実環境変数が
// 無いキーだけ putenv で写す(実環境変数が優先=Docker 系デプロイは挙動不変)。
if (is_file(dirname(__DIR__) . '/.env')) {
    Dotenv\Dotenv::createImmutable(dirname(__DIR__))->safeLoad();

    foreach ($_ENV as $key => $value) {
        if (is_string($value) && getenv($key) === false) {
            putenv($key . '=' . $value);
        }
    }
}

ポイントは3つあります。

  • .env が存在するときだけ動く。 is_file() でガードしているので、.env を置かず実環境変数だけで運用する Docker 系デプロイでは、このブロックは丸ごとスキップされます。
  • 実環境変数を絶対に上書きしない。 getenv($key) === false(=まだ載っていないキー)だけを putenv() します。実環境変数が既にあるキーには触れないので、実環境変数が常に優先されます。設置形態による挙動差が出ません。
  • getenv() を選ぶ理由。 createImmutable()$_ENV / $_SERVER を既に埋めているので、足りないのは getenv() 側だけ。だから $_ENV を回して getenv() へ写せば、既存の getenv() 直読みコードをそのまま生かせます。読み口の置換ゼロで挙動を揃えられるのが狙いです。

createUnsafeImmutable() に差し替える手もありますが、その場合 .env の値が 無条件に putenv() されるため、「実環境変数を優先したい」という要件を別途書く必要が出ます。今回は「実環境変数優先・.env は穴埋め」を明示したかったので、既定の createImmutable() + 手動ブリッジにしました。

なお、この設置フローには派生の落とし穴がもう一つあります。immutable リポジトリは 既にセットされたキーを上書きしない ため、インストーラが .env を書き直したあと同一プロセス内で読み直しても古い値が残ります。インストーラ側では、書いた直後に $_ENV / $_SERVER / putenv() を明示的に上書きしてから後続処理へ渡す、という対処を入れています。

// public_html/install/index.php(.env を書いた直後)
foreach ($env as $key => $value) {
    $_ENV[$key] = $value;
    $_SERVER[$key] = $value;
    putenv($key . '=' . $value);
}

「immutable は上書きしない」という v5 の性質が、リクエスト入口と設置処理の両方で顔を出したかたちです。

学び

  • ライブラリのメジャー更新は「環境変数の載る場所」を変えることがある。 phpdotenv は v4→v5 で getenv() / putenv() を既定から外し、$_ENV / $_SERVER のみになりました。getenv() 直読みのコードは、この一点だけで静かに既定値化します。
  • 読み口を1本化する。 理想は $_ENV(または設定ローダ)に寄せること。過渡期はエントリポイントの env ブリッジ1箇所で $_ENVgetenv() を吸収すると、散らばった getenv() を触らずに揃えられます。
  • 「実環境変数を上書きしない」を明示する。 .env が有る設置と、実環境変数だけの設置(Docker 等)の両方を1つのコードで通すには、「足りないキーだけ穴埋め」の条件が効きます。createUnsafe* で無条件に putenv() するときは、優先順位の要件が消えていないか確認しましょう。
  • 同じ .env でも設置形態で結果が割れる。 Docker(実環境変数)では露見せず、共有レンサバ(.env のみ)で初めて出る、という時間差バグになりがちです。設置形態ごとに一度は実機確認を。

一次資料

  • vlucas/phpdotenv v5.6.3 の実装
    • 既定アダプタが $_SERVER / $_ENV のみ(PutenvAdapter を含まない): vendor/vlucas/phpdotenv/src/Repository/RepositoryBuilder.phpDEFAULT_ADAPTERS
    • getenv() を載せるには createUnsafeImmutable() 等で PutenvAdapter を明示追加: vendor/vlucas/phpdotenv/src/Dotenv.php
  • 現象と対処(共有ホスティング Tier A 設置・#707 / PR #708)
    • env ブリッジ本体: public_html/index.php
    • getenv() 直読み側の例: src/Http/BasePath.php / src/Http/RuntimeServiceProvider.php / src/Media/MediaServiceProvider.php
    • $_SERVER / $_ENV 直読み側(既定でも効く設定ローダ): NENE2 ConfigLoader::readEnvironmentValues()
    • 設置時の immutable 上書き対処: public_html/install/index.php

関連記事

「共有レンタルサーバーの罠」シリーズの ⑤(最終回) です。


── 森 秀之(彩音インターナショナル) 🔗 ayane.co.jp

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?