Remove exception when accessing a non-existing field with ContentEntityInterface::get()

Created on 30 November 2024, about 2 months ago

Problem/Motivation

There is an attempt to remove magic access for fields on content entities πŸ“Œ [meta] Deprecate __get/__set() on ContentEntityBase Needs work

One concern mentioned there is that $entity->get('field_name') throws an exception, while $entity->field_name does not.

This results in pretty unwieldy code:

if ($entity->hasField('field_name') && $entity->get('field_name')->value)

This made sense many years ago when it was added, but since then we have nullable operator, so if we remove the exception, we can do this:

if ($entity->get('field_name')?->value)

Steps to reproduce

Proposed resolution

Remove the exception.

Remaining tasks

Per my comment in the meta issue, there is one problem with this:

The only problem I see is someone is relying on the exception and is catching that, then it would result in a php warning or something instead. like this:

Unsure how to approach that:

1. We could ignore that, and that code will just need to be be adjusted. I hope people haven't done that a lot :)
2. We trigger a deprecation before throwing the exception and remove it in D12. Somewhat unlikely that code doing that will actually see that deprecation, but it gives it a chance and time to adjust.

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

πŸ“Œ Task
Status

Active

Version

11.1 πŸ”₯

Component

entity system

Created by

πŸ‡¨πŸ‡­Switzerland berdir Switzerland

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @berdir
  • Merge request !10406Resolve #3490787 "Remove exception when" β†’ (Open) created by berdir
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Proof of Concept MR

  • Pipeline finished with Failed
    about 2 months ago
    Total: 685s
    #355071
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Turns out that core does on fact have one example that relies on the exception being thrown.

  • Pipeline finished with Failed
    about 2 months ago
    Total: 721s
    #355085
  • πŸ‡ΊπŸ‡ΈUnited States smustgrave

    But good news is that the functional test was random.

  • I actually created πŸ“Œ Provide FieldableEntityInterface get() functionality that returns NULL instead of throwing exception Active before finding this issue, so closed that one as a dupe. There I proposed adding a second parameter to get() or a new get-like method that returns NULL instead of throwing an exception, to account for anything relying on the exception being thrown. So either

    /**
       * Gets a field item list.
       *
       * @param string $field_name
       *   The name of the field to get; e.g., 'title' or 'name'.
       *
       * @param bool $throw_exception
       *   Whether to throw an exception if field name is invalid or return NULL.
       *
       * @return \Drupal\Core\Field\FieldItemListInterface|null
       *   The field item list, containing the field items.
       *
       * @throws \InvalidArgumentException
       *   If an invalid field name is given.
       */
      public function get($field_name, bool $throw_exception = TRUE);
    

    or

    /**
       * Gets a field item list safely.
       *
       * @param string $field_name
       *   The name of the field to get; e.g., 'title' or 'name'.
       *
       * @return \Drupal\Core\Field\FieldItemListInterface|null
       *   The field item list, containing the field items, or NULL if invalid field name.
       */
      public function safeGet(string $field_name): ?FieldItemListInterface {
        try {
          return $this->get($field_name);
        }
        catch (\InvalidArgumentException) {
          return NULL;
        }
      }
    
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    A parameter is the solution to the return behavior BC break, but it comes with its own BC problems of adding arguments to an existing method on an interface. Likely has a smaller impact though, I hope ContentEntityBase::get() wasn't customized much. We could introduce it with a default of TRUE and then change that.

    In slack fetch() was also proposed, but I'm not really sure that "Usually fetch calls considered to be returning something or null" is such a common standard, certainly haven't heard about that but might be common in some frameworks? Fetch to me sounds like it actively fetches something from somewhere, while get just returns the available data.

  • Changing the signature to get() to have two arguments or adding a "safe" method to FieldableEntityInterface would both introduce a BC break. I think it'd make more sense to do the former, if for no other reason that it doesn't require an additional change to the Twig allow list. (Though I imagine most Twig templates use the magic getter instead of get).

  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    I think I'd also prefer an argument on the existing method, that also matches ContainerInterface::get(). The problem is that even if we don't add it to the interface for BC, it still breaks subclasses that don't have it (you can add optional parameters that parent classes/interfaces don't have, but you can't not have optional parameters that parent classes/interfaces have).

    The only BC safe way would be some func_get_args() trickery.

    > (Though I imagine most Twig templates use the magic getter instead of get).

    FWIW, I created this issue from the discussion in πŸ“Œ [meta] Deprecate __get/__set() on ContentEntityBase Needs work , which currently requires to change twig templates. I want to create a twig issue to improve it's handling of content entities anyway, so the twig templates remain as they are, but node.foo.value actually translates to $node->get('foo')->value by default. Either with throw_exception = FALSE OR a hasField() check. And on the field level too, because node.reference_field.entity is really unpredictable right now in twig (if there is no entity, it falls back to getEntity(), which returns the host node. even I got bitten by that several times).

  • The only BC safe way would be some func_get_args() trickery.

    I think this might make sense, along with a deprecation triggered. Is there also a way to annotate in the interface that the method parameters will change?

Production build 0.71.5 2024