concrete5のカスタマイズの強力な味方、サービスプロバイダとファサードを理解しよう

菱川拓郎
菱川拓郎

コンクリートファイブジャパン 菱川です。

concrete5 でカスタマイズを行う際、「コアを改変しない」というのがルールです。

このルールは concrete5 に限らず、オープンソースプロダクトをカスタマイズする際は、どのプロダクトを使っていても耳にするフレーズかと思います。

concrete5 のレガシーバージョンでは、concrete フォルダ内のファイルをひとつ上の階層に複製すると、そちらが優先されるというオーバーライドの仕組みがあり、コアファイルをカスタマイズする場合は、元のファイルには手を加えずに、オーバーライドしたファイルを変更するというルールになっていました。

バージョン7になっても、ブロックのテンプレートやJavaScriptなどは、ひとつ上の階層への移動ではなく concrete フォルダから階層構造を維持して application フォルダに移すようになったというルールの変更はあるものの、以前の通りオーバーライドが使えます。

しかし、concrete5 のコアAPIのカスタマイズを行いたい場合は、オーバーライドの仕組みは廃止され、新しい柔軟なサービスプロバイダ(IoCコンテナ)とファサード(Facade)を使うようになりました。この2つの仕組みは、現在開発者から非常に人気の高いPHPフレームワークの「Laravel」から輸入されました。これらのおかげで concrete5 のカスタマイズはコアを改変せずとも、相当深いところまで安全かつ柔軟に行うことができるのですが、concrete5 のソースコードを追いかけるのを難しくしている要因でもあります。今日はこの2つについて簡単に解説してみます。

 

サービスプロバイダ(IoCコンテナ)とは

concrete5 のコアのテンプレートを見ていると、下記のようなコードが含まれていることがあります。

<?php
// concrete/src/Localization/LocalizationServiceProvider.php

namespace Concrete\Core\Localization;

use Concrete\Core\Foundation\Service\Provider as ServiceProvider;

class LocalizationServiceProvider extends ServiceProvider
{
    public function register()
    {
        $singletons = array(
            'helper/localization/countries' => '\Concrete\Core\Localization\Service\CountryList',
            'helper/localization/states_provinces' => '\Concrete\Core\Localization\Service\StatesProvincesList',
            'helper/lists/countries' => '\Concrete\Core\Localization\Service\CountryList',
            'helper/lists/states_provinces' => '\Concrete\Core\Localization\Service\StatesProvincesList',
            
            // ここで定義されてますね。
            'helper/date' => '\Concrete\Core\Localization\Service\Date',

            'localization/countries' => '\Concrete\Core\Localization\Service\CountryList',
            'localization/states_provinces' => '\Concrete\Core\Localization\Service\StatesProvincesList',
            'localization/languages' => '\Concrete\Core\Localization\Service\LanguageList',
            'lists/countries' => '\Concrete\Core\Localization\Service\CountryList',
            'lists/states_provinces' => '\Concrete\Core\Localization\Service\StatesProvincesList',
            'date' => '\Concrete\Core\Localization\Service\Date',
        );

        foreach ($singletons as $key => $value) {
            $this->app->singleton($key, $value);
        }
    }
}

シングルトンとして登録されていますので、2回目以降このクラスを呼び出した際は、作成済みのインスタンスが返ってくる仕組みになっています。便利ですね。

そして、例えばこの Date クラスの挙動を変更したい場合、このキーとクラスの割り当てを変更することで、アプリケーション全体で自分で作った別のクラスに差し替えることができます。

差し替えの具体的な方法は、公式サイトのドキュメント「コアファイルをオーバーライドする」をご参照ください。


サービスプロバイダの一覧は、concrete/config/app.php に記載されています。

<?php
// concrete/config/app.php

return array(
...
    /**
     * Core Providers
     */
    'providers'           => array(
        // Router service provider
        'core_router' => 'Concrete\Core\Routing\RoutingServiceProvider',

        'core_file'                   => '\Concrete\Core\File\FileServiceProvider',
        'core_encryption'             => '\Concrete\Core\Encryption\EncryptionServiceProvider',
        'core_validation'             => '\Concrete\Core\Validation\ValidationServiceProvider',
        'core_localization'           => '\Concrete\Core\Localization\LocalizationServiceProvider',
        'core_multilingual'           => '\Concrete\Core\Multilingual\MultilingualServiceProvider',
        'core_feed'                   => '\Concrete\Core\Feed\FeedServiceProvider',
        'core_html'                   => '\Concrete\Core\Html\HtmlServiceProvider',
...

おまけとして、よく使うヘルパー系を中心に、サービスプロバイダキーと実クラス名との対照表を作りました。開発の際に、あれ?このキーに対応するクラスはなんだっけ?と思い出せない時にお使いください。検索できます。

concrete5 5.7.5.6 pick up list of services - gist


Facade(ファサード)とは

ファサードが何のために必要なのかを説明するのが難しいのですが、よく使うクラスは、静的な記法でサクッと書きたい!という用途に対応するためのものなのかなと思います。

例えば、concrete5 での処理中でログを残したい場合、下記のような記述でログの保存が行われています。

\Log::info('Foo');

この Log クラスは一体どこにあるのでしょうか?その答えは、concrete/config/app.php にあります。

<?php
// concrete/config/app.php

return array(
...
    /**
     * Core Facades
     */
    'facades'             => array(
        'Core'     => '\Concrete\Core\Support\Facade\Application',
        'Session'  => '\Concrete\Core\Support\Facade\Session',
        'Cookie'   => '\Concrete\Core\Support\Facade\Cookie',
        'Database' => '\Concrete\Core\Support\Facade\Database',
        'ORM'      => '\Concrete\Core\Support\Facade\DatabaseORM',
        'Events'   => '\Concrete\Core\Support\Facade\Events',
        'Route'    => '\Concrete\Core\Support\Facade\Route',
        'UserInfo' => '\Concrete\Core\Support\Facade\UserInfoFactory',
        
        // Log クラスへの静的なアクセスは、ここで指定されたクラスに届く
        'Log'      => '\Concrete\Core\Support\Facade\Log',
        'Image'    => '\Concrete\Core\Support\Facade\Image',
        'Config'   => '\Concrete\Core\Support\Facade\Config',
        'URL'      => '\Concrete\Core\Support\Facade\Url'
    ),
...

Log クラスへの静的なアクセスは、いったん Concrete\Core\Support\Facade\Log クラスを経由します。さらにこのクラスの中身も見てみましょう。

<?php
// concrete/src/Support/Facade/Log.php

namespace Concrete\Core\Support\Facade;

class Log extends Facade
{

    public static function getFacadeAccessor()
    {
        return 'log';
    }

}

非常にシンプルな中身ですね。Facade クラスを継承し、getFacadeAccessor メソッドで 'log' という文字列を返しています。この文字列は、先ほどご紹介したIoCコンテナ(サービスプロバイダ)に登録されているキーを指定しています。そして、この 'log' というキーには、 concrete/src/Logging/LoggingServiceProvider.php ファイル内で Concrete\Core\Logging\Logger クラスが割り当てられています。

この、サービスプロバイダを経由しているというのがポイントです。コードとしては、クラスの静的メソッドを呼び出しているように見えますが、裏側ではサービスプロバイダが対象となるクラスをインスタンス化して、インスタンスのメソッドを呼び出しているという仕組みになっています。

なんでそんな仕組みが必要なのかって?静的に1行で書けた方が便利でしょ!(ということだと思いますタブン)

というわけで、このファサードクラスも、サービスプロバイダでリバインドすることで実体のクラスを差し替えることが可能です。


少し複雑に見えますが、理解できるととても柔軟な仕組みですので、ぜひご活用ください!