diff --git a/core/core.services.yml b/core/core.services.yml index 7d83bb12f8..08e056c736 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -620,6 +620,11 @@ services: arguments: ['@entity_type.manager'] tags: - { name: event_subscriber } + entity_events_subscriber: + class: Drupal\Core\EventSubscriber\EntityEventsSubscriber + arguments: ['@module_handler'] + tags: + - { name: event_subscriber } entity.definition_update_manager: class: Drupal\Core\Entity\EntityDefinitionUpdateManager arguments: ['@entity_type.manager', '@entity.last_installed_schema.repository', '@entity_field.manager', '@entity_type.listener', '@field_storage_definition.listener'] diff --git a/core/lib/Drupal/Core/Entity/EntityEvent.php b/core/lib/Drupal/Core/Entity/EntityEvent.php new file mode 100644 index 0000000000..9c52f07ec1 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityEvent.php @@ -0,0 +1,42 @@ +entity = $entity; + } + + /** + * Returns the entity wrapped by this event. + * + * @return \Drupal\Core\Entity\EntityInterface + * The entity object. + */ + public function getEntity() { + return $this->entity; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityEvents.php b/core/lib/Drupal/Core/Entity/EntityEvents.php new file mode 100644 index 0000000000..0686d52115 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityEvents.php @@ -0,0 +1,183 @@ + self::CREATE, + 'presave' => self::PRESAVE, + 'insert' => self::INSERT, + 'update' => self::UPDATE, + 'predelete' => self::PREDELETE, + 'delete' => self::DELETE, + ]; + + /** + * Name of the event fired when creating an entity. + * + * This hook runs after a new entity object has just been instantiated. + * + * @see hook_entity_create() + * + * @Event + * + * @var string + */ + const CREATE = 'entity.create'; + + /** + * Name of the event fired before an entity is created or updated. + * + * You can get the original entity object from $entity->original when it is an + * update of the entity. + * + * @see hook_entity_presave() + * + * @Event + * + * @var string + */ + const PRESAVE = 'entity.presave'; + + /** + * Name of the event fired after creating an entity. + * + * This event fires once the entity has been stored. Note that changes to the + * entity made by subscribers to the event will not be saved. + * + * @see hook_entity_insert() + * + * @Event + * + * @var string + */ + const INSERT = 'entity.insert'; + + /** + * Name of the event fired after updating an existing entity. + * + * This event fires once the entity has been stored. Note that changes to the + * entity made by subscribers to the event will not be saved. Get the original + * entity object from $entity->original. + * + * @see hook_entity_update() + * + * @Event + * + * @var string + */ + const UPDATE = 'entity.update'; + + /** + * Name of the event fired before deleting an entity. + * + * @see hook_entity_predelete() + * + * @Event + * + * @var string + */ + const PREDELETE = 'entity.predelete'; + + /** + * Name of the event fired after deleting an entity. + * + * @see hook_entity_delete() + * + * @Event + * + * @var string + */ + const DELETE = 'entity.delete'; + + /** + * Returns the event name for creation of an entity of a specific type. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return string + * The event name. + */ + public static function create($entity_type_id) { + return self::CREATE . '.' . $entity_type_id; + } + + /** + * Returns the event name when pre-saving an entity of a specific type. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return string + * The event name. + */ + public static function presave($entity_type_id) { + return self::PRESAVE . '.' . $entity_type_id; + } + + /** + * Returns the event name for inserting an entity of a specific type. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return string + * The event name. + */ + public static function insert($entity_type_id) { + return self::INSERT . '.' . $entity_type_id; + } + + /** + * Returns the event name for updating an entity of a specific type. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return string + * The event name. + */ + public static function update($entity_type_id) { + return self::UPDATE . '.' . $entity_type_id; + } + + /** + * Returns the event name just before deleting an entity of a specific type. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return string + * The event name. + */ + public static function predelete($entity_type_id) { + return self::PREDELETE . '.' . $entity_type_id; + } + + /** + * Returns the event name for deletion of an entity of a specific type. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return string + * The event name. + */ + public static function delete($entity_type_id) { + return self::DELETE . '.' . $entity_type_id; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index 0182b75cf8..e1f289389f 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -247,8 +247,23 @@ protected function setStaticCache(array $entities) { protected function invokeHook($hook, EntityInterface $entity) { // Invoke the hook. $this->moduleHandler()->invokeAll($this->entityTypeId . '_' . $hook, [$entity]); - // Invoke the respective entity-level hook. - $this->moduleHandler()->invokeAll('entity_' . $hook, [$entity]); + + // Dispatch events for the invoked hook. + /** @var \Drupal\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */ + $event_dispatcher = \Drupal::service('event_dispatcher'); + $event = new EntityEvent($entity); + + // Dispatch an event for the entity type-specific hook (i.e., + // hook_ENTITY_TYPE_$hook). + if (method_exists(EntityEvents::class, $hook)) { + $event_name = call_user_func([EntityEvents::class, $hook], $this->entityTypeId); + $event_dispatcher->dispatch($event, $event_name); + } + + // Dispatch an event for the entity-level hook (i.e., hook_entity_$hook). + if (isset(EntityEvents::$hookToEventMap[$hook])) { + $event_dispatcher->dispatch($event, EntityEvents::$hookToEventMap[$hook]); + } } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/EntityEventsSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EntityEventsSubscriber.php new file mode 100644 index 0000000000..74d02ff9f1 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/EntityEventsSubscriber.php @@ -0,0 +1,64 @@ +moduleHandler = $module_handler; + } + + /** + * Provides hooks for entity specific events. + * + * @param \Drupal\Core\Entity\EntityEvent $event + * The entity event. + * @param string $event_name + * The related entity event name. + */ + public function onEntityEvent(EntityEvent $event, $event_name) { + $hook = array_search($event_name, EntityEvents::$hookToEventMap); + if ($hook) { + $this->moduleHandler->invokeAll('entity_' . $hook, [$event->getEntity()]); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Hooks should be executed before other subscribers for BC. + $priority = -1000; + $events[EntityEvents::CREATE][] = ['onEntityEvent', $priority]; + $events[EntityEvents::PRESAVE][] = ['onEntityEvent', $priority]; + $events[EntityEvents::INSERT][] = ['onEntityEvent', $priority]; + $events[EntityEvents::UPDATE][] = ['onEntityEvent', $priority]; + $events[EntityEvents::PREDELETE][] = ['onEntityEvent', $priority]; + $events[EntityEvents::DELETE][] = ['onEntityEvent', $priority]; + return $events; + } + +} diff --git a/core/modules/system/tests/modules/entity_test_event/entity_test_event.info.yml b/core/modules/system/tests/modules/entity_test_event/entity_test_event.info.yml new file mode 100644 index 0000000000..9a0bfb07bd --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_event/entity_test_event.info.yml @@ -0,0 +1,5 @@ +name: 'Entity test event' +type: module +description: 'Provides entity events.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/entity_test_event/entity_test_event.services.yml b/core/modules/system/tests/modules/entity_test_event/entity_test_event.services.yml new file mode 100644 index 0000000000..e20270cd9e --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_event/entity_test_event.services.yml @@ -0,0 +1,5 @@ +services: + entity_test_event.event_subscriber: + class: \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber + tags: + - { name: event_subscriber } diff --git a/core/modules/system/tests/modules/entity_test_event/src/EventSubscriber/TestEventSubscriber.php b/core/modules/system/tests/modules/entity_test_event/src/EventSubscriber/TestEventSubscriber.php new file mode 100644 index 0000000000..812c89c912 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_event/src/EventSubscriber/TestEventSubscriber.php @@ -0,0 +1,64 @@ +getEntity()->name->value === 'hei') { + $event->getEntity()->name->value .= ' ho'; + } + } + + /** + * Test EntityEvents::UPDATE event callback. + * + * @param \Drupal\Core\Entity\EntityEvent $event + * Insert event. + */ + public function onUpdate(EntityEvent $event) { + if ($event->getEntity()->name->value === 'hei') { + $event->getEntity()->name->value .= ' ho'; + } + } + + /** + * Test EntityEvents::DELETE event callback. + * + * @param \Drupal\Core\Entity\EntityEvent $event + * Insert event. + */ + public function onDelete(EntityEvent $event) { + if ($event->getEntity()->name->value === 'hei ho') { + EntityTest::create([ + 'name' => 'hei_ho', + ])->save(); + $event->getEntity()->name->value .= ' ho'; + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[EntityEvents::INSERT] = 'onInsert'; + $events[EntityEvents::UPDATE] = 'onUpdate'; + $events[EntityEvents::DELETE] = 'onDelete'; + return $events; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityEventsTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityEventsTest.php new file mode 100644 index 0000000000..a2f5f576c1 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityEventsTest.php @@ -0,0 +1,79 @@ +installEntitySchema('entity_test'); + } + + /** + * Test insert event. + * + * @see \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber + */ + public function testEventsInsert() { + $entity = EntityTest::create([ + 'name' => 'hei', + ]); + $entity->save(); + $this->assertEquals('hei ho', $entity->name->value); + } + + /** + * Test update event. + * + * @see \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber + */ + public function testEventsUpdate() { + $entity = EntityTest::create([ + 'name' => 'meh', + ]); + $entity->save(); + $this->assertEquals('meh', $entity->name->value); + + $entity->name->value = 'hei'; + $entity->save(); + $this->assertEquals('hei ho', $entity->name->value); + } + + /** + * Test delete event. + * + * @see \Drupal\entity_test_event\EventSubscriber\TestEventSubscriber + */ + public function testEventsDelete() { + $entities = \Drupal::entityTypeManager()->getStorage('entity_test') + ->loadByProperties(['name' => 'hei_ho']); + $this->assertCount(0, $entities); + + $entity = EntityTest::create([ + 'name' => 'hei', + ]); + $entity->save(); + $entity->delete(); + + // Note the delete event creates another entity. + $entities = \Drupal::entityTypeManager()->getStorage('entity_test') + ->loadByProperties(['name' => 'hei_ho']); + $this->assertCount(1, $entities); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php index 90b0f1c6ce..ef800d4c63 100644 --- a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php @@ -13,6 +13,7 @@ use Drupal\Core\Language\Language; use Drupal\Tests\UnitTestCase; use Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @coversDefaultClass \Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage @@ -133,6 +134,7 @@ protected function setUpKeyValueEntityStorage($uuid_key = 'uuid') { $this->languageManager->expects($this->any()) ->method('getCurrentLanguage') ->willReturn($language); + $event_dispatcher = $this->prophesize(EventDispatcherInterface::class); $this->entityStorage = new KeyValueEntityStorage($this->entityType, $this->keyValueStore, $this->uuidService, $this->languageManager, new MemoryCache()); $this->entityStorage->setModuleHandler($this->moduleHandler); @@ -140,6 +142,7 @@ protected function setUpKeyValueEntityStorage($uuid_key = 'uuid') { $container = new ContainerBuilder(); $container->set('entity_field.manager', $this->entityFieldManager); $container->set('entity_type.manager', $this->entityTypeManager); + $container->set('event_dispatcher', $event_dispatcher->reveal()); $container->set('language_manager', $this->languageManager); $container->set('cache_tags.invalidator', $this->cacheTagsInvalidator); \Drupal::setContainer($container);