diff --git a/src/Configs/EntityTransformDisplayInterface.php b/src/Configs/EntityTransformDisplayInterface.php index 6e4f8393530903cffc60ffeca23ba86866a563bc..b56cea899ee813780c668d31257302d5b9355977 100644 --- a/src/Configs/EntityTransformDisplayInterface.php +++ b/src/Configs/EntityTransformDisplayInterface.php @@ -3,10 +3,48 @@ namespace Drupal\transform_api\Configs; use Drupal\Core\Entity\Display\EntityDisplayInterface; +use Drupal\Core\Entity\FieldableEntityInterface; /** * Interface for transform modes. */ interface EntityTransformDisplayInterface extends EntityDisplayInterface { + /** + * Builds a transform array for the components of an entity. + * + * See the buildMultiple() method for details. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity being displayed. + * + * @return array + * A transform array for the entity. + * + * @see \Drupal\transform_api\Configs\EntityTransformDisplayInterface::buildMultiple() + */ + public function build(FieldableEntityInterface $entity); + + /** + * Builds a transform array for the components of a set of entities. + * + * This only includes the components handled by the Display object, but + * excludes 'extra fields', that are typically transformed through specific, + * ad-hoc code in EntityTransformBuilderInterface::buildComponents() or in + * hook_entity_transform() implementations. + * + * hook_entity_transform_build_alter() is invoked on each entity, allowing 3rd + * party code to alter the transform array. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface[] $entities + * The entities being transformed. + * + * @return array + * A transform array for the entities, indexed by the same keys as the + * $entities array parameter. + * + * @see hook_entity_transform_build_alter() + */ + public function buildMultiple(array $entities); + } diff --git a/src/Entity/EntityTransformBuilder.php b/src/Entity/EntityTransformBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..ea4ad52386a824793f5c58b1c93829dbdb5bca13 --- /dev/null +++ b/src/Entity/EntityTransformBuilder.php @@ -0,0 +1,570 @@ +<?php + +namespace Drupal\transform_api\Entity; + +use Drupal\Component\Utility\Crypt; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Entity\EntityHandlerBase; +use Drupal\Core\Entity\EntityHandlerInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldItemInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Security\TrustedCallbackInterface; +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; +use Drupal\transform_api\Configs\EntityTransformDisplayInterface; +use Drupal\transform_api\EntityTransformBuilderInterface; +use Drupal\transform_api\EventSubscriber\TransformationCache; +use Drupal\transform_api\Repository\EntityTransformRepositoryInterface; +use Drupal\transform_api\Transform; +use Drupal\transform_api\Transform\EntityTransform; +use Drupal\transform_api\Transform\TransformInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Base class for entity transform handlers. + * + * @ingroup entity_api + */ +class EntityTransformBuilder extends EntityHandlerBase implements EntityHandlerInterface, EntityTransformBuilderInterface, TrustedCallbackInterface { + + /** + * The type of entities for which this transform handler is instantiated. + * + * @var string + */ + protected $entityTypeId; + + /** + * Information about the entity type. + * + * @var \Drupal\Core\Entity\EntityTypeInterface + */ + protected $entityType; + + /** + * The entity repository service. + * + * @var \Drupal\transform_api\Repository\EntityTransformRepositoryInterface + */ + protected $entityRepository; + + /** + * The entity display repository. + * + * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface + */ + protected $entityDisplayRepository; + + /** + * The cache bin used to store the transformation cache. + * + * @var string + */ + protected $cacheBin = 'transform'; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The transformation caching service. + * + * @var \Drupal\transform_api\EventSubscriber\TransformationCache + */ + protected TransformationCache $transformationCache; + + /** + * The EntityTransformDisplay objects created for individual field transforms. + * + * @var \Drupal\transform_api\Configs\EntityTransformDisplayInterface[] + * + * @see \Drupal\transform_api\Entity\EntityTransformBuilder::getSingleFieldDisplay() + */ + protected $singleFieldDisplays; + + /** + * Constructs a new EntityTransformBuilder. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository service. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\transform_api\EventSubscriber\TransformationCache $transformation_cache + * The transformation cache. + * @param \Drupal\transform_api\Repository\EntityTransformRepositoryInterface $entity_display_repository + * The entity display repository. + */ + public function __construct(EntityTypeInterface $entity_type, EntityRepositoryInterface $entity_repository, LanguageManagerInterface $language_manager, TransformationCache $transformation_cache, EntityTransformRepositoryInterface $entity_display_repository) { + $this->entityTypeId = $entity_type->id(); + $this->entityType = $entity_type; + $this->entityRepository = $entity_repository; + $this->languageManager = $language_manager; + $this->transformationCache = $transformation_cache; + $this->entityDisplayRepository = $entity_display_repository; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity.repository'), + $container->get('language_manager'), + $container->get('transform_api.transformation_cache'), + $container->get('transform_api.entity_display.repository') + ); + } + + /** + * {@inheritdoc} + */ + public function configureTransform(EntityTransform $transform, EntityInterface $entity, $transform_mode = 'full', $langcode = NULL) { + // Allow modules to change the transform mode. + $this->moduleHandler()->alter('entity_transform_mode', $transform_mode, $entity); + + // Ensure that from now on we are dealing with the proper translation + // object. + $entity = $this->entityRepository->getTranslationFromContext($entity, $langcode); + $transform->setEntity($entity, FALSE); + + // Cache the transformed output if permitted by the transform mode and + // global entity type configuration. + if ($this->isTransformModeCacheable($transform_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) { + $transform->setValues([ + 'entity_type' => $this->entityTypeId, + 'ids' => $entity->id(), + 'transform_mode' => $transform_mode, + ]); + $transform->addCacheTags(Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags())); + $transform->addCacheContexts($entity->getCacheContexts()); + $transform->setCacheMaxAge($entity->getCacheMaxAge()); + // @todo Add support for other cache bins. + /* $transform->setCacheBin($this->cacheBin) */ + + if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) { + $transform->setValue('langcode', $entity->language()->getId()); + } + } + + // Allow modules to reconfigure. + $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_transform_configure', [ + $transform, $entity, $transform_mode, $langcode, + ]); + $this->moduleHandler()->invokeAll('entity_transform_configure', [$transform, $entity, $transform_mode, $langcode]); + } + + /** + * {@inheritdoc} + */ + public function transform(EntityInterface $entity, $transform_mode = 'full', $langcode = NULL) { + $build = $this->transformEntity($entity, $transform_mode, $langcode); + $build['#pre_transform'][] = [$this, 'build']; + + return $build; + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['build', 'buildMultiple']; + } + + /** + * {@inheritdoc} + */ + public function transformMultiple(array $entities = [], $transform_mode = 'full', $langcode = NULL) { + $build_list = [ + '#sorted' => TRUE, + '#pre_transform' => [[$this, 'buildMultiple']], + ]; + $weight = 0; + foreach ($entities as $key => $entity) { + $transform = EntityTransform::createFromEntity($entity); + $cached = $this->transformationCache->get($transform); + if ($cached !== FALSE) { + $transform->setBuild($cached); + $transform->setWeight($weight++); + $build_list[$key] = $transform; + } + else { + $build_list[$key] = $this->transformEntity($transform->getEntity(), $transform_mode, $langcode); + $build_list[$key]['#transform'] = $transform; + $build_list[$key]['#weight'] = $weight++; + } + } + + return $build_list; + } + + /** + * Builds the transform array for an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to transform. + * @param string $transform_mode + * (optional) The transform mode that used to transform the entity. + * @param string $langcode + * (optional) For which language the entity should be transformed, defaults + * to the current content language. + * + * @return array + * A transform array for the entity. + */ + protected function transformEntity(EntityInterface $entity, $transform_mode = 'full', $langcode = NULL) { + // Set build defaults. + $transformation = $this->getBuildDefaults($entity, $transform_mode); + $entityType = $this->entityTypeId; + $this->moduleHandler()->alter([$entityType . '_transform_defaults', 'entity_transform_defaults'], $transformation, $entity, $transform_mode); + return $transformation; + } + + /** + * Provides entity-specific defaults to the build process. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which the defaults should be provided. + * @param string $transform_mode + * The transform mode that should be used. + * + * @return array + * Transformation array with defaults. + */ + protected function getBuildDefaults(EntityInterface $entity, $transform_mode) { + return [ + 'type' => 'entity', + 'entity_type' => $entity->getEntityTypeId(), + 'bundle' => $entity->bundle(), + 'id' => $entity->id(), + 'transform_mode' => $transform_mode, + "#{$this->entityTypeId}" => $entity, + // @todo This is deprecated and will be removed at some point. + "#entity" => $entity, + ]; + } + + /** + * Builds an entity's transform; augments entity defaults. + * + * This function is assigned as a #pre_transform callback in ::transform(). + * + * It transforms the transformable array for a single entity to the same + * structure as if we were transforming multiple entities, and then calls the + * default ::buildMultiple() #pre_transform callback. + * + * @param array $build + * A transformable array containing build information and context for an + * entity transform. + * + * @return array + * The updated transformable array. + * + * @see \Drupal\Core\Render\RendererInterface::render() + */ + public function build(array $build) { + $build_list = [$build]; + $build_list = $this->buildMultiple($build_list); + return $build_list[0]; + } + + /** + * Builds multiple entities' transforms; augments entity defaults. + * + * This function is assigned as a #pre_transform callback + * in ::transformMultiple(). + * + * By delaying the building of an entity until the #pre_transform processing + * in \Drupal::service('transform_api.transformer')->transform(), the + * processing cost of assembling an entity's transformable array is saved + * on cache-hit requests. + * + * @param array $build_list + * A transformable array containing build information and context for an + * entity transform. + * + * @return array + * The updated transformable array. + * + * @see \Drupal\Core\Render\RendererInterface::render() + */ + public function buildMultiple(array $build_list) { + // Build the transform modes and display objects. + $transform_modes = []; + $entity_type_key = "#{$this->entityTypeId}"; + $transform_hook = "{$this->entityTypeId}_transform"; + + // Find the keys for the ContentEntities in the build; Store entities for + // transformation by transform_mode. + $children = Transform::children($build_list); + foreach ($children as $key) { + if ($build_list[$key] instanceof TransformInterface) { + continue; + } + if (isset($build_list[$key][$entity_type_key])) { + $entity = $build_list[$key][$entity_type_key]; + if ($entity instanceof FieldableEntityInterface) { + $transform_modes[$build_list[$key]['transform_mode']][$key] = $entity; + } + } + } + + // Build content for the displays represented by the entities. + foreach ($transform_modes as $transform_mode => $transform_mode_entities) { + $displays = EntityTransformDisplay::collectTransformDisplays($transform_mode_entities, $transform_mode); + $this->buildComponents($build_list, $transform_mode_entities, $displays, $transform_mode); + foreach (array_keys($transform_mode_entities) as $key) { + // Allow for alterations while building, before transforming. + $entity = $build_list[$key][$entity_type_key]; + $display = $displays[$entity->bundle()]; + + $this->moduleHandler()->invokeAll($transform_hook, [&$build_list[$key], $entity, $display, $transform_mode]); + $this->moduleHandler()->invokeAll('entity_transform', [&$build_list[$key], $entity, $display, $transform_mode]); + + $this->alterBuild($build_list[$key], $entity, $display, $transform_mode); + } + } + + foreach ($build_list as $key => $build) { + if ($build instanceof TransformInterface) { + continue; + } + if (isset($build['#transform'])) { + /** @var \Drupal\transform_api\Transform\EntityTransform $transform */ + $transform = $build['#transform']; + unset($build['#transform']); + $transform->setBuild($build); + $build_list[$key] = $transform; + } + } + + return $build_list; + } + + /** + * {@inheritdoc} + */ + public function buildComponents(array &$build, array $entities, array $displays, $transform_mode) { + $entities_by_bundle = []; + foreach ($entities as $id => $entity) { + // Initialize the field item attributes for the fields being displayed. + // The entity can include fields that are not displayed, and the display + // can include components that are not fields, so we want to act on the + // intersection. However, the entity can have many more fields than are + // displayed, so we avoid the cost of calling $entity->getProperties() + // by iterating the intersection as follows. + foreach ($displays[$entity->bundle()]->getComponents() as $name => $options) { + if ($entity->hasField($name)) { + foreach ($entity->get($name) as $item) { + $item->_attributes = []; + } + } + } + + // Group the entities by bundle. + $entities_by_bundle[$entity->bundle()][$id] = $entity; + } + + // Invoke hook_entity_prepare_transform(). + $this->moduleHandler()->invokeAll('entity_prepare_transform', [ + $this->entityTypeId, + $entities, + $displays, + $transform_mode, + ]); + + // Let the displays build their transform arrays. + foreach ($entities_by_bundle as $bundle => $bundle_entities) { + $display = $displays[$bundle]; + $display_build = $display->buildMultiple($bundle_entities); + foreach ($bundle_entities as $id => $entity) { + $build[$id] += $display_build[$id]; + } + + if ($display->getComponent('label')) { + $build[$id] += [ + 'label' => $entity->label(), + ]; + } + + if ($display->getComponent('links')) { + $build[$id] += [ + 'url' => $entity->toUrl()->toString(), + ]; + } + } + } + + /** + * Specific per-entity building. + * + * @param array $build + * The transform array that is being created. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be prepared. + * @param \Drupal\transform_api\Configs\EntityTransformDisplayInterface $display + * The entity transform display holding the display options configured for + * the entity components. + * @param string $transform_mode + * The transform mode that should be used to prepare the entity. + */ + protected function alterBuild(array &$build, EntityInterface $entity, EntityTransformDisplayInterface $display, $transform_mode) {} + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return [$this->entityTypeId . '_transform']; + } + + /** + * {@inheritdoc} + */ + public function resetCache(array $entities = NULL) { + // If no set of specific entities is provided, invalidate the entity + // transform builder's cache tag. This will invalidate all entities + // transformed by this transform builder. + // Otherwise, if a set of specific entities is provided, invalidate those + // specific entities only, plus their list cache tags, because any lists in + // which these entities are transformed, must be invalidated as well. + // However, even in this case, we might invalidate more cache items than + // necessary. + // When we have a way to invalidate only those cache items that have both + // the individual entity's cache tag and the transform builder's cache tag, + // we'll be able to optimize this further. + if (isset($entities)) { + $tags = []; + foreach ($entities as $entity) { + $tags = Cache::mergeTags($tags, $entity->getCacheTags()); + $tags = Cache::mergeTags($tags, $entity->getEntityType()->getListCacheTags()); + } + Cache::invalidateTags($tags); + } + else { + Cache::invalidateTags($this->getCacheTags()); + } + } + + /** + * Determines whether the transform mode is cacheable. + * + * @param string $transform_mode + * Name of the transform mode that should be transformed. + * + * @return bool + * TRUE if the transform mode can be cached, FALSE otherwise. + */ + protected function isTransformModeCacheable($transform_mode) { + if ($transform_mode == 'default') { + // The 'default' is not an actual transform mode. + return TRUE; + } + $transform_modes_info = $this->entityDisplayRepository->getTransformModes($this->entityTypeId); + return !empty($transform_modes_info[$transform_mode]['cache']); + } + + /** + * {@inheritdoc} + */ + public function transformField(FieldItemListInterface $items, $display_options = []) { + $entity = $items->getEntity(); + // If the field is not translatable and the entity is, then the field item + // list always points to the default translation of the entity. Attempt to + // fetch it in the current content language. + if (!$items->getFieldDefinition()->isTranslatable() && $entity->isTranslatable()) { + $entity = $this->entityRepository->getTranslationFromContext($entity); + } + + $field_name = $items->getFieldDefinition()->getName(); + $display = $this->getSingleFieldDisplay($entity, $field_name, $display_options); + + $output = []; + $build = $display->build($entity); + if (isset($build[$field_name])) { + $output = $build[$field_name]; + } + + return $output; + } + + /** + * {@inheritdoc} + */ + public function transformFieldItem(FieldItemInterface $item, $display_options = []) { + $entity = $item->getEntity(); + $field_name = $item->getFieldDefinition()->getName(); + + // Clone the entity since we are going to modify field values. + $clone = clone $entity; + + // Push the item as the single value for the field, and defer + // to transformField() to build the transform array for the whole list. + $clone->{$field_name}->setValue([$item->getValue()]); + $elements = $this->transformField($clone->{$field_name}, $display_options); + + // Extract the part of the transform array we need. + $output = $elements[0] ?? []; + if (isset($elements['#access'])) { + $output['#access'] = $elements['#access']; + } + + return $output; + } + + /** + * Gets an EntityTransformDisplay for transforming an individual field. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param string $field_name + * The field name. + * @param string|array $display_options + * The display options passed to the transformField() method. + * + * @return \Drupal\transform_api\Configs\EntityTransformDisplayInterface + * An EntityTransformDisplay with a single field. + */ + protected function getSingleFieldDisplay($entity, $field_name, $display_options) { + if (is_string($display_options)) { + // Transform mode: use the Display configured for the transform mode. + $transform_mode = $display_options; + $display = EntityTransformDisplay::collectTransformDisplay($entity, $transform_mode); + // Hide all fields except the current one. + foreach (array_keys($entity->getFieldDefinitions()) as $name) { + if ($name != $field_name) { + $display->removeComponent($name); + } + } + } + else { + // Array of custom display options: use a runtime Display for the + // '_custom' transform mode. Persist the displays created, to reduce + // the number of objects (displays and transformer plugins) created + // when transforming a series of fields individually for cases such + // as views tables. + $entity_type_id = $entity->getEntityTypeId(); + $bundle = $entity->bundle(); + $key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . Crypt::hashBase64(serialize($display_options)); + if (!isset($this->singleFieldDisplays[$key])) { + $this->singleFieldDisplays[$key] = EntityTransformDisplay::create([ + 'targetEntityType' => $entity_type_id, + 'bundle' => $bundle, + 'status' => TRUE, + ])->setComponent($field_name, $display_options); + } + $display = $this->singleFieldDisplays[$key]; + } + + return $display; + } + +} diff --git a/src/Entity/EntityTransformDisplay.php b/src/Entity/EntityTransformDisplay.php index 8a45febe000a66849b257d6a876bb71e6e147aee..aac3647c21c1e3831a0eb826d140a3d5ba18e7cf 100644 --- a/src/Entity/EntityTransformDisplay.php +++ b/src/Entity/EntityTransformDisplay.php @@ -3,9 +3,13 @@ namespace Drupal\transform_api\Entity; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityDisplayBase; use Drupal\Core\Entity\EntityDisplayPluginCollection; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Render\Element; +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; +use Drupal\layout_builder\Field\LayoutSectionItemList; use Drupal\transform_api\Configs\EntityTransformDisplayInterface; /** @@ -171,6 +175,9 @@ class EntityTransformDisplay extends EntityDisplayBase implements EntityTransfor return $displays[$entity->bundle()]; } + /** + * {@inheritdoc} + */ public function __construct(array $values, $entity_type) { $this->pluginManager = \Drupal::service('plugin.manager.transform_api.field_transform'); $this->transformer = \Drupal::service('transform_api.transformer'); @@ -224,4 +231,90 @@ class EntityTransformDisplay extends EntityDisplayBase implements EntityTransfor ]; } + /** + * {@inheritdoc} + */ + public function build(FieldableEntityInterface $entity) { + $build = $this->buildMultiple([$entity]); + return $build[0]; + } + + /** + * {@inheritdoc} + */ + public function buildMultiple(array $entities) { + $build_list = []; + foreach ($entities as $key => $entity) { + $build_list[$key] = []; + } + + // Run field transformers. + foreach ($this->getComponents() as $name => $options) { + /** @var \Drupal\transform_api\FieldTransformInterface $transformer */ + if ($transformer = $this->getRenderer($name)) { + // Group items across all entities and pass them to the transformer's + // prepareTransform() method. + $grouped_items = []; + foreach ($entities as $id => $entity) { + $items = $entity->get($name); + $items->filterEmptyItems(); + $grouped_items[$id] = $items; + } + $transformer->prepareTransform($grouped_items); + + // Then let the transformer build the output for each entity. + foreach ($entities as $id => $entity) { + $items = $grouped_items[$id]; + // Making an exception for LayoutSectionItemList. + // @todo Find a better solution or wait and see + // what https://www.drupal.org/node/2942975 might bring + if ($items instanceof LayoutSectionItemList) { + /** @var \Drupal\Core\Access\AccessResultInterface $field_access */ + $field_access = AccessResult::allowed(); + } + else { + /** @var \Drupal\Core\Access\AccessResultInterface $field_access */ + $field_access = $items->access('view', NULL, TRUE); + } + // The language of the field values to display is already determined + // in the incoming $entity. The transformer should build its output of + // those values using: + // - the entity language if the entity is translatable, + // - the current "content language" otherwise. + if ($entity instanceof TranslatableDataInterface && $entity->isTranslatable()) { + $transform_langcode = $entity->language()->getId(); + } + else { + $transform_langcode = NULL; + } + $build_list[$id][$name] = $field_access->isAllowed() ? $transformer->transform($items, $transform_langcode) : []; + // Apply the field access cacheability metadata to the + // transformation array. + $this->renderer->addCacheableDependency($build_list[$id][$name], $field_access); + // Let other modules alter the field transformation array. + \Drupal::moduleHandler()->alter('field_transform', $build_list[$id][$name]); + } + } + } + + foreach ($entities as $id => $entity) { + // Assign the configured weights. + foreach ($this->getComponents() as $name => $options) { + if (isset($build_list[$id][$name]) && !Element::isEmpty($build_list[$id][$name])) { + $build_list[$id][$name]['#weight'] = $options['weight']; + } + } + + // Let other modules alter the transformable array. + $context = [ + 'entity' => $entity, + 'transform_mode' => $this->originalMode, + 'display' => $this, + ]; + \Drupal::moduleHandler()->alter('entity_transform_build', $build_list[$id], $context); + } + + return $build_list; + } + } diff --git a/src/EntityTransformBuilderInterface.php b/src/EntityTransformBuilderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2896d98724c4ff6002e07a65f29db2718c89d7d8 --- /dev/null +++ b/src/EntityTransformBuilderInterface.php @@ -0,0 +1,172 @@ +<?php + +namespace Drupal\transform_api; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldItemInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\transform_api\Transform\EntityTransform; + +/** + * Defines an interface for entity transform builders. + * + * @ingroup entity_api + */ +interface EntityTransformBuilderInterface { + + /** + * Builds the component fields and properties of a set of entities. + * + * @param array &$build + * The transformable array representing the entity content. + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * The entities whose content is being built. + * @param \Drupal\transform_api\Configs\EntityTransformDisplayInterface[] $displays + * The array of entity transform displays holding the display options + * configured for the entity components, keyed by bundle name. + * @param string $transform_mode + * The transform mode in which the entity is being transformed. + */ + public function buildComponents(array &$build, array $entities, array $displays, $transform_mode); + + /** + * Configure a transform for an entity. + * + * @param \Drupal\transform_api\Transform\EntityTransform $transform + * The transform to configure. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to configure the transform for. + * @param string $transform_mode + * (optional) The transform mode that used to transform the entity. + * @param string $langcode + * (optional) For which language the entity should be transformed, defaults + * to the current content language. + */ + public function configureTransform(EntityTransform $transform, EntityInterface $entity, $transform_mode = 'full', $langcode = NULL); + + /** + * Builds the transform array for the provided entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to transform. + * @param string $transform_mode + * (optional) The transform mode that used to transform the entity. + * @param string $langcode + * (optional) For which language the entity should be transformed, defaults + * to the current content language. + * + * @return array + * A transform array for the entity. + * + * @throws \InvalidArgumentException + * Can be thrown when the set of parameters is inconsistent, like when + * trying to transform a Comment and passing a Node which is not the one the + * comment belongs to, or not passing one, and having the comment node not + * be available for loading. + */ + public function transform(EntityInterface $entity, $transform_mode = 'full', $langcode = NULL); + + /** + * Builds the transform array for the provided entities. + * + * @param array $entities + * An array of entities implementing EntityInterface to transform. + * @param string $transform_mode + * (optional) The transform mode that should be used to transform + * the entity. + * @param string $langcode + * (optional) For which language the entity should be transformed, defaults + * to the current content language. + * + * @return array + * A transform array for the entities, indexed by the same keys as the + * entities array passed in $entities. + * + * @throws \InvalidArgumentException + * Can be thrown when the set of parameters is inconsistent, like when + * trying to transform Comments and passing a Node which is not the one the + * comments belongs to, or not passing one, and having the comments node not + * be available for loading. + */ + public function transformMultiple(array $entities = [], $transform_mode = 'full', $langcode = NULL); + + /** + * Resets the entity transform cache. + * + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * (optional) If specified, the cache is reset for the given entities only. + */ + public function resetCache(array $entities = NULL); + + /** + * Builds a transformable array for the value of a single field in an entity. + * + * The resulting output is a fully themed field with label and multiple + * values. + * + * This function can be used by third-party modules that need to output an + * isolated field. + * - The FieldTransformInterface::transform() method can be used to output a + * single formatted field value, without label or wrapping field + * transformation. + * + * The function takes care of invoking the prepare_transform steps. It also + * respects field access permissions. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * FieldItemList containing the values to be displayed. + * @param string|array $display_options + * Can be either: + * - The name of a transform mode. The field will be displayed according to + * the display settings specified for this transform mode in the $field + * definition for the field in the entity's bundle. If no display settings + * are found for the transform mode, the settings for the 'default' + * transform mode will be used. + * - An array of display options. The following key/value pairs are allowed: + * - label: (string) Position of the label. The default 'field' theme + * implementation supports the values 'include' and 'omit'. + * Defaults to 'omit'. + * - type: (string) The transformer to use. Defaults to the + * 'default_transformer' for the field type. The default transformer + * will also be used if the requested transformer is not available. + * - settings: (array) Settings specific to the transformer. Defaults to + * the transformer's default settings. + * - weight: (float) The weight to assign to the transformable element. + * Defaults to 0. + * + * @return array + * A transform array for the field values. + * + * @see \Drupal\Core\Entity\EntityTransformBuilderInterface::viewFieldItem() + */ + public function transformField(FieldItemListInterface $items, $display_options = []); + + /** + * Builds a transformable array for a single field item. + * + * @param \Drupal\Core\Field\FieldItemInterface $item + * FieldItem to be displayed. + * @param string|array $display_options + * Can be either the name of a transform mode, or an array of display + * settings. See EntityTransformBuilderInterface::transformField() for + * more information. + * + * @return array + * A transform array for the field item. + * + * @see \Drupal\Core\Entity\EntityTransformBuilderInterface::viewField() + */ + public function transformFieldItem(FieldItemInterface $item, $display_options = []); + + /** + * The cache tag associated with this entity transform builder. + * + * An entity transform builder is instantiated on a per-entity type basis, + * so the cache tags are also per-entity type. + * + * @return array + * An array of cache tags. + */ + public function getCacheTags(); + +} diff --git a/src/Plugin/Transform/Type/Entity.php b/src/Plugin/Transform/Type/Entity.php index 38632d1bd88e760955ae821689acc472ca01ccac..aeec0e5a2110d1c31b7da0251613f37d44b9faa6 100644 --- a/src/Plugin/Transform/Type/Entity.php +++ b/src/Plugin/Transform/Type/Entity.php @@ -29,6 +29,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * id = "entity", * title = "Entity transform" * ) + * + * @deprecated in transform_api:1.1.0 and is removed from transform_api:2.0.0. + * Use the EntityTransformBuilder instead. */ class Entity extends TransformationTypeBase { @@ -130,7 +133,7 @@ class Entity extends TransformationTypeBase { if (is_array($transform->getValue('ids'))) { return $this->prepareMultipleTransforms($transform->getValue('entity_type'), $transform->getValue('ids'), $transform->getValue('transform_mode') ?? EntityTransformRepositoryInterface::DEFAULT_DISPLAY_MODE, $langcode); } - elseif (!is_null($entity_transform->getEntity())) { + elseif (!is_null($entity_transform->getEntities())) { return $this->transformEntity($entity_transform->getEntity(), $entity_transform->getFields(), $entity_transform->getDisplay()); } else { diff --git a/src/Transform/EntityTransform.php b/src/Transform/EntityTransform.php index 374fe21a8bf3a2ed5ebf2a8f15fa3cc7639bd1aa..52a992fdf5041dac2fb8f2b1aba1b7282e9fb3fb 100644 --- a/src/Transform/EntityTransform.php +++ b/src/Transform/EntityTransform.php @@ -3,38 +3,55 @@ namespace Drupal\transform_api\Transform; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Language\LanguageInterface; use Drupal\transform_api\Configs\EntityTransformDisplayInterface; +use Drupal\transform_api\EntityTransformBuilderInterface; use Drupal\transform_api\Repository\EntityTransformRepositoryInterface; /** * A transform of one or more entities. */ -class EntityTransform extends PluginTransformBase { +class EntityTransform extends TransformBase { /** - * The entity to transform. + * The entities to transform. * - * @var \Drupal\Core\Entity\EntityInterface|null + * @var \Drupal\Core\Entity\EntityInterface[] */ - protected EntityInterface|null $entity = NULL; + protected array $entities = []; /** * The transform mode to use for transformation. * - * @var \Drupal\transform_api\Configs\EntityTransformDisplayInterface|null + * @var string */ - protected EntityTransformDisplayInterface|null $display = NULL; + protected string $transformMode = EntityTransformRepositoryInterface::DEFAULT_DISPLAY_MODE; /** - * Field transforms for the fields of the entity. + * The language to use for transformation. * - * @var \Drupal\transform_api\FieldTransformInterface[] + * @var string|null */ - protected array $fields = []; + protected ?string $langcode = NULL; /** - * Construct a EntityTransform. + * Whether this transform is for multiple entities. + * + * @var bool + */ + protected bool $multiple = FALSE; + + /** + * Prepared transformation array for the entity. + * + * @var array + */ + protected array $build = []; + + /** + * Construct an EntityTransform. + * + * $entity_type and $ids can be left empty, but then you must call either + * setEntity or setEntities() afterward. * * @param string $entity_type * The type of the entity. @@ -46,19 +63,17 @@ class EntityTransform extends PluginTransformBase { * (Optional) The language to use for transformation. */ public function __construct($entity_type, $ids, $transform_mode = EntityTransformRepositoryInterface::DEFAULT_DISPLAY_MODE, $langcode = NULL) { - $this->values = [ - 'entity_type' => $entity_type, - 'ids' => $ids, - 'transform_mode' => $transform_mode, - 'langcode' => $langcode ?? \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(), - ]; - if (is_array($ids)) { - foreach ($ids as $id) { - $this->cacheTags[] = $entity_type . ':' . $id; + $this->transformMode = $transform_mode; + $this->langcode = $langcode; + if (!empty($entity_type) && !empty($ids)) { + if (is_array($ids)) { + $entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple($ids); + $this->setEntities($entities); + } + else { + $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($ids); + $this->setEntity($entity); } - } - else { - $this->cacheTags[] = $entity_type . ':' . $ids; } } @@ -73,14 +88,14 @@ class EntityTransform extends PluginTransformBase { * {@inheritdoc} */ public function getAlterIdentifiers() { - return [$this->getTransformType(), $this->values['entity_type']]; + return [$this->getTransformType(), $this->getEntity()->getEntityTypeId()]; } /** * {@inheritdoc} */ public function isMultiple() { - return is_array($this->getValue('ids')); + return $this->multiple; } /** @@ -93,61 +108,129 @@ class EntityTransform extends PluginTransformBase { /** * Return the entity to be transformed. * - * @return \Drupal\Core\Entity\EntityInterface|null + * @return \Drupal\Core\Entity\EntityInterface * The entity or NULL if not found. */ - public function getEntity(): ?EntityInterface { - return $this->entity; + public function getEntity(): EntityInterface { + return reset($this->entities); } /** * Set the entity to be transformed. * - * @param \Drupal\Core\Entity\EntityInterface|null $entity + * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be transformed. + * @param bool $configure + * Whether to configure the transform via EntityTransformBuilder. */ - public function setEntity(?EntityInterface $entity): void { - $this->entity = $entity; + public function setEntity(EntityInterface $entity, $configure = TRUE) { + $this->multiple = FALSE; + $this->entities = [$entity]; + if ($configure) { + $this->getTransformBuilder()->configureTransform($this, $this->getEntity(), $this->getTransformMode(), $this->getLangcode()); + } } /** - * Return the field transforms for the fields on the entity. + * Return the entities to be transformed. * - * @return \Drupal\transform_api\FieldTransformInterface[] - * Array of field transforms. + * @return \Drupal\Core\Entity\EntityInterface[] The entities or an empty array if not found. + * The entities or an empty array if not found. */ - public function getFields(): array { - return $this->fields; + public function getEntities(): array { + return $this->entities; } /** - * Set the field transforms for the fields on the entity. + * Set the entities to be transformed. * - * @param \Drupal\transform_api\FieldTransformInterface[] $fields - * Array of field transforms. + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * The entities to be transformed. */ - public function setFields(array $fields): void { - $this->fields = $fields; + public function setEntities(array $entities) { + $this->multiple = TRUE; + $this->entities = $entities; } /** * Return the transform mode used to be used for transformation. * - * @return \Drupal\transform_api\Configs\EntityTransformDisplayInterface|null - * The transform mode. + * @return string + * The transform mode used to be used for transformation. */ - public function getDisplay(): ?EntityTransformDisplayInterface { - return $this->display; + public function getTransformMode(): string { + return $this->transformMode; } /** - * Set the transform mode to be used for transformation. + * Set the transform mode used to be used for transformation. * - * @param \Drupal\transform_api\Configs\EntityTransformDisplayInterface|null $display - * The transform mode. + * @param string $transformMode + * The transform mode used to be used for transformation. */ - public function setDisplay(?EntityTransformDisplayInterface $display): void { - $this->display = $display; + public function setTransformMode(string $transformMode) { + $this->transformMode = $transformMode; + } + + /** + * Return the language code used to be used for transformation. + * + * @return string|null + * The language code used to be used for transformation. + */ + public function getLangcode(): ?string { + return $this->langcode; + } + + /** + * Set the language code used to be used for transformation. + * + * @param string $langcode + * The language code used to be used for transformation. + */ + public function setLangcode(string $langcode) { + $this->langcode = $langcode; + } + + /** + * Get the EntityTransformBuilder for the entity's type. + * + * @return \Drupal\transform_api\EntityTransformBuilderInterface + * The EntityTransformBuilder for the entity's type. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function getTransformBuilder(): EntityTransformBuilderInterface { + /** @var \Drupal\transform_api\EntityTransformBuilderInterface $handler */ + $handler = \Drupal::entityTypeManager()->getHandler($this->getEntity()->getEntityTypeId(), 'transform_builder'); + return $handler; + } + + /** + * {@inheritdoc} + */ + public function transform(): array { + if (!empty($this->build)) { + $transformation = $this->build; + } + elseif ($this->isMultiple()) { + $transformation = $this->getTransformBuilder()->transformMultiple($this->getEntities(), $this->getTransformMode(), $this->getLangcode()); + } + else { + $transformation = $this->getTransformBuilder()->transform($this->getEntity(), $this->getTransformMode(), $this->getLangcode()); + } + $this->applyTo($transformation); + return $transformation; + } + + /** + * Set the prepared transformation of the entity. + * + * @param array $build + * The prepared build. + */ + public function setBuild(array $build) { + $this->build = $build; } /** @@ -155,19 +238,20 @@ class EntityTransform extends PluginTransformBase { * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be transformed. - * @param \Drupal\transform_api\FieldTransformInterface[] $fields - * The field transforms for the entity fields. - * @param \Drupal\transform_api\Configs\EntityTransformDisplayInterface $display - * The transform display mode to be used for transformation. + * @param array $build + * The prepared transformation of the entity. + * @param string $transform_mode + * (Optional) The transform mode to be used for transformation. * * @return EntityTransform * A fully prepared EntityTransform. + * + * @internal */ - public static function createPrepared(EntityInterface $entity, array $fields, EntityTransformDisplayInterface $display): EntityTransform { - $transform = new self($entity->getEntityTypeId(), $entity->id(), $display->getMode()); + public static function createPrepared(EntityInterface $entity, array $build, $transform_mode = EntityTransformRepositoryInterface::DEFAULT_DISPLAY_MODE): EntityTransform { + $transform = new self('', 0, $transform_mode); $transform->setEntity($entity); - $transform->setFields($fields); - $transform->setDisplay($display); + $transform->setBuild($build); return $transform; } @@ -183,7 +267,9 @@ class EntityTransform extends PluginTransformBase { * An EntityTransform based on the entity. */ public static function createFromEntity(EntityInterface $entity, $transform_mode = EntityTransformRepositoryInterface::DEFAULT_DISPLAY_MODE) { - return new self($entity->getEntityTypeId(), $entity->id(), $transform_mode, $entity->language()->getId()); + $transform = new self('', 0, $transform_mode); + $transform->setEntity($entity); + return $transform; } /** @@ -198,15 +284,9 @@ class EntityTransform extends PluginTransformBase { * An EntityTransform based on the entities. */ public static function createFromMultipleEntities(array $entities, $transform_mode = EntityTransformRepositoryInterface::DEFAULT_DISPLAY_MODE) { - $ids = []; - $entityTypeId = ''; - $langcode = NULL; - foreach ($entities as $entity) { - $ids[] = $entity->id(); - $entityTypeId = $entity->getEntityTypeId(); - $langcode = $entity->language()->getId(); - } - return new self($entityTypeId, $ids, $transform_mode, $langcode); + $transform = new self('', 0, $transform_mode); + $transform->setEntities($entities); + return $transform; } } diff --git a/src/Transform/FieldTransform.php b/src/Transform/FieldTransform.php index 43613f67ce870ae9176fa541bb0fb12969675a69..de12d43ad21b2a5ea2168747f40b80cd320c0c1f 100644 --- a/src/Transform/FieldTransform.php +++ b/src/Transform/FieldTransform.php @@ -9,6 +9,9 @@ use Drupal\transform_api\Repository\EntityTransformRepositoryInterface; /** * A transform for a field. + * + * @deprecated in transform_api:1.1.0 and is removed from transform_api:2.0.0. + * Use the EntityTransformBuilder instead. */ class FieldTransform extends TransformBase { diff --git a/src/Transform/PluginTransformBase.php b/src/Transform/PluginTransformBase.php index 53fe2fea9b63e2210575eed8ccb8e8f69a9f087c..a0656fb48379aebe97591632dcba0a3d6c8eed2f 100644 --- a/src/Transform/PluginTransformBase.php +++ b/src/Transform/PluginTransformBase.php @@ -2,21 +2,44 @@ namespace Drupal\transform_api\Transform; +use Drupal\transform_api\TransformationTypeInterface; + /** * Base class for transforms with a transform type plugin. */ abstract class PluginTransformBase extends TransformBase { + /** + * The plugin belonging to this transform. + * + * @var \Drupal\transform_api\TransformationTypeInterface + */ + protected TransformationTypeInterface $plugin; + + /** + * Return the plugin belonging to this transform. + * + * @return \Drupal\transform_api\TransformationTypeInterface + * The plugin belonging to this transform. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + protected function getPlugin(): TransformationTypeInterface { + if (empty($this->plugin)) { + /** @var \Drupal\transform_api\TransformationTypeManager $transformationTypeManager */ + $transformationTypeManager = \Drupal::service('plugin.manager.transform_api.transformation_type'); + $this->plugin = $transformationTypeManager->createInstance($this->getTransformType(), $this->getValues()); + } + return $this->plugin; + } + /** * {@inheritDoc} * * @throws \Drupal\Component\Plugin\Exception\PluginException */ public function transform() { - /** @var \Drupal\transform_api\TransformationTypeManager $transformationTypeManager */ - $transformationTypeManager = \Drupal::service('plugin.manager.transform_api.transformation_type'); - $plugin = $transformationTypeManager->createInstance($this->getTransformType(), $this->getValues()); - return $plugin->transform($this); + return $this->getPlugin()->transform($this); } } diff --git a/src/Transform/RefinableTransform.php b/src/Transform/RefinableTransform.php new file mode 100644 index 0000000000000000000000000000000000000000..9a25d8496977d95aeef150d7cef3877acb2e41c7 --- /dev/null +++ b/src/Transform/RefinableTransform.php @@ -0,0 +1,73 @@ +<?php + +namespace Drupal\transform_api\Transform; + +use Drupal\Core\Cache\RefinableCacheableDependencyTrait; + +/** + * + */ +abstract class RefinableTransform extends TransformBase { + + use RefinableCacheableDependencyTrait; + + /** + * The transformation array. + * + * @var array + */ + protected array $transformation = []; + + /** + * The transformation weight. + * + * @var int|null + */ + protected $weight = NULL; + + /** + * Gets the transformation array. + * + * @return array + * The transformation array. + */ + public function getTransformation(): array { + return $this->transformation; + } + + /** + * Set the transformation array to output. + * + * @param array $transformation + * The transformation array to output. + */ + public function setTransformation(array $transformation) { + $this->transformation = $transformation; + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->weight; + } + + /** + * Set the transformation weight. + * + * @param int|null $weight + * The transformation weight or NULL for no weight. + */ + public function setWeight($weight) { + $this->weight = $weight; + } + + /** + * {@inheritdoc} + */ + public function transform() { + $this->applyTo($this->transformation); + return $this->transformation; + } + +} diff --git a/src/Transform/TransformBase.php b/src/Transform/TransformBase.php index a1b285940ad363eb2ec18e15ef5449e41342f5ba..3136b3a2058ba2733d6c645bd63fb1c1385eb5ca 100644 --- a/src/Transform/TransformBase.php +++ b/src/Transform/TransformBase.php @@ -14,7 +14,14 @@ abstract class TransformBase extends CacheableMetadata implements TransformInter * * @var array */ - protected $values = []; + protected array $values = []; + + /** + * The weight of the transform. + * + * @var int|null + */ + protected $weight = NULL; /** * {@inheritdoc} @@ -71,14 +78,24 @@ abstract class TransformBase extends CacheableMetadata implements TransformInter * {@inheritdoc} */ public function getWeight() { - return NULL; + return $this->weight; + } + + /** + * Sets the weight of the transform. + * + * @param int $weight + * The weight of the transform. + */ + public function setWeight(int $weight) { + $this->weight = $weight; } /** * {@inheritdoc} */ public function shouldBeCached() { - return TRUE; + return !empty($this->values); } /** diff --git a/src/Transformer.php b/src/Transformer.php index c769bb4d7e3862abc345698f3e1593f25a322146..5c6e84d5f304ea8e30f750f55fa2f6c5985026a7 100644 --- a/src/Transformer.php +++ b/src/Transformer.php @@ -49,7 +49,7 @@ class Transformer { */ protected ControllerResolverInterface $controllerResolver; - protected const RESERVED_WORDS = ['#cache', '#collapse', '#lazy_transformer']; + protected const RESERVED_WORDS = ['#cache', '#collapse', '#lazy_transformer', '#sorted']; /** * Constructs a Transformer service. @@ -211,17 +211,20 @@ class Transformer { $this->handleLazyTransformers($transformation); } $collapse = $transformation['#collapse'] ?? FALSE; - unset($transformation['#cache']); - unset($transformation['#collapse']); - unset($transformation['#lazy_transformer']); + $sorted = $transformation['#sorted'] ?? FALSE; + foreach (self::RESERVED_WORDS as $word) { + unset($transformation[$word]); + } foreach ($transformation as $key => $value) { if (is_array($value)) { $this->cleanupCacheMetadata($transformation[$key]); } } + if ($sorted) { + $transformation = array_values($transformation); + } if ($collapse) { - $count = count($transformation); - if ($count == 1) { + if (count($transformation) == 1) { $transformation = $transformation[array_key_first($transformation)]; } } diff --git a/transform_api.api.php b/transform_api.api.php index 115c2049bacff57b1f27d8aee2c2e54ed984ad45..73bdb2cd973124bb02bc08f321421eefe9658a70 100644 --- a/transform_api.api.php +++ b/transform_api.api.php @@ -9,9 +9,10 @@ * @addtogroup hooks * @{ */ +use Drupal\transform_api\Transform\EntityTransform; /** - * Alter all transformation arrays as they are transformed. + * Alter all transformation arrays after they are transformed. * * @param array $transformation * The transformation array of the transform. @@ -21,12 +22,14 @@ function hook_transform_alter(&$transformation) { } /** - * Alter transformation arrays with a specific HOOK identifier. + * Alter transformation arrays with a specific TRANSFORM_TYPE identifier. + * + * These can include 'entity', 'field', 'block' and more. * * @param array $transformation * The transformation array of the transform. */ -function hook_HOOK_transform_alter(&$transformation) { +function hook_TRANSFORM_TYPE_transform_alter(&$transformation) { } @@ -40,6 +43,231 @@ function hook_blocks_transform_config_alter(&$blocks) { } +/** + * Alter the transform array generated by an EntityDisplay for an entity. + * + * @param array $build + * The transform array generated by the EntityDisplay. + * @param array $context + * An associative array containing: + * - entity: The entity being transformed. + * - transform_mode: The transform mode; for example, 'full' or 'teaser'. + * - display: The EntityDisplay holding the display options. + * + * @ingroup entity_crud + */ +function hook_entity_transform_build_alter(&$build, $context) { + +} + +/** + * Change the transform mode of an entity that is being transformed. + * + * @param string $transform_mode + * The transform_mode that is to be used to transform the entity. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that is being transformed. + * + * @ingroup entity_crud + */ +function hook_entity_transform_mode_alter(&$transform_mode, \Drupal\Core\Entity\EntityInterface $entity) { + // For nodes, change the transform mode when it is teaser. + if ($entity->getEntityTypeId() == 'node' && $transform_mode == 'teaser') { + $transform_mode = 'my_custom_transform_mode'; + } +} + +/** + * Change the default configuration of an EntityTransform before transformation. + * + * This happens just before cache checking during transformation. + * + * Invoked for a specific entity type. + * + * @param \Drupal\transform_api\Transform\EntityTransform $transform + * The entity transform. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be transformed. + * @param string $transform_mode + * The transform mode used for transformation. + * @param string|null $langcode + * The language code used for transformation. + * + * @see hook_entity_transform_configure() + */ +function hook_ENTITY_TYPE_transform_configure(EntityTransform $transform, \Drupal\Core\Entity\EntityInterface $entity, $transform_mode, $langcode) { +} + +/** + * Change the default configuration of an EntityTransform before transformation. + * + * This happens just before cache checking during transformation. + * + * @param \Drupal\transform_api\Transform\EntityTransform $transform + * The entity transform. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be transformed. + * @param string $transform_mode + * The transform mode used for transformation. + * @param string|null $langcode + * The language code used for transformation. + * + * @see hook_ENTITY_TYPE_transform_configure() + */ +function hook_entity_transform_configure(EntityTransform $transform, \Drupal\Core\Entity\EntityInterface $entity, $transform_mode, $langcode) { +} + +/** + * Alter entity transformation array default values. + * + * Invoked for a specific entity type. + * + * @param array &$build + * A transform array containing the entity's default values. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that is being transformed. + * @param string $transform_mode + * The transform_mode that is to be used to transform the entity. + * + * @see \Drupal\Core\Render\RendererInterface::render() + * @see \Drupal\Core\Entity\EntityViewBuilder + * @see hook_entity_transform_defaults_alter() + * @see hook_ENTITY_TYPE_transform_configure() + * + * @ingroup entity_crud + */ +function hook_ENTITY_TYPE_transform_defaults_alter(array &$build, \Drupal\Core\Entity\EntityInterface $entity, $transform_mode) { + +} + +/** + * Alter entity transformation array default values. + * + * @param array &$build + * A transform array containing the entity's default values. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that is being transformed. + * @param string $transform_mode + * The transform_mode that is to be used to transform the entity. + * + * @see \Drupal\transform_api\Transformer::transform() + * @see \Drupal\transform_api\Entity\EntityTransformBuilder + * @see hook_ENTITY_TYPE_build_defaults_alter() + * @see hook_entity_transform_configure() + * + * @ingroup entity_crud + */ +function hook_entity_transform_defaults_alter(array &$build, \Drupal\Core\Entity\EntityInterface $entity, $transform_mode) { + +} + +/** + * Act on entities of a particular type being assembled before transforming. + * + * @param array &$build + * A transformable array representing the entity content. The module may add + * elements to $build prior to transforming. The structure of $build is a + * transformable array as expected by + * \Drupal\transform_api\TransformInterface::transform(). + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The entity transform display holding the display options configured for the + * entity components. + * @param string $transform_mode + * The transform mode the entity is rendered in. + * + * @see hook_entity_transform_alter() + * @see hook_ENTITY_TYPE_transform() + * + * @ingroup entity_crud + */ +function hook_entity_transform(array &$build, \Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display, $transform_mode) { + // Only do the extra work if the component is configured to be transformed. + // This assumes a 'mymodule_addition' extra field has been defined for the + // entity bundle in hook_entity_extra_field_info(). + if ($display->getComponent('mymodule_addition')) { + $build['mymodule_addition'] = [ + 'value' => mymodule_addition($entity), + '#collapse' => TRUE, + ]; + } +} + +/** + * Act on entities of a particular type being assembled before transforming. + * + * @param array &$build + * A transformable array representing the entity content. The module may add + * elements to $build prior to transforming. The structure of $build is a + * transformable array as expected by + * \Drupal\transform_api\TransformInterface::transform(). + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + * @param \Drupal\transform_api\Configs\EntityTransformDisplayInterface $display + * The entity transform display holding the display options configured for the + * entity components. + * @param string $transform_mode + * The transform mode the entity is transformed in. + * + * @see hook_ENTITY_TYPE_transform_alter() + * @see hook_entity_transform() + * + * @ingroup entity_crud + */ +function hook_ENTITY_TYPE_transform(array &$build, \Drupal\Core\Entity\EntityInterface $entity, \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display, $transform_mode) { + // Only do the extra work if the component is configured to be displayed. + // This assumes a 'mymodule_addition' extra field has been defined for the + // entity bundle in hook_entity_extra_field_info(). + if ($display->getComponent('mymodule_addition')) { + $build['mymodule_addition'] = [ + 'value' => mymodule_addition($entity), + '#collapse' => TRUE, + ]; + } +} + +/** + * Act on entities as they are being prepared for transformation. + * + * Allows you to operate on multiple entities as they are being prepared for + * transformation. Only use this if attaching the data during the entity loading + * phase is not appropriate, for example when attaching other 'entity' style + * objects. + * + * @param string $entity_type_id + * The type of entities being transformed (i.e. node, user, comment). + * @param array $entities + * The entities keyed by entity ID. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface[] $displays + * The array of entity transform displays holding the display options + * configured for the entity components, keyed by bundle name. + * @param string $transform_mode + * The transform mode. + * + * @ingroup entity_crud + */ +function hook_entity_prepare_transform($entity_type_id, array $entities, array $displays, $transform_mode) { + // Load a specific node into the user object for later transformation. + if (!empty($entities) && $entity_type_id == 'user') { + // Only do the extra work if the component is configured to be + // displayed. This assumes a 'mymodule_addition' extra field has been + // defined for the entity bundle in hook_entity_extra_field_info(). + $ids = []; + foreach ($entities as $id => $entity) { + if ($displays[$entity->bundle()]->getComponent('mymodule_addition')) { + $ids[] = $id; + } + } + if ($ids) { + $nodes = mymodule_get_user_nodes($ids); + foreach ($ids as $id) { + $entities[$id]->user_node = $nodes[$id]; + } + } + } +} + /** * @} End of "addtogroup hooks". */ diff --git a/transform_api.module b/transform_api.module index da409e897ae428b7b83bc39ea39c170968a50665..8b6fe359ff7b1c9e7194e230626c02ea9e7b5155 100644 --- a/transform_api.module +++ b/transform_api.module @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Url; +use Drupal\transform_api\Entity\EntityTransformBuilder; use Drupal\transform_api\EntityTransformModeInterface; use Drupal\transform_api\Plugin\Derivative\TransformLocalTask; @@ -99,27 +100,84 @@ function transform_api_entity_base_field_info_alter(&$fields, EntityTypeInterfac } /** - * Implements hook_ENTITY_TYPE_insert(). + * Implements hook_entity_extra_field_info(). */ -function transform_api_menu_insert(EntityInterface $entity) { - // Invalidate the transform block cache to update menu-based derivatives. - \Drupal::service('plugin.manager.transform_api.transform_block')->clearCachedDefinitions(); -} +function transform_api_entity_extra_field_info() { + $extra = []; + /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entityTypeBundleInfo */ + $entityTypeBundleInfo = \Drupal::service('entity_type.bundle.info'); + $entity_types = \Drupal::entityTypeManager()->getDefinitions(); + foreach ($entity_types as $entity_type) { + if (!$entity_type->hasViewBuilderClass()) { + continue; + } + $components = []; + if ($entity_type->hasKey('label')) { + $components['label'] = TRUE; + } + if ($entity_type->hasLinkTemplate('canonical')) { + $components['url'] = TRUE; + } -/** - * Implements hook_ENTITY_TYPE_update(). - */ -function transform_api_menu_update(EntityInterface $entity) { - // Invalidate the transform block cache to update menu-based derivatives. - \Drupal::service('plugin.manager.transform_api.transform_block')->clearCachedDefinitions(); + // There are some exceptions among core modules. + // Users may not have a label field, but it makes + // the display name available. + if ($entity_type->id() == 'user') { + $components['label'] = TRUE; + } + // Media should only have canonical urls if enabled in media settings. + if ($entity_type->id() == 'media' && !\Drupal::config('media.settings')->get('standalone_url')) { + unset($components['url']); + } + // Block content does not have its own URL. + if ($entity_type->id() == 'block_content') { + unset($components['url']); + } + + // For testing purposes. + $components['label'] = TRUE; + + if (empty($components)) { + continue; + } + + foreach ($entityTypeBundleInfo->getBundleInfo($entity_type->id()) as $bundle_id => $bundle) { + if ($components['label'] ?? FALSE) { + $extra[$entity_type->id()][$bundle_id]['transform']['label'] = [ + 'label' => t('Label'), + 'description' => t('The label of the entity.'), + 'weight' => -100, + 'visible' => TRUE, + ]; + } + if ($components['url'] ?? FALSE) { + $extra[$entity_type->id()][$bundle_id]['transform']['url'] = [ + 'label' => t('Canonical URL'), + 'description' => t('The URL of the entity.'), + 'weight' => 100, + 'visible' => FALSE, + ]; + } + } + } + + return $extra; } /** - * Implements hook_ENTITY_TYPE_delete(). + * Implements hook_entity_type_alter(). */ -function transform_api_menu_delete(EntityInterface $entity) { - // Invalidate the transform block cache to update menu-based derivatives. - \Drupal::service('plugin.manager.transform_api.transform_block')->clearCachedDefinitions(); +function transform_api_entity_type_alter(array &$entity_types) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + foreach ($entity_types as $entity_type_id => $entity_type) { + if (!$entity_type->isInternal()) { + if (!$entity_type->hasHandlerClass('transform_builder') && $entity_type->hasViewBuilderClass()) { + $entity_type->setHandlerClass('transform_builder', EntityTransformBuilder::class); + } + + $entity_types[$entity_type_id] = $entity_type; + } + } } /**