<?php declare(strict_types=1);
namespace Compra\EsiSW6\Administration\Subscriber;
use Compra\EsiSW6\Core\System\Service\ApiService;
use Compra\EsiSW6\Import\Service\ImportService;
use Compra\FoundationSW6\Core\System\PluginConfigService;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Event\NestedEventCollection;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerRunningEvent;
use Symfony\Component\Messenger\Event\WorkerStartedEvent;
class AdministrationSubscriber implements EventSubscriberInterface
{
public const B2B_SUITE_PLUGIN_NAME = 'SwagB2bPlatform';
protected ImportService $importService;
protected ApiService $apiService;
protected Connection $connection;
protected PluginConfigService $pluginConfigService;
protected EntityRepository $esiImportChecksumRepository;
protected EntityRepository $customerRepository;
protected EntityRepository $pluginRepository;
protected array $deletedEntities = [];
public function __construct
(
ImportService $importService,
ApiService $apiService,
Connection $connection,
PluginConfigService $pluginConfigService,
EntityRepository $esiImportChecksumRepository,
EntityRepository $customerRepository,
EntityRepository $pluginRepository
)
{
$this->importService = $importService;
$this->apiService = $apiService;
$this->connection = $connection;
$this->pluginConfigService = $pluginConfigService;
$this->esiImportChecksumRepository = $esiImportChecksumRepository;
$this->customerRepository = $customerRepository;
$this->pluginRepository = $pluginRepository;
}
public static function getSubscribedEvents(): array
{
return [
EntityWrittenContainerEvent::class => 'onEntityWritten',
WorkerStartedEvent::class => 'onWorkerStartedOrRunning',
WorkerRunningEvent::class => 'onWorkerStartedOrRunning'
];
}
/**
* Subscriber to check for deleted entities and remove those entries from esi_import_checksum table.
*
* @param EntityWrittenContainerEvent $entityWrittenContainerEvent
* @throws Exception
*/
public function onEntityWritten(EntityWrittenContainerEvent $entityWrittenContainerEvent): void
{
/** @var NestedEventCollection|null $nestedEvents */
$nestedEvents = $entityWrittenContainerEvent->getEvents();
// only collect events if nestedEvents is set
if ($nestedEvents) {
// get all nested events
$events = $nestedEvents->getElements();
foreach ($events as $event) {
if (!($event instanceof EntityDeletedEvent)) {
// if event is no EntityDeletedEvent, continue because we don't need to proceed
continue;
}
/** @var EntityDeletedEvent $event */
// get writeResults
/** @var EntityWriteResult[] $writeResults */
$writeResults = $event->getWriteResults();
// iterate writeResults and collect all entities that are deleted
foreach ($writeResults as $writeResult) {
$entityName = $writeResult->getEntityName();
$primaryKey = $writeResult->getPrimaryKey();
if (is_array($primaryKey)) {
$this->addDeletedEntity($entityName, $primaryKey,$this->deletedEntities);
} else {
$this->addDeletedEntity($entityName, [
'id' => $primaryKey
], $this->deletedEntities);
}
}
}
// only check esi checksum if current AdminUser is NOT the ESI user
if ($this->apiService->getAdminUser($entityWrittenContainerEvent->getContext()) !== ApiService::ESI_API_USER) {
$this->checkAndDeleteChecksum($entityWrittenContainerEvent->getContext());
}
// check B2B additional delete actions
$this->checkB2BAdditionalDeleteActions($entityWrittenContainerEvent->getContext());
}
}
/**
* Subscriber to handle the stop Workers during ESI import functionality.
*
* @param WorkerStartedEvent|WorkerRunningEvent $event
*/
public function onWorkerStartedOrRunning($event): void
{
$esiIsRunningFilePath = $this->importService->getEsiIsRunningFilePath();
$stopWorkersDuringEsiImport = (bool) $this->pluginConfigService->getPluginConfig('stopWorkersDuringEsiImport');
if (!$stopWorkersDuringEsiImport || !file_exists($esiIsRunningFilePath)) {
// skip further processing if should not stop workers during ESI import or "esi-is-running" file doesn't exist
return;
}
if ($this->importService->checkEsiIsRunningFileIsInAllowedLifetime()) {
// file last modified in allowed lifetime - stop the Worker
$event->getWorker()->stop();
} else {
// file is older than allowed lifetime - delete the file and don't stop the Worker
$this->importService->deleteEsiIsRunningFile();
}
}
/**
* @param string $entityName
* @param array $data
* @param array $elementsArray
* @return void
*/
protected function addDeletedEntity(string $entityName, array $data, array &$elementsArray): void
{
if (array_key_exists($entityName, $elementsArray)) {
$elementsArray[$entityName][] = $data;
} else {
$elementsArray[$entityName] = [];
$elementsArray[$entityName][] = $data;
}
}
/**
* Helper function to check the deletedElements in esi_import_checksum table.
* If en entry exists, deletes this entry from esi_import_checksum
*
* @param $context Context
* @throws Exception
*/
protected function checkAndDeleteChecksum(Context $context): void
{
if (!$this->deletedEntities) {
// skip if no deleted elements set
return;
}
$esiImportChecksumItemsToDelete = [];
// iterate all deletedElements (grouped by entity as key)
foreach ($this->deletedEntities as $entityName => $entities) {
// create querybuilder for the current entity
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder->select("LOWER(HEX(id))")
->from("esi_import_checksum");
// iterate all deletedElements of the current entity
foreach ($entities as $entity) {
$queryBuilder->resetQueryPart('where');
$queryBuilder->where("entity = :entity");
// iterate all entity keys
foreach ($entity as $entityKey => $keyValue) {
$queryBuilder->andWhere("JSON_EXTRACT(entity_keys, '$.$entityKey') = '$keyValue'");
}
$queryBuilder->setParameter('entity', $entityName);
$resultId = $this->connection->fetchOne($queryBuilder->getSQL(), $queryBuilder->getParameters());
if ($resultId) {
// add found result to array of esi_import_checksum entities that should be deleted
$esiImportChecksumItemsToDelete[] = [
'id' => $resultId
];
}
}
}
if ($esiImportChecksumItemsToDelete) {
// delete entities from esi_import_checksum
$this->esiImportChecksumRepository->delete($esiImportChecksumItemsToDelete, $context);
}
}
/**
* Helper function to check, if B2B relevant data were deleted.
* If so, we need to perform additional delete actions on specific B2B entities.
*
* @param Context $context
* @throws Exception
*/
protected function checkB2BAdditionalDeleteActions(Context $context): void
{
// first check if SwagB2bPlatform is installed at all
$b2bSuiteActiveCriteria = new Criteria();
$b2bSuiteActiveCriteria->addFilter(new EqualsFilter('name', self::B2B_SUITE_PLUGIN_NAME));
$b2bSuiteActiveCriteria->addFilter(new EqualsFilter('active', true));
if (!$this->pluginRepository->searchIds($b2bSuiteActiveCriteria, $context)->firstId()) {
// if B2B Suite is not active
return;
}
if (array_key_exists('customer', $this->deletedEntities)) {
// perform additional delete actions for customer delete
$this->performB2BAdditionalDeleteForCustomer($context);
}
}
/**
* Helper function to perform additional B2B delete action for `customer`.
* We need to:
* 1.) delete corresponding data in b2b_store_front_auth (we can use ID of deleted customer/debtor as provider_context)
* 2.) delete all customers/contacts with same customerNumber as deleted customer (from b2b_contact_debtor customField compra_eevo_debtor_customernumber)
*
* @param Context $context
* @throws Exception
* @throws \Doctrine\DBAL\Driver\Exception
*/
protected function performB2BAdditionalDeleteForCustomer(Context $context): void
{
/* 1. delete corresponding data in b2b_store_front_auth */
// first collect all IDs for searching
$deletedCustomers = array_column($this->deletedEntities['customer'], 'id');
// search possible data to delete from b2b_store_front_auth
$query = $this->connection->createQueryBuilder();
$query->select('id')
->from('b2b_store_front_auth')
->where('provider_context IN (:deletedCustomers)')
->setParameter('deletedCustomers', $deletedCustomers, Connection::PARAM_STR_ARRAY);
$customerAuthIds = $query->execute()->fetchFirstColumn();
if ($customerAuthIds) {
// collect all contact/debtor IDs for this customer/debtor
$query = $this->connection->createQueryBuilder();
$query->select('id')
->from('b2b_store_front_auth')
->where('context_owner_id IN (:customerAuthIds)')
->setParameter('customerAuthIds', $customerAuthIds, Connection::PARAM_INT_ARRAY);
$contactAuthIds = $query->execute()->fetchFirstColumn();
if ($contactAuthIds) {
// get contacts to delete with customFields
$query = $this->connection->createQueryBuilder();
$query->select('id, auth_id, JSON_UNQUOTE(JSON_EXTRACT(compra_custom_fields, \'$.compra_eevo_debtor_customernumber\')) AS customerNumber')
->from('b2b_debtor_contact')
->where('auth_id IN (:contactAuthIds)')
->setParameter('contactAuthIds', $contactAuthIds, Connection::PARAM_INT_ARRAY);
$contacts = $query->execute()->fetchAllAssociative();
// delete all corresponding data from b2b_store_front_auth
$query = $this->connection->createQueryBuilder();
$query->delete('b2b_store_front_auth')
->where('id IN (:contactAuthIds)')
->setParameter('contactAuthIds', $contactAuthIds, Connection::PARAM_INT_ARRAY);
$query->execute();
// delete all corresponding data from b2b_debtor_contact
$query = $this->connection->createQueryBuilder();
$query->delete('b2b_debtor_contact')
->where('auth_id IN (:contactAuthIds)')
->setParameter('contactAuthIds', $contactAuthIds, Connection::PARAM_INT_ARRAY);
$query->execute();
/* 2.) delete all customers with same customerNumber as deleted customer */
$customerNumbersToDelete = array_filter(array_column($contacts, 'customerNumber'));
$customerSearchCriteria = new Criteria();
$customerSearchCriteria->addFilter(new EqualsAnyFilter('customerNumber', $customerNumbersToDelete));
$result = $this->customerRepository->searchIds($customerSearchCriteria, $context);
if ($result->firstId()) {
// use array_values because getData() returns the found Ids with ID as key, but for deleting we need 0,1,2... as keys
$customersToDeleteIds = array_values($this->customerRepository->searchIds($customerSearchCriteria, $context)->getData());
$this->customerRepository->delete($customersToDeleteIds, $context);
}
}
}
}
}