Commit 03bd7fc7 authored by catch's avatar catch

Issue #2172843 by fgm, marthinal, pfrenssen, amateescu, andypost, Wim Leers,...

Issue #2172843 by fgm, marthinal, pfrenssen, amateescu, andypost, Wim Leers, stefan.r, pwieck, deepak_123: Remove ability to update entity bundle machine names
parent 10626d87
......@@ -7,8 +7,7 @@
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Config\ConfigNameException;
use Drupal\Core\Entity\EntityStorageInterface;
/**
......@@ -19,31 +18,6 @@
*/
abstract class ConfigEntityBundleBase extends ConfigEntityBase {
/**
* Renames displays when a bundle is renamed.
*/
protected function renameDisplays() {
// Rename entity displays.
if ($this->getOriginalId() !== $this->id()) {
foreach ($this->loadDisplays('entity_view_display') as $display) {
$new_id = $this->getEntityType()->getBundleOf() . '.' . $this->id() . '.' . $display->getMode();
$display->set('id', $new_id);
$display->setTargetBundle($this->id());
$display->save();
}
}
// Rename entity form displays.
if ($this->getOriginalId() !== $this->id()) {
foreach ($this->loadDisplays('entity_form_display') as $form_display) {
$new_id = $this->getEntityType()->getBundleOf() . '.' . $this->id() . '.' . $form_display->getMode();
$form_display->set('id', $new_id);
$form_display->setTargetBundle($this->id());
$form_display->save();
}
}
}
/**
* Deletes display if a bundle is deleted.
*/
......@@ -80,12 +54,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
}
// Entity bundle field definitions may depend on bundle settings.
$entity_manager->clearCachedFieldDefinitions();
if ($this->getOriginalId() != $this->id()) {
// If the entity was renamed, update the displays.
$this->renameDisplays();
$entity_manager->onBundleRename($this->getOriginalId(), $this->id(), $bundle_of);
}
}
}
......@@ -101,6 +69,33 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti
}
}
/**
* Acts on an entity before the presave hook is invoked.
*
* Used before the entity is saved and before invoking the presave hook.
*
* Ensure that config entities which are bundles of other entities cannot have
* their ID changed.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage object.
*
* @throws \Drupal\Core\Config\ConfigNameException
* Thrown when attempting to rename a bundle entity.
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Only handle renames, not creations.
if (!$this->isNew() && $this->getOriginalId() !== $this->id()) {
$bundle_type = $this->getEntityType();
$bundle_of = $bundle_type->getBundleOf();
if (!empty($bundle_of)) {
throw new ConfigNameException("The machine name of the '{$bundle_type->getLabel()}' bundle cannot be changed.");
}
}
}
/**
* Returns view or form displays for this bundle.
*
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\BundleEntityFormBase.
*/
namespace Drupal\Core\Entity;
/**
* Class BundleEntityFormBase is a base form for bundle config entities.
*/
class BundleEntityFormBase extends EntityForm {
/**
* Protects the bundle entity's ID property's form element against changes.
*
* This method is assumed to be called on a completely built entity form,
* including a form element for the bundle config entity's ID property.
*
* @param array $form
* The completely built entity bundle form array.
*
* @return array
* The updated entity bundle form array.
*/
protected function protectBundleIdElement(array $form) {
$entity = $this->getEntity();
$id_key = $entity->getEntityType()->getKey('id');
assert('isset($form[$id_key])');
$element = &$form[$id_key];
// Make sure the element is not accidentally re-enabled if it has already
// been disabled.
if (empty($element['#disabled'])) {
$element['#disabled'] = !$entity->isNew();
}
return $form;
}
}
......@@ -8,7 +8,7 @@
namespace Drupal\Core\Entity;
/**
* An interface for reacting to entity bundle creation, deletion, and renames.
* An interface for reacting to entity bundle creation and deletion.
*
* @todo Convert to Symfony events: https://www.drupal.org/node/2332935
*/
......@@ -24,20 +24,6 @@ interface EntityBundleListenerInterface {
*/
public function onBundleCreate($bundle, $entity_type_id);
/**
* Reacts to a bundle being renamed.
*
* This method runs before fields are updated with the new bundle name.
*
* @param string $bundle
* The name of the bundle being renamed.
* @param string $bundle_new
* The new name of the bundle.
* @param string $entity_type_id
* The entity type to which the bundle is bound; e.g. 'node' or 'user'.
*/
public function onBundleRename($bundle, $bundle_new, $entity_type_id);
/**
* Reacts to a bundle being deleted.
*
......
......@@ -1367,31 +1367,6 @@ public function onBundleCreate($bundle, $entity_type_id) {
$this->moduleHandler->invokeAll('entity_bundle_create', array($entity_type_id, $bundle));
}
/**
* {@inheritdoc}
*/
public function onBundleRename($bundle_old, $bundle_new, $entity_type_id) {
$this->clearCachedBundles();
// Notify the entity storage.
$storage = $this->getStorage($entity_type_id);
if ($storage instanceof EntityBundleListenerInterface) {
$storage->onBundleRename($bundle_old, $bundle_new, $entity_type_id);
}
// Rename existing base field bundle overrides.
$overrides = $this->getStorage('base_field_override')->loadByProperties(array('entity_type' => $entity_type_id, 'bundle' => $bundle_old));
foreach ($overrides as $override) {
$override->set('id', $entity_type_id . '.' . $bundle_new . '.' . $override->getName());
$override->set('bundle', $bundle_new);
$override->allowBundleRename();
$override->save();
}
// Invoke hook_entity_bundle_rename() hook.
$this->moduleHandler->invokeAll('entity_bundle_rename', array($entity_type_id, $bundle_old, $bundle_new));
$this->clearCachedFieldDefinitions();
}
/**
* {@inheritdoc}
*/
......
......@@ -1493,40 +1493,6 @@ public function onBundleCreate($bundle, $entity_type_id) { }
*/
public function onBundleDelete($bundle, $entity_type_id) { }
/**
* {@inheritdoc}
*/
public function onBundleRename($bundle, $bundle_new, $entity_type_id) {
// The method runs before the field definitions are updated, so we use the
// old bundle name.
$field_definitions = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
// We need to handle deleted fields too. For now, this only makes sense for
// configurable fields, so we use the specific API.
// @todo Use the unified store of deleted field definitions instead in
// https://www.drupal.org/node/2282119
$field_definitions += entity_load_multiple_by_properties('field_config', array('entity_type' => $this->entityTypeId, 'bundle' => $bundle, 'deleted' => TRUE, 'include_deleted' => TRUE));
$table_mapping = $this->getTableMapping();
foreach ($field_definitions as $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
$is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
$table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
$revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
$this->database->update($table_name)
->fields(array('bundle' => $bundle_new))
->condition('bundle', $bundle)
->execute();
if ($this->entityType->isRevisionable()) {
$this->database->update($revision_name)
->fields(array('bundle' => $bundle_new))
->condition('bundle', $bundle)
->execute();
}
}
}
}
/**
* {@inheritdoc}
*/
......
......@@ -139,8 +139,8 @@
* - Field configuration preSave(): hook_field_storage_config_update_forbid()
* - Node postSave(): hook_node_access_records() and
* hook_node_access_records_alter()
* - Config entities that are acting as entity bundles, in postSave():
* hook_entity_bundle_create() or hook_entity_bundle_rename() as appropriate
* - Config entities that are acting as entity bundles in postSave():
* hook_entity_bundle_create()
* - Comment: hook_comment_publish() and hook_comment_unpublish() as
* appropriate.
*
......@@ -350,6 +350,7 @@
* 'bundle_entity_type' on the \Drupal\node\Entity\Node class. Also, the
* bundle config entity type annotation must have a 'bundle_of' entry,
* giving the machine name of the entity type it is acting as a bundle for.
* These machine names are considered permanent, they may not be renamed.
* - Additional annotations can be seen on entity class examples such as
* \Drupal\node\Entity\Node (content) and \Drupal\user\Entity\Role
* (configuration). These annotations are documented on
......@@ -740,31 +741,6 @@ function hook_entity_bundle_create($entity_type_id, $bundle) {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Act on entity_bundle_rename().
*
* This hook is invoked after the operation has been performed.
*
* @param string $entity_type_id
* The entity type to which the bundle is bound.
* @param string $bundle_old
* The previous name of the bundle.
* @param string $bundle_new
* The new name of the bundle.
*
* @see entity_crud
*/
function hook_entity_bundle_rename($entity_type_id, $bundle_old, $bundle_new) {
// Update the settings associated with the bundle in my_module.settings.
$config = \Drupal::config('my_module.settings');
$bundle_settings = $config->get('bundle_settings');
if (isset($bundle_settings[$entity_type_id][$bundle_old])) {
$bundle_settings[$entity_type_id][$bundle_new] = $bundle_settings[$entity_type_id][$bundle_old];
unset($bundle_settings[$entity_type_id][$bundle_old]);
$config->set('bundle_settings', $bundle_settings);
}
}
/**
* Act on entity_bundle_delete().
*
......
......@@ -163,8 +163,7 @@ protected function getBaseFieldDefinition() {
* {@inheritdoc}
*
* @throws \Drupal\Core\Field\FieldException
* If the bundle is being changed and
* BaseFieldOverride::allowBundleRename() has not been called.
* If the bundle is being changed.
*/
public function preSave(EntityStorageInterface $storage) {
// Filter out unknown settings and make sure all settings are present, so
......@@ -189,7 +188,7 @@ public function preSave(EntityStorageInterface $storage) {
if ($this->entity_type != $this->original->entity_type) {
throw new FieldException("Cannot change the entity_type of an existing base field bundle override (entity type:{$this->entity_type}, bundle:{$this->original->bundle}, field name: {$this->field_name})");
}
if ($this->bundle != $this->original->bundle && empty($this->bundleRenameAllowed)) {
if ($this->bundle != $this->original->bundle) {
throw new FieldException("Cannot change the bundle of an existing base field bundle override (entity type:{$this->entity_type}, bundle:{$this->original->bundle}, field name: {$this->field_name})");
}
$previous_definition = $this->original;
......
......@@ -179,13 +179,6 @@ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigIn
*/
protected $itemDefinition;
/**
* Flag indicating whether the bundle name can be renamed or not.
*
* @var bool
*/
protected $bundleRenameAllowed = FALSE;
/**
* Array of constraint options keyed by constraint plugin ID.
*
......@@ -456,7 +449,7 @@ public function __sleep() {
// Only serialize necessary properties, excluding those that can be
// recalculated.
$properties = get_object_vars($this);
unset($properties['fieldStorage'], $properties['itemDefinition'], $properties['bundleRenameAllowed'], $properties['original']);
unset($properties['fieldStorage'], $properties['itemDefinition'], $properties['original']);
return array_keys($properties);
}
......@@ -528,13 +521,6 @@ public function getItemDefinition() {
return $this->itemDefinition;
}
/**
* {@inheritdoc}
*/
public function allowBundleRename() {
$this->bundleRenameAllowed = TRUE;
}
/**
* {@inheritdoc}
*/
......
......@@ -282,13 +282,4 @@ public function addConstraint($constraint_name, $options = NULL);
*/
public function setConstraints(array $constraints);
/**
* Allows a bundle to be renamed.
*
* Renaming a bundle on the instance is allowed when an entity's bundle
* is renamed and when field_entity_bundle_rename() does internal
* housekeeping.
*/
public function allowBundleRename();
}
......@@ -7,7 +7,7 @@
namespace Drupal\block_content;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\language\Entity\ContentLanguageSettings;
......@@ -15,7 +15,7 @@
/**
* Base form for category edit forms.
*/
class BlockContentTypeForm extends EntityForm {
class BlockContentTypeForm extends BundleEntityFormBase {
/**
* {@inheritdoc}
......@@ -48,7 +48,6 @@ public function form(array $form, FormStateInterface $form_state) {
'exists' => '\Drupal\block_content\Entity\BlockContentType::load',
),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#disabled' => !$block_type->isNew(),
);
$form['description'] = array(
......@@ -62,7 +61,7 @@ public function form(array $form, FormStateInterface $form_state) {
'#type' => 'checkbox',
'#title' => t('Create new revision'),
'#default_value' => $block_type->shouldCreateNewRevision(),
'#description' => t('Create a new revision by default for this block type.')
'#description' => t('Create a new revision by default for this block type.'),
);
if ($this->moduleHandler->moduleExists('language')) {
......@@ -91,7 +90,7 @@ public function form(array $form, FormStateInterface $form_state) {
'#value' => t('Save'),
);
return $form;
return $this->protectBundleIdElement($form);
}
/**
......
......@@ -497,99 +497,6 @@ function testBookDelete() {
$this->assertTrue(empty($node->book), 'Deleting childless top-level book node properly allowed.');
}
/*
* Tests node type changing machine name when type is a book allowed type.
*/
function testBookNodeTypeChange() {
$this->drupalLogin($this->adminUser);
// Change the name, machine name and description.
$edit = array(
'name' => 'Bar',
'type' => 'bar',
);
$this->drupalPostForm('admin/structure/types/manage/book', $edit, t('Save content type'));
// Ensure that the config book.settings:allowed_types has been updated with
// the new machine and the old one has been removed.
$this->assertTrue(book_type_is_allowed('bar'), 'Config book.settings:allowed_types contains the updated node type machine name "bar".');
$this->assertFalse(book_type_is_allowed('book'), 'Config book.settings:allowed_types does not contain the old node type machine name "book".');
$edit = array(
'name' => 'Basic page',
'title_label' => 'Title for basic page',
'type' => 'page',
);
$this->drupalPostForm('admin/structure/types/add', $edit, t('Save content type'));
// Add page to the allowed node types.
$edit = array(
'book_allowed_types[page]' => 'page',
'book_allowed_types[bar]' => 'bar',
);
$this->drupalPostForm('admin/structure/book/settings', $edit, t('Save configuration'));
$this->assertTrue(book_type_is_allowed('bar'), 'Config book.settings:allowed_types contains the bar node type.');
$this->assertTrue(book_type_is_allowed('page'), 'Config book.settings:allowed_types contains the page node type.');
// Test the order of the book.settings::allowed_types configuration is as
// expected. The point of this test is to prove that after changing a node
// type going to admin/structure/book/settings and pressing save without
// changing anything should not alter the book.settings configuration. The
// order will be:
// @code
// array(
// 'bar',
// 'page',
// );
// @endcode
$current_config = $this->config('book.settings')->get();
$this->drupalPostForm('admin/structure/book/settings', array(), t('Save configuration'));
$this->assertIdentical($current_config, $this->config('book.settings')->get());
// Change the name, machine name and description.
$edit = array(
'name' => 'Zebra book',
'type' => 'zebra',
);
$this->drupalPostForm('admin/structure/types/manage/bar', $edit, t('Save content type'));
$this->assertTrue(book_type_is_allowed('zebra'), 'Config book.settings:allowed_types contains the zebra node type.');
$this->assertTrue(book_type_is_allowed('page'), 'Config book.settings:allowed_types contains the page node type.');
// Test the order of the book.settings::allowed_types configuration is as
// expected. The order should be:
// @code
// array(
// 'page',
// 'zebra',
// );
// @endcode
$current_config = $this->config('book.settings')->get();
$this->drupalPostForm('admin/structure/book/settings', array(), t('Save configuration'));
$this->assertIdentical($current_config, $this->config('book.settings')->get());
$edit = array(
'name' => 'Animal book',
'type' => 'zebra',
);
$this->drupalPostForm('admin/structure/types/manage/zebra', $edit, t('Save content type'));
// Test the order of the book.settings::allowed_types configuration is as
// expected. The order should be:
// @code
// array(
// 'page',
// 'zebra',
// );
// @endcode
$current_config = $this->config('book.settings')->get();
$this->drupalPostForm('admin/structure/book/settings', array(), t('Save configuration'));
$this->assertIdentical($current_config, $this->config('book.settings')->get());
// Ensure that after all the node type changes book.settings:child_type has
// the expected value.
$this->assertEqual($this->config('book.settings')->get('child_type'), 'zebra');
}
/**
* Tests re-ordering of books.
*/
......
......@@ -186,30 +186,6 @@ function field_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundl
}
}
/**
* Implements hook_entity_bundle_rename().
*/
function field_entity_bundle_rename($entity_type, $bundle_old, $bundle_new) {
$fields = entity_load_multiple_by_properties('field_config', array('entity_type' => $entity_type, 'bundle' => $bundle_old, 'include_deleted' => TRUE));
foreach ($fields as $field) {
$id_new = $field->getTargetEntityTypeId() . '.' . $bundle_new . '.' . $field->getName();
$field->set('id', $id_new);
$field->set('bundle', $bundle_new);
// Save non-deleted fields.
if (!$field->isDeleted()) {
$field->allowBundleRename();
$field->save();
}
// Update deleted fields directly in the state storage.
else {
$state = \Drupal::state();
$deleted_fields = $state->get('field.field.deleted') ?: array();
$deleted_fields[$field->uuid] = $field->toArray();
$state->set('field.field.deleted', $deleted_fields);
}
}
}
/**
* Implements hook_entity_bundle_delete().
*
......
......@@ -167,7 +167,7 @@ public function preSave(EntityStorageInterface $storage) {
if ($this->entity_type != $this->original->entity_type) {
throw new FieldException("Cannot change an existing field's entity_type.");
}
if ($this->bundle != $this->original->bundle && empty($this->bundleRenameAllowed)) {
if ($this->bundle != $this->original->bundle) {
throw new FieldException("Cannot change an existing field's bundle.");
}
if ($storage_definition->uuid() != $this->original->getFieldStorageDefinition()->uuid()) {
......
......@@ -275,9 +275,9 @@ function testFieldAttachDelete() {
}
/**
* Test entity_bundle_create() and entity_bundle_rename().
* Test entity_bundle_create().
*/
function testEntityCreateRenameBundle() {
function testEntityCreateBundle() {
$entity_type = 'entity_test_rev';
$this->createFieldWithStorage('', $entity_type);
$cardinality = $this->fieldTestData->field_storage->getCardinality();
......@@ -298,20 +298,6 @@ function testEntityCreateRenameBundle() {
// Verify the field data is present on load.
$entity = $this->entitySaveReload($entity);
$this->assertEqual(count($entity->{$this->fieldTestData->field_name}), $cardinality, "Data is retrieved for the new bundle");
// Rename the bundle.
$new_bundle = 'test_bundle_' . Unicode::strtolower($this->randomMachineName());
entity_test_rename_bundle($this->fieldTestData->field_definition['bundle'], $new_bundle, $entity_type);
// Check that the field definition has been updated.
$this->fieldTestData->field = FieldConfig::loadByName($entity_type, $new_bundle, $this->fieldTestData->field_name);
$this->assertIdentical($this->fieldTestData->field->getTargetBundle(), $new_bundle, "Bundle name has been updated in the field.");
// Verify the field data is present on load.
$controller = $this->container->get('entity.manager')->getStorage($entity->getEntityTypeId());
$controller->resetCache();
$entity = $controller->load($entity->id());
$this->assertEqual(count($entity->{$this->fieldTestData->field_name}), $cardinality, "Bundle name has been updated in the field storage");
}
/**
......
......@@ -107,15 +107,6 @@ function field_ui_entity_bundle_create($entity_type, $bundle) {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_entity_bundle_rename().
*/
function field_ui_entity_bundle_rename($entity_type, $bundle_old, $bundle_new) {
// When a bundle is renamed, the menu needs to be rebuilt to add our
// menu item tabs.
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_form_FORM_ID_alter().
*
......
......@@ -300,9 +300,9 @@ public function testBaseFieldComponent() {
}
/**
* Tests renaming and deleting a bundle.
* Tests deleting a bundle.
*/
public function testRenameDeleteBundle() {
public function testDeleteBundle() {
// Create a node bundle, display and form display object.
$type = NodeType::create(array('type' => 'article'));
$type->save();
......@@ -310,44 +310,11 @@ public function testRenameDeleteBundle() {
entity_get_display('node', 'article', 'default')->save();
entity_get_form_display('node', 'article', 'default')->save();
// Rename the article bundle and assert the entity display is renamed.
$type->old_type = 'article';
$type->set('type', 'article_rename');
$type->save();
$old_display = entity_load('entity_view_display', 'node.article.default');
$this->assertFalse((bool) $old_display);
$old_form_display = entity_load('entity_form_display', 'node.article.default');
$this->assertFalse((bool) $old_form_display);
$new_display = entity_load('entity_view_display', 'node.article_rename.default');
$this->assertEqual('article_rename', $new_display->getTargetBundle());
$this->assertEqual('node.article_rename.default', $new_display->id());
$new_form_display = entity_load('entity_form_display', 'node.article_rename.default');
$this->assertEqual('article_rename', $new_form_display->getTargetBundle());
$this->assertEqual('node.article_rename.default', $new_form_display->id());
$expected_view_dependencies = array(
'config' => array('field.field.node.article_rename.body', 'node.type.article_rename'),
'module' => array('entity_test', 'text', 'user')
);
// Check that the display has dependencies on the bundle, fields and the
// modules that provide the formatters.
$dependencies = $new_display->calculateDependencies();
$this->assertEqual($expected_view_dependencies, $dependencies);
// Check that the form display has dependencies on the bundle, fields and
// the modules that provide the formatters.
$dependencies = $new_form_display->calculateDependencies();
$expected_form_dependencies = array(
'config' => array('field.field.node.article_rename.body', 'node.type.article_rename'),
'module' => array('text')
);
$this->assertEqual($expected_form_dependencies, $dependencies);
// Delete the bundle.
$type->delete();
$display = entity_load('entity_view_display', 'node.article_rename.default');
$display = entity_load('entity_view_display', 'node.article.default');
$this->assertFalse((bool) $display);
$form_display = entity_load('entity_form_display', 'node.article_rename.default');
$form_display = entity_load('entity_form_display', 'node.article.default');
$this->assertFalse((bool) $form_display);
}
......
......@@ -590,19 +590,6 @@ function testHiddenFields() {
}
}
/**
* Tests renaming a bundle.
*/