Commit b0900c73 authored by catch's avatar catch

Issue #2787873 by claudiu.cristea, amateescu, jibran, dawehner, catch: Add a...

Issue #2787873 by claudiu.cristea, amateescu, jibran, dawehner, catch: Add a base class for entity reference selection handlers and fix the structure of their configuration
parent 3225473d
......@@ -780,11 +780,24 @@ text_format:
# The text format should not be translated as part of the string
# translation system, so this is not marked as translatable.
# Schema for the configuration of the Entity reference selection plugins.
# Base schema for all entity reference selection handler schemas.
entity_reference_selection:
type: mapping
label: 'Entity reference selection plugin configuration'
label: 'Entity reference selection handler settings'
mapping:
target_type:
type: string
label: 'Type of item to reference'
# Schema for all entity reference selection handlers that are not providing a
# specific schema.
entity_reference_selection.*:
type: entity_reference_selection
# Schema for the entity reference 'default' selection handler settings.
entity_reference_selection.default:
type: entity_reference_selection
label: 'Default selection handler settings'
mapping:
target_bundles:
type: sequence
......@@ -792,7 +805,7 @@ entity_reference_selection:
nullable: true
sequence:
type: string
label: 'Type'
label: 'Bundle'
sort:
type: mapping
label: 'Sort settings'
......@@ -810,5 +823,7 @@ entity_reference_selection:
type: string
label: 'Bundle assigned to the auto-created entities.'
entity_reference_selection.*:
type: entity_reference_selection
# Schema for all entity reference 'default:*' selection handlers that are not
# providing a specific schema.
entity_reference_selection.default:*:
type: entity_reference_selection.default
......@@ -152,10 +152,9 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte
$value = NULL;
if (!empty($element['#value'])) {
$options = [
$options = $element['#selection_settings'] + [
'target_type' => $element['#target_type'],
'handler' => $element['#selection_handler'],
'handler_settings' => $element['#selection_settings'],
];
/** @var /Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */
$handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options);
......
......@@ -52,10 +52,9 @@ public function __construct(SelectionPluginManagerInterface $selection_manager)
public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') {
$matches = [];
$options = [
$options = $selection_settings + [
'target_type' => $target_type,
'handler' => $selection_handler,
'handler_settings' => $selection_settings,
];
$handler = $this->selectionManager->getInstance($options);
......
<?php
namespace Drupal\Core\Entity\EntityReferenceSelection;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
/**
* Provides a base class for configurable selection handlers.
*/
abstract class SelectionPluginBase extends PluginBase implements SelectionInterface, ConfigurablePluginInterface {
/**
* Constructs a new selection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'target_type' => NULL,
// @todo Remove this key in Drupal 9.0.x.
'handler' => $this->getPluginId(),
'entity' => NULL,
];
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
// Resolve backward compatibility level configurations, if any.
$this->resolveBackwardCompatibilityConfiguration($configuration);
// Merge in defaults.
$this->configuration = NestedArray::mergeDeep(
$this->defaultConfiguration(),
$configuration
);
// Ensure a backward compatibility level configuration.
$this->ensureBackwardCompatibilityConfiguration();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { }
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { }
/**
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) { }
/**
* Moves the backward compatibility level configurations in the right place.
*
* In order to keep backward compatibility, we copy all settings, except
* 'target_type', 'handler' and 'entity' under 'handler_settings', following
* the structure from the field config. If the plugin was instantiated using
* the 'handler_settings' level, those values will be used. In case of
* conflict, the root level settings will take precedence. The backward
* compatibility aware configuration will have the next structure:
* - target_type
* - handler (will be removed in Drupal 9.0.x, it's the plugin id)
* - entity
* - setting_1
* - setting_2
* ...
* - setting_N
* - handler_settings: (will be removed in Drupal 9.0.x)
* - setting_1
* - setting_2
* ...
* - setting_N
*
* @param array $configuration
* The configuration array to be altered.
*
* @deprecated Scheduled for removal in Drupal 9.0.x.
*
* @see https://www.drupal.org/node/2870971
*/
protected function resolveBackwardCompatibilityConfiguration(array &$configuration) {
if (isset($this->defaultConfiguration()['handler_settings'])) {
throw new \InvalidArgumentException("{$this->getPluginDefinition()['class']}::defaultConfiguration() should not contain a 'handler_settings' key. All settings should be placed in the root level.");
}
// Extract the BC level from the passed configuration, if any.
if (array_key_exists('handler_settings', $configuration)) {
if (!is_array($configuration['handler_settings'])) {
throw new \InvalidArgumentException("The setting 'handler_settings' is reserved and cannot be used.");
}
@trigger_error("Providing settings under 'handler_settings' is deprecated and will be removed before 9.0.0. Move the settings in the root of the configuration array. See https://www.drupal.org/node/2870971.", E_USER_DEPRECATED);
// Settings passed in the root level take precedence over BC settings.
$configuration += $configuration['handler_settings'];
unset($configuration['handler_settings']);
}
}
/**
* Ensures a backward compatibility level configuration.
*
* @deprecated Scheduled for removal in Drupal 9.0.x.
*
* @see https://www.drupal.org/node/2870971
*/
protected function ensureBackwardCompatibilityConfiguration() {
// Synchronize back 'handler_settings'.
foreach ($this->configuration as $key => $value) {
// Filter out keys that belong strictly to the root level.
if (!in_array($key, ['handler', 'target_type', 'entity', 'handler_settings'])) {
$this->configuration['handler_settings'][$key] = $value;
}
}
}
}
......@@ -39,7 +39,6 @@ public function getInstance(array $options) {
// Initialize default options.
$options += [
'handler' => $this->getPluginId($options['target_type'], 'default'),
'handler_settings' => [],
];
// A specific selection plugin ID was already specified.
......@@ -50,6 +49,7 @@ public function getInstance(array $options) {
else {
$plugin_id = $this->getPluginId($options['target_type'], $options['handler']);
}
unset($options['handler']);
return $this->createInstance($plugin_id, $options);
}
......@@ -92,10 +92,10 @@ public function getSelectionGroups($entity_type_id) {
* {@inheritdoc}
*/
public function getSelectionHandler(FieldDefinitionInterface $field_definition, EntityInterface $entity = NULL) {
$options = [
$options = $field_definition->getSetting('handler_settings') ?: [];
$options += [
'target_type' => $field_definition->getFieldStorageDefinition()->getSetting('target_type'),
'handler' => $field_definition->getSetting('handler'),
'handler_settings' => $field_definition->getSetting('handler_settings') ?: [],
'entity' => $entity,
];
return $this->getInstance($options);
......
<?php
namespace Drupal\Core\Entity\EntityReferenceSelection;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides common methods and injects services for core selection handlers.
*/
trait SelectionTrait {
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new selection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entity_manager;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager'),
$container->get('module_handler'),
$container->get('current_user')
);
}
}
......@@ -2,8 +2,7 @@
namespace Drupal\Core\Entity\Plugin\EntityReferenceSelection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginBase;
use Drupal\Core\Form\FormStateInterface;
/**
......@@ -14,28 +13,19 @@
* label = @Translation("Broken/Missing")
* )
*/
class Broken implements SelectionInterface {
class Broken extends SelectionPluginBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['selection_handler'] = [
'#markup' => t('The selected selection handler is broken.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { }
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { }
/**
* {@inheritdoc}
*/
......@@ -57,9 +47,4 @@ public function validateReferenceableEntities(array $ids) {
return [];
}
/**
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) { }
}
......@@ -66,6 +66,8 @@ public function validateReferenceableNewEntities(array $entities) {
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) {
parent::entityQueryAlter($query);
$tables = $query->getTables();
$data_table = 'comment_field_data';
if (!isset($tables['comment_field_data']['alias'])) {
......
......@@ -48,7 +48,7 @@ base_file_field_field_settings:
label: 'Reference method'
handler_settings:
type: entity_reference_selection.[%parent.handler]
label: 'Entity reference selection settings'
label: 'File selection handler settings'
file_directory:
type: string
label: 'File directory'
......
......@@ -74,9 +74,7 @@ public function testNodeHandler() {
$selection_options = [
'target_type' => 'node',
'handler' => 'default',
'handler_settings' => [
'target_bundles' => NULL,
],
'target_bundles' => NULL,
];
// Build a set of test data.
......@@ -200,10 +198,8 @@ public function testUserHandler() {
$selection_options = [
'target_type' => 'user',
'handler' => 'default',
'handler_settings' => [
'target_bundles' => NULL,
'include_anonymous' => TRUE,
],
'target_bundles' => NULL,
'include_anonymous' => TRUE,
];
// Build a set of test data.
......@@ -322,7 +318,7 @@ public function testUserHandler() {
$this->assertReferenceable($selection_options, $referenceable_tests, 'User handler (admin)');
// Test the 'include_anonymous' option.
$selection_options['handler_settings']['include_anonymous'] = FALSE;
$selection_options['include_anonymous'] = FALSE;
$referenceable_tests = [
[
'arguments' => [
......@@ -361,9 +357,7 @@ public function testCommentHandler() {
$selection_options = [
'target_type' => 'comment',
'handler' => 'default',
'handler_settings' => [
'target_bundles' => NULL,
],
'target_bundles' => NULL,
];
// Build a set of test data.
......
......@@ -3,7 +3,6 @@
namespace Drupal\taxonomy\Plugin\EntityReferenceSelection;
use Drupal\Component\Utility\Html;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Form\FormStateInterface;
use Drupal\taxonomy\Entity\Vocabulary;
......@@ -24,8 +23,13 @@ class TermSelection extends DefaultSelection {
/**
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) {
// @todo: How to set access, as vocabulary is now config?
public function defaultConfiguration() {
return [
'sort' => [
'field' => 'name',
'direction' => 'asc',
]
] + parent::defaultConfiguration();
}
/**
......@@ -49,15 +53,13 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
*/
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
if ($match || $limit) {
$this->configuration['handler_settings']['sort'] = ['field' => 'name', 'direction' => 'asc'];
return parent::getReferenceableEntities($match, $match_operator, $limit);
}
$options = [];
$bundles = $this->entityManager->getBundleInfo('taxonomy_term');
$handler_settings = $this->configuration['handler_settings'];
$bundle_names = !empty($handler_settings['target_bundles']) ? $handler_settings['target_bundles'] : array_keys($bundles);
$bundle_names = $this->getConfiguration()['target_bundles'] ?: array_keys($bundles);
foreach ($bundle_names as $bundle) {
if ($vocabulary = Vocabulary::load($bundle)) {
......
......@@ -166,8 +166,10 @@ condition.plugin.user_role:
sequence:
type: string
# Schema for the entity reference 'default:user' selection handler settings.
entity_reference_selection.default:user:
type: entity_reference_selection
type: entity_reference_selection.default
label: 'User selection handler settings'
mapping:
filter:
type: mapping
......
......@@ -83,21 +83,26 @@ public static function create(ContainerInterface $container, array $configuratio
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$selection_handler_settings = $this->configuration['handler_settings'];
// Merge in default values.
$selection_handler_settings += [
public function defaultConfiguration() {
return [
'filter' => [
'type' => '_none',
'role' => NULL,
],
'include_anonymous' => TRUE,
];
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$configuration = $this->getConfiguration();
$form['include_anonymous'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include the anonymous user.'),
'#default_value' => $selection_handler_settings['include_anonymous'],
'#default_value' => $configuration['include_anonymous'],
];
// Add user specific filter options.
......@@ -110,7 +115,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
],
'#ajax' => TRUE,
'#limit_validation_errors' => [],
'#default_value' => $selection_handler_settings['filter']['type'],
'#default_value' => $configuration['filter']['type'],
];
$form['filter']['settings'] = [
......@@ -119,18 +124,13 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#process' => [['\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', 'formProcessMergeParent']],
];
if ($selection_handler_settings['filter']['type'] == 'role') {
// Merge in default values.
$selection_handler_settings['filter'] += [
'role' => NULL,
];
if ($configuration['filter']['type'] == 'role') {
$form['filter']['settings']['role'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Restrict to the selected roles'),
'#required' => TRUE,
'#options' => array_diff_key(user_role_names(TRUE), [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID]),
'#default_value' => $selection_handler_settings['filter']['role'],
'#default_value' => $configuration['filter']['role'],
];
}
......@@ -144,11 +144,12 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$handler_settings = $this->configuration['handler_settings'];
$configuration = $this->getConfiguration();
// Filter out the Anonymous user if the selection handler is configured to
// exclude it.
if (isset($handler_settings['include_anonymous']) && !$handler_settings['include_anonymous']) {
if (!$configuration['include_anonymous']) {
$query->condition('uid', 0, '<>');
}
......@@ -158,8 +159,8 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS')
}
// Filter by role.
if (!empty($handler_settings['filter']['role'])) {
$query->condition('roles', $handler_settings['filter']['role'], 'IN');
if (!empty($configuration['filter']['role'])) {
$query->condition('roles', $configuration['filter']['role'], 'IN');
}
// Adding the permission check is sadly insufficient for users: core
......@@ -191,10 +192,10 @@ public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!empty($this->configuration['handler_settings']['filter']['role'])) {
$entities = array_filter($entities, function ($user) {
if ($role = $this->getConfiguration()['filter']['role']) {
$entities = array_filter($entities, function ($user) use ($role) {
/** @var \Drupal\user\UserInterface $user */
return !empty(array_intersect($user->getRoles(), $this->configuration['handler_settings']['filter']['role']));
return !empty(array_intersect($user->getRoles(), $role));
});
}
if (!$this->currentUser->hasPermission('administer users')) {
......@@ -210,9 +211,10 @@ public function validateReferenceableNewEntities(array $entities) {
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) {
parent::entityQueryAlter($query);
// Bail out early if we do not need to match the Anonymous user.
$handler_settings = $this->configuration['handler_settings'];
if (isset($handler_settings['include_anonymous']) && !$handler_settings['include_anonymous']) {
if (!$this->getConfiguration()['include_anonymous']) {
return;
}
......
# Schema for the views entity reference selection plugins.
# Schema for the entity reference 'views' selection handler settings.
entity_reference_selection.views:
type: mapping
label: 'View handler settings'
type: entity_reference_selection
label: 'Views selection handler settings'
mapping:
view:
type: mapping
......
......@@ -2,17 +2,12 @@
namespace Drupal\views\Plugin\EntityReferenceSelection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginBase;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'selection' entity_reference.
......@@ -24,80 +19,37 @@
* weight = 0
* )
*/
class ViewsSelection extends PluginBase implements SelectionInterface, ContainerFactoryPluginInterface {
class ViewsSelection extends SelectionPluginBase implements ContainerFactoryPluginInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
use SelectionTrait;
/**
* Constructs a new SelectionBase object.
* The loaded View object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.