DefaultSelection.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. <?php
  2. namespace Drupal\Core\Entity\Plugin\EntityReferenceSelection;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Core\Database\Query\AlterableInterface;
  5. use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
  6. use Drupal\Core\Entity\EntityFieldManagerInterface;
  7. use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginBase;
  8. use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface;
  9. use Drupal\Core\Entity\EntityRepositoryInterface;
  10. use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
  11. use Drupal\Core\Entity\EntityTypeManagerInterface;
  12. use Drupal\Core\Entity\FieldableEntityInterface;
  13. use Drupal\Core\Extension\ModuleHandlerInterface;
  14. use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
  15. use Drupal\Core\Form\FormStateInterface;
  16. use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
  17. use Drupal\Core\Session\AccountInterface;
  18. use Drupal\user\EntityOwnerInterface;
  19. use Symfony\Component\DependencyInjection\ContainerInterface;
  20. /**
  21. * Default plugin implementation of the Entity Reference Selection plugin.
  22. *
  23. * Also serves as a base class for specific types of Entity Reference
  24. * Selection plugins.
  25. *
  26. * @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager
  27. * @see \Drupal\Core\Entity\Annotation\EntityReferenceSelection
  28. * @see \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
  29. * @see \Drupal\Core\Entity\Plugin\Derivative\DefaultSelectionDeriver
  30. * @see plugin_api
  31. *
  32. * @EntityReferenceSelection(
  33. * id = "default",
  34. * label = @Translation("Default"),
  35. * group = "default",
  36. * weight = 0,
  37. * deriver = "Drupal\Core\Entity\Plugin\Derivative\DefaultSelectionDeriver"
  38. * )
  39. */
  40. class DefaultSelection extends SelectionPluginBase implements ContainerFactoryPluginInterface, SelectionWithAutocreateInterface {
  41. use DeprecatedServicePropertyTrait;
  42. /**
  43. * {@inheritdoc}
  44. */
  45. protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
  46. /**
  47. * The entity type manager service.
  48. *
  49. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  50. */
  51. protected $entityTypeManager;
  52. /**
  53. * The entity field manager service.
  54. *
  55. * @var \Drupal\Core\Entity\EntityFieldManagerInterface
  56. */
  57. protected $entityFieldManager;
  58. /**
  59. * Entity type bundle info service.
  60. *
  61. * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
  62. */
  63. public $entityTypeBundleInfo;
  64. /**
  65. * The entity repository.
  66. *
  67. * @var \Drupal\Core\Entity\EntityRepositoryInterface
  68. */
  69. protected $entityRepository;
  70. /**
  71. * The module handler service.
  72. *
  73. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  74. */
  75. protected $moduleHandler;
  76. /**
  77. * The current user.
  78. *
  79. * @var \Drupal\Core\Session\AccountInterface
  80. */
  81. protected $currentUser;
  82. /**
  83. * Constructs a new DefaultSelection object.
  84. *
  85. * @param array $configuration
  86. * A configuration array containing information about the plugin instance.
  87. * @param string $plugin_id
  88. * The plugin_id for the plugin instance.
  89. * @param mixed $plugin_definition
  90. * The plugin implementation definition.
  91. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  92. * The entity manager service.
  93. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  94. * The module handler service.
  95. * @param \Drupal\Core\Session\AccountInterface $current_user
  96. * The current user.
  97. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
  98. * The entity field manager.
  99. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
  100. * The entity type bundle info service.
  101. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
  102. * The entity repository.
  103. */
  104. public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, EntityFieldManagerInterface $entity_field_manager = NULL, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, EntityRepositoryInterface $entity_repository = NULL) {
  105. parent::__construct($configuration, $plugin_id, $plugin_definition);
  106. $this->entityTypeManager = $entity_type_manager;
  107. $this->moduleHandler = $module_handler;
  108. $this->currentUser = $current_user;
  109. if (!$entity_field_manager) {
  110. @trigger_error('Calling DefaultSelection::__construct() with the $entity_field_manager argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
  111. $entity_field_manager = \Drupal::service('entity_field.manager');
  112. }
  113. $this->entityFieldManager = $entity_field_manager;
  114. if (!$entity_type_bundle_info) {
  115. @trigger_error('Calling DefaultSelection::__construct() with the $entity_type_bundle_info argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
  116. $entity_type_bundle_info = \Drupal::service('entity_type.bundle.info');
  117. }
  118. $this->entityTypeBundleInfo = $entity_type_bundle_info;
  119. if (!$entity_repository) {
  120. @trigger_error('Calling DefaultSelection::__construct() with the $entity_repository argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
  121. $entity_repository = \Drupal::service('entity.repository');
  122. }
  123. $this->entityRepository = $entity_repository;
  124. }
  125. /**
  126. * {@inheritdoc}
  127. */
  128. public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  129. return new static(
  130. $configuration,
  131. $plugin_id,
  132. $plugin_definition,
  133. $container->get('entity_type.manager'),
  134. $container->get('module_handler'),
  135. $container->get('current_user'),
  136. $container->get('entity_field.manager'),
  137. $container->get('entity_type.bundle.info'),
  138. $container->get('entity.repository')
  139. );
  140. }
  141. /**
  142. * {@inheritdoc}
  143. */
  144. public function defaultConfiguration() {
  145. return [
  146. // For the 'target_bundles' setting, a NULL value is equivalent to "allow
  147. // entities from any bundle to be referenced" and an empty array value is
  148. // equivalent to "no entities from any bundle can be referenced".
  149. 'target_bundles' => NULL,
  150. 'sort' => [
  151. 'field' => '_none',
  152. 'direction' => 'ASC',
  153. ],
  154. 'auto_create' => FALSE,
  155. 'auto_create_bundle' => NULL,
  156. ] + parent::defaultConfiguration();
  157. }
  158. /**
  159. * {@inheritdoc}
  160. */
  161. public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
  162. $form = parent::buildConfigurationForm($form, $form_state);
  163. $configuration = $this->getConfiguration();
  164. $entity_type_id = $configuration['target_type'];
  165. $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
  166. $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
  167. if ($entity_type->hasKey('bundle')) {
  168. $bundle_options = [];
  169. foreach ($bundles as $bundle_name => $bundle_info) {
  170. $bundle_options[$bundle_name] = $bundle_info['label'];
  171. }
  172. natsort($bundle_options);
  173. $form['target_bundles'] = [
  174. '#type' => 'checkboxes',
  175. '#title' => $entity_type->getBundleLabel(),
  176. '#options' => $bundle_options,
  177. '#default_value' => (array) $configuration['target_bundles'],
  178. '#required' => TRUE,
  179. '#size' => 6,
  180. '#multiple' => TRUE,
  181. '#element_validate' => [[get_class($this), 'elementValidateFilter']],
  182. '#ajax' => TRUE,
  183. '#limit_validation_errors' => [],
  184. ];
  185. $form['target_bundles_update'] = [
  186. '#type' => 'submit',
  187. '#value' => $this->t('Update form'),
  188. '#limit_validation_errors' => [],
  189. '#attributes' => [
  190. 'class' => ['js-hide'],
  191. ],
  192. '#submit' => [[EntityReferenceItem::class, 'settingsAjaxSubmit']],
  193. ];
  194. }
  195. else {
  196. $form['target_bundles'] = [
  197. '#type' => 'value',
  198. '#value' => [],
  199. ];
  200. }
  201. if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
  202. $fields = [];
  203. foreach (array_keys($bundles) as $bundle) {
  204. $bundle_fields = array_filter($this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle), function ($field_definition) {
  205. return !$field_definition->isComputed();
  206. });
  207. foreach ($bundle_fields as $field_name => $field_definition) {
  208. /* @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  209. $columns = $field_definition->getFieldStorageDefinition()->getColumns();
  210. // If there is more than one column, display them all, otherwise just
  211. // display the field label.
  212. // @todo: Use property labels instead of the column name.
  213. if (count($columns) > 1) {
  214. foreach ($columns as $column_name => $column_info) {
  215. $fields[$field_name . '.' . $column_name] = $this->t('@label (@column)', ['@label' => $field_definition->getLabel(), '@column' => $column_name]);
  216. }
  217. }
  218. else {
  219. $fields[$field_name] = $this->t('@label', ['@label' => $field_definition->getLabel()]);
  220. }
  221. }
  222. }
  223. $form['sort']['field'] = [
  224. '#type' => 'select',
  225. '#title' => $this->t('Sort by'),
  226. '#options' => $fields,
  227. '#ajax' => TRUE,
  228. '#empty_value' => '_none',
  229. '#sort_options' => TRUE,
  230. '#limit_validation_errors' => [],
  231. '#default_value' => $configuration['sort']['field'],
  232. ];
  233. if ($entity_type->hasKey('bundle')) {
  234. $form['sort']['field']['#states'] = [
  235. 'visible' => [
  236. ':input[name^="settings[handler_settings][target_bundles]["]' => ['checked' => TRUE],
  237. ],
  238. ];
  239. }
  240. $form['sort']['settings'] = [
  241. '#type' => 'container',
  242. '#attributes' => ['class' => ['entity_reference-settings']],
  243. '#process' => [[EntityReferenceItem::class, 'formProcessMergeParent']],
  244. ];
  245. if ($configuration['sort']['field'] != '_none') {
  246. $form['sort']['settings']['direction'] = [
  247. '#type' => 'select',
  248. '#title' => $this->t('Sort direction'),
  249. '#required' => TRUE,
  250. '#options' => [
  251. 'ASC' => $this->t('Ascending'),
  252. 'DESC' => $this->t('Descending'),
  253. ],
  254. '#default_value' => $configuration['sort']['direction'],
  255. ];
  256. }
  257. }
  258. $form['auto_create'] = [
  259. '#type' => 'checkbox',
  260. '#title' => $this->t("Create referenced entities if they don't already exist"),
  261. '#default_value' => $configuration['auto_create'],
  262. '#weight' => -2,
  263. ];
  264. if ($entity_type->hasKey('bundle')) {
  265. $bundles = array_intersect_key($bundle_options, array_filter((array) $configuration['target_bundles']));
  266. $form['auto_create_bundle'] = [
  267. '#type' => 'select',
  268. '#title' => $this->t('Store new items in'),
  269. '#options' => $bundles,
  270. '#default_value' => $configuration['auto_create_bundle'],
  271. '#access' => count($bundles) > 1,
  272. '#states' => [
  273. 'visible' => [
  274. ':input[name="settings[handler_settings][auto_create]"]' => ['checked' => TRUE],
  275. ],
  276. ],
  277. '#weight' => -1,
  278. ];
  279. }
  280. return $form;
  281. }
  282. /**
  283. * {@inheritdoc}
  284. */
  285. public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
  286. parent::validateConfigurationForm($form, $form_state);
  287. // If no checkboxes were checked for 'target_bundles', store NULL ("all
  288. // bundles are referenceable") rather than empty array ("no bundle is
  289. // referenceable" - typically happens when all referenceable bundles have
  290. // been deleted).
  291. if ($form_state->getValue(['settings', 'handler_settings', 'target_bundles']) === []) {
  292. $form_state->setValue(['settings', 'handler_settings', 'target_bundles'], NULL);
  293. }
  294. // Don't store the 'target_bundles_update' button value into the field
  295. // config settings.
  296. $form_state->unsetValue(['settings', 'handler_settings', 'target_bundles_update']);
  297. }
  298. /**
  299. * Form element validation handler; Filters the #value property of an element.
  300. */
  301. public static function elementValidateFilter(&$element, FormStateInterface $form_state) {
  302. $element['#value'] = array_filter($element['#value']);
  303. $form_state->setValueForElement($element, $element['#value']);
  304. }
  305. /**
  306. * {@inheritdoc}
  307. */
  308. public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
  309. $target_type = $this->getConfiguration()['target_type'];
  310. $query = $this->buildEntityQuery($match, $match_operator);
  311. if ($limit > 0) {
  312. $query->range(0, $limit);
  313. }
  314. $result = $query->execute();
  315. if (empty($result)) {
  316. return [];
  317. }
  318. $options = [];
  319. $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
  320. foreach ($entities as $entity_id => $entity) {
  321. $bundle = $entity->bundle();
  322. $options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label());
  323. }
  324. return $options;
  325. }
  326. /**
  327. * {@inheritdoc}
  328. */
  329. public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS') {
  330. $query = $this->buildEntityQuery($match, $match_operator);
  331. return $query
  332. ->count()
  333. ->execute();
  334. }
  335. /**
  336. * {@inheritdoc}
  337. */
  338. public function validateReferenceableEntities(array $ids) {
  339. $result = [];
  340. if ($ids) {
  341. $target_type = $this->configuration['target_type'];
  342. $entity_type = $this->entityTypeManager->getDefinition($target_type);
  343. $query = $this->buildEntityQuery();
  344. $result = $query
  345. ->condition($entity_type->getKey('id'), $ids, 'IN')
  346. ->execute();
  347. }
  348. return $result;
  349. }
  350. /**
  351. * {@inheritdoc}
  352. */
  353. public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
  354. $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
  355. $values = [
  356. $entity_type->getKey('label') => $label,
  357. ];
  358. if ($bundle_key = $entity_type->getKey('bundle')) {
  359. $values[$bundle_key] = $bundle;
  360. }
  361. $entity = $this->entityTypeManager->getStorage($entity_type_id)->create($values);
  362. if ($entity instanceof EntityOwnerInterface) {
  363. $entity->setOwnerId($uid);
  364. }
  365. return $entity;
  366. }
  367. /**
  368. * {@inheritdoc}
  369. */
  370. public function validateReferenceableNewEntities(array $entities) {
  371. return array_filter($entities, function ($entity) {
  372. $target_bundles = $this->getConfiguration()['target_bundles'];
  373. if (isset($target_bundles)) {
  374. return in_array($entity->bundle(), $target_bundles);
  375. }
  376. return TRUE;
  377. });
  378. }
  379. /**
  380. * Builds an EntityQuery to get referenceable entities.
  381. *
  382. * @param string|null $match
  383. * (Optional) Text to match the label against. Defaults to NULL.
  384. * @param string $match_operator
  385. * (Optional) The operation the matching should be done with. Defaults
  386. * to "CONTAINS".
  387. *
  388. * @return \Drupal\Core\Entity\Query\QueryInterface
  389. * The EntityQuery object with the basic conditions and sorting applied to
  390. * it.
  391. */
  392. protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
  393. $configuration = $this->getConfiguration();
  394. $target_type = $configuration['target_type'];
  395. $entity_type = $this->entityTypeManager->getDefinition($target_type);
  396. $query = $this->entityTypeManager->getStorage($target_type)->getQuery();
  397. // If 'target_bundles' is NULL, all bundles are referenceable, no further
  398. // conditions are needed.
  399. if (is_array($configuration['target_bundles'])) {
  400. // If 'target_bundles' is an empty array, no bundle is referenceable,
  401. // force the query to never return anything and bail out early.
  402. if ($configuration['target_bundles'] === []) {
  403. $query->condition($entity_type->getKey('id'), NULL, '=');
  404. return $query;
  405. }
  406. else {
  407. $query->condition($entity_type->getKey('bundle'), $configuration['target_bundles'], 'IN');
  408. }
  409. }
  410. if (isset($match) && $label_key = $entity_type->getKey('label')) {
  411. $query->condition($label_key, $match, $match_operator);
  412. }
  413. // Add entity-access tag.
  414. $query->addTag($target_type . '_access');
  415. // Add the Selection handler for system_query_entity_reference_alter().
  416. $query->addTag('entity_reference');
  417. $query->addMetaData('entity_reference_selection_handler', $this);
  418. // Add the sort option.
  419. if ($configuration['sort']['field'] !== '_none') {
  420. $query->sort($configuration['sort']['field'], $configuration['sort']['direction']);
  421. }
  422. return $query;
  423. }
  424. /**
  425. * Helper method: Passes a query to the alteration system again.
  426. *
  427. * This allows Entity Reference to add a tag to an existing query so it can
  428. * ask access control mechanisms to alter it again.
  429. */
  430. protected function reAlterQuery(AlterableInterface $query, $tag, $base_table) {
  431. // Save the old tags and metadata.
  432. // For some reason, those are public.
  433. $old_tags = $query->alterTags;
  434. $old_metadata = $query->alterMetaData;
  435. $query->alterTags = [$tag => TRUE];
  436. $query->alterMetaData['base_table'] = $base_table;
  437. $this->moduleHandler->alter(['query', 'query_' . $tag], $query);
  438. // Restore the tags and metadata.
  439. $query->alterTags = $old_tags;
  440. $query->alterMetaData = $old_metadata;
  441. }
  442. }