lib/Phpfastcache/Core/Pool/TaggableCacheItemPoolTrait.php
<?php
/**
*
* This file is part of Phpfastcache.
*
* @license MIT License (MIT)
*
* For full copyright and license information, please see the docs/CREDITS.txt and LICENCE files.
*
* @author Georges.L (Geolim4) <contact@geolim4.com>
* @author Contributors https://github.com/PHPSocialNetwork/phpfastcache/graphs/contributors
*/
declare(strict_types=1);
namespace Phpfastcache\Core\Pool;
use DateTime;
use Phpfastcache\Core\Item\ExtendedCacheItemInterface;
use Phpfastcache\Exceptions\PhpfastcacheCoreException;
use Phpfastcache\Exceptions\PhpfastcacheDriverException;
use Phpfastcache\Exceptions\PhpfastcacheInvalidArgumentException;
use Phpfastcache\Exceptions\PhpfastcacheLogicException;
use Psr\Cache\CacheItemInterface;
/**
* Trait TaggableCacheItemPoolTrait
* @package Phpfastcache\Core\Pool
*/
trait TaggableCacheItemPoolTrait
{
use ExtendedCacheItemPoolTrait;
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
*/
public function getItemsByTagsAsJsonString(
array $tagNames,
int $option = \JSON_THROW_ON_ERROR,
int $depth = 512,
int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE
): string {
return \json_encode(
\array_map(
static fn(CacheItemInterface $item) => $item->get(),
\array_values($this->getItemsByTags($tagNames, $strategy))
),
$option,
$depth,
);
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
*/
public function getItemsByTags(array $tagNames, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): array
{
$items = [];
foreach (\array_unique($tagNames) as $tagName) {
$items[] = $this->fetchItemsByTagFromBackend($tagName);
}
$items = \array_merge([], ...$items);
switch ($strategy) {
case TaggableCacheItemPoolInterface::TAG_STRATEGY_ALL:
foreach ($items as $key => $item) {
if (\array_diff($tagNames, $item->getTags())) {
unset($items[$key]);
}
}
break;
case TaggableCacheItemPoolInterface::TAG_STRATEGY_ONLY:
foreach ($items as $key => $item) {
if (\array_diff($tagNames, $item->getTags()) || \array_diff($item->getTags(), $tagNames)) {
unset($items[$key]);
}
}
break;
}
return $items;
}
/**
* @param string $tagName
* @return ExtendedCacheItemInterface[]
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
*/
protected function fetchItemsByTagFromBackend(string $tagName): array
{
$driverResponse = $this->getItem($this->getTagKey($tagName));
if ($driverResponse->isHit()) {
$tagsItems = (array)$driverResponse->get();
/**
* getItems() may provide expired item(s)
* themselves provided by a cache of item
* keys based stored the tag item.
* Therefore, we pass a filter callback
* to remove the expired Item(s) provided by
* the item keys passed through getItems()
*
* #headache
*/
return \array_filter(
$this->getItems(\array_unique(\array_keys($tagsItems))),
static fn (ExtendedCacheItemInterface $item) => $item->isHit(),
);
}
return [];
}
/**
* @param string $key
* @return string
*/
protected function getTagKey(string $key): string
{
return TaggableCacheItemPoolInterface::DRIVER_TAGS_KEY_PREFIX . $key;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws PhpfastcacheDriverException
*/
public function deleteItemsByTags(array $tagNames, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
$return = true;
foreach ($this->getItemsByTags($tagNames, $strategy) as $item) {
$result = $this->deleteItem($item->getKey());
if ($result !== true) {
$return = $result;
}
}
return $return;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws PhpfastcacheDriverException
*/
public function deleteItemsByTag(string $tagName, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
$return = true;
foreach ($this->getItemsByTag($tagName, $strategy) as $item) {
$result = $this->deleteItem($item->getKey());
if ($result !== true) {
$return = $result;
}
}
return $return;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
*/
public function getItemsByTag(string $tagName, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): array
{
$items = $this->fetchItemsByTagFromBackend($tagName);
if ($strategy === TaggableCacheItemPoolInterface::TAG_STRATEGY_ONLY) {
foreach ($items as $key => $item) {
if (\array_diff($item->getTags(), [$tagName])) {
unset($items[$key]);
}
}
}
return $items;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function incrementItemsByTags(array $tagNames, int $step = 1, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
$return = true;
foreach ($tagNames as $tagName) {
$result = $this->incrementItemsByTag($tagName, $step, $strategy);
if ($result !== true) {
$return = $result;
}
}
return $return;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function incrementItemsByTag(string $tagName, int $step = 1, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
foreach ($this->getItemsByTag($tagName, $strategy) as $item) {
$item->increment($step);
$this->saveDeferred($item);
}
return $this->commit();
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function decrementItemsByTags(array $tagNames, int $step = 1, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
$return = true;
foreach ($tagNames as $tagName) {
$result = $this->decrementItemsByTag($tagName, $step, $strategy);
if ($result !== true) {
$return = $result;
}
}
return $return;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function decrementItemsByTag(string $tagName, int $step = 1, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
foreach ($this->getItemsByTag($tagName, $strategy) as $item) {
$item->decrement($step);
$this->saveDeferred($item);
}
return $this->commit();
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function appendItemsByTags(array $tagNames, array|string $data, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
$return = true;
foreach ($tagNames as $tagName) {
$result = $this->appendItemsByTag($tagName, $data, $strategy);
if ($result !== true) {
$return = $result;
}
}
return $return;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function appendItemsByTag(string $tagName, array|string $data, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
foreach ($this->getItemsByTag($tagName, $strategy) as $item) {
$item->append($data);
$this->saveDeferred($item);
}
return $this->commit();
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function prependItemsByTags(array $tagNames, array|string $data, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
$return = true;
foreach ($tagNames as $tagName) {
$result = $this->prependItemsByTag($tagName, $data, $strategy);
if ($result !== true) {
$return = $result;
}
}
return $return;
}
/**
* @inheritDoc
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws \ReflectionException
*/
public function prependItemsByTag(string $tagName, array|string $data, int $strategy = TaggableCacheItemPoolInterface::TAG_STRATEGY_ONE): bool
{
foreach ($this->getItemsByTag($tagName, $strategy) as $item) {
$item->prepend($data);
$this->saveDeferred($item);
}
return $this->commit();
}
/**
* @param array<mixed> $wrapper
* @return string[]
*/
protected function driverUnwrapTags(array $wrapper): array
{
return $wrapper[TaggableCacheItemPoolInterface::DRIVER_TAGS_WRAPPER_INDEX];
}
/**
* @param ExtendedCacheItemInterface $item
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheDriverException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
*/
protected function cleanItemTags(ExtendedCacheItemInterface $item): void
{
if (!empty($item->getTags()) || !empty($item->getRemovedTags())) {
$this->driverWriteTags($item->removeTags($item->getTags()));
}
}
/**
* @param ExtendedCacheItemInterface $item
* @return bool
* @throws PhpfastcacheCoreException
* @throws PhpfastcacheInvalidArgumentException
* @throws PhpfastcacheLogicException
* @throws PhpfastcacheDriverException
*/
protected function driverWriteTags(ExtendedCacheItemInterface $item): bool
{
/**
* Do not attempt to write tags
* on tags item, it can lead
* to an infinite recursive calls
*/
if (str_starts_with($item->getKey(), TaggableCacheItemPoolInterface::DRIVER_TAGS_KEY_PREFIX)) {
throw new PhpfastcacheLogicException('Trying to set tag(s) to an Tag item index: ' . $item->getKey());
}
if (!$item->getTags() && !$item->getRemovedTags()) {
return true;
}
/**
* @var $tagsItems ExtendedCacheItemInterface[]
*/
$tagsItems = $this->getItems($this->getTagKeys($item->getTags()));
foreach ($tagsItems as $tagsItem) {
$data = $tagsItem->get();
$expTimestamp = $item->getExpirationDate()->getTimestamp();
/**
* Using the key will
* avoid to use array_unique
* that has slow performances
*/
$data = \array_merge((array)$data, [$item->getKey() => $expTimestamp]);
$tagsItem->set($data);
/**
* Recalculate the expiration date
*
* If the $tagsItem does not have
* any cache item references left
* then remove it from tagsItems index
*/
$tagsItem->expiresAt((new DateTime())->setTimestamp(max($data)));
$this->driverWrite($tagsItem);
$tagsItem->setHit(true);
}
/**
* Also update removed tags to
* keep the index up to date
*/
$tagsItems = $this->getItems($this->getTagKeys($item->getRemovedTags()));
foreach ($tagsItems as $tagsItem) {
$data = (array)$tagsItem->get();
unset($data[$item->getKey()]);
$tagsItem->set($data);
/**
* Recalculate the expiration date
*
* If the $tagsItem does not have
* any cache item references left
* then remove it from tagsItems index
*/
if (\count($data)) {
$tagsItem->expiresAt((new DateTime())->setTimestamp(max($data)));
$this->driverWrite($tagsItem);
$tagsItem->setHit(true);
} else {
$this->deleteItem($tagsItem->getKey());
}
}
return true;
}
/**
* @param array<string> $keys
* @return array<string>
*/
protected function getTagKeys(array $keys): array
{
return \array_map(
fn (string $key) => $this->getTagKey($key),
$keys
);
}
}