[PHP]stream_resolve_include_path完全ガイド|インクルードパスを解決してファイルの存在確認とオートローダーを安全に実装する

PHP

はじめに

PHPで requireinclude を使う際、ファイルが include_path のどこに存在するかを事前に確認したいケースがあります。realpath() はカレントディレクトリやフルパスを基準に解決しますが、include_path の各ディレクトリを順番に探してはくれません。

stream_resolve_include_path() は、include_path の設定に従ってファイルを探し、最初に見つかったフルパスを返す関数です。include が実際にどのファイルを読み込むかを、実際に読み込む前に確認できる唯一の手段です。


関数の基本情報

項目内容
関数名stream_resolve_include_path()
対応バージョンPHP 5.3.2 以降
返り値string(フルパス)/ false(見つからない場合)
カテゴリストリーム関数

構文

stream_resolve_include_path(string $filename): string|false

パラメータ

パラメータ説明
$filenamestring解決するファイル名またはパス

返り値

  • stringinclude_path 内で最初に見つかったファイルのフルパス
  • falseinclude_path のどのディレクトリにも見つからなかった場合

include_path の仕組みとの関係

<?php

// include_path の確認
echo get_include_path() . PHP_EOL;
// 例: .:/usr/local/lib/php:/var/www/html/lib

// stream_resolve_include_path() は include_path を左から順に探す
// 1. カレントディレクトリ(.)
// 2. /usr/local/lib/php
// 3. /var/www/html/lib
// …の順で filename を探し、最初に見つかったフルパスを返す

$resolved = stream_resolve_include_path('config/database.php');
if ($resolved !== false) {
    echo "見つかりました: {$resolved}" . PHP_EOL;
} else {
    echo "include_path 内に見つかりません" . PHP_EOL;
}

realpath() との違い

比較項目stream_resolve_include_path()realpath()
探索範囲include_path の全ディレクトリを順に探す指定パスそのもの(または CWD からの相対パス)
include_path を考慮
ファイルが存在しない場合false を返すfalse を返す
シンボリックリンクの解決
用途include/require 前の事前確認絶対パスの正規化・存在確認
<?php

// realpath() は include_path を考慮しない
$path = '/absolute/path/to/file.php';
var_dump(realpath('config.php'));                        // カレントディレクトリのみ確認
var_dump(stream_resolve_include_path('config.php'));     // include_path 全体を確認

基本的な使い方

<?php

// include_path を設定
set_include_path(
    '/var/www/html/lib' . PATH_SEPARATOR .
    '/var/www/html/vendor' . PATH_SEPARATOR .
    get_include_path()
);

// ファイルの解決
$file = stream_resolve_include_path('Database/Connection.php');

if ($file !== false) {
    echo "解決済みパス: {$file}" . PHP_EOL;
    // → /var/www/html/lib/Database/Connection.php
    require $file;
} else {
    throw new \RuntimeException('Database/Connection.php が見つかりません');
}

実践的なクラスベースの活用例


例1:インクルードパスリゾルバー(IncludePathResolver)

include_path の各エントリを管理し、ファイルの解決・存在確認・パス一覧取得をまとめて行うリゾルバークラスです。

<?php

class IncludePathResolver
{
    /**
     * ファイルを include_path から解決して返す
     */
    public function resolve(string $filename): string|false
    {
        return stream_resolve_include_path($filename);
    }

    /**
     * 複数ファイルを一括解決して返す
     *
     * @param  string[] $filenames
     * @return array<string, string|false> ファイル名 => 解決済みパス or false
     */
    public function resolveAll(array $filenames): array
    {
        $results = [];
        foreach ($filenames as $filename) {
            $results[$filename] = stream_resolve_include_path($filename);
        }
        return $results;
    }

    /**
     * ファイルが include_path に存在するか確認する
     */
    public function exists(string $filename): bool
    {
        return stream_resolve_include_path($filename) !== false;
    }

    /**
     * 現在の include_path エントリを配列で返す
     *
     * @return string[]
     */
    public function getPaths(): array
    {
        return explode(PATH_SEPARATOR, get_include_path());
    }

    /**
     * include_path にディレクトリを追加する
     */
    public function addPath(string $dir, bool $prepend = false): void
    {
        $current = get_include_path();
        if ($prepend) {
            set_include_path($dir . PATH_SEPARATOR . $current);
        } else {
            set_include_path($current . PATH_SEPARATOR . $dir);
        }
    }

    /**
     * include_path を一時的に変更してコールバックを実行する
     */
    public function withPath(string|array $paths, callable $callback): mixed
    {
        $original = get_include_path();

        $newPaths = is_array($paths) ? implode(PATH_SEPARATOR, $paths) : $paths;
        set_include_path($newPaths);

        try {
            return $callback($this);
        } finally {
            set_include_path($original);
        }
    }

    /**
     * 全パスエントリとそこに含まれるファイル数をレポートする
     */
    public function report(): void
    {
        echo "=== Include Path Report ===" . PHP_EOL;
        foreach ($this->getPaths() as $i => $path) {
            $realPath = realpath($path);
            $exists   = $realPath !== false;
            $count    = $exists ? count(glob($realPath . '/*.php') ?: []) : 0;
            echo sprintf(
                "  [%d] %s%s%s\n",
                $i + 1,
                $path,
                $exists ? '' : ' (存在しない)',
                $exists ? " ({$count} .php files)" : ''
            );
        }
    }
}

// 使用例
$resolver = new IncludePathResolver();

// 現在の include_path を確認
$resolver->report();

// 複数ファイルを一括解決
$files = $resolver->resolveAll([
    'autoload.php',
    'Nonexistent/File.php',
]);

foreach ($files as $name => $path) {
    echo "{$name}: " . ($path !== false ? $path : '見つからない') . PHP_EOL;
}

// 一時的にパスを変更して処理
$resolver->withPath(['/tmp', '/var/www/html/lib'], function ($r) {
    echo "一時パス内の解決: " . var_export($r->resolve('test.php'), true) . PHP_EOL;
});

例2:PSR-0 互換オートローダー(Psr0Autoloader)

stream_resolve_include_path() を使って include_path を探索する PSR-0 互換のクラスオートローダーです。名前空間区切り・アンダースコア区切りの両方に対応します。

<?php

class Psr0Autoloader
{
    /** @var string[] 追加の検索ディレクトリ */
    private array $directories = [];

    /** @var array<string, string> ロード済みクラスのキャッシュ */
    private array $loadedCache = [];

    /** @var array<string, string> 失敗キャッシュ(存在しないクラス) */
    private array $failedCache = [];

    public function register(bool $prepend = false): void
    {
        spl_autoload_register([$this, 'load'], true, $prepend);
    }

    public function unregister(): void
    {
        spl_autoload_unregister([$this, 'load']);
    }

    /**
     * 追加の検索ディレクトリを登録する
     */
    public function addDirectory(string $dir): void
    {
        $real = realpath($dir);
        if ($real !== false && !in_array($real, $this->directories, true)) {
            $this->directories[] = $real;
        }
    }

    /**
     * クラス名をファイルパスに変換して include_path から解決する
     */
    public function load(string $className): bool
    {
        // ロード済み or 失敗済みキャッシュを確認
        if (isset($this->loadedCache[$className])) {
            return true;
        }
        if (isset($this->failedCache[$className])) {
            return false;
        }

        $filePath = $this->classToPath($className);

        // 追加ディレクトリを一時的に include_path に加えて探索
        $result = $this->resolveWithDirectories($filePath);

        if ($result !== false) {
            require $result;
            $this->loadedCache[$className] = $result;
            return true;
        }

        $this->failedCache[$className] = true;
        return false;
    }

    /**
     * クラス名を PSR-0 形式のファイルパスに変換する
     * 例: Vendor\Package\ClassName → Vendor/Package/ClassName.php
     *     Vendor_Package_ClassName → Vendor/Package/ClassName.php
     */
    private function classToPath(string $className): string
    {
        $className = ltrim($className, '\\');

        // 名前空間部分とクラス名を分離
        $namespaceParts = [];
        if (($nsEnd = strrpos($className, '\\')) !== false) {
            $namespace      = substr($className, 0, $nsEnd);
            $classOnly      = substr($className, $nsEnd + 1);
            $namespaceParts = explode('\\', $namespace);
        } else {
            $classOnly = $className;
        }

        // クラス名部分のアンダースコアをディレクトリ区切りに変換(PSR-0 仕様)
        $classOnly = str_replace('_', DIRECTORY_SEPARATOR, $classOnly);

        $parts = array_merge($namespaceParts, [$classOnly]);
        return implode(DIRECTORY_SEPARATOR, $parts) . '.php';
    }

    /**
     * 追加ディレクトリを include_path に加えて解決する
     */
    private function resolveWithDirectories(string $filePath): string|false
    {
        // まず現在の include_path で探す
        $resolved = stream_resolve_include_path($filePath);
        if ($resolved !== false) return $resolved;

        // 追加ディレクトリを順に試す
        foreach ($this->directories as $dir) {
            $fullPath = $dir . DIRECTORY_SEPARATOR . $filePath;
            if (file_exists($fullPath)) {
                return realpath($fullPath);
            }
        }

        return false;
    }

    public function getLoadedClasses(): array { return array_keys($this->loadedCache); }
    public function getLoadedPaths(): array   { return $this->loadedCache; }
}

// 使用例
$loader = new Psr0Autoloader();
$loader->addDirectory('/var/www/html/src');
$loader->addDirectory('/var/www/html/lib');
$loader->register();

// これ以降、クラスを使用すると自動的にファイルが探索・読み込まれる
// use App\Database\Connection;
// $conn = new Connection(); → src/App/Database/Connection.php を探索

echo "PSR-0 オートローダー登録完了" . PHP_EOL;

// ファイルパス変換のテスト
$reflection = new \ReflectionMethod($loader, 'classToPath');
$reflection->setAccessible(true);

$tests = [
    'Vendor\\Package\\ClassName',
    'Vendor_Package_ClassName',
    'SimpleClass',
];
foreach ($tests as $class) {
    echo "{$class} → " . $reflection->invoke($loader, $class) . PHP_EOL;
}

// 出力:
// Vendor\Package\ClassName → Vendor/Package/ClassName.php
// Vendor_Package_ClassName → Vendor/Package/ClassName.php
// SimpleClass → SimpleClass.php

例3:設定ファイルローダー(ConfigFileLoader)

stream_resolve_include_path() で設定ファイルの存在を確認してから安全に読み込み、環境別オーバーライドを自動適用する設定ローダーです。

<?php

class ConfigFileLoader
{
    private array  $loaded  = [];
    private array  $configs = [];
    private string $environment;

    public function __construct(
        string $environment = 'production',
        array  $includePaths = []
    ) {
        $this->environment = $environment;

        // 設定ファイル用のパスを追加
        foreach ($includePaths as $path) {
            $current = get_include_path();
            set_include_path($current . PATH_SEPARATOR . $path);
        }
    }

    /**
     * 設定ファイルを include_path から探して読み込む
     *
     * @throws \RuntimeException ファイルが見つからない場合
     */
    public function load(string $name): array
    {
        if (isset($this->configs[$name])) {
            return $this->configs[$name];
        }

        // ベース設定ファイルを解決
        $baseName = "config/{$name}.php";
        $basePath = stream_resolve_include_path($baseName);

        if ($basePath === false) {
            throw new \RuntimeException(
                "設定ファイル '{$baseName}' が include_path 内に見つかりません。\n" .
                "include_path: " . get_include_path()
            );
        }

        $config = require $basePath;
        $this->loaded[$name] = $basePath;

        // 環境別オーバーライドを探す
        $envName = "config/{$name}.{$this->environment}.php";
        $envPath = stream_resolve_include_path($envName);

        if ($envPath !== false) {
            $override = require $envPath;
            $config   = array_replace_recursive($config, $override);
            $this->loaded["{$name}.{$this->environment}"] = $envPath;
        }

        $this->configs[$name] = $config;
        return $config;
    }

    /**
     * 設定ファイルが存在するか確認する(読み込まずに)
     */
    public function exists(string $name): bool
    {
        return stream_resolve_include_path("config/{$name}.php") !== false;
    }

    /**
     * 複数の設定ファイルを一括でロードする
     *
     * @param  string[] $names
     * @return array<string, array>
     */
    public function loadMany(array $names): array
    {
        $results = [];
        foreach ($names as $name) {
            try {
                $results[$name] = $this->load($name);
            } catch (\RuntimeException $e) {
                $results[$name] = [];
                trigger_error($e->getMessage(), E_USER_WARNING);
            }
        }
        return $results;
    }

    public function getLoadedFiles(): array { return $this->loaded; }

    public function printLoadedReport(): void
    {
        echo "=== ロード済み設定ファイル ===" . PHP_EOL;
        foreach ($this->loaded as $name => $path) {
            echo "  [{$name}] {$path}" . PHP_EOL;
        }
    }
}

// 使用例(設定ファイルをインメモリでシミュレート)
// 実際には /var/www/html/config/database.php などに配置する

// include_path にテスト用ディレクトリを追加
$testConfigDir = sys_get_temp_dir() . '/config_test';
@mkdir($testConfigDir . '/config', 0755, true);

// ベース設定ファイルを作成
file_put_contents(
    $testConfigDir . '/config/database.php',
    '<?php return ["host" => "localhost", "port" => 3306, "name" => "mydb"];'
);

// 本番用オーバーライド
file_put_contents(
    $testConfigDir . '/config/database.production.php',
    '<?php return ["host" => "db.example.com", "name" => "prod_db"];'
);

$loader = new ConfigFileLoader(
    environment: 'production',
    includePaths: [$testConfigDir]
);

$dbConfig = $loader->load('database');
print_r($dbConfig);
// Array ( [host] => db.example.com [port] => 3306 [name] => prod_db )

$loader->printLoadedReport();

// クリーンアップ
array_map('unlink', glob($testConfigDir . '/config/*.php'));
rmdir($testConfigDir . '/config');
rmdir($testConfigDir);

例4:テンプレートリゾルバー(TemplateResolver)

stream_resolve_include_path() を使って、テーマ・モジュール・デフォルトの優先順でテンプレートファイルを探索するリゾルバーです。WordPressのテンプレート階層のような仕組みを実装できます。

<?php

class TemplateResolver
{
    /** @var string[] テンプレート検索ディレクトリ(優先順) */
    private array $searchPaths = [];
    private string $extension;

    /** @var array<string, string|false> 解決結果のキャッシュ */
    private array $cache = [];

    public function __construct(
        array  $searchPaths = [],
        string $extension   = '.php'
    ) {
        $this->searchPaths = $searchPaths;
        $this->extension   = $extension;
    }

    /**
     * テンプレート名を解決して最初に見つかったパスを返す
     * 候補を優先順に試し、最初にヒットしたものを使う
     *
     * @param  string[] $candidates テンプレート名の優先順リスト
     */
    public function resolve(array $candidates): string|false
    {
        $cacheKey = implode('|', $candidates);
        if (array_key_exists($cacheKey, $this->cache)) {
            return $this->cache[$cacheKey];
        }

        $original = get_include_path();
        // 検索パスを include_path の先頭に追加
        $newPath = implode(PATH_SEPARATOR, $this->searchPaths)
                 . PATH_SEPARATOR . $original;
        set_include_path($newPath);

        $found = false;
        foreach ($candidates as $name) {
            $filename = $name . $this->extension;
            $resolved = stream_resolve_include_path($filename);
            if ($resolved !== false) {
                $found = $resolved;
                break;
            }
        }

        set_include_path($original);
        $this->cache[$cacheKey] = $found;
        return $found;
    }

    /**
     * テンプレートを解決してレンダリング(出力バッファリング)
     *
     * @throws \RuntimeException テンプレートが見つからない場合
     */
    public function render(array $candidates, array $vars = []): string
    {
        $path = $this->resolve($candidates);
        if ($path === false) {
            throw new \RuntimeException(
                "テンプレートが見つかりません: " . implode(', ', $candidates)
            );
        }

        extract($vars, EXTR_SKIP);
        ob_start();
        require $path;
        return ob_get_clean();
    }

    /**
     * テンプレート階層を展開して候補リストを生成する
     * 例: page-contact → ['page-contact', 'page', 'index']
     */
    public function buildHierarchy(string $template): array
    {
        $parts      = explode('-', $template);
        $candidates = [];

        while (!empty($parts)) {
            $candidates[] = implode('-', $parts);
            array_pop($parts);
        }
        $candidates[] = 'index';
        return array_unique($candidates);
    }

    public function clearCache(): void { $this->cache = []; }
}

// 使用例(テンプレートファイルをインメモリでシミュレート)
$themeDir   = sys_get_temp_dir() . '/theme';
$defaultDir = sys_get_temp_dir() . '/default';

foreach ([$themeDir, $defaultDir] as $dir) {
    @mkdir($dir, 0755, true);
}

// デフォルトテンプレート
file_put_contents($defaultDir . '/index.php',   '<?php echo "デフォルト: index";');
file_put_contents($defaultDir . '/page.php',    '<?php echo "デフォルト: page";');

// テーマ側でオーバーライド(page のみ)
file_put_contents($themeDir . '/page.php',      '<?php echo "テーマ: page";');
file_put_contents($themeDir . '/page-about.php','<?php echo "テーマ: page-about";');

$resolver = new TemplateResolver(
    searchPaths: [$themeDir, $defaultDir],
    extension:   '.php'
);

// テンプレート階層の解決
$hierarchy = $resolver->buildHierarchy('page-contact');
echo "階層候補: " . implode(', ', $hierarchy) . PHP_EOL;
// → page-contact, page, index

$path = $resolver->resolve($hierarchy);
echo "解決済み: " . ($path ?: '見つからない') . PHP_EOL;
// → .../theme/page.php (page-contact はなく page がテーマにある)

// クリーンアップ
array_map('unlink', glob($themeDir   . '/*.php'));
array_map('unlink', glob($defaultDir . '/*.php'));
rmdir($themeDir);
rmdir($defaultDir);

例5:インクルードパスファイルスキャナー(IncludePathScanner)

stream_resolve_include_path() を活用して include_path 内の PHP ファイルを再帰スキャンし、クラス・インターフェース・トレイトの一覧を生成するスキャナーです。

<?php

class IncludePathScanner
{
    /**
     * include_path 内の全 .php ファイルを探索して返す
     *
     * @return array<string, string> 相対パス => フルパス
     */
    public function scanPhpFiles(): array
    {
        $files = [];
        foreach (explode(PATH_SEPARATOR, get_include_path()) as $dir) {
            $real = realpath($dir);
            if ($real === false || !is_dir($real)) continue;

            foreach ($this->glob($real, '*.php') as $fullPath) {
                $relative = ltrim(str_replace($real, '', $fullPath), DIRECTORY_SEPARATOR);
                // stream_resolve_include_path で正式な解決済みパスを確認
                $resolved = stream_resolve_include_path($relative);
                if ($resolved === $fullPath) {
                    $files[$relative] = $fullPath;
                }
            }
        }
        return $files;
    }

    /**
     * 特定のファイルが include_path のどのディレクトリから解決されるか確認する
     */
    public function whichPath(string $filename): ?string
    {
        foreach (explode(PATH_SEPARATOR, get_include_path()) as $dir) {
            $real = realpath($dir);
            if ($real === false) continue;
            $candidate = $real . DIRECTORY_SEPARATOR . $filename;
            if (file_exists($candidate)) {
                // stream_resolve_include_path で確定
                $resolved = stream_resolve_include_path($filename);
                if ($resolved !== false && realpath($resolved) === realpath($candidate)) {
                    return $real;
                }
            }
        }
        return null;
    }

    /**
     * シャドーイングされているファイル(複数パスに同名が存在する)を検出する
     *
     * @return array<string, string[]> ファイル名 => [優先パス, シャドーされたパス, ...]
     */
    public function findShadowed(): array
    {
        $seen     = [];
        $shadowed = [];

        foreach (explode(PATH_SEPARATOR, get_include_path()) as $dir) {
            $real = realpath($dir);
            if ($real === false || !is_dir($real)) continue;

            foreach ($this->glob($real, '*.php') as $fullPath) {
                $relative = ltrim(str_replace($real, '', $fullPath), DIRECTORY_SEPARATOR);
                if (isset($seen[$relative])) {
                    $shadowed[$relative][] = $fullPath;
                } else {
                    $seen[$relative]     = $fullPath;
                    $shadowed[$relative] = [$fullPath];
                }
            }
        }

        return array_filter($shadowed, fn($paths) => count($paths) > 1);
    }

    private function glob(string $dir, string $pattern): array
    {
        $results = [];
        $files   = glob($dir . DIRECTORY_SEPARATOR . $pattern) ?: [];
        foreach ($files as $file) {
            $results[] = $file;
        }
        // サブディレクトリは再帰しない(シンプルな1階層のみ)
        return $results;
    }
}

// 使用例
$scanner = new IncludePathScanner();

// include_path 内でのファイル解決元ディレクトリを確認
$dir = $scanner->whichPath('autoload.php');
echo "autoload.php の解決元: " . ($dir ?? '見つからない') . PHP_EOL;

// シャドーイングの検出(デバッグ用途)
$shadowed = $scanner->findShadowed();
if (empty($shadowed)) {
    echo "シャドーイングされているファイルはありません" . PHP_EOL;
} else {
    echo "シャドーイング検出:" . PHP_EOL;
    foreach ($shadowed as $file => $paths) {
        echo "  {$file}:" . PHP_EOL;
        foreach ($paths as $i => $path) {
            echo "    [" . ($i === 0 ? "優先" : "シャドー") . "] {$path}" . PHP_EOL;
        }
    }
}

例6:条件付きインクルードマネージャー(ConditionalIncludeManager)

stream_resolve_include_path() で存在確認してから、条件・バージョン・環境に応じてファイルを選択的にインクルードするマネージャーです。

<?php

class ConditionalIncludeManager
{
    /** @var array<string, bool> インクルード済みファイルの記録 */
    private array $included = [];

    /** @var array<string, string> エイリアス => 解決済みパス */
    private array $aliases = [];

    /**
     * ファイルが include_path に存在する場合のみ require する
     *
     * @return string|false 読み込んだファイルのパス、存在しなければ false
     */
    public function requireIfExists(string $filename): string|false
    {
        $path = stream_resolve_include_path($filename);
        if ($path === false) return false;

        if (!isset($this->included[$path])) {
            require $path;
            $this->included[$path] = true;
        }
        return $path;
    }

    /**
     * 候補リストから最初に見つかったファイルをインクルードする
     * (バージョン別・環境別のフォールバックに便利)
     *
     * @param  string[] $candidates
     * @return string|false
     */
    public function requireFirstFound(array $candidates): string|false
    {
        foreach ($candidates as $filename) {
            $result = $this->requireIfExists($filename);
            if ($result !== false) return $result;
        }
        return false;
    }

    /**
     * PHP バージョンに応じて互換ファイルを選択してインクルードする
     */
    public function requireForPhpVersion(
        string $baseFile,
        array  $versionMap
    ): string|false {
        // バージョンを降順でソートして最初にマッチするものを使う
        krsort($versionMap);
        foreach ($versionMap as $minVersion => $filename) {
            if (PHP_VERSION_ID >= (int)str_replace('.', '', $minVersion) * 100) {
                return $this->requireIfExists($filename);
            }
        }
        return $this->requireIfExists($baseFile);
    }

    /**
     * エイリアスを登録して別名でインクルードできるようにする
     */
    public function alias(string $alias, string $filename): void
    {
        $path = stream_resolve_include_path($filename);
        if ($path !== false) {
            $this->aliases[$alias] = $path;
        }
    }

    public function requireAlias(string $alias): string|false
    {
        if (!isset($this->aliases[$alias])) return false;
        $path = $this->aliases[$alias];
        if (!isset($this->included[$path])) {
            require $path;
            $this->included[$path] = true;
        }
        return $path;
    }

    public function getIncluded(): array { return array_keys($this->included); }
    public function isIncluded(string $path): bool { return isset($this->included[realpath($path) ?: $path]); }
}

// 使用例
$manager = new ConditionalIncludeManager();

// PHP バージョン別のポリフィルを自動選択
// $manager->requireForPhpVersion('compat/base.php', [
//     '8.1' => 'compat/php81.php',
//     '8.0' => 'compat/php80.php',
//     '7.4' => 'compat/php74.php',
// ]);

// 存在すれば読み込む(オプション機能の有効化)
$loaded = $manager->requireIfExists('extensions/optional-feature.php');
echo "optional-feature: " . ($loaded !== false ? "読み込み済み: {$loaded}" : "スキップ") . PHP_EOL;

// 優先順フォールバック
$loaded = $manager->requireFirstFound([
    'cache/redis.php',
    'cache/memcached.php',
    'cache/file.php',   // フォールバック
]);
echo "キャッシュドライバー: " . ($loaded !== false ? $loaded : "見つからない") . PHP_EOL;

echo "読み込みファイル数: " . count($manager->getIncluded()) . PHP_EOL;

例7:インクルードパス統合テストヘルパー(IncludePathTestHelper)

テスト実行時に stream_resolve_include_path() を使ってインクルードパスの設定を検証し、テストの前提条件(必須ファイルの存在・パスの正確性)をアサーションするヘルパークラスです。

<?php

class IncludePathAssertionException extends \RuntimeException {}

class IncludePathTestHelper
{
    private array $originalPaths = [];
    private array $tempDirs      = [];

    /**
     * テスト開始時に include_path を保存する
     */
    public function setUp(): void
    {
        $this->originalPaths = explode(PATH_SEPARATOR, get_include_path());
    }

    /**
     * テスト終了時に include_path を復元して一時ディレクトリを削除する
     */
    public function tearDown(): void
    {
        set_include_path(implode(PATH_SEPARATOR, $this->originalPaths));

        foreach ($this->tempDirs as $dir) {
            array_map('unlink', glob($dir . '/*') ?: []);
            @rmdir($dir);
        }
        $this->tempDirs = [];
    }

    /**
     * 指定ファイルが include_path から解決できることをアサートする
     *
     * @throws IncludePathAssertionException
     */
    public function assertResolvable(string $filename, ?string $message = null): string
    {
        $path = stream_resolve_include_path($filename);
        if ($path === false) {
            throw new IncludePathAssertionException(
                $message ?? "'{$filename}' が include_path から解決できません。\n" .
                            "include_path: " . get_include_path()
            );
        }
        return $path;
    }

    /**
     * 指定ファイルが include_path から解決できないことをアサートする
     *
     * @throws IncludePathAssertionException
     */
    public function assertNotResolvable(string $filename, ?string $message = null): void
    {
        $path = stream_resolve_include_path($filename);
        if ($path !== false) {
            throw new IncludePathAssertionException(
                $message ?? "'{$filename}' が予期せず解決されました: {$path}"
            );
        }
    }

    /**
     * 指定ファイルが期待するディレクトリから解決されることをアサートする
     *
     * @throws IncludePathAssertionException
     */
    public function assertResolvesFrom(string $filename, string $expectedDir): void
    {
        $path = $this->assertResolvable($filename);
        $dir  = dirname(realpath($path));
        $expected = realpath($expectedDir);

        if ($dir !== $expected) {
            throw new IncludePathAssertionException(
                "'{$filename}' の解決元が期待と異なります。\n" .
                "期待: {$expected}\n実際: {$dir}"
            );
        }
    }

    /**
     * テスト用の一時ファイルを include_path 配下に作成する
     */
    public function createTempFile(string $relativePath, string $content = '<?php'): string
    {
        $dir = sys_get_temp_dir() . '/php_ipath_test_' . uniqid();
        mkdir(dirname($dir . '/' . $relativePath), 0755, true);
        file_put_contents($dir . '/' . $relativePath, $content);

        $this->tempDirs[] = $dir;
        set_include_path(get_include_path() . PATH_SEPARATOR . $dir);

        return $dir . '/' . $relativePath;
    }
}

// 使用例
$helper = new IncludePathTestHelper();
$helper->setUp();

// テスト用ファイルを動的に作成
$helper->createTempFile('MyLib/Utility.php', '<?php class Utility {}');
$helper->createTempFile('MyLib/Helper.php',  '<?php class Helper {}');

// アサーションのテスト
try {
    $path = $helper->assertResolvable('MyLib/Utility.php');
    echo "OK: Utility.php → {$path}" . PHP_EOL;

    $helper->assertNotResolvable('MyLib/Nonexistent.php');
    echo "OK: Nonexistent.php は解決不可(期待通り)" . PHP_EOL;

    // 存在するファイルに assertNotResolvable をかけると例外
    $helper->assertNotResolvable('MyLib/Utility.php');

} catch (IncludePathAssertionException $e) {
    echo "アサーション失敗: " . $e->getMessage() . PHP_EOL;
} finally {
    $helper->tearDown();
    echo "include_path 復元完了" . PHP_EOL;
}

// 出力:
// OK: Utility.php → /tmp/php_ipath_test_xxxx/MyLib/Utility.php
// OK: Nonexistent.php は解決不可(期待通り)
// アサーション失敗: 'MyLib/Utility.php' が予期せず解決されました: ...
// include_path 復元完了

関連する関数との比較

関数役割
stream_resolve_include_path()include_path を探索してファイルのフルパスを解決
realpath()指定パスのシンボリックリンクを解決して正規化された絶対パスを返す
file_exists()ファイルまたはディレクトリが存在するか確認include_path は考慮しない)
get_include_path()現在の include_path設定文字列を取得
set_include_path()include_path変更する
spl_autoload_resolve_include_path()SPL オートローダー向けのパス解決(spl_autoload 拡張)

注意点とベストプラクティス

1. カレントディレクトリ(.)の扱いに注意

include_path.(カレントディレクトリ)が含まれている場合、スクリプト実行ディレクトリに同名ファイルがあると予期しない解決が起きます。本番環境では .include_path から除外することを推奨します。

// カレントディレクトリを除外した include_path を設定
$paths = array_filter(
    explode(PATH_SEPARATOR, get_include_path()),
    fn($p) => $p !== '.'
);
set_include_path(implode(PATH_SEPARATOR, $paths));

2. Composer 環境では include_path より autoload が推奨

現代の PHP 開発では Composer のオートローダーが主流です。stream_resolve_include_path() は Composer 非対応のレガシーコードや、include_path ベースのフレームワークとの統合に特に有効です。

3. 結果はキャッシュして使い回す

ファイルシステムへのアクセスを伴うため、ループ内で繰り返し呼び出す場合はキャッシュを活用しましょう。

// NG:毎回ファイルシステムにアクセス
foreach ($classes as $class) {
    $path = stream_resolve_include_path($class . '.php');
}

// OK:解決済みパスをキャッシュ
$cache = [];
foreach ($classes as $class) {
    $cache[$class] ??= stream_resolve_include_path($class . '.php');
}

4. false と空文字を区別する

返り値は string|false です。empty() ではなく === false で判定しましょう。

$path = stream_resolve_include_path('file.php');

// NG
if (!$path) { ... }       // 空文字列も false 扱いになる

// OK
if ($path === false) { ... }

まとめ

ポイント内容
基本動作include_path を左から順に探索し、最初に見つかったファイルのフルパスを返す
realpath() との違いinclude_path の全エントリを探索する点が最大の違い
返り値見つかれば string(フルパス)、見つからなければ false
判定方法=== false で厳密に判定する
活用シーンオートローダー・設定ファイルローダー・テンプレートリゾルバー・条件付きインクルード・テストヘルパー
注意点.(カレントディレクトリ)の順序・Composer 環境との住み分け・キャッシュ活用

stream_resolve_include_path()include_path という PHPの古典的な機能を安全に活用するための関数です。include が実際に読み込むファイルを事前に確定できる唯一の方法として、レガシーコードの保守・フレームワーク開発・テスト環境の構築まで幅広い場面で活躍します。

タイトルとURLをコピーしました