get content by identifier) * - ident_xxx * xxx is identifier, value is array of associated tags. This is "reverse" tag * index. It provides quick access for all tags associated with this identifier * and used when removing the identifier * * Each key is prepended with a prefix. The prefix makes sure keys from the different * installations do not conflict. By default, prefix consists from two parts * separated by underscore character and ends in yet another underscore character: * - "TYPO3" * - Hash of path to TYPO3 and user running TYPO3 */ final class ApcuBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface { /** * A prefix to separate stored data from other data possible stored in the APC. */ private string $identifierPrefix = ''; /** * Constructs this backend * * @param array $options Configuration options - unused here */ public function __construct(array $options = []) { if (!extension_loaded('apcu')) { throw new Exception('The PHP extension "apcu" must be installed and loaded in order to use the APCu backend.', 1232985914); } if (PHP_SAPI === 'cli' && ini_get('apc.enable_cli') == 0) { throw new Exception('The APCu backend cannot be used because apcu is disabled on CLI.', 1232985915); } parent::__construct($options); } public function setCache(FrontendInterface $cache): void { parent::setCache($cache); $this->identifierPrefix = 'TYPO3_' . hash('xxh3', Environment::getProjectPath() . $cache->getIdentifier()) . '_'; } /** * @param mixed $data The data to be stored. mixed is allowed due to TransientBackendInterface */ public function set(string $entryIdentifier, mixed $data, array $tags = [], ?int $lifetime = null): void { $lifetime ??= $this->defaultLifetime; $success = apcu_store($this->identifierPrefix . $entryIdentifier, $data, $lifetime); if ($success === true) { $this->removeIdentifierFromAllTags($entryIdentifier); $this->addIdentifierToTags($entryIdentifier, $tags); } else { $this->logger->alert('Error using APCu: Could not save data in the cache.'); } } /** * Loads data from the cache. * * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded */ public function get(string $entryIdentifier): mixed { $success = false; $value = apcu_fetch($this->identifierPrefix . $entryIdentifier, $success); return $success ? $value : $success; } public function has(string $entryIdentifier): bool { $success = false; apcu_fetch($this->identifierPrefix . $entryIdentifier, $success); return $success; } /** * Removes all cache entries matching the specified identifier. * Usually this only affects one entry but if - for what reason ever - * old entries for the identifier still exist, they are removed as well. * * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found */ public function remove(string $entryIdentifier): bool { $this->removeIdentifierFromAllTags($entryIdentifier); return apcu_delete($this->identifierPrefix . $entryIdentifier); } public function findIdentifiersByTag(string $tag): array { $success = false; $identifiers = apcu_fetch($this->identifierPrefix . 'tag_' . $tag, $success); if ($success === false) { return []; } return (array)$identifiers; } public function flush(): void { apcu_delete(new \APCUIterator('/^' . preg_quote($this->identifierPrefix, '/') . '/')); } public function flushByTag(string $tag): void { $identifiers = $this->findIdentifiersByTag($tag); foreach ($identifiers as $identifier) { $this->remove($identifier); } } public function flushByTags(array $tags): void { array_walk($tags, $this->flushByTag(...)); } public function collectGarbage(): void { // Noop, APCu has internal GC } private function addIdentifierToTags(string $entryIdentifier, array $tags): void { // Get identifier-to-tag index to look for updates $existingTags = $this->findTagsByIdentifier($entryIdentifier); $existingTagsUpdated = false; foreach ($tags as $tag) { // Update tag-to-identifier index $identifiers = $this->findIdentifiersByTag($tag); if (!in_array($entryIdentifier, $identifiers, true)) { $identifiers[] = $entryIdentifier; apcu_store($this->identifierPrefix . 'tag_' . $tag, $identifiers); } // Test if identifier-to-tag index needs update if (!in_array($tag, $existingTags, true)) { $existingTags[] = $tag; $existingTagsUpdated = true; } } // Update identifier-to-tag index if needed if ($existingTagsUpdated) { apcu_store($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags); } } private function removeIdentifierFromAllTags(string $entryIdentifier): void { // Get tags for this identifier $tags = $this->findTagsByIdentifier($entryIdentifier); // De-associate tags with this identifier foreach ($tags as $tag) { $identifiers = $this->findIdentifiersByTag($tag); // Formally array_search() below should never return FALSE due to // the behavior of findTagsByIdentifier(). But if reverse index is // corrupted, we still can get 'FALSE' from array_search(). This is // not a problem because we are removing this identifier from // anywhere. if (($key = array_search($entryIdentifier, $identifiers)) !== false) { unset($identifiers[$key]); if (!empty($identifiers)) { apcu_store($this->identifierPrefix . 'tag_' . $tag, $identifiers); } else { apcu_delete($this->identifierPrefix . 'tag_' . $tag); } } } // Clear reverse tag index for this identifier apcu_delete($this->identifierPrefix . 'ident_' . $entryIdentifier); } private function findTagsByIdentifier(string $identifier): array { $success = false; $tags = apcu_fetch($this->identifierPrefix . 'ident_' . $identifier, $success); return $success ? (array)$tags : []; } }