Spaces:
No application file
No application file
mautic
/
app
/bundles
/IntegrationsBundle
/Sync
/SyncDataExchange
/Internal
/ObjectHelper
/ContactObjectHelper.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(); | |
| } | |
| } | |