Commit cd9b0f4f authored by catch's avatar catch

Issue #1426804 by alexpott, swentel: Fixed Allow field storages to be...

Issue #1426804 by alexpott, swentel: Fixed Allow field storages to be persisted when they have no fields.
parent 121bbc14
......@@ -29,7 +29,6 @@ class CommentValidationTest extends EntityUnitTestBase {
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('comment');
$this->installSchema('comment', array('comment_entity_statistics'));
}
......
......@@ -37,6 +37,7 @@ protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(array('field'));
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
......@@ -85,12 +86,12 @@ public function testRecreateEntity() {
$content_type->save();
$this->configImporter->reset();
// A node type, a field storage, a field, an entity view display and an
// entity form display will be recreated.
// A node type, a field, an entity view display and an entity form display
// will be recreated.
$creates = $this->configImporter->getUnprocessedConfiguration('create');
$deletes = $this->configImporter->getUnprocessedConfiguration('delete');
$this->assertEqual(5, count($creates), 'There are 5 configuration items to create.');
$this->assertEqual(5, count($deletes), 'There are 5 configuration items to delete.');
$this->assertEqual(4, count($creates), 'There are 4 configuration items to create.');
$this->assertEqual(4, count($deletes), 'There are 4 configuration items to delete.');
$this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('update')), 'There are no configuration items to update.');
$this->assertIdentical($creates, array_reverse($deletes), 'Deletes and creates contain the same configuration names in opposite orders due to dependencies.');
......
......@@ -44,6 +44,7 @@ protected function setUp() {
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installConfig(array('field'));
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
......
......@@ -25,7 +25,6 @@ class EditorFileUsageTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('file');
$this->installSchema('node', array('node_access'));
$this->installSchema('file', array('file_usage'));
......
......@@ -47,6 +47,9 @@ field.storage.*.*:
sequence:
- type: ignore
label: 'Index'
persist_with_no_fields:
type: boolean
label: 'Persist field storage with no fields'
field.field.*.*.*:
type: field_config_base
......
......@@ -215,11 +215,12 @@ public static function postDelete(EntityStorageInterface $storage, array $fields
return;
}
// Delete field storages that have no more fields.
// Delete the associated field storages if they are not used anymore and are
// not persistent.
$storages_to_delete = array();
foreach ($fields as $field) {
$storage_definition = $field->getFieldStorageDefinition();
if (!$field->deleted && empty($field->noFieldDelete) && !$field->isUninstalling() && count($storage_definition->getBundles()) == 0) {
if (!$field->deleted && empty($field->noFieldDelete) && !$field->isUninstalling() && $storage_definition->isDeletable()) {
// Key by field UUID to avoid deleting the same storage twice.
$storages_to_delete[$storage_definition->uuid()] = $storage_definition;
}
......
......@@ -133,6 +133,21 @@ class FieldStorageConfig extends ConfigEntityBase implements FieldStorageConfigI
*/
public $locked = FALSE;
/**
* Flag indicating whether the field storage should be deleted when orphaned.
*
* By default field storages for configurable fields are removed when there
* are no remaining fields using them. If multiple modules provide bundles
* which need to use the same field storage then setting this to TRUE will
* preserve the field storage regardless of what happens to the bundles. The
* classic use case for this is node body field storage since Book, Forum, the
* Standard profile and bundle (node type) creation through the UI all use
* same field storage.
*
* @var bool
*/
protected $persist_with_no_fields = FALSE;
/**
* The custom storage indexes for the field data storage.
*
......@@ -733,4 +748,13 @@ public static function loadByName($entity_type_id, $field_name) {
return \Drupal::entityManager()->getStorage('field_storage_config')->load($entity_type_id . '.' . $field_name);
}
/**
* {@inheritdoc}
*/
public function isDeletable() {
// The field storage is not deleted, is configured to be removed when there
// are no fields and the field storage has no bundles.
return !$this->deleted && !$this->persist_with_no_fields && count($this->getBundles()) == 0;
}
}
......@@ -31,4 +31,12 @@ public function getBundles();
*/
public function isLocked();
/**
* Checks if the field storage can be deleted.
*
* @return bool
* TRUE if the field storage can be deleted.
*/
public function isDeletable();
}
......@@ -16,3 +16,4 @@ dependencies:
module:
- entity_test
- text
persist_with_no_fields: false
......@@ -16,3 +16,4 @@ dependencies:
module:
- entity_test
- text
persist_with_no_fields: false
......@@ -17,3 +17,4 @@ dependencies:
module:
- entity_test
- text
persist_with_no_fields: false
......@@ -17,3 +17,4 @@ dependencies:
module:
- entity_test
- text
persist_with_no_fields: false
......@@ -16,6 +16,7 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field_ui\OverviewBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\field\Entity\FieldStorageConfig;
......@@ -218,15 +219,14 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
);
}
// Additional row: re-use existing field.
$existing_fields = $this->getExistingFieldOptions();
// Additional row: re-use existing field storages.
$existing_fields = $this->getExistingFieldStorageOptions();
if ($existing_fields) {
// Build list of options.
$existing_field_options = array();
foreach ($existing_fields as $field_name => $info) {
$text = $this->t('@type: @field (@label)', array(
$text = $this->t('@type: @field', array(
'@type' => $info['type_label'],
'@label' => $info['label'],
'@field' => $info['field'],
));
$existing_field_options[$field_name] = Unicode::truncate($text, 80, FALSE, TRUE);
......@@ -485,45 +485,31 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
}
/**
* Returns an array of existing fields to be added to a bundle.
* Returns an array of existing field storages that can be added to a bundle.
*
* @return array
* An array of existing fields keyed by field name.
* An array of existing field storages keyed by name.
*/
protected function getExistingFieldOptions() {
protected function getExistingFieldStorageOptions() {
$options = array();
// Collect candidate fields: all fields of field storages for this
// entity type that are not already present in the current bundle.
$field_map = \Drupal::entityManager()->getFieldMap();
$field_ids = array();
if (!empty($field_map[$this->entity_type])) {
foreach ($field_map[$this->entity_type] as $field_name => $data) {
if (!in_array($this->bundle, $data['bundles'])) {
$bundle = reset($data['bundles']);
$field_ids[] = $this->entity_type . '.' . $bundle . '.' . $field_name;
}
}
}
// Load the fields and build the list of options.
if ($field_ids) {
$field_types = $this->fieldTypeManager->getDefinitions();
$fields = $this->entityManager->getStorage('field_config')->loadMultiple($field_ids);
foreach ($fields as $field) {
// Do not show:
// - locked fields,
// - fields that should not be added via user interface.
$field_type = $field->getType();
$field_storage = $field->getFieldStorageDefinition();
if (empty($field_storage->locked) && empty($field_types[$field_type]['no_ui'])) {
$options[$field->getName()] = array(
'type' => $field_type,
'type_label' => $field_types[$field_type]['label'],
'field' => $field->getName(),
'label' => $field->getLabel(),
);
}
// Load the field_storages and build the list of options.
$field_types = $this->fieldTypeManager->getDefinitions();
foreach ($this->entityManager->getFieldStorageDefinitions($this->entity_type) as $field_name => $field_storage) {
// Do not show:
// - non-configurable field storages,
// - locked field_storages,
// - field_storages that should not be added via user interface,
// - field_storages that already have a field in the bundle.
$field_type = $field_storage->getType();
if ($field_storage instanceof FieldStorageConfigInterface
&& !$field_storage->isLocked()
&& empty($field_types[$field_type]['no_ui'])
&& !in_array($this->bundle, $field_storage->getBundles(), TRUE)) {
$options[$field_name] = array(
'type' => $field_type,
'type_label' => $field_types[$field_type]['label'],
'field' => $field_name,
);
}
}
......
......@@ -21,6 +21,7 @@ class EntityDisplayTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(array('field'));
}
......@@ -268,8 +269,6 @@ public function testBaseFieldComponent() {
* Tests renaming and deleting a bundle.
*/
public function testRenameDeleteBundle() {
$this->installEntitySchema('node');
// Create a node bundle, display and form display object.
entity_create('node_type', array('type' => 'article'))->save();
entity_get_display('node', 'article', 'default')->save();
......
......@@ -21,7 +21,6 @@ class EntityFormDisplayTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->installConfig(array('field'));
}
/**
......
......@@ -72,6 +72,7 @@ function testCRUDFields() {
$this->cardinalitySettings();
$this->fieldListAdminPage();
$this->deleteField();
$this->addPersistentFieldStorage();
}
/**
......@@ -225,6 +226,30 @@ protected function deleteField() {
$this->assertResponse(200);
}
/**
* Tests that persistent field storage appears in the field UI.
*/
protected function addPersistentFieldStorage() {
$field_storage = FieldStorageConfig::loadByName('node', $this->field_name);
// Persist the field storage even if there are no fields.
$field_storage->set('persist_with_no_fields', TRUE)->save();
// Delete all instances of the field.
foreach ($field_storage->getBundles() as $node_type) {
// Delete all the body field instances.
$this->drupalPostForm('admin/structure/types/manage/' . $node_type . '/fields/node.' . $node_type . '.' . $this->field_name, array(), t('Delete field'));
$this->drupalPostForm(NULL, array(), t('Delete'));
}
// Check "Re-use existing field" appears.
$this->drupalGet('admin/structure/types/manage/page/fields');
$this->assertRaw(t('Re-use existing field'), '"Re-use existing field" was found.');
// Add a new field for the orphaned storage.
$edit = array(
'fields[_add_existing_field][label]' => $this->randomMachineName(),
'fields[_add_existing_field][field_name]' => $this->field_name,
);
$this->fieldUIAddExistingField("admin/structure/types/manage/page", $edit);
}
/**
* Asserts field settings are as expected.
*
......
......@@ -16,3 +16,4 @@ dependencies:
module:
- options
- taxonomy
persist_with_no_fields: false
......@@ -30,7 +30,6 @@ protected function setUp() {
\Drupal::service('router.builder')->rebuild();
$this->installSchema('system', array('sequences'));
$this->installSchema('comment', array('comment_entity_statistics'));
$this->installEntitySchema('node');
$this->installEntitySchema('comment');
$this->installEntitySchema('taxonomy_term');
}
......
......@@ -64,6 +64,25 @@ protected function setUp() {
$this->installSchema('system', array('url_alias', 'router'));
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
// If the concrete test sub-class installs node.module, ensure that the node
// entity schema is created before the field configurations are installed,
// because the node entity tables need to be created before the body field
// storage tables. This prevents trying to create the body field tables
// twice.
$class = get_class($this);
while ($class) {
if (property_exists($class, 'modules')) {
// Only check the modules, if the $modules property was not inherited.
$rp = new \ReflectionProperty($class, 'modules');
if ($rp->class == $class) {
if (in_array('node', $class::$modules, TRUE)) {
$this->installEntitySchema('node');
break;
}
}
}
$class = get_parent_class($class);
}
$this->installConfig(array('field', 'language'));
// Add German as a language.
......
langcode: en
status: true
dependencies:
module:
- node
- text
id: node.body
field_name: body
entity_type: node
type: text_with_summary
settings: { }
module: text
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: true
......@@ -328,14 +328,6 @@ function node_add_body_field(NodeTypeInterface $type, $label = 'Body') {
// Add or remove the body field, as needed.
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$field = FieldConfig::loadByName('node', $type->id(), 'body');
if (empty($field_storage)) {
$field_storage = entity_create('field_storage_config', array(
'field_name' => 'body',
'entity_type' => 'node',
'type' => 'text_with_summary',
));
$field_storage->save();
}
if (empty($field)) {
$field = entity_create('field_config', array(
'field_storage' => $field_storage,
......
......@@ -20,7 +20,6 @@ class NodeConditionTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
// Create the node bundles required for testing.
$type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
......
<?php
/**
* @file
* Contains \Drupal\node\Tests\NodeBodyFieldStorageTest.
*/
namespace Drupal\node\Tests;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\simpletest\KernelTestBase;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
/**
* Tests node body field storage.
*
* @group node
*/
class NodeBodyFieldStorageTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('user', 'system', 'field', 'node', 'text', 'filter', 'entity_reference');
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'sequences');
// Necessary for module uninstall.
$this->installSchema('user', 'users_data');
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installConfig(array('field'));
}
/**
* Tests node body field storage persistence even if there are no instances.
*/
public function testFieldOverrides() {
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$this->assertTrue($field_storage, 'Node body field storage exists.');
NodeType::create(['name' => 'Ponies', 'type' => 'ponies'])->save();
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$this->assertTrue(count($field_storage->getBundles()) == 1, 'Node body field storage is being used on the new node type.');
$field = FieldConfig::loadByName('node', 'ponies', 'body');
$field->delete();
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$this->assertTrue(count($field_storage->getBundles()) == 0, 'Node body field storage exists after deleting the only instance of a field.');
\Drupal::moduleHandler()->uninstall(array('node'));
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$this->assertFalse($field_storage, 'Node body field storage does not exist after uninstalling the Node module.');
}
}
......@@ -38,7 +38,6 @@ class NodeFieldOverridesTest extends EntityUnitTestBase {
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(array('user'));
$this->user = $this->createUser();
\Drupal::service('current_user')->setAccount($this->user);
......
......@@ -30,7 +30,6 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase {
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(array('filter'));
$node_type = entity_create('node_type', array('type' => 'article', 'name' => 'Article'));
......
......@@ -28,7 +28,6 @@ class NodeValidationTest extends EntityUnitTestBase {
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
// Create a node type for testing.
$type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
......
......@@ -22,3 +22,4 @@ locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
......@@ -44,7 +44,6 @@ class EntityCrudHookTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('comment');
$this->installSchema('user', array('users_data'));
......
......@@ -50,8 +50,6 @@ class EntityFieldTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('entity_test_rev');
$this->installEntitySchema('entity_test_mul');
$this->installEntitySchema('entity_test_mulrev');
......
......@@ -47,6 +47,26 @@ protected function setUp() {
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
// If the concrete test sub-class installs node.module, ensure that the node
// entity schema is created before the field configurations are installed,
// because the node entity tables need to be created before the body field
// storage tables. This prevents trying to create the body field tables
// twice.
$class = get_class($this);
while ($class) {
if (property_exists($class, 'modules')) {
// Only check the modules, if the $modules property was not inherited.
$rp = new \ReflectionProperty($class, 'modules');
if ($rp->class == $class) {
if (in_array('node', $class::$modules, TRUE)) {
$this->installEntitySchema('node');
break;
}
}
}
$class = get_parent_class($class);
}
$this->installConfig(array('field'));
}
......
......@@ -32,6 +32,16 @@ class ViewEntityDependenciesTest extends ViewUnitTestBase {
*/
public static $modules = ['node', 'comment', 'user', 'field', 'text', 'entity_reference'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Install the necessary dependencies for node type creation to work.
$this->installEntitySchema('node');
$this->installConfig(array('field'));
}
/**
* Tests the calculateDependencies method.
*/
......
......@@ -23,3 +23,4 @@ dependencies:
module:
- node
- image
persist_with_no_fields: false
......@@ -20,3 +20,4 @@ dependencies:
module:
- node
- taxonomy
persist_with_no_fields: false
......@@ -23,3 +23,4 @@ dependencies:
module:
- image
- user
persist_with_no_fields: false
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment