Unmanaged Files - Part 6

Tutorial
Tutorial Series • Part 6 of 6 • by Jeff Greenberg (The Accidental Coder)

This tutorial concludes the Unmanaged Files in Drupal series. In Part 1 we explored what unmanaged files are and when to use them. Part 2 built the foundation for our custom module and introduced the first file handler. Part 3 rendered unmanaged files dynamically within a custom block. Part 4 extended that output to Twig templates, and Part 5 introduced random selection logic. In this final installment, Part 6, we make that randomness category-aware—selecting three distinct images from separate subfolders within public://segregated_maps, ensuring each category is represented only once.

Folder Structure


public://segregated_maps/
├── africa
├── antarctica
├── asia
├── australia
├── caribbean
├── central america
├── europe
├── mideast
├── north america
├── pacific islands
└── south america
    

Each folder represents a region category. The handler guarantees that each image in the output comes from a unique category, producing a balanced, randomized trio.

RandomCategoryFileHandler.php


<?php

namespace Drupal\unmanaged_files\Service;

use Drupal\Core\File\FileSystemInterface;

/**
 * Provides category-aware random selection of unmanaged files.
 *
 * Picks three random files from distinct subfolders under public://segregated_maps.
 */
class RandomCategoryFileHandler {

  protected FileSystemInterface $fileSystem;
  protected string $basePath;

  public function __construct(FileSystemInterface $file_system) {
    $this->fileSystem = $file_system;
    $this->basePath = 'public://segregated_maps';
  }

  /**
   * Selects random files from distinct category subfolders.
   */
  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;
  }

  /**
   * Returns renderable image arrays for the random selections.
   */
  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' => 'Random map image',
        '#attributes' => ['loading' => 'lazy'],
      ];
    }

    return $renderable;
  }

}
    

RandomCategoryFilesBlock.php


<?php

namespace Drupal\unmanaged_files\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Displays three random files from distinct segregated-map categories.
 *
 * @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,
    ];
  }

}
    

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']
    

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',
    ],
  ];
}
    

unmanaged-files-category-block.html.twig


{#
/**
 * @file
 * Template for 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>{{ 'No images found in segregated map folders.'|t }}</p>
  {% endif %}
</div>
    

Result

When you place the “Random Category Files Block” in any region or custom layout, the block displays three unique images—each from a different regional subfolder under public://segregated_maps. Every page load produces a new mix while avoiding duplicates.

That concludes the six-part Unmanaged Files in Drupal series: from raw file discovery to fully themed, category-aware randomization—no managed-file overhead, pure performance and flexibility.

— Jeff Greenberg, The Accidental Coder

  • Drupal Planet