Problem/Motivation
#1053648: Convert site elements (site name, slogan, site logo) into blocks brought with it the first use of Twig {% extends %}
in core. That patch has {% extends "@block/block.html.twig" %}
which uses namespaces to extend templates based on Drupal extensions (modules and themes). This functionality is commonplace in the Symfony world and was introduced in #2143557: Add modules and themes as twig namespaces.
While working on #1053648: Convert site elements (site name, slogan, site logo) into blocks @mdrummond brought forward the idea that template extending should be easier. If you initially start out by extending @block/block.html.twig
in your theme but then decide to override block.html.twig in your theme, you need to go back and change your extends
to @mytheme/block.html.twig
.
Proposed resolution
Add a custom Twig loader to core that extends the filesystem loader to allow for extending based on the theme registry. This would allow for this simple syntax:
{% extends "block.html.twig" %}
Which by default will extend core/modules/block/template/block.html.twig, but if a theme overrides block.html.twig, it will extend from the theme's block.html.twig, and so on.
The patch also adds services tagged with twig.loader to the Twig environment, allowing contrib to do custom template loading and this is something that the Symfony TwigBundle does already: https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBu...
Beta phase evaluation
Issue category | Task because it improves TX/DX by making things work as people expect. |
---|---|
Issue priority | Normal |
Unfrozen changes | Mostly an additive change and changes the theme system which is not frozen. |
Prioritized changes | (not sure) |
Disruption | Backwards compatible, minimal impact |
Remaining tasks
Task | Novice task? | Contributor instructions | Complete? |
---|---|---|---|
Add automated tests | Instructions | Done |
User interface changes
n/a
API changes
An addition along the lines of #2143557: Add modules and themes as twig namespaces.
Comment | File | Size | Author |
---|---|---|---|
#77 | 2291449-77-testonly.patch | 12.35 KB | star-szr |
#76 | interdiff.txt | 3 KB | star-szr |
#76 | 2291449-76.patch | 24.24 KB | star-szr |
#75 | interdiff.txt | 5.26 KB | star-szr |
#75 | 2291449-75.patch | 22.1 KB | star-szr |
Comments
Comment #1
star-szrHere's an initial patch, needs tests and some more/better docs still. This copies findTemplate() from the vendor Twig_Loader_Filesystem (with coding standards in line with ours) and adds our logic to the end before the final
throw
:Comment #2
star-szrComment #3
star-szrRerolled for #2251113: Use container parameters instead of settings and removed the
print_r()
:)Working on tests.
Comment #4
dawehnerIf you write new code, use proper dependency injection, please.
Given that this full codeblock is all coming from the parent class, it would be nice to extract into a helper method, to mark it clear that this code actually belongs to the parent method, so upstream changes can be applied better.
Comment #5
star-szrThanks @dawehner! I thought DI would make sense for #1 but I didn't know how to do it immediately. I like your suggestion about a helper method, I will see what I can do.
Tests are coming along well, will post those soon then look at #4.
Comment #6
star-szrBetter docs and initial tests, based heavily on the Twig namespace tests. May need a bit more tweaking so they make more sense but it's a start.
Comment #7
star-szrHere's a shot at #4. Hope this is roughly what you meant @dawehner, if not let me know :)
Comment #8
star-szrI forgot to mention, in #6 I removed the namespace from both block--system-branding-block.html.twig templates to show how this would work.
Comment #9
dawehnerperfect!
Wait, shouldn't app.root be a single value? just a string?
Comment #11
star-szrYou can pass multiple paths as an array although we only pass one as a string. I copied that from Twig_Loader_Filesystem::__construct() although I guess the
if
could be removed in our case.Comment #12
star-szrAfter looking at #2317557: renderInline not compatible with twig_auto_reload again I'm curious if we could just add a smaller filesystem loader class with only the registry logic rather than extending the whole Twig_Loader_Filesystem class. I have no idea if this is possible, if it's possible it seems like it should be done on the service level so it can be swapped out. Maybe we can specify multiple classes to pass to the constructor of TwigEnvironment?
Part of me thinks the throw in Twig_Loader_Filesystem will prevent this from happening and yet the string loader works even though it's second in the chain…
Comment #13
star-szrIndeed after some playing around I came up with something that seems to work. Twig_Loader_Chain catches the Twig_Error_Loader exceptions, very clever!
@dawehner I think I'll need your thoughts on this one! It was a lot of fun playing around in core.services.yml :) I left a commented line in there, I thought it might also be worth exploring/discussing the idea of Twig loaders as a tagged service.
Comment #14
star-szrJust thinking out loud, but maybe we could register Twig_Loader_Filesystem and pass to the Twig environment as we do now, and then tag the theme registry and string loaders as twig.loader. That would potentially allow adding the namespace paths to Twig_Loader_Filesystem in Drupal\Core\Template\TwigEnvironment::__construct() and also adding the other 2 loaders (and potentially more from contrib).
Comment #15
joelpittetThis is looking pretty cool! Not sure what "tagged service" is care to explain?
Comment #16
star-szrSimplification and small tweaks for the registry loader.
@joelpittet same idea as Twig extensions are now, in your MODULE.services.yml you would register your loader service and tag it with twig.loader to have it added to the loader chain.
More info:
http://symfony.com/doc/current/cookbook/templating/twig_extension.html#r...
Bottom of https://www.drupal.org/node/1831138
Comment #17
joelpittetVery interesting... could you re-order this chain or replace it too?
Comment #18
star-szrI do know you can specify a "weight" when you tag something. It seems like it would be possible to replace the entire loader chain as well if you wanted similar to how you can swap out other services.
Comment #19
Jeff Burnz CreditAttribution: Jeff Burnz commentedThis looks like it's going to solve an issue I ran into this morning (see https://www.drupal.org/node/2143557#comment-9259581), where you can't have templates in a sub directory or extend won't work, i.e. themeName/templates/subdir/template.html.twig - I found this out trying to extend block. If so that would be great, sorry I did not have time today to test this patch.
Comment #20
star-szrThanks @Jeff Burnz! I want to push this forward in some way so I will try to play around with the idea of twig loaders being a tagged service, I don't want to keep changing that part of core.services.yml.
Comment #21
star-szrHere's a reroll to start with, no changes.
Comment #22
star-szrThis seems to work, let's see if anything fails. Of course this would need some more tests added to make sure other loaders can be added.
Feedback please!
Comment #23
star-szrAnd woohoo this is not a new idea! :D Kinda looks like this code would be almost what we want too, because we want at least one and we want the chain loader to be the default:
https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBu...
Comment #24
star-szrSo I think the next step will be to add this as a compiler pass using code very similar to the Symfony TwigBundle rather than the standard service_collector mechanism (\Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass).
Comment #25
star-szrSomething like this. Re-tagging for tests and updating the issue summary a bit.
Comment #26
star-szrComment #27
star-szrHere's a rough skeleton for some tests, I also did some poking around the web to get a feel for the space.
@joelpittet recently clued me into the string loader being discouraged and newer versions of Twig even say "This loader should only be used for unit testing as it has many limitations (for instance, the include or extends tag does not make any sense for a string loader).".
So I'd hesitate to make it the job of this issue but we should probably give some thought to how we're using the string loader.
This GitHub issue suggests using the array loader or template_from_string. I don't see how the latter would be practical for inline templates but the array loader might be a possibility:
https://github.com/symfony/symfony/issues/10865
Comment #28
star-szrComment #29
joelpittetI say we use it, it's useful:) The main idea (in my head at least) behind inline templates and string loader is that it's used to "rapid prototype" an idea and flush out the real template later if that idea becomes viable. Instead of going through the rigamarole of defining hook_theme in the module file, creating a template and building a render array, you just build the template in the string and pass in the data it should expect.
Preaching to the choir here a bit, but that's how I see the role which to me fits inline with the unit test purposes that sensio is proposing. In core I'd actually like to discourage using inlineTemplating just as much as we discourage using
['#markup' => '<p>test</p>']
But for getting things done, they are both awesome tools to have!
Comment #31
star-szrBased on that thread (I haven't played with it yet) at least one of the problems is that when a template is not found and you are using the string loader in the Twig loader chain, instead of a useful error/exception you will just see the name of the template you called on screen.
Comment #32
joelpittetOh yeah, good call. It would be better is maybe the StringLoader is only explicitly added when it's needed like renderInline() and 'inline_template' ilk
Comment #33
star-szrIssue created to figure out the string loader: #2369981: Not found templates are displayed literally instead of throwing an Exception due to string loader
Comment #34
star-szrThis should turn green.
Comment #35
star-szrJust remembered @Fabianx said it shouldn't be a hardcoded 0 index for the attributes.
Comment #36
Fabianx CreditAttribution: Fabianx commentedSee symfony docs, but the gist is:
...
http://symfony.com/doc/current/components/dependency_injection/tags.html...
Comment #37
star-szrHere's what I came up with at BADCamp, but I feel like I am probably missing something.
Comment #38
star-szrI do like the $tag_attributes var name better though.
Comment #40
joelpittet@Cottser me too:) too many things named attribute, indistinguishable in grep:)
Comment #41
star-szrHere's a reroll.
Comment #42
star-szrMinor: Coding standards update and variable renaming. I think the next step will be to write two change records and add the beta evaluation criteria to the issue summary.
Comment #43
star-szrTwo change record drafts created, edits welcome:
https://www.drupal.org/node/2381097
https://www.drupal.org/node/2381103
Initial beta evaluation added to the issue summary, it's my first one so I probably didn't do it right.
Comment #44
star-szrFor example, it helps if you uncomment the table rows :D
Comment #46
star-szrFix for #2376791: Move all _content routing definitions to _controller.
Comment #47
lauriiiJust a nitpick s/twig/Twig :)
Comment #48
Jeff Burnz CreditAttribution: Jeff Burnz commented{% extends "block.html.twig" %}
, epic win!Comment #49
joelpittetPutting up the bat signal, @Fabianx can you check this over?
Comment #50
star-szr@lauriii, yeah I copy/pasted that. Fixed here!
I also decided it was about time to use hyphens instead of underscores in the template names, which also allows removing the 'template' lines too :)
I also want to call out the above @todo, I don't know the best way to handle that, maybe @Fabianx or someone else has an idea that I can implement :)
Edit: Didn't mean to attach 2 interdiffs but the first one shows the twig/Twig change, the second one shows that and the template updates.
Comment #51
star-szrComment #52
Fabianx CreditAttribution: Fabianx commentedTwig has since 1.1.4 the ability to use chained loaders, can we use that here? And inject the chain-of-responsibility loaders?
The rest looks pretty good already.
Comment #53
Fabianx CreditAttribution: Fabianx commentedNvm my comment, we cleared in IRC:
Yes, we should subclass the twig loader filesystem and inject our service instead, which registers the paths on construct.
Comment #54
star-szrComment #55
star-szrSubclassed the filesystem loader, this allows us to simplify the TwigEnvironment a bit. I also moved the registry loader and new subclassed filesystem loader into a Loader namespace.
Comment #56
Fabianx CreditAttribution: Fabianx commentedNot a blocker:
I don't know Core best practices, I like to re-use the last part of the namespace with the class name.
A file system is very generic, but not saying what that class does.
A FilesystemLoader while redundant immediate tells what this is.
The same for a ThemeRegistry vs. a ThemeRegistryLoader ...
This is just moved around, so out of scope to change here, but:
Could we open a major follow-up bug to remove the file_exists.
We should have no file_exists calls during run-time.
This information needs at the minimum be at least cached.
The parent can be '[meta] Resolve known performance regressions in Drupal 8.'
RTBC, except for those two things, pushing to a core committer to review.
Comment #57
alexpottI think this should be marked as private.
Why are we not using the TaggedHandlersPass here? The logic for the singleton here looks unnecessary.
Comment #58
star-szrThanks @Fabianx @alexpott for the reviews!
1 - Sure.
2 - I don't know how to do that… asking in IRC now.
3 - Initially I brought over the compiler pass from the TwigBundle because of the below, but if we are saying this doesn't matter then I can reimplement it as a TaggedHandlersPass fairly easily, attached to the twig.loader.chain service I suppose.
These will probably never be true but I like having them there. Theoretically you could replace the whole chain with your own.
4 - Sounds good, I can create the follow-up. I think we can just remove the file_exists() check entirely, it seems unnecessary.
Comment #59
Fabianx CreditAttribution: Fabianx commented#57.2:
Can we make it private if we put it as an alias for twig.loader?
I assume yes, just making sure.
#57.3. I think the extra twig pass is okay and I like having the extra checks, too.
Comment #60
star-szr#57.1:
I think in the case of #2358037: Add search form block Twig template file we need to leave the classy namespace there, otherwise it would be referencing itself. I'm not sure if it's possible to protect from those, in other words I'm not sure if the loader class can be aware of the context from which it's called.
core/themes/bartik/templates/block--search-form-block.html.twig:
{% extends "@classy/block--search-form-block.html.twig" %}
The template for block--search-form-block.html.twig in the registry would be the one in Bartik.
However I did convert all the remaining unnecessary namespace usage in core I could find (in text.module).
2 - done and so far nothing seems to have broken :)
3 - left as is, reasoning is in #58 and #59.
4 - I re-did the namespacing, here is the follow-up to remove the file_exists although it may need more than just a straight removal: #2383413: Remove file_exists() when registering namespaces for Twig template paths
Comment #61
Fabianx CreditAttribution: Fabianx commentedRTBC, looks great and nice follow-up to the CKEditor fragile issue.
Comment #62
star-szrCreated this issue as a spin-off from what I found when playing around here: #2387069: {% extends "foo.html.twig" %} in Twig templates does not respect theme inheritance
Comment #63
davidhernandezRegarding, #57.1 I think Cottser is right. It is extending a template with the same name, so there is confusion. But, I think we can actually fix that one by extending the Bartik block template, instead of the classy search form block template. This is something I didn't realize when first making it. We can manipulate the attribute in the child template, so the changes needed can probably all be done in the child. Then, we won't need to specify "@classy".
If it works I'll create an issue for it with a fix.
Comment #64
alexpottI'm not that convinced by the arguments in #58 and #59. And I still don't get why we special case the singleton.
Comment #65
star-szr@alexpott I'm not sure exactly what you mean by "the singleton", this might be easier to hash out on IRC though. Below is my guess:
Comment #66
star-szrTalked through this on IRC, I will look at adding an optional tag attribute for when you tag services with
service_collector
to allow indicating that at least one service/handler is required (name suggestions welcome :)). Then we can use the standard TaggedHandlersPass, and we'll always use Twig_Loader_Chain even if there is only one loader.This way if other services have this use case it's available and no need to make a custom compiler pass to cover it.
Within TaggedHandlersPass this is where an exception can be thrown if the service_collector tag has the relevant tag attribute:
Comment #67
star-szrI'm not ready to unassign but the lack of activity is because I've been ill. Hoping to get back to this soon but if anyone is itching to continue don't let me stop you :)
Comment #68
hass CreditAttribution: hass commentedThis issue sounds like a possible source of the issue I found in #2402181: Optimize toolbar-menu template with Twig template inheritance. Can someone with Twig skil verify, please?
Comment #69
star-szr@hass perhaps if you replace "source" with "solution"? Nothing has been committed here.
Comment #70
star-szrI am hoping to (finally) come back to this within the next week. Not ill anymore just busy with client work.
Comment #71
star-szrReroll! One line changed to catch up with current state, see interdiff.
Comment #74
star-szrSeeming random fail in Drupal\migrate_drupal\Tests\d6\MigrateFileTest.
Comment #75
star-szrNeed to add some tests to TaggedHandlersPassTest but this seems to work so far.
Comment #76
star-szrI think this is ready for more reviews now :)
Comment #77
star-szrAnd here are just the tests since I added the PHPUnit coverage. This should fail.
Comment #79
star-szrComment #80
RainbowArrayShould this text be indented?
Should this be indented?
I did a quick pass, looks like progress is being made. Yay! Couple indentation things I wondered about listed above.
Comment #81
Fabianx CreditAttribution: Fabianx commentedI don't think the indentation matters for the tests.
RTBC - Patch looks good and uses the Service Collector as proposed by Alex.
Not sure if the addition of required => TRUE needs a change record or some documentation somewhere beneath the API function.
Comment #82
alexpottI'm very excited about twig template inheritance working with the theme registry. Committed 0f93b42 and pushed to 8.0.x. Thanks!
Comment #84
davidhernandezGreat work, everyone. This is going to be really useful.
Comment #85
star-szrYay! I updated most of the related issues.
Comment #86
Jeff Burnz CreditAttribution: Jeff Burnz commentedOh wow, amazing! Thank-you everyone who worked on this, especially Cottser, outstanding work, I'm very happy indeed :)
Comment #87
joelpittetSa-weet! Thanks @Cottser:)
Comment #88
eatings1. Wait -- it wasn't like this before?
2. Awesome, either way!
Comment #89
Jeff Burnz CreditAttribution: Jeff Burnz commentedI believe we have a follow up issue, I have post it here, this is probably afaict critical #2414255: Subtheme template inheritance working in reverse order