Problem/Motivation

Each theme can pick its theme engine, but Twig still renders much of every page. So an alternative engine can't take over the templates it was written for. That applies to a non-Twig engine, to one built for Single Directory Components (SDC), and to a Twig-based one such as a Symfony UX Twig Components-style engine.

Since #1685492: Convert theme engines into services (11.3.0), theme engines are tagged services implementing ThemeEngineInterface, so the engine-path override proposed originally here no longer applies.

Three hardcoded couplings remain:

  1. ThemeManager::render() always renders module-provided templates with twig, whatever the active theme's engine.
  2. SDC discovery only looks for a {machine_name}.twig template (ComponentPluginManager::alterDefinition()).
  3. SDC rendering builds Twig source in ComponentElement and renders it as an inline_template, which calls the twig service directly, with no engine involved. Other front-end markup is built with inline_template too.

Steps to reproduce

  1. Register a theme engine service implementing ThemeEngineInterface, and a theme that uses it.
  2. Visit a page. The engine only receives templates from the active theme and its base themes. Module templates the theme doesn't override, and every SDC, are still rendered by Twig.

Proposed resolution

Each engine renders the templates it provides, and Twig renders everything else.

This is opt-in and additive. An engine that doesn't implement the new interface gets exactly the templates it gets today, and existing Twig sites and contrib engines keep working unchanged. Full replacement of Twig with no fallback (this issue's original proposal) isn't the goal: existing engines would suddenly receive module and contrib templates they were never written for, which would be a breaking change. An engine that wants more control claims more templates.

Target: 12.1, since 12.0 is in feature freeze.

Definition of done: a theme on a non-Twig engine renders a page that mixes engine-owned and Twig-owned templates, including SDCs, with no errors.

Remaining tasks

  1. Agree on the direction here, with Theme API and SDC maintainer review, before the child issues move forward.
  2. Core changes, which together unblock an alternative engine:
  3. Themeability follow-ups, not needed for the definition of done:

User interface changes

None.

Introduced terminology

Template ownership: a theme engine declaring which templates it provides, and therefore renders.

API changes

Additive only; details are in the child issues.

  • A new interface that theme engines can implement to declare the templates they own. It's optional, and engines that don't implement it behave as today.
  • The theme registry records which engine resolves each template, instead of assuming Twig for module templates.
  • SDC discovery and rendering go through the resolving engine.

Data model changes

None. The theme registry, which is cached data, gains the resolving engine for each entry.

Release notes snippet

None. Just planning here.

Issue fork drupal-3525011

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

d34dman created an issue. See original summary.

d34dman’s picture

This issue has been created after a discussion in slack https://drupal.slack.com/archives/C1BMUQ9U6/p1747392281444149

@chx, @alexpott, @mstrelan

d34dman’s picture

Not loading default twig engine leads to a fatal error because module defined templates are exclusively processed by twig engine.

https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Co...

   // Generate the output using a template.
    $render_function = 'twig_render_template';
    $extension = '.html.twig';

    // The theme engine may use a different extension and a different
    // renderer.
    $theme_engine = $active_theme->getEngine();
    if (isset($theme_engine)) {
      if ($info['type'] != 'module') {
        if (function_exists($theme_engine . '_render_template')) {
          $render_function = $theme_engine . '_render_template';
        }
        $extension_function = $theme_engine . '_extension';
        if (function_exists($extension_function)) {
          $extension = $extension_function();
        }
      }
    }

As you can see, ThemeManager::render explicitly calls twig_render_template. It makes sense as templates defined my modules are "most" likely to be using twig.

quietone’s picture

Version: 11.2.x-dev » 11.1.x-dev
quietone’s picture

Version: 11.1.x-dev » 11.x-dev

Trying again.

If this problem was discovered on a version of Drupal that is not 11.x, add that information in the issue summary and leave the version at 11.x. In Drupal core changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies. Also mentioned on the version section of the list of issue fields documentation.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

matthand’s picture

The largest impacts I'm seeing is that the SDC rendering bypassing theme engine altogether. It creates a string that is a basic Twig template, then sends that to inline_template for rendering which doesn't even call the theme engine, it calls Twig directly.

1. SDC doesn't render itself — it writes a Twig string and hands it off.

When you use #type: component, ComponentElement::preRenderComponent() calls a private method, generateComponentTemplate(), which literally builds a Twig source string at runtime:

$template = '{# This template was dynamically generated by single-directory components #}' . PHP_EOL;
$template .= sprintf('{%% embed \'%s\' %%}', $id);
// ...for each slot:
$template .= "  {% block $slot_name %}" . PHP_EOL . "    {{ $slot_name }}" . PHP_EOL . "  {% endblock %}" . PHP_EOL;
$template .= '{% endembed %}' . PHP_EOL;

Props and slots get folded into $context, and the result is stuffed into another render element:

$element['inline-template'] = [
  '#type' => 'inline_template',
  '#template' => $inline_template,
  '#context' => $props,
];

2. inline_template renders by calling the Twig service directly — not the theme engine.

InlineTemplate::preRenderInlineTemplate() is where it actually executes:

$environment = \Drupal::service('twig');
$markup = $environment->renderInline($element['#template'], $element['#context']);

That's \Drupal::service('twig') — the concrete Twig service — not ThemeManager::render() or anything that goes through ThemeEngineInterface. There's no engine-dispatch step here at all.

matthand’s picture

The proposed resolution here predates #1685492: Convert theme engines into services, which converted theme engines to tagged services in 11.3.0. There is no longer an engine path to override. What actually blocks a non-Twig engine today is three hardcoded couplings: module templates are forced to 'twig' in ThemeManager::render(), SDC discovery only looks for .twig files, and SDC rendering generates Twig source and hands it to inline_template.

I'd like to propose turning this into a Plan issue with these children:

Core fixes (these unblock an alternative engine)

  1. #3625132: Add a template ownership interface so theme engines can tell core which templates they provide (blocks the others)
  2. #3625133: Record the resolving theme engine in the registry and stop hardcoding Twig for module templates (after 1)
  3. #3625134: Make Single Directory Component template discovery engine-aware (after 1)
  4. #3625137: Delegate Single Directory Component rendering to the active theme engine (after 3)

Themeability follow-ups

Approach: Each engine renders the templates it provides. Twig renders everything else. All changes are additive, and existing Twig sites and contrib engines keep working unchanged. Since 12.0 is in feature freeze, the realistic target is 12.1.

Definition of done: A theme on a non-Twig engine renders a page that mixes engine-owned and Twig-owned templates, with no errors.

@d34dman, are you OK with recategorizing this as a Plan issue? I'd be glad to help drive it. Feedback on #3625132: Add a template ownership interface so theme engines can tell core which templates they provide is most useful first, since everything else depends on it.

nicxvan’s picture

Please follow the ai policy you linked in your comment, specifically:

Don't just copy and paste the output verbatim.

Use your own words, and be concise.

matthand’s picture

Hi @nicxvan, I revised the plan many times before posting. This issue to allow theme engines aside from Twig is posted as one issue, but really should be about 7 issues, which is why the plan is so long. This change touches many subsystems. I'll break it down and file the related issues, then edit the comment to be less verbose.

matthand’s picture

@nicxvan, Added child issues to help break down the changes needed to deliver this issue to allow a different theme engine. And updated the plan comment above to be brief per your feedback. Since the theme engine is already a proper service, following through with the rest of the change looks very doable to me.

nicxvan’s picture

Thanks!

That is much more reasonable!

d34dman’s picture

@matthand,

Thanks for pushing this forward.

are you OK with recategorizing this as a Plan issue?

I don't mind. But I think the first round of discussion (before jumping into Planning technical details) would be, If this is the accepted way moving forward. I am guilty of proposing technical details myself (i had that information from the patch i was running to make it happen back then), so sorry for miss-directing the issue.

The proposed solution (in issue description) would be a breaking change. A breaking change as in, if anybody has written a theme engine, it had to deal with only the templates inside a theme. This meant the surface was a set which came from enabled theme and their base themes. Once we accept the proposed fix in Issue Description, it would expose a "lot" more templates to be handled by the custom engine. More importantly things coming from contrib could be in any shape and form and usage that is hard to predict.

---

@matthand, you do raise excellent use case for "SDC" which is different than the one I had used in Issue Description. (more about Symfony UX Twig Components). I do see the possibility of being able to use theme engine dedicated to SDC in Drupal.

matthand’s picture

Category: Feature request » Plan

@d34dman, thanks. Agreed that the direction needs settling before the details. I'll switch this to a Plan issue so that discussion has a home, and postpone the child issues until there's agreement.

The proposed solution (in issue description) would be a breaking change.

Yes, the approach in the current summary would be. The new plan I posted in comment 8 and already made child issues for avoids that by being opt-in. An engine that doesn't implement the new ownership interface stays exactly the way it is today, getting the templates of the active theme and its base themes. Only an engine that implements the new interface gets module or SDC templates, and only the ones it claims. Twig still renders everything else, so contrib templates render as they do now. This first child task delivers the new opt-in interface: #3625132: Add a template ownership interface so theme engines can tell core which templates they provide

That covers your use case (Symfony UX Twig Components) and an engine dedicated to SDC the same way, since each only claims what it handles and Twig still fills the gap.

If you're OK with it @d34dman, I'll update the issue summary to describe the new opt-in approach. The current proposed resolution was written before #1685492: Convert theme engines into services, so it no longer applies. It's the underlying architecture that changed since posting. Your original proposal was solid at the time.

@lauriii, @effulgentsia, @Fabianx (Theme API) and @e0ipso (SDC): would core accept this opt-in direction with a new interface? See child issues and rough plan in comment 8 above. @nicxvan, what should our next steps be?

matthand’s picture

@d34dman, I've updated the issue summary to match the opt-in plan... before architecture changes again on us! Since you wrote the original, I wanted to flag it rather than change it quietly. The earlier engine-path proposal predates #1685492: Convert theme engines into services, so I replaced it, and I tried to keep your use case front and center in the new Problem/Motivation.

Your original version is still there to compare: it's behind the "original summary" link in your first comment, and the "View changes" link on this comment shows exactly what changed.

If anything no longer reads the way you intended, or you'd frame the direction question differently, please edit it or tell me and I'll adjust it.

nicxvan’s picture

Hi @matthand I think the next step would be to get reviews from the subsystem maintainers you mentioned in 14.

Just an FYI they won't get notified by comments on d.o unless they are already following. Generally the process is to note something and give them a week or so once it's been in review.

Since this is a plan, I think it would be fine to ping them in slack asking for advice.

I would probably do a short write up in the #core-development channel asking them what their thoughts are.
Things have been a little hectic over the last couple of weeks preparing for 12 beta so it might get more traction if you wait a bit, but I don't think there is harm in reaching out there, other interested parties may have feedback there as well.

One thing that I'm not sure about is what do you expect if you replace all twig templates and a module provides one that you didn't account for, either a new module dependency, or a new feature?

I think the reason module and SDC are twig only are for consistency, but I don't actually know that for sure.

matthand’s picture

Thanks for the direction @nicxvan! I'll do a short write-up in #core-development. I'm in this for the long haul, so I'm happy to be patient.

One thing that I'm not sure about is what do you expect if you replace all twig templates and a module provides one that you didn't account for, either a new module dependency, or a new feature?

Module templates that are not overridden will fall back to Twig, which is what already happens today. A theme engine only registers the templates it finds in its theme (ThemeEngineInterface::theme()), and ThemeManager::render() sends everything else to Twig. EngineNyanCatTest covers this already: its test theme ships one Nyan Cat template, and the rest of the page still renders with Twig.

The plan keeps that fallback. An engine only gets a module template if it implements the new interface and says it has that template (#3625133: Record the resolving theme engine in the registry and stop hardcoding Twig for module templates). The registry is rebuilt when a module is installed, so a new module's templates go through the same check and end up with Twig unless the engine claims them. Engines that don't implement the interface, and Twig-only sites, behave exactly as they do now.

SDCs are different and I need to work thorough that part some more, because currently they do not fall back cleanly. In the plan #3625137: Delegate Single Directory Component rendering to the active theme engine, the theme engine needs to implement ComponentRendererInterface as well to render SDCs.