diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 0fbeeedbe0..964892e41e 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -776,11 +776,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 @@ -788,7 +801,7 @@ entity_reference_selection: nullable: true sequence: type: string - label: 'Type' + label: 'Bundle' sort: type: mapping label: 'Sort settings' @@ -806,5 +819,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 diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 9c3e705bd3..7dc8073629 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -152,11 +152,10 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte $value = NULL; if (!empty($element['#value'])) { - $options = array( + $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); $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface; diff --git a/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php index 01a29b26bb..5bf5debc5e 100644 --- a/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php +++ b/core/lib/Drupal/Core/Entity/EntityAutocompleteMatcher.php @@ -52,11 +52,10 @@ public function __construct(SelectionPluginManagerInterface $selection_manager) public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') { $matches = array(); - $options = array( + $options = $selection_settings + [ 'target_type' => $target_type, 'handler' => $selection_handler, - 'handler_settings' => $selection_settings, - ); + ]; $handler = $this->selectionManager->getInstance($options); if (isset($string)) { diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php new file mode 100644 index 0000000000..b81b6678e6 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginBase.php @@ -0,0 +1,156 @@ +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. + */ + 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.", 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. + */ + 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; + } + } + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php index 1b35d4aafa..a723e2a9fb 100644 --- a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionPluginManager.php @@ -39,7 +39,6 @@ public function getInstance(array $options) { // Initialize default options. $options += array( 'handler' => $this->getPluginId($options['target_type'], 'default'), - 'handler_settings' => array(), ); // 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,12 +92,12 @@ public function getSelectionGroups($entity_type_id) { * {@inheritdoc} */ public function getSelectionHandler(FieldDefinitionInterface $field_definition, EntityInterface $entity = NULL) { - $options = array( + $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') ?: array(), 'entity' => $entity, - ); + ]; return $this->getInstance($options); } diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionTrait.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionTrait.php new file mode 100644 index 0000000000..1d7947df21 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionTrait.php @@ -0,0 +1,74 @@ +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') + ); + } + +} diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php index d74fb6e11a..6324282c60 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/Broken.php @@ -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,12 +13,13 @@ * 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'] = array( '#markup' => t('The selected selection handler is broken.'), ); @@ -29,16 +29,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta /** * {@inheritdoc} */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { } - - /** - * {@inheritdoc} - */ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { return array(); } @@ -57,9 +47,4 @@ public function validateReferenceableEntities(array $ids) { return array(); } - /** - * {@inheritdoc} - */ - public function entityQueryAlter(SelectInterface $query) { } - } diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php index d924e1ae13..5c72b31d0a 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php @@ -4,19 +4,14 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Database\Query\AlterableInterface; -use Drupal\Core\Database\Query\SelectInterface; -use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginBase; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionTrait; use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface; use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Plugin\PluginBase; -use Drupal\Core\Session\AccountInterface; use Drupal\user\EntityOwnerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Default plugin implementation of the Entity Reference Selection plugin. @@ -38,89 +33,39 @@ * deriver = "Drupal\Core\Entity\Plugin\Derivative\DefaultSelectionDeriver" * ) */ -class DefaultSelection extends PluginBase implements SelectionInterface, SelectionWithAutocreateInterface, ContainerFactoryPluginInterface { +class DefaultSelection extends SelectionPluginBase implements ContainerFactoryPluginInterface, SelectionWithAutocreateInterface { - /** - * 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; - - /** - * Constructs a new SelectionBase 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; - } + use SelectionTrait; /** * {@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') - ); + public function defaultConfiguration() { + return [ + // 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' => [ + 'field' => '_none', + 'direction' => 'ASC', + ], + 'auto_create' => FALSE, + 'auto_create_bundle' => NULL, + ] + parent::defaultConfiguration(); } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $entity_type_id = $this->configuration['target_type']; - $selection_handler_settings = $this->configuration['handler_settings']; + $form = parent::buildConfigurationForm($form, $form_state); + + $configuration = $this->getConfiguration(); + $entity_type_id = $configuration['target_type']; $entity_type = $this->entityManager->getDefinition($entity_type_id); $bundles = $this->entityManager->getBundleInfo($entity_type_id); - // Merge-in default values. - $selection_handler_settings += 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', - ), - 'auto_create' => FALSE, - 'auto_create_bundle' => NULL, - ); - if ($entity_type->hasKey('bundle')) { $bundle_options = array(); foreach ($bundles as $bundle_name => $bundle_info) { @@ -132,7 +77,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#type' => 'checkboxes', '#title' => $this->t('Bundles'), '#options' => $bundle_options, - '#default_value' => (array) $selection_handler_settings['target_bundles'], + '#default_value' => (array) $configuration['target_bundles'], '#required' => TRUE, '#size' => 6, '#multiple' => TRUE, @@ -189,7 +134,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ) + $fields, '#ajax' => TRUE, '#limit_validation_errors' => array(), - '#default_value' => $selection_handler_settings['sort']['field'], + '#default_value' => $configuration['sort']['field'], ); $form['sort']['settings'] = array( @@ -198,12 +143,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#process' => [[EntityReferenceItem::class, 'formProcessMergeParent']], ); - if ($selection_handler_settings['sort']['field'] != '_none') { - // Merge-in default values. - $selection_handler_settings['sort'] += array( - 'direction' => 'ASC', - ); - + if ($configuration['sort']['field'] != '_none') { $form['sort']['settings']['direction'] = array( '#type' => 'select', '#title' => $this->t('Sort direction'), @@ -212,7 +152,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta 'ASC' => $this->t('Ascending'), 'DESC' => $this->t('Descending'), ), - '#default_value' => $selection_handler_settings['sort']['direction'], + '#default_value' => $configuration['sort']['direction'], ); } } @@ -220,17 +160,17 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta $form['auto_create'] = array( '#type' => 'checkbox', '#title' => $this->t("Create referenced entities if they don't already exist"), - '#default_value' => $selection_handler_settings['auto_create'], + '#default_value' => $configuration['auto_create'], '#weight' => -2, ); if ($entity_type->hasKey('bundle')) { - $bundles = array_intersect_key($bundle_options, array_filter((array) $selection_handler_settings['target_bundles'])); + $bundles = array_intersect_key($bundle_options, array_filter((array) $configuration['target_bundles'])); $form['auto_create_bundle'] = [ '#type' => 'select', '#title' => $this->t('Store new items in'), '#options' => $bundles, - '#default_value' => $selection_handler_settings['auto_create_bundle'], + '#default_value' => $configuration['auto_create_bundle'], '#access' => count($bundles) > 1, '#states' => [ 'visible' => [ @@ -248,6 +188,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta * {@inheritdoc} */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $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 @@ -262,11 +204,6 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form } /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { } - - /** * Form element validation handler; Filters the #value property of an element. */ public static function elementValidateFilter(&$element, FormStateInterface $form_state) { @@ -278,7 +215,7 @@ public static function elementValidateFilter(&$element, FormStateInterface $form * {@inheritdoc} */ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { - $target_type = $this->configuration['target_type']; + $target_type = $this->getConfiguration()['target_type']; $query = $this->buildEntityQuery($match, $match_operator); if ($limit > 0) { @@ -353,8 +290,9 @@ public function createNewEntity($entity_type_id, $bundle, $label, $uid) { */ public function validateReferenceableNewEntities(array $entities) { return array_filter($entities, function ($entity) { - if (isset($this->configuration['handler_settings']['target_bundles'])) { - return in_array($entity->bundle(), $this->configuration['handler_settings']['target_bundles']); + $target_bundles = $this->getConfiguration()['target_bundles']; + if (isset($target_bundles)) { + return in_array($entity->bundle(), $target_bundles); } return TRUE; }); @@ -374,23 +312,23 @@ public function validateReferenceableNewEntities(array $entities) { * it. */ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { - $target_type = $this->configuration['target_type']; - $handler_settings = $this->configuration['handler_settings']; + $configuration = $this->getConfiguration(); + $target_type = $configuration['target_type']; $entity_type = $this->entityManager->getDefinition($target_type); $query = $this->entityManager->getStorage($target_type)->getQuery(); // 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 (is_array($configuration['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'] === []) { + if ($configuration['target_bundles'] === []) { $query->condition($entity_type->getKey('id'), NULL, '='); return $query; } else { - $query->condition($entity_type->getKey('bundle'), $handler_settings['target_bundles'], 'IN'); + $query->condition($entity_type->getKey('bundle'), $configuration['target_bundles'], 'IN'); } } @@ -406,22 +344,14 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') $query->addMetaData('entity_reference_selection_handler', $this); // Add the sort option. - if (!empty($handler_settings['sort'])) { - $sort_settings = $handler_settings['sort']; - if ($sort_settings['field'] != '_none') { - $query->sort($sort_settings['field'], $sort_settings['direction']); - } + if ($configuration['sort']['field'] !== '_none') { + $query->sort($configuration['sort']['field'], $configuration['sort']['direction']); } return $query; } /** - * {@inheritdoc} - */ - public function entityQueryAlter(SelectInterface $query) { } - - /** * Helper method: Passes a query to the alteration system again. * * This allows Entity Reference to add a tag to an existing query so it can diff --git a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php index 2492fc4336..414c5b2311 100644 --- a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php +++ b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php @@ -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'])) { diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml index b9f8918f63..9526758ea9 100644 --- a/core/modules/file/config/schema/file.schema.yml +++ b/core/modules/file/config/schema/file.schema.yml @@ -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' diff --git a/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php b/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php index 86bc703cef..c2a1b78be9 100644 --- a/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php +++ b/core/modules/system/src/Tests/Entity/EntityReferenceSelection/EntityReferenceSelectionAccessTest.php @@ -73,9 +73,7 @@ public function testNodeHandler() { $selection_options = array( 'target_type' => 'node', 'handler' => 'default', - 'handler_settings' => array( - 'target_bundles' => NULL, - ), + 'target_bundles' => NULL, ); // Build a set of test data. @@ -199,10 +197,8 @@ public function testUserHandler() { $selection_options = array( 'target_type' => 'user', 'handler' => 'default', - 'handler_settings' => array( - 'target_bundles' => NULL, - 'include_anonymous' => TRUE, - ), + 'target_bundles' => NULL, + 'include_anonymous' => TRUE, ); // Build a set of test data. @@ -321,7 +317,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 = array( array( 'arguments' => array( @@ -360,9 +356,7 @@ public function testCommentHandler() { $selection_options = array( 'target_type' => 'comment', 'handler' => 'default', - 'handler_settings' => array( - 'target_bundles' => NULL, - ), + 'target_bundles' => NULL, ); // Build a set of test data. diff --git a/core/modules/taxonomy/src/Plugin/EntityReferenceSelection/TermSelection.php b/core/modules/taxonomy/src/Plugin/EntityReferenceSelection/TermSelection.php index 7dd4aa2d6e..75fc0529e2 100644 --- a/core/modules/taxonomy/src/Plugin/EntityReferenceSelection/TermSelection.php +++ b/core/modules/taxonomy/src/Plugin/EntityReferenceSelection/TermSelection.php @@ -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 = array(); $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)) { diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml index 627d8a6960..2f9bda44f2 100644 --- a/core/modules/user/config/schema/user.schema.yml +++ b/core/modules/user/config/schema/user.schema.yml @@ -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 diff --git a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php index 8dfba39636..378ff18d5f 100644 --- a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php +++ b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php @@ -82,21 +82,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 += array( - 'filter' => array( + 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'] = array( '#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. @@ -109,7 +114,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ), '#ajax' => TRUE, '#limit_validation_errors' => array(), - '#default_value' => $selection_handler_settings['filter']['type'], + '#default_value' => $configuration['filter']['type'], ); $form['filter']['settings'] = array( @@ -118,18 +123,13 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#process' => array(array('\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', 'formProcessMergeParent')), ); - if ($selection_handler_settings['filter']['type'] == 'role') { - // Merge in default values. - $selection_handler_settings['filter'] += array( - 'role' => NULL, - ); - + if ($configuration['filter']['type'] == 'role') { $form['filter']['settings']['role'] = array( '#type' => 'checkboxes', '#title' => $this->t('Restrict to the selected roles'), '#required' => TRUE, '#options' => array_diff_key(user_role_names(TRUE), array(RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID)), - '#default_value' => $selection_handler_settings['filter']['role'], + '#default_value' => $configuration['filter']['role'], ); } @@ -143,11 +143,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, '<>'); } @@ -157,8 +158,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 @@ -190,10 +191,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')) { @@ -209,9 +210,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; } diff --git a/core/modules/views/config/schema/views.entity_reference.schema.yml b/core/modules/views/config/schema/views.entity_reference.schema.yml index 027c62fa46..f13645ab43 100644 --- a/core/modules/views/config/schema/views.entity_reference.schema.yml +++ b/core/modules/views/config/schema/views.entity_reference.schema.yml @@ -1,8 +1,8 @@ -# 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 diff --git a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php index db19052928..f723d329be 100644 --- a/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php +++ b/core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php @@ -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; + use SelectionTrait; /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $currentUser; - - /** - * 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. + * @var \Drupal\views\ViewExecutable; */ - 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; - } + protected $view; /** * {@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') - ); + public function defaultConfiguration() { + return [ + 'view' => [ + 'view_name' => NULL, + 'display_name' => NULL, + 'arguments' => [], + ], + ] + parent::defaultConfiguration(); } /** - * The loaded View object. - * - * @var \Drupal\views\ViewExecutable; - */ - protected $view; - - /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $selection_handler_settings = $this->configuration['handler_settings']; - $view_settings = !empty($selection_handler_settings['view']) ? $selection_handler_settings['view'] : array(); + $form = parent::buildConfigurationForm($form, $form_state); + + $view_settings = $this->getConfiguration()['view']; $displays = Views::getApplicableViews('entity_reference_display'); // Filter views that list the entity type we want, and group the separate // displays by view. @@ -157,16 +109,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta } /** - * {@inheritdoc} - */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { } - - /** * Initializes a view. * * @param string|null $match @@ -184,9 +126,8 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s * Return TRUE if the view was initialized, FALSE otherwise. */ protected function initializeView($match = NULL, $match_operator = 'CONTAINS', $limit = 0, $ids = NULL) { - $handler_settings = $this->configuration['handler_settings']; - $view_name = $handler_settings['view']['view_name']; - $display_name = $handler_settings['view']['display_name']; + $view_name = $this->getConfiguration()['view']['view_name']; + $display_name = $this->getConfiguration()['view']['display_name']; // Check that the view is valid and the display still exists. $this->view = Views::getView($view_name); @@ -211,9 +152,8 @@ protected function initializeView($match = NULL, $match_operator = 'CONTAINS', $ * {@inheritdoc} */ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { - $handler_settings = $this->configuration['handler_settings']; - $display_name = $handler_settings['view']['display_name']; - $arguments = $handler_settings['view']['arguments']; + $display_name = $this->getConfiguration()['view']['display_name']; + $arguments = $this->getConfiguration()['view']['arguments']; $result = array(); if ($this->initializeView($match, $match_operator, $limit)) { // Get the results. @@ -242,9 +182,8 @@ public function countReferenceableEntities($match = NULL, $match_operator = 'CON * {@inheritdoc} */ public function validateReferenceableEntities(array $ids) { - $handler_settings = $this->configuration['handler_settings']; - $display_name = $handler_settings['view']['display_name']; - $arguments = $handler_settings['view']['arguments']; + $display_name = $this->getConfiguration()['view']['display_name']; + $arguments = $this->getConfiguration()['view']['arguments']; $result = array(); if ($this->initializeView(NULL, 'CONTAINS', 0, $ids)) { // Get the results. @@ -283,9 +222,4 @@ public static function settingsFormValidate($element, FormStateInterface $form_s $form_state->setValueForElement($element, $value); } - /** - * {@inheritdoc} - */ - public function entityQueryAlter(SelectInterface $query) { } - } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php index f51c8ea3dc..2054d43c1d 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityReferenceSelection/EntityReferenceSelectionSortTest.php @@ -96,13 +96,11 @@ public function testSort() { $selection_options = array( 'target_type' => 'node', 'handler' => 'default', - 'handler_settings' => array( - 'target_bundles' => NULL, - // Add sorting. - 'sort' => array( - 'field' => 'field_text.value', - 'direction' => 'DESC', - ), + 'target_bundles' => NULL, + // Add sorting. + 'sort' => array( + 'field' => 'field_text.value', + 'direction' => 'DESC', ), ); $handler = $this->container->get('plugin.manager.entity_reference_selection')->getInstance($selection_options); @@ -117,7 +115,7 @@ public function testSort() { $this->assertIdentical($result['article'], $expected_result, 'Query sorted by field returned expected values.'); // Assert sort by base field. - $selection_options['handler_settings']['sort'] = array( + $selection_options['sort'] = array( 'field' => 'nid', 'direction' => 'ASC', ); diff --git a/core/tests/Drupal/Tests/Core/EntityReferenceSelection/EntityReferenceSelectionUnitTest.php b/core/tests/Drupal/Tests/Core/EntityReferenceSelection/EntityReferenceSelectionUnitTest.php new file mode 100644 index 0000000000..deb7370d44 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/EntityReferenceSelection/EntityReferenceSelectionUnitTest.php @@ -0,0 +1,296 @@ +errors[] = compact('severity', 'message', 'file', 'line', 'context'); + }); + } + + /** + * Tests invalid default configuration. + * + * @covers ::defaultConfiguration + * @covers ::resolveBackwardCompatibilityConfiguration + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage TestSelectionWithInvalidDefaultConfiguration::defaultConfiguration() should not contain a 'handler_settings' key. All settings should be placed in the root level. + */ + public function testInvalidDefaultConfiguration() { + new TestSelectionWithInvalidDefaultConfiguration( + [], + 'test_selector', + ['class' => 'TestSelectionWithInvalidDefaultConfiguration'] + ); + } + + /** + * Tests the selection handler with malformed 'handler_settings' value. + * + * @covers ::setConfiguration + * @covers ::resolveBackwardCompatibilityConfiguration + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The setting 'handler_settings' is reserved and cannot be used. + */ + public function testMalformedHandlerSettingsValue() { + new TestSelection( + // The deprecated 'handler_setting' should be an array. + ['handler_settings' => FALSE], + 'test_selector', + ['class' => 'TestSelectionWithInvalidDefaultConfiguration'] + ); + } + + /** + * Provides test data for ::testSetConfiguration() + * + * @return array + * + * @see \Drupal\Tests\Core\EntityReferenceSelection\testSetConfiguration + */ + public function providerTestSetConfiguration() { + return [ + [ + [ + 'setting1' => 'foo', + 'setting2' => [ + 'bar' => 'bar value', + 'baz' => 'baz value', + ], + ], + ], + [ + [ + 'handler_settings' => [ + 'setting1' => 'foo', + 'setting2' => [ + 'bar' => 'bar value', + 'baz' => 'baz value', + ], + ], + ], + ], + [ + [ + 'setting1' => 'foo', + 'handler_settings' => [ + 'setting2' => [ + 'bar' => 'bar value', + 'baz' => 'baz value', + ], + ] + ], + ], + [ + [ + 'setting1' => 'foo', + 'setting2' => [ + 'bar' => 'bar value', + 'baz' => 'baz value', + ], + 'handler_settings' => [ + // Same setting from root level takes precedence. + 'setting2' => 'this will be overwritten', + ] + ], + ], + ]; + } + + /** + * Tests selection handler plugin configuration set. + * + * @dataProvider providerTestSetConfiguration + * @covers ::setConfiguration + * @covers ::resolveBackwardCompatibilityConfiguration + * @covers ::ensureBackwardCompatibilityConfiguration + * + * @param array $options + * The configuration passed to the plugin. + */ + public function testSetConfiguration($options) { + $selection = new TestSelection($options, 'test_selector', []); + + $expected = [ + 'target_type' => NULL, + 'handler' => 'test_selector', + 'entity' => NULL, + 'setting1' => 'foo', + 'setting2' => [ + 'qux' => 'qux value', + 'bar' => 'bar value', + 'baz' => 'baz value', + ], + 'setting3' => 'foobar', + 'handler_settings' => [ + 'setting1' => 'foo', + 'setting2' => [ + 'qux' => 'qux value', + 'bar' => 'bar value', + 'baz' => 'baz value', + ], + 'setting3' => 'foobar', + ], + ]; + + $this->assertArrayEquals($expected, $selection->getConfiguration()); + } + + /** + * Tests the selection handler plugin BC structure. + * + * @covers ::setConfiguration + * @covers ::resolveBackwardCompatibilityConfiguration + * @covers ::ensureBackwardCompatibilityConfiguration + */ + public function testSetConfigurationBcLevel() { + $config = [ + 'target_type' => 'some_entity_type_id', + 'handler' => 'test_selector', + 'setting1' => 'foo', + ]; + $selection = new TestSelection($config, 'test_selector', []); + + $expected = [ + 'target_type' => 'some_entity_type_id', + 'handler' => 'test_selector', + 'entity' => NULL, + 'setting1' => 'foo', + 'setting2' => ['qux' => 'qux value'], + 'setting3' => 'foobar', + 'handler_settings' => [ + 'setting1' => 'foo', + 'setting2' => ['qux' => 'qux value'], + 'setting3' => 'foobar', + ], + ]; + + $this->assertArrayEquals($expected, $selection->getConfiguration()); + + // Read the stored values and override a setting. + $config = $selection->getConfiguration(); + $config['setting1'] = 'bar'; + $selection->setConfiguration($config); + $expected['setting1'] = 'bar'; + $expected['handler_settings']['setting1'] = 'bar'; + + $this->assertArrayEquals($expected, $selection->getConfiguration()); + } + + /** + * Tests deprecation error triggering. + * + * @covers ::setConfiguration + * @covers ::resolveBackwardCompatibilityConfiguration + */ + public function testDeprecationErrorTriggering() { + // Configuration with no BC level. + $config = ['setting1' => TRUE]; + new TestSelection($config, 'test_selector', []); + // Check that deprecation error has not been triggered. + $this->assertNoError("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.", E_USER_DEPRECATED); + + // Configuration with BC level. + $config = ['handler_settings' => ['setting1' => TRUE]]; + new TestSelection($config, 'test_selector', []); + // Check that deprecation error has been triggered. + $this->assertError("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.", E_USER_DEPRECATED); + } + + /** + * Asserts that an error has been triggered. + * + * @param string $message + * The error message. + * @param int $severity + * The error severity. + */ + protected function assertError($message, $severity) { + $assertion_message = "Error '$message' (severity $severity) was triggered."; + foreach ($this->errors as $error) { + if ($error['message'] === $message && $error['severity'] === $severity) { + $this->assertTrue(TRUE, $assertion_message); + return; + } + } + $this->fail($assertion_message); + } + + /** + * Asserts that a specific error has not been triggered. + * + * @param string $message + * The error message. + * @param int $severity + * The error severity. + */ + protected function assertNoError($message, $severity) { + $assertion_message = "Error '$message' (severity $severity) was not triggered."; + foreach ($this->errors as $error) { + if ($error['message'] === $message && $error['severity'] === $severity) { + $this->fail($assertion_message); + return; + } + } + $this->assertTrue(TRUE, $assertion_message); + } + +} + +/** + * Provides a testing plugin. + */ +class TestSelection extends SelectionPluginBase { + + public function defaultConfiguration() { + return [ + 'setting2' => [ + 'qux' => 'qux value', + ], + 'setting3' => 'foobar', + ] + parent::defaultConfiguration(); + } + + public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { } + + public function validateReferenceableEntities(array $ids) { } + + public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS') { } + +} + +/** + * Provides a testing plugin with invalid default configuration. + */ +class TestSelectionWithInvalidDefaultConfiguration extends TestSelection { + + public function defaultConfiguration() { + return [ + 'handler_settings' => ['foo' => 'bar'], + ]; + } + +}