Once again I’m shaking my fists at the impossible hoop-jumping gauntlet that is the ExpressionEngine Parse Order. This time I want to pass the result of an Advanced Conditional as a value to a module tag parameter. It’s been long said it can’t be done. It’s time to change that.
Nesting an Advanced Conditional inside a module tag parameter. It's been said it can't be done.
{exp:channel:entries status="{if '{segment_3}' == 'closed'}closed{if:else}draft{/if}"} {title} - {status}<br> {/exp:channel:entries}
"You are using advanced conditionals in your tag, and in EE, they don't parse soon enough for your tag. Your tag is likely seeing all possible conditions when it processes."
Using conditionals in channel entries parameters
Ultimately at issue here is the ExpressionEngine® Parse Order, which is an obscure yet critical concept that every EE developer will at some point confront (and with the help of fine folks like Low, hopefully overcome).
On most days it sits quietly behind the scenes making things Just Work™ and providing the backbone for the simplicity and beauty of EE's template markup.
Then every so often it is the devil-in-the-details gremlin that riddles your project with exceptions, caveats, and "it depends" workarounds - like when it comes to conditional statements. See, in the world of the Parse Order, no two conditionals are the same - they're either considered "Simple", or "Advanced", and where they sit amongst the Parse Order matters a great deal - especially for performance, but also when we want to use them as a tag parameter.
Simple Conditionals - unfashionably early:
A conditional is considered "simple" if it is evaluating variables that are already available by the time the template parsing engine reaches the simple conditionals parsing stage (e.g. segment, embed, and user-defined global variables), the expression evaluates a single variable (i.e. contains no logical operators such as OR, AND), and the conditional does not make use of the else or elseif control structures.
In short, a simple conditional will look very much like this:
{if segment_1 == "joe"} <h1>Hi Joe!</h1> {/if}
Advanced Conditionals - fashionably late:
Any conditional that isn't a simple conditional is considered an "advanced" conditional and is evaluated much later in the template parsing order. Advanced Conditionals can use logical operators (ex: OR, AND) to compare multiple variables to multiple values.
{if username == "joe" OR username == "jane"} <h1>Hi {username}!</h1> {if:else} <h1>Who goes there?</h1> {/if}
So when EE serves up each template, the Template Engine makes multiple passes through its contents, as it incrementally steps through each stage of the Parse Order. When it comes to evaluating conditional control structures, you need to know that:
- Simple Conditionals are processed early, before module & plugins tags
- Advanced Conditionals are processed late, after module & plugin tags
The takeaway from this is that you can use a Simple Conditional as part of a tag parameter, since the conditional will be evaluated prior to the outer tag being run. However since Advanced Conditionals are parsed late, the outer tag's parameter will equal the complete, un-evaluated Advanced Conditional string.
So, what else can we pass as a tag parameter?
That is a great question. We now know we can pass simple conditionals, and referencing the Parse Order we know that we can pass early-parsed variables (segments, embeds and early globals) and preload_replace variables. So how about another plugin?
Yes, in fact - you can pass a plugin as a tag parameter. If you add parse="inward"
to your outer tag, then the Template engine will check if the content of any parameter value appears to be itself a plugin - and if so, it will be parsed immediately. See:
/* --------------------------------- /* Plugin as Parameter /* /* - Example: channel="{exp:some_plugin}" /* - A bit of a hidden feature. Has been tested but not quite /* ready to say it is ready for prime time as I might want to /* move it to earlier in processing so that if there are /* multiple plugins being used as parameters it is only called /* once instead of for every single parameter. - Paul /* ---------------------------------*/ if (substr_count($this->tag_data[$i]['tag'], LD.'exp') > 1 && isset($this->tag_data[$i]['params']['parse']) && $this->tag_data[$i]['params']['parse'] == 'inward') { .... $this->log_item("Plugin in Parameter, Processing Plugin First"); .... }
That, dear friends, is straight from the Template Engine itself - around line 1054. Hi, Paul! By now, I'm hoping you're with me on this ah-hah moment - because all we need now is a plugin that is specifically designed to evalute Advanced Conditionals. And lo and behold, one already exists.
The ifElse plugin from Mark Croxton
You may already know @croxton from Stash. Or from Switchee. Or other handy add-ons - you should check him out on Github. He's a mighty clever chap.
ifElse: Early parsing of advanced conditionals in EE templates. With ifElse, Mark has once again done the hard work to make our lives easier - he's built a plugin that coaxes EE's Template Engine to parse an advanced conditional as if it were a Simple Conditional - which is to say the conditions are evaluated before evaluating the tags within each control structure. But as an added bonus, ifElse now gives us the ability to pass an Advanced Conditional as a tag parameter.
So if we add parse="inward"
to our outer tag, and wrap our Advanced Conditional in the ifElse plugin, this is what we get:
{exp:channel:entries limit="3" parse="inward" status="{exp:ifelse}{if '{segment_3}' == 'closed'}closed{if:else}draft{/if}{/exp:ifelse}"} {title} - {status}<br> {/exp:channel:entries}
Update 2nd April: As has been astutely pointed out a few times now, this is a nonsensical example that is better achieved with a Simple Conditional, since it is operating upon an Early Parsed variable. Keep in mind however that not all variables are parsed early, so there are still going to be times where an Advanced Conditional is unavoidable. My apologies for not choosing a more appropriate example from the outset.
And it Just Works™ once again - exp:ifelse
is run, your condition is evaluated, and either 'closed' or 'draft' is returned to the status=
parameter. It was everything we'd ever hoped for, and more. Horray!
Horray indeed! But before you go, some parting advice
Nesting tags within tags within tags can be taxing for EE's Template Engine to know where one thing starts and another begins. So adhere to these two rules if you use this approach:
- 1. Use double & single quote nesting with care
-
If you're using double quotes for your outer tag parameters, use single quotes for you nested plugin-tag-as-parameter. Using all of one quote type may yield unexpected results.
- 2. For this technique to work, be sure to wrap early-parsed variables in curly brackets.
-
Early-parsed variables (as well as preload_replace variables) should be contained in curly brackets (e.g.
{variable}
) to be sure the Template Engine can evaluate them early.Note that for Simple Conditionals it is important to NOT wrap early-parsed variables in curly brackets. Thanks Paul for the reminder.
I should also say that I've only just discovered this trick, so I'd appreciate hearing feedback on your experiences, in particular if there are any other important caveats or syntax/parse rules to keep in mind. Happy templating all...
Update 1st April: As Low very correctly pointed out in the comments below, this does rely on an unofficial "hidden" feature, so there is no guarantee that future versions of EE will allow this technique. As always you should choose the solution that best suits the task at hand, and future-proof & maintainability are vital criteria to consider.
Have your say...