【PHP】相対パスを絶対パス(URL)に変換する

スクレイピング系の処理を組んでいる人から「古い環境でも動くコードはあるか？」という質問を受けました。

~~内心「それぐらいググれよカス」と思いつつ、~~ググってみると意外とまともに動くコードが見つからなかったため、気分転換に書いてみました。

--------------------------------------------------------------------------------

## コード

都合上、PHP5.2辺りでも動くように書いてます。
未検証ですが、4系でもstripos()の代替となる関数を用意すれば動くかも？

```php
function pathToUrl($pPath, $pUrl)
{
    $path = trim($pPath);    // 変換対象パス
    $url = trim($pUrl);      // 基準URL

    //-- 変換不要
    if ($path === '') { return $url; }

    if (stripos($path, 'http://') === 0 ||
        stripos($path, 'https://') === 0 ||
        stripos($path, 'mailto:') === 0 ||
        stripos($path, 'tel:') === 0) { return $path; }

    //-- #anchor
    if (strpos($path, '#') === 0) { return $url . $path; }

    //-- 基準URLを分解
    $urlAry = explode('/', $url);
    if (!isset($urlAry[2])) { return false; }

    //-- //path
    if (strpos($path, '//') === 0) { return $urlAry[0] . $path; }

    //-- 基準URLのHOME(scheme://host)
    $urlHome = $urlAry[0] . '//' . $urlAry[2];

    //-- 基準URLのパス
    if (!$pathBase = parse_url($url, PHP_URL_PATH)) { $pathBase = '/'; }

    //-- ?query
    if (strpos($path, '?') === 0) { return $urlHome . $pathBase . $path; }

    //-- /path
    if (strpos($path, '/') === 0) { return $urlHome . $path; }

    //-- ./path or ../path
    $pathBaseAry = array_filter(explode('/', $pathBase), 'strlen');
    if (strpos(end($pathBaseAry), '.') !== false) { array_pop($pathBaseAry); }

    foreach (explode('/', $path) as $pathElem) {
        if ($pathElem === '.') { continue; }
        if ($pathElem === '..') { array_pop($pathBaseAry); continue; }
        if ($pathElem !== '') { $pathBaseAry[] = $pathElem; }
    }

    return (substr($path, -1) === '/') ? $urlHome . '/' . implode('/', $pathBaseAry) . '/'
                                       : $urlHome . '/' . implode('/', $pathBaseAry);
}
```

### 動作テスト

```php
$url = 'http://user:pass@example.com/base/path/here/index.php?p=q';

$testAry = array(
    'URL' => 'https://www.google.com/gmail/',
    'mailto:' => 'mailto:info@example.com',
    'クエリのみ' => '?aaa=bbb&ccc=ddd',
    'パス＋クエリ' => 'new.php?aaa=bbb',
    'アンカーのみ' => '#anchor',
    'パス＋アンカー' => 'new.html#anchor',
    '絶対パス1' => '/new/path/abs.html',
    '絶対パス2' => '//new/path/abs.html',
    '相対パス1' => './',
    '相対パス2' => './new/path/rel2/',
    '相対パス3' => './new/path/rel3.html',
    '相対パス4' => '../../rel4.html',
    '相対パス5' => '.././/new/../././rel5.html',
);

foreach ($testAry as $title => $test) {
    echo "{$title}('{$test}') ⇒ ", pathToUrl($test, $url), "\n";
}
```

### テスト結果

```
URL('https://www.google.com/gmail/') ⇒ https://www.google.com/gmail/
mailto:('mailto:info@example.com') ⇒ mailto:info@example.com

クエリのみ('?aaa=bbb&ccc=ddd') ⇒ http://user:pass@example.com/base/path/here/index.php?aaa=bbb&ccc=ddd
パス＋クエリ('new.php?aaa=bbb') ⇒ http://user:pass@example.com/base/path/here/new.php?aaa=bbb

アンカーのみ('#anchor') ⇒ http://user:pass@example.com/base/path/here/index.php?p=q#anchor
パス＋アンカー('new.html#anchor') ⇒ http://user:pass@example.com/base/path/here/new.html#anchor

絶対パス1('/new/path/abs.html') ⇒ http://user:pass@example.com/new/path/abs.html
絶対パス2('//new/path/abs.html') ⇒ http://new/path/abs.html

相対パス1('./') ⇒ http://user:pass@example.com/base/path/here/
相対パス2('./new/path/rel2/') ⇒ http://user:pass@example.com/base/path/here/new/path/rel2/
相対パス3('./new/path/rel3.html') ⇒ http://user:pass@example.com/base/path/here/new/path/rel3.html
相対パス4('../../rel4.html') ⇒ http://user:pass@example.com/base/rel4.html
相対パス5('.././/new/../././rel5.html') ⇒ http://user:pass@example.com/base/path/rel5.html
```

--------------------------------------------------------------------------------

## Webで見つけたコードは何故上手く動かない？

1. そもそもPHPの古いバージョンに対応していない
	- これは仕方ないですね。私もPHP5.4以前の話とかはもう流石に忘れました(笑)
<br><br>
1. user や pass の指定のあるURLを考慮できていない
	- user:pass@ の部分が欠落してしまうコードが多いようです。
<br><br>
1. クエリパラメータを含むURLを考慮できていない
	- ?p=q の部分です。<br>クエリパラメータを含むURLを元に変換すると、結果がめちゃくちゃになるコードが多いようです。
<br><br>
1. mailto: や tel: を考慮できていない
	- 他にも色々ありますので、ここはもう少し丁寧に書いた方が良いと思います。<br>今回はこれで十分との事で手を抜きました。
<br><br>
1. クエリやアンカーのみを渡された時の事を考慮できていない。
	- '?name=value' や '#anchor' を渡された時です。
<br><br>
1. 相対パスのちょっと変わった書き方に対応していない
	- '.././/new/../././rel5.html' というのは流石にお遊びですが、<br>'./' という書き方に対応できていないコードが多いようです。

4と5については、関数に渡す前にパラメータを先にチェックした方が良いでしょうね。
今回は使う人の用途に合わせたため、このような形になっています。

--------------------------------------------------------------------------------

### 余談

少し気になったのは、正規表現を多用しているコードが多く見つかったという点です。

スクレイピングの性質上、関数の呼び出し回数がかなりの数になる事も多いと思いますので、できるだけ正規表現のようなコストのかかる処理は避けた方が良いのではないでしょうか。

また、パスにも様々な書き方がありますので、意外と見落としがありますね。
私も気分転換でサクっと書くつもりだったのですが、意外と時間がかかりました。

まだ見落としているところが色々とあるかもしれませんので、参考の際はご注意ください。
ご指摘などがございましたらどうぞお気軽に。