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..ad1914a --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.info.yml @@ -0,0 +1,10 @@ +name: 'Chaos tools Inline Block' +type: module +description: 'Provides the custom block functionality of Drupal core as inline blocks what will not save to the normal entity tables.' +package: 'Chaos tool suite' +version: 3.x +core: 8.x +dependencies: + - block + - inline_entity_form + - block_content 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..e8f464e --- /dev/null +++ b/modules/ctools_inline_block/ctools_inline_block.module @@ -0,0 +1,17 @@ +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/Storage/ContentEntityNullStorage.php b/modules/ctools_inline_block/src/Entity/Storage/ContentEntityNullStorage.php new file mode 100644 index 0000000..da6888c --- /dev/null +++ b/modules/ctools_inline_block/src/Entity/Storage/ContentEntityNullStorage.php @@ -0,0 +1,101 @@ +get('shadow'); + + $real_entity_type = $shadow + ? $entity_manager->getDefinition($shadow) + : $entity_type; + + parent::__construct($real_entity_type, $database, $entity_manager, $cache, $language_manager); + + if ($shadow) { + $this->entityClass = $entity_type->getClass(); + } + } + + /** + * {@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..39ffc3e --- /dev/null +++ b/modules/ctools_inline_block/src/Plugin/Block/InlineBlock.php @@ -0,0 +1,89 @@ +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; + } + + /** + * Process callback -- sets up IEF on the block configuration form. + */ + public static function enableInlineEntityForm(array $form_element, FormStateInterface $form_state) { +// $complete_form = &$form_state->getCompleteForm(); +// if (empty($complete_form['#submit'])) { +// $complete_form['#submit'] = []; +// } + ElementSubmit::attach($complete_form, $form_state); + return $form_element; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['view_mode'] = $form_state->getValue('view_mode'); + /** @var \Drupal\block_content\BlockContentInterface $entity */ + $entity = empty($form['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/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); + } + +} diff --git a/tests/src/Functional/ShadowBlockRenderTest.php b/tests/src/Functional/ShadowBlockRenderTest.php new file mode 100644 index 0000000..4d6ebfd --- /dev/null +++ b/tests/src/Functional/ShadowBlockRenderTest.php @@ -0,0 +1,55 @@ +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/tests/src/Kernel/BlockContentTest.php b/tests/src/Kernel/BlockContentTest.php new file mode 100644 index 0000000..c45f705 --- /dev/null +++ b/tests/src/Kernel/BlockContentTest.php @@ -0,0 +1,230 @@ +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 the shadow blocks. + */ + public function testNoShadowTables() { + $schema = \Drupal::database()->schema(); + $this->assertFalse($schema->tableExists('panelizer_block_content')); + } + + /** + * Tests that shadow blocks can transparently load real blocks. + */ + public function testLoadBlockContentByShadow() { + $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(); + + // Test static loading. + $this->assertShadow($original, ShadowBlockContent::load($id)); + + // Test container-ey loading. + $shadow = \Drupal::entityTypeManager() + ->getStorage('panelizer_block_content') + ->load($id); + $this->assertShadow($original, $shadow); + + // Test static multiple loading. + $shadows = ShadowBlockContent::loadMultiple([$id]); + $this->assertShadow($original, reset($shadows)); + + // Test container-ey multiple loading. + $shadows = \Drupal::entityTypeManager() + ->getStorage('panelizer_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 = ShadowBlockContent::create($values); + $this->assertInstanceOf(ShadowBlockContent::class, $block); + $this->assertSame('block_content', $block->getEntityTypeId()); + + // Test container-ey creation. + $block = \Drupal::entityTypeManager() + ->getStorage('panelizer_block_content') + ->create($values); + $this->assertInstanceOf(ShadowBlockContent::class, $block); + $this->assertSame('block_content', $block->getEntityTypeId()); + } + + /** + * @depends testCreateShadow + */ + public function testNullSave() { + $block = ShadowBlockContent::create([ + 'type' => 'test_type', + 'info' => $this->randomMachineName(), + 'body' => 'Creste di galli spaghettoni trennette pennette trennette penne zita linguettine orecchiette pappardelle gramigna casarecce.', + ]); + $block->save(); + // Shadow blocks don't write anything to the database, so there should be + // no ID. They are always considered new (and therefore cannot be deleted). + $this->assertEmpty($block->id()); + $this->assertTrue($block->isNew()); + // They should, however, have a UUID. + $this->assertNotEmpty($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('panelizer_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(); + ShadowBlockContent::load($id)->delete(); + + $this->assertNull(CoreBlockContent::load($id)); + $this->assertNull(ShadowBlockContent::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(ShadowBlockContent::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); + } + +}