【第6回】 カテゴリを考慮したランダム選択 - [Drupal非管理ファイル完全ガイド]チュートリアルシリーズ

Unmanaged Files in Drupal (Part 6): Category-Aware Random Selection
目次

編集者注:この記事は、Jeff Greenberg氏による「Drupal非管理ファイル完全ガイド」チュートリアルシリーズの第6弾及び最終回です。各記事では、Drupalにおける実践的なデータベース不要のファイル処理技術を探求してきました。この完結編では、カテゴリを考慮したランダム化を導入し、開発者が異なるサブフォルダーからユニークな画像を取得できるようにします。シリーズ全体では、概念からサービスとTwigテンプレートを使用したカスタムブロック出力までの進化を紹介しています。


このチュートリアルは、DrupalのUnmanaged Filesシリーズを締めくくるものです。パート1では、非管理ファイルとは何か、いつ使用すべきかを探りました。パート2では、カスタムモジュールの基盤を構築し、最初のファイルハンドラーを導入しました。パート3では、カスタムブロック内で非管理ファイルを動的にレンダリングしました。パート4では、その出力をTwigテンプレートに拡張し、パート5ではランダム選択ロジックを導入しました。この最終回であるパート6では、そのランダム性をカテゴリ認識型にします。public://segregated_maps内の個別のサブフォルダーから3つの異なる画像を選択し、各カテゴリが一度だけ表現されることを保証します。

フォルダー構造の例

以下のようなフォルダー構造を想定しています:

public://segregated_maps/
├── africa
├── antarctica
├── asia
├── australia
├── caribbean
├── central america
├── europe
├── mideast
├── north america
├── pacific islands
└── south america
図1. カテゴリ別に整理されたフォルダー構造

各フォルダーは地域カテゴリを表します。ハンドラーは、出力内の各画像が一意のカテゴリから来ることを保証し、バランスの取れたランダム化された3組を生成します。

RandomCategoryFileHandler.php

<?php
namespace Drupal\unmanaged_files\Service;
use Drupal\Core\File\FileSystemInterface;
/**
 * カテゴリを考慮した非管理ファイルのランダム選択を提供します。
 *
 * public://segregated_maps配下の異なるサブフォルダーから
 * 3つのランダムファイルを選択します。
 */
class RandomCategoryFileHandler {
  protected FileSystemInterface $fileSystem;
  protected string $basePath;
  public function __construct(FileSystemInterface $file_system) {
    $this->fileSystem = $file_system;
    $this->basePath = 'public://segregated_maps';
  }
  /**
   * 異なるカテゴリサブフォルダーからランダムファイルを選択します。
   */
  public function getCategoryConstrainedFiles(int $limit = 3): array {
    $selected = [];
    $base = $this->fileSystem->realpath($this->basePath);
    if (!$base || !is_dir($base)) {
      return $selected;
    }
    $dirs = glob($base . '/*', GLOB_ONLYDIR);
    if (empty($dirs)) {
      return $selected;
    }
    shuffle($dirs);
    foreach ($dirs as $dir) {
      $files = glob($dir . '/*.{jpg,jpeg,png,gif,webp}', GLOB_BRACE);
      if (!empty($files)) {
        $selected[] = $files[array_rand($files)];
      }
      if (count($selected) >= $limit) {
        break;
      }
    }
    shuffle($selected);
    return $selected;
  }
  /**
   * ランダム選択されたファイルのレンダリング可能な画像配列を返します。
   */
  public function getRenderableCategoryConstrainedFiles(int $limit = 3): array {
    $real_public = $this->fileSystem->realpath('public://');
    $base_url = '/sites/default/files';
    $files = $this->getCategoryConstrainedFiles($limit);
    $renderable = [];
    foreach ($files as $file) {
      $url = str_replace($real_public, $base_url, $file);
      $renderable[] = [
        '#theme' => 'image',
        '#uri' => $url,
        '#alt' => 'ランダムマップ画像',
        '#attributes' => ['loading' => 'lazy'],
      ];
    }
    return $renderable;
  }
}
図2. カテゴリ制約付きランダムファイルハンドラー

RandomCategoryFilesBlock.php

<?php
namespace Drupal\unmanaged_files\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
 * 異なるsegregated-mapカテゴリから3つのランダムファイルを表示します。
 *
 * @Block(
 *   id = "random_category_files_block",
 *   admin_label = @Translation("Random Category Files Block")
 * )
 */
class RandomCategoryFilesBlock extends BlockBase implements ContainerFactoryPluginInterface {
  protected $randomCategoryHandler;
  public function __construct(array $configuration, $plugin_id, $plugin_definition, $random_category_handler) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->randomCategoryHandler = $random_category_handler;
  }
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('unmanaged_files.random_category_file_handler')
    );
  }
  public function build() {
    $images = $this->randomCategoryHandler->getRenderableCategoryConstrainedFiles(3);
    return [
      '#theme' => 'unmanaged_files_category_block',
      '#images' => $images,
    ];
  }
}
図3. カテゴリランダムファイルブロックプラグイン

unmanaged_files.services.yml

services:
  unmanaged_files.handler:
    class: Drupal\unmanaged_files\Service\FileHandler
    arguments: ['@file_system','@stream_wrapper_manager']
  unmanaged_files.twig_extension:
    class: Drupal\unmanaged_files\Twig\UnmanagedFilesExtension
    arguments: ['@unmanaged_files.handler','@file_url_generator']
    tags:
      - { name: twig.extension }
  unmanaged_files.random_category_file_handler:
    class: Drupal\unmanaged_files\Service\RandomCategoryFileHandler
    arguments: ['@file_system']
図4. サービス定義ファイル

unmanaged_files.module

<?php
/**
 * Implements hook_theme().
 */
function unmanaged_files_theme() {
  return [
    'unmanaged_files_test' => [
      'variables' => [
        'image_url' => NULL,
        'uri' => NULL,
        'message' => NULL,
      ],
      'template' => 'unmanaged-files-test',
    ],
    'unmanaged_files_category_block' => [
      'variables' => [
        'images' => [],
      ],
      'template' => 'unmanaged-files-category-block',
    ],
  ];
}
図5. テーマフック定義

unmanaged-files-category-block.html.twig

{# 
/**
 * @file
 * Random Category Files Block用のテンプレート。
 */
#}
<div class="unmanaged-files-category-block">
  {% if images is not empty %}
    <div class="random-category-images">
      {% for image in images %}
        <div class="random-category-image">
          {{ image }}
        </div>
      {% endfor %}
    </div>
  {% else %}
    <p>{{ 'segregated mapフォルダーに画像が見つかりません。'|t }}</p>
  {% endif %}
</div>
図6. カテゴリブロック用Twigテンプレート

実装結果

「Random Category Files Block」を任意の領域またはカスタムレイアウトに配置すると、ブロックは3つのユニークな画像を表示します。それぞれがpublic://segregated_maps配下の異なる地域サブフォルダーから選ばれます。ページを読み込むたびに新しい組み合わせが生成され、重複は完全に回避されます。

これで、全6回にわたる「Drupal非管理ファイル完全ガイド」シリーズが完結しました。生のファイル検出から始まり、完全にテーマ化されたカテゴリ認識型ランダム化まで、管理ファイルのオーバーヘッドなしに、純粋なパフォーマンスと柔軟性を実現する方法を探求してきました。

Jeff Greenberg, The Accidental Coder

この記事は 「Unmanaged Files in Drupal (Part 6): Category-Aware Random Selection」の翻訳記事です。

カテゴリ