Unmanaged Files - Part 5

Tutorial

Part 5: Enhancing the Twig Function for Unmanaged Files

In Part 4, we rendered unmanaged files through a Twig template defined in the module. In this part, we’ll expand that flexibility by upgrading the Twig function introduced earlier so that theme authors can decide whether they want a URL or a fully rendered image tag—right from Twig.

This keeps the syntax simple, eliminates the need for a separate helper function, and demonstrates how to safely return HTML from a custom Twig function using Drupal’s is_safe option.

Updated Twig Extension

We’ll update the Twig extension class to add two optional arguments, $format and $style. When called as {{ random_unmanaged_file('url') }} (or with no argument), it returns the file URL. When called as {{ random_unmanaged_file('img') }}, it returns a ready-to-render <img> tag. You can also pass an optional image style name as a second argument — for example, {{ random_unmanaged_file('img', 'thumbnail') }} — to apply Drupal’s image styles programmatically right from Twig.

<?php

namespace Drupal\unmanaged_files\Twig;

use Drupal\unmanaged_files\Service\FileHandler;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Drupal\image\Entity\ImageStyle;
use Drupal\Component\Utility\Html;

/**
 * Twig extension exposing unmanaged file helpers to templates.
 */
final class UnmanagedFilesExtension extends AbstractExtension {

  public function __construct(
    private FileHandler $handler,
    private FileUrlGeneratorInterface $urlGen,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function getFunctions(): array {
    return [
      new TwigFunction('random_unmanaged_file', [$this, 'getRandomFile'], ['is_safe' => ['html']]),
    ];
  }

  /**
   * Returns either a file URL or an <img> tag for a random unmanaged file.
   *
   * @param string $format
   *   'url' (default) to return the file URL, or 'img' to return an <img> tag.
   * @param string|null $style
   *   (optional) Image style machine name, used only when $format = 'img'.
   *
   * @return string|null
   *   URL or HTML string, or NULL if no files found.
   */
  public function getRandomFile(string $format = 'url', ?string $style = NULL): ?string {
    $uri = $this->handler->getRandomFile();
    if (!$uri) {
      return NULL;
    }

    // Start with the absolute file URL.
    $url = $this->urlGen->generateAbsoluteString($uri);

    // Apply image style if requested.
    if ($format === 'img' && $style) {
      if ($image_style = ImageStyle::load($style)) {
        $url = $image_style->buildUrl($uri);
      }
    }

    if ($format === 'img') {
      $safeUrl = Html::escape($url);
      return '<img src="' . $safeUrl . '" alt="Random unmanaged file">';
    }

    return $url;
  }

}

Figure 1

Using the Function

With the updated function in place, clear caches (ddev drush cr or drush cr), then you can use either form directly in any Twig template:

{# Example 1: URL only #}
<p>File URL: {{ random_unmanaged_file() }}</p>

{# Example 2: Rendered image #}
{{ random_unmanaged_file('img') }}

{# Example 3: Rendered image using a specific style #}
{{ random_unmanaged_file('img', 'thumbnail') }}

Figure 2

About is_safe

Normally, Drupal escapes all output from Twig functions to prevent unsafe HTML. The is_safe flag in our function declaration tells Twig that the returned string is intentionally safe to render as HTML. It’s important to use this only when you control and sanitize the output—in this case, an internal URL built by Drupal’s file API.

Why this approach?

This small enhancement gives theme developers maximum convenience:

  • URL form: useful for background images, CSS variables, or links.
  • Image tag form: ideal for inline display, banners, or decorative images.

This progression mirrors real-world flexibility—from fixed renderings to dynamic helpers that can be reused across themes or modules.

Compared to earlier parts:

  • Block Plugin (Part 3): configurable placement for site builders.
  • Twig Template (Part 4): fixed output structure for designers.
  • Twig Function (Part 5): drop-in, reusable helper for developers and themers.

In Part 6, we’ll expand the logic inside the handler to select more than one random file and ensure that no two picks come from the same category.

  • Drupal Planet