eca-1.0.x-dev/modules/content/src/Plugin/ECA/Event/ContentEntityEvent.php
modules/content/src/Plugin/ECA/Event/ContentEntityEvent.php
<?php
namespace Drupal\eca_content\Plugin\ECA\Event;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\eca\Attribute\EcaEvent;
use Drupal\eca\Attribute\Token;
use Drupal\eca\Entity\Objects\EcaEvent as EcaEventObject;
use Drupal\eca\Event\Tag;
use Drupal\eca\Plugin\CleanupInterface;
use Drupal\eca\Plugin\ECA\Event\EventBase;
use Drupal\eca\Service\ContentEntityTypes;
use Drupal\eca_content\Event\ContentEntityBaseBundle;
use Drupal\eca_content\Event\ContentEntityBaseContentEntity;
use Drupal\eca_content\Event\ContentEntityBundleCreate;
use Drupal\eca_content\Event\ContentEntityBundleDelete;
use Drupal\eca_content\Event\ContentEntityCreate;
use Drupal\eca_content\Event\ContentEntityCustomEvent;
use Drupal\eca_content\Event\ContentEntityDelete;
use Drupal\eca_content\Event\ContentEntityEvents;
use Drupal\eca_content\Event\ContentEntityFieldValuesInit;
use Drupal\eca_content\Event\ContentEntityInsert;
use Drupal\eca_content\Event\ContentEntityLoad;
use Drupal\eca_content\Event\ContentEntityPreDelete;
use Drupal\eca_content\Event\ContentEntityPreLoad;
use Drupal\eca_content\Event\ContentEntityPreSave;
use Drupal\eca_content\Event\ContentEntityPrepareForm;
use Drupal\eca_content\Event\ContentEntityPrepareView;
use Drupal\eca_content\Event\ContentEntityRevisionCreate;
use Drupal\eca_content\Event\ContentEntityRevisionDelete;
use Drupal\eca_content\Event\ContentEntityStorageLoad;
use Drupal\eca_content\Event\ContentEntityTranslationCreate;
use Drupal\eca_content\Event\ContentEntityTranslationDelete;
use Drupal\eca_content\Event\ContentEntityTranslationInsert;
use Drupal\eca_content\Event\ContentEntityUpdate;
use Drupal\eca_content\Event\ContentEntityValidate;
use Drupal\eca_content\Event\ContentEntityView;
use Drupal\eca_content\Event\ContentEntityViewModeAlter;
use Drupal\eca_content\Event\FieldSelectionBase;
use Drupal\eca_content\Event\OptionsSelection;
use Drupal\eca_content\Event\ReferenceSelection;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Plugin implementation of the ECA Events for content entities.
*/
#[EcaEvent(
id: 'content_entity',
deriver: 'Drupal\eca_content\Plugin\ECA\Event\ContentEntityEventDeriver',
version_introduced: '1.0.0',
)]
class ContentEntityEvent extends EventBase implements CleanupInterface {
/**
* A stack of selection event instances.
*
* An instance will be removed by
* \Drupal\eca_content\Plugin\ECA\Event\ContentEntityEvent::cleanupAfterSuccessors.
*
* @var \Drupal\eca_content\Event\OptionsSelection[]
*/
protected static array $optionsSelections = [];
/**
* A stack of selection event instances.
*
* An instance will be removed by
* \Drupal\eca_content\Plugin\ECA\Event\ContentEntityEvent::cleanupAfterSuccessors.
*
* @var \Drupal\eca_content\Event\ReferenceSelection[]
*/
protected static array $referenceSelections = [];
/**
* The entity type service.
*
* @var \Drupal\eca\Service\ContentEntityTypes
*/
protected ContentEntityTypes $entityTypes;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$plugin->entityTypes = $container->get('eca.service.content_entity_types');
return $plugin;
}
/**
* {@inheritdoc}
*/
public static function definitions(): array {
return [
'bundlecreate' => [
'label' => 'Initialize content entity bundle',
'description' => 'An entity bundle object is being created (instantiated) on runtime, without being saved.',
'event_name' => ContentEntityEvents::BUNDLECREATE,
'event_class' => ContentEntityBundleCreate::class,
'tags' => Tag::CONFIG | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'bundledelete' => [
'label' => 'Delete content entity bundle',
'event_name' => ContentEntityEvents::BUNDLEDELETE,
'event_class' => ContentEntityBundleDelete::class,
'tags' => Tag::CONFIG | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'create' => [
'label' => 'Initialize content entity',
'description' => 'An entity object is being created (instantiated) on runtime, without being saved.',
'event_name' => ContentEntityEvents::CREATE,
'event_class' => ContentEntityCreate::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::RUNTIME | Tag::AFTER,
],
'revisioncreate' => [
'label' => 'Initialize content entity revision',
'description' => 'An entity revision object is being created (instantiated) on runtime, without being saved.',
'event_name' => ContentEntityEvents::REVISIONCREATE,
'event_class' => ContentEntityRevisionCreate::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::RUNTIME | Tag::AFTER,
],
'preload' => [
'label' => 'Preload content entity',
'event_name' => ContentEntityEvents::PRELOAD,
'event_class' => ContentEntityPreLoad::class,
'tags' => Tag::READ | Tag::BEFORE,
],
'load' => [
'label' => 'Load content entity',
'event_name' => ContentEntityEvents::LOAD,
'event_class' => ContentEntityLoad::class,
'tags' => Tag::CONTENT | Tag::READ | Tag::AFTER,
],
'storageload' => [
'label' => 'Load content entity from storage',
'event_name' => ContentEntityEvents::STORAGELOAD,
'event_class' => ContentEntityStorageLoad::class,
'tags' => Tag::CONTENT | Tag::READ | Tag::PERSISTENT | Tag::AFTER,
],
'presave' => [
'label' => 'Presave content entity',
'description' => 'Before a new or existing entity gets saved (persistently created or changed).',
'event_name' => ContentEntityEvents::PRESAVE,
'event_class' => ContentEntityPreSave::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::BEFORE,
],
'insert' => [
'label' => 'Insert content entity',
'description' => 'After a new entity got saved (persistently created).',
'event_name' => ContentEntityEvents::INSERT,
'event_class' => ContentEntityInsert::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'update' => [
'label' => 'Update content entity',
'description' => 'After an existing entity got saved (persistently changed).',
'event_name' => ContentEntityEvents::UPDATE,
'event_class' => ContentEntityUpdate::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'translationcreate' => [
'label' => 'Initialize content entity translation',
'description' => 'An entity translation object is being created (instantiated) on runtime, without being saved.',
'event_name' => ContentEntityEvents::TRANSLATIONCREATE,
'event_class' => ContentEntityTranslationCreate::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::RUNTIME | Tag::AFTER,
],
'translationinsert' => [
'label' => 'Insert content entity translation',
'description' => 'After a new entity translation got saved (persistently created).',
'event_name' => ContentEntityEvents::TRANSLATIONINSERT,
'event_class' => ContentEntityTranslationInsert::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'translationdelete' => [
'label' => 'Delete content entity translation',
'event_name' => ContentEntityEvents::TRANSLATIONDELETE,
'event_class' => ContentEntityTranslationDelete::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'predelete' => [
'label' => 'Predelete content entity',
'event_name' => ContentEntityEvents::PREDELETE,
'event_class' => ContentEntityPreDelete::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::BEFORE,
],
'delete' => [
'label' => 'Delete content entity',
'event_name' => ContentEntityEvents::DELETE,
'event_class' => ContentEntityDelete::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'revisiondelete' => [
'label' => 'Delete content entity revision',
'event_name' => ContentEntityEvents::REVISIONDELETE,
'event_class' => ContentEntityRevisionDelete::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::PERSISTENT | Tag::AFTER,
],
'view' => [
'label' => 'View content entity',
'event_name' => ContentEntityEvents::VIEW,
'event_class' => ContentEntityView::class,
'tags' => Tag::CONTENT | Tag::RUNTIME | Tag::VIEW | Tag::BEFORE,
],
'viewmodealter' => [
'label' => 'Alter entity view mode',
'event_name' => ContentEntityEvents::VIEWMODEALTER,
'event_class' => ContentEntityViewModeAlter::class,
'tags' => Tag::CONTENT | Tag::RUNTIME | Tag::VIEW | Tag::BEFORE,
'version_introduced' => '2.0.0',
],
'prepareview' => [
'label' => 'Prepare content entity view',
'event_name' => ContentEntityEvents::PREPAREVIEW,
'event_class' => ContentEntityPrepareView::class,
'tags' => Tag::CONTENT | Tag::RUNTIME | Tag::VIEW | Tag::BEFORE,
],
'prepareform' => [
'label' => 'Prepare content entity form',
'event_name' => ContentEntityEvents::PREPAREFORM,
'event_class' => ContentEntityPrepareForm::class,
'tags' => Tag::CONTENT | Tag::RUNTIME | Tag::VIEW | Tag::BEFORE,
],
'validate' => [
'label' => 'Validate content entity',
'description' => 'When an entity is undergoing validation.',
'event_name' => ContentEntityEvents::VALIDATE,
'event_class' => ContentEntityValidate::class,
'tags' => Tag::RUNTIME | Tag::CONTENT,
'version_introduced' => '2.1.0',
],
'fieldvaluesinit' => [
'label' => 'Init content entity field values',
'event_name' => ContentEntityEvents::FIELDVALUESINIT,
'event_class' => ContentEntityFieldValuesInit::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::RUNTIME | Tag::AFTER,
],
'custom' => [
'label' => 'ECA custom event (entity-aware)',
'event_name' => ContentEntityEvents::CUSTOM,
'event_class' => ContentEntityCustomEvent::class,
'tags' => Tag::CONTENT | Tag::WRITE | Tag::RUNTIME | Tag::AFTER,
],
'reference_selection' => [
'label' => 'Entity reference field selection',
'event_name' => ContentEntityEvents::REFERENCE_SELECTION,
'event_class' => ReferenceSelection::class,
'tags' => Tag::RUNTIME | Tag::CONTENT,
],
'options_selection' => [
'label' => 'Options field selection',
'event_name' => ContentEntityEvents::OPTIONS_SELECTION,
'event_class' => OptionsSelection::class,
'tags' => Tag::RUNTIME | Tag::CONTENT,
],
];
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
if ($this->eventClass() === ContentEntityCustomEvent::class) {
$values = [
'event_id' => '',
];
}
else {
$values = [
'type' => ContentEntityTypes::ALL,
];
if (is_subclass_of($this->eventClass(), FieldSelectionBase::class)) {
$values['field_name'] = '';
$values['token_name'] = '';
}
}
return $values + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
if ($this->eventClass() === ContentEntityCustomEvent::class) {
$form['event_id'] = [
'#type' => 'textfield',
'#title' => $this->t('Event ID'),
'#default_value' => $this->configuration['event_id'],
'#description' => $this->t('The id of the custom event (entity aware). Leave empty to trigger all entity aware events.'),
];
}
else {
$form['type'] = [
'#type' => 'select',
'#title' => $this->t('Type (and bundle)'),
'#options' => $this->entityTypes->getTypesAndBundles(TRUE),
'#default_value' => $this->configuration['type'],
];
if (is_subclass_of($this->eventClass(), FieldSelectionBase::class)) {
$form['field_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Restrict by field (machine name)'),
'#default_value' => $this->configuration['field_name'],
'#description' => $this->t('The machine name of the field to restrict the event trigger.'),
];
$form['token_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Token name holding the selection'),
'#default_value' => $this->configuration['token_name'],
'#description' => $this->t('The name of the token to hold the selection.'),
'#eca_token_reference' => TRUE,
];
}
}
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void {
parent::submitConfigurationForm($form, $form_state);
if ($this->eventClass() === ContentEntityCustomEvent::class) {
$this->configuration['event_id'] = $form_state->getValue('event_id');
}
else {
$this->configuration['type'] = $form_state->getValue('type');
if (is_subclass_of($this->eventClass(), FieldSelectionBase::class)) {
$this->configuration['field_name'] = $form_state->getValue('field_name');
$this->configuration['token_name'] = $form_state->getValue('token_name');
}
}
}
/**
* {@inheritdoc}
*/
public function generateWildcard(string $eca_config_id, EcaEventObject $ecaEvent): string {
/** @var \Drupal\eca\Plugin\ECA\Event\EventBase $plugin */
$plugin = $ecaEvent->getPlugin();
switch ($plugin->getDerivativeId()) {
case 'custom':
$configuration = $ecaEvent->getConfiguration();
return isset($configuration['event_id']) ? trim($configuration['event_id']) : '';
case 'preload':
$type = $ecaEvent->getConfiguration()['type'] ?? ContentEntityTypes::ALL;
if ($type === ContentEntityTypes::ALL) {
return '*';
}
[$entityType] = explode(' ', $type);
return $entityType;
case 'reference_selection':
case 'options_selection':
$config = $ecaEvent->getConfiguration();
$type = $config['type'] ?? ContentEntityTypes::ALL;
if ($type === ContentEntityTypes::ALL) {
$wildcard = '*::*';
}
else {
[$entityType, $bundle] = array_merge(explode(' ', $type), [ContentEntityTypes::ALL]);
if ($bundle === ContentEntityTypes::ALL) {
$wildcard = $entityType . '::*';
}
else {
$wildcard = $entityType . '::' . $bundle;
}
}
if (empty($config['field_name'])) {
$wildcard .= '::*';
}
else {
$wildcard .= '::' . $config['field_name'];
}
return $wildcard;
default:
$type = $ecaEvent->getConfiguration()['type'] ?? ContentEntityTypes::ALL;
if ($type === ContentEntityTypes::ALL) {
return '*';
}
[$entityType, $bundle] = array_merge(explode(' ', $type), [ContentEntityTypes::ALL]);
if ($bundle === ContentEntityTypes::ALL) {
return $entityType;
}
return $entityType . '::' . $bundle;
}
}
/**
* {@inheritdoc}
*/
public static function appliesForWildcard(Event $event, string $event_name, string $wildcard): bool {
if ($event instanceof ContentEntityBaseBundle) {
return in_array($wildcard, [
'*',
$event->getEntityTypeId(),
$event->getEntityTypeId() . '::' . $event->getBundle(),
], TRUE);
}
if ($event instanceof ContentEntityCustomEvent) {
return ($event->getEventId() === $wildcard) || ($wildcard === '');
}
if ($event instanceof ContentEntityBaseContentEntity) {
$entity = $event->getEntity();
return in_array($wildcard, [
'*',
$entity->getEntityTypeId(),
$entity->getEntityTypeId() . '::' . $entity->bundle(),
], TRUE);
}
if ($event instanceof ContentEntityPreLoad) {
return in_array($wildcard, ['*', $event->getEntityTypeId()], TRUE);
}
if ($event instanceof OptionsSelection) {
if (!$event->hasEntity()) {
// Can't do anything without an entity and without a specified field.
return FALSE;
}
$entity = $event->getEntity();
$field_name = $event->fieldStorageDefinition->getName();
$candidates = ['*::*::*'];
$candidates[] = '*::*::' . trim($field_name);
$candidates[] = $entity->getEntityTypeId() . '::*::*';
$candidates[] = $entity->getEntityTypeId() . '::' . $entity->bundle() . '::*';
$candidates[] = $entity->getEntityTypeId() . '::*::' . trim($field_name);
$candidates[] = $entity->getEntityTypeId() . '::' . $entity->bundle() . '::' . trim($field_name);
if (in_array($wildcard, $candidates, TRUE)) {
self::$optionsSelections[] = $event;
return TRUE;
}
return FALSE;
}
if ($event instanceof ReferenceSelection) {
$config = $event->selection->getConfiguration();
/** @var \Drupal\Core\Entity\ContentEntityInterface|null $entity */
$entity = $config['entity'] ?? NULL;
$field_name = $config['field_name'] ?? NULL;
if (!$entity || !$field_name) {
// Can't do anything without an entity and without a specified field.
return FALSE;
}
$candidates = ['*::*::*'];
$candidates[] = '*::*::' . trim($field_name);
$candidates[] = $entity->getEntityTypeId() . '::*::*';
$candidates[] = $entity->getEntityTypeId() . '::' . $entity->bundle() . '::*';
$candidates[] = $entity->getEntityTypeId() . '::*::' . trim($field_name);
$candidates[] = $entity->getEntityTypeId() . '::' . $entity->bundle() . '::' . trim($field_name);
if (in_array($wildcard, $candidates, TRUE)) {
self::$referenceSelections[] = $event;
return TRUE;
}
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function cleanupAfterSuccessors(): void {
switch ($this->getDerivativeId()) {
case 'reference_selection':
if (!($event = array_pop(self::$referenceSelections))) {
return;
}
if (!($token_name = $this->configuration['token_name'] ?? NULL)) {
return;
}
$entities = $this->tokenService->hasTokenData($token_name) ?
$this->tokenService->getTokenData($token_name) : [];
if ($entities instanceof EntityInterface) {
$entities = [$entities];
}
elseif (!is_iterable($entities)) {
$entities = [];
}
$event->selection->referenceableEntities = [];
$config = $event->selection->getConfiguration();
$target_type = $config['target_type'];
foreach ($entities as $entity) {
while ($entity instanceof TypedDataInterface) {
$entity = $entity->getValue();
}
if (is_scalar($entity)) {
$entity = $this->entityTypeManager->getStorage($target_type)
->load($entity);
}
if (!($entity instanceof EntityInterface) || ($target_type !== $entity->getEntityTypeId()) || in_array($entity, $event->selection->referenceableEntities, TRUE)) {
continue;
}
$event->selection->referenceableEntities[] = $entity;
}
return;
case 'options_selection':
if (!($event = array_pop(self::$optionsSelections))) {
return;
}
if (!($token_name = $this->configuration['token_name'] ?? NULL)) {
return;
}
$values = $this->tokenService->hasTokenData($token_name) ?
$this->tokenService->getTokenData($token_name) : [];
if (!is_iterable($values)) {
$values = [];
}
$event->allowedValues = [];
foreach ($values as $k => $v) {
if ($v instanceof TypedDataInterface) {
$v = $v->getString();
}
if (is_object($v) && !($v instanceof TranslatableMarkup) && method_exists($v, '__toString')) {
$v = (string) $v;
}
if (is_scalar($v)) {
$event->allowedValues[$k] = (string) $v;
}
elseif ($v instanceof TranslatableMarkup) {
$event->allowedValues[$k] = $v;
}
elseif (is_null($v)) {
$event->allowedValues[$k] = $k;
}
}
return;
}
}
/**
* {@inheritdoc}
*/
#[Token(
name: 'event',
description: 'The event.',
classes: [
ContentEntityPreLoad::class,
],
properties: [
new Token(name: 'entity_type_id', description: 'The entity type.'),
new Token(name: 'ids', description: 'The IDs of the entities.'),
],
)]
#[Token(
name: 'event',
description: 'The event.',
classes: [
ContentEntityViewModeAlter::class,
],
properties: [
new Token(name: 'view_mode', description: 'The view mode machine name.'),
],
)]
#[Token(
name: 'event',
description: 'The event.',
classes: [
ContentEntityBaseBundle::class,
],
properties: [
new Token(name: 'entity_type_id', description: 'The entity type.'),
new Token(name: 'bundle', description: 'The bundle machine name.'),
],
)]
protected function buildEventData(): array {
$event = $this->event;
$data = [];
if ($event instanceof ContentEntityPreLoad) {
$data += [
'entity_type_id' => $event->getEntityTypeId(),
'ids' => $event->getIds(),
];
}
elseif ($event instanceof ContentEntityViewModeAlter) {
$data += [
'view_mode' => $event->getViewMode(),
];
}
elseif ($event instanceof ContentEntityBaseBundle) {
$data += [
'entity_type_id' => $event->getEntityTypeId(),
'bundle' => $event->getBundle(),
];
}
$data += parent::buildEventData();
return $data;
}
/**
* {@inheritdoc}
*/
#[Token(
name: 'entity_view_mode',
description: 'The entity view mode.',
classes: [
ContentEntityPrepareView::class,
ContentEntityView::class,
],
)]
public function getData(string $key): mixed {
$event = $this->event;
if ($key === 'entity_view_mode' && ($event instanceof ContentEntityPrepareView || $event instanceof ContentEntityView || $event instanceof ContentEntityViewModeAlter)) {
return $event->getViewMode();
}
return parent::getData($key);
}
}
