Problem/Motivation

Notice: Undefined index: #item in user_user_view_alter() (line 409 of /Users/jungle/repo/drupal/web/core/modules/user/user.module) #0 

The line 409 of the file core/modules/user/user.module:

    $item = $build['user_picture'][$key]['#item'];

The background is that I was Implementing the avatar_field_formatter module, found that the #item in user_user_view_alter() dose not exist. As a custom image formatter, what the module does is if the image is empty, display an avatar letter generated instead.

The user_user_view_alter implementation in core assuming that the returned non-empty render array of user picture's field formatter always has #item key(s). Considering another use case, for an empty user picture, one wants to display a plain text: "No avatar" by using a custom field formatter. It may throw the same error too.

The whole function relevant:


/**
 * Implements hook_ENTITY_TYPE_view_alter() for user entities.
 *
 * This function adds a default alt tag to the user_picture field to maintain
 * accessibility.
 */
function user_user_view_alter(array &$build, UserInterface $account, EntityViewDisplayInterface $display) {
  if (user_picture_enabled() && !empty($build['user_picture'])) {
    foreach (Element::children($build['user_picture']) as $key) {
      $item = $build['user_picture'][$key]['#item'];
      if (!$item->get('alt')->getValue()) {
        $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
      }
    }
  }
}

Steps to reproduce

  1. Apply patch: 3072305-11-test-only.patch in #11
  2. Add $settings['extension_discovery_scan_tests'] = TRUE; to settings.php
  3. Install Image test module (module machine name: image_module_test)
  4. Login as user 1
  5. Visit /admin/config/people/accounts/display
  6. Select Dummy image formatter for Picture field
  7. Visit /user/1, 500 error, reproduced.

Proposed resolution

Check the existence of #item first, and make sure it's an instance of ImageItem for further processing

+++ b/core/modules/user/user.module
@@ -416,9 +417,14 @@ function user_user_view(array &$build, UserInterface $account, EntityViewDisplay
     foreach (Element::children($build['user_picture']) as $key) {
+      if (!isset( $build['user_picture'][$key]['#item'])) {
+        continue;
+      }
       $item = $build['user_picture'][$key]['#item'];
-      if (!$item->get('alt')->getValue()) {
-        $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
+      if ($item instanceof ImageItem) {
+        if (!$item->get('alt')->getValue()) {
+          $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
+        }
       }

Remaining tasks

Needs review

How to test the patch manually

  1. Apply patch: 3072305-11.patch in #11
  2. Add $settings['extension_discovery_scan_tests'] = TRUE; to settings.php
  3. Install Image test module (module machine name: image_module_test)
  4. Login as user 1
  5. Visit /admin/config/people/accounts/display
  6. Select Dummy image formatter for Picture field
  7. Visit /user/1, you should see text Dummy which provides by the Dummy image formatter in Image test module.

User interface changes

No

API changes

No

Data model changes

No

CommentFileSizeAuthor
#54 interdiff-51-54.txt1023 bytesjungle
#54 3072305-54.patch3.92 KBjungle
#51 interdiff-44-51.txt568 bytesjungle
#51 3072305-51.patch3.86 KBjungle
#44 3072305-44.patch3.87 KBandypost
#44 interdiff.txt2.86 KBandypost
#43 3072305-43.patch4.44 KBandypost
#43 interdiff.txt3.43 KBandypost
#42 3072305-42.patch3.66 KBjungle
#42 interdiff-35-42.txt1023 bytesjungle
#41 3072305-41.patch3.65 KBjungle
#41 interdiff-35-41.txt1013 bytesjungle
#36 3072305-36-test-only.patch2.52 KBjungle
#35 interdiff-33-35.txt545 bytesjungle
#35 3072305-35.patch3.64 KBjungle
#33 interdiff-30-33.txt1.98 KBjungle
#33 3072305-33.patch3.58 KBjungle
#30 intediff-20-30.txt2.31 KBjungle
#30 3072305-30.patch3.56 KBjungle
#21 3072305-21-test-only.patch2.55 KBjungle
#20 3072305-20.patch3.93 KBjungle
#14 interdiff-11-14.txt730 bytesjungle
#14 3072305-14.patch3.92 KBjungle
#11 3072305-11.patch3.92 KBjungle
#11 3072305-11-test-only.patch2.54 KBjungle
#2 undefined-index-item-3072305-2.patch750 bytesjungle
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

jungle created an issue. See original summary.

jungle’s picture

jungle’s picture

Assigned: jungle » Unassigned
Status: Active » Needs review
jungle’s picture

Issue summary: View changes
jungle’s picture

Issue summary: View changes
jungle’s picture

Issue summary: View changes
lawxen’s picture

Call to a member function get() on null in user_user_view_alter() (line 410 of /app/www/cmd/web/core/modules/user/user.module)
My error is on line 410, not 409 .

if (!$item->get('alt')->getValue()) {
lawxen’s picture

BTW @jungle thanks for writting the module [avatar_field_formatter](https://www.drupal.org/project/avatar_field_formatter), very useful.

But

A known core issue: Notice: Undefined index: #item in user_user_view_alter(), so you have to apply this patch to make this module works.

This description on [avatar_field_formatter](https://www.drupal.org/project/avatar_field_formatter) is not accurate.

Because "If we want display user picture on user entity view page" is the only condition that we should apply this patch.

Even if we use views to display user's picture and use module
[avatar_field_formatter](https://www.drupal.org/project/avatar_field_formatter)'s widget, we don't have need to apply the patch either.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

aleix’s picture

It's true, children 'alt' property must be checked before getting the value.

jungle’s picture

jungle’s picture

Issue summary: View changes

Updated issue summary

  • Added Steps to reproduce
  • Added How to test the patch manually
  • Updated Proposed resolution

The last submitted patch, 11: 3072305-11-test-only.patch, failed testing. View results

jungle’s picture

Fix coding standard

-      if (!isset( $build['user_picture'][$key]['#item'])) {
+      if (!isset($build['user_picture'][$key]['#item'])) {
jungle’s picture

longwave’s picture

#2907329: Add test that user_user_view_alter works with different formatters seems to be the same issue and has a similar fix but simpler test.

jungle’s picture

Thanks for reviewing! They not the same.

+++ b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldFormatter/DummyImageFormatter.php
@@ -0,0 +1,33 @@
+    $elements = [];
+    $elements[] = ['#markup' => 'Dummy'];
+    return $elements;

The dummy field formatter for the image field type, return a simple text. which simulates what my module avatar_field_formatter does, when the image is empty, it returns a render array does not relevant to an image at all. So nothing related to the alt attribute of an image here.

jungle’s picture

Two scopes, the patch of #2907329 does not fix this one, and patch here does not fix #2907329 either.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

jungle’s picture

Issue summary: View changes
Issue tags: +Bug Smash Initiative
FileSize
3.93 KB

Re-rolled the patch against 9.1.x, tagging "Bug Smash Initiative". Re-formatted "Steps to reproduce".

jungle’s picture

Uploading a new(re-rolled) test-only patch.

thursday_bw’s picture

Gave that a quick once over. It's neat and clean and straight forward.

Only some small things to consider.

  1. +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
    @@ -159,4 +168,18 @@ public function saveUserPicture($image) {
    +  /**
    +   * Tests dummy User picture formatter.
    +   */
    +  public function testDummyUserPictureFormatter() {
    

    Possible to refactor this into a kernel test?

    If your test implements formInterface, you can generate inputs the the build() method and submit it, in a kernel test

  2. +++ b/core/modules/user/user.module
    @@ -319,9 +320,14 @@ function user_user_view(array &$build, UserInterface $account, EntityViewDisplay
    +      if ($item instanceof ImageItem) {
    

    Relexively I bork at the need for instanceOf, we are in a procdural function though. I imagine the refactoring necessary to avoid it would be heavy

thursday_bw’s picture

Status: Needs review » Needs work
jungle’s picture

Status: Needs work » Needs review

Thanks, @thursday_bw for your review.

Re:

  1. #22.1 In this case, I reused the UserPictureTest, the benefit of refactoring to get better performance is ignorable, perhaps.
  2. #22.2, it's a bad practise that use instanceof against a class instead of an interface, however, no applicable interface to replace it here.

After chatting with @thursday_bw in slack, he agreed to set back to NR.

andypost’s picture

Except nitpick looks good to go

+++ b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldFormatter/DummyImageFormatter.php
@@ -0,0 +1,33 @@
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $elements[] = ['#markup' => 'Dummy'];
+    return $elements;

It's more readable when

return [
  ['#markup' => 'Dummy'],
];
larowlan’s picture

+++ b/core/modules/user/user.module
@@ -319,9 +320,14 @@ function user_user_view(array &$build, UserInterface $account, EntityViewDisplay
-      if (!$item->get('alt')->getValue()) {
-        $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
+      if ($item instanceof ImageItem) {

we can do this as


if ($item && array_key_exists('alt', $item->getProperties)) {

and avoid instanceof, which is generally a code-smell that indicates a missing abstraction.

But in this case, we have that abstraction, its getProperties

Which is the same thing @thursday_bw flagged too

thursday_bw’s picture

The vulnerable aspect of testing against instanceof is it prevents imageItem from being extended or replaced by a new class. eg

You do have access to all the methods of the fieldItemInterface. That opens up some possibilities.
You can also easily add a new interface.

class MyFancyImageItem extends ImageItem {}
hook_form_alter('user_profile_form' ...) {
  $form['myOldImageField']['#type'] = 'my_fancy_image_item';
}

Should someone do something like this, then test against instanceof 'ImageItem' will fail, and produce an error.

Recommendation 1

If you instead test against the object's capabilities then the person extending imageField has more control over how your
code deals with. But it wont' break it purely by overriding the class.

You'll need to dig into what data you have available for you to most flexibly test against.
Non functional, non tested example:

      if ($item->getFieldDefinition()['somekey'] == 'someidentifyingvalue') {
        if (!$item->get('alt')->getValue()) {
          $item->get('alt')->setValue(\Drupal::translation()
            ->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
        }
      }


      // or
      if ($item->fieldSettingsForm()->somePropertyOrMethodOrwhatever() == 'someidentifyingvalue') {
        if (!$item->get('alt')->getValue()) {
          $item->get('alt')->setValue(\Drupal::translation()
            ->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
        }
      }

Recommendation 2

Another option, that may have similar drawbacks is the following:

Add an interface for imageItem that extends fieldItemInterface

Create the interface:

+ interface ImageItemInterface extends FieldItemInterface {
+ ...
+ }

Implement the interface:

- class ImageItem extends FileItem {
- ...
- }
+ class ImageItem extends FileItem implements ImageItemInterface {
+ ...
+ }

Then you could test that the object implements the ImageItemInterface. like this:

      $item = $build['user_picture'][$key]['#item'];
      $interfaces = class_implements();
      if (isset($interfaces['ImageItemInterface'])) {
        if (!$item->get('alt')->getValue()) {
          $item->get('alt')->setValue(\Drupal::translation()
            ->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
        }
      }

Now ImageInterface can still be happily extended, and you're not writing ugly code that analyses the field settings.

thursday_bw’s picture

Status: Needs review » Needs work
thursday_bw’s picture

And @larowlan has honed in on exactly what we needed to know. With much less words too ;)

jungle’s picture

Status: Needs work » Needs review
FileSize
3.56 KB
2.31 KB
  1. Addressed #25, Thanks, @andypost,
  2. -  if (user_picture_enabled() && !empty($build['user_picture'])) {
    +  if (!empty($build['user_picture'] && user_picture_enabled())) {
    

    Swapped the order, !empty($build['user_picture'] looks less expensive

  3.   /**
       * Gets a property object.
       *
       * @param $property_name
       *   The name of the property to get; e.g., 'title' or 'name'.
       *
       * @return \Drupal\Core\TypedData\TypedDataInterface
       *   The property object.
       *
       * @throws \InvalidArgumentException
       *   If an invalid property name is given.
       * @throws \Drupal\Core\TypedData\Exception\MissingDataException
       *   If the complex data structure is unset and no property can be created.
       */
      public function get($property_name);
    

    the above code snippet is from \Drupal\Core\TypedData\ComplexDataInterface::get()

    Meanwhile, interface FieldItemInterface extends ComplexDataInterface {

           if (!isset($build['user_picture'][$key]['#item'])) {
             continue;
           }
    +      /** @var \Drupal\Core\Field\FieldItemInterface $item */
           $item = $build['user_picture'][$key]['#item'];
    -      if ($item instanceof ImageItem) {
    -        if (!$item->get('alt')->getValue()) {
    -          $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
    -        }
    +      if ($item->get('alt')) {
    +        $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
           }
    
    

    As $item is an instance of FieldItemInterface, so calling $item->get('alt') directly.

  4. Re #26, i trid if ($item && array_key_exists('alt', $item->getProperties)) {

    Somehow, $item->getProperties returns NULL, which makes the array_key_exists() call complaining that the 2nd parameter could not be NULL

    Thanks, @lawxen for reviewing.

  5. Re #27, thanks for your recommendations, let's see if if ($item->get('alt')) { works.

    Thanks, @thursday_bw for reviewing again.

Status: Needs review » Needs work

The last submitted patch, 30: 3072305-30.patch, failed testing. View results

larowlan’s picture

Looking nice.

  1. +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
    @@ -159,4 +168,18 @@ public function saveUserPicture($image) {
    +    $this->drupalLogin($this->webUser);
    +    // Set dummy_image_formatter to the default view mode of user entity.
    +    EntityViewDisplay::load('user.user.default')->setComponent('user_picture', [
    +      'region' => 'content',
    +      'type' => 'dummy_image_formatter',
    +    ])->save();
    +    $this->drupalGet($this->webUser->toUrl());
    

    micro-optimisation, but we're making two requests to the same URL here because $this->drupalLogin submits to user/login, which is UserLoginForm, which in term does this

    $form_state->setRedirect(
            'entity.user.canonical',
            ['user' => $account->id()]
          );
    

    i.e. goes to the $this->webUser->toUrl(), which we then do again.

    So if we setup the view display first, then did the login, and then asserted we were on the user page (to ensure we are on the correct page) we'd avoid one extra http request.

  2. +++ b/core/modules/user/user.module
    @@ -317,10 +317,14 @@ function user_user_view(array &$build, UserInterface $account, EntityViewDisplay
    -      if (!$item->get('alt')->getValue()) {
    +      if ($item->get('alt')) {
    

    this is a change in behaviour now.

    We need $item->get('alt') && !$item->get('alt')->getValue()

    the original code only runs if there is no value, now we're running it in all cases.

jungle’s picture

Status: Needs work » Needs review
FileSize
3.58 KB
1.98 KB

Addressing #32 and fixing the testing. Thanks for your feedback @lawxen.

andypost’s picture

+++ b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldFormatter/DummyImageFormatter.php
@@ -0,0 +1,33 @@
+      ['#markup' => 'Dummy'],

+++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
@@ -159,4 +168,17 @@ public function saveUserPicture($image) {
+    $this->drupalLogin($this->webUser);
+    $this->assertSession()->statusCodeEquals(200);

I'd extended a test to make sure that "Dummy" is displayed

jungle’s picture

FileSize
3.64 KB
545 bytes

Re #34, good point. addressing #34. Thanks @andypost!

jungle’s picture

FileSize
2.52 KB

A test-only patch inlines with the patch in #35.

jungle’s picture

Hidden the patch file unintentionally, displaying it.

Status: Needs review » Needs work

The last submitted patch, 36: 3072305-36-test-only.patch, failed testing. View results

andypost’s picture

Status: Needs work » Reviewed & tested by the community

Thanks, test patch just should not be last

longwave’s picture

  1. +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
    @@ -42,6 +43,14 @@ class UserPictureTest extends BrowserTestBase {
    +   * {@inheritDoc}
    

    Nit: inheritDoc -> inheritdoc

  2. +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
    @@ -159,4 +168,18 @@ public function saveUserPicture($image) {
    +   * Tests dummy User picture formatter.
    

    This isn't a great description, should we explain why we actually need a dummy formatter test?

jungle’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
1013 bytes
3.65 KB

Fixing #40, thanks @longwave!

jungle’s picture

FileSize
1023 bytes
3.66 KB

Adjusted a little bit further for #40.2

andypost’s picture

To address #32.2 - no reason to change it if we sure that element instanceof ImageItem which is only defined by standard profile.

Maybe be can move it there or refactor as user_picture_enabled() not used anymore, looks needs follow-up

Patch installs the testing module for only this case (to not affect existing)

andypost’s picture

FileSize
2.86 KB
3.87 KB

wrong patch and interdiff (other test cases were disabled for debug)

andypost’s picture

  1. +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
    @@ -169,9 +161,12 @@ public function saveUserPicture($image) {
    -   * Tests user_user_view_alter() with a dummy image formatter for user picture.
    +   * Tests user picture field with a dummy image formatter for user picture.
    

    still needs better wording(

  2. There's only 4 usage found in contrib for http://grep.xnddx.ru/search?text=user_picture_enabled
jungle’s picture

Issue tags: +Needs followup
  1. +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
    @@ -43,14 +43,6 @@ class UserPictureTest extends BrowserTestBase {
    -  protected static $modules = ['image_module_test'];
    
    @@ -169,9 +161,12 @@ public function saveUserPicture($image) {
    +    \Drupal::service('module_installer')->install(['image_module_test']);
    

    This change is great.

  2. +++ b/core/modules/user/user.module
    @@ -317,14 +318,16 @@ function user_user_view(array &$build, UserInterface $account, EntityViewDisplay
    -  if (isset($build['user_picture']) && !empty($build['user_picture']) && user_picture_enabled()) {
    +  if (!empty($build['user_picture']) && user_picture_enabled()) {
    
    Maybe be can move it there or refactor as user_picture_enabled() not used anymore, looks needs follow-up

    Tagging needs follow-up.

andypost’s picture

jungle’s picture

  1. +++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
    @@ -169,9 +161,12 @@ public function saveUserPicture($image) {
    -   * Tests user_user_view_alter() with a dummy image formatter for user picture.
    +   * Tests user picture field with a dummy image formatter for user picture.
    ...
       public function testUserViewAlter() {
    

    Tests dummy image formatter with the user picture field? and change testUserViewAlter() to testDummyImageFormatter()?

  2. +++ b/core/modules/user/user.module
    @@ -317,14 +318,16 @@ function user_user_view(array &$build, UserInterface $account, EntityViewDisplay
    -  if (isset($build['user_picture']) && !empty($build['user_picture']) && user_picture_enabled()) {
    +  if (!empty($build['user_picture']) && user_picture_enabled()) {
    

    I doubt that the change should be reverted. See interdiff-30-33.txt, in #33

andypost’s picture

longwave’s picture

+++ b/core/modules/user/tests/src/Functional/UserPictureTest.php
@@ -159,4 +160,21 @@ public function saveUserPicture($image) {
+   * Tests user picture field with a dummy image formatter for user picture.

"Tests user picture field with a non-standard field formatter"?

jungle’s picture

FileSize
3.86 KB
568 bytes

@longwave, thanks for your review and suggestion. Changed to what you suggested.

longwave’s picture

Status: Needs review » Reviewed & tested by the community

Looks great!

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/modules/user/user.module
@@ -317,8 +318,14 @@ function user_user_view(array &$build, UserInterface $account, EntityViewDisplay
+        // User picture field is provided by standard profile install, skip
+        // if the render using non-image field item which may miss alt property.

nit: 'User picture field is provided by standard profile install, skip if the render using non-image field item which may miss alt property' is not grammatically correct.

Suggest 'User picture field is provided by standard profile install. If the display is configured to use a different formatter, the #item render key may not exist, or may not be an image field.'

Or similar

Feel free to re-RTBC if the patch only changes the comment.

jungle’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
3.92 KB
1023 bytes

Thanks @lawxen. Changed as suggested, re-RTBC-ing.

larowlan’s picture

Saving review credits

  • larowlan committed aece631 on 9.1.x
    Issue #3072305 by jungle, andypost, thursday_bw, longwave, larowlan:...

  • larowlan committed fe25fd4 on 9.0.x
    Issue #3072305 by jungle, andypost, thursday_bw, longwave, larowlan:...

  • larowlan committed e6a1e87 on 8.9.x
    Issue #3072305 by jungle, andypost, thursday_bw, longwave, larowlan:...
larowlan’s picture

Version: 9.1.x-dev » 8.9.x-dev
Status: Reviewed & tested by the community » Fixed

Committed aece631 and pushed to 9.1.x. Thanks!

As this is a bugfix, and there is no disruption or string changes and we have green runs on 9.0 and 8.9, cherry-picked to those branches too.

Thanks everyone

Status: Fixed » Closed (fixed)

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