While we currently support fields with multiple values, we have no control over the number of them that will be set (i.e. the field cardinality). The computed code is executed on a per-delta basis so we don't get to choose how many of them there are. That is, the number of values will be determined before user-defined code runs. So if the computations determine that there should be X values, those with indices (deltas) past the pre-set number will never get set.

Use case: I've got a content type with many assignee fields for different review streams, where different users are meant to review different aspects. So I'd like a computed field housing the complete list of users that are assigned to the node, listing all of the user IDs from all of the fields. The cardinality here can be variable because each of the assignee fields are multi-valued, and many can be added or deleted every time the node is edited.

For each computed field, the cardinality is initially set to two (2), as this is the default number of values provided by the Add more multi-value widget. Once there are values in the DB, that's the number of values that will be processed. However, on the next computation, the form will add an additional delta because it's providing a placeholder for users to add additional values (and of course more can be added by clicking on the Add more button). In our case, as we're computing values, we don't care about any of this, and typically have the widget hidden; we don't want users entering them.

So, by default, if the computation determines that there are to be 5 values, they won't all be set until after four (4) saves.

  • First save: 2 values
  • Second save: 3 values
  • Third save: 4 values
  • Fourth save: 5 values
  • Fifth save: 5 values
  • ...

This isn't acceptable if we need the correct number of computed values to be set correctly every time. In the above example, five (5) values should be saved the first time.

The problem is that there's no means by which field definition plug-ins can specify the cardinality. Drupal core wasn't designed to allow fields to specify a programmable number of deltas.

So without adding this feature to core, it's necessary to hook into the process somewhere to:

  • Add additional placeholders before computations (say by adding the missing deltas to the form), or
  • Add the missing values afterwards, if they're in fact missing.

Altering the form would be tricky because we'd by working with the form API instead of the entity API, and we'd be dealing with lots of arrays and less OO; we wouldn't have an entity to work with just yet. But doing it after the entity gets saved wouldn't be very performant: we'd be saving the entity twice instead of once.

After some research on hooks (and the order that they run in), I think the best approach would be to implement a hook_entity_presave(), which (as per the docs), runs after the entity's fields' preSave methods. So some of the values would have been filled in; we would simply need to add the remaining deltas.

We should be able to:

  1. See how many deltas there are, and then
  2. keep adding additional ones until we get empty results from the computation.

Comments

colan created an issue. See original summary.

colan’s picture

Title: Add support for variable cardinality » Add support for computed cardinality
Issue summary: View changes
colan’s picture

Issue summary: View changes

Added some more clarification.

colan’s picture

colan’s picture

Status: Active » Needs work
StatusFileSize
new1.86 KB

Here's a proof of concept, which needs to be turned into a service and refactored to minimize the amount of non-OO code.

colan’s picture

Status: Needs work » Needs review
StatusFileSize
new7.8 KB
new8.39 KB

Here it is.

mparker17’s picture

Status: Needs review » Needs work

The code is very clear! Found a bug that prevents me from adding fields of any kind, computed fields included (see below). First, some non-blocking nit-picks:

  1. generateMissingValues() returns void. I'm not exactly sure where hook_entity_presave() is called (\Drupal\Core\Entity\EntityStorageBase::invokeHook()? \Drupal\Core\Entity\ContentEntityStorageBase::invokeHook()? \Drupal\Core\Config\Entity\ConfigEntityStorage::invokeHook()?) but its docs don't mention it expecting any return values, so the return here is unnecessary and/or a bit confusing...
    function computed_field_entity_presave(EntityInterface $entity) {
      return \Drupal::service('computed_field.multiple_values_generator')->setEntity($entity)->generateMissingValues();
    }
    
  2. I'd change the first line of the comment to say "Fetches the entity currently in use by the service."...
    /**
     * Fetches the site currently in use by the service.
     *
     * @return \Drupal\Core\Entity\EntityInterface|null
     *   The entity currently in use by the service.
     * @throws \Exception
     *   If the entity is missing.
     */
    protected function getEntity() {
      if (is_null($this->entity)) {
        throw new \Exception('This operation requires that the service be set with an entity.');
      }
      return $this->entity;
    }
    

When I go to test it...

  1. Download Drupal 8.9.0 and clone Computed Field branch 8.x-2.x (when I wrote this, the latest commit was 4b8a10b)
  2. Install Drupal using the Standard install profile. When that is done, install only the Computed Field (computed_field) module. Apply the patch in #6.
  3. Go to /admin/structure/types/manage/page/fields, add a "Computed (Integer)" field named "Number of assignees" (Machine name: field_number_of_assignees). Click "Save and continue".
    • Actual behavior: I see an error message "There was a problem creating field Number of assignees: Getting the base fields is not supported for entity type Field storage." Further testing shows this error occurs for any field I try to install. This error only happens while the patch is applied.
    • Expected behavior: I should be able to choose the field settings next, including the field cardinality.

Grepping for "Getting the base fields is not supported for entity type", it is thrown in \Drupal\Core\Entity\EntityFieldManager::buildBaseFieldDefinitions().

Notably, computed_field_entity_presave() runs for all entities, (including configuration entities like Field storage), so in \Drupal\computed_field\MultipleValuesGenerator::generateMissingValues()...

public function generateMissingValues() {
  $entity = $this->getEntity();
  $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());

  foreach ($field_definitions as $field_id => $field_definition) {
    if ($this->fieldIsNotAComputedField($field_definition)) {
      continue;
    }

    $this->generateMissingValuesForField($field_id, $field_definition);
  }
}

... $this->entityFieldManager->getFieldDefinitions() calls \Drupal\Core\Entity\EntityFieldManager::getBaseFieldDefinitions() which calls ::buildBaseFieldDefinitions(), which throws the error.

A quick smoke test suggests...

function computed_field_entity_presave(EntityInterface $entity) {
  if ($entity instanceof \Drupal\Core\Entity\FieldableEntityInterface) {
    return \Drupal::service('computed_field.multiple_values_generator')->setEntity($entity)->generateMissingValues();
  }
}

... fixes the problem; but since that conditional was based on my gut instinct about the problem, you should probably check whether that leaves out any important cases.

If it turns out that instanceof \Drupal\Core\Entity\FieldableEntityInterface is too broad of a condition, then I'd see if \Drupal\Core\Entity\ContentEntityInterface is better.

Note also that In particular, Path Aliases are now revisionable entities, and they seem to have a number of Entity API special cases and implement both FieldableEntityInterface and ContentEntityInterface, so I'd check that case (and as a maintainer, you can make the call not to support attaching computed fields to them at this time).

colan’s picture

Status: Needs work » Needs review
StatusFileSize
new7.93 KB
new1.68 KB

Thanks for the review & testing! As it was something I was thinking about anyway, this pushed me to get serious about automated testing via #3151219: Add tests. (I enabled it for the branch so it'll at least ensure the patch applies, but there are no tests yet.)

Anyway, I believe this resolves everything above.

The last submitted patch, 8: computed_field-computed_cardinality-3142725-8.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

colan’s picture

StatusFileSize
new7.91 KB
new3.52 KB

Coding standards fixes.

  • colan committed af74aa5 on 3.x
    Issue #3142725 by colan: Generated the multiple values that were missing...
colan’s picture

Version: 8.x-2.x-dev » 3.x-dev
Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

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