Current File : /var/www/prestashop/modules/ps_eventbus/src/Service/SynchronizationService.php
<?php

namespace PrestaShop\Module\PsEventbus\Service;

use PrestaShop\Module\PsEventbus\Decorator\PayloadDecorator;
use PrestaShop\Module\PsEventbus\Provider\PaginatedApiDataProviderInterface;
use PrestaShop\Module\PsEventbus\Repository\DeletedObjectsRepository;
use PrestaShop\Module\PsEventbus\Repository\EventbusSyncRepository;
use PrestaShop\Module\PsEventbus\Repository\IncrementalSyncRepository;
use PrestaShop\Module\PsEventbus\Repository\LanguageRepository;
use PrestaShop\Module\PsEventbus\Repository\LiveSyncRepository;

class SynchronizationService
{
    /**
     * @var \Ps_eventbus
     */
    private $module;

    /**
     * @var EventbusSyncRepository
     */
    private $eventbusSyncRepository;

    /**
     * @var IncrementalSyncRepository
     */
    private $incrementalSyncRepository;

    /**
     * @var LiveSyncRepository
     */
    private $liveSyncRepository;

    /**
     * @var DeletedObjectsRepository
     */
    private $deletedObjectsRepository;

    /**
     * @var LanguageRepository
     */
    private $languageRepository;

    /**
     * @var PayloadDecorator
     */
    private $payloadDecorator;

    /**
     * @var int
     */
    const RANDOM_SYNC_CHECK_MAX = 20;

    /**
     * @var int
     */
    const INCREMENTAL_SYNC_MAX_ITEMS_PER_SHOP_CONTENT = 100000;

    public function __construct(
        \Ps_eventbus $module,
        EventbusSyncRepository $eventbusSyncRepository,
        IncrementalSyncRepository $incrementalSyncRepository,
        LiveSyncRepository $liveSyncRepository,
        DeletedObjectsRepository $deletedObjectsRepository,
        LanguageRepository $languageRepository,
        PayloadDecorator $payloadDecorator
    ) {
        $this->module = $module;
        $this->eventbusSyncRepository = $eventbusSyncRepository;
        $this->incrementalSyncRepository = $incrementalSyncRepository;
        $this->liveSyncRepository = $liveSyncRepository;
        $this->deletedObjectsRepository = $deletedObjectsRepository;
        $this->languageRepository = $languageRepository;
        $this->payloadDecorator = $payloadDecorator;
    }

    /**
     * @param PaginatedApiDataProviderInterface $dataProvider
     * @param string $type
     * @param string $jobId
     * @param string $langIso
     * @param int $offset
     * @param int $limit
     * @param string $dateNow
     * @param int $scriptStartTime
     * @param bool $isFull
     *
     * @return array<mixed>
     *
     * @@throws \PrestaShopDatabaseException|EnvVarException|ApiException
     */
    public function handleFullSync(
        PaginatedApiDataProviderInterface $dataProvider,
        $type,
        $jobId,
        $langIso,
        $offset,
        $limit,
        $dateNow,
        $scriptStartTime,
        $isFull
    ) {
        $response = [];

        $data = $dataProvider->getFormattedData($offset, $limit, $langIso);

        $this->payloadDecorator->convertDateFormat($data);

        if (!empty($data)) {
            /** @var ProxyService */
            $proxyService = $this->module->getService('PrestaShop\Module\PsEventbus\Service\ProxyService');

            $response = $proxyService->upload($jobId, $data, $scriptStartTime, $isFull);

            if ($response['httpCode'] == 201) {
                $offset += $limit;
            }
        }

        $remainingObjects = (int) $dataProvider->getRemainingObjectsCount($offset, $langIso);

        if ($remainingObjects <= 0) {
            $remainingObjects = 0;
            $offset = 0;
        }

        $this->eventbusSyncRepository->updateTypeSync($type, $offset, $dateNow, $remainingObjects === 0, $langIso);

        return $this->returnSyncResponse($data, $response, $remainingObjects);
    }

    /**
     * @param PaginatedApiDataProviderInterface $dataProvider
     * @param string $type
     * @param string $jobId
     * @param int $limit
     * @param string $langIso
     * @param int $scriptStartTime
     * @param bool $isFull
     *
     * @return array<mixed>
     *
     * @@throws \PrestaShopDatabaseException|EnvVarException
     */
    public function handleIncrementalSync(
        PaginatedApiDataProviderInterface $dataProvider,
        $type,
        $jobId,
        $limit,
        $langIso,
        $scriptStartTime,
        $isFull
    ) {
        $response = [];

        $objectIds = $this->incrementalSyncRepository->getIncrementalSyncObjectIds($type, $langIso, $limit);

        if (empty($objectIds)) {
            return [
                'total_objects' => 0,
                'has_remaining_objects' => false,
                'remaining_objects' => 0,
            ];
        }

        $data = $dataProvider->getFormattedDataIncremental($limit, $langIso, $objectIds);

        $this->payloadDecorator->convertDateFormat($data);

        if (!empty($data)) {
            /** @var ProxyService */
            $proxyService = $this->module->getService('PrestaShop\Module\PsEventbus\Service\ProxyService');

            $response = $proxyService->upload($jobId, $data, $scriptStartTime, $isFull);

            if ($response['httpCode'] == 201) {
                $this->incrementalSyncRepository->removeIncrementalSyncObjects($type, $objectIds, $langIso);
            }
        } else {
            $this->incrementalSyncRepository->removeIncrementalSyncObjects($type, $objectIds, $langIso);
        }

        $remainingObjects = $this->incrementalSyncRepository->getRemainingIncrementalObjects($type, $langIso);

        return $this->returnSyncResponse($data, $response, $remainingObjects);
    }

    /**
     * disables liveSync
     *
     * @param string $shopContent
     * @param int $shopContentId
     * @param string $action
     *
     * @return void
     */
    public function sendLiveSync($shopContent, $shopContentId, $action)
    {
        if ($this->isFullSyncDone($shopContent)) {
            // $this->debounceLiveSync($shopContent);
        }
    }

    /**
     * @param int $objectId
     * @param string $type
     * @param string $createdAt
     * @param int $shopId
     * @param bool $hasMultiLang
     *
     * @return void
     */
    public function insertIncrementalSyncObject($objectId, $type, $createdAt, $shopId, $hasMultiLang = null)
    {
        if ((int) $objectId === 0) {
            return;
        }

        /*
         * randomly check if outbox for this shop-content contain more of 100k entries.
         * When random number == 10, we count number of entry exist in database for this specific shop content
         * If count > 100 000, we removed all entry corresponding to this shop content, and we enable full sync for this
         */
        if (mt_rand() % $this::RANDOM_SYNC_CHECK_MAX == 0) {
            $count = $this->incrementalSyncRepository->getIncrementalSyncObjectCountByType($type);
            if ($count > $this::INCREMENTAL_SYNC_MAX_ITEMS_PER_SHOP_CONTENT) {
                $hasDeleted = $this->incrementalSyncRepository->removeIncrementaSyncObjectByType($type);

                if ($hasDeleted) {
                    $this->eventbusSyncRepository->updateTypeSync(
                        $type,
                        0,
                        $createdAt,
                        false,
                        $this->languageRepository->getDefaultLanguageIsoCode()
                    );
                }
            }

            return;
        }

        $objectsData = [];

        if ($hasMultiLang) {
            $allIsoCodes = $this->languageRepository->getLanguagesIsoCodes();

            foreach ($allIsoCodes as $langIso) {
                if ($this->isFullSyncDone($type, $langIso)) {
                    array_push($objectsData,
                        [
                            'type' => $type,
                            'id_object' => $objectId,
                            'id_shop' => $shopId,
                            'lang_iso' => $langIso,
                            'created_at' => $createdAt,
                        ]
                    );
                }
            }
        } else {
            $defaultIsoCode = $this->languageRepository->getDefaultLanguageIsoCode();

            if ($this->isFullSyncDone($type, $defaultIsoCode)) {
                array_push($objectsData,
                    [
                        'type' => $type,
                        'id_object' => $objectId,
                        'id_shop' => $shopId,
                        'lang_iso' => $defaultIsoCode,
                        'created_at' => $createdAt,
                    ]
                );
            }
        }

        if (empty($objectsData) == false) {
            $this->incrementalSyncRepository->insertIncrementalObject($objectsData);
        }
    }

    /**
     * @param int $objectId
     * @param string $type
     * @param string $date
     * @param int $shopId
     *
     * @return void
     */
    public function insertDeletedObject($objectId, $type, $date, $shopId)
    {
        if ((int) $objectId === 0) {
            return;
        }

        $this->deletedObjectsRepository->insertDeletedObject($objectId, $type, $date, $shopId);
        $this->incrementalSyncRepository->removeIncrementalSyncObject($type, $objectId);
    }

    /**
     * @param string $shopContentName
     *
     * @return bool
     *
     * @@throws \PrestaShopDatabaseException
     */
    private function debounceLiveSync($shopContentName) // @phpstan-ignore method.unused
    {
        $dateNow = date('Y-m-d H:i:s');

        $shopContent = $this->liveSyncRepository->getShopContentInfo($shopContentName);

        $lastChangeAt = $shopContent != null ? (string) $shopContent['last_change_at'] : (string) $dateNow;
        $diff = strtotime((string) $dateNow) - strtotime((string) $lastChangeAt);

        if ($shopContent == null || $diff > 60 * 5) {
            $this->liveSyncRepository->upsertDebounce($shopContentName, $dateNow);

            return true;
        }

        return false;
    }

    /**
     * Return true if full sync is done for this shop content
     *
     * @param string $shopContent
     * @param string|null $langIso
     *
     * @return bool
     */
    private function isFullSyncDone($shopContent, $langIso = null)
    {
        return $this->eventbusSyncRepository->isFullSyncDoneForThisTypeSync($shopContent, $langIso);
    }

    /**
     * @param array<mixed> $data
     * @param array<mixed> $syncResponse
     * @param int $remainingObjects
     *
     * @return array<mixed>
     */
    private function returnSyncResponse($data, $syncResponse, $remainingObjects)
    {
        return array_merge([
            'total_objects' => count($data),
            'has_remaining_objects' => $remainingObjects > 0,
            'remaining_objects' => $remainingObjects,
            'md5' => $this->getPayloadMd5($data),
        ], $syncResponse);
    }

    /**
     * @param array<mixed> $payload
     *
     * @return string
     */
    private function getPayloadMd5($payload)
    {
        return md5(
            implode(' ', array_map(function ($payloadItem) {
                return $payloadItem['id'];
            }, $payload))
        );
    }
}