プログラムではよく実行環境に依存したパラメータを用意する必要があります。これには例えば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
スクリプトの内容はざっくり次の通りです。
-
configディレクトリ配下の.phpファイルをすべて再帰的に読み込みます。 - 読み込んだ PHP ファイルの中から、AST(抽象構文木)を生成します。
-
AST の中から
env()関数呼び出しを見つけ出し、その第1引数を取り出します。 -
引数が文字列リテラルなら、使用されている
.envキーと判断してリストに追加します。 - 文字列リテラルでない場合(変数や関数など)、動的に決定されるキーとして別枠に記録します。
これを実装すると次のようになります。
<?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 に何が必要かある程度把握できます。