<?php
namespace Abas\AbasConnector\Service\Component\Cache;
use Abas\AbasConnector\Service\Component\AbasComponent;
use Abas\AbasConnector\Service\Log\AbasLogger;
use Abas\AbasConnector\Service\RestApi\Client\AbasRestApiClient;
use Abas\AbasConnector\Service\RestApi\Meta\MetaData;
use Abas\AbasConnector\Service\RestApi\Meta\MetaDataField;
use Abas\AbasConnector\Service\RestApi\Meta\MetaDataService;
use Abas\AbasConnector\Service\RestApi\Request\AbasPostRequest;
use Abas\AbasConnector\Service\RestApi\Request\Factories\DataRequestFactory;
use Abas\AbasConnector\Service\RestApi\Response\AbasResponse;
use Abas\AbasConnector\Service\RestApi\Response\Content;
use Abas\AbasConnector\Service\Utils\Cache\CacheFactory;
use Abas\AbasConnector\Service\Utils\Cache\CacheKeyGenerator;
use Abas\AbasConnector\Service\Utils\Config\PluginConfigReader;
use Abas\AbasConnector\Service\Utils\Field\FieldUtil;
use Abas\AbasConnector\Service\Core\Exceptions\InvalidArgumentException;
use Abas\AbasConnector\Service\Core\Enums\Duration;
use Abas\AbasConnector\Service\RestApi\Response\Content\ErpDataObject;
use Abas\AbasConnector\Service\Utils\Benchmark\Benchmark;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Contracts\Cache\ItemInterface;
use function microtime;
use function str_replace;
class AbasComponentCache
{
/**
* @var string
*/
public const WORKSPACES_KEY = 'WORKSPACES';
/**
* @var AbstractAdapter
*/
private $componentAdapter;
/**
* @var AbstractAdapter
*/
private $workspaceAdapter;
/**
* @var AbasRestApiClient
*/
private $apiClient;
/**
* @var int
*/
private $defaultLifetime;
/**
* @var AbasLogger|null
*/
private $logger;
/**
* @var AbstractAdapter[]
*/
private $caches = [];
/**
* @var MetaDataService
*/
private $metaDataService;
/**
* @var PluginConfigReader
*/
private $pluginConfigReader;
/**
* @param AbasRestApiClient $apiClient
* @param MetaDataService $metaDataService
* @param PluginConfigReader $pluginConfigReader
* @param int $defaultLifetime
* @param string $directory
* @param MarshallerInterface $marshaller
* @param AbasLogger $logger
*/
public function __construct(
AbasRestApiClient $apiClient,
MetaDataService $metaDataService,
PluginConfigReader $pluginConfigReader,
int $defaultLifetime = 3600,
string $directory = null,
MarshallerInterface $marshaller,
AbasLogger $logger = null
) {
$this->componentAdapter = new FilesystemAdapter(
'AbasComponentCache',
$defaultLifetime,
$directory,
$marshaller
);
$this->workspaceAdapter = (new CacheFactory('AbasWorkspaceCache', $pluginConfigReader))->create();
$this->apiClient = $apiClient;
$this->defaultLifetime = $defaultLifetime;
$this->logger = $logger;
$this->metaDataService = $metaDataService;
$this->pluginConfigReader = $pluginConfigReader;
}
/**
* Save a component in the cache and return the bid.
*
* @param AbasComponent $component
*
* @return string bid
* @throws InvalidArgumentException
*/
public function saveComponent(AbasComponent $component): string
{
$bid = $component->getBid();
$this->saveWorkspaceIdentifier($component, $bid);
$item = $this->componentAdapter->getItem($bid);
$item->set($component);
$item->expiresAfter($this->defaultLifetime);
$this->componentAdapter->save($item);
return $bid;
}
/**
* Get a component by the bid.
*
* @param string $bid
*
* @return AbasComponent|null
* @throws InvalidArgumentException
*/
public function getComponent(string $bid): ?AbasComponent
{
$item = $this->componentAdapter->getItem($bid);
if ($item->isHit()) {
return $item->get();
}
$this->closeWorkspaceForBid($bid);
return null;
}
/**
* @param string $databaseAndGroup
* @param string $criteria
* @param string|null $headFields
* @param string|null $tableFields
* @param string $identifierField
* @param int $packageSize
*
* @return void
*/
public function loadViewCache(
string $databaseAndGroup,
string $criteria = '',
?string $headFields = null,
?string $tableFields = null,
string $identifierField = 'id',
int $packageSize = 200
): void {
$startTimeTotal = microtime(true);
$this->logger->info('AbasComponentCache: load view from cache.');
$cache = $this->getCache($databaseAndGroup);
$cache->clear();
if ($headFields === null || $tableFields === null) {
$metaData = $this->metaDataService->getMetaData($databaseAndGroup);
$headFields = $this->getFields($headFields, $metaData, false);
$tableFields = $this->getFields($tableFields, $metaData, true);
}
$resultSize = -1;
$totalSize = 0;
$offset = 0;
while ($resultSize !== 0) {
$startTime = microtime(true);
$request = DataRequestFactory::createPostRequest(
$databaseAndGroup,
$criteria,
$headFields,
$tableFields,
$packageSize,
$offset
);
$abasResponse = $this->apiClient->executeRequest($request, true);
$erpDataObjects = $abasResponse->getObjects();
if ($erpDataObjects === null) {
break;
}
$resultSize = $erpDataObjects->count();
/** @var ErpDataObject $erpDataObject */
foreach ($erpDataObjects as $erpDataObject) {
$id = $erpDataObject->getFieldValue($identifierField);
if (empty($id)) {
continue;
}
$cache->get(
CacheKeyGenerator::generateKey($id),
function (ItemInterface $item) use ($erpDataObject) {
$item->expiresAfter(Duration::DAY);
$data = $erpDataObject;
$data->setType('ErpDataObject');
$content = new Content();
$content->setData($data);
$dataResponse = new AbasResponse();
$dataResponse->setContent($content);
return $dataResponse;
}
);
}
$time = round(microtime(true) - $startTime, 2);
$timePerObject = $time / $resultSize;
$this->logger->info('AbasComponentCache: Loading took: ' . $time . ' s and ' . $timePerObject
. ' s per object.');
$totalSize += $resultSize;
if ($resultSize < $packageSize) {
break;
}
$offset += $packageSize;
}
$this->logger->info(
'AbasComponentCache: Loaded ' . $totalSize . ' objects into cache. Took: '
. Benchmark::getFormattedTime($startTimeTotal)
);
}
/**
* @param string $databaseAndGroup
*
* @return AbstractAdapter
*/
protected function getCache(string $databaseAndGroup): AbstractAdapter
{
if (!isset($this->caches[$databaseAndGroup])) {
$cacheFactory = new CacheFactory(
'AbasConnector_' . str_replace(':', '_', $databaseAndGroup),
$this->pluginConfigReader
);
$this->caches[$databaseAndGroup] = $cacheFactory->create();
}
return $this->caches[$databaseAndGroup];
}
/**
* @param string $databaseAndGroup
* @param string $id
*
* @return AbasResponse|null
*/
public function getFromCache(string $databaseAndGroup, string $id): ?AbasResponse
{
if (!\array_key_exists($databaseAndGroup, $this->caches)) {
$this->loadViewCache($databaseAndGroup);
}
$cache = $this->getCache($databaseAndGroup);
$cacheKey = CacheKeyGenerator::generateKey($id);
if ($cache->hasItem($cacheKey)) {
$item = $cache->getItem($cacheKey);
return $item->get();
}
return null;
}
/**
* @param string|null $fields
* @param MetaData $metaData
* @param bool $isTableColumn
*
* @return string
*/
protected function getFields(?string $fields, MetaData $metaData, bool $isTableColumn): string
{
if ($fields === null) {
$fieldArray = [];
$metaDataFields = $metaData->getMetaDataFields();
/** @var MetaDataField $metaDataField */
foreach ($metaDataFields as $metaDataField) {
if ($metaDataField->isSelectable() && $metaDataField->isTableColumn() === $isTableColumn) {
$fieldArray[] = $metaDataField->getName();
}
}
$fields = FieldUtil::fieldArrayToString($fieldArray);
}
return $fields;
}
/**
* Delete a component from the cache and close the workspace.
*
* @param AbasComponent $component
*
* @return void
* @throws InvalidArgumentException
*/
public function deleteComponent(AbasComponent $component): void
{
$bid = $component->getBid();
$this->closeWorkspaceForBid($bid);
$this->componentAdapter->deleteItem($bid);
}
/**
* @param AbasComponent $component
* @param string $bid
*
* @return void
* @throws InvalidArgumentException
*/
private function saveWorkspaceIdentifier(AbasComponent $component, string $bid): void
{
/** @var ItemInterface $item */
$item = $this->workspaceAdapter->getItem(self::WORKSPACES_KEY);
$workspaceIdentifiers = [];
if ($item->isHit()) {
$workspaceIdentifiers = $item->get();
}
$response = $component->getResponse();
if ($response === null) {
return;
}
$content = $response->getContent();
if ($content === null) {
return;
}
$data = $content->getData();
if ($data === null) {
return;
}
$meta = $data->getMeta();
$workspaceIdentifiers[$bid] = [
'workingSetId' => $meta->getWorkingSetId(),
'workingSetEditorId' => $meta->getWorkingSetEditorId()
];
$item->set($workspaceIdentifiers);
$this->workspaceAdapter->save($item);
}
/**
* @param string $bid
*
* @return void
*/
private function closeWorkspaceForBid(string $bid): void
{
$item = $this->workspaceAdapter->getItem(self::WORKSPACES_KEY);
if (!$item->isHit()) {
return;
}
$workspaceIdentifiers = $item->get();
if (!\array_key_exists($bid, $workspaceIdentifiers)) {
return;
}
$workingSetId = $workspaceIdentifiers[$bid]['workingSetId'];
$cancelRequest = new AbasPostRequest('/workspace/' . $workingSetId . '/commands/CANCEL');
$this->apiClient->executeRequest($cancelRequest);
unset($workspaceIdentifiers[$bid]);
$item->set($workspaceIdentifiers);
$this->workspaceAdapter->save($item);
}
/**
* @return void
*/
public function cleanupExpiredComponents(): void
{
$item = $this->workspaceAdapter->getItem(self::WORKSPACES_KEY);
$workspaceIdentifiers = $item->get();
if (is_iterable($workspaceIdentifiers)) {
foreach ($workspaceIdentifiers as $bid => $_workspaceIdentifier) {
if (!$this->componentAdapter->hasItem($bid)) {
$this->closeWorkspaceForBid($bid);
}
}
}
}
}