【Laravel】configディレクトリの中で使われているenvのキーを全て抜き出す

 プログラムではよく実行環境に依存したパラメータを用意する必要があります。これには例えばDB(データベース)接続のため情報があります。本番環境とローカル環境のDB接続のために必要な情報が異なるため別々にパラメータを用意する必要があります。Laravelでは、こうした環境ごとの設定を.envファイルにまとめconfigファイルからenv()関数を通じて読み込むことで便利にパラメータを管理できます。パラメータにはしばしばGitのリモートリポジトリに生の値をアップロードしたくないものがあり、そういったものをGit管理から外しつつ管理しやすくするためにも使えます。

 configと.envは便利なのですが、しばしば.envに何を定義すべきかわからなくなることがあります。これはLaravelの用意した設定ファイルとは別に独自の設定を持っているプログラムで起きやすいです。こういった時に config 配下でどんな .env キーが使われているのかを確認したくなります。これを実現するスクリプトを紹介します。

 使うのは nikic/php-parser というライブラリです。これはPHP言語をパースしてPHPで扱いやすいデータにしてくれるライブラリです。これでenv関数とその引数を抜き出して整理します。nikic/php-parserは次のように composer からインストールできます。

nikic/PHP-Parser: A PHP parser written in PHP

composer require nikic/php-parser

 スクリプトの内容はざっくり次の通りです。

  1. config ディレクトリ配下の .php ファイルをすべて再帰的に読み込みます。
  2. 読み込んだ PHP ファイルの中から、AST(抽象構文木)を生成します。
  3. AST の中から env() 関数呼び出しを見つけ出し、その第1引数を取り出します。
  4. 引数が文字列リテラルなら、使用されている .env キーと判断してリストに追加します。
  5. 文字列リテラルでない場合(変数や関数など)、動的に決定されるキーとして別枠に記録します。

 これを実装すると次のようになります。

<?php

declare(strict_types=1);

use PhpParser\Error;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;

require_once __DIR__ . '/vendor/autoload.php';

/**
 * .env ファイルをパースしてキー一覧を返す
 *
 * @param string $envPath .env ファイルのパス
 * @return array<string, bool> envキー => true
 */
function loadDefinedEnvKeys(string $envPath): array
{
    $defined = [];
    if (!file_exists($envPath)) {
        return $defined;
    }

    $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        $line = trim($line);
        if ($line === '' || str_starts_with($line, '#')) {
            continue;
        }
        if (strpos($line, '=') !== false) {
            [$key] = explode('=', $line, 2);
            $defined[trim($key)] = true;
        }
    }
    return $defined;
}

/**
 * 絶対パスを ./ から始まる相対パスに変換
 *
 * @param string $path
 * @return string
 */
function toRelativePath(string $path): string
{
    $base = realpath(__DIR__);
    $real = realpath($path);
    if ($real !== false && str_starts_with($real, $base)) {
        return '.' . substr($real, strlen($base));
    }
    return $path;
}

/**
 * 表形式で1行出力を組み立てる
 *
 * @param  string  $status
 * @param  string  $key
 * @param  string  $file
 * @param  int     $line
 * @param  int     $keysMaxLen
 * @return string
 */
function formatTableRow(string $status, string $key, string $file, int $line, int $keysMaxLen): string
{
    $relPath = toRelativePath($file);
    return str_pad($status, 4) . str_pad($key, $keysMaxLen) . "{$relPath}:{$line}";
}

/**
 * env() 呼び出しを収集するためのノードビジター
 */
class EnvVisitor extends NodeVisitorAbstract
{
    private string $currentFile = '';
    private array $envKeys = [];       // key => list of ['file' => string, 'line' => int]
    private array $dynamicKeys = [];   // list of ['code' => string, 'file' => string, 'line' => int]

    public function setCurrentFile(string $file): void
    {
        $this->currentFile = $file;
    }

    public function enterNode(Node $node): void
    {
        if (
            $node instanceof Node\Expr\FuncCall &&
            $node->name instanceof Node\Name &&
            $node->name->toString() === 'env'
        ) {
            $args = $node->args;
            if (count($args) >= 1) {
                $arg = $args[0]->value;
                $line = $node->getLine();
                if ($arg instanceof Node\Scalar\String_) {
                    $key = $arg->value;
                    $this->envKeys[$key][] = [
                        'file' => $this->currentFile,
                        'line' => $line,
                    ];
                } else {
                    $printer = new PhpParser\PrettyPrinter\Standard();
                    $code = $printer->prettyPrintExpr($arg);
                    $this->dynamicKeys[] = [
                        'code' => $code,
                        'file' => $this->currentFile,
                        'line' => $line,
                    ];
                }
            }
        }
    }

    public function getEnvKeys(): array
    {
        return $this->envKeys;
    }

    public function getDynamicKeys(): array
    {
        return $this->dynamicKeys;
    }
}

// === 実行部分 ===

$baseDir     = __DIR__ . '/config';
$envFilePath = __DIR__ . '/.env';
$definedEnv  = loadDefinedEnvKeys($envFilePath);

$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
$visitor = new EnvVisitor();
$traverser = new NodeTraverser();
$traverser->addVisitor($visitor);

$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($baseDir));
foreach ($iterator as $file) {
    if ($file->isFile() && $file->getExtension() === 'php') {
        $code = file_get_contents($file->getPathname());
        $visitor->setCurrentFile($file->getPathname());
        try {
            $stmts = $parser->parse($code);
            $traverser->traverse($stmts);
        } catch (Error $e) {
            echo "Parse error in {$file->getPathname()}: {$e->getMessage()}\n";
        }
    }
}

// === 出力部分 ===

$envKeys = $visitor->getEnvKeys();
$dynamicKeys = $visitor->getDynamicKeys();
ksort($envKeys);

$envKeysMaxLen = max(array_map('strlen', array_keys($envKeys)));
echo "\n=== envキー一覧 ===\n";
echo str_pad('定義', 4) . str_pad('envキー名', $envKeysMaxLen) . "出現位置\n";
echo str_repeat('-', 70) . "\n";

foreach ($envKeys as $key => $usages) {
    $status = isset($definedEnv[$key]) ? '✅' : '❌';
    foreach ($usages as $usage) {
        echo formatTableRow($status, $key, $usage['file'], $usage['line'], $envKeysMaxLen) . "\n";
    }
}

if (!empty($dynamicKeys)) {
    $dynamicKeysMaxLen = max(array_map('strlen', array_keys($dynamicKeys)));
    echo "\n=== 動的なenvキー ===\n";
    echo str_pad('式', $dynamicKeysMaxLen) . "出現位置\n";
    echo str_repeat('-', 80) . "\n";
    foreach ($dynamicKeys as $entry) {
        $code = str_pad($entry['code'], $dynamicKeysMaxLen);
        $relPath = toRelativePath($entry['file']);
        echo "{$code}{$relPath}:{$entry['line']}\n";
    }
}

 このスクリプトを実行すると、`config` ディレクトリで使われている `.env` キーを一覧として表示します。以下のような出力が得られます。


=== envキー一覧 ===
定義envキー名                               出現位置
----------------------------------------------------------------------
❌ ABLY_KEY                                   ./config/broadcasting.php:47
✅ APP_DEBUG                                  ./config/app.php:46
❌ APP_DEBUG_DATETIME                         ./config/app.php:47
✅ APP_ENV                                    ./config/app.php:30
✅ APP_ENV                                    ./config/session.php:168
✅ APP_KEY                                    ./config/app.php:127
✅ APP_NAME                                   ./config/filesystems.php:51


=== 動的なenvキー ===
式出現位置
--------------------------------------------------------------------------------
get_xxx() . '_APP_NAME'./config/app.php:18

 envキー一覧には、文字列で静的に定義されたものが表示されます。これにより `.env` ファイルにどのキーを定義しておくべきか、あるいは既に不要なキーが存在していないかを把握できます。
 一方で、動的に生成されるキー(例えば、変数や関数によって組み立てられるキー)は自動では特定できないため、該当箇所のコードがそのまま出力されます。この部分については実際に動かすかコードを読むかするなりで確認する必要があります。

 Laravel の設定ファイルは柔軟であるがゆえにプロジェクトが大きくなるとどの `.env` キーが必要なのか把握しづらくなりがちです。今回のようなスクリプトを使うことで .env に何が必要かある程度把握できます。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG