LaravelでCDNを使う

こんにちは、hacktkです。
今回はLaravelでCDNを使うときの小技を書こうと思います。
※ LaravelからCDNにファイルを置くとかではなく、URL使い分けの話です。

背景

CDNを使う場合、サイトのドメインと異なるドメインを使うかと思います。

例1:ドメインの使い分け

サイトのドメイン CDNのドメイン
www.example.com cdn.example.com

しかしLaravelのassetヘルパーはこのドメイン部分を変更できません。
お手軽にやりたいだけなのに・・・。

解決策は用意されている

とりあえずassetヘルパーの実装を見てみると、こうなっています。

vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
<?php

if (! function_exists('asset')) {
    /**
     * Generate an asset path for the application.
     *
     * @param  string  $path
     * @param  bool    $secure
     * @return string
     */
    function asset($path, $secure = null)
    {
        return app('url')->asset($path, $secure);
    }
}

他のヘルパー関数と同様に、シンプルな処理です。
ではURLファサードのassetメソッドはというと、こうです。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
<?php

/**
 * Generate the URL to an application asset.
 *
 * @param  string  $path
 * @param  bool|null  $secure
 * @return string
 */
public function asset($path, $secure = null)
{
    if ($this->isValidUrl($path)) {
        return $path;
    }

    // Once we get the root URL, we will check to see if it contains an index.php
    // file in the paths. If it does, we will remove it since it is not needed
    // for asset paths, but only for routes to endpoints in the application.
    $root = $this->getRootUrl($this->getScheme($secure));

    return $this->removeIndex($root).'/'.trim($path, '/');
}

コメントに書いてある通り、 getRootUrl メソッドでroot URLが取得されています。
このgetRootUrlメソッド、実は仮引数が1つではありません。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
<?php

/**
 * Get the base URL for the request.
 *
 * @param  string  $scheme
 * @param  string  $root
 * @return string
 */
protected function getRootUrl($scheme, $root = null)
{
    if (is_null($root)) {
        if (is_null($this->cachedRoot)) {
            $this->cachedRoot = $this->forcedRoot ?: $this->request->root();
        }

        $root = $this->cachedRoot;
    }

    $start = Str::startsWith($root, 'http://') ? 'http://' : 'https://';

    return preg_replace('~'.$start.'~', $scheme, $root, 1);
}

はい、どうやら第2引数の $root に任意の文字列を渡せば、それが使われそうですね。
そしてその用途を想定した assetFrom というメソッドがちゃんと用意されています。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
<?php

/**
 * Generate the URL to an asset from a custom root domain such as CDN, etc.
 *
 * @param  string  $root
 * @param  string  $path
 * @param  bool|null  $secure
 * @return string
 */
public function assetFrom($root, $path, $secure = null)
{
    // Once we get the root URL, we will check to see if it contains an index.php
    // file in the paths. If it does, we will remove it since it is not needed
    // for asset paths, but only for routes to endpoints in the application.
    $root = $this->getRootUrl($this->getScheme($secure), $root);

    return $this->removeIndex($root).'/'.trim($path, '/');
}

これを使ったヘルパー関数でも実装すれば良さそうですね。

ちなみに、このassetFromメソッドは5.1で追加されたもののようです。 github.com

余談:ヘルパーの追加

Laravelのプロジェクトにヘルパー関数を追加する時ってみなさんどうやっているのでしょう?
私は app/Helpers/helpers.php に実装を置いて、composer.jsonで読み込ませました。

app/Helpers/helpers.php
<?php

if (! function_exists('cdn')) {
    /**
     * CDNのURLを生成する
     *
     * @param  string  $path
     * @param  bool    $secure
     * @return string
     */
    function cdn($path, $secure = null)
    {
        $root = config('project.cdn_url');  // 環境ごとのCDNパスを指定
        return app('url')->assetFrom($root, $path, $secure);
    }
}
composer.json
"autoload": {
    "classmap": [
        "database"
    ],
    "files": [
        "app/Helpers/helpers.php"
    ],
    "psr-4": {
        "App\\": "app/"
    }
},

他に良い方法があればぜひ知りたいです。

余談2

webエンジニア募集中です!

ゼロからサービスを開発し育てていきたい。そんなwebエンジニアを募集します