【第2回】ランダム画像取得サービスの実装とテスト方法

unmanaged_files_in_drupal_building_a_random_file_handler_part_2
目次

理論からコードへ:ランダム画像を取得するシンプルなサービス

Drupalの非管理ファイルを扱うカスタムサービスの実装方法を解説します。ファイルハンドラーの構築からテストルートの作成まで、実際のコード例を通じて学べます。

このシリーズのパート1では、非管理ファイル(Drupalがエンティティとして追跡しないアセット)を使用することで、特定のユースケースをシンプルに保つための準備を行いました。パート2では、画像のフォルダーツリーをスキャンし、ランダムに1つのファイルを返す小さなDrupalサービスを作成します。カテゴリや制約はまだありません(それは後ほど追加します)。今回は、public://private://、Webルート外のディレクトリなどに配置されたファイルを、Drupalが管理せずに「認識」して使用できることを実証します。

今回実装するモジュールの構成

  • カスタムモジュール:unmanaged_files
  • サービス:unmanaged_files.handler(単一のメソッドgetRandomFile()を持つ)
  • (テスト用)選択した画像を表示するルート:/unmanaged-files/test

画像ファイルの準備と配置方法

使用する画像は、筆者のブログのリソースセクションまたはGitHubで入手できます。リンクは両方とも、この記事の最後に記載されています。

サーバーを使用している場合は、Filezillaのようなアプリケーションを使用してファイルを配置するか、scpやrsyncなどのユーティリティを使用できます。rsyncを使用する場合、構文の例はパート1に記載されています。

代わりに独自の画像を使用することもできます。その場合は、public://配下のフォルダー内にカテゴリフォルダーを作成して整理してください。独自の画像でも、サンプル画像でも、どのような配置方法であっても、画像は以下のような場所に配置する必要があります。

public://segregated_maps/<category>/<your-images>
# 実際のパスの例(環境により異なります): web/sites/default/files/segregated_maps/africa/algeria.png
図1. segregated_mapsのファイル構造

Drushを使ったモジュールの雛形作成

モジュールは手動でスキャフォールディングできます。私は以下に示すようにdrush generateコマンドを使用することを好みます。各行の最後でEnterキーを押します。

drush generate module
 Module name:
 ➤ Unmanaged Files
 Module machine name [unmanaged_files]:
 ➤ 
 Module description:
 ➤ Unmanaged files example
 Package [Custom]:
 ➤ 
 Dependencies (comma separated):
 ➤ 
 Would you like to create module file? [No]:
 ➤ 
 Would you like to create install file? [No]:
 ➤ 
 Would you like to create README.md file? [No]:
 ➤ 
図2. Drushによるモジュール生成

Drupalサービスコンテナへの登録

Drupalでは、ビジネスロジックは通常サービスとして実装されます。サービスは、コンテナに登録された再利用可能なPHPクラスであり、\Drupal::service()で直接呼び出すのではなく、コントローラー、プラグイン、または他のクラスに注入できます。

ここでは、unmanaged_files.handlerという新しいサービスを定義します。ここでは、Drupalが管理外のファイルを処理するFileHandlerクラスを見つけてインスタンス化する方法を提供するだけです。コアのfile_systemstream_wrapper_managerサービスを依存性として注入することで、クラスがpublic://パスを実際のファイルシステムの場所に解決できるようにします。

以下に示すように、web/modules/custom/unmanaged_files/unmanaged_files.services.ymlファイルを作成します。

services:
  unmanaged_files.handler:
    class: Drupal\unmanaged_files\Service\FileHandler
    arguments:
      - '@file_system'
      - '@stream_wrapper_manager'
図3. サービス定義

FileHandlerクラスの実装(基本機能)

ハンドラーは、このチュートリアルパートの中核です。これは、1つのパブリックメソッドgetRandomFile()を持つシンプルなPHPクラスです。このメソッドは、public://segregated_maps配下のすべてをスキャンし、見つかったファイルを収集して、ランダムに1つを返します。

キーポイント:

  • PHPのRecursiveDirectoryIteratorを使用して、すべてのフォルダーとサブフォルダーを走査します。
  • 各絶対ファイルシステムパスをDrupalストリームラッパーURI(public://...)に変換し直すことで、結果をDrupalのFile APIで使用できるようにします。
  • ファイルが見つからない場合、メソッドはNULLを返します。

まだ高度なことをすることではありません。Drupalが非管理ファイルを「認識」し、ランダムに1つを渡すことができることを実証するだけです。

以下に示すように、web/modules/custom/unmanaged_files/src/Service/FileHandler.phpファイルを作成します。

<?php
namespace Drupal\unmanaged_files\Service;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
final class FileHandler {
  public function __construct(
    private FileSystemInterface $fs,
    private StreamWrapperManagerInterface $swm,
  ) {}
  /**
   * public://segregated_maps配下のランダムなファイルURIを1つ返します。
   * 見つからない場合はNULLを返します。
   *
   * @return string|null  例: 'public://segregated_maps/africa/algeria.png'
   */
  public function getRandomFile(): ?string {
    $baseUri = 'public://segregated_maps';
    $basePath = $this->fs->realpath($baseUri);
    if (!$basePath || !is_dir($basePath)) {
      return NULL;
    }
    $files = [];
    $iter = new \RecursiveIteratorIterator(
      new \RecursiveDirectoryIterator($basePath, \FilesystemIterator::SKIP_DOTS)
    );
    foreach ($iter as $f) {
      if ($f->isFile()) {
        $abs = $f->getPathname(); // ディスク上の絶対パス
        // 絶対パスをpublic:// URIに変換し直します。
        // $basePathは$baseUriにマップされます。
        $rel = ltrim(substr($abs, strlen($basePath)), DIRECTORY_SEPARATOR);
        $files[] = $baseUri . '/' . str_replace(DIRECTORY_SEPARATOR, '/', $rel);
      }
    }
    if (!$files) {
      return NULL;
    }
    return $files[array_rand($files)];
  }
}
図4. 最小構成のファイルハンドラー

動作確認用のテストルート作成

ブラウザで実際の画像を確認できるように、小さなコントローラーとルートを追加します。以下に示すように、web/modules/custom/unmanaged_files/unmanaged_files.routing.ymlファイルを作成します。

unmanaged_files.test:
  path: '/unmanaged-files/test'
  defaults:
    _controller: '\Drupal\unmanaged_files\Controller\TestController::view'
    _title: 'Unmanaged files test'
  requirements:
    _permission: 'access content'
図5. テストルート定義

そして、以下に示すように、web/modules/custom/unmanaged_files/src/Controller/TestController.phpファイルを作成します。

<?php
namespace Drupal\unmanaged_files\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\unmanaged_files\Service\FileHandler;
use Drupal\Core\File\FileUrlGeneratorInterface;
final class TestController extends ControllerBase {
  public function __construct(
    private FileHandler $handler,
    private FileUrlGeneratorInterface $urlGen,
  ) {}
  public static function create(ContainerInterface $c): self {
    return new self(
      $c->get('unmanaged_files.handler'),
      $c->get('file_url_generator'),
    );
  }
  public function view(): array {
    $uri = $this->handler->getRandomFile();
    if (!$uri) {
      return [
        '#markup' => '<p>public://segregated_maps配下にファイルが見つかりません。</p>',
      ];
    }
    $url = $this->urlGen->generateAbsoluteString($uri);
    return [
      '#type' => 'container',
      '#attributes' => ['class' => ['unmanaged-files-test']],
      'info' => ['#markup' => '<p>選択: <code class="inline">' . $uri . '</code></p>'],
      'img'  => [
        '#type' => 'html_tag',
        '#tag' => 'img',
        '#attributes' => ['src' => $url, 'alt' => 'ランダムな非管理ファイル'],
      ],
      '#cache' => [
        'max-age' => 1,
      ],
    ];
  }
}
図6. テストコントローラー

デモ目的で、リフレッシュ時に画像がローテーションするように、非常に短いキャッシュ有効期間を設定しています。

モジュールの有効化と動作確認

drush en unmanaged_files -y
drush cr

次に、ブラウザでサイトを開き、以下のURLにアクセスします:

https://yoursite.example/unmanaged-files/test

画像の1つと、それが由来するURI(例:public://segregated_maps/africa/algeria.png)が表示されるはずです。ページを数回リフレッシュして、ハンドラーがファイルをローテーションしていることを確認してください。

コードと画像ファイルの入手

モジュールコードとサンプルマップ画像は、GitHubのパート2リリースページからダウンロードできます。テストを実行する前に、画像をweb/sites/default/files/segregated_mapsに解凍してください。

次のステップ

動作するハンドラーができたので、パート3~5では、サイトでこれをレンダリングする3つの方法(プリプロセス変数、ブロックプラグイン、Twig関数)を示します。パート6では、ファイルハンドラーに「カテゴリごとに1つ以下」の選択ルールを追加します。

この記事は 「Unmanaged Files in Drupal (Part 2): Building a Random File Handler」の翻訳記事です。

カテゴリ