xero_sync-8.x-1.x-dev/src/XeroSyncSynchronizer.php
src/XeroSyncSynchronizer.php
<?php
namespace Drupal\xero_sync;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\xero\TypedData\XeroComplexItemInterface;
use Drupal\xero_sync\Exception\SyncFailureException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\xero_sync\Event\XeroSyncEvents;
use Drupal\xero_sync\Event\XeroSyncPropertiesEvent;
/**
* A class that synchronizes Drupal entities and Xero items.
*/
class XeroSyncSynchronizer implements XeroSyncSynchronizerInterface {
/**
* The Xero item finder.
*
* @var \Drupal\xero_sync\XeroSyncItemFinder
*/
protected $itemFinder;
/**
* The Xero item manager.
*
* @var \Drupal\xero\XeroItemManagerInterface
*/
protected $itemManager;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The typed data manager.
*
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
/**
* An array of entity types and ids, to check for recursion.
*
* @var array
*/
protected static $recursionChecks;
/**
* Constructs a new XeroSyncSynchronizer object.
*
* @param \Drupal\xero_sync\XeroSyncItemFinderInterface $item_finder
* The Xero item finder.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
* The typed data manager.
* @param \Drupal\xero\XeroItemManagerInterface|null $item_manager
* (optional) The Xero item manager.
*/
public function __construct(XeroSyncItemFinderInterface $item_finder, EventDispatcherInterface $event_dispatcher, TypedDataManagerInterface $typed_data_manager, $item_manager = NULL) {
$this->itemFinder = $item_finder;
$this->eventDispatcher = $event_dispatcher;
$this->typedDataManager = $typed_data_manager;
$this->itemManager = $item_manager;
}
/**
* {@inheritDoc}
*/
public function syncEntityWithItem(
EntityInterface $entity,
XeroComplexItemInterface $item = NULL,
$create = TRUE,
$update = TRUE,
$previousEntityState = NULL
) {
if ($this->isRecursing($entity)) {
return;
}
$this->setRecursionCheck($entity);
try {
if (is_null($item)) {
$item = $this->itemFinder->getItem($entity, $create);
}
// Not finding an item is perfectly legitimate, as sometimes we might not
// want to sync.
if (!empty($item)) {
if ($update) {
try {
// If the update fails, keep using the previous item.
$item = $this->updateSyncedItemIfNecessary($entity, $item, FALSE, $previousEntityState);
}
// Catch in order to sync to the entity regardless of success.
catch (\Throwable $caughtError) {
}
}
$this->syncToEntityFromItem($entity, $item);
if (isset($caughtError)) {
throw $caughtError;
}
}
}
catch (\Throwable $e) {
$this->unsetRecursionCheck($entity);
throw $e;
}
$this->unsetRecursionCheck($entity);
return $item;
}
/**
* {@inheritDoc}
*/
public function unsyncEntityWithItem(EntityInterface $entity, XeroComplexItemInterface $item = NULL) {
if (is_null($item)) {
$item = $this->itemFinder->getItem($entity, FALSE);
}
if (!is_null($item)) {
$item = $this->updateSyncedItemIfNecessary($entity, $item, TRUE);
}
return $item;
}
/**
* {@inheritDoc}
*/
public function createSyncedItem(EntityInterface $entity, XeroComplexItemInterface $item = NULL, $xeroType = NULL) {
if (is_null($item)) {
if (is_null($xeroType)) {
throw new \InvalidArgumentException("When creating a synced item, either an item must be provided or the Xero type must be specified.");
}
$definition = $this->typedDataManager->createDataDefinition($xeroType);
$item = $this->typedDataManager->create($definition, []);
$item->stub = TRUE;
}
$event = $this->dispatchEvent(XeroSyncEvents::SYNC_TO_ITEM_FROM_ENTITY, $entity, $item);
$item = $event->getItem();
$createResult = $this->getItemManager()->throwErrors()->createItem($item);
if ($createResult) {
return $createResult;
}
throw new SyncFailureException("Creating a Xero item failed");
}
/**
* {@inheritDoc}
*/
public function updateSyncedItemIfNecessary(EntityInterface $entity, XeroComplexItemInterface $item, $unsync = FALSE, $previousEntityState = NULL) {
if (!is_bool($unsync)) {
throw new \InvalidArgumentException("Third argument to updateSyncedItem must be boolean.");
}
$needingUpdate = $this->mutateItemToUpdatedState($entity, $item, $unsync, $previousEntityState);
if ($needingUpdate) {
$item = $this->getItemManager()->throwErrors()->updateItem($needingUpdate);
if (!$item) {
throw new SyncFailureException("Updating a Xero item failed");
}
}
return $item;
}
/**
* {@inheritDoc}
*/
public function isItemUpdateNeeded(EntityInterface $entity, XeroComplexItemInterface $item, $previousEntityState = NULL) {
$needingUpdate = $this->mutateItemToUpdatedState($entity, $item, FALSE, $previousEntityState);
return ($needingUpdate !== FALSE);
}
/**
* {@inheritDoc}
*/
public function syncToEntityFromItem(EntityInterface $entity, XeroComplexItemInterface $item) {
$event = $this->dispatchEvent(XeroSyncEvents::SYNC_TO_ENTITY_FROM_ITEM, $entity, $item);
if ($event->isModified()) {
// Ensure event subscribers can modify the entity.
$entity = $event->getEntity();
$entity->save();
}
return $entity;
}
/**
* Update a typed data representation of a Xero item with needed changes.
*
* This method invokes XeroSync events that modify a supplied Xero item
* based on the state of a supplied entity. The modified item is returned, or
* FALSE if no modifications are needed.
*
* If the item is flagged as having been previously synced, and is a stub,
* and a previous state of the entity is supplied, then only modifications
* different from that previous state are considered.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to update the item from.
* @param \Drupal\xero\TypedData\XeroComplexItemInterface $item
* The Xero item to be updated, possibly a stub.
* @param bool $unsync
* (optional) Whether the item is being unsynced.
* @param \Drupal\Core\Entity\EntityInterface|null $previousEntityState
* (optional) The previous state of the entity, before the changes that
* are now being synchronised. Often from $entity->original.
*
* @return bool|\Drupal\xero\TypedData\XeroComplexItemInterface
* An updated typed data representation of the item, ready to send to Xero,
* or FALSE if the item does not need updating.
*/
protected function mutateItemToUpdatedState(EntityInterface $entity, XeroComplexItemInterface $item, $unsync = FALSE, $previousEntityState = NULL) {
$eventType = XeroSyncEvents::SYNC_TO_ITEM_FROM_ENTITY;
if ($unsync) {
$eventType = XeroSyncEvents::UNSYNC_ITEM;
}
// If we don't have an up-to-date representation of the item as it currently
// is on Xero, but we do know the previous state of the entity and that the
// item has been synced in the past, then we should use the item suggested
// by the previous state of the entity as the basis of comparison.
$isStub = isset($item->stub) && $item->stub;
$isSynced = isset($item->synced) && $item->synced;
if (!$unsync && $isStub && $isSynced && $previousEntityState) {
$event = $this->dispatchEvent($eventType, $previousEntityState, $item);
$item = $event->getItem();
$item->stub = FALSE;
}
$event = $this->dispatchEvent($eventType, $entity, $item);
if ($event->isModified()) {
$item = $event->getItem();
return $item;
}
return FALSE;
}
/**
* Detects whether this is a recursive attempts to synchronize an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being synchronized.
*/
protected function isRecursing(EntityInterface $entity) {
return isset(self::$recursionChecks[$entity->getEntityTypeId()]) && in_array($entity->id(), (self::$recursionChecks[$entity->getEntityTypeId()]));
}
/**
* Record an attempt to synchronize an entity, for futire recursion checking.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being synchronized.
*/
protected function setRecursionCheck(EntityInterface $entity) {
self::$recursionChecks[$entity->getEntityTypeId()][] = $entity->id();
}
/**
* Remove a recursion check that is no longer needed.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being synchronized.
*/
protected function unsetRecursionCheck(EntityInterface $entity) {
if (($key = array_search($entity->id(), self::$recursionChecks[$entity->getEntityTypeId()])) !== FALSE) {
unset(self::$recursionChecks[$entity->getEntityTypeId()][$key]);
}
}
/**
* Dispatch an event to allow subscribers to participate.
*
* @param string $eventType
* The event type.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to set on the event.
* @param \Drupal\xero\TypedData\XeroComplexItemInterface $item
* The item to set on the event.
*
* @return \Drupal\xero_sync\Event\XeroSyncPropertiesEvent
* The (possibly modified) event.
*/
protected function dispatchEvent($eventType, EntityInterface $entity, XeroComplexItemInterface $item) {
if (!isset($item->stub)) {
$item->stub = FALSE;
}
$event = new XeroSyncPropertiesEvent($entity, $item);
$eventId = $eventType . "." . $entity->getEntityTypeId();
$event = $this->eventDispatcher->dispatch($event, $eventId);
return $event;
}
/**
* Lazily get the item manager service.
*
* We don't add this as a dependency in xero_sync.services.yml, because there
* are valid uses of this service that don't depend on the item manager, and
* the item manager is only available if Xero is configured.
*
* @return \Drupal\xero\XeroItemManagerInterface
* The Xero ItemManager service.
*/
protected function getItemManager() {
if (is_null($this->itemManager)) {
$this->itemManager = \Drupal::service('xero.item_manager');
}
return $this->itemManager;
}
}
