Problem/Motivation

Currently the module doesn't specify the cardinality of the computed field.
According the documentation it's necessary to specify the cardinality for multi-value fields.

For some reason the multiple-values fields work when rendering via the twig template.

However, they definitely don't work when GETting the result via JSON:API - in that case they are considered as single-value and only the first value of the array is returned.

Steps to reproduce

  • Create a multivalue computed field field, the example test_multiple_string is fine
  • enable JSON:API on the site
  • GET the node\resource via JSON:API. The field should be multi-value, however is single-value

Proposed resolution

Enable the cardinality on the plugin.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Giuseppe87 created an issue. See original summary.

giuseppe87’s picture

StatusFileSize
new3.62 KB

Attached the patch setting up the cardinality. I hope it's okay, I'm not really expert with plugins.

Ideally, I suppose SingleValueTrait should override the default value for the annotation $cardinality, but I have no idea if that is possible and how do that, and I couldn't find an example on internet or other traits to do so.

I'd appreciate if someone could explain that (if it's actually possible) or fix the patch. Thanks

giuseppe87’s picture

StatusFileSize
new4.46 KB

I didn't see that the field may be attached also inside hook_entity_bundle_field_info_alter for the "bundle" fields.
The patch add the cardinality check also in that hook.

joachim’s picture

Status: Active » Needs work

Ah yes, I hadn't thought of that! Fields by default are single-valued, but the rendering system is lazy and just iterates anyway.

  1. +++ b/computed_field.module
    @@ -121,6 +121,12 @@ function computed_field_entity_base_field_info_alter(&$fields, EntityTypeInterfa
    +    if ($plugin->fieldIsMultiple()) {
    

    same.

  2. +++ b/computed_field.module
    @@ -177,6 +183,12 @@ function computed_field_entity_bundle_field_info_alter(&$fields, EntityTypeInter
    +    if ($plugin->fieldIsMultiple()) {
    

    We don't need this check - just pass on the cardinality, whatever it is.

  3. +++ b/src/Annotation/ComputedField.php
    @@ -61,4 +62,12 @@ class ComputedField extends Plugin {
    +  public $cardinality = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
    

    I'm not sure we should default to unlimited, as BaseFieldDefinition defaults to 1. We should be consistent.

  4. +++ b/src/Plugin/ComputedField/ComputedFieldBase.php
    @@ -43,6 +44,21 @@ abstract class ComputedFieldBase extends PluginBase implements ComputedFieldPlug
    +    return $this->pluginDefinition['cardinality'] ?: FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
    

    You don't need the default here. It will come from the annotation property's default value if not specified in the plugin annotation.

  5. +++ b/src/Plugin/ComputedField/ComputedFieldBase.php
    @@ -43,6 +44,21 @@ abstract class ComputedFieldBase extends PluginBase implements ComputedFieldPlug
    +    $cardinality = $this->getFieldCardinality();
    +    return ($cardinality === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) || ($cardinality > 1);
    

    It's simpler to just say != 1 here I think?

  6. +++ b/src/Plugin/ComputedField/ComputedFieldPluginInterface.php
    @@ -54,6 +54,21 @@ interface ComputedFieldPluginInterface extends PluginInspectionInterface, Deriva
    +   * Returns whether the field is multiple or not
    

    Missing full stop.

_pratik_’s picture

StatusFileSize
new4.24 KB
new2 KB

Changes regarding #4, not sure on point 3.
Thanks

giuseppe87’s picture

3. I chose the unlimited instead of 1 because the module "default plugin logic" is a unlimited value field.
The "single" value needs to specify SingleValueTrait. But this is a mine arbitrary idea, it seems sensible to choose "1" as default.

5. I actually initially wrote something like that, but after I copied what BaseFieldDefinition::isMultiple() does:

  public function isMultiple() {
    $cardinality = $this->getCardinality();
    return ($cardinality == static::CARDINALITY_UNLIMITED) || ($cardinality > 1);
  }

But I've no idea if this is some overkill code or there's some obscure valid reason behind it.

roxflame’s picture

Not sure if this is related, but I get errors importing nodes in feeds if they would return multiple values when calculated.

"Keywords (computed_keywords): Keywords: this field cannot hold more than 1 values."

This field does hold more than one value just fine, and displays as multiple throughout the site, but, it breaks my feeds from importing...

namespace Drupal\monomaly_computed_fields\Plugin\ComputedField;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\computed_field\Field\ComputedFieldDefinitionWithValuePluginInterface;
use Drupal\computed_field\Plugin\ComputedField\ComputedFieldBase;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\computed_field\Field\ComputedFieldSettingsTrait;

/**
 * @ComputedField(
 *   id = "monomaly_keywords_aggregator",
 *   label = @Translation("Keywords Aggregator"),
 *   field_type = "text",
 *   no_ui = FALSE,
 *   multiple_values = TRUE
 * )
 */
class SumUniqueKeywords extends ComputedFieldBase{

  /**
   * {@inheritdoc}
   */
   
  public function getFieldDefinitionSettings(): array {
    return [
        'cardinality' => BaseFieldDefinition::CARDINALITY_UNLIMITED,
    ];
  }


  // This is the method 
public function computeValue(EntityInterface $host_entity, ComputedFieldDefinitionWithValuePluginInterface $computed_field_definition): array {
    // Initialize the keywords count array and level texts.
    $keywords_count = [];
    $keywords_levels = [];

    // Iterate over all entity reference fields on the host entity, looking for taxonomy terms.
    foreach ($host_entity->getFieldDefinitions() as $field_definition) {
        if ($field_definition->getType() === 'entity_reference' &&
            $field_definition->getSetting('target_type') === 'taxonomy_term') {
            // Collect referenced taxonomy term entities.
            $referenced_terms = $host_entity->get($field_definition->getName())->referencedEntities();

            foreach ($referenced_terms as $term) {
                // Drill down into each referenced term to see if it has a keywords field.
                if ($term instanceof EntityInterface && $term->hasField('field_keywords')) {
                    // Get keyword terms from this term.
                    $keyword_terms = $term->get('field_keywords')->referencedEntities();

                    foreach ($keyword_terms as $keyword_term) {
                        // Count occurrences of each keyword.
                        $keyword_name = $keyword_term->getName();
                        if (!isset($keywords_count[$keyword_name])) {
                            $keywords_count[$keyword_name] = 0;
                            // Initialize the levels text for this keyword.
                            $keywords_levels[$keyword_name] = [
                                'level_1' => $keyword_term->field_level_1->value ?? '',
                                'level_2' => $keyword_term->field_level_2->value ?? '',
                                'level_3' => $keyword_term->field_level_3->value ?? '',
                            ];
                        }
                        $keywords_count[$keyword_name]++;
                    }
                }
            }
        }
    }

    // Set the computed values with level texts.
    $items = [];
    foreach ($keywords_count as $keyword_name => $count) {
        // Append level text based on count to the keyword.
        $level_text = '';
        if ($count == 1) {
            $level_text = $keywords_levels[$keyword_name]['level_1'];
        } elseif ($count == 2) {
            $level_text = $keywords_levels[$keyword_name]['level_2'];
        } elseif ($count >= 3) {
            $level_text = $keywords_levels[$keyword_name]['level_3'];
        }
        $output = "$keyword_name $count: $level_text";
        $items[] = $output;
    }

    // Debugging output.
    \Drupal::logger('monomaly_computed_fields')->info('Computed Field Debug: ' . print_r($items, TRUE));

    return $items;
    }
}

KarlShea made their first commit to this issue’s fork.

karlshea’s picture

@roxflame I think cardinality was supposed to go in your plugin's getFieldCardinality(), not in getFieldDefinitionSettings

karlshea’s picture

Status: Needs work » Needs review
colan’s picture

joachim’s picture

> 3. I chose the unlimited instead of 1 because the module "default plugin logic" is a unlimited value field.
> The "single" value needs to specify SingleValueTrait. But this is a mine arbitrary idea, it seems sensible to choose "1" as default.

The plugin logic was a mistake on my part -- I hadn't checked what base fields and config fields do.

We're still in alpha releases, so it's fine to change our API.

> Could this be related to #3437020: Allow computed function callbacks to determine cardinality dynamically?

Yes, they seem similar. Though I don't really understand the patch over there. Let's get this one in first.

joachim’s picture

Status: Needs review » Fixed

Made a few changes, added a test.

Committed the MR.

  • joachim committed 92e3ea18 on 4.0.x
    Issue #3350715 by Giuseppe87, _pratik_, joachim: Fixed computed field...

Status: Fixed » Closed (fixed)

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

giuseppe87’s picture

Hi, may suggest that the changelog of alpha9 explain that this MR introduced a breaking change, changing the default cardinality of the plugin from unlimited to single?

I don't know if there are others can be affected too, and if this can happen also with the rendering system, but when I switched from alpha8 + patch to alpha9 I got problems due this MR - namely, via JSON:API empty fields returned "false" instead of an empty array.

Considering I was puzzled a bit before finding the cause, and I'm the one opening the issue, others could be have even more difficulties to find the problem.