PHP Tips | 文字列処理:URL・パス系 – パス結合ユーティリティ

PHP PHP
スポンサーリンク

「パス結合ユーティリティ」で何を楽にしたいのか

まず、やりたいことのイメージからいきます。

ベース: "/var/www/html"
追加:   "logs/app.log"
→ "/var/www/html/logs/app.log"

ベース: "/var/www/html/"
追加:   "/logs/app.log"
→ "/var/www/html/logs/app.log"(スラッシュが重複しない)

ベース: "/var/www/html"
追加:   "../shared/config.php"
→ "/var/www/shared/config.php"(.. をちゃんと解決)

業務でよくあるのは、こんな場面です。

  • 設定ファイルで「ベースディレクトリ」と「相対パス」を分けて書いておき、実行時に結合したい。
  • ログディレクトリやテンポラリディレクトリの下に、さらにサブディレクトリやファイル名を足していきたい。
  • OS が変わっても(Windows/Linux)同じコードでパスを扱いたい。

ここを毎回 "{$base}/{$child}" みたいに手書きすると、
スラッシュの重複、.. の解決、末尾スラッシュの有無などで、すぐにバグります。

だからこそ、「パス結合専用の小さなユーティリティ」を持っておくと、
コード全体がかなり落ち着きます。


まずは「単純な結合」でつまずきポイントを知る

よくあるダメな書き方

初心者がやりがちなのは、こういう書き方です。

$base = '/var/www/html';
$child = 'logs/app.log';

$path = $base . '/' . $child;
// /var/www/html/logs/app.log
PHP

一見問題なさそうですが、少し条件が変わると途端に怪しくなります。

$base  = '/var/www/html/';
$child = 'logs/app.log';

$path = $base . '/' . $child;
// /var/www/html//logs/app.log  ← スラッシュが二重
PHP

逆に、子の方に先頭スラッシュが付いていると、こうなります。

$base  = '/var/www/html';
$child = '/logs/app.log';

$path = $base . '/' . $child;
// /var/www/html//logs/app.log
PHP

「スラッシュの重複」を毎回気にしながら書くのは、
正直しんどいです。

さらに「..」や「.」が入るとカオスになる

$base  = '/var/www/html';
$child = '../shared/config.php';

$path = $base . '/' . $child;
// /var/www/html/../shared/config.php
PHP

見た目としては「まあそうだよね」ですが、
本当に欲しいのは /var/www/shared/config.php です。

つまり、「結合」と同時に「正規化(../ の解決)」もしたくなります。


シンプルな「パス結合」ユーティリティを作る

スラッシュの重複を気にしないで済む joinPath

まずは、「スラッシュの重複」だけをきれいにする関数から始めます。

/**
 * パスを結合する(スラッシュの重複を解消)
 * 例: joinPath('/var/www/html', 'logs/app.log')
 *   → /var/www/html/logs/app.log
 */
function joinPath(string ...$segments): string
{
    $parts = [];

    foreach ($segments as $seg) {
        if ($seg === '') {
            continue;
        }
        $parts[] = $seg;
    }

    $path = implode('/', $parts);

    // 連続する / を 1 個にまとめる
    $path = preg_replace('#/+#', '/', $path);

    return $path;
}
PHP

使い方の例です。

echo joinPath('/var/www/html', 'logs/app.log');
// /var/www/html/logs/app.log

echo joinPath('/var/www/html/', '/logs/', '/app.log');
// /var/www/html/logs/app.log

echo joinPath('var', 'www', 'html');
// var/www/html
PHP

ここでのポイントは二つです。

一つ目は、「可変長引数(string ...$segments)」にしていること。
joinPath($a, $b) でも joinPath($a, $b, $c) でも書けるので、使い勝手がいいです。

二つ目は、「最後にまとめて /// にする」こと。
先頭や途中のスラッシュの有無を、呼び出し側でいちいち気にしなくて済みます。


「..」「.」も解決する正規化付きパス結合

normalizePath で「../」をちゃんと処理する

さっきの joinPath だけだと、../ はそのまま残ります。

echo joinPath('/var/www/html', '../shared/config.php');
// /var/www/html/../shared/config.php
PHP

これを「本当に意味のあるパス」にしたいなら、
.././ を解決する必要があります。

そのための小さな関数を用意します。

/**
 * パスを正規化する
 * - 連続する / を 1 個に
 * - . を削除
 * - .. を解決
 */
function normalizePath(string $path): string
{
    // 連続する / を 1 個に
    $path = preg_replace('#/+#', '/', $path);

    $segments = explode('/', $path);
    $stack    = [];

    foreach ($segments as $seg) {
        if ($seg === '' || $seg === '.') {
            continue;
        }
        if ($seg === '..') {
            array_pop($stack);
            continue;
        }
        $stack[] = $seg;
    }

    $normalized = '/' . implode('/', $stack);

    return $normalized === '/' ? $normalized : rtrim($normalized, '/');
}
PHP

動きのイメージです。

echo normalizePath('/var/www/html/../shared/config.php');
// /var/www/shared/config.php

echo normalizePath('//var///www/./html');
// /var/www/html
PHP

joinPath と normalizePath を組み合わせる

これで、「結合+正規化」を一発でやる関数が作れます。

/**
 * パスを結合して正規化する
 * 例: joinAndNormalizePath('/var/www/html', '../shared/config.php')
 *   → /var/www/shared/config.php
 */
function joinAndNormalizePath(string ...$segments): string
{
    $joined = joinPath(...$segments);
    return normalizePath($joined);
}
PHP

使い方の例です。

echo joinAndNormalizePath('/var/www/html', '../shared/config.php');
// /var/www/shared/config.php

echo joinAndNormalizePath('/var/www', 'html', 'logs/../tmp/app.log');
// /var/www/html/tmp/app.log
PHP

ここまでくると、「パス結合」に関してはかなり安心して書けるようになります。


URL 版の「パス結合」をどう考えるか

URL の「パス部分だけ」を結合したい場合

URL の場合も、「パス部分だけ」を結合したいことがあります。

$baseUrl = 'https://example.com/app';
$path    = 'assets/css/style.css';

// → https://example.com/app/assets/css/style.css
PHP

このときは、

  1. parse_url で URL を分解
  2. パス部分だけを joinAndNormalizePath で結合
  3. URL を再構築

という流れにします。

/**
 * ベース URL のパスに、追加パスを結合して新しい URL を作る
 */
function joinUrlPath(string $baseUrl, string ...$paths): string
{
    $parts = parse_url($baseUrl);

    if ($parts === false) {
        return $baseUrl;
    }

    $scheme   = $parts['scheme']   ?? null;
    $host     = $parts['host']     ?? null;
    $port     = $parts['port']     ?? null;
    $basePath = $parts['path']     ?? '';
    $query    = $parts['query']    ?? null;
    $fragment = $parts['fragment'] ?? null;
    $user     = $parts['user']     ?? null;
    $pass     = $parts['pass']     ?? null;

    // ベースのパスと追加パスを結合+正規化
    $newPath = joinAndNormalizePath($basePath, ...$paths);

    // URL を再構築
    $url = '';

    if ($scheme !== null) {
        $url .= $scheme . '://';
    }

    if ($user !== null) {
        $url .= $user;
        if ($pass !== null) {
            $url .= ':' . $pass;
        }
        $url .= '@';
    }

    if ($host !== null) {
        $url .= $host;
    }

    if ($port !== null) {
        $url .= ':' . $port;
    }

    $url .= $newPath;

    if ($query !== null && $query !== '') {
        $url .= '?' . $query;
    }

    if ($fragment !== null && $fragment !== '') {
        $url .= '#' . $fragment;
    }

    return $url;
}
PHP

使い方の例です。

echo joinUrlPath('https://example.com/app', 'assets/css/style.css');
// https://example.com/app/assets/css/style.css

echo joinUrlPath('https://example.com/app/', '/assets/', 'css/style.css');
// https://example.com/app/assets/css/style.css

echo joinUrlPath('https://example.com/app/index.php', '../assets/js/app.js');
// https://example.com/app/assets/js/app.js
PHP

「URL のパス結合」も、ローカルパスと同じ感覚で書けるようになります。


実務での使いどころのイメージ

設定ファイルで「ベースディレクトリ+相対パス」を分けておく

例えば、設定ファイルにこう書いてあるとします。

$config = [
    'base_dir'   => '/var/www/app',
    'log_dir'    => 'storage/logs',
    'cache_dir'  => 'storage/cache',
];
PHP

実際に使うときは、こう書けます。

$logFile   = joinAndNormalizePath($config['base_dir'], $config['log_dir'], 'app.log');
$cacheFile = joinAndNormalizePath($config['base_dir'], $config['cache_dir'], 'data.cache');
PHP

ベースディレクトリを変えたくなったときも、
設定の base_dir を変えるだけで済みます。

静的ファイルの URL を組み立てる

$baseUrl   = 'https://cdn.example.com/assets';
$cssUrl    = joinUrlPath($baseUrl, 'css/style.css');
$scriptUrl = joinUrlPath($baseUrl, 'js/app.js');
PHP

テンプレート側では、こう書くだけです。

<link rel="stylesheet" href="<?= $cssUrl ?>">
<script src="<?= $scriptUrl ?>"></script>
PHP

「スラッシュの有無」をテンプレート側で気にしなくていいのは、
地味ですがかなりのストレス軽減になります。


まとめ:今日からの「パス結合ユーティリティ」

大事なところだけ、ぎゅっとまとめます。

文字列連結でパスを組み立てると、スラッシュの重複や .. の解決で必ずつまずく。
パス結合は、「結合」と「正規化(//. .. の整理)」をセットで考えると安定する。
ローカルパス用には joinPathnormalizePath、それをまとめた joinAndNormalizePath を用意しておくと便利。
URL のパス結合は、parse_url で分解してからパスだけを joinAndNormalizePath に通し、最後に URL を再構築する。

核になるコードは、この3つです。

function joinPath(string ...$segments): string
{
    $parts = [];
    foreach ($segments as $seg) {
        if ($seg === '') continue;
        $parts[] = $seg;
    }
    $path = implode('/', $parts);
    return preg_replace('#/+#', '/', $path);
}

function normalizePath(string $path): string
{
    $path = preg_replace('#/+#', '/', $path);
    $segments = explode('/', $path);
    $stack = [];
    foreach ($segments as $seg) {
        if ($seg === '' || $seg === '.') continue;
        if ($seg === '..') { array_pop($stack); continue; }
        $stack[] = $seg;
    }
    $normalized = '/' . implode('/', $stack);
    return $normalized === '/' ? $normalized : rtrim($normalized, '/');
}

function joinAndNormalizePath(string ...$segments): string
{
    return normalizePath(joinPath(...$segments));
}
PHP

もし、あなたのコードの中で「$base . '/' . $child をあちこちで書いているところ」があれば、そこがこのユーティリティの差し替えポイントです。

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