Commit fa0af0b3 authored by alexpott's avatar alexpott

Issue #1978714 by amateescu, claudiu.cristea, pfrenssen, yched, jibran,...

Issue #1978714 by amateescu, claudiu.cristea, pfrenssen, yched, jibran, filijonka, dawehner, alexpott, tstoeckler: Entity reference doesn't update its field settings when referenced entity bundles are deleted
parent de2e8a99
......@@ -766,6 +766,7 @@ entity_reference_selection:
target_bundles:
type: sequence
label: 'types'
nullable: true
sequence:
type: string
label: 'Type'
......
......@@ -186,4 +186,14 @@ public function setTypedConfig(TypedConfigManagerInterface $typed_config) {
$this->typedConfig = $typed_config;
}
/**
* Determines if this element allows NULL as a value.
*
* @return bool
* TRUE if NULL is a valid value, FALSE otherwise.
*/
public function isNullable() {
return isset($this->definition['nullable']) && $this->definition['nullable'] == TRUE;
}
}
......@@ -106,9 +106,13 @@ protected function checkValue($key, $value) {
(($type == 'double' || $type == 'integer') && $element instanceof FloatInterface) ||
($type == 'boolean' && $element instanceof BooleanInterface) ||
($type == 'string' && $element instanceof StringInterface) ||
// Null values are allowed for all types.
// Null values are allowed for all primitive types.
($value === NULL);
}
// Array elements can also opt-in for allowing a NULL value.
elseif ($element instanceof ArrayElement && $element->isNullable() && $value === NULL) {
$success = TRUE;
}
$class = get_class($element);
if (!$success) {
return array($error_key => "variable type is $type but applied schema class is $class");
......
......@@ -111,7 +111,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
// Merge-in default values.
$selection_handler_settings += array(
'target_bundles' => array(),
// For the 'target_bundles' setting, a NULL value is equivalent to "allow
// entities from any bundle to be referenced" and an empty array value is
// equivalent to "no entities from any bundle can be referenced".
'target_bundles' => NULL,
'sort' => array(
'field' => '_none',
),
......@@ -128,7 +131,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#type' => 'checkboxes',
'#title' => $this->t('Bundles'),
'#options' => $bundle_options,
'#default_value' => (!empty($selection_handler_settings['target_bundles'])) ? $selection_handler_settings['target_bundles'] : array(),
'#default_value' => (array) $selection_handler_settings['target_bundles'],
'#required' => TRUE,
'#size' => 6,
'#multiple' => TRUE,
......@@ -207,7 +210,15 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { }
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// If no checkboxes were checked for 'target_bundles', store NULL ("all
// bundles are referenceable") rather than empty array ("no bundle is
// referenceable" - typically happens when all referenceable bundles have
// been deleted).
if ($form_state->getValue(['settings', 'handler_settings', 'target_bundles']) === []) {
$form_state->setValue(['settings', 'handler_settings', 'target_bundles'], NULL);
}
}
/**
* {@inheritdoc}
......@@ -326,8 +337,20 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS')
$entity_type = $this->entityManager->getDefinition($target_type);
$query = $this->entityManager->getStorage($target_type)->getQuery();
if (!empty($handler_settings['target_bundles'])) {
$query->condition($entity_type->getKey('bundle'), $handler_settings['target_bundles'], 'IN');
// If 'target_bundles' is NULL, all bundles are referenceable, no further
// conditions are needed.
if (isset($handler_settings['target_bundles']) && is_array($handler_settings['target_bundles'])) {
// If 'target_bundles' is an empty array, no bundle is referenceable,
// force the query to never return anything and bail out early.
if ($handler_settings['target_bundles'] === []) {
$query->condition($entity_type->getKey('id'), NULL, '=');
return $query;
}
else {
$query->condition($entity_type->getKey('bundle'), $handler_settings['target_bundles'], 'IN');
}
}
if (isset($match) && $label_key = $entity_type->getKey('label')) {
......
......@@ -151,7 +151,7 @@ public function getConstraints() {
list($current_handler) = explode(':', $this->getSetting('handler'), 2);
if ($current_handler === 'default') {
$handler_settings = $this->getSetting('handler_settings');
if (!empty($handler_settings['target_bundles'])) {
if (isset($handler_settings['target_bundles'])) {
$constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$constraints[] = $constraint_manager->create('ComplexData', [
'entity' => [
......@@ -298,9 +298,12 @@ public function hasNewEntity() {
* {@inheritdoc}
*/
public static function calculateDependencies(FieldDefinitionInterface $field_definition) {
$dependencies = [];
$dependencies = parent::calculateDependencies($field_definition);
$manager = \Drupal::entityManager();
$target_entity_type = $manager->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
// Depend on default values entity types configurations.
if ($default_value = $field_definition->getDefaultValueLiteral()) {
$target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
foreach ($default_value as $value) {
if (is_array($value) && isset($value['target_uuid'])) {
$entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']);
......@@ -312,6 +315,19 @@ public static function calculateDependencies(FieldDefinitionInterface $field_def
}
}
}
// Depend on target bundle configurations.
$handler = $field_definition->getSetting('handler_settings');
if (!empty($handler['target_bundles'])) {
if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) {
if ($storage = $manager->getStorage($bundle_entity_type_id)) {
foreach ($storage->loadMultiple($handler['target_bundles']) as $bundle) {
$dependencies[$bundle->getConfigDependencyKey()][] = $bundle->getConfigDependencyName();
}
}
}
}
return $dependencies;
}
......@@ -319,12 +335,15 @@ public static function calculateDependencies(FieldDefinitionInterface $field_def
* {@inheritdoc}
*/
public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) {
$changed = FALSE;
$changed = parent::onDependencyRemoval($field_definition, $dependencies);
$entity_manager = \Drupal::entityManager();
$target_entity_type = $entity_manager->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
// Try to update the default value config dependency, if possible.
if ($default_value = $field_definition->getDefaultValueLiteral()) {
$target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
foreach ($default_value as $key => $value) {
if (is_array($value) && isset($value['target_uuid'])) {
$entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']);
$entity = $entity_manager->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']);
// @see \Drupal\Core\Field\EntityReferenceFieldItemList::processDefaultValue()
if ($entity && isset($dependencies[$entity->getConfigDependencyKey()][$entity->getConfigDependencyName()])) {
unset($default_value[$key]);
......@@ -336,6 +355,41 @@ public static function onDependencyRemoval(FieldDefinitionInterface $field_defin
$field_definition->setDefaultValue($default_value);
}
}
// Update the 'target_bundles' handler setting if a bundle config dependency
// has been removed.
$bundles_changed = FALSE;
$handler_settings = $field_definition->getSetting('handler_settings');
if (!empty($handler_settings['target_bundles'])) {
if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) {
if ($storage = $entity_manager->getStorage($bundle_entity_type_id)) {
foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) {
if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) {
unset($handler_settings['target_bundles'][$bundle->id()]);
$bundles_changed = TRUE;
// In case we deleted the only target bundle allowed by the field
// we have to log a warning message because the field will not
// function correctly anymore.
if ($handler_settings['target_bundles'] === []) {
\Drupal::logger('entity_reference')->critical('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference field (entity_type: %entity_type, bundle: %bundle) no longer has any valid bundle it can reference. The field is not working correctly anymore and has to be adjusted.', [
'%target_bundle' => $bundle->label(),
'%target_entity_type' => $bundle->getEntityType()->getBundleOf(),
'%field_name' => $field_definition->getName(),
'%entity_type' => $field_definition->getTargetEntityTypeId(),
'%bundle' => $field_definition->getTargetBundle()
]);
}
}
}
}
}
}
if ($bundles_changed) {
$field_definition->setSetting('handler_settings', $handler_settings);
}
$changed |= $bundles_changed;
return $changed;
}
......
......@@ -10,7 +10,6 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field\FieldConfigInterface;
......@@ -211,3 +210,49 @@ function entity_reference_query_entity_reference_alter(AlterableInterface $query
$handler = $query->getMetadata('entity_reference_selection_handler');
$handler->entityQueryAlter($query);
}
/**
* Implements hook_entity_bundle_delete().
*
* We are duplicating the work done by
* \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onDependencyRemoval()
* because we need to take into account bundles that are not provided by a
* config entity type so they are not part of the config dependencies.
*/
function entity_reference_entity_bundle_delete($entity_type_id, $bundle) {
// Gather a list of all entity reference fields.
$map = \Drupal::entityManager()->getFieldMapByFieldType('entity_reference');
$ids = [];
foreach ($map as $type => $info) {
foreach ($info as $name => $data) {
foreach ($data['bundles'] as $bundle_name) {
$ids[] = "$type.$bundle_name.$name";
}
}
}
// Update the 'target_bundles' handler setting if needed.
foreach (FieldConfig::loadMultiple($ids) as $field_config) {
if ($field_config->getSetting('target_type') == $entity_type_id) {
$handler_settings = $field_config->getSetting('handler_settings');
if (isset($handler_settings['target_bundles'][$bundle])) {
unset($handler_settings['target_bundles'][$bundle]);
$field_config->setSetting('handler_settings', $handler_settings);
$field_config->save();
// In case we deleted the only target bundle allowed by the field we
// have to log a warning message because the field will not function
// correctly anymore.
if ($handler_settings['target_bundles'] === []) {
\Drupal::logger('entity_reference')->critical('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference field (entity_type: %entity_type, bundle: %bundle) no longer has any valid bundle it can reference. The field is not working correctly anymore and has to be adjusted.', [
'%target_bundle' => $bundle,
'%target_entity_type' => $entity_type_id,
'%field_name' => $field_config->getName(),
'%entity_type' => $field_config->getTargetEntityTypeId(),
'%bundle' => $field_config->getTargetBundle()
]);
}
}
}
}
}
<?php
/**
* @file
* Contains \Drupal\entity_reference\Tests\EntityReferenceSettingsTest.
*/
namespace Drupal\entity_reference\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\NodeType;
use Drupal\simpletest\KernelTestBase;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests entity reference field settings.
*
* @group entity_reference
*/
class EntityReferenceSettingsTest extends KernelTestBase {
use EntityReferenceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'taxonomy', 'field', 'user', 'text', 'entity_reference', 'entity_test'];
/**
* Testing node type.
*
* @var \Drupal\node\Entity\NodeType
*/
protected $nodeType;
/**
* Testing vocabulary.
*
* @var \Drupal\taxonomy\Entity\Vocabulary
*/
protected $vocabulary;
/**
* An entity bundle that is not stored as a configuration entity.
*
* @var string
*/
protected $customBundle;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setup();
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('entity_test');
$this->nodeType = NodeType::create([
'type' => Unicode::strtolower($this->randomMachineName()),
'name' => $this->randomString(),
]);
$this->nodeType->save();
$this->vocabulary = Vocabulary::create([
'vid' => Unicode::strtolower($this->randomMachineName()),
'name' => $this->randomString(),
]);
$this->vocabulary->save();
// Create a custom bundle.
$this->customBundle = 'test_bundle_' . Unicode::strtolower($this->randomMachineName());
entity_test_create_bundle($this->customBundle, NULL, 'entity_test');
}
/**
* Tests that config bundle deletions are mirrored in field config settings.
*/
public function testConfigTargetBundleDeletion() {
// Attach an entity reference field to $this->nodeType.
$name = Unicode::strtolower($this->randomMachineName());
$label = $this->randomString();
$vid = $this->vocabulary->id();
$handler_settings = ['target_bundles' => [$vid => $vid]];
$this->createEntityReferenceField('node', $this->nodeType->id(), $name, $label, 'taxonomy_term', 'default', $handler_settings);
// Check that the 'target_bundle' setting contains the vocabulary.
$field_config = FieldConfig::loadByName('node', $this->nodeType->id(), $name);
$actual_handler_settings = $field_config->getSetting('handler_settings');
$this->assertEqual($handler_settings, $actual_handler_settings);
// Delete the vocabulary.
$this->vocabulary->delete();
// Check that the deleted vocabulary is no longer present in the
// 'target_bundles' field setting.
$field_config = FieldConfig::loadByName('node', $this->nodeType->id(), $name);
$handler_settings = $field_config->getSetting('handler_settings');
$this->assertTrue(empty($handler_settings['target_bundles']));
}
/**
* Tests that deletions of custom bundles are mirrored in field settings.
*/
public function testCustomTargetBundleDeletion() {
// Attach an entity reference field to $this->nodeType.
$name = Unicode::strtolower($this->randomMachineName());
$label = $this->randomString();
$handler_settings = ['target_bundles' => [$this->customBundle => $this->customBundle]];
$this->createEntityReferenceField('node', $this->nodeType->id(), $name, $label, 'entity_test', 'default', $handler_settings);
// Check that the 'target_bundle' setting contains the custom bundle.
$field_config = FieldConfig::loadByName('node', $this->nodeType->id(), $name);
$actual_handler_settings = $field_config->getSetting('handler_settings');
$this->assertEqual($handler_settings, $actual_handler_settings);
// Delete the custom bundle.
entity_test_delete_bundle($this->customBundle, 'entity_test');
// Check that the deleted bundle is no longer present in the
// 'target_bundles' field setting.
$field_config = FieldConfig::loadByName('node', $this->nodeType->id(), $name);
$handler_settings = $field_config->getSetting('handler_settings');
$this->assertTrue(empty($handler_settings['target_bundles']));
}
}
......@@ -77,7 +77,7 @@ public function testNodeHandler() {
'target_type' => 'node',
'handler' => 'default',
'handler_settings' => array(
'target_bundles' => array(),
'target_bundles' => NULL,
),
);
......@@ -203,7 +203,7 @@ public function testUserHandler() {
'target_type' => 'user',
'handler' => 'default',
'handler_settings' => array(
'target_bundles' => array(),
'target_bundles' => NULL,
'include_anonymous' => TRUE,
),
);
......@@ -345,7 +345,7 @@ public function testCommentHandler() {
'target_type' => 'comment',
'handler' => 'default',
'handler_settings' => array(
'target_bundles' => array(),
'target_bundles' => NULL,
),
);
......
......@@ -100,7 +100,7 @@ public function testSort() {
'target_type' => 'node',
'handler' => 'default',
'handler_settings' => array(
'target_bundles' => array(),
'target_bundles' => NULL,
// Add sorting.
'sort' => array(
'field' => 'field_text.value',
......
......@@ -95,6 +95,14 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// Don't call the parent validation handler because we don't have any
// 'target_bundles' setting.
}
/**
* Initializes a view.
*
......
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