Problem/Motivation

Whilst working with Layout Builder blocks, we noticed some of our blocks were not correctly ordered.

CategorizingPluginManagerTrait::getSortedDefinitions() performs a comparison between the category of $a and $b to figure out how to sort the two items relative to each other. If the category is different, then the items are ordered by category name, otherwise they are ordered by plugin admin label.

However, both the category name and the admin label are instances of Drupal\Core\StringTranslation\TranslatableMarkup

So the line:

if ($a['category'] != $b['category']) {

Actually compares if the TranslatableMarkup objects are the same, rather than the rendered, stringified value. In some circumstances, the translatedMarkup has not yet been rendered at the time of the comparison (its value is NULL), and so the two categories, despite the same label, are different and sorted as such.

This causes some block plugins to not sort properly.

Here is a screenshot of xdebugging the categories:

As you can see, the translatedMarkup is set to NULL at this point, because it has not yet been evaluated. But, in subsequent comparisons the value for this particular item will be set, but for other, as yet uncompared items, they will not - so the objects will differ.

Here you can see how the items are not alphabetically sorted:

In the subsequent calls to strnatcasecmp() those values do actually get cast as strings and rendered, but those happen after the category comparison, which is why some of the plugins have not had their category rendered yet.

Steps to reproduce

Create multiple block plugins with translatable categories, using different categories for some of the plugins annotations like below

@Block(
 *   id = "your_block_id",
 *   admin_label = @Translation("Block Label"),
 *   category = @Translation("Primary"),
 * )

Then observe if the blocks are arranged alphabetically within those categories or not.

Proposed resolution

At the moment CategorizingPluginManagerTrait::getSortedDefinitions() compares $a['category'] and $b['category'] rather than the rendered output of those values. Simply casting those values as strings should fix the issue, as the __toString() magic method will render those values for us.

if ((string) $a['category'] != (string) $b['category']) { ... }

Remaining tasks

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

Fix issue with plugins not being ordered appropriately if TranslatableMarkup for the category names has not yet been rendered.

I came across #2824890: Convert uasort to array_multisort in \Drupal\Core\Plugin\CategorizingPluginManagerTrait::getSortedDefinitions() during investigating this issue, and the approach taken in that issue does also fix this problem but that issue seems to have stalled and was aimed at addressing a PHP7 problem, which was addressed elsewhere.

Issue fork drupal-3308449

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:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

owenbush created an issue. See original summary.

owenbush’s picture

Title: CategorizingPluginManagerTrait::getSortedDefinitions() sometimes returns plugins in the incorrect order » CategorizingPluginManagerTrait::getSortedDefinitions() sometimes returns plugins in the incorrect order if they have categories or labels translated
owenbush’s picture

Issue summary: View changes
owenbush’s picture

owenbush’s picture

Status: Active » Needs review

I have made the change and modified some BlockManagerTests which would fail without this change. Marking as Needs Review.

owenbush’s picture

For those wanting to safely use a patch with composer I've uploaded a copy of the diff as a patch.

Dave Reid’s picture

Status: Needs review » Reviewed & tested by the community

I have confirmed this resolves the issue we see in block sorting.

owenbush’s picture

Attaching a test-only patch that should fail, to show that this is an issue.

Status: Reviewed & tested by the community » Needs work
owenbush’s picture

Status: Needs work » Reviewed & tested by the community

Moving back to RTBC, because it was automatically moved back to Needs Work, despite that being a test only patch designed to fail.

Status: Reviewed & tested by the community » Needs work
owenbush’s picture

Status: Needs work » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work
owenbush’s picture

Reuploading the working patch to stop the issue being marked as needs work every time the test only patch is retested.

owenbush’s picture

Status: Needs work » Reviewed & tested by the community
alexpott’s picture

Version: 9.5.x-dev » 9.4.x-dev
Status: Reviewed & tested by the community » Fixed

Nice fix and test. Backporting to 9.4.x as a low-risk bug fix.

Committed and pushed d3cf1e9277 to 10.1.x and 53fdb351ef to 10.0.x and 6de387a4ca to 9.5.x and cbdb894196 to 9.4.x. Thanks!

  • alexpott committed d3cf1e9 on 10.1.x
    Issue #3308449 by owenbush: CategorizingPluginManagerTrait::...

  • alexpott committed 53fdb35 on 10.0.x
    Issue #3308449 by owenbush: CategorizingPluginManagerTrait::...

  • alexpott committed 6de387a on 9.5.x
    Issue #3308449 by owenbush: CategorizingPluginManagerTrait::...

  • alexpott committed cbdb894 on 9.4.x
    Issue #3308449 by owenbush: CategorizingPluginManagerTrait::...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.