Problem/Motivation
Sometimes, you need to move a bunch of fields out of the {{ content }}
array and render them elsewhere in your template.
However, you definitely have to render {{ content }}
somewhere, or you don't get the expected cache tags for your output.
To avoid the fields showing up twice, you're supposed to do something like this:
<div class="somewhere-fancy">
{{ content.field_1 }}
{{ content.field_2 }}
</div>
<div class="content">
{{ content|without('field_1', 'field_2') }}
</div>
That works okay if you're only dealing with a small number of fields, but sometimes you need to move *a lot* of fields elsewhere in the template. This gets clumsy.
Twig lets you iterate over an array, but without() can't handle an array argument.
Proposed resolution
Option A: Allow a single array argument
@see #11
Change the without()
Twig filter to notice if you pass it an array, and if so, use that for its arguments.
This would allow, something like:
{%
set fancy_fields = [
'field_certification',
'field_job_title',
'field_is_staff',
'field_is_practitioner',
'field_is_instructor',
]
%}
<article>
<div class="fancy">
{% for field in fancy_fields %}
{{ content[field] }}
{% endfor %}
</div>
<div class="content">
{{ content|without(fancy_fields) }}
</div>
</article>
Drupal\Core\Template\TwigExtension::withoutFilter()
will currently blow up with a warning if you try to pass an array. So we should be able to change this without breaking any BC.
Option B: Mixed arguments that handle strings or arrays
See patch #34
Allow a mix of arrays and strings. Even more usable than the above, since if you have 2 fancy regions, you can just do:
<article>
<div class="important">
{% for field in important_fields %}
{{ content[field] }}
{% endfor %}
</div>
<div class="fancy">
{% for field in fancy_fields %}
{{ content[field] }}
{% endfor %}
</div>
<div class="content">
{{ content|without(important_fields, fancy_fields, 'field_special_case') }}
</div>
</article>
Implementations of B to consider:
-
#18: Explicit foreach, one loop, needs inner loop with sort of duplicate code.
foreach ($args as $arg) { if (is_array($arg)) { foreach ($arg as $key) { if (isset($filtered_element[$key])) { unset($filtered_element[$key]); } } } elseif (isset($filtered_element[$arg])) { unset($filtered_element[$arg]); } }
-
#19: 2 loops: 1 to get all the keys, second loop to unset everything
$keys = []; foreach ($args as $arg) { if (is_array($arg)) { foreach ($arg as $key) { $keys[] = $key; } } else { $keys[] = $arg; } } foreach ($keys as $key) { if (isset($filtered_element[$key])) { unset($filtered_element[$key]); } }
-
#34: RecursiveIterator classes to only iterate once, no duplicate code
// Since the remaining arguments can be a mix of arrays and strings, we use // some native PHP iterator classes to allow us to recursively iterate over // everything in a single pass. $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($args)); foreach ($iterator as $key) { if (isset($filtered_element[$key])) { unset($filtered_element[$key]); } }
Remaining tasks
Do it.Add some tests that show it works as expected.Decide on #11 (single array argument) vs. #18 (allow mix of strings and arrays) -- we're leaning towards B.: Winner: Mix of strings and arraysDecide if any of the alternate implementations in #19 or #21 are better than #18: Winner: #21 (plus nits fixed in #34)< /li>Review.- RTBC.
- Commit.
User interface changes
-
API changes
Twig templates using the without()
filter can optionally use a mix of arrays and strings for the arguments, instead of having to specify them all manually.
Data model changes
-
Release notes snippet
N/a
Comment | File | Size | Author |
---|---|---|---|
#34 | 3093577.21_34.interdiff.txt | 902 bytes | dww |
#34 | 3093577-34.patch | 4.6 KB | dww |
Comments
Comment #2
dwwStill needs tests, but let's make sure this doesn't break any current tests...
Manual testing locally works great.
Comment #3
dwwFound the existing tests for TwigExtension, so added a trivial case to prove this works. Passes locally. Can't imagine how this breaks anything else. What's the word, bot?
Comment #4
Ghost of Drupal PastSo yeah, let's do this.
Comment #5
dwwSweet, thanks!
Updating remaining tasks. ;)
Comment #6
pifagorLook good for me
Comment #7
Krzysztof DomańskiThe change record will be useful here.
Comment #8
Krzysztof DomańskiUpdated documentation.
Comment #9
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedThis feels wrong, but so does the original comment.
If that was correct, then the new one is correct as well.
---
Besides that setting to RTBC (in my role as theme api maintainer), looks good to me.
Comment #10
dwwThanks for the reviews!
Sadly, the new docs are indeed wrong, or at least, misleading. You can't pass an array of arrays. But probably you should be able to. ;)
Typo.
I'll fix these given the current approach.
Comment #11
dwwThis fixes the doc problems from #8.
However, now that I'm starting to use this on a real site, I'm seeing a need for either multiple arrays, or a mix of arrays and strings. Otherwise, you have to jump through some hoops with
merge()
inside your Twig template. We're merging here, anyway. Might as well make this even easier.Stay tuned.
Comment #12
dwwNow this allows
mixed[]
as the arguments, either arrays of keys, or directly string keys. With updated docs and tests.Maybe there's a slightly more elegant want to do this, but I didn't want to have to iterate through
$args
twice to make a single$keys
array first, then actuallyunset()
stuff. Nitpicks welcome. ;)I added this as a separate option B to the summary's proposed solution, showing why allowing mixed is even better in real world scenarios.
Also needs review and RTBC again, so updating remaining tasks... ;)
I'll wait for a CR until we agree on #11 vs. this approach.
Leaving both #11 and #12 patches visible in the file table, since those are the two viable approaches to consider.
Cheers,
-Derek
Comment #13
Ghost of Drupal Pastnew RecursiveIteratorIterator(new RecursiveArrayIterator($args));
to simplify your loop.Comment #14
Fabianx CreditAttribution: Fabianx as a volunteer and at Tag1 Consulting commentedtypo - additional space in here ;)
---
RTBC - so that anyone can set RTBC status for #12 if they want to, but not setting that status so that #13 can be considered.
Comment #15
dwwRe: #13:
Hah, MUCH simpler. ;) 🤦♂️What's the world coming to? ;)
Re: #14 Nice catch. Fixed here.
Comment #17
Krzysztof Domański1. #16 Unrelated random test failure.
2. Can we add an additional test case? Two arrays with a duplicate key.
Comment #18
dwwRe: #17.2: Sure, why not? Moar Tests(tm) is always good. ;) Especially since it's inside an existing test class and it's effectively free to test more combinations of this.
Comment #19
dwwFor comparison, 2 alternate approaches...
This does an initial loop to find all the keys.
Then a second loop to unset them.
No changes to test coverage. Passes locally.
Comment #20
dwwWith the "simple" solution from #13. ;)
edit: Note: we need
\
in front for PHP to find the right class. These are PHP native classes. Using exactly #13 fails with:Comment #21
dww#20 plus a code comment to explain WTF. ;)
Comment #22
dwwNote: if we go with #18 and up, the issue title should be changed to:
"Let our Twig without() filter take arrays and strings as arguments"
before commit.
Thanks!
-Derek
Comment #23
dwwCanceled the test run on #20. If we do that, we need the code comment from #21. ;) So let's save the bot cycles churning on #20...
Comment #24
dwwPut the implementation comparison for #18 vs. #19 vs. #21 into the summary for easier review.
Okay, people, let's pick. ;)
Thanks!
-Derek
Comment #25
Krzysztof DomańskiOption B +1
1. Re #21 Let's treat only arrays (not objects) as having children for recursive iteration.
The RecursiveArrayIterator class
2. Can we use The ArrayIterator class instead of The RecursiveArrayIterator class?
$iterator = new \RecursiveIteratorIterator(new \ArrayIterator($args));
Comment #26
Krzysztof Domański-- edited --
Comment #27
dww@Krzysztof Domański re: #25
Agreed on option B. I think it'd be weird to require and handle only a single array, and would probably result in more trouble than it helps. Preemptively solving #22 by re-titling this now. If someone really thinks we need option A, we can reset it.
Re: #25.1: wow, the "cure" is turning into something worse than the "disease". ;) So much for simple. :/ How, exactly, would someone be able to pass an object into this inside a twig template? Seems like unnecessary complication.
Re: #25.2: I don't think so. The whole point is that the
$args
array fromfunc_get_args()
might include nested arrays. That's what aRecursiveArrayIterator
handles. If we make this just anArrayIterator
, we'd only walk through the keys in$args
itself, not sub arrays. I think. ;)Comment #28
dwwRe: #25.2 : I just confirmed #27 in local testing:
Doing exactly this:
Results in this:
Doing this:
Results in this:
Line 675 would be this:
So yeah, as expected, we need
\RecursiveArrayIterator
to get any benefit from #21 over #18.Cheers,
-Derek
Comment #29
Krzysztof Domański+1 for #21.
Comment #30
lauriiiCould someone write a change record for the API change? 🙏
Comment #31
dwwSure. If we're cool with mix of strings and multiple arrays (#21) here we go:
https://www.drupal.org/node/3102853
I assumed this won't be backported to 8.8.x, so I put it as introduced in 8.9.x branch and 8.9.0-alpha1 release. Please adjust accordingly when committing this and publishing that.
Thanks,
-Derek
Comment #32
dwwI guess it's safe to restore the status, too, since the only objection to RTBC was the lack of a CR...
Comment #33
lauriiiThank you for opening the change record!
How about documenting this as
string[]|string ...
? 🧐The isset is redundant because unset doesn't give an error for unsetting non-existing array items.
Comment #34
dwwRe: #33.1: If you prefer, sure. I still think mixed[] is more true, but I don't really feel that strongly.
Re: #33.2. Okay, fixed.
Thanks,
-Derek
Comment #35
lauriiiI still think mixed[] is more true, but I don't really feel that strongly.
I'm wondering if I'm missing something here since I felt
mixed[]
was inaccurate. Wouldn'tmixed[]
essentially translate into arrays of mixed type of items? AFAIK what we accept on this parameter is either array of strings or a string.Comment #36
dww@lauriii re: #35: I've never found a good reference on The Right Way(tm) to document ... in general, that's all. :) You're totally right that's what each element of ... can be: a string, or an array of strings. But technically the "..." is itself an array, and its children elements are of mixed type (strings or arrays). That's my confusion. But if we ignore the array, #33.1 is indeed better. I'll plan to do that from now on whenever documenting ... args.
Can we call #34 RTBC?
Thanks!
-Derek
Comment #37
lauriiiI think #34 is ready for RTBC! Thank you for addressing the feedback @dww!
Comment #38
Percy101 CreditAttribution: Percy101 as a volunteer commentedThe patch applied cleanly
Comment #39
alexpottCommitted and pushed 12dcba2532 to 9.0.x and d6ee61eaeb to 8.9.x. Thanks!
Comment #40
alexpottI don't think this is a notable enough change to announce on the release notes - themers don't have to make any changes to existing code and the CR covers the new functionality.
Comment #44
losewnExample: in variable 'content' was 10 elements (name, date, id,...)
{{ content }}
- get all fields{{ content|without('date') }}
get all fields(name, (((deleted - date))), id,...), except 'date'