Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO;
class DateRange
{
public function __construct(
private ?\DateTimeInterface $fromDate,
private ?\DateTimeInterface $toDate,
) {
}
/**
* Get the value of fromDate.
*/
public function getFromDate()
{
return $this->fromDate;
}
/**
* Get the value of toDate.
*/
public function getToDate()
{
return $this->toDate;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Mapping;
class FieldMappingDAO
{
private bool $isRequired;
/**
* @param string $internalObject
* @param string $internalField
* @param string $integrationObject
* @param string $integrationField
* @param string $syncDirection
* @param bool $isRequired
*/
public function __construct(
private $internalObject,
private $internalField,
private $integrationObject,
private $integrationField,
private $syncDirection,
$isRequired,
) {
$this->isRequired = (bool) $isRequired;
}
/**
* @return string
*/
public function getInternalObject()
{
return $this->internalObject;
}
/**
* @return string
*/
public function getInternalField()
{
return $this->internalField;
}
/**
* @return string
*/
public function getIntegrationObject()
{
return $this->integrationObject;
}
/**
* @return string
*/
public function getIntegrationField()
{
return $this->integrationField;
}
/**
* @return string
*/
public function getSyncDirection()
{
return $this->syncDirection;
}
public function isRequired(): bool
{
return $this->isRequired;
}
}

View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Mapping;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
class MappingManualDAO
{
private array $objectsMapping = [];
private array $internalObjectsMapping = [];
private array $integrationObjectsMapping = [];
public function __construct(
private string $integration,
) {
}
public function getIntegration(): string
{
return $this->integration;
}
public function addObjectMapping(ObjectMappingDAO $objectMappingDAO): void
{
$internalObjectName = $objectMappingDAO->getInternalObjectName();
$integrationObjectName = $objectMappingDAO->getIntegrationObjectName();
if (!array_key_exists($internalObjectName, $this->objectsMapping)) {
$this->objectsMapping[$internalObjectName] = [];
}
$this->objectsMapping[$internalObjectName][$integrationObjectName] = $objectMappingDAO;
if (!array_key_exists($internalObjectName, $this->internalObjectsMapping)) {
$this->internalObjectsMapping[$internalObjectName] = [];
}
$this->internalObjectsMapping[$internalObjectName][] = $integrationObjectName;
if (!array_key_exists($integrationObjectName, $this->integrationObjectsMapping)) {
$this->integrationObjectsMapping[$integrationObjectName] = [];
}
$this->integrationObjectsMapping[$integrationObjectName][] = $internalObjectName;
}
public function getObjectMapping(string $internalObjectName, string $integrationObjectName): ?ObjectMappingDAO
{
if (!array_key_exists($internalObjectName, $this->objectsMapping)) {
return null;
}
if (!array_key_exists($integrationObjectName, $this->objectsMapping[$internalObjectName])) {
return null;
}
return $this->objectsMapping[$internalObjectName][$integrationObjectName];
}
/**
* @throws ObjectNotFoundException
*/
public function getMappedIntegrationObjectsNames(string $internalObjectName): array
{
if (!array_key_exists($internalObjectName, $this->internalObjectsMapping)) {
throw new ObjectNotFoundException($internalObjectName);
}
return $this->internalObjectsMapping[$internalObjectName];
}
/**
* @throws ObjectNotFoundException
*/
public function getMappedInternalObjectsNames(string $integrationObjectName): array
{
if (!array_key_exists($integrationObjectName, $this->integrationObjectsMapping)) {
throw new ObjectNotFoundException($integrationObjectName);
}
return $this->integrationObjectsMapping[$integrationObjectName];
}
public function getInternalObjectNames(): array
{
return array_keys($this->internalObjectsMapping);
}
/**
* Get a list of fields that should sync from Mautic to the integration.
*
* @throws ObjectNotFoundException
*/
public function getInternalObjectFieldsToSyncToIntegration(string $internalObjectName): array
{
if (!array_key_exists($internalObjectName, $this->internalObjectsMapping)) {
throw new ObjectNotFoundException($internalObjectName);
}
$fields = [];
$integrationObjectsNames = $this->internalObjectsMapping[$internalObjectName];
foreach ($integrationObjectsNames as $integrationObjectName) {
/** @var ObjectMappingDAO $objectMappingDAO */
$objectMappingDAO = $this->objectsMapping[$internalObjectName][$integrationObjectName];
$fieldMappings = $objectMappingDAO->getFieldMappings();
foreach ($fieldMappings as $fieldMapping) {
if (ObjectMappingDAO::SYNC_TO_MAUTIC === $fieldMapping->getSyncDirection() && !$fieldMapping->isRequired()) {
// Ignore because this field is a one way sync from the integration to Mautic nor is required
continue;
}
$fields[$fieldMapping->getInternalField()] = true;
}
}
return array_keys($fields);
}
/**
* Get a list of internal fields that are required.
*
* @throws ObjectNotFoundException
*/
public function getInternalObjectRequiredFieldNames(string $internalObjectName): array
{
if (!array_key_exists($internalObjectName, $this->internalObjectsMapping)) {
throw new ObjectNotFoundException($internalObjectName);
}
$fields = [];
$integrationObjectsNames = $this->internalObjectsMapping[$internalObjectName];
foreach ($integrationObjectsNames as $integrationObjectName) {
/** @var ObjectMappingDAO $objectMappingDAO */
$objectMappingDAO = $this->objectsMapping[$internalObjectName][$integrationObjectName];
$fieldMappings = $objectMappingDAO->getFieldMappings();
foreach ($fieldMappings as $fieldMapping) {
if (!$fieldMapping->isRequired()) {
continue;
}
$fields[$fieldMapping->getInternalField()] = true;
}
}
return array_keys($fields);
}
public function getIntegrationObjectNames(): array
{
return array_keys($this->integrationObjectsMapping);
}
/**
* Get a list of fields that should sync from the integration to Mautic.
*
* @throws ObjectNotFoundException
*/
public function getIntegrationObjectFieldsToSyncToMautic(string $integrationObjectName): array
{
if (!array_key_exists($integrationObjectName, $this->integrationObjectsMapping)) {
throw new ObjectNotFoundException($integrationObjectName);
}
$fields = [];
$internalObjectsNames = $this->integrationObjectsMapping[$integrationObjectName];
foreach ($internalObjectsNames as $internalObjectName) {
/** @var ObjectMappingDAO $objectMappingDAO */
$objectMappingDAO = $this->objectsMapping[$internalObjectName][$integrationObjectName];
$fieldMappings = $objectMappingDAO->getFieldMappings();
foreach ($fieldMappings as $fieldMapping) {
if (ObjectMappingDAO::SYNC_TO_INTEGRATION === $fieldMapping->getSyncDirection() && !$fieldMapping->isRequired()) {
// Ignore because this field is a one way sync from Mautic to the integration nor a required field
continue;
}
$fields[$fieldMapping->getIntegrationField()] = true;
}
}
return array_keys($fields);
}
/**
* Get a list of integration fields that are required.
*
* @throws ObjectNotFoundException
*/
public function getIntegrationObjectRequiredFieldNames(string $integrationObjectName): array
{
if (!array_key_exists($integrationObjectName, $this->integrationObjectsMapping)) {
throw new ObjectNotFoundException($integrationObjectName);
}
$fields = [];
$internalObjectsNames = $this->integrationObjectsMapping[$integrationObjectName];
foreach ($internalObjectsNames as $internalObjectName) {
/** @var ObjectMappingDAO $objectMappingDAO */
$objectMappingDAO = $this->objectsMapping[$internalObjectName][$integrationObjectName];
$fieldMappings = $objectMappingDAO->getFieldMappings();
foreach ($fieldMappings as $fieldMapping) {
if (!$fieldMapping->isRequired()) {
continue;
}
$fields[$fieldMapping->getIntegrationField()] = true;
}
}
return array_keys($fields);
}
/**
* @throws FieldNotFoundException
* @throws ObjectNotFoundException
*/
public function getIntegrationMappedField(string $integrationObjectName, string $internalObjectName, string $internalFieldName): string
{
if (!array_key_exists($internalObjectName, $this->internalObjectsMapping)) {
throw new ObjectNotFoundException($internalObjectName);
}
if (!array_key_exists($integrationObjectName, $this->objectsMapping[$internalObjectName])) {
throw new ObjectNotFoundException($integrationObjectName);
}
/** @var ObjectMappingDAO $objectMappingDAO */
$objectMappingDAO = $this->objectsMapping[$internalObjectName][$integrationObjectName];
$fieldMappings = $objectMappingDAO->getFieldMappings();
foreach ($fieldMappings as $fieldMapping) {
if ($fieldMapping->getInternalField() === $internalFieldName) {
return $fieldMapping->getIntegrationField();
}
}
throw new FieldNotFoundException($internalFieldName, $internalObjectName);
}
/**
* @throws FieldNotFoundException
* @throws ObjectNotFoundException
*/
public function getInternalMappedField(string $internalObjectName, string $integrationObjectName, string $integrationFieldName): string
{
if (!array_key_exists($internalObjectName, $this->internalObjectsMapping)) {
throw new ObjectNotFoundException($internalObjectName);
}
if (!array_key_exists($integrationObjectName, $this->objectsMapping[$internalObjectName])) {
throw new ObjectNotFoundException($integrationObjectName);
}
/** @var ObjectMappingDAO $objectMappingDAO */
$objectMappingDAO = $this->objectsMapping[$internalObjectName][$integrationObjectName];
$fieldMappings = $objectMappingDAO->getFieldMappings();
foreach ($fieldMappings as $fieldMapping) {
if ($fieldMapping->getIntegrationField() === $integrationFieldName) {
return $fieldMapping->getInternalField();
}
}
throw new FieldNotFoundException($integrationFieldName, $integrationObjectName);
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Mapping;
class ObjectMappingDAO
{
public const SYNC_TO_MAUTIC = 'mautic';
public const SYNC_TO_INTEGRATION = 'integration';
public const SYNC_BIDIRECTIONALLY = 'bidirectional';
private array $internalIdMapping = [];
private array $integrationIdMapping = [];
/**
* @var FieldMappingDAO[]
*/
private array $fieldMappings = [];
public function __construct(
private string $internalObjectName,
private string $integrationObjectName,
) {
}
/**
* @param string $internalField
* @param string $integrationField
* @param string $direction
* @param bool $isRequired
*/
public function addFieldMapping($internalField, $integrationField, $direction = self::SYNC_BIDIRECTIONALLY, $isRequired = false): self
{
$this->fieldMappings[] = new FieldMappingDAO(
$this->internalObjectName,
$internalField,
$this->integrationObjectName,
$integrationField,
$direction,
$isRequired
);
return $this;
}
/**
* @return FieldMappingDAO[]
*/
public function getFieldMappings(): array
{
return $this->fieldMappings;
}
public function getMappedIntegrationObjectId(int $internalObjectId): ?int
{
if (array_key_exists($internalObjectId, $this->internalIdMapping)) {
return $this->internalIdMapping[$internalObjectId];
}
return null;
}
/**
* @param mixed $integrationObjectId
*
* @return mixed|null
*/
public function getMappedInternalObjectId($integrationObjectId)
{
if (array_key_exists($integrationObjectId, $this->integrationIdMapping)) {
return $this->integrationIdMapping[$integrationObjectId];
}
return null;
}
public function getInternalObjectName(): string
{
return $this->internalObjectName;
}
public function getIntegrationObjectName(): string
{
return $this->integrationObjectName;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Mapping;
class RemappedObjectDAO
{
/**
* @param mixed $oldObjectId
* @param mixed $newObjectId
*/
public function __construct(
private string $integration,
private string $oldObjectName,
private $oldObjectId,
private string $newObjectName,
private $newObjectId,
) {
}
public function getIntegration(): string
{
return $this->integration;
}
public function getOldObjectName(): string
{
return $this->oldObjectName;
}
/**
* @return mixed
*/
public function getOldObjectId()
{
return $this->oldObjectId;
}
public function getNewObjectName(): string
{
return $this->newObjectName;
}
/**
* @return mixed
*/
public function getNewObjectId()
{
return $this->newObjectId;
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Mapping;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
class UpdatedObjectMappingDAO
{
private \DateTimeInterface $objectModifiedDate;
private ?ObjectMapping $objectMapping = null;
/**
* @param string $integration
* @param string $integrationObjectName
* @param mixed $integrationObjectId
*/
public function __construct(
private $integration,
private $integrationObjectName,
private $integrationObjectId,
\DateTimeInterface $objectModifiedDate,
) {
$this->objectModifiedDate = $objectModifiedDate instanceof \DateTimeImmutable ? new \DateTime(
$objectModifiedDate->format('Y-m-d H:i:s'),
$objectModifiedDate->getTimezone()
) : $objectModifiedDate;
}
public function getIntegration(): string
{
return $this->integration;
}
public function getIntegrationObjectName(): string
{
return $this->integrationObjectName;
}
/**
* @return mixed
*/
public function getIntegrationObjectId()
{
return $this->integrationObjectId;
}
public function getObjectModifiedDate(): \DateTimeInterface
{
return $this->objectModifiedDate;
}
public function setObjectMapping(ObjectMapping $objectMapping): void
{
$this->objectMapping = $objectMapping;
}
/**
* This is set after the ObjectMapping entity has been persisted to the database with the updates from this object.
*/
public function getObjectMapping(): ?ObjectMapping
{
return $this->objectMapping;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
class InformationChangeRequestDAO
{
private ?\DateTimeInterface $possibleChangeDateTime = null;
private ?\DateTimeInterface $certainChangeDateTime = null;
/**
* @param string $integration
* @param string $objectName
* @param mixed $objectId
* @param string $field
*/
public function __construct(
private $integration,
private $objectName,
private $objectId,
private $field,
private NormalizedValueDAO $newValue,
) {
}
public function getIntegration(): string
{
return $this->integration;
}
/**
* @return mixed
*/
public function getObjectId()
{
return $this->objectId;
}
public function getObject(): string
{
return $this->objectName;
}
public function getField(): string
{
return $this->field;
}
public function getNewValue(): NormalizedValueDAO
{
return $this->newValue;
}
public function getPossibleChangeDateTime(): ?\DateTimeInterface
{
return $this->possibleChangeDateTime;
}
public function setPossibleChangeDateTime(?\DateTimeInterface $possibleChangeDateTime = null): self
{
$this->possibleChangeDateTime = $possibleChangeDateTime;
return $this;
}
public function getCertainChangeDateTime(): ?\DateTimeInterface
{
return $this->certainChangeDateTime;
}
public function setCertainChangeDateTime(?\DateTimeInterface $certainChangeDateTime = null): self
{
$this->certainChangeDateTime = $certainChangeDateTime;
return $this;
}
}

View File

@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync;
use DateTimeInterface;
use Mautic\IntegrationsBundle\Exception\InvalidValueException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object\Contact;
class InputOptionsDAO
{
/**
* @var string
*/
private $integration;
private bool $firstTimeSync;
private bool $disablePush;
private bool $disablePull;
private bool $disableActivityPush;
private ?ObjectIdsDAO $mauticObjectIds;
private ?ObjectIdsDAO $integrationObjectIds;
private ?\DateTimeInterface $startDateTime;
private ?\DateTimeInterface $endDateTime;
private array $options;
/**
* Example $input:
* [
* 'integration' => 'Magento', // required
* 'first-time-sync' => true,
* 'disable-push' => false,
* 'disable-pull' => false,
* 'disable-activity-push' => false,
* 'mautic-object-id' => ['contact:12', 'contact:13'] or a ObjectIdsDAO object,
* 'integration-object-id' => ['Lead:hfskjdhf', 'Lead:hfskjdhr'] or a ObjectIdsDAO object,
* 'start-datetime' => '2019-09-12T12:01:20' or a DateTimeInterface object, Expecting UTC timezone
* 'end-datetime' => '2019-09-12T12:01:20' or a DateTimeInterface object, Expecting UTC timezone
* ].
*
* @throws InvalidValueException
*/
public function __construct(array $input)
{
if (empty($input['integration'])) {
throw new InvalidValueException('An integration must be specified. None provided.');
}
$input = $this->fixNaming($input);
$this->integration = $input['integration'];
$this->firstTimeSync = (bool) ($input['first-time-sync'] ?? false);
$this->disablePush = (bool) ($input['disable-push'] ?? false);
$this->disablePull = (bool) ($input['disable-pull'] ?? false);
$this->disableActivityPush = (bool) ($input['disable-activity-push'] ?? false);
$this->startDateTime = $this->validateDateTime($input, 'start-datetime');
$this->endDateTime = $this->validateDateTime($input, 'end-datetime');
$this->mauticObjectIds = $this->validateObjectIds($input, 'mautic-object-id');
$this->integrationObjectIds = $this->validateObjectIds($input, 'integration-object-id');
$this->options = $this->validateOptions($input);
}
public function getIntegration(): string
{
return $this->integration;
}
public function isFirstTimeSync(): bool
{
return $this->firstTimeSync;
}
public function pullIsEnabled(): bool
{
return !$this->disablePull;
}
public function activityPushIsEnabled(): bool
{
return !$this->disableActivityPush;
}
public function pushIsEnabled(): bool
{
return !$this->disablePush;
}
public function getMauticObjectIds(): ?ObjectIdsDAO
{
return $this->mauticObjectIds;
}
public function getIntegrationObjectIds(): ?ObjectIdsDAO
{
return $this->integrationObjectIds;
}
public function getStartDateTime(): ?\DateTimeInterface
{
return $this->startDateTime;
}
public function getEndDateTime(): ?\DateTimeInterface
{
return $this->endDateTime;
}
public function getOptions(): array
{
return $this->options;
}
/**
* @throws InvalidValueException
*/
private function validateDateTime(array $input, string $optionName): ?\DateTimeInterface
{
if (empty($input[$optionName])) {
return null;
}
if ($input[$optionName] instanceof \DateTimeInterface) {
return $input[$optionName];
} else {
try {
return is_string($input[$optionName]) ? new \DateTimeImmutable($input[$optionName], new \DateTimeZone('UTC')) : null;
} catch (\Throwable) {
throw new InvalidValueException("'$input[$optionName]' is not valid. Use 'Y-m-d H:i:s' format like '2018-12-24 20:30:00' or something like '-10 minutes'");
}
}
}
/**
* @throws InvalidValueException
*/
private function validateObjectIds(array $input, string $optionName): ?ObjectIdsDAO
{
if (empty($input[$optionName])) {
return null;
}
if ($input[$optionName] instanceof ObjectIdsDAO) {
return $input[$optionName];
} elseif (is_array($input[$optionName])) {
return ObjectIdsDAO::createFromCliOptions($input[$optionName]);
} else {
throw new InvalidValueException("{$optionName} option has an unexpected type. Use an array or ObjectIdsDAO object.");
}
}
/**
* This method exists only because Mautic leads were renamed to contacts. Users will be able
* to use the "contact" keywoard and developers "lead" as the integration bundle use "lead" everywhere.
*/
private function fixNaming(array $input): array
{
if (empty($input['mautic-object-id'])) {
return $input;
}
if (!is_array($input['mautic-object-id'])) {
return $input;
}
foreach ($input['mautic-object-id'] as $key => $mauticObjectId) {
$input['mautic-object-id'][$key] = preg_replace(
'/^contact:/',
Contact::NAME.':',
"$mauticObjectId"
);
}
return $input;
}
private function validateOptions(array $input): array
{
if (is_array($input['options'] ?? null)) {
return $input['options'];
}
$options = [];
if (is_array($input['option'] ?? null)) {
foreach ($input['option'] as $option) {
$parsedOption = explode(':', $option);
if (2 === count($parsedOption)) {
$options[$parsedOption[0]] = $parsedOption[1];
}
}
}
return $options;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
/**
* Holds IDs for different types of objects. Can be used for Mautic or integration objects.
*/
class ObjectIdsDAO
{
/**
* Expected structure:
* [
* 'objectA' => [12, 13],
* 'objectB' => ['asfdaswty', 'wetegdfsd'],
* ].
*
* @var array[]
*/
private array $objects = [];
/**
* Expected $cliOptions structure:
* [
* 'abjectA:12',
* 'abjectA:13',
* 'abjectB:asfdaswty',
* 'abjectB:wetegdfsd',
* ]
* Simply put, an array of object types and IDs separated by colon.
*
* @param string[] $cliOptions
*/
public static function createFromCliOptions(array $cliOptions): self
{
$objectsIdDAO = new self();
foreach ($cliOptions as $cliOption) {
if (is_string($cliOption) && str_contains($cliOption, ':')) {
$objectsIdDAO->addObjectId(...explode(':', $cliOption));
}
}
return $objectsIdDAO;
}
public function addObjectId(string $objectType, string $id): void
{
if (!isset($this->objects[$objectType])) {
$this->objects[$objectType] = [];
}
$this->objects[$objectType][] = $id;
}
/**
* @return string[]
*
* @throws ObjectNotFoundException
*/
public function getObjectIdsFor(string $objectType): array
{
if (empty($this->objects[$objectType])) {
throw new ObjectNotFoundException("Object {$objectType} doesn't have any IDs to return");
}
return $this->objects[$objectType];
}
/**
* @return string[]
*/
public function getObjectTypes(): array
{
return array_keys($this->objects);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Order;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
class FieldDAO
{
/**
* @param string $name
*/
public function __construct(
private $name,
private NormalizedValueDAO $value,
) {
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
public function getValue(): NormalizedValueDAO
{
return $this->value;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Order;
class NotificationDAO
{
public function __construct(
private ObjectChangeDAO $objectChangeDAO,
private string $message,
) {
}
/**
* @return ObjectChangeDAO
*/
public function getMauticObject(): string
{
return $this->objectChangeDAO->getMappedObject();
}
public function getMauticObjectId(): int
{
return (int) $this->objectChangeDAO->getMappedObjectId();
}
public function getIntegration(): string
{
return $this->objectChangeDAO->getIntegration();
}
public function getIntegrationObject(): string
{
return $this->objectChangeDAO->getObject();
}
/**
* @return mixed
*/
public function getIntegrationObjectId()
{
return $this->objectChangeDAO->getObjectId();
}
public function getMessage(): string
{
return $this->message;
}
}

View File

@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Order;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\FieldDAO as ReportFieldDAO;
class ObjectChangeDAO
{
/**
* @var FieldDAO[]
*/
private array $fields = [];
private ?ObjectMapping $objectMapping = null;
/**
* @var FieldDAO[]
*/
private array $fieldsByState = [
ReportFieldDAO::FIELD_CHANGED => [],
ReportFieldDAO::FIELD_UNCHANGED => [],
ReportFieldDAO::FIELD_REQUIRED => [],
];
/**
* @param string $integration
* @param string $object
* @param mixed $objectId
* @param string $mappedObject Name of the source object type
* @param mixed $mappedId ID of the source object
* @param \DateTimeInterface $changeDateTime Date\Time the object was last changed
*/
public function __construct(
private $integration,
private $object,
private $objectId,
private $mappedObject,
private $mappedId,
private ?\DateTimeInterface $changeDateTime = null,
) {
}
public function getIntegration(): string
{
return $this->integration;
}
public function addField(FieldDAO $fieldDAO, string $state = ReportFieldDAO::FIELD_CHANGED): self
{
$this->fields[$fieldDAO->getName()] = $fieldDAO;
$this->fieldsByState[$state][$fieldDAO->getName()] = $fieldDAO;
if (ReportFieldDAO::FIELD_REQUIRED === $state) {
// Make this field also available to the unchanged fields array so the integration can get which
// ever one it wants based on it's implementation (i.e. patch vs put)
$this->fieldsByState[ReportFieldDAO::FIELD_UNCHANGED][$fieldDAO->getName()] = $fieldDAO;
}
return $this;
}
/**
* @return string
*/
public function getObject()
{
return $this->object;
}
/**
* @param mixed $objectId
*/
public function setObjectId($objectId): void
{
$this->objectId = $objectId;
}
/**
* @return mixed
*/
public function getObjectId()
{
return $this->objectId;
}
/**
* Returns the name/type for the object in this system that is being synced to the other.
*
* @return string
*/
public function getMappedObject()
{
return $this->mappedObject;
}
/**
* Returns the ID for the object in this system that is being synced to the other.
*
* @return mixed
*/
public function getMappedObjectId()
{
return $this->mappedId;
}
/**
* @param string $name
*
* @return FieldDAO|null
*/
public function getField($name)
{
return $this->fields[$name] ?? null;
}
/**
* Returns all fields whether changed, unchanged required.
*
* @return FieldDAO[]
*/
public function getFields(): array
{
return $this->fields;
}
/**
* Returns only fields that we assume have been changed/modified.
*
* @return FieldDAO[]
*/
public function getChangedFields(): array
{
return $this->fieldsByState[ReportFieldDAO::FIELD_CHANGED];
}
/**
* Returns only fields that are required but were not updated.
*
* @return FieldDAO[]
*/
public function getRequiredFields(): array
{
return $this->fieldsByState[ReportFieldDAO::FIELD_REQUIRED];
}
/**
* Returns fields that were mapped that values were known even though the value was not updated. It does include FieldDAO::FIELD_REQUIRED fields.
*
* @return FieldDAO[]
*/
public function getUnchangedFields(): array
{
return $this->fieldsByState[ReportFieldDAO::FIELD_UNCHANGED];
}
public function shouldSync(): bool
{
return !empty(count($this->fields));
}
public function getChangeDateTime(): \DateTimeInterface
{
return $this->changeDateTime;
}
/**
* @return ObjectChangeDAO
*/
public function setChangeDateTime(?\DateTimeInterface $changeDateTime = null)
{
if (null === $changeDateTime) {
$changeDateTime = new \DateTime();
}
$this->changeDateTime = $changeDateTime;
return $this;
}
public function setObjectMapping(ObjectMapping $objectMapping): void
{
$this->objectMapping = $objectMapping;
}
/**
* This is set after the ObjectMapping entity has been persisted to the database with the updates from this object.
*/
public function getObjectMapping(): ObjectMapping
{
return $this->objectMapping;
}
public function removeField(string $field): void
{
unset($this->fields[$field]);
unset($this->fieldsByState[ReportFieldDAO::FIELD_CHANGED][$field]);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Order;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
class ObjectMappingsDAO
{
/**
* @var ObjectMapping[]
*/
private array $updatedMappings = [];
/**
* @var ObjectMapping[]
*/
private array $newMappings = [];
public function addUpdatedObjectMapping(ObjectMapping $objectMapping): void
{
$this->updatedMappings[] = $objectMapping;
}
public function addNewObjectMapping(ObjectMapping $objectMapping): void
{
$this->newMappings[] = $objectMapping;
}
/**
* @return ObjectMapping[]
*/
public function getUpdatedMappings(): array
{
return $this->updatedMappings;
}
/**
* @return ObjectMapping[]
*/
public function getNewMappings(): array
{
return $this->newMappings;
}
}

View File

@@ -0,0 +1,305 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Order;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
use Mautic\IntegrationsBundle\Exception\UnexpectedValueException;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\RemappedObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\UpdatedObjectMappingDAO;
class OrderDAO
{
/**
* @var ObjectChangeDAO[][]
*/
private array $identifiedObjects = [];
/**
* @var ObjectChangeDAO[][]
*/
private array $unidentifiedObjects = [];
/**
* Array of all changed objects.
*
* @var ObjectChangeDAO[][]
*/
private array $changedObjects = [];
/**
* @var ObjectMapping[]
*/
private array $objectMappings = [];
/**
* @var UpdatedObjectMappingDAO[]
*/
private array $updatedObjectMappings = [];
/**
* @var RemappedObjectDAO[]
*/
private array $remappedObjects = [];
/**
* @var ObjectChangeDAO[]
*/
private array $deleteTheseObjects = [];
private array $retryTheseLater = [];
private int $objectCounter = 0;
/**
* @var NotificationDAO[]
*/
private array $notifications = [];
/**
* @param bool $isFirstTimeSync
* @param string $integration
*/
public function __construct(
private \DateTimeInterface $syncDateTime,
private $isFirstTimeSync,
private $integration,
private array $options = [],
) {
}
public function addObjectChange(ObjectChangeDAO $objectChangeDAO): self
{
if (!isset($this->identifiedObjects[$objectChangeDAO->getObject()])) {
$this->identifiedObjects[$objectChangeDAO->getObject()] = [];
$this->unidentifiedObjects[$objectChangeDAO->getObject()] = [];
$this->changedObjects[$objectChangeDAO->getObject()] = [];
}
$this->changedObjects[$objectChangeDAO->getObject()][] = $objectChangeDAO;
++$this->objectCounter;
if ($objectChangeDAO->getObjectId()) {
$this->identifiedObjects[$objectChangeDAO->getObject()][$objectChangeDAO->getObjectId()] = $objectChangeDAO;
return $this;
}
// These objects are not already tracked and thus possibly need to be created
$this->unidentifiedObjects[$objectChangeDAO->getObject()][$objectChangeDAO->getMappedObjectId()] = $objectChangeDAO;
return $this;
}
/**
* @throws UnexpectedValueException
*/
public function getChangedObjectsByObjectType(string $objectType): array
{
if (isset($this->changedObjects[$objectType])) {
return $this->changedObjects[$objectType];
}
throw new UnexpectedValueException("There are no change objects for object type '$objectType'");
}
/**
* @return ObjectChangeDAO[][]
*/
public function getIdentifiedObjects(): array
{
return $this->identifiedObjects;
}
/**
* @return ObjectChangeDAO[][]
*/
public function getUnidentifiedObjects(): array
{
return $this->unidentifiedObjects;
}
/**
* Create a new mapping between the Mautic and Integration objects.
*
* @param string $integrationObjectName
* @param string|int $integrationObjectId
*/
public function addObjectMapping(
ObjectChangeDAO $objectChangeDAO,
$integrationObjectName,
$integrationObjectId,
?\DateTimeInterface $objectModifiedDate = null,
): void {
if (null === $objectModifiedDate) {
$objectModifiedDate = new \DateTime();
}
$objectMapping = new ObjectMapping();
$objectMapping->setIntegration($this->integration)
->setInternalObjectName($objectChangeDAO->getMappedObject())
->setInternalObjectId($objectChangeDAO->getMappedObjectId())
->setIntegrationObjectName($integrationObjectName)
->setIntegrationObjectId($integrationObjectId)
->setLastSyncDate($objectModifiedDate);
$this->objectMappings[] = $objectMapping;
}
/**
* Update an existing mapping in the case of conversions (i.e. Lead converted to Contact).
*
* @param mixed $oldObjectId
* @param string $oldObjectName
* @param string $newObjectName
* @param mixed $newObjectId
*/
public function remapObject($oldObjectName, $oldObjectId, $newObjectName, $newObjectId = null): void
{
if (null === $newObjectId) {
$newObjectId = $oldObjectId;
}
$this->remappedObjects[$oldObjectId] = new RemappedObjectDAO($this->integration, $oldObjectName, $oldObjectId, $newObjectName, $newObjectId);
}
/**
* Update the last sync date of an existing mapping.
*/
public function updateLastSyncDate(ObjectChangeDAO $objectChangeDAO, ?\DateTimeInterface $objectModifiedDate = null): void
{
if (null === $objectModifiedDate) {
$objectModifiedDate = new \DateTime();
}
$this->updatedObjectMappings[] = new UpdatedObjectMappingDAO(
$this->integration,
$objectChangeDAO->getObject(),
$objectChangeDAO->getObjectId(),
$objectModifiedDate
);
}
/**
* Mark an object as deleted in the integration so Mautic doesn't continue to attempt to sync it.
*/
public function deleteObject(ObjectChangeDAO $objectChangeDAO): void
{
$this->deleteTheseObjects[] = $objectChangeDAO;
}
/**
* If there is a temporary issue with syncing the object, tell the sync engine to not wipe out the tracked changes on Mautic's object fields
* so that they are attempted again for the next sync.
*/
public function retrySyncLater(ObjectChangeDAO $objectChangeDAO): void
{
if (!isset($this->retryTheseLater[$objectChangeDAO->getMappedObject()])) {
$this->retryTheseLater[$objectChangeDAO->getMappedObject()] = [];
}
$this->retryTheseLater[$objectChangeDAO->getMappedObject()][$objectChangeDAO->getMappedObjectId()] = $objectChangeDAO;
}
public function noteObjectSyncIssue(ObjectChangeDAO $objectChangeDAO, string $message): void
{
$this->notifications[] = new NotificationDAO($objectChangeDAO, $message);
}
/**
* @return ObjectMapping[]
*/
public function getObjectMappings(): array
{
return $this->objectMappings;
}
/**
* @return UpdatedObjectMappingDAO[]
*/
public function getUpdatedObjectMappings(): array
{
return $this->updatedObjectMappings;
}
/**
* @return ObjectChangeDAO[]
*/
public function getDeletedObjects(): array
{
return $this->deleteTheseObjects;
}
/**
* @return RemappedObjectDAO[]
*/
public function getRemappedObjects(): array
{
return $this->remappedObjects;
}
/**
* @return NotificationDAO[]
*/
public function getNotifications()
{
return $this->notifications;
}
/**
* @return ObjectChangeDAO[]
*/
public function getSuccessfullySyncedObjects(): array
{
$synced = [];
foreach ($this->changedObjects as $objectChanges) {
foreach ($objectChanges as $objectChange) {
if (isset($this->retryTheseLater[$objectChange->getMappedObject()][$objectChange->getMappedObjectId()])) {
continue;
}
$synced[] = $objectChange;
}
}
return $synced;
}
public function getIdentifiedObjectIds(string $object): array
{
if (!array_key_exists($object, $this->identifiedObjects)) {
return [];
}
return array_keys($this->identifiedObjects[$object]);
}
/**
* @return \DateTime
*/
public function getSyncDateTime(): \DateTimeInterface
{
return $this->syncDateTime;
}
public function isFirstTimeSync(): bool
{
return $this->isFirstTimeSync;
}
public function shouldSync(): bool
{
return !empty($this->changedObjects);
}
public function getObjectCount(): int
{
return $this->objectCounter;
}
public function getOptions(): array
{
return $this->options;
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Order;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\RemappedObjectDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
class OrderResultsDAO
{
/**
* @var ObjectMapping[][]
*/
private array $newObjectMappings = [];
/**
* @var ObjectMapping[][]
*/
private array $updatedObjectMappings = [];
/**
* @var RemappedObjectDAO[][]
*/
private array $remappedObjects = [];
/**
* @var ObjectChangeDAO[][]
*/
private array $deletedObjects = [];
/**
* @param ObjectMapping[] $newObjectMappings
* @param ObjectMapping[] $updatedObjectMappings
* @param RemappedObjectDAO[] $remappedObjects
* @param ObjectChangeDAO[] $deletedObjects
*/
public function __construct(array $newObjectMappings, array $updatedObjectMappings, array $remappedObjects, array $deletedObjects)
{
$this->groupNewObjectMappingsByObjectName($newObjectMappings);
$this->groupUpdatedObjectMappingsByObjectName($updatedObjectMappings);
$this->groupRemappedObjectsByObjectName($remappedObjects);
$this->groupDeletedObjectsByObjectName($deletedObjects);
}
/**
* @return ObjectMapping[]
*/
public function getObjectMappings(string $objectName): array
{
$newObjectMappings = $this->newObjectMappings[$objectName] ?? [];
$updatedObjectMappings = $this->updatedObjectMappings[$objectName] ?? [];
return array_merge($newObjectMappings, $updatedObjectMappings);
}
/**
* @return ObjectMapping[]
*
* @throws ObjectNotFoundException
*/
public function getNewObjectMappings(string $objectName): array
{
if (!isset($this->newObjectMappings[$objectName])) {
throw new ObjectNotFoundException($objectName);
}
return $this->newObjectMappings[$objectName];
}
/**
* @return ObjectMapping[]
*
* @throws ObjectNotFoundException
*/
public function getUpdatedObjectMappings(string $objectName): array
{
if (!isset($this->updatedObjectMappings[$objectName])) {
throw new ObjectNotFoundException($objectName);
}
return $this->updatedObjectMappings[$objectName];
}
/**
* @return RemappedObjectDAO[]
*
* @throws ObjectNotFoundException
*/
public function getRemappedObjects(string $objectName): array
{
if (!isset($this->remappedObjects[$objectName])) {
throw new ObjectNotFoundException($objectName);
}
return $this->remappedObjects[$objectName];
}
/**
* @return ObjectChangeDAO[]
*
* @throws ObjectNotFoundException
*/
public function getDeletedObjects(string $objectName): array
{
if (!isset($this->deletedObjects[$objectName])) {
throw new ObjectNotFoundException($objectName);
}
return $this->deletedObjects[$objectName];
}
/**
* @param ObjectMapping[] $objectMappings
*/
private function groupNewObjectMappingsByObjectName(array $objectMappings): void
{
foreach ($objectMappings as $objectMapping) {
if (!isset($this->newObjectMappings[$objectMapping->getIntegrationObjectName()])) {
$this->newObjectMappings[$objectMapping->getIntegrationObjectName()] = [];
}
$this->newObjectMappings[$objectMapping->getIntegrationObjectName()][] = $objectMapping;
}
}
/**
* @param ObjectMapping[] $objectMappings
*/
private function groupUpdatedObjectMappingsByObjectName(array $objectMappings): void
{
foreach ($objectMappings as $objectMapping) {
if (!isset($this->updatedObjectMappings[$objectMapping->getIntegrationObjectName()])) {
$this->updatedObjectMappings[$objectMapping->getIntegrationObjectName()] = [];
}
$this->updatedObjectMappings[$objectMapping->getIntegrationObjectName()][] = $objectMapping;
}
}
/**
* @param RemappedObjectDAO[] $remappedObjects
*/
private function groupRemappedObjectsByObjectName(array $remappedObjects): void
{
foreach ($remappedObjects as $remappedObject) {
if (!isset($this->remappedObjects[$remappedObject->getNewObjectName()])) {
$this->remappedObjects[$remappedObject->getNewObjectName()] = [];
}
$this->remappedObjects[$remappedObject->getNewObjectName()][] = $remappedObject;
}
}
/**
* @param ObjectChangeDAO[] $deletedObjects
*/
private function groupDeletedObjectsByObjectName(array $deletedObjects): void
{
foreach ($deletedObjects as $deletedObject) {
if (!isset($this->deletedObjects[$deletedObject->getObject()])) {
$this->deletedObjects[$deletedObject->getObject()] = [];
}
$this->deletedObjects[$deletedObject->getObject()][] = $deletedObject;
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\RelationDAO;
class RelationsDAO implements \Iterator, \Countable
{
/**
* @var RelationDAO[]
*/
private array $relations = [];
private int $position = 0;
/**
* @param RelationDAO[] $relations
*/
public function addRelations(array $relations): void
{
foreach ($relations as $relation) {
$this->addRelation($relation);
}
}
public function addRelation(RelationDAO $relation): void
{
$this->relations[] = $relation;
}
public function current(): RelationDAO
{
return $this->relations[$this->position];
}
public function next(): void
{
++$this->position;
}
public function key(): int
{
return $this->position;
}
public function valid(): bool
{
return isset($this->relations[$this->position]);
}
public function rewind(): void
{
$this->position = 0;
}
public function count(): int
{
return count($this->relations);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Report;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
class FieldDAO
{
public const FIELD_CHANGED = 'changed';
public const FIELD_REQUIRED = 'required';
public const FIELD_UNCHANGED = 'unchanged';
private ?\DateTimeInterface $changeDateTime = null;
public function __construct(
private string $name,
private NormalizedValueDAO $value,
private string $state = self::FIELD_CHANGED,
) {
}
public function getName(): string
{
return $this->name;
}
public function getValue(): NormalizedValueDAO
{
return $this->value;
}
public function getChangeDateTime(): ?\DateTimeInterface
{
return $this->changeDateTime;
}
public function setChangeDateTime(\DateTimeInterface $changeDateTime): self
{
$this->changeDateTime = $changeDateTime;
return $this;
}
public function getState(): string
{
return $this->state;
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Report;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
class ObjectDAO
{
/**
* @var FieldDAO[]
*/
private array $fields = [];
/**
* @param string $object
* @param mixed $objectId
*/
public function __construct(
private $object,
private $objectId,
private ?\DateTimeInterface $changeDateTime = null,
) {
}
public function getChangeDateTime(): ?\DateTimeInterface
{
return $this->changeDateTime;
}
public function setChangeDateTime(\DateTimeInterface $changeDateTime): self
{
$this->changeDateTime = $changeDateTime;
return $this;
}
/**
* @return $this
*/
public function addField(FieldDAO $fieldDAO)
{
$this->fields[$fieldDAO->getName()] = $fieldDAO;
return $this;
}
/**
* @return mixed
*/
public function getObjectId()
{
return $this->objectId;
}
/**
* @return string
*/
public function getObject()
{
return $this->object;
}
/**
* @param string $name
*
* @return FieldDAO|null
*
* @throws FieldNotFoundException
*/
public function getField($name)
{
if (!isset($this->fields[$name])) {
throw new FieldNotFoundException($name, $this->object);
}
return $this->fields[$name];
}
/**
* @return FieldDAO[]
*/
public function getFields(): array
{
return $this->fields;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Report;
class RelationDAO
{
private ?int $relObjectInternalId = null;
public function __construct(
private string $objectName,
private string $relFieldName,
private string $relObjectName,
private string $objectIntegrationId,
private string $relObjectIntegrationId,
) {
}
public function getObjectName(): string
{
return $this->objectName;
}
public function getRelObjectName(): string
{
return $this->relObjectName;
}
public function getRelFieldName(): string
{
return $this->relFieldName;
}
public function getObjectIntegrationId(): string
{
return $this->objectIntegrationId;
}
public function getRelObjectIntegrationId(): string
{
return $this->relObjectIntegrationId;
}
public function getRelObjectInternalId(): ?int
{
return $this->relObjectInternalId;
}
public function setRelObjectInternalId(int $relObjectInternalId): void
{
$this->relObjectInternalId = $relObjectInternalId;
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Report;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\RemappedObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\RelationsDAO;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
class ReportDAO
{
private array $objects = [];
private array $remappedObjects = [];
private RelationsDAO $relationsDAO;
/**
* @param string $integration
*/
public function __construct(
private $integration,
) {
$this->relationsDAO = new RelationsDAO();
}
/**
* @return string
*/
public function getIntegration()
{
return $this->integration;
}
/**
* @return $this
*/
public function addObject(ObjectDAO $objectDAO)
{
if (!isset($this->objects[$objectDAO->getObject()])) {
$this->objects[$objectDAO->getObject()] = [];
}
$this->objects[$objectDAO->getObject()][$objectDAO->getObjectId()] = $objectDAO;
return $this;
}
/**
* @param mixed $oldObjectId
* @param string $oldObjectName
* @param string $newObjectName
* @param mixed $newObjectId
*/
public function remapObject($oldObjectName, $oldObjectId, $newObjectName, $newObjectId = null): void
{
if (null === $newObjectId) {
$newObjectId = $oldObjectId;
}
$this->remappedObjects[$oldObjectId] = new RemappedObjectDAO($this->integration, $oldObjectName, $oldObjectId, $newObjectName, $newObjectId);
}
/**
* @throws ObjectNotFoundException
* @throws FieldNotFoundException
*/
public function getInformationChangeRequest($objectName, $objectId, $fieldName): InformationChangeRequestDAO
{
if (empty($this->objects[$objectName][$objectId])) {
throw new ObjectNotFoundException($objectName.':'.$objectId);
}
/** @var ObjectDAO $reportObject */
$reportObject = $this->objects[$objectName][$objectId];
$reportField = $reportObject->getField($fieldName);
$informationChangeRequest = new InformationChangeRequestDAO(
$this->integration,
$objectName,
$objectId,
$fieldName,
$reportField->getValue()
);
$informationChangeRequest->setPossibleChangeDateTime($reportObject->getChangeDateTime())
->setCertainChangeDateTime($reportField->getChangeDateTime());
return $informationChangeRequest;
}
/**
* @return ObjectDAO[]
*/
public function getObjects(?string $objectName)
{
$returnedObjects = [];
if (null === $objectName) {
foreach ($this->objects as $objects) {
foreach ($objects as $object) {
$returnedObjects[] = $object;
}
}
return $returnedObjects;
}
return $this->objects[$objectName] ?? [];
}
/**
* @return RemappedObjectDAO[]
*/
public function getRemappedObjects(): array
{
return $this->remappedObjects;
}
/**
* @param int $objectId
*/
public function getObject(string $objectName, $objectId): ?ObjectDAO
{
if (!isset($this->objects[$objectName])) {
return null;
}
if (!isset($this->objects[$objectName][$objectId])) {
return null;
}
return $this->objects[$objectName][$objectId];
}
public function shouldSync(): bool
{
return !empty($this->objects);
}
public function getRelations(): RelationsDAO
{
return $this->relationsDAO;
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Request;
class ObjectDAO
{
/**
* @var string[]
*/
private array $fields = [];
/**
* @var string[]
*/
private array $requiredFields = [];
public function __construct(
private string $object,
/**
* Date/time based on last synced date for the object or the start date/time fed through the command's arguments.
* This value does not change between iterations.
*/
private ?\DateTimeInterface $fromDateTime = null,
/**
* Date/Time the sync started.
*/
private ?\DateTimeInterface $toDateTime = null,
) {
}
public function getObject(): string
{
return $this->object;
}
/**
* @return self
*/
public function addField(string $field)
{
$this->fields[] = $field;
return $this;
}
/**
* @return string[]
*/
public function getFields(): array
{
return $this->fields;
}
public function setRequiredFields(array $fields): void
{
$this->requiredFields = $fields;
}
/**
* @return string[]
*/
public function getRequiredFields(): array
{
return $this->requiredFields;
}
public function getFromDateTime(): ?\DateTimeInterface
{
return $this->fromDateTime;
}
public function getToDateTime(): ?\DateTimeInterface
{
return $this->toDateTime;
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Sync\Request;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InputOptionsDAO;
class RequestDAO
{
private int $syncIteration;
/**
* @var ObjectDAO[]
*/
private array $objects = [];
public function __construct(
private string $syncToIntegration,
int $syncIteration,
private InputOptionsDAO $inputOptionsDAO,
) {
$this->syncIteration = (int) $syncIteration;
}
/**
* @return self
*/
public function addObject(ObjectDAO $objectDAO)
{
$this->objects[] = $objectDAO;
return $this;
}
/**
* @return ObjectDAO[]
*/
public function getObjects(): array
{
return $this->objects;
}
public function getSyncIteration(): int
{
return $this->syncIteration;
}
public function isFirstTimeSync(): bool
{
return $this->inputOptionsDAO->isFirstTimeSync();
}
/**
* The integration that will be synced to.
*/
public function getSyncToIntegration(): string
{
return $this->syncToIntegration;
}
/**
* Returns DAO object with all input options.
*/
public function getInputOptionsDAO(): InputOptionsDAO
{
return $this->inputOptionsDAO;
}
/**
* Returns true if there are objects to sync.
*/
public function shouldSync(): bool
{
return !empty($this->objects);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Value;
class EncodedValueDAO
{
public const STRING_TYPE = 'string';
public const INT_TYPE = 'int';
public const FLOAT_TYPE = 'float';
public const DOUBLE_TYPE = 'double';
public const DATETIME_TYPE = 'datetime';
public const BOOLEAN_TYPE = 'boolean';
/**
* @param string $type
* @param string $value
*/
public function __construct(
private $type,
private $value,
) {
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Value;
class NormalizedValueDAO
{
public const BOOLEAN_TYPE = 'boolean';
public const DATE_TYPE = 'date';
public const DATETIME_TYPE = 'datetime';
public const DOUBLE_TYPE = 'double';
public const EMAIL_TYPE = 'email';
public const FLOAT_TYPE = 'float';
public const INT_TYPE = 'int';
public const LOOKUP_TYPE = 'lookup';
public const MULTISELECT_TYPE = 'multiselect';
public const PHONE_TYPE = 'phone';
public const SELECT_TYPE = 'select';
public const STRING_TYPE = 'string';
public const REGION_TYPE = 'region';
public const TEXT_TYPE = 'text';
public const TEXTAREA_TYPE = 'textarea';
public const TIME_TYPE = 'time';
public const URL_TYPE = 'url';
public const REFERENCE_TYPE = 'reference';
/**
* @var mixed
*/
private $normalizedValue;
/**
* @param string $type
* @param mixed $value
* @param mixed $normalizedValue
*/
public function __construct(
private $type,
private $value,
$normalizedValue = null,
) {
$this->normalizedValue = $normalizedValue ?: $value;
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @return mixed
*/
public function getOriginalValue()
{
return $this->value;
}
/**
* @return mixed
*/
public function getNormalizedValue()
{
return $this->normalizedValue;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\DAO\Value;
class ReferenceValueDAO implements \Stringable
{
private ?int $value = null;
private ?string $type = null;
public function getValue(): ?int
{
return $this->value;
}
public function setValue(int $value): void
{
$this->value = $value;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): void
{
$this->type = $type;
}
public function __toString(): string
{
return (string) $this->value;
}
/** @return array<string, mixed> */
public function __serialize(): array
{
return [
'value' => $this->value,
'types' => $this->type,
];
}
/** @param array<string, mixed> $data */
public function __unserialize(array $data): void
{
$this->value = $data['value'] ?? null;
$this->type = $data['type'] ?? null;
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class ConflictUnresolvedException extends \Exception
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class FieldNotFoundException extends \Exception
{
/**
* @param int $code
* @param \Exception|null $previous
*/
public function __construct($field, $object, $code = 0, ?\Throwable $previous = null)
{
parent::__construct(sprintf('The %s field is not mapped for the %s object.', $field, $object), $code, $previous);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class HandlerNotSupportedException extends \Exception
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class InternalIdNotFoundException extends \Exception
{
public function __construct(string $object)
{
parent::__construct("ID for object $object not found");
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class ObjectDeletedException extends \Exception
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class ObjectNotFoundException extends \Exception
{
public function __construct(string $object)
{
parent::__construct("$object was not found in the mapping");
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class ObjectNotSupportedException extends \Exception
{
public function __construct(string $integration, string $object)
{
parent::__construct("$integration does not support a $object object");
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Exception;
class ObjectSyncSkippedException extends \Exception
{
}

View File

@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Helper;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
use Mautic\IntegrationsBundle\Entity\ObjectMappingRepository;
use Mautic\IntegrationsBundle\Event\InternalObjectFindEvent;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\RemappedObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\UpdatedObjectMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ObjectDAO;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectDeletedException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectProvider;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class MappingHelper
{
public function __construct(
private FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
private ObjectMappingRepository $objectMappingRepository,
private ObjectProvider $objectProvider,
private EventDispatcherInterface $dispatcher,
) {
}
/**
* @throws ObjectDeletedException
* @throws ObjectNotFoundException
* @throws ObjectNotSupportedException
*/
public function findMauticObject(MappingManualDAO $mappingManualDAO, string $internalObjectName, ObjectDAO $integrationObjectDAO): ObjectDAO
{
// Check if this contact is already tracked
if ($internalObject = $this->objectMappingRepository->getInternalObject(
$mappingManualDAO->getIntegration(),
$integrationObjectDAO->getObject(),
$integrationObjectDAO->getObjectId(),
$internalObjectName
)) {
if ($internalObject['is_deleted']) {
throw new ObjectDeletedException();
}
return new ObjectDAO(
$internalObjectName,
$internalObject['internal_object_id'],
new \DateTime($internalObject['last_sync_date'], new \DateTimeZone('UTC'))
);
}
// We don't know who this is so search Mautic
$uniqueIdentifierFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier(['object' => $internalObjectName]);
$identifiers = [];
foreach ($uniqueIdentifierFields as $field => $fieldLabel) {
try {
$integrationField = $mappingManualDAO->getIntegrationMappedField($integrationObjectDAO->getObject(), $internalObjectName, $field);
if ($integrationValue = $integrationObjectDAO->getField($integrationField)) {
$identifiers[$field] = $integrationValue->getValue()->getNormalizedValue();
}
} catch (FieldNotFoundException) {
}
}
if (empty($identifiers)) {
// No fields found to search for contact so return null
return new ObjectDAO($internalObjectName, null);
}
try {
$event = new InternalObjectFindEvent(
$this->objectProvider->getObjectByName($internalObjectName)
);
} catch (ObjectNotFoundException) {
// Throw this exception for BC.
throw new ObjectNotSupportedException(MauticSyncDataExchange::NAME, $internalObjectName);
}
$event->setFieldValues($identifiers);
$this->dispatcher->dispatch(
$event,
IntegrationEvents::INTEGRATION_FIND_INTERNAL_RECORDS,
);
$foundObjects = $event->getFoundObjects();
if (!$foundObjects) {
// No contacts were found
return new ObjectDAO($internalObjectName, null);
}
// Match found!
$objectId = $foundObjects[0]['id'];
// Let's store the relationship since we know it
$objectMapping = new ObjectMapping();
$objectMapping->setLastSyncDate($integrationObjectDAO->getChangeDateTime())
->setIntegration($mappingManualDAO->getIntegration())
->setIntegrationObjectName($integrationObjectDAO->getObject())
->setIntegrationObjectId($integrationObjectDAO->getObjectId())
->setInternalObjectName($internalObjectName)
->setInternalObjectId($objectId);
$this->saveObjectMapping($objectMapping);
return new ObjectDAO($internalObjectName, $objectId);
}
/**
* Returns corresponding Mautic entity class name for the given Mautic object.
*
* @throws ObjectNotSupportedException
*/
public function getMauticEntityClassName(string $internalObject): string
{
try {
return $this->objectProvider->getObjectByName($internalObject)->getEntityName();
} catch (ObjectNotFoundException) {
// Throw this exception instead to keep BC.
throw new ObjectNotSupportedException(MauticSyncDataExchange::NAME, $internalObject);
}
}
/**
* @throws ObjectDeletedException
*/
public function findIntegrationObject(string $integration, string $integrationObjectName, ObjectDAO $internalObjectDAO): ObjectDAO
{
if ($integrationObject = $this->objectMappingRepository->getIntegrationObject(
$integration,
$internalObjectDAO->getObject(),
$internalObjectDAO->getObjectId(),
$integrationObjectName
)) {
if ($integrationObject['is_deleted']) {
throw new ObjectDeletedException();
}
return new ObjectDAO(
$integrationObjectName,
$integrationObject['integration_object_id'],
new \DateTime($integrationObject['last_sync_date'], new \DateTimeZone('UTC'))
);
}
return new ObjectDAO($integrationObjectName, null);
}
/**
* @param ObjectMapping[] $mappings
*/
public function saveObjectMappings(array $mappings): void
{
foreach ($mappings as $mapping) {
$this->saveObjectMapping($mapping);
}
}
public function updateObjectMappings(array $mappings): void
{
foreach ($mappings as $mapping) {
try {
$this->updateObjectMapping($mapping);
} catch (ObjectNotFoundException) {
continue;
}
}
}
/**
* @param RemappedObjectDAO[] $mappings
*/
public function remapIntegrationObjects(array $mappings): void
{
foreach ($mappings as $mapping) {
$this->objectMappingRepository->updateIntegrationObject(
$mapping->getIntegration(),
$mapping->getOldObjectName(),
$mapping->getOldObjectId(),
$mapping->getNewObjectName(),
$mapping->getNewObjectId()
);
}
}
/**
* @param ObjectChangeDAO[] $objects
*/
public function markAsDeleted(array $objects): void
{
foreach ($objects as $object) {
$this->objectMappingRepository->markAsDeleted($object->getIntegration(), $object->getObject(), $object->getObjectId());
}
}
private function saveObjectMapping(ObjectMapping $objectMapping): void
{
$this->objectMappingRepository->saveEntity($objectMapping);
$this->objectMappingRepository->detachEntity($objectMapping);
}
/**
* @throws ObjectNotFoundException
*/
private function updateObjectMapping(UpdatedObjectMappingDAO $updatedObjectMappingDAO): void
{
/** @var ObjectMapping $objectMapping */
$objectMapping = $this->objectMappingRepository->findOneBy(
[
'integration' => $updatedObjectMappingDAO->getIntegration(),
'integrationObjectName' => $updatedObjectMappingDAO->getIntegrationObjectName(),
'integrationObjectId' => $updatedObjectMappingDAO->getIntegrationObjectId(),
]
);
if (!$objectMapping) {
throw new ObjectNotFoundException($updatedObjectMappingDAO->getIntegrationObjectName().':'.$updatedObjectMappingDAO->getIntegrationObjectId());
}
$objectMapping->setLastSyncDate($updatedObjectMappingDAO->getObjectModifiedDate());
$this->saveObjectMapping($objectMapping);
// Make the ObjectMapping available to the IntegrationEvents::INTEGRATION_BATCH_SYNC_COMPLETED_* events
$updatedObjectMappingDAO->setObjectMapping($objectMapping);
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Helper;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\RelationDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\Exception\InternalIdNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
class RelationsHelper
{
/**
* @var ObjectDAO[]
*/
private array $objectsToSynchronize = [];
public function __construct(
private MappingHelper $mappingHelper,
) {
}
public function processRelations(MappingManualDAO $mappingManualDao, ReportDAO $syncReport): void
{
$this->objectsToSynchronize = [];
foreach ($syncReport->getRelations() as $relationObject) {
if (0 < $relationObject->getRelObjectInternalId()) {
continue;
}
$this->processRelation($mappingManualDao, $syncReport, $relationObject);
}
}
public function getObjectsToSynchronize(): array
{
return $this->objectsToSynchronize;
}
/**
* @throws \Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException
* @throws \Mautic\IntegrationsBundle\Sync\Exception\ObjectDeletedException
* @throws \Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException
*/
private function processRelation(MappingManualDAO $mappingManualDao, ReportDAO $syncReport, RelationDAO $relationObject): void
{
$relObjectDao = new ObjectDAO($relationObject->getRelObjectName(), $relationObject->getRelObjectIntegrationId());
try {
$internalObjectName = $this->getInternalObjectName($mappingManualDao, $relationObject->getRelObjectName());
$internalObjectId = $this->getInternalObjectId($mappingManualDao, $relationObject, $relObjectDao);
$this->addObjectInternalId($internalObjectId, $internalObjectName, $relationObject, $syncReport);
} catch (ObjectNotFoundException) {
return; // We are not mapping this object
} catch (InternalIdNotFoundException) {
$this->objectsToSynchronize[] = $relObjectDao;
}
}
/**
* @throws InternalIdNotFoundException
*/
private function getInternalObjectId(MappingManualDAO $mappingManualDao, RelationDAO $relationObject, ObjectDAO $relObjectDao): int
{
$relObject = $this->findInternalObject($mappingManualDao, $relationObject->getRelObjectName(), $relObjectDao);
$internalObjectId = (int) $relObject->getObjectId();
if ($internalObjectId) {
return $internalObjectId;
}
throw new InternalIdNotFoundException($relationObject->getRelObjectName());
}
/**
* @throws ObjectNotFoundException
* @throws \Mautic\IntegrationsBundle\Sync\Exception\ObjectDeletedException
* @throws \Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException
*/
private function findInternalObject(MappingManualDAO $mappingManualDao, string $relObjectName, ObjectDAO $objectDao): ObjectDAO
{
$internalObjectsName = $this->getInternalObjectName($mappingManualDao, $relObjectName);
return $this->mappingHelper->findMauticObject($mappingManualDao, $internalObjectsName, $objectDao);
}
/**
* @throws \Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException
*/
private function addObjectInternalId(int $relObjectId, string $relInternalType, RelationDAO $relationObject, ReportDAO $syncReport): void
{
$relationObject->setRelObjectInternalId($relObjectId);
$objectDAO = $syncReport->getObject($relationObject->getObjectName(), $relationObject->getObjectIntegrationId());
$referenceValue = $objectDAO->getField($relationObject->getRelFieldName())->getValue()->getNormalizedValue();
$referenceValue->setType($relInternalType);
$referenceValue->setValue($relObjectId);
}
/**
* @return mixed
*
* @throws ObjectNotFoundException
*/
private function getInternalObjectName(MappingManualDAO $mappingManualDao, string $relObjectName)
{
$internalObjectsNames = $mappingManualDao->getMappedInternalObjectsNames($relObjectName);
return $internalObjectsNames[0];
}
}

View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Helper;
use Doctrine\DBAL\Connection;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
class SyncDateHelper
{
private ?\DateTimeInterface $syncFromDateTime = null;
private ?\DateTimeInterface $syncToDateTime = null;
private ?\DateTimeImmutable $syncDateTime = null;
/**
* @var \DateTimeInterface[]
*/
private array $lastObjectSyncDates = [];
private ?\DateTimeInterface $internalSyncStartDateTime = null;
public function __construct(
private Connection $connection,
) {
}
public function setSyncDateTimes(?\DateTimeInterface $fromDateTime = null, ?\DateTimeInterface $toDateTime = null): void
{
$this->syncFromDateTime = $fromDateTime;
$this->syncToDateTime = $toDateTime;
$this->syncDateTime = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$this->lastObjectSyncDates = [];
}
public function getSyncFromDateTime(string $integration, string $object): \DateTimeInterface
{
if ($this->syncFromDateTime) {
// The command requested a specific start date so use it
return $this->syncFromDateTime;
}
$key = $integration.$object;
if (isset($this->lastObjectSyncDates[$key])) {
// Use the same sync date for integrations to paginate properly
return $this->lastObjectSyncDates[$key];
}
if (MauticSyncDataExchange::NAME !== $integration && $lastSync = $this->getLastSyncDateForObject($integration, $object)) {
// Use the latest sync date recorded
$this->lastObjectSyncDates[$key] = $lastSync;
} else {
// Otherwise, just sync the last 24 hours
$this->lastObjectSyncDates[$key] = new \DateTimeImmutable('-24 hours', new \DateTimeZone('UTC'));
}
return $this->lastObjectSyncDates[$key];
}
public function getSyncToDateTime(): ?\DateTimeInterface
{
if ($this->syncToDateTime) {
return $this->syncToDateTime;
}
return $this->syncDateTime;
}
public function getSyncDateTime(): ?\DateTimeInterface
{
return $this->syncDateTime;
}
/**
* @return \DateTimeImmutable|null
*/
public function getLastSyncDateForObject(string $integration, string $object): ?\DateTimeInterface
{
$qb = $this->connection->createQueryBuilder();
$result = $qb
->select('max(m.last_sync_date)')
->from(MAUTIC_TABLE_PREFIX.'sync_object_mapping', 'm')
->where(
$qb->expr()->eq('m.integration', ':integration'),
$qb->expr()->eq('m.integration_object_name', ':object')
)
->setParameter('integration', $integration)
->setParameter('object', $object)
->executeQuery()
->fetchOne();
if (!$result) {
return null;
}
$lastSync = new \DateTimeImmutable($result, new \DateTimeZone('UTC'));
// The last sync is out of the requested sync date/time range
if ($this->syncFromDateTime && $lastSync < $this->syncFromDateTime) {
return null;
}
// The last sync is out of the requested sync date/time range
if ($lastSync > $this->getSyncToDateTime()) {
return null;
}
return $lastSync;
}
public function getInternalSyncStartDateTime(): ?\DateTimeInterface
{
return $this->internalSyncStartDateTime;
}
public function setInternalSyncStartDateTime(): void
{
if ($this->internalSyncStartDateTime) {
return;
}
$this->internalSyncStartDateTime = $this->calculateInternalSyncStartDateTime();
}
private function calculateInternalSyncStartDateTime(): \DateTimeInterface
{
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
// If there is no syncToDateTime value use "now"
if (!$this->getSyncToDateTime()) {
return $now;
}
// Clone it so that we don't modify the initial object
$syncToDateTime = clone $this->getSyncToDateTime();
// We should compare in UTC timezone
if (method_exists($syncToDateTime, 'setTimezone')) {
$syncToDateTime->setTimezone(new \DateTimeZone('UTC'));
}
// If syncToDate is less than now then use syncToDate, because otherwise we may delete
// changes that aren't supposed to be deleted from the sync_object_field_change_report table
return min($now, $syncToDateTime);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Logger;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
class DebugLogger
{
private static ?LoggerInterface $logger = null;
public function __construct(LoggerInterface $logger)
{
static::$logger = $logger;
}
/**
* @param string $integration
* @param string $loggedFrom
* @param string $message
* @param string $urgency
*/
public static function log($integration, $message, $loggedFrom = null, array $context = [], $urgency = LogLevel::DEBUG): void
{
if (!static::$logger) {
return;
}
if (null !== $loggedFrom) {
$context['logged from'] = $loggedFrom;
}
static::$logger->$urgency(strtoupper($integration).' SYNC: '.$message, $context);
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Service\BulkNotificationInterface;
use Mautic\IntegrationsBundle\Sync\Notification\Helper\UserNotificationBuilder;
use Mautic\UserBundle\Entity\User;
class BulkNotification
{
public function __construct(
private BulkNotificationInterface $bulkNotification,
private UserNotificationBuilder $userNotificationBuilder,
private EntityManagerInterface $entityManager,
) {
}
public function addNotification(
string $deduplicateValue,
string $message,
string $integrationDisplayName,
string $objectDisplayName,
string $mauticObject,
int $id,
string $linkText,
): void {
$link = $this->userNotificationBuilder->buildLink($mauticObject, $id, $linkText);
$userIds = $this->userNotificationBuilder->getUserIds($mauticObject, $id);
foreach ($userIds as $userId) {
/** @var User $user */
$user = $this->entityManager->getReference(User::class, $userId);
$this->bulkNotification->addNotification(
$deduplicateValue,
$this->userNotificationBuilder->formatMessage($message, $link),
null,
$this->userNotificationBuilder->formatHeader($integrationDisplayName, $objectDisplayName),
'ri-refresh-line',
null,
$user
);
}
}
/**
* @param \DateTime|null $deduplicateDateTimeFrom If last 24 hours for deduplication does not fit, change it here
*/
public function flush(?\DateTime $deduplicateDateTimeFrom = null): void
{
$this->bulkNotification->flush($deduplicateDateTimeFrom);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Handler;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\NotificationDAO;
use Mautic\IntegrationsBundle\Sync\Notification\Helper\CompanyHelper;
use Mautic\IntegrationsBundle\Sync\Notification\Helper\UserNotificationHelper;
use Mautic\IntegrationsBundle\Sync\Notification\Writer;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
class CompanyNotificationHandler implements HandlerInterface
{
public function __construct(
private Writer $writer,
private UserNotificationHelper $userNotificationHelper,
private CompanyHelper $companyHelper,
) {
}
public function getIntegration(): string
{
return MauticSyncDataExchange::NAME;
}
public function getSupportedObject(): string
{
return MauticSyncDataExchange::OBJECT_COMPANY;
}
/**
* @throws \Doctrine\ORM\ORMException
* @throws \Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException
*/
public function writeEntry(NotificationDAO $notificationDAO, string $integrationDisplayName, string $objectDisplayName): void
{
$this->writer->writeAuditLogEntry(
$notificationDAO->getIntegration(),
$notificationDAO->getMauticObject(),
$notificationDAO->getMauticObjectId(),
'sync',
[
'integrationObject' => $notificationDAO->getIntegrationObject(),
'integrationObjectId' => $notificationDAO->getIntegrationObjectId(),
'message' => $notificationDAO->getMessage(),
]
);
$this->userNotificationHelper->writeNotification(
$notificationDAO->getMessage(),
$integrationDisplayName,
$objectDisplayName,
$notificationDAO->getMauticObject(),
$notificationDAO->getMauticObjectId(),
(string) $this->companyHelper->getCompanyName($notificationDAO->getMauticObjectId())
);
}
public function finalize(): void
{
// Nothing to do
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Handler;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\NotificationDAO;
use Mautic\IntegrationsBundle\Sync\Notification\Helper\UserSummaryNotificationHelper;
use Mautic\IntegrationsBundle\Sync\Notification\Writer;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object\Contact;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadEventLog;
use Mautic\LeadBundle\Entity\LeadEventLogRepository;
class ContactNotificationHandler implements HandlerInterface
{
private ?string $integrationDisplayName = null;
private ?string $objectDisplayName = null;
public function __construct(
private Writer $writer,
private LeadEventLogRepository $leadEventRepository,
private EntityManagerInterface $em,
private UserSummaryNotificationHelper $userNotificationHelper,
) {
}
public function getIntegration(): string
{
return MauticSyncDataExchange::NAME;
}
public function getSupportedObject(): string
{
return Contact::NAME;
}
/**
* @throws \Doctrine\ORM\ORMException
*/
public function writeEntry(NotificationDAO $notificationDAO, string $integrationDisplayName, string $objectDisplayName): void
{
$this->integrationDisplayName = $integrationDisplayName;
$this->objectDisplayName = $objectDisplayName;
$this->writer->writeAuditLogEntry(
$notificationDAO->getIntegration(),
$notificationDAO->getMauticObject(),
$notificationDAO->getMauticObjectId(),
'sync',
[
'integrationObject' => $notificationDAO->getIntegrationObject(),
'integrationObjectId' => $notificationDAO->getIntegrationObjectId(),
'message' => $notificationDAO->getMessage(),
]
);
$this->writeEventLogEntry($notificationDAO->getIntegration(), $notificationDAO->getMauticObjectId(), $notificationDAO->getMessage());
// Store these so we can send one notice to the user
$this->userNotificationHelper->storeSummaryNotification($integrationDisplayName, $objectDisplayName, $notificationDAO->getMauticObjectId());
}
public function finalize(): void
{
$this->userNotificationHelper->writeNotifications(
Contact::NAME,
'mautic.integration.sync.user_notification.contact_message'
);
}
/**
* @throws \Doctrine\ORM\ORMException
*/
private function writeEventLogEntry(string $integration, int $contactId, string $message): void
{
$eventLog = new LeadEventLog();
$eventLog
->setLead($this->em->getReference(Lead::class, $contactId))
->setBundle('integrations')
->setObject($integration)
->setAction('sync')
->setProperties(
[
'message' => $message,
'integration' => $this->integrationDisplayName,
'object' => $this->objectDisplayName,
]
);
$this->leadEventRepository->saveEntity($eventLog);
$this->leadEventRepository->detachEntity($eventLog);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Handler;
use Mautic\IntegrationsBundle\Sync\Exception\HandlerNotSupportedException;
class HandlerContainer
{
private array $handlers = [];
public function registerHandler(HandlerInterface $handler): void
{
if (!isset($this->handlers[$handler->getIntegration()])) {
$this->handlers[$handler->getIntegration()] = [];
}
$this->handlers[$handler->getIntegration()][$handler->getSupportedObject()] = $handler;
}
/**
* @return HandlerInterface
*
* @throws HandlerNotSupportedException
*/
public function getHandler(string $integration, string $object)
{
if (!isset($this->handlers[$integration])) {
throw new HandlerNotSupportedException("$integration does not have any registered handlers");
}
if (!isset($this->handlers[$integration][$object])) {
throw new HandlerNotSupportedException("$integration does not have any registered handlers for the object $object");
}
return $this->handlers[$integration][$object];
}
/**
* @return HandlerInterface[]
*/
public function getHandlers(): array
{
return array_reduce($this->handlers, fn ($accumulator, $integrationHandlers): array => array_merge($accumulator, $integrationHandlers), []);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Handler;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\NotificationDAO;
interface HandlerInterface
{
public function getIntegration(): string;
public function getSupportedObject(): string;
public function writeEntry(NotificationDAO $notificationDAO, string $integrationDisplayName, string $objectDisplayName): void;
/**
* Finalize notifications such as pushing summary entries to the user notifications tray.
*/
public function finalize(): void;
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Helper;
use Doctrine\DBAL\Connection;
class CompanyHelper
{
public function __construct(
private Connection $connection,
) {
}
/**
* @return string|bool
*/
public function getCompanyName(int $id)
{
return $this->connection->createQueryBuilder()
->select('c.companyname')
->from(MAUTIC_TABLE_PREFIX.'companies', 'c')
->where('c.id = '.$id)
->executeQuery()
->fetchOne();
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Helper;
use Mautic\IntegrationsBundle\Event\InternalObjectOwnerEvent;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object\ObjectInterface;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectProvider;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class OwnerProvider
{
public function __construct(
private EventDispatcherInterface $dispatcher,
private ObjectProvider $objectProvider,
) {
}
/**
* @param int[] $objectIds
*
* @return ObjectInterface
*
* @throws ObjectNotSupportedException
*/
public function getOwnersForObjectIds(string $objectName, array $objectIds): array
{
if (empty($objectIds)) {
return [];
}
try {
$object = $this->objectProvider->getObjectByName($objectName);
} catch (ObjectNotFoundException) {
// Throw this exception for BC.
throw new ObjectNotSupportedException(MauticSyncDataExchange::NAME, $objectName);
}
$event = new InternalObjectOwnerEvent($object, $objectIds);
$this->dispatcher->dispatch($event, IntegrationEvents::INTEGRATION_FIND_OWNER_IDS);
return $event->getOwners();
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Helper;
use Mautic\IntegrationsBundle\Event\InternalObjectRouteEvent;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectProvider;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class RouteHelper
{
/**
* @var RouEventDispatcherInterfaceter
*/
private $dispatcher;
public function __construct(
private ObjectProvider $objectProvider,
EventDispatcherInterface $dispatcher,
) {
$this->dispatcher = $dispatcher;
}
/**
* @throws ObjectNotSupportedException
*/
public function getRoute(string $object, int $id): string
{
try {
$event = new InternalObjectRouteEvent($this->objectProvider->getObjectByName($object), $id);
} catch (ObjectNotFoundException) {
// Throw this exception instead to keep BC.
throw new ObjectNotSupportedException(MauticSyncDataExchange::NAME, $object);
}
$this->dispatcher->dispatch($event, IntegrationEvents::INTEGRATION_BUILD_INTERNAL_OBJECT_ROUTE);
return $event->getRoute();
}
/**
* @throws ObjectNotSupportedException
*/
public function getLink(string $object, int $id, string $linkText): string
{
$route = $this->getRoute($object, $id);
return sprintf('<a href="%s">%s</a>', $route, $linkText);
}
/**
* @throws ObjectNotSupportedException
*/
public function getRoutes(string $object, array $ids): array
{
$routes = [];
foreach ($ids as $id) {
$routes[$id] = $this->getRoute($object, $id);
}
return $routes;
}
/**
* @throws ObjectNotSupportedException
*/
public function getLinkCsv(string $object, array $ids): string
{
$links = [];
$routes = $this->getRoutes($object, $ids);
foreach ($routes as $id => $route) {
$links[] = sprintf('[<a href="%s">%s</a>]', $route, $id);
}
return implode(', ', $links);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Helper;
use Doctrine\DBAL\Connection;
class UserHelper
{
public function __construct(
private Connection $connection,
) {
}
public function getAdminUsers(): array
{
$qb = $this->connection->createQueryBuilder();
$results = $qb->select('u.id')
->from(MAUTIC_TABLE_PREFIX.'users', 'u')
->join('u', MAUTIC_TABLE_PREFIX.'roles', 'r', 'r.id = u.role_id')
->where(
$qb->expr()->and(
$qb->expr()->eq('r.is_published', 1),
$qb->expr()->eq('r.is_admin', 1),
$qb->expr()->eq('u.is_published', 1)
)
)
->executeQuery()
->fetchAllAssociative();
$admins = [];
foreach ($results as $result) {
$admins[] = (int) $result['id'];
}
return $admins;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Helper;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserNotificationBuilder
{
public function __construct(
private UserHelper $userHelper,
private OwnerProvider $ownerProvider,
private RouteHelper $routeHelper,
private TranslatorInterface $translator,
) {
}
/**
* @return int[]
*
* @throws ObjectNotSupportedException
*/
public function getUserIds(string $mauticObject, int $id): array
{
$owners = $this->ownerProvider->getOwnersForObjectIds($mauticObject, [$id]);
if (!empty($owners[0]['owner_id'])) {
return [(int) $owners[0]['owner_id']];
}
return $this->userHelper->getAdminUsers();
}
public function buildLink(string $mauticObject, int $id, string $linkText): string
{
return $this->routeHelper->getLink($mauticObject, $id, $linkText);
}
public function formatHeader(string $integrationDisplayName, string $objectDisplayName): string
{
return $this->translator->trans(
'mautic.integration.sync.user_notification.header',
[
'%integration%' => $integrationDisplayName,
'%object%' => $objectDisplayName,
]
);
}
public function formatMessage(string $message, string $link): string
{
return $this->translator->trans(
'mautic.integration.sync.user_notification.sync_error',
[
'%name%' => $link,
'%message%' => $message,
]
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Helper;
use Doctrine\ORM\ORMException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\Notification\Writer;
class UserNotificationHelper
{
public function __construct(
private Writer $writer,
private UserNotificationBuilder $userNotificationBuilder,
) {
}
/**
* @throws ORMException
* @throws ObjectNotSupportedException
*/
public function writeNotification(
string $message,
string $integrationDisplayName,
string $objectDisplayName,
string $mauticObject,
int $id,
string $linkText,
?string $deduplicateValue = null,
?\DateTime $deduplicateDateTimeFrom = null,
): void {
$link = $this->userNotificationBuilder->buildLink($mauticObject, $id, $linkText);
$userIds = $this->userNotificationBuilder->getUserIds($mauticObject, $id);
foreach ($userIds as $userId) {
$this->writer->writeUserNotification(
$this->userNotificationBuilder->formatHeader($integrationDisplayName, $objectDisplayName),
$this->userNotificationBuilder->formatMessage($message, $link),
$userId,
$deduplicateValue,
$deduplicateDateTimeFrom
);
}
}
}

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification\Helper;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\Notification\Writer;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserSummaryNotificationHelper
{
private array $userNotifications = [];
private ?string $integrationDisplayName = null;
/**
* @var string
*/
private $objectDisplayName;
private ?string $mauticObject = null;
private ?string $listTranslationKey = null;
public function __construct(
private Writer $writer,
private UserHelper $userHelper,
private OwnerProvider $ownerProvider,
private RouteHelper $routeHelper,
private TranslatorInterface $translator,
) {
}
/**
* @throws ObjectNotSupportedException
* @throws \Doctrine\ORM\ORMException
*/
public function writeNotifications(string $mauticObject, string $listTranslationKey): void
{
$this->mauticObject = $mauticObject;
$this->listTranslationKey = $listTranslationKey;
if (empty($this->userNotifications)) {
return;
}
foreach ($this->userNotifications as $integrationDisplayName => $integrationNotifications) {
foreach ($integrationNotifications as $objectDisplayName => $objectNotifications) {
$this->integrationDisplayName = $integrationDisplayName;
$this->objectDisplayName = $objectDisplayName;
$this->findAndSendToUsers($objectNotifications);
}
}
$this->userNotifications = [];
}
public function storeSummaryNotification(string $integrationDisplayName, string $objectDisplayName, int $id): void
{
if (!isset($this->userNotifications[$integrationDisplayName])) {
$this->userNotifications[$integrationDisplayName] = [];
}
if (!isset($this->userNotifications[$integrationDisplayName][$objectDisplayName])) {
$this->userNotifications[$integrationDisplayName][$objectDisplayName] = [];
}
$this->userNotifications[$integrationDisplayName][$objectDisplayName][$id] = $id;
}
/**
* @throws \Doctrine\ORM\ORMException
* @throws ObjectNotSupportedException
*/
private function findAndSendToUsers(array $ids): void
{
$results = $this->ownerProvider->getOwnersForObjectIds($this->mauticObject, $ids);
$owners = [];
// Group by owner ID.
foreach ($results as $result) {
$ownerId = $result['owner_id'];
if (!isset($owners[$ownerId])) {
$owners[$ownerId] = [];
}
$owners[$ownerId][] = (int) $result['id'];
}
foreach ($owners as $userId => $ownedObjectIds) {
// Keep track of who is left over to send to admins instead
$ids = array_diff($ids, $ownedObjectIds);
$this->writeNotification($ownedObjectIds, $userId);
}
if (count($ids)) {
// Send the rest to admins
$adminUserIds = $this->userHelper->getAdminUsers();
foreach ($adminUserIds as $userId) {
$this->writeNotification($ids, $userId);
}
}
}
/**
* @throws ObjectNotSupportedException
* @throws \Doctrine\ORM\ORMException
*/
private function writeNotification(array $ids, int $userId): void
{
$count = count($ids);
if ($count > 25) {
$this->writer->writeUserNotification(
$this->translator->trans(
'mautic.integration.sync.user_notification.header',
[
'%integration%' => $this->integrationDisplayName,
'%object%' => ucfirst($this->objectDisplayName),
]
),
$this->translator->trans(
'mautic.integration.sync.user_notification.count_message',
['%count%' => $count]
),
$userId
);
return;
}
$this->writer->writeUserNotification(
$this->translator->trans(
'mautic.integration.sync.user_notification.header',
[
'%integration%' => $this->integrationDisplayName,
'%object%' => ucfirst($this->objectDisplayName),
]
),
$this->translator->trans(
$this->listTranslationKey,
[
'%contacts%' => $this->routeHelper->getLinkCsv($this->mauticObject, $ids),
]
),
$userId
);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification;
use Mautic\IntegrationsBundle\Exception\IntegrationNotFoundException;
use Mautic\IntegrationsBundle\Helper\ConfigIntegrationsHelper;
use Mautic\IntegrationsBundle\Helper\SyncIntegrationsHelper;
use Mautic\IntegrationsBundle\Integration\Interfaces\ConfigFormSyncInterface;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\NotificationDAO;
use Mautic\IntegrationsBundle\Sync\Exception\HandlerNotSupportedException;
use Mautic\IntegrationsBundle\Sync\Notification\Handler\HandlerContainer;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Symfony\Contracts\Translation\TranslatorInterface;
class Notifier
{
public function __construct(
private HandlerContainer $handlerContainer,
private SyncIntegrationsHelper $syncIntegrationsHelper,
private ConfigIntegrationsHelper $configIntegrationsHelper,
private TranslatorInterface $translator,
) {
}
/**
* @param NotificationDAO[] $notifications
* @param string $integrationHandler
*
* @throws HandlerNotSupportedException
* @throws IntegrationNotFoundException
*/
public function noteMauticSyncIssue(array $notifications, $integrationHandler = MauticSyncDataExchange::NAME): void
{
foreach ($notifications as $notification) {
$handler = $this->handlerContainer->getHandler($integrationHandler, $notification->getMauticObject());
$integrationDisplayName = $this->syncIntegrationsHelper->getIntegration($notification->getIntegration())->getDisplayName();
$objectDisplayName = $this->getObjectDisplayName($notification->getIntegration(), $notification->getIntegrationObject());
$handler->writeEntry($notification, $integrationDisplayName, $objectDisplayName);
}
}
/**
* Finalizes notifications such as pushing summary entries to the user notifications.
*/
public function finalizeNotifications(): void
{
foreach ($this->handlerContainer->getHandlers() as $handler) {
$handler->finalize();
}
}
private function getObjectDisplayName(string $integration, string $object): string
{
try {
$configIntegration = $this->configIntegrationsHelper->getIntegration($integration);
} catch (IntegrationNotFoundException) {
return ucfirst($object);
}
if (!$configIntegration instanceof ConfigFormSyncInterface) {
return ucfirst($object);
}
$objects = $configIntegration->getSyncConfigObjects();
if (!isset($objects[$object])) {
return ucfirst($object);
}
return $this->translator->trans($objects[$object]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\Notification;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\CoreBundle\Model\NotificationModel;
use Mautic\UserBundle\Entity\User;
class Writer
{
public function __construct(
private NotificationModel $notificationModel,
private AuditLogModel $auditLogModel,
private EntityManagerInterface $em,
) {
}
/**
* @throws \Doctrine\ORM\ORMException
*/
public function writeUserNotification(string $header, string $message, int $userId, ?string $deduplicateValue = null, ?\DateTime $deduplicateDateTimeFrom = null): void
{
$this->notificationModel->addNotification(
$message,
null,
false,
$header,
'ri-refresh-line',
null,
$this->em->getReference(User::class, $userId),
$deduplicateValue,
$deduplicateDateTimeFrom
);
}
public function writeAuditLogEntry(string $bundle, string $object, ?int $objectId, string $action, array $details): void
{
$log = [
'bundle' => $bundle,
'object' => $object,
'objectId' => $objectId,
'action' => $action,
'details' => $details,
];
$this->auditLogModel->writeToLog($log);
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Helper;
use Mautic\ChannelBundle\Helper\ChannelListHelper;
use Mautic\IntegrationsBundle\Event\MauticSyncFieldsLoadEvent;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\EncodedValueDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object\Contact;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectProvider;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\IntegrationsBundle\Sync\VariableExpresser\VariableExpresserHelperInterface;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\LeadBundle\Model\LeadModel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class FieldHelper
{
private array $fieldList = [];
private array $requiredFieldList = [];
private array $syncFields = [];
/**
* @var EventDispatcher
*/
private $eventDispatcher;
public function __construct(
private FieldModel $fieldModel,
private FieldsWithUniqueIdentifier $fieldWithUniqueIdentifier,
private VariableExpresserHelperInterface $variableExpresserHelper,
private ChannelListHelper $channelListHelper,
private TranslatorInterface $translator,
EventDispatcherInterface $eventDispatcher,
private ObjectProvider $objectProvider,
) {
$this->eventDispatcher = $eventDispatcher;
}
public function getFieldList(string $object): array
{
if (!isset($this->fieldList[$object])) {
$this->fieldList[$object] = $this->fieldModel->getFieldListWithProperties($object);
}
return $this->fieldList[$object];
}
public function getNormalizedFieldType(string $type): string
{
return match ($type) {
'boolean' => NormalizedValueDAO::BOOLEAN_TYPE,
'date', 'datetime', 'time' => NormalizedValueDAO::DATETIME_TYPE,
'number' => NormalizedValueDAO::FLOAT_TYPE,
'select' => NormalizedValueDAO::SELECT_TYPE,
'multiselect' => NormalizedValueDAO::MULTISELECT_TYPE,
default => NormalizedValueDAO::STRING_TYPE,
};
}
/**
* @throws ObjectNotSupportedException
*/
public function getFieldObjectName(string $objectName): string
{
try {
return $this->objectProvider->getObjectByName($objectName)->getEntityName();
} catch (ObjectNotFoundException) {
// Throwing different exception to keep BC.
throw new ObjectNotSupportedException(MauticSyncDataExchange::NAME, $objectName);
}
}
public function getFieldChangeObject(array $fieldChange): FieldDAO
{
$changeTimestamp = new \DateTimeImmutable($fieldChange['modified_at'], new \DateTimeZone('UTC'));
$columnType = $fieldChange['column_type'];
$columnValue = $fieldChange['column_value'];
$newValue = $this->variableExpresserHelper->decodeVariable(new EncodedValueDAO($columnType, $columnValue));
$reportFieldDAO = new FieldDAO($fieldChange['column_name'], $newValue);
$reportFieldDAO->setChangeDateTime($changeTimestamp);
return $reportFieldDAO;
}
public function getSyncFields(string $objectName): array
{
if (isset($this->syncFields[$objectName])) {
return $this->syncFields[$objectName];
}
$this->syncFields[$objectName] = $this->fieldModel->getFieldList(
false,
true,
[
'isPublished' => true,
'object' => $objectName,
]
);
// Dispatch event to add possibility to add field from some listener
$event = new MauticSyncFieldsLoadEvent($objectName, $this->syncFields[$objectName]);
$event = $this->eventDispatcher->dispatch($event, IntegrationEvents::INTEGRATION_MAUTIC_SYNC_FIELDS_LOAD);
$this->syncFields[$event->getObjectName()] = $event->getFields();
// Add ID as a read only field
$this->syncFields[$objectName]['mautic_internal_id'] = $this->translator->trans('mautic.core.id');
if (Contact::NAME !== $objectName) {
uksort($this->syncFields[$objectName], 'strnatcmp');
return $this->syncFields[$objectName];
}
// Mautic contacts have "pseudo" fields such as channel do not contact, timeline, etc.
$channels = $this->channelListHelper->getFeatureChannels([LeadModel::CHANNEL_FEATURE], true);
foreach ($channels as $label => $channel) {
$this->syncFields[$objectName]['mautic_internal_dnc_'.$channel] = $this->translator->trans('mautic.integration.sync.channel_dnc', ['%channel%' => $label]);
}
// Add the timeline link
$this->syncFields[$objectName]['mautic_internal_contact_timeline'] = $this->translator->trans('mautic.integration.sync.contact_timeline');
uksort($this->syncFields[$objectName], 'strnatcmp');
return $this->syncFields[$objectName];
}
/**
* @return mixed[]
*/
public function getRequiredFields(string $object): array
{
if (isset($this->requiredFieldList[$object])) {
return $this->requiredFieldList[$object];
}
$requiredFields = $this->fieldModel->getFieldList(
false,
false,
[
'isPublished' => true,
'isRequired' => true,
'object' => $object,
]
);
// We don't use unique identifier field for companies.
if ('company' === $object) {
$this->requiredFieldList[$object] = $requiredFields;
return $this->requiredFieldList[$object];
}
$uniqueIdentifierFields = $this->fieldWithUniqueIdentifier->getFieldsWithUniqueIdentifier(
[
'isPublished' => true,
'object' => $object,
]
);
$this->requiredFieldList[$object] = array_merge($requiredFields, $uniqueIdentifierFields);
return $this->requiredFieldList[$object];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner\Exception;
class FieldSchemaNotFoundException extends \Exception
{
public function __construct(string $object, string $alias)
{
parent::__construct(sprintf('Schema for alias "%s" of object "%s" not found', $alias, $object));
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner\Exception;
class ReferenceNotFoundException extends \Exception
{
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
use Mautic\IntegrationsBundle\Sync\Notification\BulkNotification;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner\Exception\FieldSchemaNotFoundException;
use Mautic\LeadBundle\Entity\LeadFieldRepository;
use Mautic\LeadBundle\Field\SchemaDefinition;
final class FieldValidator implements FieldValidatorInterface
{
/**
* @var mixed[]
*/
private array $fieldSchemaData = [];
public function __construct(
private LeadFieldRepository $leadFieldRepository,
private BulkNotification $bulkNotification,
) {
}
/**
* @param ObjectChangeDAO[] $changedObjects
*/
public function validateFields(string $objectName, array $changedObjects): void
{
foreach ($changedObjects as $changedObject) {
foreach ($changedObject->getFields() as $field) {
$fieldName = $field->getName();
try {
$schema = $this->getFieldSchema($objectName, $fieldName);
} catch (FieldSchemaNotFoundException) {
continue;
}
$fieldValue = $field->getValue();
$normalizedValue = $fieldValue->getNormalizedValue();
$schemaDefinition = SchemaDefinition::getSchemaDefinition(
$schema['alias'],
$schema['type'],
$schema['isUniqueIdentifer'],
$schema['charLengthLimit']
);
if (is_string($normalizedValue) && !$this->isFieldLengthValid($schemaDefinition, $normalizedValue)) {
$changedObject->removeField($fieldName);
$message = sprintf("Custom field '%s' with value '%s' exceeded maximum allowed length and was ignored during the sync.", $schema['label'], $normalizedValue);
$this->addNotification($message, $changedObject, $fieldName, 'length');
continue;
}
if (!$this->isFieldTypeValid($schemaDefinition, $fieldValue)) {
$changedObject->removeField($fieldName);
$message = sprintf("Custom field '%s' of type '%s' did not match integration type '%s' and was ignored during the sync.", $schema['label'], $schema['type'], $fieldValue->getType());
$this->addNotification($message, $changedObject, $fieldName, 'type');
continue;
}
}
}
$this->bulkNotification->flush();
}
/**
* @param mixed[] $schemaDefinition
*/
private function isFieldLengthValid(array $schemaDefinition, string $normalizedValue): bool
{
$schemaLength = SchemaDefinition::getFieldCharLengthLimit($schemaDefinition);
if (null === $schemaLength) {
return true;
}
$actualLength = mb_strlen($normalizedValue);
return $actualLength <= $schemaLength;
}
/**
* @param mixed[] $schemaDefinition
*/
private function isFieldTypeValid(array $schemaDefinition, NormalizedValueDAO $field): bool
{
return match ($schemaDefinition['type']) {
'date', 'datetime', 'time', 'boolean' => $field->getType() === $schemaDefinition['type'],
'float' => in_array($field->getType(), [
NormalizedValueDAO::DOUBLE_TYPE,
NormalizedValueDAO::FLOAT_TYPE,
NormalizedValueDAO::INT_TYPE,
]),
default => true,
};
}
/**
* @return mixed[]
*
* @throws FieldSchemaNotFoundException
*/
private function getFieldSchema(string $object, string $alias): array
{
if (!isset($this->fieldSchemaData[$object])) {
$this->fieldSchemaData[$object] = $this->leadFieldRepository->getFieldSchemaData($object);
}
if (!isset($this->fieldSchemaData[$object][$alias])) {
throw new FieldSchemaNotFoundException($object, $alias);
}
return $this->fieldSchemaData[$object][$alias];
}
private function addNotification(string $message, ObjectChangeDAO $changedObject, string $fieldName, string $type): void
{
$integrationName = $changedObject->getIntegration();
$integrationObjectName = $changedObject->getObject();
$integrationObjectId = $changedObject->getMappedObjectId();
$deduplicateValue = $integrationName.'-'.$integrationObjectName.'-'.$fieldName.'-'.$type;
$this->bulkNotification->addNotification(
$deduplicateValue,
sprintf('%s Your %s integration plugin may be configured improperly.', $message, $integrationName),
$integrationName,
sprintf('%s %s', $integrationObjectId, $integrationObjectName),
$integrationObjectName,
0,
sprintf('%s %s %s', $integrationName, $integrationObjectName, $integrationObjectId)
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
interface FieldValidatorInterface
{
/**
* @param ObjectChangeDAO[] $changedObjects
*/
public function validateFields(string $objectName, array $changedObjects): void;
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner;
use Mautic\IntegrationsBundle\Event\InternalObjectCreateEvent;
use Mautic\IntegrationsBundle\Event\InternalObjectUpdateEvent;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectMappingsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\OrderDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Helper\MappingHelper;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectProvider;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class OrderExecutioner
{
public function __construct(
private MappingHelper $mappingHelper,
private EventDispatcherInterface $dispatcher,
private ObjectProvider $objectProvider,
private ReferenceResolverInterface $referenceResolver,
private FieldValidatorInterface $fieldValidator,
) {
}
public function execute(OrderDAO $syncOrderDAO): ObjectMappingsDAO
{
$identifiedObjects = $syncOrderDAO->getIdentifiedObjects();
$unidentifiedObjects = $syncOrderDAO->getUnidentifiedObjects();
$objectMappings = new ObjectMappingsDAO();
foreach ($identifiedObjects as $objectName => $updateObjects) {
$this->referenceResolver->resolveReferences($objectName, $updateObjects);
$this->fieldValidator->validateFields($objectName, $updateObjects);
$this->updateObjects($objectMappings, $objectName, $updateObjects, $syncOrderDAO);
}
foreach ($unidentifiedObjects as $objectName => $createObjects) {
$this->referenceResolver->resolveReferences($objectName, $createObjects);
$this->fieldValidator->validateFields($objectName, $createObjects);
$this->createObjects($objectMappings, $objectName, $createObjects);
}
return $objectMappings;
}
/**
* @param ObjectChangeDAO[] $updateObjects
*/
private function updateObjects(ObjectMappingsDAO $objectMappings, string $objectName, array $updateObjects, OrderDAO $syncOrderDAO): void
{
$updateCount = count($updateObjects);
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Updating %d %s object(s)',
$updateCount,
$objectName
),
self::class.':'.__FUNCTION__
);
if (0 === $updateCount) {
return;
}
try {
$event = new InternalObjectUpdateEvent(
$this->objectProvider->getObjectByName($objectName),
$syncOrderDAO->getIdentifiedObjectIds($objectName),
$updateObjects
);
} catch (ObjectNotFoundException) {
DebugLogger::log(
MauticSyncDataExchange::NAME,
$objectName,
self::class.':'.__FUNCTION__
);
return;
}
$this->dispatcher->dispatch($event, IntegrationEvents::INTEGRATION_UPDATE_INTERNAL_OBJECTS);
$updatedObjectMappings = $event->getUpdatedObjectMappings();
$this->mappingHelper->updateObjectMappings($updatedObjectMappings);
// The ObjectMapping entity is pushed into UpdatedObjectMappingDAO in MappingHelper::updateObjectMapping in order
// to make it available to the IntegrationEvents::INTEGRATION_BATCH_SYNC_COMPLETED_* events
foreach ($updatedObjectMappings as $updatedObjectMapping) {
if (!$updatedObjectMapping->getObjectMapping()) {
continue;
}
$objectMappings->addUpdatedObjectMapping($updatedObjectMapping->getObjectMapping());
}
}
/**
* @param ObjectChangeDAO[] $createObjects
*/
private function createObjects(ObjectMappingsDAO $objectMappings, string $objectName, array $createObjects): void
{
$createCount = count($createObjects);
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Creating %d %s object(s)',
$createCount,
$objectName
),
self::class.':'.__FUNCTION__
);
if (0 === $createCount) {
return;
}
try {
$event = new InternalObjectCreateEvent(
$this->objectProvider->getObjectByName($objectName),
$createObjects
);
} catch (ObjectNotFoundException) {
DebugLogger::log(
MauticSyncDataExchange::NAME,
$objectName,
self::class.':'.__FUNCTION__
);
return;
}
$this->dispatcher->dispatch($event, IntegrationEvents::INTEGRATION_CREATE_INTERNAL_OBJECTS);
$createdObjectMappings = $event->getObjectMappings();
$this->mappingHelper->saveObjectMappings($createdObjectMappings);
// Make ObjectMappings available to the IntegrationEvents::INTEGRATION_BATCH_SYNC_COMPLETED_* events
foreach ($createdObjectMappings as $createdObjectMapping) {
$objectMappings->addNewObjectMapping($createdObjectMapping);
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner;
use Doctrine\DBAL\Connection;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\ReferenceValueDAO;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner\Exception\ReferenceNotFoundException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object\Contact;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
final class ReferenceResolver implements ReferenceResolverInterface
{
public function __construct(
private Connection $connection,
) {
}
/**
* @param ObjectChangeDAO[] $changedObjects
*/
public function resolveReferences(string $objectName, array $changedObjects): void
{
if (Contact::NAME !== $objectName) {
DebugLogger::log(
'N/A',
sprintf(
'references are currently resolved only for %s. Given %s',
Contact::NAME,
$objectName
),
__CLASS__.':'.__FUNCTION__
);
return;
}
foreach ($changedObjects as $changedObject) {
foreach ($changedObject->getFields() as $field) {
$value = $field->getValue();
$normalizedValue = $value->getNormalizedValue();
if (!$normalizedValue instanceof ReferenceValueDAO) {
continue;
}
try {
$resolvedReference = $this->resolveReference($normalizedValue);
} catch (ReferenceNotFoundException) {
$resolvedReference = null;
}
$resolvedValue = new NormalizedValueDAO($value->getType(), $resolvedReference, $resolvedReference);
$changedObject->addField(new FieldDAO($field->getName(), $resolvedValue));
}
}
}
/**
* @throws ReferenceNotFoundException
*/
private function resolveReference(ReferenceValueDAO $value): ?string
{
if (MauticSyncDataExchange::OBJECT_COMPANY === $value->getType() && 0 < $value->getValue()) {
return $this->getCompanyNameById($value->getValue());
}
return null;
}
/**
* @throws ReferenceNotFoundException
*/
private function getCompanyNameById(int $id): string
{
$qb = $this->connection->createQueryBuilder();
$qb->select('c.companyname');
$qb->from(MAUTIC_TABLE_PREFIX.'companies', 'c');
$qb->where('c.id = :id');
$qb->setParameter('id', $id);
$name = $qb->executeQuery()->fetchOne();
if (false === $name) {
throw new ReferenceNotFoundException(sprintf('Company reference for ID "%d" not found', $id));
}
return $name;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
interface ReferenceResolverInterface
{
/**
* @param ObjectChangeDAO[] $changedObjects
*/
public function resolveReferences(string $objectName, array $changedObjects): void;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object;
use Mautic\LeadBundle\Entity\Company as CompanyEntity;
final class Company implements ObjectInterface
{
public const NAME = 'company';
public const ENTITY = CompanyEntity::class;
public function getName(): string
{
return self::NAME;
}
public function getEntityName(): string
{
return self::ENTITY;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object;
use Mautic\LeadBundle\Entity\Lead;
final class Contact implements ObjectInterface
{
public const NAME = 'lead'; // kept as lead for BC
public const ENTITY = Lead::class;
public function getName(): string
{
return self::NAME;
}
public function getEntityName(): string
{
return self::ENTITY;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object;
interface ObjectInterface
{
/**
* Returns name key of the object.
*/
public function getName(): string;
/**
* Returns full Doctrine entity class name of the object.
*/
public function getEntityName(): string;
}

View File

@@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectHelper;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\UpdatedObjectMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\CompanyRepository;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\Model\CompanyModel;
class CompanyObjectHelper implements ObjectHelperInterface
{
/**
* @var string[]|null
*/
private ?array $uniqueIdentifierFields = null;
/**
* @var array<string,Company>
*/
private array $companiesCreated = [];
public function __construct(
private CompanyModel $model,
private CompanyRepository $repository,
private Connection $connection,
private FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
) {
}
/**
* @param ObjectChangeDAO[] $objects
*
* @return ObjectMapping[]
*/
public function create(array $objects): array
{
$objectMappings = [];
foreach ($objects as $object) {
$fields = $object->getFields();
$company = $this->getCompanyEntity($fields);
foreach ($fields as $field) {
$company->addUpdatedField($field->getName(), $field->getValue()->getNormalizedValue());
}
$this->model->saveEntity($company);
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Created company ID %d',
$company->getId()
),
self::class.':'.__FUNCTION__
);
$objectMapping = new ObjectMapping();
$objectMapping->setLastSyncDate($object->getChangeDateTime())
->setIntegration($object->getIntegration())
->setIntegrationObjectName($object->getMappedObject())
->setIntegrationObjectId($object->getMappedObjectId())
->setInternalObjectName(MauticSyncDataExchange::OBJECT_COMPANY)
->setInternalObjectId($company->getId());
$objectMappings[] = $objectMapping;
}
// Detach to free RAM after all companies are processed in case there are duplicates in the same batch
foreach ($this->companiesCreated as $company) {
$this->repository->detachEntity($company);
}
// Reset companies created for the next batch
$this->companiesCreated = [];
return $objectMappings;
}
/**
* @param ObjectChangeDAO[] $objects
*
* @return UpdatedObjectMappingDAO[]
*/
public function update(array $ids, array $objects): array
{
$updatedMappedObjects = [];
if (!$ids) {
return $updatedMappedObjects;
}
/** @var Company[] $companies */
$companies = $this->model->getEntities(['ids' => $ids]);
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Found %d companies to update with ids %s',
count($companies),
implode(', ', $ids)
),
self::class.':'.__FUNCTION__
);
foreach ($companies as $company) {
/** @var ObjectChangeDAO $changedObject */
$changedObject = $objects[$company->getId()];
$fields = $changedObject->getFields();
foreach ($fields as $field) {
$company->addUpdatedField($field->getName(), $field->getValue()->getNormalizedValue());
}
$this->model->saveEntity($company);
$this->repository->detachEntity($company);
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Updated company ID %d',
$company->getId()
),
self::class.':'.__FUNCTION__
);
// Integration name and ID are stored in the change's mappedObject/mappedObjectId
$updatedMappedObjects[] = new UpdatedObjectMappingDAO(
$changedObject->getIntegration(),
$changedObject->getMappedObject(),
$changedObject->getMappedObjectId(),
$changedObject->getChangeDateTime()
);
}
return $updatedMappedObjects;
}
/**
* Unfortunately the CompanyRepository doesn't give us what we need so we have to write our own queries.
*
* @param int $start
* @param int $limit
*/
public function findObjectsBetweenDates(\DateTimeInterface $from, \DateTimeInterface $to, $start, $limit): array
{
$qb = $this->connection->createQueryBuilder();
$qb->select('*')
->from(MAUTIC_TABLE_PREFIX.'companies', 'c')
->where(
$qb->expr()->or(
$qb->expr()->and(
$qb->expr()->isNotNull('c.date_modified'),
$qb->expr()->comparison('c.date_modified', 'BETWEEN', ':dateFrom and :dateTo')
),
$qb->expr()->and(
$qb->expr()->isNull('c.date_modified'),
$qb->expr()->comparison('c.date_added', 'BETWEEN', ':dateFrom and :dateTo')
)
)
)
->setParameter('dateFrom', $from->format('Y-m-d H:i:s'))
->setParameter('dateTo', $to->format('Y-m-d H:i:s'))
->setFirstResult($start)
->setMaxResults($limit);
return $qb->executeQuery()->fetchAllAssociative();
}
public function findObjectsByIds(array $ids): array
{
if (!count($ids)) {
return [];
}
$qb = $this->connection->createQueryBuilder();
$qb->select('*')
->from(MAUTIC_TABLE_PREFIX.'companies', 'c')
->where(
$qb->expr()->in('id', $ids)
);
return $qb->executeQuery()->fetchAllAssociative();
}
public function findObjectsByFieldValues(array $fields): array
{
$q = $this->connection->createQueryBuilder()
->select('c.id')
->from(MAUTIC_TABLE_PREFIX.'companies', 'c');
foreach ($fields as $col => $val) {
// Use andWhere because Mautic treats conflicting unique identifiers as different objects
$q->{$this->repository->getUniqueIdentifiersWherePart()}("c.$col = :".$col)
->setParameter($col, $val);
}
return $q->executeQuery()->fetchAllAssociative();
}
public function findOwnerIds(array $objectIds): array
{
if (empty($objectIds)) {
return [];
}
$qb = $this->connection->createQueryBuilder();
$qb->select('c.owner_id, c.id');
$qb->from(MAUTIC_TABLE_PREFIX.'companies', 'c');
$qb->where('c.owner_id IS NOT NULL');
$qb->andWhere('c.id IN (:objectIds)');
$qb->setParameter('objectIds', $objectIds, ArrayParameterType::INTEGER);
return $qb->executeQuery()->fetchAllAssociative();
}
public function findObjectById(int $id): ?Company
{
return $this->repository->getEntity($id);
}
public function setFieldValues(Company $company): void
{
$this->model->setFieldValues($company, []);
}
/**
* @return string[]
*/
private function getUniqueIdentifierFields(): array
{
if (null === $this->uniqueIdentifierFields) {
$uniqueIdentifierFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier(['object' => MauticSyncDataExchange::OBJECT_COMPANY]);
$this->uniqueIdentifierFields = array_keys($uniqueIdentifierFields);
}
return $this->uniqueIdentifierFields;
}
/**
* @param FieldDAO[] $fields
*/
private function getCompanyEntity(array $fields): Company
{
$uniqueIdentifierFields = $this->getUniqueIdentifierFields();
// Create a key based on the concatenation of unique identifier values
$companyKey = '';
foreach ($uniqueIdentifierFields as $uniqueIdentifierField) {
if (isset($fields[$uniqueIdentifierField])) {
$companyKey .= strtolower($fields[$uniqueIdentifierField]->getValue()->getNormalizedValue());
}
}
// Check if a company with matching values was created in the same batch as another
if (!empty($companyKey) && isset($this->companiesCreated[$companyKey])) {
return $this->companiesCreated[$companyKey];
}
// Create a new company but ensure a unique key
$companyKey = $companyKey ?: uniqid();
return $this->companiesCreated[$companyKey] = new Company();
}
}

View File

@@ -0,0 +1,396 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectHelper;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\UpdatedObjectMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object\Contact;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\LeadBundle\DataObject\LeadManipulator;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadRepository;
use Mautic\LeadBundle\Exception\ImportFailedException;
use Mautic\LeadBundle\Field\FieldList;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
use Mautic\LeadBundle\Model\LeadModel;
class ContactObjectHelper implements ObjectHelperInterface
{
private ?array $availableFields = null;
/**
* @var string[]|null
*/
private ?array $uniqueIdentifierFields = null;
/**
* @var array<string,Lead>
*/
private array $contactsCreated = [];
public function __construct(
private LeadModel $model,
private LeadRepository $repository,
private Connection $connection,
private DoNotContactModel $dncModel,
private FieldList $fieldList,
private FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
) {
}
/**
* @param ObjectChangeDAO[] $objects
*
* @return ObjectMapping[]
*/
public function create(array $objects): array
{
$availableFields = $this->getAvailableFields();
$objectMappings = [];
foreach ($objects as $object) {
$fields = $object->getFields();
$contact = $this->getContactEntity($fields);
$pseudoFields = [];
foreach ($fields as $field) {
if (in_array($field->getName(), $availableFields)) {
$contact->addUpdatedField($field->getName(), $field->getValue()->getNormalizedValue());
} else {
$pseudoFields[$field->getName()] = $field;
}
}
$contact->setManipulator(new LeadManipulator('integrations', 'create'));
// Create the contact before processing pseudo fields
$this->model->saveEntity($contact);
// Process the pseudo field
$this->processPseudoFields($contact, $pseudoFields, $object->getIntegration());
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Created lead ID %d',
$contact->getId()
),
self::class.':'.__FUNCTION__
);
$objectMapping = new ObjectMapping();
$objectMapping->setLastSyncDate($object->getChangeDateTime())
->setIntegration($object->getIntegration())
->setIntegrationObjectName($object->getMappedObject())
->setIntegrationObjectId($object->getMappedObjectId())
->setInternalObjectName(Contact::NAME)
->setInternalObjectId($contact->getId());
$objectMappings[] = $objectMapping;
}
// Detach to free RAM after all contacts are processed in case there are duplicates in the same batch
foreach ($this->contactsCreated as $contact) {
$this->repository->detachEntity($contact);
}
// Reset contacts created for the next batch
$this->contactsCreated = [];
return $objectMappings;
}
/**
* @param ObjectChangeDAO[] $objects
*
* @return UpdatedObjectMappingDAO[]
*/
public function update(array $ids, array $objects): array
{
/** @var Lead[] $contacts */
$contacts = $this->model->getEntities(['ids' => $ids]);
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Found %d leads to update with ids %s',
count($contacts),
implode(', ', $ids)
),
self::class.':'.__FUNCTION__
);
$availableFields = $this->getAvailableFields();
$updatedMappedObjects = [];
foreach ($contacts as $contact) {
/** @var ObjectChangeDAO $changedObject */
$changedObject = $objects[$contact->getId()];
$fields = $changedObject->getFields();
$pseudoFields = [];
foreach ($fields as $field) {
if (in_array($field->getName(), $availableFields)) {
$contact->addUpdatedField($field->getName(), $field->getValue()->getNormalizedValue());
} else {
$pseudoFields[$field->getName()] = $field;
}
}
$contact->setManipulator(new LeadManipulator('integrations', 'update'));
// Create the contact before processing pseudo fields
$this->model->saveEntity($contact);
// Process the pseudo field
$this->processPseudoFields($contact, $pseudoFields, $changedObject->getIntegration());
$this->repository->detachEntity($contact);
DebugLogger::log(
MauticSyncDataExchange::NAME,
sprintf(
'Updated lead ID %d',
$contact->getId()
),
self::class.':'.__FUNCTION__
);
// Integration name and ID are stored in the change's mappedObject/mappedObjectId
$updatedMappedObjects[] = new UpdatedObjectMappingDAO(
$changedObject->getIntegration(),
$changedObject->getMappedObject(),
$changedObject->getMappedObjectId(),
$changedObject->getChangeDateTime()
);
}
return $updatedMappedObjects;
}
/**
* Unfortunately the LeadRepository doesn't give us what we need so we have to write our own queries.
*
* @param int $start
* @param int $limit
*/
public function findObjectsBetweenDates(\DateTimeInterface $from, \DateTimeInterface $to, $start, $limit): array
{
$qb = $this->connection->createQueryBuilder();
$qb->select('*')
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
->where(
$qb->expr()->and(
$qb->expr()->isNotNull('l.date_identified'),
$qb->expr()->or(
$qb->expr()->and(
$qb->expr()->isNotNull('l.date_modified'),
$qb->expr()->gte('l.date_modified', ':dateFrom'),
$qb->expr()->lt('l.date_modified', ':dateTo')
),
$qb->expr()->and(
$qb->expr()->isNull('l.date_modified'),
$qb->expr()->gte('l.date_added', ':dateFrom'),
$qb->expr()->lt('l.date_added', ':dateTo')
)
)
)
)
->setParameter('dateFrom', $from->format('Y-m-d H:i:s'))
->setParameter('dateTo', $to->format('Y-m-d H:i:s'))
->setFirstResult($start)
->setMaxResults($limit);
return $qb->executeQuery()->fetchAllAssociative();
}
public function findObjectsByIds(array $ids): array
{
if (!count($ids)) {
return [];
}
$qb = $this->connection->createQueryBuilder();
$qb->select('*')
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
->where(
$qb->expr()->in('id', $ids)
);
return $qb->executeQuery()->fetchAllAssociative();
}
public function findObjectsByFieldValues(array $fields): array
{
$q = $this->connection->createQueryBuilder()
->select('l.id')
->from(MAUTIC_TABLE_PREFIX.'leads', 'l');
foreach ($fields as $col => $val) {
// Use andWhere because Mautic treats conflicting unique identifiers as different objects
$q->{$this->repository->getUniqueIdentifiersWherePart()}("l.$col = :".$col)
->setParameter($col, $val);
}
return $q->executeQuery()->fetchAllAssociative();
}
public function getDoNotContactStatus(int $contactId, string $channel): int
{
$q = $this->connection->createQueryBuilder();
$q->select('dnc.reason')
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
->where(
$q->expr()->and(
$q->expr()->eq('dnc.lead_id', ':contactId'),
$q->expr()->eq('dnc.channel', ':channel')
)
)
->setParameter('contactId', $contactId)
->setParameter('channel', $channel)
->setMaxResults(1);
$status = $q->executeQuery()->fetchOne();
if (false === $status) {
return DoNotContact::IS_CONTACTABLE;
}
return (int) $status;
}
public function findOwnerIds(array $objectIds): array
{
if (empty($objectIds)) {
return [];
}
$qb = $this->connection->createQueryBuilder();
$qb->select('c.owner_id, c.id');
$qb->from(MAUTIC_TABLE_PREFIX.'leads', 'c');
$qb->where('c.owner_id IS NOT NULL');
$qb->andWhere('c.id IN (:objectIds)');
$qb->setParameter('objectIds', $objectIds, ArrayParameterType::INTEGER);
return $qb->executeQuery()->fetchAllAssociative();
}
public function findObjectById(int $id): ?Lead
{
return $this->repository->getEntity($id);
}
/**
* @throws ImportFailedException
*/
public function setFieldValues(Lead $lead): void
{
$this->model->setFieldValues($lead, []);
}
private function getAvailableFields(): array
{
if (null === $this->availableFields) {
$availableFields = $this->fieldList->getFieldList(false, false);
$this->availableFields = array_keys($availableFields);
}
return $this->availableFields;
}
/**
* @return string[]
*/
private function getUniqueIdentifierFields(): array
{
if (null === $this->uniqueIdentifierFields) {
$uniqueIdentifierFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier(['object' => MauticSyncDataExchange::OBJECT_CONTACT]);
$this->uniqueIdentifierFields = array_keys($uniqueIdentifierFields);
}
return $this->uniqueIdentifierFields;
}
/**
* @param FieldDAO[] $fields
*/
private function processPseudoFields(Lead $contact, array $fields, string $integration): void
{
foreach ($fields as $name => $field) {
if (str_starts_with($name, 'mautic_internal_dnc_')) {
$channel = str_replace('mautic_internal_dnc_', '', $name);
$dncReason = $this->getDoNotContactReason($field->getValue()->getNormalizedValue());
if (DoNotContact::IS_CONTACTABLE === $dncReason) {
$this->dncModel->removeDncForContact($contact->getId(), $channel);
continue;
}
$this->dncModel->addDncForContact(
$contact->getId(),
$channel,
$dncReason,
$integration,
true,
true,
true
);
}
if ('owner_id' == $name) {
$ownerId = $field->getValue()->getNormalizedValue();
$this->model->updateLeadOwner($contact, $ownerId);
}
// Ignore all others as unrecognized
}
}
private function getDoNotContactReason($value): int
{
$value = (int) $value;
if (in_array($value, [DoNotContact::BOUNCED, DoNotContact::UNSUBSCRIBED, DoNotContact::MANUAL, DoNotContact::IS_CONTACTABLE])) {
return $value;
}
// Assume manually removed
return DoNotContact::MANUAL;
}
/**
* @param FieldDAO[] $fields
*/
private function getContactEntity(array $fields): Lead
{
$uniqueIdentifierFields = $this->getUniqueIdentifierFields();
// Create a key based on the concatenation of unique identifier values
$contactKey = '';
foreach ($uniqueIdentifierFields as $uniqueIdentifierField) {
if (isset($fields[$uniqueIdentifierField])) {
$contactKey .= strtolower($fields[$uniqueIdentifierField]->getValue()->getNormalizedValue());
}
}
// Check if a contact with matching values was created in the same batch as another
if (!empty($contactKey) && isset($this->contactsCreated[$contactKey])) {
return $this->contactsCreated[$contactKey];
}
// Create a new contact but ensure a unique key
$contactKey = $contactKey ?: uniqid();
return $this->contactsCreated[$contactKey] = new Lead();
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectHelper;
use Mautic\IntegrationsBundle\Entity\ObjectMapping;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\UpdatedObjectMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
interface ObjectHelperInterface
{
/**
* @param ObjectChangeDAO[] $objects
*
* @return ObjectMapping[]
*/
public function create(array $objects): array;
/**
* @param ObjectChangeDAO[] $objects
*
* @return UpdatedObjectMappingDAO[]
*/
public function update(array $ids, array $objects): array;
/**
* @param int $start
* @param int $limit
*/
public function findObjectsBetweenDates(\DateTimeInterface $from, \DateTimeInterface $to, $start, $limit): array;
public function findObjectsByIds(array $ids): array;
public function findObjectsByFieldValues(array $fields): array;
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal;
use Mautic\IntegrationsBundle\Event\InternalObjectEvent;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Object\ObjectInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ObjectProvider
{
/**
* Cached internal objects.
*
* @var ObjectInterface[]
*/
private array $objects = [];
public function __construct(
private EventDispatcherInterface $dispatcher,
) {
}
/**
* @throws ObjectNotFoundException
*/
public function getObjectByName(string $name): ObjectInterface
{
$this->collectObjects();
foreach ($this->objects as $object) {
if ($object->getName() === $name) {
return $object;
}
}
throw new ObjectNotFoundException("Internal object '{$name}' was not found");
}
/**
* @throws ObjectNotFoundException
*/
public function getObjectByEntityName(string $entityName): ObjectInterface
{
$this->collectObjects();
foreach ($this->objects as $object) {
if ($object->getEntityName() === $entityName) {
return $object;
}
}
throw new ObjectNotFoundException("Internal object was not found for entity '{$entityName}'");
}
/**
* Dispatches an event to collect all internal objects.
* It caches the objects to a local property so it won't dispatch every time but only once.
*/
private function collectObjects(): void
{
if (empty($this->objects)) {
$event = new InternalObjectEvent();
$this->dispatcher->dispatch($event, IntegrationEvents::INTEGRATION_COLLECT_INTERNAL_OBJECTS);
$this->objects = $event->getObjects();
}
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ReportBuilder;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\FieldDAO as ReportFieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\ObjectDAO as RequestObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Helper\FieldHelper;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectHelper\ContactObjectHelper;
use Mautic\IntegrationsBundle\Sync\ValueNormalizer\ValueNormalizer;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Router;
class FieldBuilder
{
private ValueNormalizer $valueNormalizer;
private ?array $mauticObject = null;
private ?RequestObjectDAO $requestObject = null;
public function __construct(
private Router $router,
private FieldHelper $fieldHelper,
private ContactObjectHelper $contactObjectHelper,
) {
$this->valueNormalizer = new ValueNormalizer();
}
/**
* @throws FieldNotFoundException
*/
public function buildObjectField(
string $field,
array $mauticObject,
RequestObjectDAO $requestObject,
string $integration,
string $defaultState = ReportFieldDAO::FIELD_CHANGED,
): ReportFieldDAO {
$this->mauticObject = $mauticObject;
$this->requestObject = $requestObject;
// Special handling of the ID field
if ('mautic_internal_id' === $field) {
return $this->addContactIdField($field);
}
// Special handling of the owner ID field
if ('owner_id' === $field) {
return $this->createOwnerIdReportFieldDAO($field, (int) $mauticObject['owner_id']);
}
// Special handling of DNC fields
if (str_starts_with($field, 'mautic_internal_dnc_')) {
return $this->addDoNotContactField($field);
}
// Special handling of timeline URL
if ('mautic_internal_contact_timeline' === $field) {
return $this->addContactTimelineField($integration, $field);
}
return $this->addCustomField($field, $defaultState);
}
private function addContactIdField(string $field): ReportFieldDAO
{
$normalizedValue = new NormalizedValueDAO(
NormalizedValueDAO::INT_TYPE,
$this->mauticObject['id']
);
return new ReportFieldDAO($field, $normalizedValue);
}
private function createOwnerIdReportFieldDAO(string $field, int $ownerId): ReportFieldDAO
{
return new ReportFieldDAO(
$field,
new NormalizedValueDAO(
NormalizedValueDAO::INT_TYPE,
$ownerId
)
);
}
private function addDoNotContactField(string $field): ReportFieldDAO
{
$channel = str_replace('mautic_internal_dnc_', '', $field);
$normalizedValue = new NormalizedValueDAO(
NormalizedValueDAO::INT_TYPE,
$this->contactObjectHelper->getDoNotContactStatus((int) $this->mauticObject['id'], $channel)
);
return new ReportFieldDAO($field, $normalizedValue);
}
private function addContactTimelineField(string $integration, string $field): ReportFieldDAO
{
$normalizedValue = new NormalizedValueDAO(
NormalizedValueDAO::URL_TYPE,
$this->router->generate(
'mautic_plugin_timeline_view',
[
'integration' => $integration,
'leadId' => $this->mauticObject['id'],
],
UrlGeneratorInterface::ABSOLUTE_URL
)
);
return new ReportFieldDAO($field, $normalizedValue);
}
/**
* @throws FieldNotFoundException
*/
private function addCustomField(string $field, string $defaultState): ReportFieldDAO
{
// The rest should be Mautic custom fields and if not, just ignore
$mauticFields = $this->fieldHelper->getFieldList($this->requestObject->getObject());
if (!isset($mauticFields[$field])) {
// Field must have been deleted or something so let's skip
throw new FieldNotFoundException($field, $this->requestObject->getObject());
}
$requiredFields = $this->requestObject->getRequiredFields();
$fieldType = $this->fieldHelper->getNormalizedFieldType($mauticFields[$field]['type']);
$normalizedValue = $this->valueNormalizer->normalizeForMautic($fieldType, $this->mauticObject[$field]);
return new ReportFieldDAO(
$field,
$normalizedValue,
in_array($field, $requiredFields) ? ReportFieldDAO::FIELD_REQUIRED : $defaultState
);
}
}

View File

@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ReportBuilder;
use Mautic\IntegrationsBundle\Event\InternalCompanyEvent;
use Mautic\IntegrationsBundle\Event\InternalContactEvent;
use Mautic\IntegrationsBundle\Event\InternalObjectFindByIdEvent;
use Mautic\IntegrationsBundle\Event\InternalObjectFindEvent;
use Mautic\IntegrationsBundle\Exception\InvalidValueException;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\DAO\DateRange;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ObjectDAO as ReportObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\ObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\RequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectProvider;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\Lead;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class FullObjectReportBuilder
{
public function __construct(
private FieldBuilder $fieldBuilder,
private ObjectProvider $objectProvider,
private EventDispatcherInterface $dispatcher,
) {
}
public function buildReport(RequestDAO $requestDAO): ReportDAO
{
$syncReport = new ReportDAO($requestDAO->getSyncToIntegration());
$requestedObjects = $requestDAO->getObjects();
$limit = 200;
$start = $limit * ($requestDAO->getSyncIteration() - 1);
foreach ($requestedObjects as $requestedObjectDAO) {
try {
DebugLogger::log(
$requestDAO->getSyncToIntegration(),
sprintf(
'Searching for %s objects between %s and %s (%d,%d)',
$requestedObjectDAO->getObject(),
$requestedObjectDAO->getFromDateTime()->format(DATE_ATOM),
$requestedObjectDAO->getToDateTime()->format(DATE_ATOM),
$start,
$limit
),
self::class.':'.__FUNCTION__
);
$event = new InternalObjectFindEvent(
$this->objectProvider->getObjectByName($requestedObjectDAO->getObject())
);
if ($requestDAO->getInputOptionsDAO()->getMauticObjectIds()) {
$idChunks = array_chunk($requestDAO->getInputOptionsDAO()->getMauticObjectIds()->getObjectIdsFor($requestedObjectDAO->getObject()), $limit);
$idChunk = $idChunks[$requestDAO->getSyncIteration() - 1] ?? [];
$event->setIds($idChunk);
} else {
$event->setDateRange(
new DateRange(
$requestedObjectDAO->getFromDateTime(),
$requestedObjectDAO->getToDateTime()
)
);
$event->setStart($start);
$event->setLimit($limit);
}
$this->dispatcher->dispatch(
$event,
IntegrationEvents::INTEGRATION_FIND_INTERNAL_RECORDS
);
$foundObjects = $event->getFoundObjects();
$this->processObjects($requestedObjectDAO, $syncReport, $foundObjects);
} catch (ObjectNotFoundException $exception) {
DebugLogger::log(
MauticSyncDataExchange::NAME,
$exception->getMessage(),
self::class.':'.__FUNCTION__
);
}
}
return $syncReport;
}
/**
* @throws ObjectNotFoundException
*/
private function processObjects(ObjectDAO $requestedObjectDAO, ReportDAO $syncReport, array $foundObjects): void
{
$fields = $requestedObjectDAO->getFields();
if ($this->dispatcher->hasListeners(IntegrationEvents::INTEGRATION_FIND_INTERNAL_RECORD)) {
$event = new InternalObjectFindByIdEvent($this->objectProvider->getObjectByName($requestedObjectDAO->getObject()));
}
foreach ($foundObjects as $object) {
$modifiedDateTime = new \DateTime(
!empty($object['date_modified']) ? $object['date_modified'] : $object['date_added'],
new \DateTimeZone('UTC')
);
$reportObjectDAO = new ReportObjectDAO($requestedObjectDAO->getObject(), $object['id'], $modifiedDateTime);
$syncReport->addObject($reportObjectDAO);
if (isset($event)) {
// Update object id rather than creating the event again.
$event->setId((int) $object['id']);
$this->dispatcher->dispatch($event, IntegrationEvents::INTEGRATION_FIND_INTERNAL_RECORD);
if (!$event->getEntity()) {
// Object not found, continue.
continue;
}
try {
$this->dispatchBeforeFieldChangesEvent($syncReport->getIntegration(), $event->getEntity());
} catch (InvalidValueException) {
// Object is not eligible, continue.
continue;
}
}
foreach ($fields as $field) {
try {
$reportFieldDAO = $this->fieldBuilder->buildObjectField($field, $object, $requestedObjectDAO, $syncReport->getIntegration());
$reportObjectDAO->addField($reportFieldDAO);
} catch (FieldNotFoundException $exception) {
// Field is not supported so keep going
DebugLogger::log(
MauticSyncDataExchange::NAME,
$exception->getMessage(),
self::class.':'.__FUNCTION__
);
}
}
}
}
/**
* @throws InvalidValueException
*/
private function dispatchBeforeFieldChangesEvent(string $integrationName, object $object): void
{
if ($object instanceof Lead) {
if ($this->dispatcher->hasListeners(IntegrationEvents::INTEGRATION_BEFORE_FULL_CONTACT_REPORT_BUILD)) {
$this->dispatcher->dispatch(
new InternalContactEvent($integrationName, $object),
IntegrationEvents::INTEGRATION_BEFORE_FULL_CONTACT_REPORT_BUILD
);
}
return;
}
if ($object instanceof Company) {
if ($this->dispatcher->hasListeners(IntegrationEvents::INTEGRATION_BEFORE_FULL_COMPANY_REPORT_BUILD)) {
$this->dispatcher->dispatch(
new InternalCompanyEvent($integrationName, $object),
IntegrationEvents::INTEGRATION_BEFORE_FULL_COMPANY_REPORT_BUILD
);
}
return;
}
throw new InvalidValueException('An object type should be specified. None matches.');
}
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ReportBuilder;
use Mautic\IntegrationsBundle\Entity\FieldChangeRepository;
use Mautic\IntegrationsBundle\Event\InternalObjectFindEvent;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ObjectDAO as ReportObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\ObjectDAO as RequestObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\RequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Helper\FieldHelper;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ObjectProvider;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class PartialObjectReportBuilder
{
private array $reportObjects = [];
private array $lastProcessedTrackedId = [];
private array $objectsWithMissingFields = [];
private ?ReportDAO $syncReport = null;
public function __construct(
private FieldChangeRepository $fieldChangeRepository,
private FieldHelper $fieldHelper,
private FieldBuilder $fieldBuilder,
private ObjectProvider $objectProvider,
private EventDispatcherInterface $dispatcher,
) {
}
public function buildReport(RequestDAO $requestDAO): ReportDAO
{
$this->syncReport = new ReportDAO($requestDAO->getSyncToIntegration());
$requestedObjects = $requestDAO->getObjects();
foreach ($requestedObjects as $objectDAO) {
try {
if (!isset($this->lastProcessedTrackedId[$objectDAO->getObject()])) {
$this->lastProcessedTrackedId[$objectDAO->getObject()] = 0;
}
$fieldsChanges = $this->fieldChangeRepository->findChangesBefore(
$requestDAO->getSyncToIntegration(),
$this->fieldHelper->getFieldObjectName($objectDAO->getObject()),
$objectDAO->getToDateTime(),
$this->lastProcessedTrackedId[$objectDAO->getObject()]
);
$this->reportObjects = [];
foreach ($fieldsChanges as $fieldChange) {
$this->processFieldChange($fieldChange, $objectDAO);
}
try {
$incompleteObjects = $this->findObjectsWithMissingFields($objectDAO);
$this->completeObjectsWithMissingFields($incompleteObjects, $objectDAO);
} catch (ObjectNotFoundException $exception) {
// Process the others
DebugLogger::log(
$requestDAO->getSyncToIntegration(),
$exception->getMessage(),
self::class.':'.__FUNCTION__
);
}
} catch (ObjectNotFoundException $exception) {
DebugLogger::log(
$requestDAO->getSyncToIntegration(),
$exception->getMessage(),
self::class.':'.__FUNCTION__
);
}
}
return $this->syncReport;
}
/**
* @throws ObjectNotFoundException
*/
private function processFieldChange(array $fieldChange, RequestObjectDAO $objectDAO): void
{
$objectId = (int) $fieldChange['object_id'];
// Track the last processed ID to prevent loops for objects that were set to be retried later
if ($objectId > $this->lastProcessedTrackedId[$objectDAO->getObject()]) {
$this->lastProcessedTrackedId[$objectDAO->getObject()] = $objectId;
}
$object = $this->objectProvider->getObjectByEntityName($fieldChange['object_type'])->getName();
$objectId = (int) $fieldChange['object_id'];
$modifiedDateTime = new \DateTime($fieldChange['modified_at'], new \DateTimeZone('UTC'));
if (!array_key_exists($object, $this->reportObjects)) {
$this->reportObjects[$object] = [];
}
if (!array_key_exists($objectId, $this->reportObjects[$object])) {
/* @var ReportObjectDAO $reportObjectDAO */
$this->reportObjects[$object][$objectId] = $reportObjectDAO = new ReportObjectDAO($object, $objectId);
$this->syncReport->addObject($reportObjectDAO);
$reportObjectDAO->setChangeDateTime($modifiedDateTime);
}
/** @var ReportObjectDAO $reportObjectDAO */
$reportObjectDAO = $this->reportObjects[$object][$objectId];
$reportObjectDAO->addField(
$this->fieldHelper->getFieldChangeObject($fieldChange)
);
// Track the latest change as the object's change date/time
if ($reportObjectDAO->getChangeDateTime() > $modifiedDateTime) {
$reportObjectDAO->setChangeDateTime($modifiedDateTime);
}
}
/**
* @throws ObjectNotFoundException
*/
private function findObjectsWithMissingFields(RequestObjectDAO $requestObjectDAO): array
{
$objectName = $requestObjectDAO->getObject();
$fields = $requestObjectDAO->getFields();
$syncObjects = $this->syncReport->getObjects($objectName);
$this->objectsWithMissingFields = [];
foreach ($syncObjects as $syncObject) {
$missingFields = [];
foreach ($fields as $field) {
try {
$syncObject->getField($field);
} catch (FieldNotFoundException) {
$missingFields[] = $field;
}
}
if ($missingFields) {
$this->objectsWithMissingFields[$syncObject->getObjectId()] = $missingFields;
}
}
if (!$this->objectsWithMissingFields) {
return [];
}
$event = new InternalObjectFindEvent($this->objectProvider->getObjectByName($objectName));
$event->setIds(array_keys($this->objectsWithMissingFields));
$this->dispatcher->dispatch($event, IntegrationEvents::INTEGRATION_FIND_INTERNAL_RECORDS);
return $event->getFoundObjects();
}
private function completeObjectsWithMissingFields(array $incompleteObjects, RequestObjectDAO $requestObjectDAO): void
{
foreach ($incompleteObjects as $incompleteObject) {
$missingFields = $this->objectsWithMissingFields[$incompleteObject['id']];
$reportObjectDAO = $this->syncReport->getObject($requestObjectDAO->getObject(), $incompleteObject['id']);
foreach ($missingFields as $field) {
try {
$reportFieldDAO = $this->fieldBuilder->buildObjectField(
$field,
$incompleteObject,
$requestObjectDAO,
$this->syncReport->getIntegration(),
FieldDAO::FIELD_UNCHANGED
);
$reportObjectDAO->addField($reportFieldDAO);
} catch (FieldNotFoundException $exception) {
// Field is not supported so keep going
DebugLogger::log(
$this->syncReport->getIntegration(),
$exception->getMessage(),
self::class.':'.__FUNCTION__
);
}
}
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange;
use Mautic\IntegrationsBundle\Entity\FieldChangeRepository;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectMappingsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\OrderDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ObjectDAO as ReportObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\RequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectDeletedException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\Helper\MappingHelper;
use Mautic\IntegrationsBundle\Sync\Helper\SyncDateHelper;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Helper\FieldHelper;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\Executioner\OrderExecutioner;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ReportBuilder\FullObjectReportBuilder;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Internal\ReportBuilder\PartialObjectReportBuilder;
class MauticSyncDataExchange implements SyncDataExchangeInterface
{
public const NAME = 'mautic';
public const OBJECT_CONTACT = 'lead'; // kept as lead for BC
public const OBJECT_COMPANY = 'company';
public function __construct(
private FieldChangeRepository $fieldChangeRepository,
private FieldHelper $fieldHelper,
private MappingHelper $mappingHelper,
private FullObjectReportBuilder $fullObjectReportBuilder,
private PartialObjectReportBuilder $partialObjectReportBuilder,
private OrderExecutioner $orderExecutioner,
private SyncDateHelper $syncDateHelper,
) {
}
public function getSyncReport(RequestDAO $requestDAO): ReportDAO
{
if ($requestDAO->isFirstTimeSync() || $requestDAO->getInputOptionsDAO()->getMauticObjectIds()) {
return $this->fullObjectReportBuilder->buildReport($requestDAO);
}
return $this->partialObjectReportBuilder->buildReport($requestDAO);
}
public function executeSyncOrder(OrderDAO $syncOrderDAO): ObjectMappingsDAO
{
return $this->orderExecutioner->execute($syncOrderDAO);
}
/**
* @return ReportObjectDAO
*
* @throws ObjectNotFoundException
* @throws ObjectNotSupportedException
* @throws ObjectDeletedException
*/
public function getConflictedInternalObject(MappingManualDAO $mappingManualDAO, string $internalObjectName, ReportObjectDAO $integrationObjectDAO)
{
// Check to see if we have a match
$internalObjectDAO = $this->mappingHelper->findMauticObject($mappingManualDAO, $internalObjectName, $integrationObjectDAO);
if (!$internalObjectDAO->getObjectId()) {
return new ReportObjectDAO($internalObjectName, null);
}
$fieldChanges = $this->fieldChangeRepository->findChangesForObject(
$mappingManualDAO->getIntegration(),
$this->mappingHelper->getMauticEntityClassName($internalObjectName),
$internalObjectDAO->getObjectId()
);
foreach ($fieldChanges as $fieldChange) {
$internalObjectDAO->addField(
$this->fieldHelper->getFieldChangeObject($fieldChange)
);
}
return $internalObjectDAO;
}
/**
* @param ObjectChangeDAO[] $objectChanges
*/
public function cleanupProcessedObjects(array $objectChanges): void
{
foreach ($objectChanges as $changedObjectDAO) {
try {
$object = $this->fieldHelper->getFieldObjectName($changedObjectDAO->getMappedObject());
$objectId = $changedObjectDAO->getMappedObjectId();
$this->fieldChangeRepository->deleteEntitiesForObject(
(int) $objectId,
$object,
$changedObjectDAO->getIntegration(),
$this->syncDateHelper->getInternalSyncStartDateTime()
);
} catch (ObjectNotSupportedException $exception) {
DebugLogger::log(
self::NAME,
$exception->getMessage(),
self::class.':'.__FUNCTION__
);
}
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncDataExchange;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\OrderDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\RequestDAO;
interface SyncDataExchangeInterface
{
/**
* Sync to integration.
*/
public function getSyncReport(RequestDAO $requestDAO): ReportDAO;
/**
* Sync from integration.
*/
public function executeSyncOrder(OrderDAO $syncOrderDAO);
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncJudge\Modes;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ConflictUnresolvedException;
use Mautic\IntegrationsBundle\Sync\SyncJudge\SyncJudgeInterface;
class BestEvidence implements JudgementModeInterface
{
use DateComparisonTrait;
/**
* @throws ConflictUnresolvedException
*/
public static function adjudicate(
InformationChangeRequestDAO $leftChangeRequest,
InformationChangeRequestDAO $rightChangeRequest,
): InformationChangeRequestDAO {
try {
return HardEvidence::adjudicate($leftChangeRequest, $rightChangeRequest);
} catch (ConflictUnresolvedException) {
}
if (null === $leftChangeRequest->getPossibleChangeDateTime() || null === $rightChangeRequest->getPossibleChangeDateTime()) {
throw new ConflictUnresolvedException();
}
$possibleChangeCompare = self::compareDateTimes(
$leftChangeRequest->getPossibleChangeDateTime(),
$rightChangeRequest->getPossibleChangeDateTime()
);
if (SyncJudgeInterface::NO_WINNER === $possibleChangeCompare) {
throw new ConflictUnresolvedException();
}
if (SyncJudgeInterface::LEFT_WINNER === $possibleChangeCompare) {
return $leftChangeRequest;
}
return $rightChangeRequest;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncJudge\Modes;
use Mautic\IntegrationsBundle\Sync\SyncJudge\SyncJudgeInterface;
trait DateComparisonTrait
{
/**
* @return string self::LEFT_WINNER|self::RIGHT_WINNER|self::NO_WINNER
*/
private static function compareDateTimes(?\DateTimeInterface $leftDateTime = null, ?\DateTimeInterface $rightDateTime = null): string
{
if (null !== $leftDateTime && (null === $rightDateTime || $leftDateTime > $rightDateTime)) {
return SyncJudgeInterface::LEFT_WINNER;
}
if (null !== $rightDateTime && (null === $leftDateTime || $rightDateTime > $leftDateTime)) {
return SyncJudgeInterface::RIGHT_WINNER;
}
return SyncJudgeInterface::NO_WINNER;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncJudge\Modes;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ConflictUnresolvedException;
class FuzzyEvidence implements JudgementModeInterface
{
/**
* @throws ConflictUnresolvedException
*/
public static function adjudicate(
InformationChangeRequestDAO $leftChangeRequest,
InformationChangeRequestDAO $rightChangeRequest,
): InformationChangeRequestDAO {
try {
return BestEvidence::adjudicate($leftChangeRequest, $rightChangeRequest);
} catch (ConflictUnresolvedException) {
}
if (
$leftChangeRequest->getCertainChangeDateTime()
&& $rightChangeRequest->getPossibleChangeDateTime()
&& $leftChangeRequest->getCertainChangeDateTime() > $rightChangeRequest->getPossibleChangeDateTime()
) {
return $leftChangeRequest;
}
if (
$rightChangeRequest->getCertainChangeDateTime()
&& $leftChangeRequest->getPossibleChangeDateTime()
&& $rightChangeRequest->getCertainChangeDateTime() > $leftChangeRequest->getPossibleChangeDateTime()
) {
return $rightChangeRequest;
}
throw new ConflictUnresolvedException();
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncJudge\Modes;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ConflictUnresolvedException;
use Mautic\IntegrationsBundle\Sync\SyncJudge\SyncJudgeInterface;
class HardEvidence implements JudgementModeInterface
{
use DateComparisonTrait;
/**
* @throws ConflictUnresolvedException
*/
public static function adjudicate(
InformationChangeRequestDAO $leftChangeRequest,
InformationChangeRequestDAO $rightChangeRequest,
): InformationChangeRequestDAO {
if (null === $leftChangeRequest->getCertainChangeDateTime() || null === $rightChangeRequest->getCertainChangeDateTime()) {
throw new ConflictUnresolvedException();
}
$certainChangeCompare = self::compareDateTimes(
$leftChangeRequest->getCertainChangeDateTime(),
$rightChangeRequest->getCertainChangeDateTime()
);
if (SyncJudgeInterface::NO_WINNER === $certainChangeCompare) {
throw new ConflictUnresolvedException();
}
if (SyncJudgeInterface::LEFT_WINNER === $certainChangeCompare) {
return $leftChangeRequest;
}
return $rightChangeRequest;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncJudge\Modes;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
interface JudgementModeInterface
{
public static function adjudicate(
InformationChangeRequestDAO $leftChangeRequest,
InformationChangeRequestDAO $rightChangeRequest,
): InformationChangeRequestDAO;
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncJudge;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ConflictUnresolvedException;
use Mautic\IntegrationsBundle\Sync\SyncJudge\Modes\BestEvidence;
use Mautic\IntegrationsBundle\Sync\SyncJudge\Modes\FuzzyEvidence;
use Mautic\IntegrationsBundle\Sync\SyncJudge\Modes\HardEvidence;
final class SyncJudge implements SyncJudgeInterface
{
/**
* @param string $mode
*
* @return InformationChangeRequestDAO
*
* @throws ConflictUnresolvedException
*/
public function adjudicate(
$mode,
InformationChangeRequestDAO $leftChangeRequest,
InformationChangeRequestDAO $rightChangeRequest,
) {
if ($leftChangeRequest->getNewValue() === $rightChangeRequest->getNewValue()) {
return $leftChangeRequest;
}
return match ($mode) {
SyncJudgeInterface::HARD_EVIDENCE_MODE => HardEvidence::adjudicate($leftChangeRequest, $rightChangeRequest),
SyncJudgeInterface::BEST_EVIDENCE_MODE => BestEvidence::adjudicate($leftChangeRequest, $rightChangeRequest),
default => FuzzyEvidence::adjudicate($leftChangeRequest, $rightChangeRequest),
};
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncJudge;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ConflictUnresolvedException;
interface SyncJudgeInterface
{
/**
* Winner is selected based on the field was updated after the loser.
*/
public const HARD_EVIDENCE_MODE = 'hard';
/**
* Winner is selected based on hard evidence if available, otherwise if the object of the winner was updated after the object of the loser.
*/
public const BEST_EVIDENCE_MODE = 'best';
/**
* Winner is selected based on the probability that it was updated after the loser.
*/
public const FUZZY_EVIDENCE_MODE = 'fuzzy';
public const LEFT_WINNER = 'left';
public const RIGHT_WINNER = 'right';
public const NO_WINNER = 'no';
/**
* @param string $mode
*
* @return InformationChangeRequestDAO
*
* @throws ConflictUnresolvedException
*/
public function adjudicate(
$mode,
InformationChangeRequestDAO $leftChangeRequest,
InformationChangeRequestDAO $rightChangeRequest,
);
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Helper;
use Mautic\IntegrationsBundle\Exception\InvalidValueException;
use Mautic\IntegrationsBundle\Exception\RequiredValueException;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\ObjectMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
class ValueHelper
{
private ?NormalizedValueDAO $normalizedValueDAO = null;
private ?string $fieldState = null;
private ?string $syncDirection = null;
/**
* @throws InvalidValueException
*/
public function getValueForIntegration(NormalizedValueDAO $normalizedValueDAO, string $fieldState, string $syncDirection): NormalizedValueDAO
{
$this->normalizedValueDAO = $normalizedValueDAO;
$this->fieldState = $fieldState;
$this->syncDirection = $syncDirection;
$newValue = $this->getValue(ObjectMappingDAO::SYNC_TO_MAUTIC);
return new NormalizedValueDAO($normalizedValueDAO->getType(), $normalizedValueDAO->getNormalizedValue(), $newValue);
}
/**
* @throws InvalidValueException
*/
public function getValueForMautic(NormalizedValueDAO $normalizedValueDAO, string $fieldState, string $syncDirection): NormalizedValueDAO
{
$this->normalizedValueDAO = $normalizedValueDAO;
$this->fieldState = $fieldState;
$this->syncDirection = $syncDirection;
$newValue = $this->getValue(ObjectMappingDAO::SYNC_TO_INTEGRATION);
return new NormalizedValueDAO($normalizedValueDAO->getType(), $normalizedValueDAO->getNormalizedValue(), $newValue);
}
/**
* @return float|int|mixed|string
*
* @throws InvalidValueException
*/
private function getValue(string $directionToIgnore)
{
$value = $this->normalizedValueDAO->getNormalizedValue();
// If the field is not required, do not force a value
if (FieldDAO::FIELD_REQUIRED !== $this->fieldState) {
return $value;
}
// If the field is not configured to update the Integration, do not force a value
if ($directionToIgnore === $this->syncDirection) {
return $value;
}
// If the value is not empty (including 0 or false), do not force a value
if (null !== $value && '' !== $value) {
return $value;
}
return match ($this->normalizedValueDAO->getType()) {
NormalizedValueDAO::EMAIL_TYPE, NormalizedValueDAO::DATE_TYPE, NormalizedValueDAO::DATETIME_TYPE, NormalizedValueDAO::BOOLEAN_TYPE => $this->normalizedValueDAO->getOriginalValue(),
NormalizedValueDAO::INT_TYPE => 0,
NormalizedValueDAO::DOUBLE_TYPE, NormalizedValueDAO::FLOAT_TYPE => 1.0,
default => throw new RequiredValueException("Required field can't be empty"),
};
}
}

View File

@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Integration;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InputOptionsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\OrderDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\ObjectDAO as RequestObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\RequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectDeletedException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Helper\MappingHelper;
use Mautic\IntegrationsBundle\Sync\Helper\SyncDateHelper;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\SyncDataExchangeInterface;
class IntegrationSyncProcess
{
private ?InputOptionsDAO $inputOptionsDAO = null;
private ?MappingManualDAO $mappingManualDAO = null;
private ?SyncDataExchangeInterface $syncDataExchange = null;
public function __construct(
private SyncDateHelper $syncDateHelper,
private MappingHelper $mappingHelper,
private ObjectChangeGenerator $objectChangeGenerator,
) {
}
public function setupSync(InputOptionsDAO $inputOptionsDAO, MappingManualDAO $mappingManualDAO, SyncDataExchangeInterface $syncDataExchange): void
{
$this->inputOptionsDAO = $inputOptionsDAO;
$this->mappingManualDAO = $mappingManualDAO;
$this->syncDataExchange = $syncDataExchange;
}
/**
* @throws ObjectNotFoundException
*/
public function getSyncReport(int $syncIteration): ReportDAO
{
$integrationRequestDAO = new RequestDAO(MauticSyncDataExchange::NAME, $syncIteration, $this->inputOptionsDAO);
$integrationObjectsNames = $this->mappingManualDAO->getIntegrationObjectNames();
$mauticObjectTypes = $integrationRequestDAO->getInputOptionsDAO()->getMauticObjectIds() ?
$integrationRequestDAO->getInputOptionsDAO()->getMauticObjectIds()->getObjectTypes() : [];
$hasMauticObjectIDs = 0 < count($mauticObjectTypes);
foreach ($integrationObjectsNames as $integrationObjectName) {
if ($hasMauticObjectIDs) {
$mappedInternalObjectsNames = [];
try {
$mappedInternalObjectsNames = $this->mappingManualDAO->getMappedInternalObjectsNames($integrationObjectName);
} catch (ObjectNotFoundException) {
}
if (1 > count(array_intersect($mauticObjectTypes, $mappedInternalObjectsNames))) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Integration to Mautic; skipping sync for the %s object because object IDs have been explicitly specified for other objects',
$integrationObjectName
),
__CLASS__.':'.__FUNCTION__
);
continue;
}
}
$integrationObjectFields = $this->mappingManualDAO->getIntegrationObjectFieldsToSyncToMautic($integrationObjectName);
if (0 === count($integrationObjectFields)) {
// No fields configured for a sync
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Integration to Mautic; there are no fields for the %s object',
$integrationObjectName
),
self::class.':'.__FUNCTION__
);
continue;
}
$objectSyncFromDateTime = $this->syncDateHelper->getSyncFromDateTime($this->mappingManualDAO->getIntegration(), $integrationObjectName);
$objectSyncToDateTime = $this->syncDateHelper->getSyncToDateTime();
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Integration to Mautic; syncing from %s to %s for the %s object with %d fields',
$objectSyncFromDateTime->format('Y-m-d H:i:s'),
$objectSyncToDateTime->format('Y-m-d H:i:s'),
$integrationObjectName,
count($integrationObjectFields)
),
self::class.':'.__FUNCTION__
);
$integrationRequestObject = new RequestObjectDAO(
$integrationObjectName,
$objectSyncFromDateTime,
$objectSyncToDateTime
);
foreach ($integrationObjectFields as $integrationObjectField) {
$integrationRequestObject->addField($integrationObjectField);
}
$integrationRequestObject->setRequiredFields($this->mappingManualDAO->getIntegrationObjectRequiredFieldNames($integrationObjectName));
$integrationRequestDAO->addObject($integrationRequestObject);
}
return $integrationRequestDAO->shouldSync()
? $this->syncDataExchange->getSyncReport($integrationRequestDAO)
:
new ReportDAO($this->mappingManualDAO->getIntegration());
}
/**
* @throws ObjectNotFoundException
*/
public function getSyncOrder(ReportDAO $syncReport): OrderDAO
{
$syncOrder = new OrderDAO($this->syncDateHelper->getSyncDateTime(), $this->inputOptionsDAO->isFirstTimeSync(), $this->mappingManualDAO->getIntegration(), $this->inputOptionsDAO->getOptions());
$internalObjectNames = $this->mappingManualDAO->getInternalObjectNames();
foreach ($internalObjectNames as $internalObjectName) {
$internalObjects = $syncReport->getObjects($internalObjectName);
$mappedIntegrationObjectNames = $this->mappingManualDAO->getMappedIntegrationObjectsNames($internalObjectName);
foreach ($mappedIntegrationObjectNames as $mappedIntegrationObjectName) {
$objectMapping = $this->mappingManualDAO->getObjectMapping($internalObjectName, $mappedIntegrationObjectName);
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Mautic to integration; syncing %d objects for the %s object mapped to the %s integration object',
count($internalObjects),
$internalObjectName,
$mappedIntegrationObjectName
),
self::class.':'.__FUNCTION__
);
foreach ($internalObjects as $internalObject) {
try {
$integrationObject = $this->mappingHelper->findIntegrationObject(
$this->mappingManualDAO->getIntegration(),
$mappedIntegrationObjectName,
$internalObject
);
$objectChange = $this->objectChangeGenerator->getSyncObjectChange(
$syncReport,
$this->mappingManualDAO,
$objectMapping,
$internalObject,
$integrationObject
);
if ($objectChange->shouldSync()) {
$syncOrder->addObjectChange($objectChange);
}
} catch (ObjectDeletedException) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
"Mautic to integration; Mautic's %s:%s object was deleted from the integration so don't try to sync",
$internalObject->getObject(),
$internalObject->getObjectId()
),
self::class.':'.__FUNCTION__
);
continue;
}
}
}
}
return $syncOrder;
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Integration;
use Mautic\IntegrationsBundle\Exception\InvalidValueException;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\FieldMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\ObjectMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ObjectDAO as ReportObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Helper\ValueHelper;
class ObjectChangeGenerator
{
public function __construct(
private ValueHelper $valueHelper,
) {
}
/**
* @return ObjectChangeDAO
*
* @throws ObjectNotFoundException
*/
public function getSyncObjectChange(
ReportDAO $syncReport,
MappingManualDAO $mappingManual,
ObjectMappingDAO $objectMapping,
ReportObjectDAO $internalObject,
ReportObjectDAO $integrationObject,
) {
$objectChange = new ObjectChangeDAO(
$mappingManual->getIntegration(),
$integrationObject->getObject(),
$integrationObject->getObjectId(),
$internalObject->getObject(),
$internalObject->getObjectId()
);
if ($integrationObject->getObjectId()) {
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
"Mautic to integration; found a match between the integration %s:%s object and Mautic's %s:%s object",
$integrationObject->getObject(),
(string) $integrationObject->getObjectId(),
$internalObject->getObject(),
(string) $internalObject->getObjectId()
),
self::class.':'.__FUNCTION__
);
} else {
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
'Mautic to integration: no match found for %s:%s',
$internalObject->getObject(),
(string) $internalObject->getObjectId()
),
self::class.':'.__FUNCTION__
);
}
/** @var FieldMappingDAO[] $fieldMappings */
$fieldMappings = $objectMapping->getFieldMappings();
foreach ($fieldMappings as $fieldMappingDAO) {
$this->addFieldToObjectChange($fieldMappingDAO, $syncReport, $mappingManual, $internalObject, $integrationObject, $objectChange);
}
// Set the change date/time from the object so that we can update last sync date based on this
$objectChange->setChangeDateTime($internalObject->getChangeDateTime());
return $objectChange;
}
/**
* @throws ObjectNotFoundException
*/
private function addFieldToObjectChange(
FieldMappingDAO $fieldMappingDAO,
ReportDAO $syncReport,
MappingManualDAO $mappingManual,
ReportObjectDAO $internalObject,
ReportObjectDAO $integrationObject,
ObjectChangeDAO $objectChange,
): void {
// Skip adding fields for the push process that should sync to Mautic only.
if (ObjectMappingDAO::SYNC_TO_MAUTIC === $fieldMappingDAO->getSyncDirection()) {
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
"Mautic to integration; the %s object's field %s was skipped because it's configured to sync to Mautic",
$integrationObject->getObject(),
$fieldMappingDAO->getIntegrationField()
),
__CLASS__.':'.__FUNCTION__
);
return;
}
try {
$fieldState = $internalObject->getField($fieldMappingDAO->getInternalField())->getState();
$internalInformationChangeRequest = $syncReport->getInformationChangeRequest(
$internalObject->getObject(),
$internalObject->getObjectId(),
$fieldMappingDAO->getInternalField()
);
} catch (FieldNotFoundException) {
return;
}
try {
$newValue = $this->valueHelper->getValueForIntegration(
$internalInformationChangeRequest->getNewValue(),
$fieldState,
$fieldMappingDAO->getSyncDirection()
);
} catch (InvalidValueException) {
return; // Field has to be skipped
}
// Note: bidirectional conflicts were handled by Internal\ObjectChangeGenerator
$objectChange->addField(
new FieldDAO($fieldMappingDAO->getIntegrationField(), $newValue),
$fieldState
);
// ObjectMappingDAO::SYNC_TO_INTEGRATION
// ObjectMappingDAO::SYNC_BIDIRECTIONALLY
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
"Mautic to integration; syncing %s object's %s field %s with a value of %s",
$integrationObject->getObject(),
$fieldState,
$fieldMappingDAO->getIntegrationField(),
var_export($newValue->getNormalizedValue(), true)
),
self::class.':'.__FUNCTION__
);
}
}

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Internal;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InputOptionsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\OrderDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\ObjectDAO as RequestObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Request\RequestDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectDeletedException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotSupportedException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectSyncSkippedException;
use Mautic\IntegrationsBundle\Sync\Helper\SyncDateHelper;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
class MauticSyncProcess
{
private ?InputOptionsDAO $inputOptionsDAO = null;
private ?MappingManualDAO $mappingManualDAO = null;
private ?MauticSyncDataExchange $syncDataExchange = null;
public function __construct(
private SyncDateHelper $syncDateHelper,
private ObjectChangeGenerator $objectChangeGenerator,
) {
}
public function setupSync(InputOptionsDAO $inputOptionsDAO, MappingManualDAO $mappingManualDAO, MauticSyncDataExchange $syncDataExchange): void
{
$this->inputOptionsDAO = $inputOptionsDAO;
$this->mappingManualDAO = $mappingManualDAO;
$this->syncDataExchange = $syncDataExchange;
}
/**
* @throws ObjectNotFoundException
*/
public function getSyncReport(int $syncIteration): ReportDAO
{
$internalRequestDAO = new RequestDAO($this->mappingManualDAO->getIntegration(), $syncIteration, $this->inputOptionsDAO);
$mauticObjectTypes = $internalRequestDAO->getInputOptionsDAO()->getMauticObjectIds() ?
$internalRequestDAO->getInputOptionsDAO()->getMauticObjectIds()->getObjectTypes() : [];
$hasMauticObjectIDs = 0 < count($mauticObjectTypes);
$internalObjectsNames = $this->mappingManualDAO->getInternalObjectNames();
foreach ($internalObjectsNames as $internalObjectName) {
if ($hasMauticObjectIDs) {
try {
$internalRequestDAO->getInputOptionsDAO()->getMauticObjectIds()->getObjectIdsFor($internalObjectName);
} catch (ObjectNotFoundException) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Mautic to integration; skipping sync for the %s object because certain object IDs are specified for other object(s)',
$internalObjectName
),
__CLASS__.':'.__FUNCTION__
);
continue;
}
}
$internalObjectFields = $this->mappingManualDAO->getInternalObjectFieldsToSyncToIntegration($internalObjectName);
if (0 === count($internalObjectFields)) {
// No fields configured for a sync
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Mautic to integration; there are no fields for the %s object',
$internalObjectName
),
self::class.':'.__FUNCTION__
);
continue;
}
$objectSyncFromDateTime = $this->syncDateHelper->getSyncFromDateTime(MauticSyncDataExchange::NAME, $internalObjectName);
$objectSyncToDateTime = $this->syncDateHelper->getSyncToDateTime();
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Mautic to integration; syncing from %s to %s for the %s object with %d fields',
$objectSyncFromDateTime->format('Y-m-d H:i:s'),
$objectSyncToDateTime->format('Y-m-d H:i:s'),
$internalObjectName,
count($internalObjectFields)
),
self::class.':'.__FUNCTION__
);
$internalRequestObject = new RequestObjectDAO($internalObjectName, $objectSyncFromDateTime, $objectSyncToDateTime);
foreach ($internalObjectFields as $internalObjectField) {
$internalRequestObject->addField($internalObjectField);
}
// Set required fields for easy access; mainly for Mautic
$internalRequestObject->setRequiredFields($this->mappingManualDAO->getInternalObjectRequiredFieldNames($internalObjectName));
$internalRequestDAO->addObject($internalRequestObject);
}
return $internalRequestDAO->shouldSync()
? $this->syncDataExchange->getSyncReport($internalRequestDAO)
:
new ReportDAO(MauticSyncDataExchange::NAME);
}
/**
* @throws ObjectNotFoundException
* @throws ObjectNotSupportedException
*/
public function getSyncOrder(ReportDAO $syncReport): OrderDAO
{
$orderDAO = new OrderDAO($this->syncDateHelper->getSyncDateTime(), $this->inputOptionsDAO->isFirstTimeSync(), $this->mappingManualDAO->getIntegration(), $this->inputOptionsDAO->getOptions());
$integrationObjectsNames = $this->mappingManualDAO->getIntegrationObjectNames();
foreach ($integrationObjectsNames as $integrationObjectName) {
$integrationObjects = $syncReport->getObjects($integrationObjectName);
$mappedInternalObjectsNames = $this->mappingManualDAO->getMappedInternalObjectsNames($integrationObjectName);
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Integration to Mautic; found %d objects for the %s object mapped to the %s Mautic object(s)',
count($integrationObjects),
$integrationObjectName,
implode(', ', $mappedInternalObjectsNames)
),
self::class.':'.__FUNCTION__
);
foreach ($mappedInternalObjectsNames as $mappedInternalObjectName) {
$objectMapping = $this->mappingManualDAO->getObjectMapping($mappedInternalObjectName, $integrationObjectName);
foreach ($integrationObjects as $integrationObject) {
try {
$internalObject = $this->syncDataExchange->getConflictedInternalObject(
$this->mappingManualDAO,
$mappedInternalObjectName,
$integrationObject
);
$objectChange = $this->objectChangeGenerator->getSyncObjectChange(
$syncReport,
$this->mappingManualDAO,
$objectMapping,
$internalObject,
$integrationObject
);
if ($objectChange->shouldSync()) {
$orderDAO->addObjectChange($objectChange);
}
} catch (ObjectDeletedException) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Integration to Mautic; the %s object with ID %s is marked deleted and thus not synced',
$integrationObject->getObject(),
$integrationObject->getObjectId()
),
self::class.':'.__FUNCTION__
);
} catch (ObjectSyncSkippedException $exception) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Integration to Mautic; the %s object with ID %s is skipped and thus not synced with message: '.$exception->getMessage(),
$integrationObject->getObject(),
$integrationObject->getObjectId()
),
__CLASS__.':'.__FUNCTION__
);
}
}
}
}
return $orderDAO;
}
}

View File

@@ -0,0 +1,369 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Internal;
use Mautic\IntegrationsBundle\Exception\InvalidValueException;
use Mautic\IntegrationsBundle\Exception\RequiredValueException;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\FieldMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\ObjectMappingDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InformationChangeRequestDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\FieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectChangeDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\FieldDAO as ReportFieldDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ObjectDAO as ReportObjectDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\Exception\ConflictUnresolvedException;
use Mautic\IntegrationsBundle\Sync\Exception\FieldNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectNotFoundException;
use Mautic\IntegrationsBundle\Sync\Exception\ObjectSyncSkippedException;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\Notification\BulkNotification;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\Helper\FieldHelper;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\IntegrationsBundle\Sync\SyncJudge\SyncJudgeInterface;
use Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Helper\ValueHelper;
class ObjectChangeGenerator
{
private array $judgementModes = [
SyncJudgeInterface::HARD_EVIDENCE_MODE,
SyncJudgeInterface::BEST_EVIDENCE_MODE,
SyncJudgeInterface::FUZZY_EVIDENCE_MODE,
];
public function __construct(
private SyncJudgeInterface $syncJudge,
private ValueHelper $valueHelper,
private FieldHelper $fieldHelper,
private BulkNotification $bulkNotification,
) {
}
/**
* @return ObjectChangeDAO
*
* @throws ObjectNotFoundException
*/
public function getSyncObjectChange(
ReportDAO $syncReport,
MappingManualDAO $mappingManual,
ObjectMappingDAO $objectMapping,
ReportObjectDAO $internalObject,
ReportObjectDAO $integrationObject,
) {
$objectChange = new ObjectChangeDAO(
$mappingManual->getIntegration(),
$internalObject->getObject(),
$internalObject->getObjectId(),
$integrationObject->getObject(),
$integrationObject->getObjectId()
);
if ($internalObject->getObjectId()) {
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
"Integration to Mautic; found a match between Mautic's %s:%s object and the integration %s:%s object ",
$internalObject->getObject(),
(string) $internalObject->getObjectId(),
$integrationObject->getObject(),
(string) $integrationObject->getObjectId()
),
self::class.':'.__FUNCTION__
);
} else {
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
'Integration to Mautic; no match found for %s:%s',
$integrationObject->getObject(),
(string) $integrationObject->getObjectId()
),
self::class.':'.__FUNCTION__
);
}
/** @var FieldMappingDAO[] $fieldMappings */
$fieldMappings = $objectMapping->getFieldMappings();
foreach ($fieldMappings as $fieldMappingDAO) {
$this->addFieldToObjectChange($fieldMappingDAO, $syncReport, $mappingManual, $internalObject, $integrationObject, $objectChange);
}
// Set the change date/time from the object so that we can update last sync date based on this
$objectChange->setChangeDateTime($integrationObject->getChangeDateTime());
return $objectChange;
}
/**
* @throws ObjectNotFoundException
* @throws ObjectSyncSkippedException
*/
private function addFieldToObjectChange(
FieldMappingDAO $fieldMappingDAO,
ReportDAO $syncReport,
MappingManualDAO $mappingManual,
ReportObjectDAO $internalObject,
ReportObjectDAO $integrationObject,
ObjectChangeDAO $objectChange,
): void {
// Skip adding fields for the pull process that should sync to integration only.
if (ObjectMappingDAO::SYNC_TO_INTEGRATION === $fieldMappingDAO->getSyncDirection()) {
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
"Integration to Mautic; the %s object's field %s was skipped because it's configured to sync to the integration",
$internalObject->getObject(),
$fieldMappingDAO->getInternalField()
),
__CLASS__.':'.__FUNCTION__
);
return;
}
try {
$integrationFieldState = $integrationObject->getField($fieldMappingDAO->getIntegrationField())->getState();
$internalFieldState = $this->getFieldState(
$fieldMappingDAO->getInternalObject(),
$fieldMappingDAO->getInternalField(),
$integrationFieldState
);
$integrationInformationChangeRequest = $syncReport->getInformationChangeRequest(
$integrationObject->getObject(),
$integrationObject->getObjectId(),
$fieldMappingDAO->getIntegrationField()
);
} catch (FieldNotFoundException) {
return;
}
try {
// If syncing bidirectional, let the sync judge determine what value should be used for the field
if (ObjectMappingDAO::SYNC_BIDIRECTIONALLY === $fieldMappingDAO->getSyncDirection()) {
$this->judgeThenAddFieldToObjectChange($mappingManual, $internalObject, $fieldMappingDAO, $integrationInformationChangeRequest, $objectChange, $internalFieldState);
return;
}
$newValue = $this->valueHelper->getValueForMautic(
$integrationInformationChangeRequest->getNewValue(),
$internalFieldState,
$fieldMappingDAO->getSyncDirection()
);
} catch (RequiredValueException $e) {
$isNewObject = (null === $internalObject->getObjectId());
$this->notifyAboutInvalidValue($e, $fieldMappingDAO, $integrationInformationChangeRequest, $isNewObject);
if ($isNewObject) {
// Empty required field for new contact means completely rejected contact from sync
throw new ObjectSyncSkippedException(sprintf("Skipping creating lead '%s' because required value for internal field '%s' is empty", $integrationInformationChangeRequest->getObjectId(), $fieldMappingDAO->getInternalField()));
}
return; // Empty required field for existing contact is skipped
} catch (InvalidValueException) {
return; // Field has to be skipped
}
// Add the value to the field based on the field state
$objectChange->addField(
new FieldDAO($fieldMappingDAO->getInternalField(), $newValue),
$internalFieldState
);
// ObjectMappingDAO::SYNC_TO_MAUTIC
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
'Integration to Mautic; syncing %s %s with a value of %s',
$internalFieldState,
$fieldMappingDAO->getInternalField(),
var_export($newValue->getNormalizedValue(), true)
),
self::class.':'.__FUNCTION__
);
}
private function judgeThenAddFieldToObjectChange(
MappingManualDAO $mappingManual,
ReportObjectDAO $internalObject,
FieldMappingDAO $fieldMappingDAO,
InformationChangeRequestDAO $integrationInformationChangeRequest,
ObjectChangeDAO $objectChange,
string $fieldState,
): void {
try {
$internalField = $internalObject->getField($fieldMappingDAO->getInternalField());
} catch (FieldNotFoundException) {
$internalField = null;
}
if (!$internalField) {
$newValue = $this->valueHelper->getValueForMautic(
$integrationInformationChangeRequest->getNewValue(),
$fieldState,
$fieldMappingDAO->getSyncDirection()
);
$objectChange->addField(
new FieldDAO($fieldMappingDAO->getInternalField(), $newValue),
$fieldState
);
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
"Integration to Mautic; the sync is bidirectional but no conflicts were found so syncing the %s object's %s field %s with a value of %s",
$internalObject->getObject(),
$fieldState,
$fieldMappingDAO->getInternalField(),
var_export($newValue->getNormalizedValue(), true)
),
self::class.':'.__FUNCTION__
);
return;
}
$internalInformationChangeRequest = new InformationChangeRequestDAO(
MauticSyncDataExchange::NAME,
$internalObject->getObject(),
$internalObject->getObjectId(),
$internalField->getName(),
$internalField->getValue()
);
$possibleChangeDateTime = $internalObject->getChangeDateTime();
$certainChangeDateTime = $internalField->getChangeDateTime();
// If we know certain change datetime and it's newer than possible change datetime
// then we have to update possible change datetime otherwise comparision doesn't work correctly
if ($certainChangeDateTime && ($certainChangeDateTime > $possibleChangeDateTime)) {
$possibleChangeDateTime = $certainChangeDateTime;
}
$internalInformationChangeRequest->setPossibleChangeDateTime($possibleChangeDateTime);
$internalInformationChangeRequest->setCertainChangeDateTime($certainChangeDateTime);
// There is a conflict so let the judge determine which value comes out on top
foreach ($this->judgementModes as $judgeMode) {
try {
$this->makeJudgement(
$mappingManual,
$judgeMode,
$fieldMappingDAO,
$objectChange,
$integrationInformationChangeRequest,
$internalInformationChangeRequest,
$fieldState
);
break;
} catch (ConflictUnresolvedException) {
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
'Integration to Mautic; no winner was determined using the %s judging mode for object %s field %s',
$judgeMode,
$internalObject->getObject(),
$fieldMappingDAO->getInternalField()
),
self::class.':'.__FUNCTION__
);
}
}
}
/**
* @throws ConflictUnresolvedException
*/
private function makeJudgement(
MappingManualDAO $mappingManual,
string $judgeMode,
FieldMappingDAO $fieldMappingDAO,
ObjectChangeDAO $objectChange,
InformationChangeRequestDAO $integrationInformationChangeRequest,
InformationChangeRequestDAO $internalInformationChangeRequest,
string $fieldState,
): void {
$winningChangeRequest = $this->syncJudge->adjudicate(
$judgeMode,
$internalInformationChangeRequest,
$integrationInformationChangeRequest
);
$newValue = $this->valueHelper->getValueForMautic(
$winningChangeRequest->getNewValue(),
$fieldState,
$fieldMappingDAO->getSyncDirection()
);
$objectChange->addField(
new FieldDAO($fieldMappingDAO->getInternalField(), $newValue),
$fieldState
);
DebugLogger::log(
$mappingManual->getIntegration(),
sprintf(
"Integration to Mautic; sync judge determined to sync %s to the %s object's %s field %s with a value of %s using the %s judging mode",
$winningChangeRequest->getIntegration(),
$winningChangeRequest->getObject(),
$fieldState,
$fieldMappingDAO->getInternalField(),
var_export($newValue->getNormalizedValue(), true),
$judgeMode
),
self::class.':'.__FUNCTION__
);
}
private function getFieldState(string $object, string $field, string $integrationFieldState): string
{
// If this is a Mautic required field, return required
if (isset($this->fieldHelper->getRequiredFields($object)[$field])) {
return ReportFieldDAO::FIELD_REQUIRED;
}
return $integrationFieldState;
}
private function notifyAboutInvalidValue(
InvalidValueException $e,
FieldMappingDAO $fieldMappingDAO,
InformationChangeRequestDAO $integrationInformationChangeRequest,
bool $isNewObject,
): void {
$newObjectSkippedMessagePart = ($isNewObject) ? ' New object sync skipped.' : '';
$message = sprintf(
"Field '%s' for object ID '%s' mapped to internal '%s' with value '%s'",
$integrationInformationChangeRequest->getField(),
$integrationInformationChangeRequest->getObjectId(),
$fieldMappingDAO->getIntegrationField(),
$integrationInformationChangeRequest->getNewValue()->getOriginalValue()
);
$deduplicateValue = static::class.'-'.
$integrationInformationChangeRequest->getIntegration().'-'.
$fieldMappingDAO->getInternalObject().'-'.
$integrationInformationChangeRequest->getField();
$this->bulkNotification->addNotification(
$deduplicateValue,
$e->getMessage().$newObjectSkippedMessagePart,
$integrationInformationChangeRequest->getIntegration(),
$fieldMappingDAO->getIntegrationObject(),
$fieldMappingDAO->getInternalObject(),
0,
$message
);
$this->bulkNotification->flush();
}
}

View File

@@ -0,0 +1,339 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncProcess;
use Mautic\IntegrationsBundle\Event\CompletedSyncIterationEvent;
use Mautic\IntegrationsBundle\Event\SyncEvent;
use Mautic\IntegrationsBundle\Exception\IntegrationNotFoundException;
use Mautic\IntegrationsBundle\IntegrationEvents;
use Mautic\IntegrationsBundle\Sync\DAO\Mapping\MappingManualDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InputOptionsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\ObjectIdsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\ObjectMappingsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\OrderDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Order\OrderResultsDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\Report\ReportDAO;
use Mautic\IntegrationsBundle\Sync\Exception\HandlerNotSupportedException;
use Mautic\IntegrationsBundle\Sync\Helper\MappingHelper;
use Mautic\IntegrationsBundle\Sync\Helper\RelationsHelper;
use Mautic\IntegrationsBundle\Sync\Helper\SyncDateHelper;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\Notification\Notifier;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\SyncDataExchangeInterface;
use Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Integration\IntegrationSyncProcess;
use Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Internal\MauticSyncProcess;
use Mautic\IntegrationsBundle\Sync\SyncService\SyncServiceInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class SyncProcess
{
private ?int $syncIteration = null;
public function __construct(
private SyncDateHelper $syncDateHelper,
private MappingHelper $mappingHelper,
private RelationsHelper $relationsHelper,
private IntegrationSyncProcess $integrationSyncProcess,
private MauticSyncProcess $mauticSyncProcess,
private EventDispatcherInterface $eventDispatcher,
private Notifier $notifier,
private MappingManualDAO $mappingManualDAO,
private MauticSyncDataExchange $internalSyncDataExchange,
private SyncDataExchangeInterface $integrationSyncDataExchange,
private InputOptionsDAO $inputOptionsDAO,
private SyncServiceInterface $syncService,
) {
}
/**
* Execute sync with integration.
*/
public function execute(): void
{
defined('MAUTIC_INTEGRATION_ACTIVE_SYNC') or define('MAUTIC_INTEGRATION_ACTIVE_SYNC', 1);
// Setup/prepare for the sync
$this->syncDateHelper->setSyncDateTimes($this->inputOptionsDAO->getStartDateTime(), $this->inputOptionsDAO->getEndDateTime());
$this->integrationSyncProcess->setupSync($this->inputOptionsDAO, $this->mappingManualDAO, $this->integrationSyncDataExchange);
$this->mauticSyncProcess->setupSync($this->inputOptionsDAO, $this->mappingManualDAO, $this->internalSyncDataExchange);
if ($this->inputOptionsDAO->pullIsEnabled()) {
$this->executeIntegrationSync();
}
if ($this->inputOptionsDAO->pushIsEnabled()) {
$this->syncDateHelper->setInternalSyncStartDateTime();
$this->executeInternalSync();
}
// Tell listeners sync is done
$this->eventDispatcher->dispatch(
new SyncEvent($this->inputOptionsDAO),
IntegrationEvents::INTEGRATION_POST_EXECUTE
);
}
private function executeIntegrationSync(): void
{
$this->syncIteration = 1;
while (true) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf('Integration to Mautic; syncing iteration %s', $this->syncIteration),
self::class.':'.__FUNCTION__
);
$syncReport = $this->integrationSyncProcess->getSyncReport($this->syncIteration);
if (!$syncReport->shouldSync()) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
'Integration to Mautic; no objects were mapped to be synced',
self::class.':'.__FUNCTION__
);
break;
}
// Update the mappings in case objects have been converted such as Lead -> Contact
$this->mappingHelper->remapIntegrationObjects($syncReport->getRemappedObjects());
// Maps relations, synchronizes missing objects if necessary
$this->manageRelations($syncReport);
// Convert the integrations' report into an "order" or instructions for Mautic
$syncOrder = $this->mauticSyncProcess->getSyncOrder($syncReport);
if (!$syncOrder->shouldSync()) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
'Integration to Mautic; no object changes were recorded possible due to field direction configurations',
self::class.':'.__FUNCTION__
);
break;
}
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Integration to Mautic; syncing %d total objects',
$syncOrder->getObjectCount()
),
self::class.':'.__FUNCTION__
);
// Execute the sync instructions
$objectMappings = $this->internalSyncDataExchange->executeSyncOrder($syncOrder);
// Dispatch an event to allow subscribers to take action after this batch of objects has been synced to Mautic
$orderResults = $this->getOrderResultsForIntegrationSync($syncOrder, $objectMappings);
$this->eventDispatcher->dispatch(
new CompletedSyncIterationEvent($orderResults, $this->syncIteration, $this->inputOptionsDAO, $this->mappingManualDAO),
IntegrationEvents::INTEGRATION_BATCH_SYNC_COMPLETED_INTEGRATION_TO_MAUTIC
);
unset($orderResults);
if ($this->shouldStopIntegrationSync()) {
break;
}
// Fetch the next iteration/batch
++$this->syncIteration;
}
}
private function executeInternalSync(): void
{
$this->syncIteration = 1;
while (true) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf('Mautic to integration; syncing iteration %s', $this->syncIteration),
self::class.':'.__FUNCTION__
);
$syncReport = $this->mauticSyncProcess->getSyncReport($this->syncIteration);
if (!$syncReport->shouldSync()) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
'Mautic to integration; no objects were mapped to be synced',
self::class.':'.__FUNCTION__
);
break;
}
// Convert the internal report into an "order" or instructions for the integration
$syncOrder = $this->integrationSyncProcess->getSyncOrder($syncReport);
if (!$syncOrder->shouldSync()) {
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
'Mautic to integration; no object changes were recorded possible due to field direction configurations',
self::class.':'.__FUNCTION__
);
// Finalize notifications such as injecting user notifications
$this->notifier->finalizeNotifications();
break;
}
DebugLogger::log(
$this->mappingManualDAO->getIntegration(),
sprintf(
'Mautic to integration; syncing %d total objects',
$syncOrder->getObjectCount()
),
self::class.':'.__FUNCTION__
);
// Execute the sync instructions
$this->integrationSyncDataExchange->executeSyncOrder($syncOrder);
// Save mappings and cleanup
$this->finalizeSync($syncOrder);
// Dispatch an event to allow subscribers to take action after this batch of objects has been synced to the integration
$orderResults = $this->getOrderResultsForInternalSync($syncOrder);
$this->eventDispatcher->dispatch(
new CompletedSyncIterationEvent($orderResults, $this->syncIteration, $this->inputOptionsDAO, $this->mappingManualDAO),
IntegrationEvents::INTEGRATION_BATCH_SYNC_COMPLETED_MAUTIC_TO_INTEGRATION
);
unset($orderResults);
// Fetch the next iteration/batch
++$this->syncIteration;
}
}
private function manageRelations(ReportDAO $syncReport): void
{
// Map relations
$this->relationsHelper->processRelations($this->mappingManualDAO, $syncReport);
// Relation objects we need to synchronize
$objectsToSynchronize = $this->relationsHelper->getObjectsToSynchronize();
if (!empty($objectsToSynchronize)) {
$this->synchronizeMissingObjects($objectsToSynchronize, $syncReport);
}
}
private function synchronizeMissingObjects(array $objectsToSynchronize, ReportDAO $syncReport): void
{
$inputOptions = $this->getInputOptionsForObjects($objectsToSynchronize);
// We need to synchronize missing relation ids
$this->processParallelSync($inputOptions);
// Now we can map relations for objects we have just synchronised
$this->relationsHelper->processRelations($this->mappingManualDAO, $syncReport);
}
/**
* @throws \Mautic\IntegrationsBundle\Exception\InvalidValueException
*/
private function getInputOptionsForObjects(array $objectsToSynchronize): InputOptionsDAO
{
$mauticObjectIds = new ObjectIdsDAO();
foreach ($objectsToSynchronize as $object) {
$mauticObjectIds->addObjectId($object->getObject(), $object->getObjectId());
}
$integration = $this->mappingManualDAO->getIntegration();
return new InputOptionsDAO([
'integration' => $integration,
'integration-object-id' => $mauticObjectIds,
]);
}
/**
* @throws IntegrationNotFoundException
*/
private function processParallelSync($inputOptions): void
{
$currentSyncProcess = clone $this->integrationSyncProcess;
$this->syncService->processIntegrationSync($inputOptions);
// We need to bring back current $inputOptions which were overwritten by new sync
$this->integrationSyncProcess = $currentSyncProcess;
}
private function shouldStopIntegrationSync(): bool
{
// We don't want to iterate sync for specific ids
return null !== $this->inputOptionsDAO->getIntegrationObjectIds();
}
/**
* @throws IntegrationNotFoundException
* @throws HandlerNotSupportedException
*/
private function finalizeSync(OrderDAO $syncOrder): void
{
// Save the mappings between Mautic objects and the integration's objects
$this->mappingHelper->saveObjectMappings($syncOrder->getObjectMappings());
// Remap integration objects to Mautic objects if applicable
$this->mappingHelper->remapIntegrationObjects($syncOrder->getRemappedObjects());
// Update last sync dates on existing object mappings
$this->mappingHelper->updateObjectMappings($syncOrder->getUpdatedObjectMappings());
// Tell sync that these objects have been deleted and not to continue re-syncing them
$this->mappingHelper->markAsDeleted($syncOrder->getDeletedObjects());
// Inject notifications
$this->notifier->noteMauticSyncIssue($syncOrder->getNotifications());
// Cleanup field tracking for successfully synced objects
$this->internalSyncDataExchange->cleanupProcessedObjects($syncOrder->getSuccessfullySyncedObjects());
}
private function getOrderResultsForIntegrationSync(OrderDAO $syncOrder, ObjectMappingsDAO $objectMappings): OrderResultsDAO
{
// New objects were processed by OrderExecutioner
$newObjectMappings = $objectMappings->getNewMappings();
// Updated objects were processed by OrderExecutioner
$updatedObjectMappings = $objectMappings->getUpdatedMappings();
// Remapped objects
$remappedObjects = $syncOrder->getRemappedObjects();
// Deleted objects
$deletedObjects = $syncOrder->getDeletedObjects();
return new OrderResultsDAO($newObjectMappings, $updatedObjectMappings, $remappedObjects, $deletedObjects);
}
private function getOrderResultsForInternalSync(OrderDAO $syncOrder): OrderResultsDAO
{
// New object mappings
$newObjectMappings = $syncOrder->getObjectMappings();
// Updated object mappings
$updatedObjectMappings = [];
foreach ($syncOrder->getUpdatedObjectMappings() as $updatedObjectMapping) {
if (!$updatedObjectMapping->getObjectMapping()) {
continue;
}
$updatedObjectMappings[] = $updatedObjectMapping->getObjectMapping();
}
// Remapped objects
$remappedObjects = $syncOrder->getRemappedObjects();
// Deleted objects
$deletedObjects = $syncOrder->getDeletedObjects();
return new OrderResultsDAO($newObjectMappings, $updatedObjectMappings, $remappedObjects, $deletedObjects);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncService;
use GuzzleHttp\Exception\ClientException;
use Mautic\IntegrationsBundle\Helper\SyncIntegrationsHelper;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InputOptionsDAO;
use Mautic\IntegrationsBundle\Sync\Helper\MappingHelper;
use Mautic\IntegrationsBundle\Sync\Helper\RelationsHelper;
use Mautic\IntegrationsBundle\Sync\Helper\SyncDateHelper;
use Mautic\IntegrationsBundle\Sync\Logger\DebugLogger;
use Mautic\IntegrationsBundle\Sync\Notification\Notifier;
use Mautic\IntegrationsBundle\Sync\SyncDataExchange\MauticSyncDataExchange;
use Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Integration\IntegrationSyncProcess;
use Mautic\IntegrationsBundle\Sync\SyncProcess\Direction\Internal\MauticSyncProcess;
use Mautic\IntegrationsBundle\Sync\SyncProcess\SyncProcess;
use Psr\Log\LogLevel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class SyncService implements SyncServiceInterface
{
public function __construct(
private MauticSyncDataExchange $internalSyncDataExchange,
private SyncDateHelper $syncDateHelper,
private MappingHelper $mappingHelper,
private RelationsHelper $relationsHelper,
private SyncIntegrationsHelper $syncIntegrationsHelper,
private EventDispatcherInterface $eventDispatcher,
private Notifier $notifier,
private IntegrationSyncProcess $integratinSyncProcess,
private MauticSyncProcess $mauticSyncProcess,
) {
}
/**
* @throws \Mautic\IntegrationsBundle\Exception\IntegrationNotFoundException
*/
public function processIntegrationSync(InputOptionsDAO $inputOptionsDAO): void
{
$integrationSyncProcess = new SyncProcess(
$this->syncDateHelper,
$this->mappingHelper,
$this->relationsHelper,
$this->integratinSyncProcess,
$this->mauticSyncProcess,
$this->eventDispatcher,
$this->notifier,
$this->syncIntegrationsHelper->getMappingManual($inputOptionsDAO->getIntegration()),
$this->internalSyncDataExchange,
$this->syncIntegrationsHelper->getSyncDataExchange($inputOptionsDAO->getIntegration()),
$inputOptionsDAO,
$this
);
DebugLogger::log(
$inputOptionsDAO->getIntegration(),
sprintf(
'Starting %s sync from %s date/time',
$inputOptionsDAO->isFirstTimeSync() ? 'first time' : 'subsequent',
$inputOptionsDAO->getStartDateTime() ? $inputOptionsDAO->getStartDateTime()->format('Y-m-d H:i:s') : 'yet to be determined'
),
self::class.':'.__FUNCTION__
);
try {
$integrationSyncProcess->execute();
} catch (ClientException $exception) {
// The sync failed to communicate with the integration so log it
DebugLogger::log($inputOptionsDAO->getIntegration(), $exception->getMessage(), null, [], LogLevel::ERROR);
}
}
public function initiateDebugLogger(DebugLogger $logger): void
{
// Yes it's a hack to prevent from having to pass the logger as a dependency into dozens of classes
// So not doing anything with the logger, just need Symfony to initiate the service
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\SyncService;
use Mautic\IntegrationsBundle\Sync\DAO\Sync\InputOptionsDAO;
interface SyncServiceInterface
{
public function processIntegrationSync(InputOptionsDAO $inputOptionsDAO);
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\ValueNormalizer;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
final class ValueNormalizer implements ValueNormalizerInterface
{
/**
* @param mixed $value
*/
public function normalizeForMautic(string $type, $value): NormalizedValueDAO
{
switch ($type) {
case NormalizedValueDAO::STRING_TYPE:
case NormalizedValueDAO::TEXT_TYPE:
case NormalizedValueDAO::TEXTAREA_TYPE:
case NormalizedValueDAO::URL_TYPE:
case NormalizedValueDAO::EMAIL_TYPE:
case NormalizedValueDAO::SELECT_TYPE:
case NormalizedValueDAO::MULTISELECT_TYPE:
case NormalizedValueDAO::REGION_TYPE:
case NormalizedValueDAO::LOOKUP_TYPE:
return new NormalizedValueDAO($type, $value, (string) $value);
case NormalizedValueDAO::INT_TYPE:
return new NormalizedValueDAO($type, $value, (int) $value);
case NormalizedValueDAO::FLOAT_TYPE:
case NormalizedValueDAO::DOUBLE_TYPE:
return new NormalizedValueDAO($type, $value, (float) $value);
case NormalizedValueDAO::DATE_TYPE:
case NormalizedValueDAO::DATETIME_TYPE:
// We expect a string value.
if (is_string($value)) {
return new NormalizedValueDAO($type, $value, new \DateTime($value));
}
// Other value types we normalize to null.
return new NormalizedValueDAO($type, $value, null);
case NormalizedValueDAO::BOOLEAN_TYPE:
$value = 'false' === $value ? false : $value;
$value = 'true' === $value ? true : $value;
return new NormalizedValueDAO($type, $value, (bool) $value);
default:
throw new \InvalidArgumentException('Variable type, '.$type.', not supported');
}
}
/**
* @return mixed
*/
public function normalizeForIntegration(NormalizedValueDAO $value)
{
return $value->getNormalizedValue();
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\ValueNormalizer;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
interface ValueNormalizerInterface
{
public function normalizeForMautic(string $value, $type): NormalizedValueDAO;
/**
* @return mixed
*/
public function normalizeForIntegration(NormalizedValueDAO $value);
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\VariableExpresser;
use Mautic\IntegrationsBundle\Sync\DAO\Value\EncodedValueDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
use Mautic\IntegrationsBundle\Sync\ValueNormalizer\ValueNormalizer;
final class VariableExpresserHelper implements VariableExpresserHelperInterface
{
public const TRUE_BOOLEAN_VALUE = 'true';
public const FALSE_BOOLEAN_VALUE = 'false';
private ValueNormalizer $valueNormalizer;
public function __construct()
{
$this->valueNormalizer = new ValueNormalizer();
}
public function decodeVariable(EncodedValueDAO $encodedValueDAO): NormalizedValueDAO
{
$value = $encodedValueDAO->getValue();
return $this->valueNormalizer->normalizeForMautic($encodedValueDAO->getType(), $value);
}
/**
* @param mixed $var
*/
public function encodeVariable($var): EncodedValueDAO
{
if (is_null($var)) {
return new EncodedValueDAO(EncodedValueDAO::STRING_TYPE, '');
}
if (is_int($var)) {
return new EncodedValueDAO(EncodedValueDAO::INT_TYPE, (string) $var);
}
if (is_string($var)) {
return new EncodedValueDAO(EncodedValueDAO::STRING_TYPE, (string) $var);
}
if (is_float($var)) {
return new EncodedValueDAO(EncodedValueDAO::FLOAT_TYPE, (string) $var);
}
if (is_double($var)) {
return new EncodedValueDAO(EncodedValueDAO::DOUBLE_TYPE, (string) $var);
}
if ($var instanceof \DateTime) {
return new EncodedValueDAO(EncodedValueDAO::DATETIME_TYPE, $var->format('c'));
}
if (is_bool($var)) {
return new EncodedValueDAO(
EncodedValueDAO::BOOLEAN_TYPE,
true === $var ? self::TRUE_BOOLEAN_VALUE : self::FALSE_BOOLEAN_VALUE
);
}
throw new \InvalidArgumentException('Variable type for '.var_export($var, true).' not supported');
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Mautic\IntegrationsBundle\Sync\VariableExpresser;
use Mautic\IntegrationsBundle\Sync\DAO\Value\EncodedValueDAO;
use Mautic\IntegrationsBundle\Sync\DAO\Value\NormalizedValueDAO;
interface VariableExpresserHelperInterface
{
public function decodeVariable(EncodedValueDAO $EncodedValueDAO): NormalizedValueDAO;
/**
* @param mixed $var
*/
public function encodeVariable($var): EncodedValueDAO;
}