From a3b983a44de65ceb4f0c0347136cca29b3f03349 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Mon, 9 Sep 2024 11:26:03 +0200 Subject: [PATCH 1/4] Rewrite module to use default field storage --- image_field_caption.info.yml | 3 +- image_field_caption.install | 376 +++++++++------- image_field_caption.module | 307 +++---------- image_field_caption.services.yml | 7 - src/ImageCaptionItem.php | 104 ++++- src/ImageCaptionStorage.php | 425 ------------------ .../FieldFormatter/ImageCaptionFormatter.php | 2 + 7 files changed, 355 insertions(+), 869 deletions(-) delete mode 100644 image_field_caption.services.yml delete mode 100644 src/ImageCaptionStorage.php diff --git a/image_field_caption.info.yml b/image_field_caption.info.yml index 17d7bf9..e04a8e9 100644 --- a/image_field_caption.info.yml +++ b/image_field_caption.info.yml @@ -2,4 +2,5 @@ name: Image Field Caption description: 'Provides a caption textarea for image fields.' package: Other type: module -core_version_requirement: ^9 || ^10 +core_version_requirement: ^9.2 || ^10 +php: 7.1 diff --git a/image_field_caption.install b/image_field_caption.install index 92e9220..63a974b 100644 --- a/image_field_caption.install +++ b/image_field_caption.install @@ -1,172 +1,222 @@ 'The base table for the image_field_caption module.', - 'fields' => [ - 'entity_type' => [ - 'description' => 'The entity type attached to this caption', - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - ], - 'bundle' => [ - 'description' => 'The bundle attached to this caption', - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - ], - 'field_name' => [ - 'description' => 'The field name attached to this caption', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ], - 'entity_id' => [ - 'description' => 'The entity id attached to this caption', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'revision_id' => [ - 'description' => 'The entity id attached to this caption, or NULL if the entity type is not versioned', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - ], - 'language' => [ - 'description' => 'The language attached to this caption', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ], - 'delta' => [ - 'description' => 'The sequence number for this caption, used for multi-value fields', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'caption' => [ - 'description' => 'The caption text.', - 'type' => 'text', - 'not null' => FALSE, - ], - 'caption_format' => [ - 'description' => 'The caption format.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ], - ], - 'indexes' => [ - 'entity_type' => ['entity_type'], - 'bundle' => ['bundle'], - 'entity_id' => ['entity_id'], - 'revision_id' => ['revision_id'], - 'language' => ['language'], - ], - 'primary key' => [ - 'entity_type', - 'field_name', - 'entity_id', - 'language', - 'delta', - ], - ]; - - // Image Field Caption revision table. - $schema['image_field_caption_revision'] = [ - 'description' => 'The revision table for the image_field_caption module.', - 'fields' => [ - 'entity_type' => [ - 'description' => 'The entity type attached to this caption', - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - ], - 'bundle' => [ - 'description' => 'The bundle attached to this caption', - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - ], - 'field_name' => [ - 'description' => 'The field name attached to this caption', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ], - 'entity_id' => [ - 'description' => 'The entity id attached to this caption', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'revision_id' => [ - 'description' => 'The entity id attached to this caption, or NULL if the entity type is not versioned', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'language' => [ - 'description' => 'The language attached to this caption', - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ], - 'delta' => [ - 'description' => 'The sequence number for this caption, used for multi-value fields', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'caption' => [ - 'description' => 'The caption text.', - 'type' => 'text', - 'not null' => FALSE, - ], - 'caption_format' => [ - 'description' => 'The caption format.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ], - ], - 'indexes' => [ - 'entity_type' => ['entity_type'], - 'bundle' => ['bundle'], - 'entity_id' => ['entity_id'], - 'revision_id' => ['revision_id'], - 'language' => ['language'], - ], - 'primary key' => [ - 'entity_type', - 'field_name', - 'entity_id', - 'revision_id', - 'language', - 'delta', - ], - ]; - - return $schema; +function image_field_caption_uninstall(): void { + _image_field_caption_field_type_schema_column_remove_helper('image', ['caption', 'caption_format']); } -/* @todo Programmatically set the default formatter for all fields that uses this field formatter using image_field_caption_uninstall(). */ +/** + * Helper function to add new columns to a field type. + * + * @param $field_type + * The field type id. + * @param array $columns_to_add + * array of the column names from schema() function. + * + * @see https://gist.github.com/JPustkuchen/ce53d40303a51ca5f17ce7f48c363b9b + * @see https://www.drupal.org/project/drupal/issues/937442 + */ +function _image_field_caption_field_type_schema_column_add_helper(string $field_type, array $columns_to_add = []): void { + $processed_fields = []; + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $field_definition = $field_type_manager->getDefinition($field_type); + $field_item_class = $field_definition['class']; + + $schema = \Drupal::database()->schema(); + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_field_manager = \Drupal::service('entity_field.manager'); + $entity_field_map = $entity_field_manager->getFieldMapByFieldType($field_type); + // The key-value collection for tracking installed storage schema. + $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql'); + $entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed'); + + foreach ($entity_field_map as $entity_type_id => $field_map) { + $entity_storage = $entity_type_manager->getStorage($entity_type_id); + + // Only SQL storage based entities are supported / throw known exception. + if (!($entity_storage instanceof SqlContentEntityStorage)) { + continue; + } + + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id); + /** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_storage->getTableMapping($field_storage_definitions); + // Only need field storage definitions of our field type: + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */ + foreach (array_intersect_key($field_storage_definitions, $field_map) as $field_storage_definition) { + $field_name = $field_storage_definition->getName(); + try { + $table = $table_mapping->getFieldTableName($field_name); + } catch (SqlContentEntityStorageException $e) { + // Custom storage? Broken site? No matter what, if there is no table + // or column, there's little we can do. + continue; + } + // See if the field has a revision table. + $revision_table = NULL; + if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) { + if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { + $revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition); + } + elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { + $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); + } + } + // Load the installed field schema so that it can be updated. + $schema_key = "$entity_type_id.field_schema_data.$field_name"; + $field_schema_data = $entity_storage_schema_sql->get($schema_key); + + $processed_fields[] = [$entity_type_id, $field_name]; + // Loop over each new column and add it as a schema column change. + foreach ($columns_to_add as $column_id) { + $column = $table_mapping->getFieldColumnName($field_storage_definition, $column_id); + // Add `initial_from_field` to the new spec, as this will copy over + // the entire data. + $field_schema = $field_item_class::schema($field_storage_definition); + $spec = $field_schema['columns'][$column_id]; + + // Add the new column. + $schema->addField($table, $column, $spec); + if ($revision_table) { + $schema->addField($revision_table, $column, $spec); + } + + // Add the new column to the installed field schema. + if (!empty($field_schema_data)) { + $field_schema_data[$table]['fields'][$column] = $field_schema['columns'][$column_id]; + $field_schema_data[$table]['fields'][$column]['not null'] = FALSE; + if ($revision_table) { + $field_schema_data[$revision_table]['fields'][$column] = $field_schema['columns'][$column_id]; + $field_schema_data[$revision_table]['fields'][$column]['not null'] = FALSE; + } + } + } + + // Save changes to the installed field schema. + if (!empty($field_schema_data)) { + $entity_storage_schema_sql->set($schema_key, $field_schema_data); + } + if ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { + $key = "$entity_type_id.field_storage_definitions"; + if ($definitions = $entity_definitions_installed->get($key)) { + $definitions[$field_name] = $field_storage_definition; + $entity_definitions_installed->set($key, $definitions); + } + } + } + } +} + +/** + * Helper function to remove columns from a field type. + * + * @param $field_type + * The field type id. + * @param array $columns_to_remove + * array of the column names from schema() function. + * + * @see https://gist.github.com/JPustkuchen/ce53d40303a51ca5f17ce7f48c363b9b + * @see https://www.drupal.org/project/drupal/issues/937442 + */ +function _image_field_caption_field_type_schema_column_remove_helper(string $field_type, array $columns_to_remove = []): void { + $processed_fields = []; + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $field_definition = $field_type_manager->getDefinition($field_type); + $field_item_class = $field_definition['class']; + + $schema = \Drupal::database()->schema(); + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_field_manager = \Drupal::service('entity_field.manager'); + $entity_field_map = $entity_field_manager->getFieldMapByFieldType($field_type); + // The key-value collection for tracking installed storage schema. + $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql'); + $entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed'); + + foreach ($entity_field_map as $entity_type_id => $field_map) { + $entity_storage = $entity_type_manager->getStorage($entity_type_id); + + // Only SQL storage based entities are supported / throw known exception. + if (!($entity_storage instanceof SqlContentEntityStorage)) { + continue; + } + + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id); + /** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_storage->getTableMapping($field_storage_definitions); + // Only need field storage definitions of our field type: + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */ + foreach (array_intersect_key($field_storage_definitions, $field_map) as $field_storage_definition) { + $field_name = $field_storage_definition->getName(); + try { + $table = $table_mapping->getFieldTableName($field_name); + } catch (SqlContentEntityStorageException $e) { + // Custom storage? Broken site? No matter what, if there is no table + // or column, there's little we can do. + continue; + } + // See if the field has a revision table. + $revision_table = NULL; + if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) { + if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { + $revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition); + } + elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { + $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); + } + } + // Load the installed field schema so that it can be updated. + $schema_key = "$entity_type_id.field_schema_data.$field_name"; + $field_schema_data = $entity_storage_schema_sql->get($schema_key); + + $processed_fields[] = [$entity_type_id, $field_name]; + // Loop over each new column and add it as a schema column change. + foreach ($columns_to_remove as $column_id) { + $column = $table_mapping->getFieldColumnName($field_storage_definition, $column_id); + // Add `initial_from_field` to the new spec, as this will copy over + // the entire data. + $field_schema = $field_item_class::schema($field_storage_definition); + $spec = $field_schema['columns'][$column_id]; + + // Add the new column. + $schema->dropField($table, $column); + if ($revision_table) { + $schema->dropField($revision_table, $column); + } + + // Remove the column from the installed field schema. + if (!empty($field_schema_data)) { + unset($field_schema_data[$table]['fields'][$column]); + if ($revision_table) { + unset($field_schema_data[$revision_table]['fields'][$column]); + } + } + } + + // Save changes to the installed field schema. + if (!empty($field_schema_data)) { + $entity_storage_schema_sql->set($schema_key, $field_schema_data); + } + if ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { + $key = "$entity_type_id.field_storage_definitions"; + if ($definitions = $entity_definitions_installed->get($key)) { + $definitions[$field_name] = $field_storage_definition; + $entity_definitions_installed->set($key, $definitions); + } + } + } + } +} diff --git a/image_field_caption.module b/image_field_caption.module index d49aa67..63f8999 100644 --- a/image_field_caption.module +++ b/image_field_caption.module @@ -5,112 +5,72 @@ * Provides a caption textarea for image fields. */ -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Component\Utility\NestedArray; - -/** - * List to do. - * - * Support for Views, maybe built in in D8? - * Support the revision management. - * */ +use Drupal\image\Plugin\Field\FieldWidget\ImageWidget; +use Drupal\image_field_caption\ImageCaptionItem; /** * Implements hook_field_info_alter(). */ -function image_field_caption_field_info_alter(&$info) { - // Set a new class for the image fields. - $info['image']['class'] = '\Drupal\image_field_caption\ImageCaptionItem'; +function image_field_caption_field_info_alter(array &$info): void { + // Override the image field type class + $info['image']['class'] = ImageCaptionItem::class; + + // Enable translation for the caption property + $info['image']['column_groups']['caption'] = [ + 'label' => t('Caption'), + 'translatable' => TRUE, + ]; } /** * Implements hook_field_widget_single_element_form_alter(). */ -function image_field_caption_field_widget_single_element_form_alter(&$element, FormStateInterface $form_state, $context) { - /** @var \Drupal\field\Entity\FieldConfig $field */ +function image_field_caption_field_widget_single_element_form_alter(array &$element, FormStateInterface $form_state, $context): void { $field = $context['items']->getFieldDefinition(); - // If the current field is an image field. - if ($field->getType() == 'image') { - // Get the current field settings. - $settings = $field->getSettings(); - // Check if the current field has the caption. - if (!empty($settings['caption_field'])) { - $element['#caption_field_required'] = $settings['caption_field_required']; - $element['#process'][] = '_image_field_caption_widget_process'; - } + assert($field instanceof FieldDefinitionInterface); + + if ($field->getType() !== 'image') { + return; } + + $settings = $field->getSettings(); + $element['#caption_field'] = $settings['caption_field']; + $element['#caption_field_required'] = $settings['caption_field_required']; + $element['#process'][] = '_image_field_caption_widget_process'; } /** * Custom callback function for the #process of an image field type. */ -function _image_field_caption_widget_process($element, &$form_state, $form) { - // Get the entity. - // $entity = $form_state->getFormObject()->getEntity(); - // // Get the fields definitions. - // $field_definitions = $entity->getFieldDefinitions(); - // // Get the current field definition. - // if (!empty($field_definitions[$element['#field_name']])) { - // $field_definition = $field_definitions[$element['#field_name']]; - // } - // elseif (!empty($field_definitions[$element['#field_parents'][0]])) { - // $field_definition = $field_definitions[$element['#field_parents'][0]]; - // } - // else { - // $field_definition = NULL; - // }. - // Get the current field values (form state). - $field_values = $form_state->getValues(); - // If the field has parents (ex: paragraphs) then get the nested values. - if (!empty($element['#field_parents'])) { - $field_values = NestedArray::getValue($field_values, $element['#field_parents']); - } - $field_value = (isset($field_values[$element['#field_name']][$element['#delta']]['image_field_caption'])) ? $field_values[$element['#field_name']][$element['#delta']]['image_field_caption'] : []; +function _image_field_caption_widget_process(array $element, FormStateInterface $form_state, array $form): array { + $item = $element['#value']; + $item['fids'] = $element['fids']['#value']; - // Add the additional caption fields. - $element['image_field_caption'] = [ + // TODO: Make #allowed_formats configurable. + + $element['caption'] = [ '#title' => t('Caption'), '#type' => 'text_format', - '#value' => (!empty($field_value['value'])) ? $field_value['value'] : ((!empty($element['#value']['caption'])) ? $element['#value']['caption'] : []), - '#default_value' => (!empty($element['#value']['caption'])) ? $element['#value']['caption'] : (!empty($element['#value']['image_field_caption']) ? $element['#value']['image_field_caption']['value'] : ''), - '#access' => (bool) $element['#value']['fids'], - '#format' => (!empty($field_value['format'])) ? $field_value['format'] : ((!empty($element['#value']['caption_format'])) ? $element['#value']['caption_format'] : 'plain_text'), + '#default_value' => $item['caption'] ?? '', + '#access' => $item['fids'] && $element['#caption_field'], + '#format' => $item['caption_format'] ?? 'plain_text', '#required' => $element['#caption_field_required'], - '#element_validate' => $element['#caption_field_required'] ? ['_image_field_caption_validate_required'] : [], + '#element_validate' => $element['#caption_field_required'] ? [[ImageWidget::class, 'validateRequiredFields']] : [], ]; return $element; } -/** - * Validate callback for caption field, if the user wants them required. - * - * This is separated in a validate function instead of a #required flag to - * avoid being validated on the process callback. - */ -function _image_field_caption_validate_required($element, FormStateInterface $form_state) { - // Only do validation if the function is triggered from other places than - // the image process form. - // Only do validation if the function is triggered from other places than - // the image process form. - $triggering_element = $form_state->getTriggeringElement(); - if (!empty($triggering_element['#submit']) && in_array('file_managed_file_submit', $triggering_element['#submit'], TRUE)) { - $form_state->setLimitValidationErrors([]); - } -} - /** * Implements hook_theme(). */ -function image_field_caption_theme() { +function image_field_caption_theme(): array { return [ 'image_caption_formatter' => [ // As we extend the default image format, the variables passed to the - // callback function are the same than the original - // "callback" function ("image_formatter"). + // callback function are the same as the original (image_formatter). 'variables' => [ 'item' => NULL, 'item_attributes' => NULL, @@ -131,10 +91,11 @@ function image_field_caption_theme() { * (template_preprocess_image_formatter()) and also: * - caption: An optional caption text. */ -function template_preprocess_image_caption_formatter(&$variables) { - Drupal::moduleHandler()->loadInclude('image', 'inc', 'image.field'); +function template_preprocess_image_caption_formatter(array &$variables): void { // Prepare the variables array with the original function. + Drupal::moduleHandler()->loadInclude('image', 'inc', 'image.field'); template_preprocess_image_formatter($variables); + // Set the caption value. $values = $variables['item']->getValue(); if (!empty($values['caption'])) { @@ -147,187 +108,19 @@ function template_preprocess_image_caption_formatter(&$variables) { } /** - * Implements hook_entity_storage_load(). - */ -function image_field_caption_entity_storage_load(array $entities, $entity_type_id) { - $imageCaption = Drupal::service('image_field_caption.storage'); - - if (in_array($entity_type_id, $imageCaption->list('entity_type'))) { - // This means we already have some captions. - // No need to do all kinds of checking then. - /** @var \Drupal\Core\Entity\Entity $entity */ - foreach ($entities as $entity) { - // Same load avoiding check. - if (in_array($entity->bundle(), $imageCaption->list('bundle'))) { - $needToSave = FALSE; - - /** @var \Drupal\Core\Field\FieldItemList $field */ - foreach ($entity->getFields() as $fieldName => $field) { - $values = $entity->get($fieldName)->getValue(); - foreach ($values as $delta => $value) { - // Get the caption associated to this field. - $revision_id = (empty($entity->getRevisionId()) ? $entity->id() : $entity->getRevisionId()); - $caption = $imageCaption->getCaption( - $entity->getEntityTypeId(), - $entity->bundle(), - $fieldName, - $entity->id(), - $revision_id, - $entity->language()->getId(), - $delta - ); - - // Set the caption value. - if (!empty($caption)) { - $values[$delta] = $values[$delta] + $caption; - $needToSave = TRUE; - } - } - - if ($needToSave) { - // Save all values. - $entity->get($fieldName)->setValue($values); - } - } - - } - } - } -} - -/** - * Implements hook_entity_insert(). + * Implements hook_config_schema_info_alter(). */ -function image_field_caption_entity_insert(EntityInterface $entity) { - image_field_caption_entity_update($entity); -} - -/** - * Implements hook_entity_update(). - */ -function image_field_caption_entity_update(EntityInterface $entity) { - $imageCaption = Drupal::service('image_field_caption.storage'); - - // For a fieldable entity. - if (($entity instanceof FieldableEntityInterface)) { - // Get the field names of all image fields. - $field_names = _image_field_caption_get_image_field_names($entity); - foreach ($field_names as $field_name) { - // Get the current field settings. - $settings = $entity->get($field_name)->getSettings(); - // If the caption is not enabled => pass this field. - if (empty($settings['caption_field'])) { - continue; - } - // Delete the caption associated to this field. - $imageCaption->deleteCaption($entity->getEntityTypeId(), $entity->bundle(), $field_name, $entity->id(), $entity->language() - ->getId()); - // Delete the caption revision associated to this field. - /* - $imageCaption->deleteCaptionRevision( - $entity->getEntityTypeId(), $entity->bundle(), - $field_name, $entity->id(), $entity->getRevisionId(), - $entity->language()->getId()); */ - // Get the current field values. - $values = $entity->get($field_name)->getValue(); - foreach ($values as $delta => $value) { - // If a caption text is defined. - if (!empty($value['image_field_caption']['value'])) { - // Insert the caption associated to this field. - // @todo Do the insertion using a multiple query instead several queries into a foreach; - $revision_id = (empty($entity->getRevisionId()) ? $entity->id() : $entity->getRevisionId()); - $imageCaption->insertCaption( - $entity->getEntityTypeId(), - $entity->bundle(), - $field_name, - $entity->id(), - $revision_id, - $entity->language()->getId(), - $delta, - $value['image_field_caption']['value'], - $value['image_field_caption']['format'] - ); - // Insert the caption revision associated to this field. - /* - if ($entity->isNewRevision()) { - $imageCaption->insertCaptionRevision( - $entity->getEntityTypeId(), - $entity->bundle(), - $field_name, - $entity->id(), - $revision_id, - $entity->language()->getId(), - $delta, - $value['image_field_caption']['value'], - $value['image_field_caption']['format'] - ); - } - */ - } - } - } - } -} - -/** - * Implements hook_entity_delete(). - */ -function image_field_caption_entity_delete(EntityInterface $entity) { - $imageCaption = Drupal::service('image_field_caption.storage'); - - // For a fieldable entity. - if (($entity instanceof FieldableEntityInterface)) { - // Get the field names of all image fields. - $field_names = _image_field_caption_get_image_field_names($entity); - foreach ($field_names as $field_name) { - // Delete the caption associated to this field. - $imageCaption->deleteCaption($entity->getEntityTypeId(), $entity->bundle(), $field_name, $entity->id(), $entity->language() - ->getId()); - // Delete the caption revisions associated to this field. - /* - $imageCaption->deleteCaptionRevisions( - $entity->getEntityTypeId(), $entity->bundle(), - $field_name, $entity->id(), $entity->language()->getId() - );*/ - } - } -} - -/** - * Implements hook_entity_revision_delete(). - */ -function image_field_caption_entity_revision_delete(EntityInterface $entity) { - // $imageCaption = Drupal::service('image_field_caption.storage'); - /* - // For a fieldable entity. - if (($entity instanceof FieldableEntityInterface)) { - // Get the field names of all image fields. - $field_names = _image_field_caption_get_image_field_names($entity); - if (!empty($field_names)) { - // Delete the caption revisions associated to this specific revision. - $imageCaption->deleteCaptionRevisionsByRevisionId($entity->getRevisionId()); - } - } - */ -} - -/** - * Determines the image fields on an entity. - * - * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * An entity whose fields to analyze. - * - * @return array - * The names of the fields on this entity that support formatted text. - */ -function _image_field_caption_get_image_field_names(FieldableEntityInterface $entity) { - // Check if fields definitions are available. - $field_definitions = $entity->getFieldDefinitions(); - if (empty($field_definitions)) { - return []; - } - // Only return image fields. - return array_keys(array_filter($field_definitions, function (FieldDefinitionInterface $definition) { - return in_array($definition->getType(), ['image'], TRUE); - })); +function image_field_caption_config_schema_info_alter(array &$definitions): void { + $definitions['field.field_settings.image']['mapping']['caption_field'] = [ + 'type' => 'boolean', + 'label' => 'Enable Caption field', + ]; + $definitions['field.field_settings.image']['mapping']['caption_field_required'] = [ + 'type' => 'boolean', + 'label' => 'Caption field required', + ]; + $definitions['field_default_image']['mapping']['caption'] = [ + 'type' => 'label', + 'label' => 'Caption', + ]; } diff --git a/image_field_caption.services.yml b/image_field_caption.services.yml deleted file mode 100644 index 845c2e6..0000000 --- a/image_field_caption.services.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - image_field_caption.storage: - class: Drupal\image_field_caption\ImageCaptionStorage - arguments: - - '@cache.data' - - '@cache_tags.invalidator' - - '@database' diff --git a/src/ImageCaptionItem.php b/src/ImageCaptionItem.php index 1367735..02c0ada 100644 --- a/src/ImageCaptionItem.php +++ b/src/ImageCaptionItem.php @@ -2,46 +2,90 @@ namespace Drupal\image_field_caption; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\TypedData\DataDefinition; use Drupal\image\Plugin\Field\FieldType\ImageItem; /** - * Class to provide functionlality for ImageCaptionItem. + * Class to provide functionality for ImageCaptionItem. */ class ImageCaptionItem extends ImageItem { + /** + * {@inheritdoc} + */ + public static function defaultStorageSettings() { + $settings = parent::defaultStorageSettings(); + $settings['default_image']['caption'] = ''; + + return $settings; + } + /** * {@inheritdoc} */ public static function defaultFieldSettings() { - return [ - 'default_image' => [ - 'caption' => '', - ], - 'caption_field' => FALSE, - 'caption_field_required' => FALSE, - ] + parent::defaultFieldSettings(); + $settings = parent::defaultFieldSettings(); + $settings['default_image']['caption'] = ''; + $settings['caption_field'] = FALSE; + $settings['caption_field_required'] = FALSE; + + return $settings; + } + + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $schema = parent::schema($field_definition); + + $schema['columns']['caption'] = [ + 'description' => 'The caption text.', + 'type' => 'text', + 'not null' => FALSE, + ]; + + $schema['columns']['caption_format'] = [ + 'description' => 'The caption format.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ]; + + return $schema; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties = parent::propertyDefinitions($field_definition); + + $properties['caption'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('Caption')) + ->setDescription(new TranslatableMarkup("Short description of the image displayed underneath the image.")); + + $properties['caption_format'] = DataDefinition::create('filter_format') + ->setLabel(new TranslatableMarkup('Text format of the caption')); + + return $properties; } /** * {@inheritdoc} */ public function fieldSettingsForm(array $form, FormStateInterface $form_state) { - // Get base form from ImageItem. $element = parent::fieldSettingsForm($form, $form_state); - // Get field settings. $settings = $this->getSettings(); - // Get the default field settings. - $settings_default = self::defaultFieldSettings(); // Add caption option. $element['caption_field'] = [ '#type' => 'checkbox', '#title' => t('Enable Caption field'), - '#default_value' => (!empty($settings['caption_field'])) ? $settings['caption_field'] : $settings_default['caption_field'], - '#description' => t('Adds an extra text area for captions on image fields.'), + '#default_value' => $settings['caption_field'], + '#description' => $this->t('Short description of the image displayed underneath the image.'), '#weight' => 13, ]; + // Add caption (required) option. $element['caption_field_required'] = [ '#type' => 'checkbox', @@ -51,16 +95,44 @@ class ImageCaptionItem extends ImageItem { '#weight' => 14, '#states' => [ 'visible' => [ - ':input[name="settings[image_caption_field]"]' => ['checked' => TRUE], + ':input[name="settings[caption_field]"]' => ['checked' => TRUE], ], ], ]; + // Add default caption. $element['default_image']['caption'] = [ '#type' => 'value', - '#value' => (!empty($settings['default_image']['caption'])) ? $settings['default_image']['caption'] : $settings_default['default_image']['caption'], + '#value' => $settings['default_image']['caption'] ?? '', ]; return $element; } + + /** + * {@inheritdoc} + */ + protected function defaultImageForm(array &$element, array $settings) { + parent::defaultImageForm($element, $settings); + + $element['default_image']['caption'] = [ + '#type' => 'textfield', + '#title' => $this->t('Caption'), + '#description' => $this->t('Short description of the image displayed underneath the image.'), + '#default_value' => $settings['default_image']['caption'] ?? '', + ]; + } + + /** + * {@inheritdoc} + */ + public function setValue($values, $notify = TRUE): void { + if (isset($values['caption']) && is_array($values['caption'])) { + $values['caption_format'] = $values['caption']['format']; + $values['caption'] = $values['caption']['value']; + } + + parent::setValue($values, $notify); + } + } diff --git a/src/ImageCaptionStorage.php b/src/ImageCaptionStorage.php deleted file mode 100644 index 9640903..0000000 --- a/src/ImageCaptionStorage.php +++ /dev/null @@ -1,425 +0,0 @@ -cacheBackend = $cacheBackend; - $this->cacheTagsInvalidator = $cacheTagsInvalidator; - $this->database = $database; - } - - /** - * Check if a caption is already defined for the specified arguments. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param string $bundle - * The bundle, like 'article' or 'news'. - * @param string $field_name - * The field name of the image field, - * like 'field_image' or 'field_article_image'. - * @param int $entity_id - * The entity id. - * @param int $revision_id - * The revision id. - * @param string $language - * The language key, like 'en' or 'fr'. - * @param int $delta - * The delta of the image field. - * - * @return bool - * TRUE if a caption exists or FALSE if not. - */ - public function isCaption($entity_type, $bundle, $field_name, $entity_id, $revision_id, $language, $delta) { - return (!empty(self::getCaption($entity_type, $bundle, $field_name, $entity_id, $revision_id, $language, $delta))) ? TRUE : FALSE; - } - - /** - * Get a caption from the database for the specified arguments. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param string $bundle - * The bundle, like 'article' or 'news'. - * @param string $field_name - * The field name of the image field, - * like 'field_image' or 'field_article_image'. - * @param int $entity_id - * The entity id. - * @param int $revision_id - * The revision id. - * @param string $language - * The language key, like 'en' or 'fr'. - * @param int $delta - * The delta of the image field. - * - * @return array - * A caption array - * - caption: The caption text. - * - caption_format: The caption format. - * or an empty array, if no value found. - */ - public function getCaption($entity_type, $bundle, $field_name, $entity_id, $revision_id, $language, $delta) { - $captions = &drupal_static(__FUNCTION__); - - $cacheKey = $this->getCacheKey($entity_type, $entity_id, $revision_id, $language, $field_name, $delta); - - if (isset($captions[$cacheKey])) { - $caption = $captions[$cacheKey]; - } - elseif ($cached = $this->cacheBackend->get($cacheKey)) { - $caption = $cached->data; - } - else { - // Query. - $query = $this->database->select($this->tableData, 'ifc'); - $result = $query - ->fields('ifc', ['caption', 'caption_format']) - ->condition('entity_type', $entity_type, '=') - ->condition('bundle', $bundle, '=') - ->condition('field_name', $field_name, '=') - ->condition('entity_id', $entity_id, '=') - ->condition('revision_id', $revision_id, '=') - ->condition('language', $language, '=') - ->condition('delta', $delta, '=') - ->execute() - ->fetchAssoc(); - - // Caption array. - $caption = []; - if (!empty($result)) { - $caption = $result; - } - - // Let the cache depends on the entity. - // @todo Use getCacheTags() to get the default list. - $this->cacheBackend->set( - $cacheKey, - $caption, - Cache::PERMANENT, - [ - $field_name, - 'image_field_caption', - ] - ); - } - - return $caption; - } - - /** - * Insert a caption into the database for the specified arguments. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param string $bundle - * The bundle, like 'article' or 'news'. - * @param string $field_name - * The field name of the image field, - * like 'field_image' or 'field_article_image'. - * @param int $entity_id - * The entity id. - * @param int $revision_id - * The revision id. - * @param string $language - * The language key, like 'en' or 'fr'. - * @param int $delta - * The delta of the image field. - * @param string $caption - * The caption text. - * @param string $caption_format - * The text format of the caption. - */ - public function insertCaption($entity_type, $bundle, $field_name, $entity_id, $revision_id, $language, $delta, $caption, $caption_format) { - - $query = $this->database->insert($this->tableData); - $query - ->fields([ - 'entity_type' => $entity_type, - 'bundle' => $bundle, - 'field_name' => $field_name, - 'entity_id' => $entity_id, - 'revision_id' => $revision_id, - 'language' => $language, - 'delta' => $delta, - 'caption' => $caption, - 'caption_format' => $caption_format, - ]) - ->execute(); - $this->clearCache($field_name); - } - - /** - * Insert a caption revision into the database for the specified arguments. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param string $bundle - * The bundle, like 'article' or 'news'. - * @param string $field_name - * The field name of the image field, - * like 'field_image' or 'field_article_image'. - * @param int $entity_id - * The entity id. - * @param int $revision_id - * The revision id. - * @param string $language - * The language key, like 'en' or 'fr'. - * @param int $delta - * The delta of the image field. - * @param string $caption - * The caption text. - * @param string $caption_format - * The text format of the caption. - */ - public function insertCaptionRevision($entity_type, $bundle, $field_name, $entity_id, $revision_id, $language, $delta, $caption, $caption_format) { - $query = $this->database->insert($this->tableRevision); - $query - ->fields([ - 'entity_type' => $entity_type, - 'bundle' => $bundle, - 'field_name' => $field_name, - 'entity_id' => $entity_id, - 'revision_id' => $revision_id, - 'language' => $language, - 'delta' => $delta, - 'caption' => $caption, - 'caption_format' => $caption_format, - ]) - ->execute(); - $this->clearCache($field_name); - } - - /** - * Delete a caption from the database for the specified arguments. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param string $bundle - * The bundle, like 'article' or 'news'. - * @param string $field_name - * The field name of the image field, like - * 'field_image' or 'field_article_image'. - * @param int $entity_id - * The entity id. - * @param string $language - * The language key, like 'en' or 'fr'. - */ - public function deleteCaption($entity_type, $bundle, $field_name, $entity_id, $language) { - $query = $this->database->delete($this->tableData); - $query - ->condition('entity_type', $entity_type, '=') - ->condition('bundle', $bundle, '=') - ->condition('field_name', $field_name, '=') - ->condition('entity_id', $entity_id, '=') - ->condition('language', $language, '=') - ->execute(); - $this->clearCache($field_name); - // @todo Try to return the count of the affected rows. - } - - /** - * Delete a caption revision from the database for the specified arguments. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param string $bundle - * The bundle, like 'article' or 'news'. - * @param string $field_name - * The field name of the image field, like - * 'field_image' or 'field_article_image'. - * @param int $entity_id - * The entity id. - * @param int $revision_id - * The revision id. - * @param string $language - * The language key, like 'en' or 'fr'. - */ - public function deleteCaptionRevision($entity_type, $bundle, $field_name, $entity_id, $revision_id, $language) { - $query = $this->database->delete($this->tableRevision); - $query - ->condition('entity_type', $entity_type, '=') - ->condition('bundle', $bundle, '=') - ->condition('field_name', $field_name, '=') - ->condition('entity_id', $entity_id, '=') - ->condition('revision_id', $revision_id, '=') - ->condition('language', $language, '=') - ->execute(); - $this->clearCache($field_name); - // @todo Try to return the count of the affected rows. - } - - /** - * Delete all captions revisions for the specified arguments. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param string $bundle - * The bundle, like 'article' or 'news'. - * @param string $field_name - * The field name of the image field, like 'field_image' - * or 'field_article_image'. - * @param int $entity_id - * The entity id. - * @param string $language - * The language key, like 'en' or 'fr'. - */ - public function deleteCaptionRevisions($entity_type, $bundle, $field_name, $entity_id, $language) { - $query = $this->database->delete($this->tableRevision); - $query - ->condition('entity_type', $entity_type, '=') - ->condition('bundle', $bundle, '=') - ->condition('field_name', $field_name, '=') - ->condition('entity_id', $entity_id, '=') - ->condition('language', $language, '=') - ->execute(); - $this->clearCache($field_name); - // @todo Try to return the count of the affected rows. - } - - /** - * Delete all captions revisions for a specific revision id. - * - * @param int $revision_id - * The revision id. - */ - public function deleteCaptionRevisionsByRevisionId($revision_id) { - $query = $this->database->delete($this->tableRevision); - $query - ->condition('revision_id', $revision_id, '=') - ->execute(); - } - - /** - * Clears the cache for a certain field name. - * - * @param string $field_name - * The field name of the image field, like 'field_image' - * or 'field_article_image'. - */ - public function clearCache($field_name) { - $this->cacheTagsInvalidator->invalidateTags([ - $field_name, - 'image_field_caption', - ]); - } - - /** - * Constructs the cache key. - * - * @param string $entity_type - * The entity type, like 'node' or 'comment'. - * @param int $entity_id - * The entity id. - * @param int $revision_id - * The revision id. - * @param string $language - * The language key, like 'en' or 'fr'. - * @param string $field_name - * The field name of the image field, like 'field_image' - * or 'field_article_image'. - * @param int $delta - * The delta of the image field. - */ - public function getCacheKey($entity_type, $entity_id, $revision_id, $language, $field_name, $delta) { - return implode( - ":", - [ - 'caption', - $entity_type, - $entity_id, - $revision_id, - $language, - $field_name, - $delta, - ] - ); - } - - /** - * Function to list out entity type field. - */ - public function list($key = 'entity_type') { - $list = &drupal_static(__FUNCTION__); - - if (!isset($list[$key])) { - // Query. - $query = $this->database->select($this->tableData, 'ifc'); - $result = $query - ->fields('ifc', [$key]) - ->distinct() - ->execute() - ->fetchAll(); - - $list[$key] = []; - foreach ($result as $row) { - $list[$key][] = $row->{$key}; - } - } - - return $list[$key]; - } - -} diff --git a/src/Plugin/Field/FieldFormatter/ImageCaptionFormatter.php b/src/Plugin/Field/FieldFormatter/ImageCaptionFormatter.php index 7ba4c62..c761a0d 100644 --- a/src/Plugin/Field/FieldFormatter/ImageCaptionFormatter.php +++ b/src/Plugin/Field/FieldFormatter/ImageCaptionFormatter.php @@ -23,10 +23,12 @@ class ImageCaptionFormatter extends ImageFormatter { */ public function viewElements(FieldItemListInterface $items, $langcode) { $elements = parent::viewElements($items, $langcode); + foreach ($elements as $delta => $element) { // Set a new theme callback function for the image caption formatter. $elements[$delta]['#theme'] = 'image_caption_formatter'; } + return $elements; } -- GitLab From 0066f4a953597385e284bfb5bdee941c4dc78b66 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Mon, 9 Sep 2024 11:28:27 +0200 Subject: [PATCH 2/4] Add back image module dependency --- image_field_caption.info.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/image_field_caption.info.yml b/image_field_caption.info.yml index e04a8e9..4861584 100644 --- a/image_field_caption.info.yml +++ b/image_field_caption.info.yml @@ -1,6 +1,10 @@ name: Image Field Caption description: 'Provides a caption textarea for image fields.' package: Other + +dependencies: + - drupal:image + type: module core_version_requirement: ^9.2 || ^10 php: 7.1 -- GitLab From 2e5f7c84145f267c1ac44c37e759d48a9aea07b7 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Mon, 9 Sep 2024 11:31:50 +0200 Subject: [PATCH 3/4] Fix cs issues --- README.md | 3 ++- image_field_caption.install | 19 +++++++++++++------ image_field_caption.module | 7 +++---- src/ImageCaptionItem.php | 5 ++++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8bc42d9..2c46688 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ Adds an extra text area for captions on image fields. Similar to the alt and title text fields available with an image field, -the caption text area can be used to enter text or html descriptions of an image. +the caption text area can be used to enter text or html descriptions of an +image. For a full description of the module, visit the [Image Field Caption](https://www.drupal.org/project/image_field_caption). diff --git a/image_field_caption.install b/image_field_caption.install index 63a974b..dab5ae6 100644 --- a/image_field_caption.install +++ b/image_field_caption.install @@ -1,5 +1,10 @@ getName(); try { $table = $table_mapping->getFieldTableName($field_name); - } catch (SqlContentEntityStorageException $e) { + } + catch (SqlContentEntityStorageException $e) { // Custom storage? Broken site? No matter what, if there is no table // or column, there's little we can do. continue; @@ -123,10 +129,10 @@ function _image_field_caption_field_type_schema_column_add_helper(string $field_ /** * Helper function to remove columns from a field type. * - * @param $field_type + * @param string $field_type * The field type id. * @param array $columns_to_remove - * array of the column names from schema() function. + * Array of the column names from schema() function. * * @see https://gist.github.com/JPustkuchen/ce53d40303a51ca5f17ce7f48c363b9b * @see https://www.drupal.org/project/drupal/issues/937442 @@ -163,7 +169,8 @@ function _image_field_caption_field_type_schema_column_remove_helper(string $fie $field_name = $field_storage_definition->getName(); try { $table = $table_mapping->getFieldTableName($field_name); - } catch (SqlContentEntityStorageException $e) { + } + catch (SqlContentEntityStorageException $e) { // Custom storage? Broken site? No matter what, if there is no table // or column, there's little we can do. continue; diff --git a/image_field_caption.module b/image_field_caption.module index 63f8999..8bae0a6 100644 --- a/image_field_caption.module +++ b/image_field_caption.module @@ -14,10 +14,10 @@ use Drupal\image_field_caption\ImageCaptionItem; * Implements hook_field_info_alter(). */ function image_field_caption_field_info_alter(array &$info): void { - // Override the image field type class + // Override the image field type class. $info['image']['class'] = ImageCaptionItem::class; - // Enable translation for the caption property + // Enable translation for the caption property. $info['image']['column_groups']['caption'] = [ 'label' => t('Caption'), 'translatable' => TRUE, @@ -48,8 +48,7 @@ function _image_field_caption_widget_process(array $element, FormStateInterface $item = $element['#value']; $item['fids'] = $element['fids']['#value']; - // TODO: Make #allowed_formats configurable. - + // @todo Make #allowed_formats configurable. $element['caption'] = [ '#title' => t('Caption'), '#type' => 'text_format', diff --git a/src/ImageCaptionItem.php b/src/ImageCaptionItem.php index 02c0ada..8fb5d92 100644 --- a/src/ImageCaptionItem.php +++ b/src/ImageCaptionItem.php @@ -35,6 +35,9 @@ class ImageCaptionItem extends ImageItem { return $settings; } + /** + * {@inheritdoc} + */ public static function schema(FieldStorageDefinitionInterface $field_definition) { $schema = parent::schema($field_definition); @@ -65,7 +68,7 @@ class ImageCaptionItem extends ImageItem { ->setDescription(new TranslatableMarkup("Short description of the image displayed underneath the image.")); $properties['caption_format'] = DataDefinition::create('filter_format') - ->setLabel(new TranslatableMarkup('Text format of the caption')); + ->setLabel(new TranslatableMarkup('Text format of the caption')); return $properties; } -- GitLab From fffc01d559c45e4b43729619d5cef93916d19417 Mon Sep 17 00:00:00 2001 From: Dieter Holvoet Date: Mon, 9 Sep 2024 11:45:46 +0200 Subject: [PATCH 4/4] Fix issue reported by phpstan --- src/ImageCaptionItem.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageCaptionItem.php b/src/ImageCaptionItem.php index 8fb5d92..3ef0772 100644 --- a/src/ImageCaptionItem.php +++ b/src/ImageCaptionItem.php @@ -79,6 +79,7 @@ class ImageCaptionItem extends ImageItem { public function fieldSettingsForm(array $form, FormStateInterface $form_state) { $element = parent::fieldSettingsForm($form, $form_state); $settings = $this->getSettings(); + $defaultSettings = self::defaultFieldSettings(); // Add caption option. $element['caption_field'] = [ @@ -93,7 +94,7 @@ class ImageCaptionItem extends ImageItem { $element['caption_field_required'] = [ '#type' => 'checkbox', '#title' => t('Caption field required'), - '#default_value' => (!empty($settings['caption_field_required'])) ? $settings['caption_field_required'] : $settings_default['caption_field_required'], + '#default_value' => (!empty($settings['caption_field_required'])) ? $settings['caption_field_required'] : $defaultSettings['caption_field_required'], '#description' => '', '#weight' => 14, '#states' => [ -- GitLab