replication-8.x-1.x-dev/src/Normalizer/ContentEntityNormalizer.php

src/Normalizer/ContentEntityNormalizer.php
<?php

namespace Drupal\replication\Normalizer;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\file\FileInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\multiversion\Entity\Index\MultiversionIndexFactory;
use Drupal\multiversion\Entity\Storage\ContentEntityStorageInterface;
use Drupal\multiversion\Entity\WorkspaceInterface;
use Drupal\replication\Event\ReplicationContentDataAlterEvent;
use Drupal\replication\Event\ReplicationDataEvents;
use Drupal\replication\UsersMapping;
use Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait;
use Drupal\serialization\Normalizer\NormalizerBase;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

class ContentEntityNormalizer extends NormalizerBase implements DenormalizerInterface {

  use FieldableEntityNormalizerTrait;

  /**
   * @var string[]
   */
  protected $supportedInterfaceOrClass = ['Drupal\Core\Entity\ContentEntityInterface'];

  /**
   * @var \Drupal\multiversion\Entity\Index\MultiversionIndexFactory
   */
  protected $indexFactory;

  /**
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
   */
  protected $selectionManager;

  /**
   * @var \Drupal\replication\UsersMapping
   */
  protected $usersMapping;

  /**
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $dispatcher;

  /**
   * @var string[]
   */
  protected $format = ['json'];

  /**
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  private $moduleHandler;

  /**
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   * @param \Drupal\multiversion\Entity\Index\MultiversionIndexFactory $index_factory
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   * @param \Drupal\replication\UsersMapping $users_mapping
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   */
  public function __construct(EntityManagerInterface $entity_manager, MultiversionIndexFactory $index_factory, LanguageManagerInterface $language_manager, UsersMapping $users_mapping, ModuleHandlerInterface $module_handler, SelectionPluginManagerInterface $selection_manager = NULL, EventDispatcherInterface $event_dispatcher = NULL) {
    $this->entityManager = $entity_manager;
    $this->indexFactory = $index_factory;
    $this->languageManager = $language_manager;
    $this->usersMapping = $users_mapping;
    $this->moduleHandler = $module_handler;
    $this->selectionManager = $selection_manager;
    $this->dispatcher = $event_dispatcher;
  }

  /**
   * {@inheritdoc}
   */
  public function normalize($entity, $format = NULL, array $context = []) {
    $workspace = isset($entity->workspace->entity) ? $entity->workspace->entity : null;
    $rev_tree_index = $this->indexFactory->get('multiversion.entity_index.rev.tree', $workspace);

    $entity_type_id = $context['entity_type'] = $entity->getEntityTypeId();
    $entity_type = $this->entityManager->getDefinition($entity_type_id);

    $id_key = $entity_type->getKey('id');
    $revision_key = $entity_type->getKey('revision');
    $uuid_key = $entity_type->getKey('uuid');

    $entity_uuid = $entity->uuid();
    $entity_default_language = $entity->language();
    $entity_languages = $entity->getTranslationLanguages();

    // Create the basic data array with JSON-LD data.
    $data = [
      '@context' => [
        '_id' => '@id',
        '@language' => $entity_default_language->getId(),
      ],
      '@type' => $entity_type_id,
      '_id' => $entity_uuid,
    ];

    // New or mocked entities might not have a rev yet.
    if (!empty($entity->_rev->value)) {
      $data['_rev'] = $entity->_rev->value;
    }

    // Loop through each language of the entity
    $field_definitions = $entity->getFieldDefinitions();
    foreach ($entity_languages as $entity_language) {
      $translation = $entity->getTranslation($entity_language->getId());
      // Add the default language
      $data[$entity_language->getId()] =
        [
          '@context' => [
            '@language' => $entity_language->getId(),
          ]
        ];
      foreach ($translation as $name => $field) {
        // Add data for each field (through the field's normalizer.
        $field_type = $field_definitions[$name]->getType();
        $items = $this->serializer->normalize($field, $format, $context);
        if ($field_type == 'password') {
          continue;
        }
        $data[$entity_language->getId()][$name] = $items;
      }
      // Override the normalization for the _deleted special field, just so that we
      // follow the API spec.
      if (isset($translation->_deleted->value) && $translation->_deleted->value == TRUE) {
        $data[$entity_language->getId()]['_deleted'] = TRUE;
        $data['_deleted'] = TRUE;
      }
      elseif (isset($data[$entity_language->getId()]['_deleted'])) {
        unset($data[$entity_language->getId()]['_deleted']);
      }
    }

    // @todo: Needs test.}
    // Normalize the $entity->_rev->revisions value.
    if (!empty($entity->_rev->revisions)) {
      $data['_revisions']['ids'] = $entity->_rev->revisions;
      $data['_revisions']['start'] = count($data['_revisions']['ids']);
    }

    if (!empty($context['query']['conflicts'])) {
      $conflicts = $rev_tree_index->getConflicts($entity_uuid);
      foreach ($conflicts as $rev => $status) {
        $data['_conflicts'][] = $rev;
      }
    }

    // Finally we remove certain fields that are "local" to this host.
    unset($data['workspace'], $data[$id_key], $data[$revision_key], $data[$uuid_key]);
    foreach ($entity_languages as $entity_language) {
      $langcode = $entity_language->getId();
      unset($data[$langcode]['workspace'], $data[$langcode][$id_key], $data[$langcode][$revision_key], $data[$langcode][$uuid_key]);
    }

    $event = new ReplicationContentDataAlterEvent($entity, $data, $format, $context);
    $this->dispatcher->dispatch(ReplicationDataEvents::ALTER_CONTENT_DATA, $event);

    return $event->getData();
  }

  /**
   * {@inheritdoc}
   */
  public function denormalize($data, $class, $format = NULL, array $context = []) {
    // Make sure these values start as NULL
    $entity_type_id = NULL;
    $entity_uuid = NULL;
    $entity_id = NULL;

    // Get the default language of the entity
    $default_langcode = $data['@context']['@language'];
    // Get all of the configured languages of the site
    $site_languages = $this->languageManager->getLanguages();

    // Resolve the UUID.
    if (empty($entity_uuid) && !empty($data['_id'])) {
      $entity_uuid = $data['_id'];
    }
    else {
      throw new UnexpectedValueException('The uuid value is missing.');
    }

    // Resolve the entity type ID.
    if (isset($data['@type'])) {
      $entity_type_id = $data['@type'];
    }
    elseif (!empty($context['entity_type'])) {
      $entity_type_id = $context['entity_type'];
    }

    // Map data from the UUID index.
    // @todo: {@link https://www.drupal.org/node/2599938 Needs test.}
    if (!empty($entity_uuid)) {
      $uuid_index = (isset($context['workspace']) && ($context['workspace'] instanceof WorkspaceInterface)) ? $this->indexFactory->get('multiversion.entity_index.uuid', $context['workspace']) : $this->indexFactory->get('multiversion.entity_index.uuid');
      if ($record = $uuid_index->get($entity_uuid)) {
        $entity_id = $record['entity_id'];
        if (empty($entity_type_id)) {
          $entity_type_id = $record['entity_type_id'];
        }
        elseif ($entity_type_id != $record['entity_type_id']) {
          throw new UnexpectedValueException('The entity_type value does not match the existing UUID record.');
        }
      }
    }

    if (empty($entity_type_id)) {
      throw new UnexpectedValueException('The entity_type value is missing.');
    }

    // Add the _rev field to the $data array.
    $rev = null;
    if (isset($data['_rev'])) {
      $rev = $data['_rev'];
    }
    $revisions = [];
    if (isset($data['_revisions']['start']) && isset($data['_revisions']['ids'])) {
      $revisions = $data['_revisions'];
    }

    $entity_type = $this->entityManager->getDefinition($entity_type_id);
    $id_key = $entity_type->getKey('id');
    $revision_key = $entity_type->getKey('revision');
    $bundle_key = $entity_type->getKey('bundle');

    $translations = [];
    foreach ($data as $key => $translation) {
      // Skip any keys that start with '_' or '@'.
      if (in_array($key{0}, ['_', '@'])) {
        continue;
      }
      // When language is configured, undefined or not applicable go ahead with
      // denormalization.
      elseif (isset($site_languages[$key])
        || $key === LanguageInterface::LANGCODE_NOT_SPECIFIED
        || $key === LanguageInterface::LANGCODE_NOT_APPLICABLE) {
        $context['language'] = $key;
        $translations[$key] = $this->denormalizeTranslation($translation, $entity_id, $entity_uuid, $entity_type_id, $bundle_key, $entity_type, $id_key, $context, $rev, $revisions);
      }
      // Configure the language, then do denormalization.
      elseif (is_array($translation) && $this->moduleHandler->moduleExists('language')) {
        $language = ConfigurableLanguage::createFromLangcode($key);
        $language->save();
        $context['language'] = $key;
        $translations[$key] = $this->denormalizeTranslation($translation, $entity_id, $entity_uuid, $entity_type_id, $bundle_key, $entity_type, $id_key, $context, $rev, $revisions);
      }
    }

    // @todo {@link https://www.drupal.org/node/2599926 Use the passed $class to instantiate the entity.}

    $entity = NULL;
    if ($entity_id && $entity_type_id != 'file' && !empty($translations[$default_langcode])) {
      $entity = $this->createEntityInstance($translations[$default_langcode], $entity_type, $format, $context);
    }
    elseif ($entity_type_id == 'file' && !empty($translations[$default_langcode])) {
      unset($translations[$default_langcode][$id_key], $translations[$default_langcode][$revision_key]);
      $translations[$default_langcode]['status'][0]['value'] = FILE_STATUS_PERMANENT;
      $translations[$default_langcode]['uid'][0]['target_id'] = $this->usersMapping->getUidFromConfig();

      $entity = $this->createEntityInstance($translations[$default_langcode], $entity_type, $format, $context);
    }
    elseif (!empty($translations[$default_langcode])) {
      unset($translations[$default_langcode][$id_key], $translations[$default_langcode][$revision_key]);
      $entity = $this->createEntityInstance($translations[$default_langcode], $entity_type, $format, $context);
    }

    if ($entity instanceof ContentEntityInterface) {
      foreach ($site_languages as $site_language) {
        $langcode = $site_language->getId();
        if ($entity->language()->getId() != $langcode && isset($translations[$langcode]) && !$entity->hasTranslation($langcode)) {
          $entity->addTranslation($langcode, $translations[$langcode]);
        }
      }
    }

    if ($entity_id && $entity) {
      $entity->enforceIsNew(FALSE);
      $entity->setNewRevision(FALSE);
      $entity->_rev->is_stub = FALSE;
    }

    Cache::invalidateTags([$entity_type_id . '_list']);

    return $entity;
  }

  /**
   * @param $translation
   * @param int $entity_id
   * @param \string $entity_uuid
   * @param string $entity_type_id
   * @param $bundle_key
   * @param $entity_type
   * @param $id_key
   * @param $context
   * @param $rev
   * @param array $revisions
   * @return mixed
   */
  private function denormalizeTranslation($translation, $entity_id, $entity_uuid, $entity_type_id, $bundle_key, $entity_type, $id_key, $context, $rev = null, array $revisions = []) {
    // Add the _rev field to the $translation array.
    if (isset($rev)) {
      $translation['_rev'] = [['value' => $rev]];
    }
    if (isset($revisions['start']) && isset($revisions['ids'])) {
      $translation['_rev'][0]['revisions'] = $revisions['ids'];
    }
    if (isset($entity_uuid)) {
      $translation['uuid'][0]['value'] = $entity_uuid;
    }

    // We need to nest the data for the _deleted field in its Drupal-specific
    // structure since it's un-nested to follow the API spec when normalized.
    // @todo {@link https://www.drupal.org/node/2599938 Needs test for situation when a replication overwrites delete.}
    $deleted = isset($translation['_deleted']) ? $translation['_deleted'] : FALSE;
    $translation['_deleted'] = [['value' => $deleted]];

    if ($entity_id) {
      // @todo {@link https://www.drupal.org/node/2599938 Needs test.}
      $translation[$id_key] = ['value' => $entity_id];
    }

    $bundle_id = $entity_type_id;
    if ($entity_type->hasKey('bundle')) {
      if (!empty($translation[$bundle_key][0]['value'])) {
        // Add bundle info when entity is not new.
        $bundle_id = $translation[$bundle_key][0]['value'];
        $translation[$bundle_key] = $bundle_id;
      }
      elseif (!empty($translation[$bundle_key][0]['target_id'])) {
        // Add bundle info when entity is new.
        $bundle_id = $translation[$bundle_key][0]['target_id'];
        $translation[$bundle_key] = $bundle_id;
      }
    }

    // Denormalize entity reference fields.
    foreach ($translation as $field_name => $field_info) {
      if (!is_array($field_info)) {
        continue;
      }
      foreach ($field_info as $delta => $item) {
        if (!is_array($item)) {
          continue;
        }
        if (isset($item['target_uuid'])) {
          $translation[$field_name][$delta] = $item;
          $fields = $this->entityManager->getFieldDefinitions($entity_type_id, $bundle_id);
          // Figure out what bundle we should use when creating the stub.
          $settings = $fields[$field_name]->getSettings();

          // Find the target entity type and target bundle IDs and figure out if
          // the referenced entity exists or not.
          $target_entity_uuid = $item['target_uuid'];

          // Denormalize link field type as an entity reference field if it
          // has info about 'target_uuid' and 'entity_type_id'. These are used
          // to denormalize 'uri' in formats like 'entity:ENTITY_TYPE/ID'.
          $type = $fields[$field_name]->getType();
          if ($type == 'link' && isset($item['entity_type_id'])) {
            $target_entity_type_id = $item['entity_type_id'];
          }
          else {
            $target_entity_type_id = $settings['target_type'];
          }

          if ($target_entity_type_id === 'user') {
            $translation[$field_name] = $this->usersMapping->mapReferenceField($translation, $field_name);
            continue;
          }

          if (isset($settings['handler_settings']['target_bundles'])) {
            $target_bundle_id = reset($settings['handler_settings']['target_bundles']);
          }
          else {
            // @todo: Update when {@link https://www.drupal.org/node/2412569
            // this setting is configurable}.
            $bundles = $this->entityManager->getBundleInfo($target_entity_type_id);
            $target_bundle_id = key($bundles);
          }
          $target_entity = null;
          $uuid_index = (isset($context['workspace']) && ($context['workspace'] instanceof WorkspaceInterface)) ? $this->indexFactory->get('multiversion.entity_index.uuid', $context['workspace']) : $this->indexFactory->get('multiversion.entity_index.uuid');
          if ($target_entity_info = $uuid_index->get($target_entity_uuid)) {
            $target_entity = $this->entityManager
              ->getStorage($target_entity_info['entity_type_id'])
              ->load($target_entity_info['entity_id']);
          }

          // Try to get real bundle.
          if (!empty($item['entity_type_id'])) {
            $bundle_key = $this->entityManager->getStorage($item['entity_type_id'])->getEntityType()->getKey('bundle');
            if (!empty($item[$bundle_key])) {
              $target_bundle_id = $item[$bundle_key];
            }
          }

          // This set the correct uri for link field if the target entity
          // already exists.
          if ($type == 'link' && $target_entity) {
            $id = $target_entity->id();
            $translation[$field_name][$delta]['uri'] = "entity:$target_entity_type_id/$id";
          }
          elseif ($target_entity) {
            $translation[$field_name][$delta]['target_id'] = $target_entity->id();
            // Special handling for Entity Reference Revisions, it needs the
            // revision ID in addition to the primary entity ID.
            if ($type === 'entity_reference_revisions') {
              $revision_key = $target_entity->getEntityType()->getKey('revision');
              $translation[$field_name][$delta]['target_revision_id'] = $target_entity->{$revision_key}->value;
            }
          }
          // If the target entity doesn't exist we need to create a stub entity
          // in its place to ensure that the replication continues to work.
          // The stub entity will be updated when it's full entity comes around
          // later in the replication.
          else {
            $options['target_type'] = $target_entity_type_id;
            if (isset($settings['handler_settings'])) {
              $options['handler_settings'] = $settings['handler_settings'];
            }
            /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface $selection_instance */
            $selection_instance = $this->selectionManager->getInstance($options);
            // We use a temporary label and entity owner ID as this will be
            // backfilled later anyhow, when the real entity comes around.
            $target_entity = $selection_instance
              ->createNewEntity($target_entity_type_id, $target_bundle_id, rand(), 1);

            if (is_subclass_of($target_entity->getEntityType()->getStorageClass(), ContentEntityStorageInterface::class)) {
              // Set the target workspace if we have it in context.
              if (isset($context['workspace'])
                && ($context['workspace'] instanceof WorkspaceInterface)
                && $target_entity->getEntityType()->get('workspace') !== FALSE) {
                $target_entity->workspace->target_id = $context['workspace']->id();
              }
              // Set the UUID to what we received to ensure it gets updated when
              // the full entity comes around later.
              $target_entity->uuid->value = $target_entity_uuid;
              // Indicate that this revision is a stub.
              $target_entity->_rev->is_stub = TRUE;
              $target_entity->langcode->value = $context['language'];
              // Populate uri and filename fields if we have the info for them
              // in the field item.
              if ($target_entity instanceof FileInterface) {
                if (isset($item['uri'])) {
                  $target_entity->setFileUri($item['uri']);
                }
                if (isset($item['filename'])) {
                  $target_entity->setFilename($item['filename']);
                }
                if (isset($item['filesize'])) {
                  $target_entity->setSize($item['filesize']);
                }
                if (isset($item['filemime'])) {
                  $target_entity->setMimeType($item['filemime']);
                }
              }

              // This will ensure that stub poll_choice entities will not be saved
              // in Drupal\poll\Entity\Poll:preSave()
              if ($target_entity_type_id === 'poll_choice') {
                $target_entity->needsSaving(FALSE);
              }

              // Populate the data field.
              $translation[$field_name][$delta]['target_id'] = NULL;
              $translation[$field_name][$delta]['entity'] = $target_entity;
            }
          }
          if (isset($translation[$field_name][$delta]['entity_type_id'])) {
            unset($translation[$field_name][$delta]['entity_type_id']);
          }
          if (isset($translation[$field_name][$delta]['target_uuid'])) {
            unset($translation[$field_name][$delta]['target_uuid']);
          }
        }
      }
    }

    // Denormalize parent field for menu_link_content entity type.
    if ($entity_type_id == 'menu_link_content' && !empty($translation['parent'][0]['value'])) {
      $translation['parent'][0]['value'] = $this->denormalizeMenuLinkParent($translation['parent'][0]['value'], $context, $translation['@context']['@language']);
    }

    // Unset the comment field item if CID is NULL.
    if (!empty($translation['comment']) && is_array($translation['comment'])) {
      foreach ($translation['comment'] as $delta => $item) {
        if (empty($item['cid'])) {
          unset($translation['comment'][$delta]);
        }
      }
    }

    // Exclude "name" field (the user name) for comment entity type because
    // we'll change it during replication if it's a duplicate.
    if ($entity_type_id == 'comment' && isset($translation['name'])) {
      unset($translation['name']);
    }

    // Clean-up attributes we don't needs anymore.
    // Remove changed info, otherwise we can get validation errors when the
    // 'changed' value for existing entity is higher than for the new entity (revision).
    // @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraintValidator::validate().
    foreach (['@context', '@type', '_id', '_revisions', 'changed'] as $key) {
      if (isset($translation[$key])) {
        unset($translation[$key]);
      }
    }

    return $translation;
  }

  /**
   * Handles entity creation for fieldable and non-fieldable entities.
   *
   * This makes sure denormalization runs on field items.
   *
   * @param array $data
   * @param EntityTypeInterface $entity_type
   * @param $format
   * @param array $context
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *
   * @see \Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait
   */
  private function createEntityInstance(array $data, EntityTypeInterface $entity_type, $format, array $context = []) {
    // The bundle property will be required to denormalize a bundleable
    // fieldable entity.
    if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
      // Extract bundle data to pass into entity creation if the entity type uses
      // bundles.
      if ($entity_type->hasKey('bundle')) {
        // Get an array containing the bundle only. This also remove the bundle
        // key from the $data array.
        $create_params = $this->extractBundleData($data, $entity_type);
      }
      else {
        $create_params = [];
      }

      // Create the entity from bundle data only, then apply field values after.
      $entity = $this->entityManager->getStorage($entity_type->id())->create($create_params);
      $this->denormalizeFieldData($data, $entity, $format, $context);
    }
    else {
      // Create the entity from all data.
      $entity = $this->entityManager->getStorage($entity_type->id())->create($data);
    }

    return $entity;
  }

  /**
   * @param $data
   * @param $context
   * @param $langcode
   * @return string
   */
  protected function denormalizeMenuLinkParent($data, $context, $langcode) {
    if (strpos($data, 'menu_link_content') === 0) {
      list($type, $uuid, $id) = explode(':', $data);
      if ($type === 'menu_link_content' && $uuid && is_numeric($id)) {
        $storage = $this->entityManager->getStorage('menu_link_content');
        $parent = $storage->loadByProperties(['uuid' => $uuid]);
        $parent = reset($parent);
        if ($parent instanceof MenuLinkContentInterface && $parent->id() && $parent->id() != $id) {
          return $type . ':' . $uuid . ':' . $parent->id();
        }
        elseif (!$parent) {
          // Create a new menu link as stub.
          $parent = $storage->create([
            'uuid' => $uuid,
            'link' => 'internal:/',
            'langcode' => $langcode,
          ]);

          // Set the target workspace if we have it in context.
          if (isset($context['workspace'])
            && ($context['workspace'] instanceof WorkspaceInterface)) {
            $parent->workspace->target_id = $context['workspace']->id();
          }
          // Indicate that this revision is a stub.
          $parent->_rev->is_stub = TRUE;
          $parent->save();
          return $type . ':' . $uuid . ':' . $parent->id();
        }
      }
    }
    return $data;
  }

}

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

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