「パス結合ユーティリティ」で何を楽にしたいのか
まず、やりたいことのイメージからいきます。
ベース: "/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
PHPjoinPath と 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このときは、
parse_urlで URL を分解- パス部分だけを
joinAndNormalizePathで結合 - 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「スラッシュの有無」をテンプレート側で気にしなくていいのは、
地味ですがかなりのストレス軽減になります。
まとめ:今日からの「パス結合ユーティリティ」
大事なところだけ、ぎゅっとまとめます。
文字列連結でパスを組み立てると、スラッシュの重複や .. の解決で必ずつまずく。
パス結合は、「結合」と「正規化(// や . .. の整理)」をセットで考えると安定する。
ローカルパス用には joinPath と normalizePath、それをまとめた 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 をあちこちで書いているところ」があれば、そこがこのユーティリティの差し替えポイントです。
