Problem/Motivation

In #3220021: [meta] Ensure compatibility of Drupal 9 with PHP 8.1 (as it evolves) the fact that EntityInterface::label() can return a NULL becomes an issue because we often pass the result of label() to string function that in PHP 8.1 trigger deprecations when not passed a string.

There are quite few places in core where the result of label() is assumed to be a string and if it is not then what happens next does not make much sense. Currently we are getting things like empty link tags, for example.

Steps to reproduce

Proposed resolution

Where the label is NULL and the result is being used in a string function for display - use the entity ID instead.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

alexpott created an issue. See original summary.

alexpott’s picture

Status: Active » Needs review
FileSize
11.06 KB

Here's all the label related change from #3220021: [meta] Ensure compatibility of Drupal 9 with PHP 8.1 (as it evolves). I'm not entirely sure about the scope of this and how best to do it and whether or not we want tests. Once we support PHP 8.1 we get implicit test coverage via the PHP deprecations.

This patch was generated by doing git checkout 3220021-php81-compatibility (git diff 9.3.x 3220021-php81-compatibility -G label --name-only) and the removing the small about of unrelated change.

longwave’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
    @@ -352,7 +352,8 @@ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTA
    +      // @todo or should we use the ID here?
    +      $options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label() ?? '');
    

    Still an @todo here. I would suggest yes, because at least the ID lets us distinguish between two entities with no label, if that ever happens.

  2. +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationUITestBase.php
    @@ -228,7 +228,7 @@ protected function doTestTranslationOverview() {
    +        $this->assertSession()->elementTextEquals('xpath', "//table//a[@href='{$view_url}']", $entity->getTranslation($langcode)->label() ?? $entity->getTranslation($langcode)->id());
    

    Is this really correct? This feels out of place in test code. We should just make sure the entities under test have a label, I think.

  3. +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
    @@ -25,7 +25,8 @@ class LayoutBuilderController {
    +    // @todo what is the correct thing to do here when there is no label?
    +    return $this->t('Edit layout for %label', ['%label' => $section_storage->label() ?? $section_storage->getStorageType() . ' ' . $section_storage->getStorageId()]);
    

    Should we just say "Edit layout" if there is no label?

alexpott’s picture

@longwave thanks for the review, sorry for taking a while to get back to this one.

Re #3.1 I agree that we should use the ID too... but I'm pondering if we should have a generic rule for entities that, if there is no label, then we return "content item 1" or whatever the label singular + entity ID would be. OTOH that would make more difficult to determine that a label is not set. Not sure.

Re #3.2 I think this is a good change because we're testing the unlabelled entity ID fallback :)

Re #3.3 I wonder if there possibly could be multiple layouts on the page all with Edit layout pointing to different section storages. I guess this is why I think we need something consistent to deal with an unlabelled entity.

alexpott’s picture

@berdir pointed out the existence of #2939931: Add an implementation of EntityDisplayBase::label() which is definitely related.

longwave’s picture

Re #3.2 I feel that if we are testing a fallback we should have an explicit test for it, rather than using the same fallback logic in the test assertion; otherwise we risk refactoring away a test for the fallback by mistake and it becomes untested.

alexpott’s picture

Status: Needs work » Needs review
FileSize
1.74 KB
10.95 KB

@longwave re #3.2 this is a TestBase class - and in my mind it needs to deal with whatever the Content Translation UI outputs. This patch changes that so it needs to change so support both types of label.

Here's a patch with the @todo's removed.

Status: Needs review » Needs work

The last submitted patch, 7: 3232673-7.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
967 bytes
10.94 KB

I'm really not sure that the no label behaviour in DefaultSelection is correct but it is extensively tested in \Drupal\Tests\system\Kernel\Entity\EntityReferenceSelectionReferenceableTest so I don't think we should be changing that behaviour here. It opens up quite a few cans of worms about what to do with the match operator if the label can be an entity ID.

alexpott’s picture

  1. +++ b/core/modules/contact/contact.module
    @@ -123,7 +123,7 @@ function contact_mail($key, &$message, $params) {
    -    '@form' => !empty($params['contact_form']) ? $params['contact_form']->label() : NULL,
    +    '@form' => !empty($params['contact_form']) ? $params['contact_form']->label() : '',
    

    I think that label is required for Contact Forms. it is in the UI... in fact this bit here is about what to do when there is no contact form - ie. it's the user contact page. So this bit looks fine. NULL is just a not default value for a string replacement value.

  2. +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
    @@ -25,7 +25,7 @@ class LayoutBuilderController {
    -    return $this->t('Edit layout for %label', ['%label' => $section_storage->label()]);
    +    return $this->t('Edit layout for %label', ['%label' => $section_storage->label() ?? $section_storage->getStorageType() . ' ' . $section_storage->getStorageId()]);
    

    Let's work out why this is happening.

  3. +++ b/core/modules/user/user.module
    @@ -852,7 +852,7 @@ function user_user_role_insert(RoleInterface $role) {
    -      'label' => t('Add the @label role to the selected user(s)', ['@label' => $role->label()]),
    +      'label' => t('Add the @label role to the selected user(s)', ['@label' => $role->label() ?? $role->id()]),
           'configuration' => [
             'rid' => $role->id(),
           ],
    @@ -865,7 +865,7 @@ function user_user_role_insert(RoleInterface $role) {
    
    @@ -865,7 +865,7 @@ function user_user_role_insert(RoleInterface $role) {
         $action = Action::create([
           'id' => $remove_id,
           'type' => 'user',
    -      'label' => t('Remove the @label role from the selected user(s)', ['@label' => $role->label()]),
    +      'label' => t('Remove the @label role from the selected user(s)', ['@label' => $role->label() ?? $role->id()]),
    

    I don't like this change - pretty sure it is due to test roles with no labels. Let's fix that.

  4. +++ b/core/modules/workspaces/src/WorkspaceRepository.php
    @@ -62,7 +62,7 @@ public function loadTree() {
    -        return strnatcasecmp($a->label(), $b->label());
    +        return strnatcasecmp($a->label() ?? $a->id(), $b->label() ?? $a->id());
    

    I don't like this change - label is required for workflows - pretty sure this is due to tests.

I've added some exceptions so we can break things to find out where the NULLs are coming from.

I still think that generic entity code has to support NULL labels as that is allowed by the interface but on entities where the label is required we should be making sure it is there.

longwave’s picture

> I still think that generic entity code has to support NULL labels as that is allowed by the interface but on entities where the label is required we should be making sure it is there.

+1 - was looking at this patch while you posted the above comments and agree with this, in concrete cases we should figure out why the label is NULL (probably tests, as you say) but in the generic case we still need to allow it.

Status: Needs review » Needs work

The last submitted patch, 10: 3232673-10.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
6.81 KB
31.97 KB

Fixing the tests... leaving the exceptions in until we're green.

alexpott’s picture

Fixing the layout one too...

The last submitted patch, 13: 3232673-13.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 14: 3232673-14.patch, failed testing. View results

andypost’s picture

+++ b/core/modules/user/user.module
@@ -847,6 +847,10 @@ function user_user_role_insert(RoleInterface $role) {
+  if ($role->label() === NULL) {
+    throw new LogicException('Label can\'t be NULL it is required.');

+++ b/core/modules/workspaces/src/WorkspaceRepository.php
@@ -62,6 +62,9 @@ public function loadTree() {
+        if ($a->label() === NULL || $b->label() === NULL) {
+          throw new \LogicException('labels are required');

Maybe it could use asserts?

alexpott’s picture

Status: Needs work » Needs review
FileSize
2.15 KB
34.31 KB

@andypost that's a good idea. These exceptions are here just for testing and I was planning to remove them but I think once I've got this green I'll change them to asserts and add tets.

alexpott’s picture

We're green - changed everything to assertions. I thought about adding tests but I'm not sure it is worth it. The advantage of adding assertions is that tests will fail even on PHP versions less than 8.1.

I disagree that a change record is needed here. There is no real change here. Code like user_user_role_insert() always required a label to work as expected and in regular runtime you can't create a role without a label via the UI.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
    @@ -352,7 +352,7 @@ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTA
         foreach ($entities as $entity_id => $entity) {
           $bundle = $entity->bundle();
    -      $options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label());
    +      $options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label() ?? '');
         }
     
    

    should we fall back to the ID here? an empty string doesn't make sense in an option list?

  2. +++ b/core/modules/comment/tests/src/Kernel/Views/CommentAdminViewTest.php
    @@ -78,6 +78,7 @@ protected function setUp($import_test_views = TRUE): void {
         $admin_role = Role::create([
           'id' => 'admin',
           'permissions' => ['administer comments', 'skip comment approval'],
    +      'label' => $this->randomString(),
    

    most of the roles with missing label have a fixed id: admin, authenticated, test_something. should we just use the same string for the label to make it easier to identify if shown anywhere in debug output or so? clearly wasn't really needed here, but do we really need random labels?

  3. +++ b/core/modules/jsonapi/tests/src/Functional/RoleTest.php
    @@ -53,7 +53,7 @@ protected function setUpAuthorization($method) {
         $role = Role::create([
           'id' => 'llama',
    -      'name' => $this->randomString(),
    +      'label' => 'Llama',
         ]);
    

    here you did what I suggested, as it's actually used below.

    Also, I thought enabled config entities do have schema validation? but I guess we have no way to make a field required there?

alexpott’s picture

Thanks for the review @Berdir

Re #20.1 I experimented with that in #7... we have extensive test coverage of this in \Drupal\Tests\system\Kernel\Entity\EntityReferenceSelectionReferenceableTest - I think if we want to change that we should do that in a follow-up.

Re #20.2 and .3 sure we can do that. It didn't seem to matter - you've always got the ID. Part of me is tempted to change the config behaviour and if there is no label use the ID instead. But that's a much larger issue and more change. Will use the ID.

[EDIT: fixed where we have extensive test coverage... copy pasta error - thanks @Berdir]

alexpott’s picture

Less randomness....

The last submitted patch, 19: 3232673-19.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 22: 3232673-22.patch, failed testing. View results

Berdir’s picture

Fine to leave the options thing to a separate issue if that's breaking existing tests. Is that the wrong issue though, that doesn't look related?

Yeah, test labels aren't a big deal, but IIRC you were often a proponent of _not_ using random strings when we aren't specifically testing for that too :)

alexpott’s picture

Status: Needs work » Needs review
FileSize
3.39 KB
35.23 KB

Fixing the assertions and making the docs for label() correct with the reality.

alexpott’s picture

@Berdir checkout #7 - that patch did...

+++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
@@ -352,8 +352,7 @@ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTA
     $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
     foreach ($entities as $entity_id => $entity) {
       $bundle = $entity->bundle();
-      // @todo or should we use the ID here?
-      $options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label() ?? '');
+      $options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label() ?? $entity_id);
     }

The fails are in \Drupal\Tests\system\Kernel\Entity\EntityReferenceSelectionReferenceableTest - the link in #21 was a copy paste error that I'll fix...

Status: Needs review » Needs work

The last submitted patch, 26: 3232673-26.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
961 bytes
35.24 KB

Whoops.

longwave’s picture

Couple of questions/comments but everything else looks OK to me now. Would be nice to get #2939931: Add an implementation of EntityDisplayBase::label() done and then we can undo some of this again, I think.

  1. +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
    @@ -231,8 +231,8 @@ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b)
    +      $a_label = $a->label() ?? '';
    +      $b_label = $b->label() ?? '';
    

    Would having a secondary sort by ID mean a more stable sort? But then all entities should really have labels and I bet this only affects tests, so it probably doesn't matter.

  2. +++ b/core/modules/workspaces/src/WorkspaceRepository.php
    @@ -62,6 +63,7 @@ public function loadTree() {
    +        assert(Inspector::assertStringable($a->label()) && Inspector::assertStringable($b->label()), 'Workspace labels are expected to be a string.');
    

    I guess there will never be a huge number of workspaces, but this might be asserted many times as it's inside the sort callback.

    Should we just fall back to ID or '' instead here?

alexpott’s picture

Thanks for the review @longwave

RE #30.1 the problem with changing this to sort by ID when the label is missing is that this is a change in behaviour and then you are sorting the proverbial apples and pears. That's why I went for the no change to HEAD fix.

RE #30.2 well asserts should be disabled in production so the number of times is immaterial. The reason I didn't fallback to ID is the same as above - we'd then be sorting apples and pears.

I think it is best to rely on PHP 8's stable sorting for when the labels are equivalent. See https://wiki.php.net/rfc/stable_sorting - in practical terms this means the ID is used for sorting because the most common db (mysql) tends to return rows in the order of ID.

longwave’s picture

Status: Needs review » Reviewed & tested by the community

OK, keeping existing behaviour makes sense. Everything else looks sane so this is RTBC.

> the most common db (mysql) tends to return rows in the order of ID

PHP stable sorts will only be stable if this assumption holds, but in practice I have found making this assumption can be dangerous, especially across environments that ostensibly have the same database rows but the results of an unsorted query can be consistently returned in different orders.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Some of this starts to look a bit crufty, but I guess that's what you get with stricter typing from PHP and less strict behaviour from us, and unless we make label required to return a string we don't have other options. Making minimal changes to avoid changing current behaviour (apart from the UI message substitutions using ID as a fallback) all makes sense.

Committed 9da1866 and pushed to 9.3.x. Thanks!

  • catch committed 9da1866 on 9.3.x
    Issue #3232673 by alexpott, longwave, Berdir, andypost: \Drupal\Core\...
catch’s picture

Status: Fixed » Closed (fixed)

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