Skip to main content
Fix spelling in question title
Source Link
Mario Steinitz
  • 4.8k
  • 1
  • 15
  • 34

How to properly define and return simple array computed field property of simple array

/**
 * Field item of type 'responsive_class'.
 *
 * @FieldType(
 *   id = "responsive_class",
 *   label = @Translation("Responsive class"),
 *   module = "responsive_class_field",
 *   default_widget = "responsive_class_widget",
 *   default_formatter = "responsive_class_formatter",
 *   cardinality = 1,
 *   translatable = FALSE,
 * )
 */
class ResponsiveClassItem extends FieldItemBase implements FieldItemInterface {
  [...]

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return [
      'columns' => [
        'value' => [
          'description' => 'The selected responsive styles.',
          'type' => 'blob',
          'not null' => TRUE,
          'serialize' => TRUE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties = [];
    $properties['value'] = DataDefinition::create('responsive_class')
      ->setLabel(new TranslatableMarkup('Responsive class settings'))
      ->setRequired(FALSE);;

    $properties['classes'] = DataDefinition::create('responsive_class_classes')
      ->setLabel(new TranslatableMarkup('Generated responsive classes'))
      ->setComputed(TRUE)
      ->setReadOnly(TRUE);

    return $properties;
  }
 

  /**
   * {@inheritdoc}
   */
  public function getValue() {
    $values = parent::getValue();
    $values['classes'] = $this->getClasses($values['value']);
    return $values;
  }

  /**
   * Generate and return responsive classes.
   *
   * Uses the 'pattern' field storage setting to generate a class for every
   * breakpoint and return them as classes array. The pattern may contain
   * the placeholders {breakpoint} and {value} that will be replaced by the
   * configured breakpoint values.
   *
   * @param array $breakpoints
   *   Array of breakpoint values, whereas every breakpoint value is an
   *   associative array with the following keys:
   *   - breakpoint_id: ID of the breakpoint in the breakpoints definition.
   *   - breakpoint: Replacement value for the {breakpoint} token.
   *   - value: Replacement value for the {value} token.
   *
   * @return array
   *   Array of generated classes.
   */
  protected function getClasses(array $breakpoints) {
    $pattern = $this->getSetting('pattern');
    $classes = [];
    foreach ($breakpoints as $value) {
      $class = str_replace('{breakpoint}', $value['breakpoint'], $pattern);
      $class = str_replace('{value}', $value['value'], $class);
      $class = Html::getClass($class);
      $classes[] = $class;
    }
    return $classes;
  }

}

While the above approach is working, I have the strong feeling that computed properties aren't intended to just being attached in the getValue() method. So I'm seeking to improve the source code.

How to properly define and return computed field property of simple array

/**
 * Field item of type 'responsive_class'.
 *
 * @FieldType(
 *   id = "responsive_class",
 *   label = @Translation("Responsive class"),
 *   module = "responsive_class_field",
 *   default_widget = "responsive_class_widget",
 *   default_formatter = "responsive_class_formatter",
 *   cardinality = 1,
 *   translatable = FALSE,
 * )
 */
class ResponsiveClassItem extends FieldItemBase implements FieldItemInterface {
  [...]

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return [
      'columns' => [
        'value' => [
          'description' => 'The selected responsive styles.',
          'type' => 'blob',
          'not null' => TRUE,
          'serialize' => TRUE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties = [];
    $properties['value'] = DataDefinition::create('responsive_class')
      ->setLabel(new TranslatableMarkup('Responsive class settings'))
      ->setRequired(FALSE);;

    $properties['classes'] = DataDefinition::create('responsive_class_classes')
      ->setLabel(new TranslatableMarkup('Generated responsive classes'))
      ->setComputed(TRUE)
      ->setReadOnly(TRUE);

    return $properties;
  }
 

  /**
   * {@inheritdoc}
   */
  public function getValue() {
    $values = parent::getValue();
    $values['classes'] = $this->getClasses($values['value']);
    return $values;
  }

  /**
   * Generate and return responsive classes.
   *
   * Uses the 'pattern' field storage setting to generate a class for every
   * breakpoint and return them as classes array. The pattern may contain
   * the placeholders {breakpoint} and {value} that will be replaced by the
   * configured breakpoint values.
   *
   * @param array $breakpoints
   *   Array of breakpoint values, whereas every breakpoint value is an
   *   associative array with the following keys:
   *   - breakpoint_id: ID of the breakpoint in the breakpoints definition.
   *   - breakpoint: Replacement value for the {breakpoint} token.
   *   - value: Replacement value for the {value} token.
   *
   * @return array
   *   Array of generated classes.
   */
  protected function getClasses(array $breakpoints) {
    $pattern = $this->getSetting('pattern');
    $classes = [];
    foreach ($breakpoints as $value) {
      $class = str_replace('{breakpoint}', $value['breakpoint'], $pattern);
      $class = str_replace('{value}', $value['value'], $class);
      $class = Html::getClass($class);
      $classes[] = $class;
    }
    return $classes;
  }

}

While the above approach is working, I have the strong feeling that computed properties aren't intended to just being attached in the getValue() method.

How to properly define and return simple array computed field property

/**
 * Field item of type 'responsive_class'.
 *
 * @FieldType(
 *   id = "responsive_class",
 *   label = @Translation("Responsive class"),
 *   module = "responsive_class_field",
 *   default_widget = "responsive_class_widget",
 *   default_formatter = "responsive_class_formatter",
 *   cardinality = 1,
 *   translatable = FALSE,
 * )
 */
class ResponsiveClassItem extends FieldItemBase implements FieldItemInterface {
  [...]

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return [
      'columns' => [
        'value' => [
          'description' => 'The selected responsive styles.',
          'type' => 'blob',
          'not null' => TRUE,
          'serialize' => TRUE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties = [];
    $properties['value'] = DataDefinition::create('responsive_class')
      ->setLabel(new TranslatableMarkup('Responsive class settings'))
      ->setRequired(FALSE);;

    $properties['classes'] = DataDefinition::create('responsive_class_classes')
      ->setLabel(new TranslatableMarkup('Generated responsive classes'))
      ->setComputed(TRUE)
      ->setReadOnly(TRUE);

    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public function getValue() {
    $values = parent::getValue();
    $values['classes'] = $this->getClasses($values['value']);
    return $values;
  }

  /**
   * Generate and return responsive classes.
   *
   * Uses the 'pattern' field storage setting to generate a class for every
   * breakpoint and return them as classes array. The pattern may contain
   * the placeholders {breakpoint} and {value} that will be replaced by the
   * configured breakpoint values.
   *
   * @param array $breakpoints
   *   Array of breakpoint values, whereas every breakpoint value is an
   *   associative array with the following keys:
   *   - breakpoint_id: ID of the breakpoint in the breakpoints definition.
   *   - breakpoint: Replacement value for the {breakpoint} token.
   *   - value: Replacement value for the {value} token.
   *
   * @return array
   *   Array of generated classes.
   */
  protected function getClasses(array $breakpoints) {
    $pattern = $this->getSetting('pattern');
    $classes = [];
    foreach ($breakpoints as $value) {
      $class = str_replace('{breakpoint}', $value['breakpoint'], $pattern);
      $class = str_replace('{value}', $value['value'], $class);
      $class = Html::getClass($class);
      $classes[] = $class;
    }
    return $classes;
  }

}

While the above approach is working, I have the strong feeling that computed properties aren't intended to just being attached in the getValue() method. So I'm seeking to improve the source code.

Source Link
Mario Steinitz
  • 4.8k
  • 1
  • 15
  • 34

How to properly define and return computed field property of simple array

We created a custom field type featuring a computed classes property that returns a simple array of generated CSS classes. The field itself allows content editors to define styling options depending on responsive breakpoints. Given the content editors choices, the CSS classes will be generated using a CSS class pattern defined within the field storage settings. For now, the property is generated and added to the field value within the getValue() method of our field item:

src/Plugin/Field/FieldItem/ResponsiveClassItem.php

/**
 * Field item of type 'responsive_class'.
 *
 * @FieldType(
 *   id = "responsive_class",
 *   label = @Translation("Responsive class"),
 *   module = "responsive_class_field",
 *   default_widget = "responsive_class_widget",
 *   default_formatter = "responsive_class_formatter",
 *   cardinality = 1,
 *   translatable = FALSE,
 * )
 */
class ResponsiveClassItem extends FieldItemBase implements FieldItemInterface {
  [...]

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return [
      'columns' => [
        'value' => [
          'description' => 'The selected responsive styles.',
          'type' => 'blob',
          'not null' => TRUE,
          'serialize' => TRUE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties = [];
    $properties['value'] = DataDefinition::create('responsive_class')
      ->setLabel(new TranslatableMarkup('Responsive class settings'))
      ->setRequired(FALSE);;

    $properties['classes'] = DataDefinition::create('responsive_class_classes')
      ->setLabel(new TranslatableMarkup('Generated responsive classes'))
      ->setComputed(TRUE)
      ->setReadOnly(TRUE);

    return $properties;
  }


  /**
   * {@inheritdoc}
   */
  public function getValue() {
    $values = parent::getValue();
    $values['classes'] = $this->getClasses($values['value']);
    return $values;
  }

  /**
   * Generate and return responsive classes.
   *
   * Uses the 'pattern' field storage setting to generate a class for every
   * breakpoint and return them as classes array. The pattern may contain
   * the placeholders {breakpoint} and {value} that will be replaced by the
   * configured breakpoint values.
   *
   * @param array $breakpoints
   *   Array of breakpoint values, whereas every breakpoint value is an
   *   associative array with the following keys:
   *   - breakpoint_id: ID of the breakpoint in the breakpoints definition.
   *   - breakpoint: Replacement value for the {breakpoint} token.
   *   - value: Replacement value for the {value} token.
   *
   * @return array
   *   Array of generated classes.
   */
  protected function getClasses(array $breakpoints) {
    $pattern = $this->getSetting('pattern');
    $classes = [];
    foreach ($breakpoints as $value) {
      $class = str_replace('{breakpoint}', $value['breakpoint'], $pattern);
      $class = str_replace('{value}', $value['value'], $class);
      $class = Html::getClass($class);
      $classes[] = $class;
    }
    return $classes;
  }

}

src/Plugin/DataType/ResponsiveClassClassesData.php

/**
 * Data type for responsive class field classes.
 *
 * @DataType(
 *   id = "responsive_class_classes",
 *   label = @Translation("Responsive Class Classes"),
 *   description = @Translation("Generated classes of the responsive class field."),
 * )
 */
class ResponsiveClassClassesData extends TypedData {

}

It lets us access the generated classes of the field via the value array and attach it to it's parent entities attribute classes like so:

responsive_class_field.module

/**
 * Implements hook_entity_display_build_alter().
 */
function responsive_class_field_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
  if (!$entity instanceof FieldableEntityInterface) {
    return;
  }

  $field_definitions = $entity->getFieldDefinitions();
  foreach ($field_definitions as $field_name => $field_definition) {
    if ($field_definition->getType() != 'responsive_class') {
      continue;
    }

    $field = $entity->get($field_name);
    if ($field->isEmpty()) {
      continue;
    }

    $classes = $field->getValue()[0]['classes'];
    foreach ($classes as $class) {
      $build['#attributes']['class'][] = $class;
    }
  }
}

And now coming to the real question

While the above approach is working, I have the strong feeling that computed properties aren't intended to just being attached in the getValue() method.

Spending decent hours diving into core source code and field API documentation left me without a better solution. What is the 'Drupal way' to providing such a property?