<?php
namespace App\Service\Cache;
use App\Form\Type\Task\TaskType;
use App\Model\Civility\Civility;
use App\Model\Field\Field;
use App\Model\GetParamsRequest\GetParamsRequest;
use App\Model\LegalForm\LegalForm;
use App\Model\Listing\Listing;
use App\Model\Origin\Origin;
use App\Model\Potential\Potential;
use App\Model\ProspectType\ProspectType;
use App\Model\Quote\Quote;
use App\Model\QuoteState\QuoteState;
use App\Model\Service\Service;
use App\Model\StructureType\StructureType;
use App\Model\TaskName\TaskName;
use App\Model\TaskType\TaskType as TaskTypeModel;
use App\Model\TVA\TVA;
use App\Model\Unit\Unit;
use App\Model\Vat\Vat;
use App\Model\ViewOrder\ViewOrder;
use App\Model\Warranty\Warranty;
use App\Model\Workforce\Workforce;
use App\Security\User;
use App\Service\Cache\Exception\UnableToSaveKeyException;
use App\V4\Model\Civility\Civility as CivilityV4;
use App\V4\Model\LegalForm\LegalForm as LegalFormV4;
use App\V4\Model\Listing\Listing as ListingV4;
use App\V4\Model\Origin\Origin as OriginV4;
use App\V4\Model\Potential\Potential as PotentialV4;
use App\V4\Model\ProspectType\ProspectType as ProspectTypeV4;
use App\V4\Model\QuoteReason\QuoteReason as QuoteReasonV4;
use App\V4\Model\QuoteState\QuoteState as QuoteStateV4;
use App\V4\Model\Service\Service as ServiceV4;
use App\V4\Model\StructureType\StructureType as StructureTypeV4;
use App\V4\Model\TVA\TVA as TVAV4;
use App\V4\Model\Workforce\Workforce as WorkforceV4;
use DateTime;
use Psr\Cache\CacheException;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\Cache\ItemInterface;
class CacheManager
{
public const CACHE_ACTION_KEY_LIST = 'list';
public const CACHE_ACTION_KEY_LIST_ACTIVE = 'list_active';
public const CACHE_ACTION_KEY_ITEM = 'item';
protected const REDIS_KEY_PATTERN = '%s__%s__%s_%s'; // {env}__customer_{id_customer}__{entity}_{action}
protected const DEFAULT_TTL = 3600 * 10;
protected const DEFAULT_TTL_GRACE = 900;
private const MEMORY_CACHE_LIMIT = 25;
/** @var TokenStorage */
protected $tokenStorage;
/** @var string */
protected $environment;
/** @var TagAwareAdapterInterface */
protected $adapter;
/**
* @var Request
*/
private $requestStack;
/**
* Used to prevent multiple calls to Redis for the same key.
*/
private $memoryCache = [];
public function __construct(
TokenStorage $tokenStorage,
TagAwareAdapterInterface $adapter,
RequestStack $request,
string $environment
) {
$this->tokenStorage = $tokenStorage;
$this->adapter = $adapter;
$this->environment = $environment;
$this->requestStack = $request;
}
/**
* Generates the cache key name from the entity and action.
*/
public function getKeyName(string $entityName, string $action = '', ?string $customerId = null, ?string $id = null): string
{
$cacheKey = sprintf(
self::REDIS_KEY_PATTERN,
$this->environment,
$this->getCustomerPrefix($customerId),
$entityName,
$action
).($id ? '_'.$id : '');
return $this->replaceReservedCharacters($cacheKey);
}
/**
* Fetches a key from the cache.
*
* @throws InvalidArgumentException
*/
public function get(string $entityName, string $action, ?string $id = null)
{
if ($this->requestStack->getCurrentRequest() instanceof Request && $this->requestStack->getCurrentRequest()->query->has('delcache')) {
return null;
}
$key = $this->getKeyName($entityName, $action, null, $id);
if (isset($this->memoryCache[$key])) {
return $this->memoryCache[$key];
}
$cache = $this->adapter->getItem($key);
if ($cache->isHit()) {
$value = $cache->get();
$this->memoryCache[$key] = is_array($value) ? $this->wrapResponse($value, $cache->getMetadata()) : $value;
if (count($this->memoryCache) >= self::MEMORY_CACHE_LIMIT) {
array_shift($this->memoryCache);
}
return $this->memoryCache[$key];
}
return null;
}
/**
* Add isCache and cacheDate fields to the response.
*/
public function wrapResponse(array $data, array $metaData): array
{
$date = $this->getCacheDate($metaData);
return array_merge($data, ['isCache' => true, 'cacheDate' => $date]);
}
public function getCacheDate(array $metaData)
{
$date = null;
if (isset($metaData['tags'])) {
// Search date key
$result = array_filter($metaData['tags'], function ($item) {
if (false !== stripos($item, 'utcdatetime_')) {
return true;
}
return false;
});
if (is_array($result) && count($result) > 0) {
$date = explode('_', array_pop($result));
$date = end($date);
}
}
return $date;
}
/**
* Adds / Overrides a key in the cache.
*
* @throws CacheException
* @throws InvalidArgumentException
* @throws UnableToSaveKeyException
*/
public function set(string $entityName, string $action, $value, ?string $id = null, ?int $expiresInSec = self::DEFAULT_TTL): void
{
$item = $this->adapter->getItem($this->getKeyName($entityName, $action, null, $id));
$tags = [
$this->environment,
$entityName,
$action,
sprintf('%s_%s', $entityName, $action),
];
$item->tag($this->getCustomerPrefix());
$item->tag($this->replaceReservedCharacters($entityName));
foreach ($tags as $tag) {
$item->tag($this->replaceReservedCharacters($tag));
$item->tag($this->replaceReservedCharacters(sprintf('%s_%s', $this->getCustomerPrefix(), $tag)));
// Tag the current date
$item->tag('utcdatetime_'.(new DateTime())->format('Y-m-d-H-i'));
}
$item->set($value);
$item->expiresAfter($expiresInSec ?: random_int(-self::DEFAULT_TTL_GRACE, self::DEFAULT_TTL_GRACE) + self::DEFAULT_TTL);
if (!$this->adapter->save($item)) {
$exception = new UnableToSaveKeyException();
$exception
->setKey($item->getKey())
->setValue($item->get());
throw $exception;
}
}
/**
* Removes a key fron the cache.
*
* @throws InvalidArgumentException
*/
public function invalidate(string $entityName, string $action, ?string $customerId = null, ?string $id = null): void
{
$customerId = $this->getCustomerId($customerId);
$this->adapter->deleteItem($this->getKeyName($entityName, $action, $customerId, $id));
$this->memoryCache = [];
}
/**
* Invalidate all cache using supplied tags.
*
* @throws InvalidArgumentException
*/
public function invalidateTag(array $tags): bool
{
$tags = array_map(function ($value) {
return self::replaceReservedCharacters($value);
}, $tags);
$this->memoryCache = [];
return $this->adapter->invalidateTags($tags);
}
public function invalidateAllKeys(): void
{
$this->memoryCache = [];
$this->adapter->clear();
}
/**
* Replaces all reserved characters with a dot.
*/
public static function replaceReservedCharacters(string $key): string
{
return str_replace(str_split(ItemInterface::RESERVED_CHARACTERS, 1), '.', $key);
}
public function getCustomerPrefix(?string $customerId = null): string
{
if (!$customerId) {
$token = $this->tokenStorage->getToken();
if (null === $token || !$token instanceof TokenInterface) {
throw new UnauthorizedHttpException('', 'Unable to get Token.');
}
$user = $token->getUser();
if (!$user instanceof User) {
throw new UnauthorizedHttpException('', 'Unable to get User.');
}
}
return sprintf('customer_%s', $customerId ?? $user->getCustomerId());
}
/**
* @throws InvalidArgumentException
*/
public function invalidateCustomerIdKeys(?string $customerId): void
{
$customerId = $this->getCustomerId($customerId);
$this->invalidateTag([$this->getCustomerPrefix($customerId)]);
}
/**
* @throws InvalidArgumentException
*/
public function invalidateCustomerIdParamsKeys(?string $customerId): void
{
$customerId = $this->getCustomerId($customerId);
$this->invalidate(StructureType::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(StructureTypeV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Listing::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Civility::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(CivilityV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(LegalForm::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(LegalFormV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidateTag([self::replaceReservedCharacters(Quote::class)]);
$this->invalidate(Origin::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(OriginV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Potential::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(PotentialV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(ProspectType::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(ProspectTypeV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(QuoteState::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(TaskName::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(TaskType::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(TaskTypeModel::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(TVA::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(TVAV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Vat::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Warranty::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Unit::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Service::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(ServiceV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(Workforce::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(WorkforceV4::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(GetParamsRequest::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidate(ViewOrder::class, self::CACHE_ACTION_KEY_LIST, $customerId);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.QuoteStateV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.QuoteReasonV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.ListingV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.Field::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.Vat::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.Warranty::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.Unit::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.StructureTypeV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.CivilityV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.LegalFormV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.OriginV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.PotentialV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.ProspectTypeV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.TVAV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.ServiceV4::class]);
$this->invalidateTag([$this->getCustomerPrefix($customerId).'_'.WorkforceV4::class]);
}
private function getCustomerId(?string $customerId): ?string
{
if (!$customerId) {
$token = $this->tokenStorage->getToken();
if (!$token instanceof TokenInterface) {
throw new UnauthorizedHttpException('', 'Unable to get Token.');
}
$user = $token->getUser();
if (!$user instanceof User) {
throw new UnauthorizedHttpException('', 'Unable to get User.');
}
$customerId = $user->getCustomerId();
}
return $customerId;
}
}