Drupalで既存のサービスを上書きする


Drupalのサービスとは

Drupalではカスタマイズ性を確保するためにD8から「サービス」という仕組み導入しています。
サービスはざっくりいうと、任意のクラスにサービス名という「名札」を付け、クラス名で直接呼び出すのを防ぐ仕組みです。

例えばDrupalのコアにはDrupal\Core\Language\LanguageManagerというクラスがあります。

このクラスを呼び出すには通常のPHPのやり方だと

use Drupal\Core\Language\LanguageManager;
$language_manager = new LanguageManager();

と書く必要がありますが、このクラスは/core/core.services.yml


language_manager:
    class: Drupal\Core\Language\LanguageManager
    arguments: ['@language.default']

のようにサービスとして登録されているので、

\Drupal::service('language_manager')

という風に呼び出すこともできます。コアやコントリビュートモジュールではほとんどの場合後者が使用されています。

サービスのメリット

これにはどのようなメリットがあるのでしょうか。

例えば、Drupalサイト開発中にLanguageManager内の処理を一部だけ変更する必要が出てきたとします。
もし、コアやコントリビュートモジュールでLanguageManagerを直接呼び出していた場合、処理を変更するのは困難になります。
単純な方法としては

  • LanguageManagerクラスそのものを上書きする
  • LanguageManagerに修正を加えたMyLanguageManagerを作り、呼び出し側でそちらを呼び出すように変更する

等があると思いますが、どちらにしてもコアやコントリビュートの一部に手を加えないといけません。画像は後者の方法で書き換える場合のイメージです。(白がカスタムモジュール、青はコアかコントリビュート。)コアやコントリビュートをがっつり書き換えてしまってますね。

逆にコアやコントリビュートモジュールのすべてがlanguage_managerサービスを介してLanguageManagerを呼び出していた場合、ServiceProviderというクラスを書くことでlanguage_managerの紐付け対象をMyLanguageManagerに差し替えることができます。こちらはすべてカスタムコードで完結していますね。

つまりサービス = クラスそのものやクラスの呼び出し元を修正することなくオーバーライドできるようにする仕組みということですね。

開発中に修正したいコアやコントリビュートのコードが出てきた場合は、まずはその箇所がサービスとして登録されているかどうかを確認するのがおすすめです。(登録されてたらラッキー!)

サービスを上書きする方法

先程説明したように、サービスを上書きするには

  • MyLanguageManager
  • MyModuleServiceProvider

という2つのクラスを書く必要があります。

MyLanguageManager

こちらは既存クラスを上書きするためのクラスです。

  • クラス名は適当でOK
  • ディレクトリも多分適当でいい(既存クラスのディレクトリを真似られる場合はその方がベター)

namespace Drupal\my_module;

use Drupal\Core\Language\LanguageManager;

class MyLanguageManager extends LanguageManager {

  // 上書きしたいメソッドだけ上書き.
  function getCurrentLanguage($type = LanguageInterface::TYPE_INTERFACE) {
    // 処理
  }

}

MyModuleServiceProvider

こちらはサービスクラスの切り替えを指示するためのクラスです。

  • クラス名は[モジュール名]ServiceProviderの形式にする
  • ディレクトリはmy_module/src/MyModuleServiceProvider.php
  • ServiceProviderBaseを継承する
namespace Drupal\my_module;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

/**
 * Language Managerクラスのサービスを上書きする.
 */
class MyModuleServiceProvider extends ServiceProviderBase {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    if ($container->hasDefinition('language_manager')) {
      $definition = $container->getDefinition('language_manager');
      $definition->setClass('Drupal\my_module\MyLanguageManager')
    }
  }
}

元のサービスは

arguments: ['@language.default']

の部分でlanguage.defaultという他のサービスを引数にしてますが、そちらは何も明示しなくても勝手に引き継がれるようです。

というわけで、Drupalの既存のサービスを上書きする方法でした。

参考: https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/altering-existing-services-providing-dynamic