contentserialize-8.x-1.x-dev/src/MissingReferenceFixer.php
src/MissingReferenceFixer.php
<?php
namespace Drupal\contentserialize;
use Drupal\bulkentity\EntityLoaderInterface;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Tracks references between entities that can't be set on import.
*
* It's possible that an entity is imported before all its dependencies (eg. if
* there's a cyclic refrence it can't be avoided). This object will track which
* entities are missing references to which others and will call a custom
* callback when the import's finished.
*
* @todo Add tests.
*/
class MissingReferenceFixer {
/**
* The batch size to use when loading referenced entities.
*
* @var int
*/
const BATCH_SIZE = 50;
/**
* @var array
*/
protected $targets = [];
/**
* Callbacks to fix missing entity references keyed by entity type and UUID.
*
* @var array
*/
protected $callbacks = [];
/**
* The entity loader
*
* @var \Drupal\bulkentity\EntityLoaderInterface
*/
protected $entityLoader;
/**
* Creates a new missing reference fixer.
*
* @param \Drupal\bulkentity\EntityLoaderInterface $entity_loader
* The bulk entity loader.
*/
public function __construct(EntityLoaderInterface $entity_loader) {
$this->entityLoader = $entity_loader;
}
/**
* Register that an imported entity has a reference to a non-existent entity.
*
* @param string $type
* The entity type ID of the referencing entity;
* @param string $uuid
* The UUID of the referencing entity;
* @param string $target_type
* The entity type ID of the missing referenced entity;
* @param $target_uuid
* The UUId of the missing referenced entity;
* @param callable $callback
* A callback that will fix the missing dependency; it takes three
* arguments:
* - the loaded referencing entity object;
* - the entity ID of the referenced entity;
* - the revision ID of the referenced entity (may be NULL).
*/
public function register($type, $uuid, $target_type, $target_uuid, callable $callback) {
$this->callbacks[$type][$uuid][] = [$callback, $target_type, $target_uuid];
$this->targets[$target_type][] = $target_uuid;
}
/**
* Fix all registered missing references.
*/
public function fix() {
$map = [];
$vids = [];
foreach ($this->loadReferencedEntities() as $entity) {
$map[$entity->uuid()] = $entity->id();
// @todo Remove the special casing of entity reference revisions once
// #2667748 lands.
$vid = $entity->getRevisionId();
if ($vid) {
$vids[$entity->uuid()] = $vid;
}
}
// @todo Verify that the referenced entity types match.
foreach ($this->loadReferencingEntities() as $entity) {
try {
foreach ($this->callbacks[$entity->getEntityTypeId()][$entity->uuid()] as $callback_data) {
list($callback,, $target_uuid) = $callback_data;
$callback($entity, $map[$target_uuid], $vids[$target_uuid] ?? NULL);
}
$entity->save();
}
catch (\Exception $e) {
watchdog_exception('contentserialize', $e);
}
}
}
/**
* Batch load referencing entities.
*
* @return \Generator|\Drupal\Core\Entity\ContentEntityInterface[]
*/
protected function loadReferencingEntities() {
// Although you can query by UUID with an entity query, the results won't
// map the serial ID to UUID so we need to load the entities.
foreach ($this->callbacks as $entity_type_id => $entities_data) {
$query = \Drupal::entityQuery($entity_type_id)
->condition('uuid', array_keys($entities_data), 'IN');
yield from $this->entityLoader->byQuery( static::BATCH_SIZE, $query);
}
}
/**
* Batch load referenced entities.
*
* @return \Generator|\Drupal\Core\Entity\ContentEntityInterface[]
*/
protected function loadReferencedEntities() {
foreach ($this->targets as $target_type => $uuids) {
$query = \Drupal::entityQuery($target_type)
->condition('uuid', $uuids, 'IN');
yield from $this->entityLoader->byQuery( static::BATCH_SIZE, $query);
}
}
/**
* Get the callbacks to run for a referencing entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The referencing entity
*
* @return \Generator
*/
protected function getCallbacks(ContentEntityInterface $entity) {
yield from $this->callbacks[$entity->getEntityTypeId()][$entity->uuid()];
}
}
