diff --git a/ctools.services.yml b/ctools.services.yml index 046813b..eaa38b8 100644 --- a/ctools.services.yml +++ b/ctools.services.yml @@ -31,3 +31,8 @@ services: ctools.context_mapper: class: Drupal\ctools\ContextMapper arguments: ['@entity.repository'] + ctools.serializable.tempstore.factory: + class: Drupal\ctools\SerializableTempstoreFactory + arguments: ['@keyvalue.expirable', '@lock', '@request_stack', '%user.tempstore.expire%'] + tags: + - { name: backend_overridable } diff --git a/modules/ctools_inline_block/ctools_inline_block.info.yml b/modules/ctools_inline_block/ctools_inline_block.info.yml new file mode 100644 index 0000000..5765535 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.info.yml @@ -0,0 +1,9 @@ +name: 'Inline Blocks' +type: module +description: 'Exposes the custom block functionality of Drupal core as inline blocks what do not save to the database.' +package: 'Chaos tool suite' +version: 3.x +core: 8.x +dependencies: + - inline_entity_form + - block_content diff --git a/modules/ctools_inline_block/ctools_inline_block.install b/modules/ctools_inline_block/ctools_inline_block.install new file mode 100644 index 0000000..81c9110 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.install @@ -0,0 +1,30 @@ + [ + 'fields' => [ + 'uuid' => [ + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'description' => 'UUID of the inline block.', + ], + 'loader' => [ + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'description' => 'ID of the service which can load the block.', + ], + 'data' => [ + 'type' => 'text', + 'description' => 'Additional data needed to load the block.', + ], + ], + 'primary key' => ['uuid'], + ], + ]; +} diff --git a/modules/ctools_inline_block/ctools_inline_block.module b/modules/ctools_inline_block/ctools_inline_block.module new file mode 100644 index 0000000..141b9c2 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.module @@ -0,0 +1,39 @@ +getPlugin(); + + if ($plugin instanceof InlineBlock) { + \Drupal::database() + ->insert('inline_block') + ->fields([ + 'uuid' => $plugin->getEntity()->uuid(), + 'loader' => 'ctools.block_loader', + 'data' => $block->id(), + ]) + ->execute(); + } +} + +/** + * Implements hook_ENTITY_TYPE_delete(). + */ +function ctools_inline_block_block_delete(BlockInterface $block) { + \Drupal::database() + ->delete('inline_block') + ->condition('loader', 'ctools.block_loader') + ->condition('data', $block->id()) + ->execute(); + + $plugin = $block->getPlugin(); + + if ($plugin instanceof InlineBlock) { + $plugin->getEntity()->delete(); + } +} diff --git a/modules/ctools_inline_block/ctools_inline_block.services.yml b/modules/ctools_inline_block/ctools_inline_block.services.yml new file mode 100644 index 0000000..3eed444 --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.services.yml @@ -0,0 +1,5 @@ +services: + ctools.block_loader: + class: '\Drupal\ctools_inline_block\BlockLoader' + arguments: + - '@entity_type.manager' diff --git a/modules/ctools_inline_block/src/BlockLoader.php b/modules/ctools_inline_block/src/BlockLoader.php new file mode 100644 index 0000000..40d413b --- /dev/null +++ b/modules/ctools_inline_block/src/BlockLoader.php @@ -0,0 +1,39 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function load($uuid, $data = NULL) { + // $data is expected to be the ID of the block config entity. + if ($data) { + return $this->entityTypeManager->getStorage('block')->load($data)->getPlugin()->getEntity(); + } + else { + throw new \InvalidArgumentException('No block entity ID was specified'); + } + } + +} diff --git a/modules/ctools_inline_block/src/BlockLoaderInterface.php b/modules/ctools_inline_block/src/BlockLoaderInterface.php new file mode 100644 index 0000000..526c064 --- /dev/null +++ b/modules/ctools_inline_block/src/BlockLoaderInterface.php @@ -0,0 +1,23 @@ +uuid(); + } + + /** + * {@inheritdoc} + */ + public function save() { + return $this->entityTypeManager() + ->getStorage('inline_block_content') + ->save($this); + } + + /** + * {@inheritdoc} + */ + public function delete() { + parent::delete(); + + $this->entityTypeManager() + ->getStorage('inline_block_content') + ->resetCache((array) $this->id()); + } + +} diff --git a/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php b/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php new file mode 100644 index 0000000..fd45f8b --- /dev/null +++ b/modules/ctools_inline_block/src/Entity/InlineBlockStorage.php @@ -0,0 +1,138 @@ +getDefinition('block_content'), + $database, + $entity_manager, + $cache, + $language_manager + ); + + $this->entityClass = $entity_type->getClass(); + } + + /** + * {@inheritdoc} + */ + protected function cleanIds(array $ids) { + return array_map('strval', $ids); + } + + /** + * {@inheritdoc} + */ + protected function getFromStorage(array $ids = NULL) { + $id_key = $this->idKey; + $this->idKey = 'uuid'; + $entities = parent::getFromStorage($ids); + $this->idKey = $id_key; + return $entities; + } + + /** + * {@inheritdoc} + */ + protected function buildQuery($ids, $revision_id = FALSE) { + return $this->database + ->select('inline_block', 'ib') + ->fields('ib') + ->condition('uuid', $ids, 'IN'); + } + + /** + * {@inheritdoc} + */ + protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) { + $mapped = []; + foreach ($records as $uuid => $record) { + $mapped[$uuid] = \Drupal::service($record->loader)->load($uuid, $record->data); + } + return $mapped; + } + + /** + * {@inheritdoc} + */ + public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeCreate(EntityTypeInterface $entity_type) { + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + $this->entityType = $entity_type; + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + } + + /** + * {@inheritdoc} + */ + protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { + } + + /** + * {@inheritdoc} + */ + protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) { + } + + /** + * {@inheritdoc} + */ + protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = array()) { + } + +} diff --git a/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php b/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php new file mode 100644 index 0000000..9662b98 --- /dev/null +++ b/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php @@ -0,0 +1,76 @@ +entity) && isset($this->configuration['entity'])) { + $this->entity = unserialize($this->configuration['entity']); + } + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $options = $this->entityManager->getViewModeOptionsByBundle('block_content', $this->getDerivativeId()); + $form['view_mode'] = array( + '#type' => 'select', + '#options' => $options, + '#title' => $this->t('View mode'), + '#description' => $this->t('Output the block in this view mode.'), + '#default_value' => $this->configuration['view_mode'], + '#access' => (count($options) > 1), + ); + $form['title']['#description'] = $this->t('The title of the block as shown to the user.'); + $form['entity'] = [ + '#type' => 'inline_entity_form', + '#entity_type' => 'inline_block_content', + '#bundle' => $this->getDerivativeId(), + // If the #default_value is NULL, a new entity will be created. + '#default_value' => $this->getEntity(), + ]; + $form['#process'][] = [static::class, 'enableInlineEntityForm']; + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['view_mode'] = $form_state->getValue('view_mode'); + /** @var \Drupal\block_content\BlockContentInterface $entity */ + $entity = $form['settings']['entity']['#entity'] ?: $form['entity']['#entity']; + $this->configuration['label'] = $entity->label(); + $this->configuration['langcode'] = $form_state->getValue('langcode'); + + $this->entity = $entity; + $this->configuration['entity'] = serialize($entity); + } + +} diff --git a/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php b/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php new file mode 100644 index 0000000..ba73f8d --- /dev/null +++ b/modules/ctools_inline_block/src/Plugin/Derivative/InlineBlock.php @@ -0,0 +1,68 @@ +blockContentTypeStorage = $block_content_type_storage; + $this->translation = $translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + $entity_manager = $container->get('entity.manager'); + return new static( + $entity_manager->getStorage('block_content_type'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $block_types = $this->blockContentTypeStorage->loadMultiple(); + // Reset the discovered definitions. + $this->derivatives = []; + /** @var $type \Drupal\block_content\Entity\BlockContent */ + foreach ($block_types as $type) { + $this->derivatives[$type->id()] = $base_plugin_definition; + $this->derivatives[$type->id()]['admin_label'] = $this->translation->translate('Inline @type', ['@type' => $type->label()]); + $this->derivatives[$type->id()]['config_dependencies']['content'] = array( + $type->getConfigDependencyName() + ); + } + return $this->derivatives; + } + +} diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml new file mode 100644 index 0000000..02982e4 --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/block_content.type.basic.yml @@ -0,0 +1,5 @@ +id: basic +label: 'Basic block' +revision: 0 +description: 'A basic block contains a title and a body.' +langcode: en diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml new file mode 100644 index 0000000..e6accfd --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/core.entity_view_display.block_content.basic.default.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.basic + - field.field.block_content.basic.body + module: + - text +id: block_content.basic.default +targetEntityType: block_content +bundle: basic +mode: default +content: + body: + type: text_default + weight: 0 + label: hidden + settings: { } + third_party_settings: { } +hidden: { } diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml new file mode 100644 index 0000000..89118ef --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/config/install/field.field.block_content.basic.body.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.basic + - field.storage.block_content.body + module: + - text +id: block_content.basic.body +field_name: body +entity_type: block_content +bundle: basic +label: Body +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + display_summary: false +field_type: text_with_summary diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml new file mode 100644 index 0000000..deb4ad3 --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/ctools_inline_block_test.info.yml @@ -0,0 +1,8 @@ +type: module +name: CTools Inline Block Test +description: 'Required for CTools Inline Block simpletests only.' +core: 8.x +dependencies: + - ctools_inline_block + - block_content + - text diff --git a/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/src/Plugin/Block/BlockContentBlock.php b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/src/Plugin/Block/BlockContentBlock.php new file mode 100644 index 0000000..5f1dd50 --- /dev/null +++ b/modules/ctools_inline_block/tests/modules/ctools_inline_block_test/src/Plugin/Block/BlockContentBlock.php @@ -0,0 +1,37 @@ +getDerivativeId(); + if (!isset($this->blockContent)) { + $this->blockContent = $this->entityManager->loadEntityByUuid('inline_block_content', $uuid); + } + return $this->blockContent; + } + +} diff --git a/modules/ctools_inline_block/tests/src/Functional/RenderTest.php b/modules/ctools_inline_block/tests/src/Functional/RenderTest.php new file mode 100644 index 0000000..755269c --- /dev/null +++ b/modules/ctools_inline_block/tests/src/Functional/RenderTest.php @@ -0,0 +1,53 @@ +assertSession(); + + $block_content = BlockContent::create([ + 'type' => 'basic', + 'info' => $this->randomMachineName(16), + 'body' => 'Yeah, I like animals better than people sometimes... Especially dogs.', + ]); + $block_content->save(); + + $block = $this->placeBlock('block_content:' . $block_content->uuid(), [ + 'region' => 'content', + ]); + $selector = '#block-' . $block->id(); + + $this->drupalGet('/user/login'); + + $concrete_output = $assert->elementExists('css', $selector)->getOuterHtml(); + + $block->set('plugin', 'panelizer_' . $block->getPluginId())->save(); + $this->getSession()->reload(); + $shadow_output = $assert->elementExists('css', $selector)->getOuterHtml(); + + $this->assertSame($concrete_output, $shadow_output); + } + +} diff --git a/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php b/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php new file mode 100644 index 0000000..33d0f3f --- /dev/null +++ b/modules/ctools_inline_block/tests/src/Kernel/StorageTest.php @@ -0,0 +1,223 @@ +installEntitySchema('block_content'); + + // Create a custom block type. + BlockContentType::create([ + 'id' => 'test_type', + 'label' => $this->randomMachineName(), + ])->save(); + + // Create a text field on the custom block type. + $storage = FieldStorageConfig::create([ + 'entity_type' => 'block_content', + 'type' => 'text', + 'field_name' => 'body', + ]); + $storage->save(); + + FieldConfig::create([ + 'bundle' => 'test_type', + 'field_storage' => $storage, + ])->save(); + } + + /** + * Tests that database tables are not created for inline blocks. + */ + public function testNoShadowTables() { + $this->assertFalse(\Drupal::database()->schema()->tableExists('inline_block_content')); + } + + /** + * Tests that inline blocks can transparently load concrete blocks. + */ + public function testLoad() { + $original = CoreBlockContent::create([ + 'type' => 'test_type', + 'info' => $this->randomMachineName(), + 'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.', + ]); + $original->save(); + + $id = $original->uuid(); + + // Test static loading. + $this->assertShadow($original, InlineBlockContent::load($id)); + + // Test container-ey loading. + $shadow = \Drupal::entityTypeManager() + ->getStorage('inline_block_content') + ->load($id); + $this->assertShadow($original, $shadow); + + // Test static multiple loading. + $shadows = InlineBlockContent::loadMultiple([$id]); + $this->assertShadow($original, reset($shadows)); + + // Test container-ey multiple loading. + $shadows = \Drupal::entityTypeManager() + ->getStorage('inline_block_content') + ->loadMultiple([$id]); + $this->assertShadow($original, reset($shadows)); + + // Test loading a shadow block using the original block's UUID. + $shadow = \Drupal::service('entity.repository') + ->loadEntityByUuid('panelizer_block_content', $original->uuid()); + $this->assertShadow($original, $shadow); + } + + /** + * Tests that shadow blocks can be created like normal custom blocks. + */ + public function testCreateShadow() { + $values = [ + 'type' => 'test_type', + 'info' => $this->randomMachineName(), + ]; + + // Test static creation. + $block = InlineBlockContent::create($values); + $this->assertInstanceOf(InlineBlockContent::class, $block); + $this->assertSame('block_content', $block->getEntityTypeId()); + + // Test container-ey creation. + $block = \Drupal::entityTypeManager() + ->getStorage('inline_block_content') + ->create($values); + $this->assertInstanceOf(InlineBlockContent::class, $block); + $this->assertSame('block_content', $block->getEntityTypeId()); + } + + /** + * @depends testCreateShadow + */ + public function testNullSave() { + $block = InlineBlockContent::create([ + 'type' => 'test_type', + 'info' => $this->randomMachineName(), + 'body' => 'Creste di galli spaghettoni trennette pennette trennette penne zita linguettine orecchiette pappardelle gramigna casarecce.', + ]); + $block->save(); + // Inline blocks use their UUID as their ID. + $this->assertEquals($block->id(), $block->uuid()); + + // Ensure that no custom blocks were actually created. + $count = \Drupal::entityQuery('block_content') + ->count() + ->execute(); + $this->assertNumeric($count); + $this->assertEmpty($count); + + // Nothing should be written to the field tables. + $count = \Drupal::database() + ->select('block_content__body') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertNumeric($count); + $this->assertEmpty($count); + + $count = \Drupal::database() + ->select('block_content_revision__body') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertNumeric($count); + $this->assertEmpty($count); + + // No shadow blocks should be written into the database. + $count = \Drupal::entityQuery('inline_block_content') + ->count() + ->execute(); + $this->assertNumeric($count); + $this->assertEmpty($count); + } + + /** + * @depends testLoadBlockContentByShadow + */ + public function testDeleteBlockContentByShadow() { + $original = CoreBlockContent::create([ + 'type' => 'test_type', + 'info' => $this->randomMachineName(), + 'body' => 'Pasta ipsum dolor sit amet tripoline farfalle croxetti mezzelune spirali manicotti ditalini spaghetti calamarata ricciolini ciriole spirali cencioni.', + ]); + $original->save(); + + $id = $original->id(); + InlineBlockContent::load($id)->delete(); + + $this->assertNull(CoreBlockContent::load($id)); + $this->assertNull(InlineBlockContent::load($id)); + } + + /** + * Asserts that a value is numeric. + * + * @param mixed $value + * The value to check. + */ + protected function assertNumeric($value) { + $this->assertTrue(is_numeric($value)); + } + + /** + * Asserts that a shadow block and standard custom block are identical. + * + * @param \Drupal\block_content\BlockContentInterface $original + * The original standard custom block. + * @param \Drupal\block_content\BlockContentInterface $shadow + * The shadow block. + */ + protected function assertShadow(BlockContentInterface $original, BlockContentInterface $shadow) { + $this->assertInstanceOf(InlineBlockContent::class, $shadow); + // Shadow blocks should masquerade as normal custom blocks. + $this->assertSame('block_content', $shadow->getEntityTypeId()); + $this->assertSame($original->isNew(), $shadow->isNew()); + $this->assertSame($original->id(), $shadow->id()); + $this->assertSame($original->label(), $shadow->label()); + $this->assertSame($original->uuid(), $shadow->uuid()); + + // Shadow blocks should have all custom block fields and load their values. + $this->assertTrue($shadow->hasField('body')); + $this->assertSame($original->body->value, $shadow->body->value); + } + +} diff --git a/src/SerializableTempstore.php b/src/SerializableTempstore.php new file mode 100644 index 0000000..3eed41c --- /dev/null +++ b/src/SerializableTempstore.php @@ -0,0 +1,10 @@ +id() ?: session_id(); + } + + // Store the data for this collection in the database. + $storage = $this->storageFactory->get("user.shared_tempstore.$collection"); + return new SerializableTempstore($storage, $this->lockBackend, $owner, $this->requestStack, $this->expire); + } + +}