cms_content_sync-3.0.x-dev/src/Plugin/EntityReferenceHandlerBase.php

src/Plugin/EntityReferenceHandlerBase.php
<?php

namespace Drupal\cms_content_sync\Plugin;

use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\PullIntent;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Entity\TranslatableInterface;

use function t;

/**
 * Providing a base implementation for any reference field type.
 */
abstract class EntityReferenceHandlerBase extends FieldHandlerBase {

  /**
   * {@inheritdoc}
   */
  public function getHandlerSettings($current_values, $type = 'both') {
    $options = [];

    $referenced_entity_types = $this->getReferencedEntityTypes();
    if (!$this->forcePushingReferencedEntities()
      && !$this->forceEmbeddingReferencedEntities()
      && 'pull' !== $type
      && !in_array('view', $referenced_entity_types)
      && !in_array('classy_paragraphs_style', $referenced_entity_types)
      && !in_array('group_role', $referenced_entity_types)
      && !in_array('workflow_state', $referenced_entity_types)
      && !in_array('domain', $referenced_entity_types)) {
      $options['export_referenced_entities'] = [
        '#type' => 'checkbox',
        '#title' => 'Push referenced entities',
        '#default_value' => $current_values['export_referenced_entities'] ?? $this->shouldPushReferencedEntities(TRUE),
      ];
    }

    if ($this->allowSubscribeFilter() && $this->flow && 'push' !== $type) {
      $type = $this->fieldDefinition->getSetting('target_type');
      $bundles = $this->fieldHelper->getEntityReferenceFieldAllowedTargetBundles($this->fieldDefinition);

      global $config;
      $override = !empty($config['cms_content_sync.flow.' . $this->flow->id()]['per_bundle_settings'][$this->entityTypeName][$this->bundleName]['properties'][$this->fieldName]['handler_settings']['subscribe_only_to'])
                ? $config['cms_content_sync.flow.' . $this->flow->id()]['per_bundle_settings'][$this->entityTypeName][$this->bundleName]['properties'][$this->fieldName]['handler_settings']['subscribe_only_to']
                : (!empty($config['cms_content_sync.flow.' . $this->flow->id()]['sync_entities'][$this->entityTypeName . '-' . $this->bundleName . '-' . $this->fieldName]['handler_settings']['subscribe_only_to'])
            ? $config['cms_content_sync.flow.' . $this->flow->id()]['sync_entities'][$this->entityTypeName . '-' . $this->bundleName . '-' . $this->fieldName]['handler_settings']['subscribe_only_to']
            : NULL);

      $entities = [];
      $current = !empty($override) ? $override : (empty($current_values['subscribe_only_to']) ? NULL : $current_values['subscribe_only_to']);
      if (!empty($current)) {
        $storage = \Drupal::entityTypeManager()->getStorage($type);
        $repository = \Drupal::service('entity.repository');

        foreach ($current as $ref) {
          $entity = NULL;

          if (isset($ref['uuid'])) {
            $entity = $repository->loadEntityByUuid($ref['type'], $ref['uuid']);
          }
          elseif (isset($ref['target_id'])) {
            $entity = $storage->load($ref['target_id']);
          }

          if ($entity) {
            $entities[] = $entity;
          }
        }
      }

      $options['subscribe_only_to'] = [
        '#type' => 'entity_autocomplete',
            // The textfield component that the autocomplete inherits from sets this to 128 by default. We have no
            // restriction, so we set this to a very high number that can allow 100 terms.
        '#maxlength' => 4096,
        '#size' => 30,
        '#target_type' => $type,
        '#tags' => TRUE,
        '#selection_settings' => [
          'target_bundles' => $bundles,
        ],
        '#title' => 'Subscribe only to',
        '#disabled' => !empty($override),
        '#description' => !empty($override) ? $this->t('Value provided via settings.php.') : '',
        '#default_value' => $entities,
      ];
    }

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function validateHandlerSettings(array &$form, FormStateInterface $form_state, string $entity_type_name, string $bundle_name, string $field_name, $current_values) {
    if (!$this->shouldPushReferencedEntities() && !$this->shouldEmbedReferencedEntities()) {
      return;
    }

    $reference_types = $this->getReferencedEntityTypes();

    foreach ($current_values['per_bundle_settings'] as $entity_type => $bundles) {
      foreach ($bundles as $bundle => $config) {
        $settings = $config['settings'];

        // Ignore ignored configs.
        if (Flow::HANDLER_IGNORE == $settings['handler']) {
          continue;
        }

        $index = array_search($entity_type, $reference_types);

        // Ignore configs that don't match our entity type.
        if (FALSE === $index) {
          continue;
        }

        // This one has a push handler, so we can ignore it in further validation.
        unset($reference_types[$index]);
      }
    }

    // All referenced entities have a handler, so we're good.
    if (!count($reference_types)) {
      return;
    }

    // We are just about to load this element, so we don't have any form element available yet. Validation will be
    // triggered again when the form is submitted.
    if (empty($form[$this->entityTypeName][$this->bundleName]['properties'][$field_name]['handler'])) {
      return;
    }

    // No fitting handler was found- inform the user that they are missing some
    // configuration.
    if ($this->forcePushingReferencedEntities() || $this->forceEmbeddingReferencedEntities()) {
      $element = &$form[$this->entityTypeName][$this->bundleName]['properties'][$field_name]['handler'];
    }
    else {
      $element = &$form[$this->entityTypeName][$this->bundleName]['properties'][$field_name]['handler_settings']['export_referenced_entities'];
    }

    foreach ($reference_types as $type) {
      $form_state->setError(
            $element,
            t(
                'You want to push %referenced\'s that are referenced in %source automatically, but you have not defined any handler for this entity type. Please scroll to the bundles of this entity type, add a handler and set "push" to "referenced" there.',
                [
                  '%referenced' => $type,
                  '%source' => $entity_type_name . '.' . $bundle_name . '.' . $field_name
                ]
            )
        );
    }
  }

  /**
   * Get the referenced entity types from the field definition.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
   *   The field definition to get the referenced entity types from.
   *
   * @return array
   *   The referencable entity types.
   */
  public static function getReferencedEntityTypesFromFieldDefinition(FieldDefinitionInterface $fieldDefinition) {
    if ('dynamic_entity_reference' == $fieldDefinition->getFieldStorageDefinition()->getType()) {
      if ($fieldDefinition->getFieldStorageDefinition()->getSetting('exclude_entity_types')) {
        $entity_types = EntityHandlerPluginManager::getEntityTypes();

        $included = [];
        $excluded = $fieldDefinition->getFieldStorageDefinition()->getSetting('entity_type_ids');
        foreach ($entity_types as $entity_type) {
          if (!in_array($entity_type['entity_type'], $excluded)) {
            $included[] = $entity_type['entity_type'];
          }
        }

        return $included;
      }

      return $fieldDefinition->getFieldStorageDefinition()->getSetting('entity_type_ids');
    }

    if ('workflow' == $fieldDefinition->getFieldStorageDefinition()->getType()) {
      return ['workflow_state'];
    }

    $entity_definition = $fieldDefinition
      ->getFieldStorageDefinition()
      ->getPropertyDefinition('entity');

    if (empty($entity_definition)) {
      if ($fieldDefinition->getTargetEntityTypeId()) {
        return [$fieldDefinition->getTargetEntityTypeId()];
      }

      return [];
    }

    $reference_type = $entity_definition
      ->getTargetDefinition()
      ->getEntityTypeId();

    return [$reference_type];
  }

  /**
   * {@inheritdoc}
   */
  public function pull(PullIntent $intent) {
    $action = $intent->getAction();

    // Deletion doesn't require any action on field basis for static data.
    if (SyncIntent::ACTION_DELETE == $action) {
      return FALSE;
    }

    return $this->setValues($intent);
  }

  /**
   * {@inheritdoc}
   */
  public function push(PushIntent $intent) {
    $action = $intent->getAction();
    /**
     * @var \Drupal\Core\Entity\EntityInterface $entity
     */
    $entity = $intent->getEntity();

    // Deletion doesn't require any action on field basis for static data.
    if (SyncIntent::ACTION_DELETE == $action) {
      return FALSE;
    }

    $data = $entity->get($this->fieldName)->getValue();

    $result = [];

    foreach ($data as $delta => $value) {
      $reference = $this->loadReferencedEntityFromFieldValue($value);

      if (!$reference || $reference->uuid() == $intent->getUuid()) {
        continue;
      }

      unset($value['target_id']);

      $result[] = $this->serializeReference($intent, $reference, $value);
    }

    $intent->setProperty($this->fieldName, $result, $this->fieldDefinition->isRequired());

    return TRUE;
  }

  /**
   * Don't expose option, but force push.
   */
  protected function forcePushingReferencedEntities() {
    return FALSE;
  }

  /**
   * Don't expose option, but force push.
   */
  protected function forceEmbeddingReferencedEntities() {
    return FALSE;
  }

  /**
   * Check if referenced entities should be embedded automatically.
   *
   * @param bool $default
   *   Whether to get the default value (TRUE) if none is set yet.
   *
   * @return bool
   *   Returns true if the referenced entities should be embedded automatically.
   */
  protected function shouldEmbedReferencedEntities($default = FALSE) {
    if ($this->forceEmbeddingReferencedEntities()) {
      return TRUE;
    }

    if (isset($this->settings['handler_settings']['embed_referenced_entities'])) {
      return (bool) $this->settings['handler_settings']['embed_referenced_entities'];
    }

    if ($default) {
      return TRUE;
    }

    return FALSE;
  }

  /**
   * Check if referenced entities should be pushed automatically.
   *
   * @param bool $default
   *   Whether to get the default value (TRUE) if none is set yet.
   *
   * @return bool
   *   Returns true if the referenced entities should be pushed automatically.
   */
  protected function shouldPushReferencedEntities($default = FALSE) {
    // Not syndicating views.
    $getReferencedEntityTypes = $this->getReferencedEntityTypes();
    if (in_array('view', $getReferencedEntityTypes)) {
      return FALSE;
    }

    if ($this->forcePushingReferencedEntities()) {
      return TRUE;
    }

    if (isset($this->settings['handler_settings']['export_referenced_entities'])) {
      return (bool) $this->settings['handler_settings']['export_referenced_entities'];
    }

    if ($default) {
      return TRUE;
    }

    return FALSE;
  }

  /**
   * Check if referenced entities are allowed to be pushed.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   *
   * @return bool
   *   Returns true if referenced entities are allowed to be pushed.
   */
  protected function allowPushingReferencedEntities() {
    $referenced_entity_types = \Drupal::entityTypeManager()->getStorage($this->getReferencedEntityTypes());
    foreach ($referenced_entity_types as $referenced_entity_type) {
      if ($referenced_entity_type instanceof ConfigEntityStorage) {
        return FALSE;
      }
    }

    return TRUE;
  }

  /**
   * @return bool
   */
  protected function allowSubscribeFilter() {
    return FALSE;
  }

  /**
   * Get the referenced entity types.
   *
   * @return array
   *   The referencable entity types.
   */
  protected function getReferencedEntityTypes() {
    return self::getReferencedEntityTypesFromFieldDefinition($this->fieldDefinition);
  }

  /**
   * Load the entity that is either referenced or embedded by $definition.
   *
   * @param \Drupal\cms_content_sync\PullIntent $inent
   *   The pulled entity to load the reference from.
   * @param $definition
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   Returns the referenced entity.
   */
  protected function loadReferencedEntity(PullIntent $intent, $definition) {
    return $intent->loadEmbeddedEntity($definition);
  }

  /**
   * Provide a fallback value if the field is required and no value is provided.
   *
   * @return mixed
   */
  protected function getFallbackReference() {
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  protected function setValues(PullIntent $intent) {
    if ($intent->shouldMergeChanges() && !$this->forceMergeOverwrite()) {
      return FALSE;
    }
    /**
     * @var \Drupal\Core\Entity\EntityInterface $entity
     */
    $entity = $intent->getEntity();

    $data = $intent->getProperty($this->fieldName);

    $status = $intent->getEntityStatus();
    $language = $entity instanceof TranslatableInterface ? $entity->language()->getId() : 'und';
    $status->resetMissingReferences($language, $this->fieldName);

    $values = [];
    foreach ($data ?? [] as $value) {
      $reference = $this->loadReferencedEntity($intent, $value);

      if ($reference) {
        $info = $intent->getEmbeddedEntityData($value);

        $attributes = $this->getFieldValuesForReference($reference, $intent, $info);

        if (is_array($attributes)) {
          $values[] = array_merge($info, $attributes);
        }
        else {
          $values[] = $attributes;
        }
      }
      elseif (!$this->shouldEmbedReferencedEntities()) {
        // Shortcut: If it's just one value and a normal entity_reference field, the MissingDependencyManager will
        // directly update the field value of the entity and save it. Otherwise it will request a full pull of the
        // entity. So this saves some performance for simple references.
        if ('entity_reference' === $this->fieldDefinition->getType() && !$this->fieldDefinition->getFieldStorageDefinition()->isMultiple()) {
          $intent->saveUnresolvedDependency($value, $this->fieldName);
        }
        else {
          $intent->saveUnresolvedDependency($value);
        }

        $status->addMissingReference($language, $this->fieldName, $value);
      }
    }

    if (empty($values)) {
      if (!$this->isFieldRequired($intent)) {
        $entity->set($this->fieldName, NULL);
      }
      elseif ($default = $this->getFallbackReference()) {
        $entity->set($this->fieldName, $default);
      }
    }
    else {
      $entity->set($this->fieldName, $values);
    }

    return TRUE;
  }

  /**
   * Check for required fields.
   *
   * As not every field correctly declares its requirement.
   */
  protected function isFieldRequired(SyncIntent $intent) {
    if ($this->fieldDefinition->isRequired()) {
      return TRUE;
    }

    $entity = $intent->getEntity();
    if ($entity) {
      if ('media' == $entity->getEntityTypeId()) {
        if ('uid' == $this->fieldName || 'revision_user' == $this->fieldName) {
          return TRUE;
        }
      }
      elseif ('bibcite_reference' == $entity->getEntityTypeId()) {
        if ('uid' == $this->fieldName) {
          return TRUE;
        }
      }
    }

    return FALSE;
  }

  /**
   * Get the values to be set to the $entity->field_*.
   *
   * @param \Drupal\Core\Entity\EntityInterface $reference
   *   The referenced entity.
   * @param \Drupal\cms_content_sync\PullIntent $intent
   *   The pull intent.
   * @param array $info
   *   The embedded entity data.
   *
   * @return array
   *   The field values for the referenced entity.
   */
  protected function getFieldValuesForReference(EntityInterface $reference, PullIntent $intent, array $info) {
    if ('entity_reference_revisions' == $this->fieldDefinition->getType() || 'cohesion_entity_reference_revisions' == $this->fieldDefinition->getType()) {
      $reference_data = [
        'target_id' => $reference->id(),
      ];

      if ($reference instanceof RevisionableInterface || 'paragraph' == $reference->getEntityTypeId()) {
        $reference_data['target_revision_id'] = $reference->getRevisionId() ?? $reference->getLoadedRevisionId();
      }

      return $reference_data;
    }
    else {
      $attributes = [
        'target_id' => $reference->id(),
      ];
    }

    return $attributes;
  }

  /**
   * Load the referenced entity, given the $entity->field_* value.
   *
   * @param $value
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   *
   * @return null|\Drupal\Core\Entity\EntityInterface
   *   The referenced entity from the field value.
   */
  protected function loadReferencedEntityFromFieldValue($value) {
    if (empty($value['target_id'])) {
      return NULL;
    }

    $entityTypeManager = \Drupal::entityTypeManager();
    $reference_type = $value['target_type'] ?? $this->getReferencedEntityTypes()[0];

    $storage = $entityTypeManager
      ->getStorage($reference_type);

    $target_id = $value['target_id'];

    return $storage
      ->load($target_id);
  }

  /**
   * Get invalid subfields.
   */
  protected function getInvalidSubfields() {
    return [];
  }

  /**
   * Serialize the give reference.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   * @throws \GuzzleHttp\Exception\GuzzleException
   *
   * @return array|object
   *   The serialized reference.
   */
  protected function serializeReference(PushIntent $intent, EntityInterface $reference, $value) {
    foreach ($this->getInvalidSubfields() as $field) {
      unset($value[$field]);
    }
    foreach ($value as $key => $data) {
      if ('field_' == substr($key, 0, 6)) {
        unset($value[$key]);
      }
    }

    // Allow mapping by label.
    if ('taxonomy_term' == $reference->getEntityTypeId()) {
      $value['label'] = $reference->label();
    }

    if ($this->shouldEmbedReferencedEntities()) {
      return $intent->embed($reference, $value);
    }

    try {
      if ($reference->hasLinkTemplate('canonical')) {
        $view_url = $reference->toUrl('canonical', [
          'absolute' => TRUE,
          // Workaround for PathProcessorAlias::processOutbound to explicitly ignore us
          // as we always want the pure, unaliased e.g. /node/:id path because
          // we don't use the URL for end-users but for editors and it has to
          // be reliable (aliases can be removed or change).
          'alias' => TRUE,
        ] + ($reference instanceof TranslatableInterface ? ['language' => $reference->language()] : []))->toString();
      }
      elseif ($reference->hasLinkTemplate('edit-form')) {
        $view_url = $reference->toUrl('edit-form', [
          'absolute' => TRUE,
          // Workaround for PathProcessorAlias::processOutbound to explicitly ignore us
          // as we always want the pure, unaliased e.g. /node/:id path because
          // we don't use the URL for end-users but for editors and it has to
          // be reliable (aliases can be removed or change).
          'alias' => TRUE,
        ] + ($reference instanceof TranslatableInterface ? ['language' => $reference->language()] : []))->toString();
      }
      else {
        $view_url = NULL;
      }
    }
    catch (\Exception $e) {
      $view_url = NULL;
    }

    if ($this->shouldPushReferencedEntities()) {
      return $intent->addDependency($reference, $value, TRUE, $view_url);
    }

    return $intent->addReference(
          $reference,
          $value,
        $view_url
      );
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc