Loading core/lib/Drupal/Core/DefaultContent/Importer.php +18 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ use Drupal\user\EntityOwnerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * A service for handling import of content. Loading Loading @@ -45,6 +46,7 @@ public function __construct( private readonly FileSystemInterface $fileSystem, private readonly LanguageManagerInterface $languageManager, private readonly EntityRepositoryInterface $entityRepository, private readonly EventDispatcherInterface $eventDispatcher, ) {} /** Loading @@ -70,6 +72,9 @@ public function importContent(Finder $content, Existing $existing = Existing::Er return; } $event = new PreImportEvent($content, $existing); $skip = $this->eventDispatcher->dispatch($event)->getSkipList(); $account = $this->accountSwitcher->switchToAdministrator(); try { Loading @@ -80,6 +85,19 @@ public function importContent(Finder $content, Existing $existing = Existing::Er assert(is_string($entity_type_id)); assert(is_string($path)); // The event subscribers asked to skip importing this entity. If they // explained why, log that. if (array_key_exists($uuid, $skip)) { if ($skip[$uuid]) { $this->logger?->info('Skipped importing @entity_type @uuid because: %reason', [ '@entity_type' => $entity_type_id, '@uuid' => $uuid, '%reason' => $skip[$uuid], ]); } continue; } $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) { Loading core/lib/Drupal/Core/DefaultContent/PreImportEvent.php 0 → 100644 +72 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\DefaultContent; use Symfony\Contracts\EventDispatcher\Event; /** * Event dispatched before default content is imported. * * Subscribers to this event should avoid modifying content, because it is * probably about to change again. This event is best used for tasks like * notifications, logging, or updating a value in state. It can also be used * to skip importing certain entities, identified by their UUID. */ final class PreImportEvent extends Event { /** * Entity UUIDs that should not be imported. * * @var string[] */ private array $skip = []; /** * Constructs a PreImportEvent object. * * @param \Drupal\Core\DefaultContent\Finder $finder * The content finder, which has information on the entities to create * in the necessary dependency order. * @param \Drupal\Core\DefaultContent\Existing $existing * What the importer will do when importing an entity that already exists. */ public function __construct( public readonly Finder $finder, public readonly Existing $existing, ) {} /** * Adds an entity UUID to the skip list. * * @param string $uuid * The UUID of an entity that should not be imported. * @param string|\Stringable|null $reason * (optional) A reason why the entity is being skipped. Defaults to NULL. * * @throws \InvalidArgumentException * If the given UUID is not one of the ones being imported. */ public function skip(string $uuid, string|\Stringable|null $reason = NULL): void { if (array_key_exists($uuid, $this->finder->data)) { $this->skip[$uuid] = $reason; } else { throw new \InvalidArgumentException("Content entity '$uuid' cannot be skipped, because it is not one of the entities being imported."); } } /** * Returns the list of entity UUIDs that should not be imported. * * @return string|\Stringable|null[] * An array whose keys are the UUIDs of the entities that should not be * imported, and the values are either a short explanation of why that * entity was skipped, or NULL if no explanation was given. */ public function getSkipList(): array { return $this->skip; } } core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php +45 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\block_content\BlockContentInterface; use Drupal\block_content\Entity\BlockContentType; use Drupal\Component\Serialization\Yaml; use Drupal\Core\DefaultContent\PreImportEvent; use Drupal\Core\DefaultContent\Existing; use Drupal\Core\DefaultContent\Finder; use Drupal\Core\DefaultContent\Importer; Loading @@ -33,6 +34,7 @@ use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait; use Psr\Log\LogLevel; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @covers \Drupal\Core\DefaultContent\Importer Loading Loading @@ -277,7 +279,50 @@ private function assertContentWasImported(): void { $this->assertInstanceOf(Section::class, $section); $this->assertCount(2, $section->getComponents()); $this->assertSame('system_powered_by_block', $section->getComponent('03b45f14-cf74-469a-8398-edf3383ce7fa')->getPluginId()); } /** * Tests that the pre-import event allows skipping certain entities. */ public function testPreImportEvent(): void { $invalid_uuid_detected = FALSE; $listener = function (PreImportEvent $event) use (&$invalid_uuid_detected): void { $event->skip('3434bd5a-d2cd-4f26-bf79-a7f6b951a21b', 'Decided not to!'); try { $event->skip('not-a-thing'); } catch (\InvalidArgumentException) { $invalid_uuid_detected = TRUE; } }; \Drupal::service(EventDispatcherInterface::class) ->addListener(PreImportEvent::class, $listener); $finder = new Finder($this->contentDir); $this->assertSame('menu_link_content', $finder->data['3434bd5a-d2cd-4f26-bf79-a7f6b951a21b']['_meta']['entity_type']); /** @var \Drupal\Core\DefaultContent\Importer $importer */ $importer = \Drupal::service(Importer::class); $logger = new TestLogger(); $importer->setLogger($logger); $importer->importContent($finder, Existing::Error); // The entity we skipped should not be here, and the reason why should have // been logged. $menu_link = \Drupal::service(EntityRepositoryInterface::class) ->loadEntityByUuid('menu_link_content', '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b'); $this->assertNull($menu_link); $this->assertTrue($logger->hasInfo([ 'message' => 'Skipped importing @entity_type @uuid because: %reason', 'context' => [ '@entity_type' => 'menu_link_content', '@uuid' => '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b', '%reason' => 'Decided not to!', ], ])); // We should have caught an exception for trying to skip an invalid UUID. $this->assertTrue($invalid_uuid_detected); } } Loading
core/lib/Drupal/Core/DefaultContent/Importer.php +18 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ use Drupal\user\EntityOwnerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * A service for handling import of content. Loading Loading @@ -45,6 +46,7 @@ public function __construct( private readonly FileSystemInterface $fileSystem, private readonly LanguageManagerInterface $languageManager, private readonly EntityRepositoryInterface $entityRepository, private readonly EventDispatcherInterface $eventDispatcher, ) {} /** Loading @@ -70,6 +72,9 @@ public function importContent(Finder $content, Existing $existing = Existing::Er return; } $event = new PreImportEvent($content, $existing); $skip = $this->eventDispatcher->dispatch($event)->getSkipList(); $account = $this->accountSwitcher->switchToAdministrator(); try { Loading @@ -80,6 +85,19 @@ public function importContent(Finder $content, Existing $existing = Existing::Er assert(is_string($entity_type_id)); assert(is_string($path)); // The event subscribers asked to skip importing this entity. If they // explained why, log that. if (array_key_exists($uuid, $skip)) { if ($skip[$uuid]) { $this->logger?->info('Skipped importing @entity_type @uuid because: %reason', [ '@entity_type' => $entity_type_id, '@uuid' => $uuid, '%reason' => $skip[$uuid], ]); } continue; } $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) { Loading
core/lib/Drupal/Core/DefaultContent/PreImportEvent.php 0 → 100644 +72 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\DefaultContent; use Symfony\Contracts\EventDispatcher\Event; /** * Event dispatched before default content is imported. * * Subscribers to this event should avoid modifying content, because it is * probably about to change again. This event is best used for tasks like * notifications, logging, or updating a value in state. It can also be used * to skip importing certain entities, identified by their UUID. */ final class PreImportEvent extends Event { /** * Entity UUIDs that should not be imported. * * @var string[] */ private array $skip = []; /** * Constructs a PreImportEvent object. * * @param \Drupal\Core\DefaultContent\Finder $finder * The content finder, which has information on the entities to create * in the necessary dependency order. * @param \Drupal\Core\DefaultContent\Existing $existing * What the importer will do when importing an entity that already exists. */ public function __construct( public readonly Finder $finder, public readonly Existing $existing, ) {} /** * Adds an entity UUID to the skip list. * * @param string $uuid * The UUID of an entity that should not be imported. * @param string|\Stringable|null $reason * (optional) A reason why the entity is being skipped. Defaults to NULL. * * @throws \InvalidArgumentException * If the given UUID is not one of the ones being imported. */ public function skip(string $uuid, string|\Stringable|null $reason = NULL): void { if (array_key_exists($uuid, $this->finder->data)) { $this->skip[$uuid] = $reason; } else { throw new \InvalidArgumentException("Content entity '$uuid' cannot be skipped, because it is not one of the entities being imported."); } } /** * Returns the list of entity UUIDs that should not be imported. * * @return string|\Stringable|null[] * An array whose keys are the UUIDs of the entities that should not be * imported, and the values are either a short explanation of why that * entity was skipped, or NULL if no explanation was given. */ public function getSkipList(): array { return $this->skip; } }
core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php +45 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\block_content\BlockContentInterface; use Drupal\block_content\Entity\BlockContentType; use Drupal\Component\Serialization\Yaml; use Drupal\Core\DefaultContent\PreImportEvent; use Drupal\Core\DefaultContent\Existing; use Drupal\Core\DefaultContent\Finder; use Drupal\Core\DefaultContent\Importer; Loading @@ -33,6 +34,7 @@ use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait; use Psr\Log\LogLevel; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @covers \Drupal\Core\DefaultContent\Importer Loading Loading @@ -277,7 +279,50 @@ private function assertContentWasImported(): void { $this->assertInstanceOf(Section::class, $section); $this->assertCount(2, $section->getComponents()); $this->assertSame('system_powered_by_block', $section->getComponent('03b45f14-cf74-469a-8398-edf3383ce7fa')->getPluginId()); } /** * Tests that the pre-import event allows skipping certain entities. */ public function testPreImportEvent(): void { $invalid_uuid_detected = FALSE; $listener = function (PreImportEvent $event) use (&$invalid_uuid_detected): void { $event->skip('3434bd5a-d2cd-4f26-bf79-a7f6b951a21b', 'Decided not to!'); try { $event->skip('not-a-thing'); } catch (\InvalidArgumentException) { $invalid_uuid_detected = TRUE; } }; \Drupal::service(EventDispatcherInterface::class) ->addListener(PreImportEvent::class, $listener); $finder = new Finder($this->contentDir); $this->assertSame('menu_link_content', $finder->data['3434bd5a-d2cd-4f26-bf79-a7f6b951a21b']['_meta']['entity_type']); /** @var \Drupal\Core\DefaultContent\Importer $importer */ $importer = \Drupal::service(Importer::class); $logger = new TestLogger(); $importer->setLogger($logger); $importer->importContent($finder, Existing::Error); // The entity we skipped should not be here, and the reason why should have // been logged. $menu_link = \Drupal::service(EntityRepositoryInterface::class) ->loadEntityByUuid('menu_link_content', '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b'); $this->assertNull($menu_link); $this->assertTrue($logger->hasInfo([ 'message' => 'Skipped importing @entity_type @uuid because: %reason', 'context' => [ '@entity_type' => 'menu_link_content', '@uuid' => '3434bd5a-d2cd-4f26-bf79-a7f6b951a21b', '%reason' => 'Decided not to!', ], ])); // We should have caught an exception for trying to skip an invalid UUID. $this->assertTrue($invalid_uuid_detected); } }