Unmanaged Files - Part 6
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