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:
- See how many deltas there are, and then
- keep adding additional ones until we get empty results from the computation.
| Comment | File | Size | Author |
|---|---|---|---|
| #10 | interdiff-3142725-8-10.diff | 3.52 KB | colan |
| #10 | computed_field-computed_cardinality-3142725-10.patch | 7.91 KB | colan |
Comments
Comment #2
colanComment #3
colanAdded some more clarification.
Comment #4
colanComment #5
colanHere's a proof of concept, which needs to be turned into a service and refactored to minimize the amount of non-OO code.
Comment #6
colanHere it is.
Comment #7
mparker17The 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:
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...When I go to test it...
4b8a10b)computed_field) module. Apply the patch in #6./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".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()......
$this->entityFieldManager->getFieldDefinitions()calls\Drupal\Core\Entity\EntityFieldManager::getBaseFieldDefinitions()which calls::buildBaseFieldDefinitions(), which throws the error.A quick smoke test suggests...
... 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\FieldableEntityInterfaceis too broad of a condition, then I'd see if\Drupal\Core\Entity\ContentEntityInterfaceis 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).
Comment #8
colanThanks 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.
Comment #10
colanCoding standards fixes.
Comment #13
colan