Unmanaged Files in Drupal (Part 5): Enhancing the Twig Function

From Template to Function: Give Theme Developers Full Control Over File Output
Part 5: Enhancing the Twig Function for Unmanaged Files

Editor’s note: This article is the fifth part of Jeff Greenberg’s ongoing “Unmanaged Files in Drupal” tutorial series. Each installment builds on the last, exploring different ways to manage and render files without database overhead. In this part, Jeff enhances the Twig function introduced earlier, giving developers and themers more flexibility directly within Drupal templates.

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.

Note: The vision of this web portal is to help promote news and stories around the Drupal community and promote and celebrate the people and organizations in the community. We strive to create and distribute our content based on these content policy. If you see any omission/variation on this please reach out to us at #thedroptimes channel on Drupal Slack and we will try to address the issue as best we can.

Related Organizations

Upcoming Events

Latest Opportunities