association-1.0.0-alpha2/src/Plugin/EntityReferenceSelection/AssociatedEntityReferenceSelector.php
src/Plugin/EntityReferenceSelection/AssociatedEntityReferenceSelector.php
<?php namespace Drupal\association\Plugin\EntityReferenceSelection; use Drupal\association\AssociationNegotiatorInterface; use Drupal\association\Entity\AssociationInterface; use Drupal\association\EntityAdapterManagerInterface; use Drupal\association\Plugin\Derivative\AssociatedEntityReferenceSelectorDeriver; use Drupal\Component\Plugin\Definition\PluginDefinitionInterface; use Drupal\Component\Utility\Html; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\Attribute\EntityReferenceSelection; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginBase; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Reference selection plugin that provides entities from the same association. * * Allows entities to reference other entities from the same entity * associations. Does not allow creation of new associated content. */ #[EntityReferenceSelection( id: 'association_associated_entity', label: new TranslatableMarkup('Associated entity'), group: 'association_associated_entity', entity_types: [], weight: 1, deriver: AssociatedEntityReferenceSelectorDeriver::class, )] class AssociatedEntityReferenceSelector extends SelectionPluginBase implements ContainerFactoryPluginInterface { /** * Creates a new instance of the AssociatedEntityReferenceSelector class. * * @param array $configuration * The plugin configuraiton settings. * @param string $pluginId * The plugin identifier. * @param array|\Drupal\Component\Plugin\Definition\PluginDefinitionInterface $pluginDefinition * The entity reference selection plugin definition. * @param \Drupal\Core\Database\Connection $db * The default database connection. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entityRepo * The entity repository to manage loaded entity translations. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The entity type manager service. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager * The entity field manager service. * @param \Drupal\association\EntityAdapterManagerInterface $adapterManager * The entity adapter manager service. * @param \Drupal\association\AssociationNegotiatorInterface $assocNegotiator * The association negotiator service. */ public function __construct( array $configuration, string $pluginId, array|PluginDefinitionInterface $pluginDefinition, protected Connection $db, protected EntityRepositoryInterface $entityRepo, protected EntityTypeManagerInterface $entityTypeManager, protected EntityFieldManagerInterface $entityFieldManager, protected EntityAdapterManagerInterface $adapterManager, protected AssociationNegotiatorInterface $assocNegotiator, ) { parent::__construct($configuration, $pluginId, $pluginDefinition); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('database'), $container->get('entity.repository'), $container->get('entity_type.manager'), $container->get('entity_field.manager'), $container->get('strategy.manager.association.entity_adapter'), $container->get('association.negotiator') ); } /** * {@inheritdoc} */ public function defaultConfiguration(): array { return [ 'target_bundles' => NULL, 'sort' => [ 'field' => '_none', 'dir' => 'ASC', ], ] + parent::defaultConfiguration(); } /** * {@inheritdoc} */ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0): array { $query = $this->buildQuery($match, $match_operator); if ($limit > 0) { $query->range(0, $limit); } $options = []; if ($ids = $query->execute()) { $targetType = $this->getConfiguration()['target_type']; $storage = $this->entityTypeManager->getStorage($targetType); foreach ($storage->loadMultiple($ids) as $entity_id => $entity) { $bundle = $entity->bundle(); $translated = $this->entityRepo->getTranslationFromContext($entity); $options[$bundle][$entity_id] = Html::escape($translated->label() ?? ''); } } return $options; } /** * {@inheritdoc} */ public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS'): int { return $this->buildQuery($match, $match_operator) ->count() ->execute(); } /** * {@inheritdoc} */ public function validateReferenceableEntities(array $ids): array { if ($ids) { $entityTypeId = $this->configuration['target_type']; $entityType = $this->entityTypeManager->getDefinition($entityTypeId); return $this->buildQuery() ->condition($entityType->getKey('id'), $ids, 'IN') ->execute(); } return []; } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { $form = parent::buildConfigurationForm($form, $form_state); // The settings form that incorporates this selection plugin is // normally a field definition settings form which is an EntityForm. To get // the field definition and field owning entity you can use // FormState::getFormObject() and then EntityForm::getEntity() to get the // field definition config instance. // Just check to ensure the expected types are being returned incase this // plugin is being embedded in another contexts. $config = $this->getConfiguration(); $targetType = $this->configuration['target_type']; $entityType = $this->entityTypeManager->getDefinition($targetType); $form['instructions'] = [ '#type' => 'container', '#attributes' => [ 'class' => ['messages'], ], '#markup' => $this->t('This handler is only designed to only return associated entities that belongs to the same associations as the entity that owns this field. When used outside of the association context, this selector will return no results.'), ]; if ('association_page' !== $targetType && $entityType->hasKey('bundle')) { $bundles = $this->adapterManager ->getAdapterByEntityType($targetType) ->getBundles(); $selectedBundles = array_intersect_key($bundles, $config['target_bundles'] ?? []); $sortableFields = $this->getSortableFields($entityType, $selectedBundles); $form['target_bundles'] = [ '#type' => 'checkboxes', '#title' => $this->t('@entity_type bundles', ['@entity_type' => $entityType->getLabel()]), '#options' => $bundles, '#element_validate' => [[static::class, 'elementValidateFilter']], '#default_value' => $selectedBundles, '#description' => $this->t('This entity reference selector will only find associated entities. Though bundle options are shown, they may produce no results when using bundles that are not available in the same associations.'), // Use a form process callback to build #ajax property properly and also // to avoid code duplication. // @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::fieldSettingsAjaxProcess() '#ajax' => TRUE, '#limit_validation_errors' => [], ]; } else { $sortableFields = $this->getSortableFields($entityType, [$targetType]); $form['target_bundles'] = [ '#type' => 'value', '#value' => NULL, ]; } $form['sort'] = [ '#type' => 'container', '#attributes' => [], '#tree' => TRUE, 'field' => [ '#type' => 'select', '#disabled' => empty($sortableFields), '#options' => $sortableFields, '#default_value' => $config['sort']['field'] ?? NULL, '#states' => [ 'visible' => [ ':input[name^="settings[handler_settings][target_bundles]["]' => ['checked' => TRUE], ], ], ], 'dir' => [ '#type' => 'select', '#title' => $this->t('Sort direction'), '#options' => [ 'ASC' => $this->t('Ascending'), 'DESC' => $this->t('Descending'), ], '#default_value' => $config['sort']['dir'] ?? 'ASC', '#states' => [ 'visible' => [ ':input[name^="settings[handler_settings][target_bundles]["]' => ['checked' => TRUE], ], ], ], ]; return $form; } /** * Gets an array of sortable field. * * The field name is the array value and the key is the field property label. * The return value is compatible with use for checkboxes or select form * element options. * * @param \Drupal\Core\Entity\EntityTypeInterface $entityType * The entity type of the entity to find sortable fields for. * @param array $bundles * The entity bundles to include for enumerating the sortable fields. * * @return array<string,\Stringable|string> * List of sortable field properties, with the field and property name as * the array key, and a display label to use as the array value. */ protected function getSortableFields(EntityTypeInterface $entityType, array $bundles): array { $sortable = []; if ($entityType->entityClassImplements(FieldableEntityInterface::class)) { /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $bundleFields */ $bundleFields = []; foreach ($bundles as $bundle) { // Gather all the fields from the selected bundles. We're only finding // sortable fields based on field storage properties, the same field // in multiple bundles only needs to be processed once. $bundleFields += $this->entityFieldManager->getFieldDefinitions($entityType->id(), $bundle); } foreach ($bundleFields as $fieldName => $fieldDef) { $storageDef = $fieldDef->getFieldStorageDefinition(); // Only non-computed single value fields can be sorted correctly. if (1 !== $storageDef->getCardinality() || $fieldDef->isComputed()) { continue; } $columns = $storageDef->getColumns(); // If there is more than one column, display them all, otherwise just // display the field label. if (count($columns) > 1) { $propNames = $storageDef->getPropertyNames(); foreach ($columns as $column => $columnDef) { $sortable[$fieldName . '.' . $column] = $this->t('@label (@column)', [ '@label' => $fieldDef->getLabel(), '@column' => $propNames[$column] ?? $column, ]); } } else { $sortable[$fieldName] = $fieldDef->getLabel(); } } } return $sortable; } /** * Form element validation handler; Filters the #value property of an element. */ public static function elementValidateFilter(&$element, FormStateInterface $form_state) { $element['#value'] = array_filter($element['#value']); $form_state->setValueForElement($element, $element['#value']); } /** * Create and entity query to filter and/or fetch referenceable entities. * * @param string|null $match * The search text to match. * @param string $operator * The string match operation if a string match to match is provided. * * @return \Drupal\Core\Entity\Query\QueryInterface * They entity query to fetch and filter the entities that are valid for * this reference selector plugin. */ protected function buildQuery(?string $match = NULL, string $operator = 'CONTAINS'): QueryInterface { [ 'entity' => $entity, 'target_type' => $targetType, 'target_bundles' => $bundles, 'sort' => $sort, ] = $this->configuration; $entityType = $this->entityTypeManager->getDefinition($targetType); $query = $this->entityTypeManager ->getStorage($targetType) ->getQuery() ->accessCheck(TRUE); $assoc = $entity instanceof AssociationInterface ? $entity : $this->assocNegotiator->byEntity($entity); if (!$assoc && [] === $bundles) { // No association or no bundles so force query to return empty results. return $query->condition($entityType->getKey('id'), NULL, '='); } // If 'target_bundles' is NULL, all bundles are referenceable, skip this. if (is_array($bundles) && $bundles) { if (!$entityType->hasKey('bundle')) { // If 'target_bundle' is set and entity type doesn't support bundles. throw new UnsupportedEntityTypeDefinitionException(\sprintf( "Trying to use non-empty 'target_bundle' configuration on entity type '%s' without bundle support.", $entityType->id(), )); } // For single bundle values it's better to use the equals comparison. count($bundles) === 1 ? $query->condition($entityType->getKey('bundle'), reset($bundles)) : $query->condition($entityType->getKey('bundle'), $bundles, 'IN'); } if (isset($match) && $labelKey = $entityType->getKey('label')) { $query->condition($labelKey, $match, $operator); } if (isset($sort['field']) && $sort['field'] !== '_none') { $query->sort($sort['field'], $sort['dir'] ?? 'ASC'); } // Add the tag/metadata for entity access and the entity_reference query. return $query ->addTag($targetType . '_access') ->addTag('entity_reference') ->addMetaData('entity_reference_selection_handler', $this); } /** * {@inheritdoc} */ public function entityQueryAlter(SelectInterface $query): void { $handler = $query->getMetaData('entity_reference_selection_handler'); if ($handler instanceof AssociatedEntityReferenceSelector) { ['target_type' => $targetType, 'entity' => $entity] = $handler->getConfiguration(); $linkType = $this->entityTypeManager->getDefinition('association_link'); $assoc = $entity instanceof AssociationInterface ? $entity : $this->assocNegotiator->byEntity($entity); $subquery = $this->db->select($linkType->getBaseTable(), 'assoc_link') ->fields('assoc_link', ['id']) ->condition('association', $assoc->id()) ->condition('entity_type', $targetType) ->where('`base_table`.`nid` = `assoc_link`.`target`'); $query->exists($subquery); } } }