BaseFieldDefinition::isStorageRequired wrongly falls back to ::isRequired

Created on 23 September 2019, about 5 years ago
Updated 30 January 2023, almost 2 years ago

Problem/Motivation

\Drupal\Core\Field\RequiredFieldStorageDefinitionInterface and its only method ::isStorageRequired() have been introduced in #2390495: Support marking field storage definitions as required β†’ .
BaseFieldDefinition implements that interface and method and also provides its own setter method ::setStorageRequired().
The method implementation \Drupal\Core\Field\BaseFieldDefinition::isStorageRequired() however falls back to \Drupal\Core\Field\BaseFieldDefinition::isRequired() if the setter has not been used.

This means that defining a field like this:

$field = BaseFieldDefinition::create('string')
        ->setRequired(TRUE);

will result automatically into a field, whose storage is required and the following will return TRUE even without it being explicitly set:
$field->isStorageRequired();

However ::isStorageRequired() is documented like this:

   * If a field storage is required, NOT NULL constraints will be added
   * automatically for the required properties of a field type.

Core is not doing that yet automatically, but there is an issue to implement that, which is in an almost finished state - #2841291: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field β†’ .

So now we have 2 methods for whether a field is required - isRequired and isStorageRequired, where the first is used for form inputs and validation purposes, while the second is used to flag the storage of the field in a shared table as required - for example by adding the NOT NULL constraint to the schema as described in the method's documentation.

Please note that we even already have an implementation in core, which is checking whether a field is storage required and if so a NOT NULL constraint is being added. From \Drupal\comment\CommentStorageSchema::getSharedTableFieldSchema():

        case 'entity_type':
        case 'field_name':
          assert($storage_definition instanceof RequiredFieldStorageDefinitionInterface);
          if ($storage_definition->isStorageRequired()) {
            // The 'entity_type' and 'field_name' are required so they also need
            // to be marked as NOT NULL.
            $schema['fields'][$field_name]['not null'] = TRUE;
          }
          break;

Unfortunately there are 4 problems with ::isStorageRequired() falling back to ::isRequired():

  1. If we clone a required base field and return it as non-required from an implementation of hook_entity_bundle_field_info() for a specific bundle, then we'll still not be able to save an entity for that bundle without providing a value for the overridden field as the storage might already be created with a NOT NULL constraint for that field.
  2. It is impossible to make the base field non-required after it was installed even if we haven't explicitly requested the NOT NULL constraint.
  3. We cannot yet properly update \Drupal\Core\Field\BaseFieldDefinition::createFromFieldStorageDefinition() and let it inherit the storage_required flag, because it is wrong to set the flag if it wasn't set on the passed field storage definition, but TRUE is returned because of the fallback to ::isRequired().
  4. This issue is also a blocker for #3014760: [PP-2] Create an abstract composite field definition/storage definition and update BaseFieldDefinition to use it β†’ .
    The proposal there is that the field storage definition becomes a property on the base field definition in order to achieve a clean separation between a field storage and a field definition similar to configurable fields. As such it is not possible for the storage definition to call its parent - the field definition from within ::isStorageRequired() and access ::isRequired().

As the use cases and meaning of a required and storage required are different a check for storage required should not fall back to required.

It is worth mentioning that regarding bundle fields only FieldConfig is aware of the required property and FieldStorageConfig does not know anything about it.

Only FieldConfigInterface extends the FieldDefinitionInterface where ::isRequired() is defined. The FieldStorageDefinitionInterface does not extend FieldDefinitionInterface.

This does not match BaseFieldDefinition where everything belongs together, but only on bundle fields we have a clear separation between what is a field definition setting and what is a field storage setting.

Currently there is no workaround for allowing a field on a specific bundle to not be storage required, if a field has gotten a NOT NULL constraint as documented just because it has been simply flagged as required but not as storage required. Core might not be adding the NOT NULL constraint automatically yet, but has documented that it should be added if the field is storage required and doing that in custom or contrib code is therefore perfectly valid and according to the core's documentation. Considering the 4 different problems this is not to be confused with simply being a documentation issue.

Steps to reproduce

The test from the attached patch in #2 πŸ› BaseFieldDefinition::isStorageRequired wrongly falls back to ::isRequired Needs work - \Drupal\KernelTests\Core\Entity\EntityFieldTest::testRequiredChangeOnBundleOverride() is showing an example of how core behaves with and without automatic NOT NULL constraint based on the output of ::isStorageRequired().

There we create a required base field and two bundles, where for the second bundle we return an altered base field definition by flagging the field as non-required.

This represents a perfectly valid use case as we might want something to be optional on one bundle, but required on another.

Then we create and save an entity for the bundle where the field is not required.
First we do that with current behaviour where NOT NULL constraints are not yet added automatically based on the output of ::isStorageRequired(). We assert that the entity is saved properly.
Then we recreate the entity tables by using a different storage schema handler, which adds the NOT NULL constraint to storage required fields. As one can see from the test results we cannot anymore save an entity for the bundle without providing a value for a field which isn't required on that bundle, but is required on the first bundle.

Proposed resolution

As the use cases and meaning of a required and storage required are different a check for storage required should not fall back to required.

Therefore we need to remove the fallback to ::isRequired() from within ::isStorageRequired().

If we want a field storage to be required, then we should explicitly request that by calling ::setStorageRequired(TRUE).

Remaining tasks

Review.

User interface changes

None.

API changes

BaseFieldDefinition::isStorageRequired() does not fall back to isRequired anymore.

If we want a field storage to be required, then we should explicitly request that by calling ::setStorageRequired(TRUE).

Data model changes

None.

Release notes snippet

πŸ› Bug report
Status

Needs work

Version

10.1 ✨

Component
FieldΒ  β†’

Last updated 5 days ago

Created by

πŸ‡©πŸ‡ͺGermany hchonov πŸ‡ͺπŸ‡ΊπŸ‡©πŸ‡ͺπŸ‡§πŸ‡¬

Live updates comments and jobs are added and updated live.
  • Needs subsystem maintainer review

    It is used to alert the maintainer(s) of a particular core subsystem that an issue significantly impacts their subsystem, and their signoff is needed (see the governance policy draft for more information). Also, if you use this tag, make sure the issue component is set to the correct subsystem. If an issue significantly impacts more than one subsystem, use needs framework manager review instead.

  • Regression

    It restores functionality that was present in earlier versions.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • The Needs Review Queue Bot β†’ tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

    Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

    Consult the Drupal Contributor Guide β†’ to find step-by-step guides for working with issues.

Production build 0.71.5 2024