Skip to content

Fluid metadata parsing bug when using a relationship field #5365

Description

@robinsowell

This one is ugly, but at least there is a current workaround.

Fluid Field metadata conditionals fail when the current item is a Relationship field

Minimal reproduction

Create a Fluid field with the short name fluid_field containing these two ungrouped fields:

  1. A Text field named my_text.
  2. A Relationship field named my_related.

Add both fields to an entry in this order:

my_text
my_related

Then for the tag:

{fluid_field}

  {fluid_field:my_text}
      <div class="text">
          {content}
      </div>
  {/fluid_field:my_text}

  {fluid_field:my_related}
      <ul class="related-entries">
          {content status="open"}
              <li>{content:title}</li>
          {/content}
      </ul>
  {/fluid_field:my_related}

  {if fluid_field:last}
      <p>LAST FLUID ITEM</p>
  {/if}

{/fluid_field}

Expected result

Because my_related is the final item in the Fluid field, this conditional should evaluate as true:

{if fluid_field:last}
    <p>LAST FLUID ITEM</p>
{/if}

The rendered output should include:

<p>LAST FLUID ITEM</p>

Actual result

The Relationship field content renders, but the fluid_field:last conditional does not render its contents.

If any additional Fluid item is added after the Relationship field—even an unrelated fieldtype—the conditional works when that new non-Relationship item becomes the last item.

This makes the behavior appear data-dependent, but it is actually dependent on the fieldtype occupying the relevant Fluid position.

Template workaround

The immediate workaround is to output the Fluid metadata variable as a brace-wrapped single tag inside the conditional. The docs note this is necessary for metadata variables using a parameter, but it's a problem regardless of that when you have that weird last relationship field thing going on. In any case- the workaround is bracing your variables.

{fluid_field}

    {fluid_field:my_text}
        <div class="text">
            {content}
        </div>
    {/fluid_field:my_text}

    {fluid_field:my_related}
        <ul class="related-entries">
            {content status="open"}
                <li>{content:title}</li>
            {/content}
        </ul>
    {/fluid_field:my_related}

    {if '{fluid_field:last}' == '1'}
        <p>LAST FLUID ITEM</p>
    {/if}

{/fluid_field}

In other words, replace:

{if fluid_field:last}

with:

{if '{fluid_field:last}' == '1'}

Why the error occurs

The Fluid parser correctly calculates the metadata for each Fluid item.

In:

system/ee/legacy/libraries/Fluid_field_parser.php

the parser constructs metadata including:

$meta = [
    $fluid_field_name . ':first' => (int) ($g == 0 && $firstInGroup),
    $fluid_field_name . ':last' => (int) (($g + 1) == $total_groups && $lastInGroup),
    $fluid_field_name . ':count' => $i + 1,
    $fluid_field_name . ':index' => $i,
    // ...
];

The Relationship field is therefore correctly assigned a last value of 1 when it is the final Fluid item.

The problem occurs later in:

system/ee/ExpressionEngine/Addons/fluid_field/Service/Tag.php

Tag::parse() has a special execution path for Relationship fields:

if ($field->getType() == 'relationship') {
    // Delegate Relationship content to the Relationship parser.

    $tagdata = $relationship_parser->parse(
        $field->getContentId(),
        $tagdata,
        $channel
    );

    $tagdata = $field->replaceTag($tagdata);
    $field->setName($name);

    return $tagdata;
}

That branch returns before reaching the normal Fluid conditional processing:

$tagdata = $this->parseConditionals($field, $tagdata, $meta);

For non-Relationship fields, the Fluid metadata array is passed to parseConditionals(). This allows a conditional such as:

{if fluid_field:last}

to be evaluated using the calculated fluid_field:last value.

For Relationship fields, the method returns before that step. The Relationship parser handles the related-entry tags, but the current Fluid metadata is not passed through the normal Fluid conditional-processing path.

This explains why adding another field appears to fix the problem: the new final field follows the normal parsing path, so its last conditional is evaluated.

Other metadata potentially affected

The problem is not limited to last. It can potentially affect any Fluid metadata used directly as a conditional variable while the current Fluid item is a Relationship field.

The documented Fluid metadata includes:

  • first
  • last
  • etc...

Grouped Fluid fields also provide metadata including:

  • first_group
  • last_group
  • etc..

These variables and the grouped syntax are documented in the ExpressionEngine Fluid Field documentation.

Possible symptoms include:

  • Opening markup not rendering when a Relationship field is first.
  • Closing markup not rendering when a Relationship field is last.
  • Count- or index-based classes being omitted.
  • Previous/next field logic failing.
  • Separators between Fluid items disappearing.
  • Group wrappers becoming unbalanced.
  • A Fluid field containing only one Relationship item failing both first and last conditionals.
  • Output changing when an unrelated Fluid item is added or reordered.

Standalone output of the metadata may still work:

Last value: {fluid_field:last}

The failure concerns using the metadata directly as a conditional variable:

{if fluid_field:last}

Why the workaround works

Tag::parse() begins by replacing brace-wrapped metadata tags:

$tagdata = $this->replaceMetaTags($meta);

That replacement happens before the Relationship-specific branch.

Consequently, this conditional:

{if '{fluid_field:last}' == '1'}

is converted to something equivalent to:

{if '1' == '1'}

before the Relationship field delegates its content to the Relationship parser and returns early.

By contrast, this form:

{if fluid_field:last}

does not contain a standalone {fluid_field:last} tag for replaceMetaTags() to replace. It depends on the later call to parseConditionals(), which the Relationship branch skips.

The workaround therefore forces the metadata substitution to occur during the portion of Tag::parse() that both normal fields and Relationship fields execute.

The same pattern can be used for other Fluid metadata:

{if '{fluid_field:first}' == '1'}
    First item
{/if}

{if '{fluid_field:count}' == '2'}
    Second item
{/if}

{if '{fluid_field:current_field_name}' == 'my_related'}
    Current item is the Relationship field
{/if}

{if '{fluid_field:next_field_name}' == ''}
    There is no next item
{/if}

Parameterized metadata can use the same explicit comparison:

{if '{fluid_field:last name="my_related"}' == '1'}
    This is the last my_related field
{/if}

Grouped versus ungrouped Fluid fields

The ungrouped example should be used as the primary reproduction because it isolates the problem without introducing group parsing.

However, grouped Fluid fields appear susceptible to the same underlying issue. ExpressionEngine ultimately parses each child field in a group using a FieldFacade. If a child field is a Relationship field, it follows the same Relationship-specific branch in Tag::parse().

For example:

{fluid_field}

    {fluid_field:my_group}
        {fields}

            {fluid_field:my_text}
                {content}
            {/fluid_field:my_text}

            {fluid_field:my_related}
                {content status="open"}
                    {content:title}
                {/content}
            {/fluid_field:my_related}

            {if fluid_field:last_in_group}
                LAST FIELD IN GROUP
            {/if}

        {/fields}
    {/fluid_field:my_group}

{/fluid_field}

If my_related is the last child field in the group, the same early-return path may prevent the direct last_in_group conditional from receiving Fluid metadata.

The corresponding workaround is:

{if '{fluid_field:last_in_group}' == '1'}
    LAST FIELD IN GROUP
{/if}

ExpressionEngine 7.5 also supports accessing grouped fields without a {fields} loop by using group-prefixed field names. That syntax should receive separate regression coverage because its tag extraction differs, although Relationship child fields ultimately reach the same special parsing branch.

Suggested technical approach

A minimal fix would be to process the remaining Fluid metadata conditionals after Relationship parsing but before returning from the Relationship branch:

if ($field->getType() == 'relationship') {
    // Existing Relationship parsing...

    $tagdata = $field->replaceTag($tagdata);

    // Evaluate remaining outer Fluid conditionals using Fluid metadata.
    $tagdata = $this->parseConditionals($field, $tagdata, $meta);

    $field->setName($name);

    return $tagdata;
}

The ordering matters. Calling the general Fluid conditional parser before the Relationship parser could prematurely evaluate or remove conditionals that depend on related-entry data.

A more targeted implementation would identify Fluid metadata variables in conditional expressions and replace only those variables with their values before delegating to the Relationship parser. That would avoid evaluating unrelated Relationship conditionals in the wrong context.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions