├── Classes ├── Backend │ ├── AbstractBackend.php │ ├── ApcuBackend.php │ ├── BackendInterface.php │ ├── FileBackend.php │ ├── FileBackendEntryDto.php │ ├── FreezableBackendInterface.php │ ├── IterableBackendInterface.php │ ├── IterableMultiBackend.php │ ├── MemcachedBackend.php │ ├── MultiBackend.php │ ├── NullBackend.php │ ├── PdoBackend.php │ ├── PhpCapableBackendInterface.php │ ├── RedisBackend.php │ ├── RequireOnceFromValueTrait.php │ ├── SimpleFileBackend.php │ ├── TaggableBackendInterface.php │ ├── TaggableMultiBackend.php │ ├── TransientMemoryBackend.php │ ├── WithSetupInterface.php │ └── WithStatusInterface.php ├── BackendInstantiationTrait.php ├── CacheAwareInterface.php ├── CacheFactory.php ├── CacheFactoryInterface.php ├── EnvironmentConfiguration.php ├── Exception.php ├── Exception │ ├── ClassAlreadyLoadedException.php │ ├── DuplicateIdentifierException.php │ ├── InvalidBackendException.php │ ├── InvalidCacheException.php │ ├── InvalidDataException.php │ ├── NoSuchCacheException.php │ └── NotSupportedByBackendException.php ├── Frontend │ ├── AbstractFrontend.php │ ├── CacheEntryIterator.php │ ├── FrontendInterface.php │ ├── PhpFrontend.php │ ├── StringFrontend.php │ └── VariableFrontend.php └── Psr │ ├── Cache │ ├── CacheFactory.php │ ├── CacheItem.php │ └── CachePool.php │ ├── InvalidArgumentException.php │ └── SimpleCache │ ├── SimpleCache.php │ └── SimpleCacheFactory.php ├── LICENSE.txt ├── Readme.md ├── Resources └── Private │ ├── DDL.sql │ ├── mysql.DDL.sql │ └── pgsql.DDL.sql ├── Tests ├── BaseTestCase.php ├── Functional │ └── Backend │ │ ├── PdoBackendTest.php │ │ └── RedisBackendTest.php ├── PhpUnitBootstrap.php └── Unit │ ├── Backend │ ├── AbstractBackendTest.php │ ├── ApcuBackendTest.php │ ├── FileBackendEntryDtoTest.php │ ├── FileBackendTest.php │ ├── IterableMultiBackendTest.php │ ├── MemcacheBackendTest.php │ ├── MemcachedBackendTest.php │ ├── MultiBackendTest.php │ ├── PdoBackendTest.php │ ├── RedisBackendTest.php │ ├── SimpleFileBackendTest.php │ ├── TaggableMultiBackendTest.php │ └── TransientMemoryBackendTest.php │ ├── Frontend │ ├── AbstractFrontendTest.php │ ├── PhpFrontendTest.php │ ├── StringFrontendTest.php │ └── VariableFrontendTest.php │ └── Psr │ ├── Cache │ └── CachePoolTest.php │ └── SimpleCache │ ├── SimpleCacheFactoryTest.php │ └── SimpleCacheTest.php ├── composer.json └── phpunit.xml.dist /Classes/Backend/AbstractBackend.php: -------------------------------------------------------------------------------- 1 | environmentConfiguration = $environmentConfiguration; 67 | 68 | if (is_array($options) || $options instanceof \Iterator) { 69 | $this->setProperties($options); 70 | } 71 | } 72 | 73 | /** 74 | * @param array $properties 75 | * @param boolean $throwExceptionIfPropertyNotSettable 76 | * @return void 77 | * @throws \InvalidArgumentException 78 | */ 79 | protected function setProperties(array $properties, bool $throwExceptionIfPropertyNotSettable = true): void 80 | { 81 | foreach ($properties as $propertyName => $propertyValue) { 82 | $propertyWasSet = $this->setProperty($propertyName, $propertyValue); 83 | if ($propertyWasSet) { 84 | continue; 85 | } 86 | 87 | if ($throwExceptionIfPropertyNotSettable) { 88 | throw new \InvalidArgumentException('Invalid cache backend option "' . $propertyName . '" for backend of type "' . get_class($this) . '"', 1231267498); 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * @param string $propertyName 95 | * @param mixed $propertyValue 96 | * @return boolean 97 | */ 98 | protected function setProperty(string $propertyName, $propertyValue): bool 99 | { 100 | $setterName = 'set' . ucfirst($propertyName); 101 | if (method_exists($this, $setterName)) { 102 | $this->$setterName($propertyValue); 103 | return true; 104 | } 105 | 106 | return false; 107 | } 108 | 109 | /** 110 | * Sets a reference to the cache frontend which uses this backend 111 | * 112 | * @param FrontendInterface $cache The frontend for this backend 113 | * @return void 114 | * @api 115 | */ 116 | public function setCache(FrontendInterface $cache): void 117 | { 118 | $this->cache = $cache; 119 | $this->cacheIdentifier = $this->cache->getIdentifier(); 120 | $applicationIdentifier = $this->environmentConfiguration instanceof EnvironmentConfiguration ? $this->environmentConfiguration->getApplicationIdentifier() : ''; 121 | $this->identifierPrefix = md5($applicationIdentifier) . ':' . $this->cacheIdentifier . ':'; 122 | } 123 | 124 | /** 125 | * Sets the default lifetime for this cache backend 126 | * 127 | * @param integer|string $defaultLifetime Default lifetime of this cache backend in seconds. 0 means unlimited lifetime. 128 | * @return void 129 | * @throws \InvalidArgumentException 130 | * @api 131 | */ 132 | public function setDefaultLifetime($defaultLifetime): void 133 | { 134 | if ((int)$defaultLifetime < 0) { 135 | throw new \InvalidArgumentException('The default lifetime must be given as a positive integer', 1233072774); 136 | } 137 | $this->defaultLifetime = (int)$defaultLifetime; 138 | } 139 | 140 | /** 141 | * Calculates the expiry time by the given lifetime. If no lifetime is 142 | * specified, the default lifetime is used. 143 | * 144 | * @param integer $lifetime The lifetime in seconds 145 | * @return \DateTime The expiry time 146 | */ 147 | protected function calculateExpiryTime(?int $lifetime = null): \DateTime 148 | { 149 | if ($lifetime === self::UNLIMITED_LIFETIME || ($lifetime === null && $this->defaultLifetime === self::UNLIMITED_LIFETIME)) { 150 | return new \DateTime(self::DATETIME_EXPIRYTIME_UNLIMITED, new \DateTimeZone('UTC')); 151 | } 152 | 153 | if ($lifetime === null) { 154 | $lifetime = $this->defaultLifetime; 155 | } 156 | return new \DateTime('now +' . $lifetime . ' seconds', new \DateTimeZone('UTC')); 157 | } 158 | 159 | /** 160 | * Returns the internally used, prefixed entry identifier for the given public 161 | * entry identifier. 162 | * 163 | * While Flow applications will mostly refer to the simple entry identifier, it 164 | * may be necessary to know the actual identifier used by the cache backend 165 | * in order to share cache entries with other applications. This method allows 166 | * for retrieving it. 167 | * 168 | * Note that, in case of the AbstractBackend, this method is returns just the 169 | * given entry identifier. 170 | * 171 | * @param string $entryIdentifier The short entry identifier, for example "NumberOfPostedArticles" 172 | * @return string The prefixed identifier, for example "d59b7012de96aecf8171f8760323fe0a:Flow_Fusion_Content:NumberOfPostedArticles:" 173 | * @api 174 | */ 175 | public function getPrefixedIdentifier(string $entryIdentifier): string 176 | { 177 | return $this->identifierPrefix . $entryIdentifier; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Classes/Backend/BackendInterface.php: -------------------------------------------------------------------------------- 1 | data; 32 | } 33 | 34 | /** 35 | * @return string[] 36 | */ 37 | public function getTags(): array 38 | { 39 | return $this->tags; 40 | } 41 | 42 | public function getExpiryTime(): int 43 | { 44 | return $this->expiryTime; 45 | } 46 | 47 | public function isExpired(): bool 48 | { 49 | return ($this->getExpiryTime() !== 0 && $this->getExpiryTime() < $_SERVER['REQUEST_TIME']); 50 | } 51 | 52 | public function isTaggedWith(string $tag): bool 53 | { 54 | return !empty($this->tags) && in_array($tag, $this->tags, true); 55 | } 56 | 57 | public function __toString(): string 58 | { 59 | $metaData = implode(' ', $this->tags) . str_pad((string)$this->expiryTime, static::EXPIRYTIME_LENGTH) . str_pad((string)strlen($this->data), static::DATASIZE_DIGITS); 60 | return $this->data . $metaData; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Classes/Backend/FreezableBackendInterface.php: -------------------------------------------------------------------------------- 1 | logger?->error(($this->throwableStorage ? $this->throwableStorage->logThrowable($throwable) : $message), LogEnvironment::fromMethodName(__METHOD__)); 34 | $this->handleError($throwable); 35 | return null; 36 | } 37 | return parent::buildSubBackend($backendClassName, $backendOptions); 38 | } 39 | 40 | /** 41 | * @throws Throwable 42 | */ 43 | public function current(): mixed 44 | { 45 | $this->prepareBackends(); 46 | foreach ($this->backends as $backend) { 47 | try { 48 | return $backend->current(); 49 | } catch (Throwable $throwable) { 50 | $this->logger?->error('Failed retrieving current cache entry using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 51 | $this->handleError($throwable); 52 | $this->removeUnhealthyBackend($backend); 53 | } 54 | } 55 | return false; 56 | } 57 | 58 | /** 59 | * @throws Throwable 60 | */ 61 | public function next(): void 62 | { 63 | $this->prepareBackends(); 64 | foreach ($this->backends as $backend) { 65 | try { 66 | $backend->next(); 67 | } catch (Throwable $throwable) { 68 | $this->logger?->error('Failed retrieving next cache entry using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 69 | $this->handleError($throwable); 70 | $this->removeUnhealthyBackend($backend); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * @throws Throwable 77 | */ 78 | public function key(): string|int|bool|null|float 79 | { 80 | $this->prepareBackends(); 81 | foreach ($this->backends as $backend) { 82 | try { 83 | return $backend->key(); 84 | } catch (Throwable $throwable) { 85 | $this->logger?->error('Failed retrieving cache entry key using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 86 | $this->handleError($throwable); 87 | $this->removeUnhealthyBackend($backend); 88 | } 89 | } 90 | return false; 91 | } 92 | 93 | /** 94 | * @throws Throwable 95 | */ 96 | public function valid(): bool 97 | { 98 | $this->prepareBackends(); 99 | foreach ($this->backends as $backend) { 100 | try { 101 | return $backend->valid(); 102 | } catch (Throwable $throwable) { 103 | $this->logger?->error('Failed checking if current cache entry is valid using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 104 | $this->handleError($throwable); 105 | $this->removeUnhealthyBackend($backend); 106 | } 107 | } 108 | return false; 109 | } 110 | 111 | /** 112 | * @throws Throwable 113 | */ 114 | public function rewind(): void 115 | { 116 | $this->prepareBackends(); 117 | foreach ($this->backends as $backend) { 118 | try { 119 | $backend->rewind(); 120 | } catch (Throwable $throwable) { 121 | $this->logger?->error('Failed rewinding cache entries using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 122 | $this->handleError($throwable); 123 | $this->removeUnhealthyBackend($backend); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Classes/Backend/MultiBackend.php: -------------------------------------------------------------------------------- 1 | logErrors && class_exists(Bootstrap::class) && Bootstrap::$staticObjectManager instanceof ObjectManagerInterface) { 49 | try { 50 | $logger = Bootstrap::$staticObjectManager->get(LoggerInterface::class); 51 | assert($logger instanceof LoggerInterface); 52 | $this->logger = $logger; 53 | $throwableStorage = Bootstrap::$staticObjectManager->get(ThrowableStorageInterface::class); 54 | assert($throwableStorage instanceof ThrowableStorageInterface); 55 | $this->throwableStorage = $throwableStorage; 56 | } catch (UnknownObjectException) { 57 | // Logging might not be available during compile time 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * @throws Throwable 64 | */ 65 | protected function prepareBackends(): void 66 | { 67 | if ($this->initialized === true) { 68 | return; 69 | } 70 | foreach ($this->backendConfigurations as $backendConfiguration) { 71 | $backendOptions = $backendConfiguration['backendOptions'] ?? []; 72 | $backend = $this->buildSubBackend($backendConfiguration['backend'], $backendOptions); 73 | if ($backend !== null) { 74 | $this->backends[] = $backend; 75 | } 76 | } 77 | $this->initialized = true; 78 | } 79 | 80 | /** 81 | * @throws Throwable 82 | */ 83 | protected function buildSubBackend(string $backendClassName, array $backendOptions): ?BackendInterface 84 | { 85 | try { 86 | $backend = $this->instantiateBackend($backendClassName, $backendOptions, $this->environmentConfiguration); 87 | $backend->setCache($this->cache); 88 | } catch (Throwable $throwable) { 89 | $this->logger?->error('Failed creating sub backend ' . $backendClassName . ' for ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 90 | $this->handleError($throwable); 91 | $backend = null; 92 | } 93 | return $backend; 94 | } 95 | 96 | /** 97 | * @throws Throwable 98 | */ 99 | public function set(string $entryIdentifier, string $data, array $tags = [], ?int $lifetime = null): void 100 | { 101 | $this->prepareBackends(); 102 | foreach ($this->backends as $backend) { 103 | try { 104 | $backend->set($entryIdentifier, $data, $tags, $lifetime); 105 | } catch (Throwable $throwable) { 106 | $this->logger?->error('Failed setting cache entry using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 107 | $this->handleError($throwable); 108 | $this->removeUnhealthyBackend($backend); 109 | if (!$this->setInAllBackends) { 110 | return; 111 | } 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * @throws Throwable 118 | */ 119 | public function get(string $entryIdentifier) 120 | { 121 | $this->prepareBackends(); 122 | foreach ($this->backends as $backend) { 123 | try { 124 | return $backend->get($entryIdentifier); 125 | } catch (Throwable $throwable) { 126 | $this->logger?->error('Failed retrieving cache entry using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 127 | $this->handleError($throwable); 128 | $this->removeUnhealthyBackend($backend); 129 | } 130 | } 131 | return false; 132 | } 133 | 134 | /** 135 | * @throws Throwable 136 | */ 137 | public function has(string $entryIdentifier): bool 138 | { 139 | $this->prepareBackends(); 140 | foreach ($this->backends as $backend) { 141 | try { 142 | return $backend->has($entryIdentifier); 143 | } catch (Throwable $throwable) { 144 | $this->logger?->error('Failed checking if cache entry exists using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 145 | $this->handleError($throwable); 146 | $this->removeUnhealthyBackend($backend); 147 | } 148 | } 149 | return false; 150 | } 151 | 152 | /** 153 | * @throws Throwable 154 | */ 155 | public function remove(string $entryIdentifier): bool 156 | { 157 | $this->prepareBackends(); 158 | $result = false; 159 | foreach ($this->backends as $backend) { 160 | try { 161 | $result = $result || $backend->remove($entryIdentifier); 162 | } catch (Throwable $throwable) { 163 | $this->logger?->error('Failed removing cache entry using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 164 | $this->handleError($throwable); 165 | $this->removeUnhealthyBackend($backend); 166 | } 167 | } 168 | return $result; 169 | } 170 | 171 | /** 172 | * @throws Throwable 173 | */ 174 | public function flush(): void 175 | { 176 | $this->prepareBackends(); 177 | foreach ($this->backends as $backend) { 178 | try { 179 | $backend->flush(); 180 | } catch (Throwable $throwable) { 181 | $this->logger?->error('Failed flushing cache using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 182 | $this->handleError($throwable); 183 | $this->removeUnhealthyBackend($backend); 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * @throws Throwable 190 | */ 191 | public function collectGarbage(): void 192 | { 193 | $this->prepareBackends(); 194 | foreach ($this->backends as $backend) { 195 | try { 196 | $backend->collectGarbage(); 197 | } catch (Throwable $throwable) { 198 | $this->logger?->error('Failed collecting garbage using cache backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 199 | $this->handleError($throwable); 200 | $this->removeUnhealthyBackend($backend); 201 | } 202 | } 203 | } 204 | 205 | /** 206 | * This setter is used by AbstractBackend::setProperties() 207 | */ 208 | protected function setBackendConfigurations(array $backendConfigurations): void 209 | { 210 | $this->backendConfigurations = $backendConfigurations; 211 | } 212 | 213 | /** 214 | * This setter is used by AbstractBackend::setProperties() 215 | */ 216 | protected function setSetInAllBackends(bool $setInAllBackends): void 217 | { 218 | $this->setInAllBackends = $setInAllBackends; 219 | } 220 | 221 | /** 222 | * This setter is used by AbstractBackend::setProperties() 223 | */ 224 | protected function setDebug(bool $debug): void 225 | { 226 | $this->debug = $debug; 227 | } 228 | 229 | /** 230 | * This setter is used by AbstractBackend::setProperties() 231 | */ 232 | public function setRemoveUnhealthyBackends(bool $removeUnhealthyBackends): void 233 | { 234 | $this->removeUnhealthyBackends = $removeUnhealthyBackends; 235 | } 236 | 237 | /** 238 | * This setter is used by AbstractBackend::setProperties() 239 | */ 240 | public function setLogErrors(bool $logErrors): void 241 | { 242 | $this->logErrors = $logErrors; 243 | } 244 | 245 | /** 246 | * @throws Throwable 247 | */ 248 | protected function handleError(Throwable $throwable): void 249 | { 250 | if ($this->debug) { 251 | throw $throwable; 252 | } 253 | } 254 | 255 | protected function removeUnhealthyBackend(BackendInterface $unhealthyBackend): void 256 | { 257 | if ($this->removeUnhealthyBackends === false || count($this->backends) <= 1) { 258 | return; 259 | } 260 | $i = array_search($unhealthyBackend, $this->backends, true); 261 | if ($i !== false) { 262 | unset($this->backends[$i]); 263 | $this->logger?->warning(sprintf('Removing unhealthy cache backend %s from backends used by %s', get_class($unhealthyBackend), get_class($this)), LogEnvironment::fromMethodName(__METHOD__)); 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /Classes/Backend/NullBackend.php: -------------------------------------------------------------------------------- 1 | $tags ignored 127 | * @return integer 128 | * @api 129 | */ 130 | public function flushByTags(array $tags): int 131 | { 132 | return 0; 133 | } 134 | 135 | /** 136 | * Does nothing 137 | * 138 | * @return void 139 | * @api 140 | */ 141 | public function collectGarbage(): void 142 | { 143 | } 144 | 145 | /** 146 | * Does nothing 147 | * 148 | * @param string $identifier An identifier which describes the cache entry to load 149 | * @return void 150 | * @api 151 | */ 152 | public function requireOnce(string $identifier) 153 | { 154 | } 155 | } 156 | // @codeCoverageIgnoreEnd 157 | -------------------------------------------------------------------------------- /Classes/Backend/PhpCapableBackendInterface.php: -------------------------------------------------------------------------------- 1 | get($entryIdentifier)); 36 | if ($value === '') { 37 | return false; 38 | } 39 | if (isset($this->_requiredEntryIdentifiers[$entryIdentifier])) { 40 | return false; 41 | } 42 | $this->_requiredEntryIdentifiers[$entryIdentifier] = true; 43 | return include('data:text/plain;base64,' . base64_encode($value)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Classes/Backend/TaggableBackendInterface.php: -------------------------------------------------------------------------------- 1 | $tags The tags the entries must have 36 | * @return integer The number of entries which have been affected by this flush 37 | * @api 38 | */ 39 | public function flushByTags(array $tags): int; 40 | 41 | /** 42 | * Finds and returns all cache entry identifiers which are tagged by the 43 | * specified tag. 44 | * 45 | * @param string $tag The tag to search for 46 | * @return string[] An array with identifiers of all matching entries. An empty array if no entries matched 47 | * @api 48 | */ 49 | public function findIdentifiersByTag(string $tag): array; 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Backend/TaggableMultiBackend.php: -------------------------------------------------------------------------------- 1 | logger?->error(($this->throwableStorage ? $this->throwableStorage->logThrowable($throwable) : $message), LogEnvironment::fromMethodName(__METHOD__)); 34 | $this->handleError($throwable); 35 | return null; 36 | } 37 | return parent::buildSubBackend($backendClassName, $backendOptions); 38 | } 39 | 40 | /** 41 | * @throws Throwable 42 | */ 43 | public function flushByTag(string $tag): int 44 | { 45 | $this->prepareBackends(); 46 | $flushed = 0; 47 | foreach ($this->backends as $backend) { 48 | try { 49 | $flushed += $backend->flushByTag($tag); 50 | } catch (Throwable $throwable) { 51 | $this->logger?->error('Failed flushing cache by tag using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 52 | $this->handleError($throwable); 53 | $this->removeUnhealthyBackend($backend); 54 | } 55 | } 56 | return $flushed; 57 | } 58 | 59 | /** 60 | * @param array $tags The tags the entries must have 61 | * @return integer The number of entries which have been affected by this flush 62 | * @throws Throwable 63 | * @psalm-suppress MethodSignatureMismatch 64 | */ 65 | public function flushByTags(array $tags): int 66 | { 67 | $flushed = 0; 68 | foreach ($tags as $tag) { 69 | $flushed += $this->flushByTag($tag); 70 | } 71 | return $flushed; 72 | } 73 | 74 | /** 75 | * @return string[] 76 | * @throws Throwable 77 | */ 78 | public function findIdentifiersByTag(string $tag): array 79 | { 80 | $this->prepareBackends(); 81 | $identifiers = []; 82 | foreach ($this->backends as $backend) { 83 | try { 84 | $identifiers[] = $backend->findIdentifiersByTag($tag); 85 | } catch (Throwable $throwable) { 86 | $this->logger?->error('Failed finding identifiers by tag using backend ' . get_class($backend) . ' in ' . get_class($this) . ': ' . $this->throwableStorage?->logThrowable($throwable), LogEnvironment::fromMethodName(__METHOD__)); 87 | $this->handleError($throwable); 88 | $this->removeUnhealthyBackend($backend); 89 | } 90 | } 91 | // avoid array_merge in the loop, this trades memory for speed 92 | return array_values(array_unique(array_merge(...$identifiers))); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Classes/Backend/TransientMemoryBackend.php: -------------------------------------------------------------------------------- 1 | cache instanceof FrontendInterface) { 51 | throw new Exception('No cache frontend has been set yet via setCache().', 1238244992); 52 | } 53 | 54 | 55 | $this->entries[$entryIdentifier] = $data; 56 | foreach ($tags as $tag) { 57 | $this->tagsAndEntries[$tag][$entryIdentifier] = true; 58 | } 59 | } 60 | 61 | /** 62 | * Loads data from the cache. 63 | * 64 | * @param string $entryIdentifier An identifier which describes the cache entry to load 65 | * @return mixed The cache entry's content as a string or false if the cache entry could not be loaded 66 | * @api 67 | */ 68 | public function get(string $entryIdentifier) 69 | { 70 | return $this->entries[$entryIdentifier] ?? false; 71 | } 72 | 73 | /** 74 | * Checks if a cache entry with the specified identifier exists. 75 | * 76 | * @param string $entryIdentifier An identifier specifying the cache entry 77 | * @return boolean true if such an entry exists, false if not 78 | * @api 79 | */ 80 | public function has(string $entryIdentifier): bool 81 | { 82 | return isset($this->entries[$entryIdentifier]); 83 | } 84 | 85 | /** 86 | * Removes all cache entries matching the specified identifier. 87 | * 88 | * @param string $entryIdentifier Specifies the cache entry to remove 89 | * @return boolean true if the entry could be removed or false if no entry was found 90 | * @api 91 | */ 92 | public function remove(string $entryIdentifier): bool 93 | { 94 | if (!isset($this->entries[$entryIdentifier])) { 95 | return false; 96 | } 97 | unset($this->entries[$entryIdentifier]); 98 | foreach (array_keys($this->tagsAndEntries) as $tag) { 99 | if (isset($this->tagsAndEntries[$tag][$entryIdentifier])) { 100 | unset($this->tagsAndEntries[$tag][$entryIdentifier]); 101 | } 102 | } 103 | return true; 104 | } 105 | 106 | /** 107 | * Finds and returns all cache entry identifiers which are tagged by the 108 | * specified tag. 109 | * 110 | * @param string $tag The tag to search for 111 | * @return string[] An array with identifiers of all matching entries. An empty array if no entries matched 112 | * @api 113 | */ 114 | public function findIdentifiersByTag(string $tag): array 115 | { 116 | if (isset($this->tagsAndEntries[$tag])) { 117 | return array_map( 118 | static function ($value) { 119 | return (string)$value; 120 | }, 121 | array_keys($this->tagsAndEntries[$tag]) 122 | ); 123 | } 124 | return []; 125 | } 126 | 127 | /** 128 | * Removes all cache entries of this cache. 129 | * 130 | * @return void 131 | * @api 132 | */ 133 | public function flush(): void 134 | { 135 | $this->entries = []; 136 | $this->tagsAndEntries = []; 137 | } 138 | 139 | /** 140 | * Removes all cache entries of this cache which are tagged by the specified tag. 141 | * 142 | * @param string $tag The tag the entries must have 143 | * @return integer The number of entries which have been affected by this flush 144 | * @api 145 | */ 146 | public function flushByTag(string $tag): int 147 | { 148 | $identifiers = $this->findIdentifiersByTag($tag); 149 | foreach ($identifiers as $identifier) { 150 | $this->remove($identifier); 151 | } 152 | return count($identifiers); 153 | } 154 | 155 | /** 156 | * Removes all cache entries of this cache which are tagged by any of the specified tags. 157 | * 158 | * @api 159 | */ 160 | public function flushByTags(array $tags): int 161 | { 162 | $flushed = 0; 163 | foreach ($tags as $tag) { 164 | $flushed += $this->flushByTag($tag); 165 | } 166 | return $flushed; 167 | } 168 | 169 | /** 170 | * Does nothing 171 | * 172 | * @return void 173 | * @api 174 | */ 175 | public function collectGarbage(): void 176 | { 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Classes/Backend/WithSetupInterface.php: -------------------------------------------------------------------------------- 1 | environmentConfiguration = $environmentConfiguration; 43 | } 44 | 45 | /** 46 | * Factory method which creates the specified cache along with the specified kind of backend. 47 | * After creating the cache, it will be registered at the cache manager. 48 | * 49 | * @param string $cacheIdentifier The name / identifier of the cache to create 50 | * @param string $cacheObjectName Object name of the cache frontend 51 | * @param string $backendObjectName Object name of the cache backend 52 | * @param array $backendOptions (optional) Array of backend options 53 | * @return FrontendInterface The created cache frontend 54 | * @throws InvalidBackendException 55 | * @throws InvalidCacheException 56 | * @api 57 | */ 58 | public function create(string $cacheIdentifier, string $cacheObjectName, string $backendObjectName, array $backendOptions = []): FrontendInterface 59 | { 60 | $backend = $this->instantiateBackend($backendObjectName, $backendOptions, $this->environmentConfiguration); 61 | $cache = $this->instantiateCache($cacheIdentifier, $cacheObjectName, $backend); 62 | $backend->setCache($cache); 63 | 64 | return $cache; 65 | } 66 | 67 | /** 68 | * @param string $cacheIdentifier 69 | * @param string $cacheObjectName 70 | * @param BackendInterface $backend 71 | * @return FrontendInterface 72 | * @throws InvalidCacheException 73 | */ 74 | protected function instantiateCache(string $cacheIdentifier, string $cacheObjectName, BackendInterface $backend): FrontendInterface 75 | { 76 | $cache = new $cacheObjectName($cacheIdentifier, $backend); 77 | if (!$cache instanceof FrontendInterface) { 78 | throw new InvalidCacheException('"' . $cacheObjectName . '" is not a valid cache frontend object.', 1216304300); 79 | } 80 | 81 | return $cache; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Classes/CacheFactoryInterface.php: -------------------------------------------------------------------------------- 1 | applicationIdentifier = $applicationIdentifier; 55 | $this->fileCacheBasePath = $fileCacheBasePath; 56 | $this->maximumPathLength = $maximumPathLength; 57 | } 58 | 59 | /** 60 | * @return int 61 | */ 62 | public function getMaximumPathLength(): int 63 | { 64 | return $this->maximumPathLength; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getFileCacheBasePath(): string 71 | { 72 | return $this->fileCacheBasePath; 73 | } 74 | 75 | /** 76 | * @return string 77 | */ 78 | public function getApplicationIdentifier(): string 79 | { 80 | return $this->applicationIdentifier; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Classes/Exception.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 50 | $this->backend = $backend; 51 | } 52 | 53 | /** 54 | * Initializes this frontend 55 | * 56 | * The backend is connected with this frontend in initializeObject(), not in __construct(), because the Cache Factory 57 | * needs an opportunity to register the cache before the backend's setCache() method is called. See 58 | * CacheFactory::create() for details. A backend (for example the SimpleFileBackend) may rely on the cache already 59 | * being registered at the CacheManager when its setCache() method is called. 60 | * 61 | * @return void 62 | */ 63 | public function initializeObject() 64 | { 65 | // FIXME: This can be removed in next major, since the Backend gets the Frontend set in the cache factory now. 66 | $this->backend->setCache($this); 67 | } 68 | 69 | /** 70 | * Returns this cache's identifier 71 | * 72 | * @return string The identifier for this cache 73 | * @api 74 | */ 75 | public function getIdentifier(): string 76 | { 77 | return $this->identifier; 78 | } 79 | 80 | /** 81 | * Returns the backend used by this cache 82 | * 83 | * @return BackendInterface The backend used by this cache 84 | * @api 85 | */ 86 | public function getBackend(): BackendInterface 87 | { 88 | return $this->backend; 89 | } 90 | 91 | /** 92 | * Checks if a cache entry with the specified identifier exists. 93 | * 94 | * @param string $entryIdentifier An identifier specifying the cache entry 95 | * @return boolean true if such an entry exists, false if not 96 | * @throws \InvalidArgumentException 97 | * @api 98 | */ 99 | public function has(string $entryIdentifier): bool 100 | { 101 | if (!$this->isValidEntryIdentifier($entryIdentifier)) { 102 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058486); 103 | } 104 | 105 | return $this->backend->has($entryIdentifier); 106 | } 107 | 108 | /** 109 | * Removes the given cache entry from the cache. 110 | * 111 | * @param string $entryIdentifier An identifier specifying the cache entry 112 | * @return boolean true if such an entry exists, false if not 113 | * @throws \InvalidArgumentException 114 | * @api 115 | */ 116 | public function remove(string $entryIdentifier): bool 117 | { 118 | if (!$this->isValidEntryIdentifier($entryIdentifier)) { 119 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058495); 120 | } 121 | 122 | return $this->backend->remove($entryIdentifier); 123 | } 124 | 125 | /** 126 | * Removes all cache entries of this cache. 127 | * 128 | * @return void 129 | * @api 130 | */ 131 | public function flush() 132 | { 133 | $this->backend->flush(); 134 | } 135 | 136 | /** 137 | * Removes all cache entries of this cache which are tagged by the specified tag. 138 | * 139 | * @param string $tag The tag the entries must have 140 | * @return integer The number of entries which have been affected by this flush 141 | * @throws \InvalidArgumentException 142 | * @api 143 | */ 144 | public function flushByTag(string $tag): int 145 | { 146 | if (!$this->isValidTag($tag)) { 147 | throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057359); 148 | } 149 | if ($this->backend instanceof TaggableBackendInterface) { 150 | return $this->backend->flushByTag($tag); 151 | } 152 | return 0; 153 | } 154 | 155 | /** 156 | * Removes all cache entries of this cache which are tagged by any of the specified tags. 157 | * 158 | * @param array $tags The tags the entries must have 159 | * @return integer The number of entries which have been affected by this flush 160 | * @throws \InvalidArgumentException 161 | * @api 162 | */ 163 | public function flushByTags(array $tags): int 164 | { 165 | foreach ($tags as $tag) { 166 | if (!$this->isValidTag($tag)) { 167 | throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1646209443); 168 | } 169 | } 170 | if ($this->backend instanceof TaggableBackendInterface) { 171 | return $this->backend->flushByTags($tags); 172 | } 173 | return 0; 174 | } 175 | 176 | /** 177 | * Does garbage collection 178 | * 179 | * @return void 180 | * @api 181 | */ 182 | public function collectGarbage() 183 | { 184 | $this->backend->collectGarbage(); 185 | } 186 | 187 | /** 188 | * Checks the validity of an entry identifier. Returns true if it's valid. 189 | * 190 | * @param string $identifier An identifier to be checked for validity 191 | * @return boolean 192 | * @api 193 | */ 194 | public function isValidEntryIdentifier(string $identifier): bool 195 | { 196 | return preg_match(self::PATTERN_ENTRYIDENTIFIER, $identifier) === 1; 197 | } 198 | 199 | /** 200 | * Checks the validity of a tag. Returns true if it's valid. 201 | * 202 | * @param string $tag An identifier to be checked for validity 203 | * @return boolean 204 | * @api 205 | */ 206 | public function isValidTag(string $tag): bool 207 | { 208 | return preg_match(self::PATTERN_TAG, $tag) === 1; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Classes/Frontend/CacheEntryIterator.php: -------------------------------------------------------------------------------- 1 | frontend = $frontend; 44 | $this->backend = $backend; 45 | $this->backend->rewind(); 46 | } 47 | 48 | /** 49 | * Returns the data of the current cache entry pointed to by the cache entry 50 | * iterator. 51 | * 52 | * @return mixed 53 | * @api 54 | */ 55 | public function current(): mixed 56 | { 57 | return $this->frontend->get((string) $this->backend->key()); 58 | } 59 | 60 | /** 61 | * Move forward to the next cache entry 62 | * 63 | * @return void 64 | * @api 65 | */ 66 | public function next(): void 67 | { 68 | $this->backend->next(); 69 | } 70 | 71 | /** 72 | * Returns the identifier of the current cache entry pointed to by the cache 73 | * entry iterator. 74 | * 75 | * @return string 76 | * @api 77 | */ 78 | public function key(): string 79 | { 80 | return (string) $this->backend->key(); 81 | } 82 | 83 | /** 84 | * Checks if current position of the cache entry iterator is valid 85 | * 86 | * @return boolean true if the current element of the iterator is valid, otherwise false 87 | * @api 88 | */ 89 | public function valid(): bool 90 | { 91 | return $this->backend->valid(); 92 | } 93 | 94 | /** 95 | * Rewind the cache entry iterator to the first element 96 | * 97 | * @return void 98 | * @api 99 | */ 100 | public function rewind(): void 101 | { 102 | $this->backend->rewind(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Classes/Frontend/FrontendInterface.php: -------------------------------------------------------------------------------- 1 | $tags The tags the entries must have 116 | * @return integer The number of entries which have been affected by this flush 117 | * @api 118 | */ 119 | public function flushByTags(array $tags): int; 120 | 121 | /** 122 | * Does garbage collection 123 | * 124 | * @return void 125 | * @api 126 | */ 127 | public function collectGarbage(); 128 | 129 | /** 130 | * Checks the validity of an entry identifier. Returns true if it's valid. 131 | * 132 | * @param string $identifier An identifier to be checked for validity 133 | * @return boolean 134 | * @api 135 | */ 136 | public function isValidEntryIdentifier(string $identifier): bool; 137 | 138 | /** 139 | * Checks the validity of a tag. Returns true if it's valid. 140 | * 141 | * @param string $tag A tag to be checked for validity 142 | * @return boolean 143 | * @api 144 | */ 145 | public function isValidTag(string $tag): bool; 146 | } 147 | -------------------------------------------------------------------------------- /Classes/Frontend/PhpFrontend.php: -------------------------------------------------------------------------------- 1 | isValidEntryIdentifier($entryIdentifier)) { 53 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057752); 54 | } 55 | $code = $this->backend->get($entryIdentifier); 56 | 57 | if ($code === false) { 58 | return false; 59 | } 60 | 61 | preg_match('/^(?:.*\n){1}((?:.*\n)*)(?:.+\n?|\n)$/', $code, $matches); 62 | 63 | return $matches[1]; 64 | } 65 | 66 | /** 67 | * Returns the code wrapped in php tags as written to the cache, ready to be included. 68 | * 69 | * @param string $entryIdentifier 70 | * @return string 71 | * @throws \InvalidArgumentException 72 | */ 73 | public function getWrapped(string $entryIdentifier) 74 | { 75 | if (!$this->isValidEntryIdentifier($entryIdentifier)) { 76 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057752); 77 | } 78 | 79 | return $this->backend->get($entryIdentifier); 80 | } 81 | 82 | /** 83 | * Saves the PHP source code in the cache. 84 | * 85 | * @param string $entryIdentifier An identifier used for this cache entry, for example the class name 86 | * @param string $sourceCode PHP source code 87 | * @param array $tags Tags to associate with this cache entry 88 | * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. 89 | * @return void 90 | * @throws InvalidDataException 91 | * @throws \InvalidArgumentException 92 | * @throws \Neos\Cache\Exception 93 | * @api 94 | */ 95 | public function set(string $entryIdentifier, $sourceCode, array $tags = [], ?int $lifetime = null) 96 | { 97 | if (!$this->isValidEntryIdentifier($entryIdentifier)) { 98 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1264023823); 99 | } 100 | if (!is_string($sourceCode)) { 101 | throw new InvalidDataException('The given source code is not a valid string.', 1264023824); 102 | } 103 | foreach ($tags as $tag) { 104 | if (!$this->isValidTag($tag)) { 105 | throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1264023825); 106 | } 107 | } 108 | $sourceCode = 'backend->set($entryIdentifier, $sourceCode, $tags, $lifetime); 110 | } 111 | 112 | /** 113 | * Loads PHP code from the cache and require_onces it right away. 114 | * 115 | * @param string $entryIdentifier An identifier which describes the cache entry to load 116 | * @return mixed Potential return value from the include operation 117 | * @api 118 | */ 119 | public function requireOnce(string $entryIdentifier) 120 | { 121 | return $this->backend->requireOnce($entryIdentifier); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Classes/Frontend/StringFrontend.php: -------------------------------------------------------------------------------- 1 | isValidEntryIdentifier($entryIdentifier)) { 43 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057566); 44 | } 45 | if (!is_string($string)) { 46 | throw new InvalidDataException('Given data is of type "' . gettype($string) . '", but a string is expected for string cache.', 1222808333); 47 | } 48 | foreach ($tags as $tag) { 49 | if (!$this->isValidTag($tag)) { 50 | throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057512); 51 | } 52 | } 53 | 54 | $this->backend->set($entryIdentifier, $string, $tags, $lifetime); 55 | } 56 | 57 | /** 58 | * Finds and returns a variable value from the cache. 59 | * 60 | * @param string $entryIdentifier Identifier of the cache entry to fetch 61 | * @return string|false The value or false if the cache entry could not be loaded 62 | * @throws \InvalidArgumentException 63 | * @api 64 | */ 65 | public function get(string $entryIdentifier) 66 | { 67 | if (!$this->isValidEntryIdentifier($entryIdentifier)) { 68 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233057752); 69 | } 70 | 71 | return $this->backend->get($entryIdentifier); 72 | } 73 | 74 | /** 75 | * Finds and returns all cache entries which are tagged by the specified tag. 76 | * 77 | * @param string $tag The tag to search for 78 | * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched 79 | * @throws NotSupportedByBackendException 80 | * @throws \InvalidArgumentException 81 | * @api 82 | */ 83 | public function getByTag(string $tag): array 84 | { 85 | if (!$this->backend instanceof TaggableBackendInterface) { 86 | throw new NotSupportedByBackendException('The backend must implement TaggableBackendInterface. Please choose a different cache backend or adjust the code using this cache.', 1483487409); 87 | } 88 | if (!$this->isValidTag($tag)) { 89 | throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057772); 90 | } 91 | 92 | $entries = []; 93 | $identifiers = $this->backend->findIdentifiersByTag($tag); 94 | foreach ($identifiers as $identifier) { 95 | $entries[$identifier] = $this->backend->get($identifier); 96 | } 97 | return $entries; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Classes/Frontend/VariableFrontend.php: -------------------------------------------------------------------------------- 1 | useIgBinary = extension_loaded('igbinary'); 43 | parent::initializeObject(); 44 | } 45 | 46 | /** 47 | * Saves the value of a PHP variable in the cache. Note that the variable 48 | * will be serialized if necessary. 49 | * 50 | * @param string $entryIdentifier An identifier used for this cache entry 51 | * @param mixed $variable The variable to cache 52 | * @param array $tags Tags to associate with this cache entry 53 | * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. 54 | * @return void 55 | * @throws \InvalidArgumentException 56 | * @throws \Neos\Cache\Exception 57 | * @api 58 | */ 59 | public function set(string $entryIdentifier, $variable, array $tags = [], ?int $lifetime = null) 60 | { 61 | if (!$this->isValidEntryIdentifier($entryIdentifier)) { 62 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058264); 63 | } 64 | foreach ($tags as $tag) { 65 | if (!$this->isValidTag($tag)) { 66 | throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233058269); 67 | } 68 | } 69 | if ($this->useIgBinary === true) { 70 | $this->backend->set($entryIdentifier, igbinary_serialize($variable), $tags, $lifetime); 71 | } else { 72 | $this->backend->set($entryIdentifier, serialize($variable), $tags, $lifetime); 73 | } 74 | } 75 | 76 | /** 77 | * Finds and returns a variable value from the cache. 78 | * 79 | * @param string $entryIdentifier Identifier of the cache entry to fetch 80 | * @return mixed The value 81 | * @throws \InvalidArgumentException 82 | * @api 83 | */ 84 | public function get(string $entryIdentifier) 85 | { 86 | if (!$this->isValidEntryIdentifier($entryIdentifier)) { 87 | throw new \InvalidArgumentException('"' . $entryIdentifier . '" is not a valid cache entry identifier.', 1233058294); 88 | } 89 | 90 | $rawResult = $this->backend->get($entryIdentifier); 91 | if ($rawResult === false) { 92 | return false; 93 | } 94 | return ($this->useIgBinary === true) ? igbinary_unserialize($rawResult) : unserialize($rawResult); 95 | } 96 | 97 | /** 98 | * Finds and returns all cache entries which are tagged by the specified tag. 99 | * 100 | * @param string $tag The tag to search for 101 | * @return array An array with the identifier (key) and content (value) of all matching entries. An empty array if no entries matched 102 | * @throws NotSupportedByBackendException 103 | * @throws \InvalidArgumentException 104 | * @api 105 | */ 106 | public function getByTag(string $tag): array 107 | { 108 | if (!$this->backend instanceof TaggableBackendInterface) { 109 | throw new NotSupportedByBackendException('The backend must implement TaggableBackendInterface. Please choose a different cache backend or adjust the code using this cache.', 1483487409); 110 | } 111 | if (!$this->isValidTag($tag)) { 112 | throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233058312); 113 | } 114 | 115 | $entries = []; 116 | $identifiers = $this->backend->findIdentifiersByTag($tag); 117 | foreach ($identifiers as $identifier) { 118 | $rawResult = $this->backend->get($identifier); 119 | if ($rawResult !== false) { 120 | $entries[$identifier] = ($this->useIgBinary === true) ? igbinary_unserialize($rawResult) : unserialize($rawResult); 121 | } 122 | } 123 | return $entries; 124 | } 125 | 126 | /** 127 | * Returns an iterator over the entries of this cache 128 | * 129 | * @return \Neos\Cache\Frontend\CacheEntryIterator 130 | * @throws NotSupportedByBackendException 131 | */ 132 | public function getIterator(): CacheEntryIterator 133 | { 134 | if (!$this->backend instanceof IterableBackendInterface) { 135 | throw new NotSupportedByBackendException('The cache backend (%s) configured for cache "%s" cannot be used as an iterator. Please choose a different cache backend or adjust the code using this cache.', 1371463860); 136 | } 137 | return new CacheEntryIterator($this, $this->backend); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Classes/Psr/Cache/CacheFactory.php: -------------------------------------------------------------------------------- 1 | environmentConfiguration = $environmentConfiguration; 43 | } 44 | 45 | /** 46 | * Factory method which creates the specified cache along with the specified kind of backend. 47 | * The identifier uniquely identifiers the specific cache, so that entries inside are unique. 48 | * 49 | * @param string $cacheIdentifier The name / identifier of the cache to create. 50 | * @param string $backendObjectName Object name of the cache backend 51 | * @param array $backendOptions (optional) Array of backend options 52 | * @return CacheItemPoolInterface 53 | * @throws InvalidBackendException 54 | */ 55 | public function create($cacheIdentifier, $backendObjectName, array $backendOptions = []): CacheItemPoolInterface 56 | { 57 | $backend = $this->instantiateBackend($backendObjectName, $backendOptions, $this->environmentConfiguration); 58 | $cache = $this->instantiateCache($cacheIdentifier, $backend); 59 | // TODO: Remove this need. 60 | $fakeFrontend = new VariableFrontend($cacheIdentifier, $backend); 61 | $backend->setCache($fakeFrontend); 62 | 63 | return $cache; 64 | } 65 | 66 | /** 67 | * @param string $cacheIdentifier 68 | * @param BackendInterface $backend 69 | * @return CacheItemPoolInterface 70 | */ 71 | protected function instantiateCache($cacheIdentifier, $backend): CacheItemPoolInterface 72 | { 73 | return new CachePool($cacheIdentifier, $backend); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Classes/Psr/Cache/CacheItem.php: -------------------------------------------------------------------------------- 1 | key = $key; 56 | $this->hit = $hit; 57 | if ($hit === false) { 58 | $value = null; 59 | } 60 | $this->value = $value; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getKey(): string 67 | { 68 | return $this->key; 69 | } 70 | 71 | /** 72 | * @return mixed 73 | */ 74 | public function get(): mixed 75 | { 76 | return $this->value; 77 | } 78 | 79 | /** 80 | * @return bool 81 | */ 82 | public function isHit(): bool 83 | { 84 | return $this->hit; 85 | } 86 | 87 | /** 88 | * @param mixed $value 89 | * @return static 90 | */ 91 | public function set(mixed $value): static 92 | { 93 | $this->value = $value; 94 | return $this; 95 | } 96 | 97 | /** 98 | * @param ?\DateTimeInterface $expiration 99 | * @return static 100 | */ 101 | public function expiresAt(?\DateTimeInterface $expiration): static 102 | { 103 | $this->expirationDate = null; 104 | if ($expiration instanceof \DateTimeInterface) { 105 | $this->expirationDate = $expiration; 106 | } 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * @param \DateInterval|int|null $time 113 | * @return static 114 | */ 115 | public function expiresAfter(int|\DateInterval|null $time): static 116 | { 117 | $expiresAt = null; 118 | if ($time instanceof \DateInterval) { 119 | $expiresAt = (new \DateTime())->add($time); 120 | } 121 | 122 | if (is_int($time)) { 123 | $expiresAt = new \DateTime('@' . (time() + $time)); 124 | } 125 | 126 | return $this->expiresAt($expiresAt); 127 | } 128 | 129 | /** 130 | * @return \DateTime|null 131 | */ 132 | public function getExpirationDate() 133 | { 134 | return $this->expirationDate; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Classes/Psr/Cache/CachePool.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 64 | $this->backend = $backend; 65 | } 66 | 67 | /** 68 | * Returns a Cache Item representing the specified key. 69 | * 70 | * @param string $key 71 | * @return CacheItemInterface 72 | * @throws InvalidArgumentException 73 | */ 74 | public function getItem(string $key): CacheItemInterface 75 | { 76 | if (!$this->isValidEntryIdentifier($key)) { 77 | throw new InvalidArgumentException('"' . $key . '" is not a valid cache entry identifier.', 1514738649629); 78 | } 79 | 80 | $rawResult = $this->backend->get($key); 81 | if ($rawResult === false) { 82 | return new CacheItem($key, false); 83 | } 84 | 85 | $value = unserialize($rawResult); 86 | return new CacheItem($key, true, $value); 87 | } 88 | 89 | /** 90 | * Returns a traversable set of cache items. 91 | * 92 | * @param string[] $keys 93 | * @return array 94 | * @throws InvalidArgumentException 95 | */ 96 | public function getItems(array $keys = []): iterable 97 | { 98 | return array_map(function ($key) { 99 | return $this->getItem($key); 100 | }, $keys); 101 | } 102 | 103 | /** 104 | * Confirms if the cache contains specified cache item. 105 | * 106 | * @param string $key 107 | * @return bool 108 | * @throws InvalidArgumentException 109 | */ 110 | public function hasItem(string $key): bool 111 | { 112 | if (!$this->isValidEntryIdentifier($key)) { 113 | throw new InvalidArgumentException('"' . $key . '" is not a valid cache entry identifier.', 1514738924982); 114 | } 115 | 116 | return $this->backend->has($key); 117 | } 118 | 119 | /** 120 | * Deletes all items in the pool. 121 | * 122 | * @return bool 123 | */ 124 | public function clear(): bool 125 | { 126 | $this->backend->flush(); 127 | return true; 128 | } 129 | 130 | /** 131 | * Removes the item from the pool. 132 | * 133 | * @param string $key 134 | * @return bool 135 | * @throws InvalidArgumentException 136 | */ 137 | public function deleteItem(string $key): bool 138 | { 139 | if (!$this->isValidEntryIdentifier($key)) { 140 | throw new InvalidArgumentException('"' . $key . '" is not a valid cache entry identifier.', 1514741469583); 141 | } 142 | 143 | return $this->backend->remove($key); 144 | } 145 | 146 | /** 147 | * Removes multiple items from the pool. 148 | * 149 | * @param string[] $keys 150 | * @return bool 151 | * @throws InvalidArgumentException 152 | */ 153 | public function deleteItems(array $keys): bool 154 | { 155 | $deleted = true; 156 | foreach ($keys as $key) { 157 | $deleted = $this->deleteItem($key) ? $deleted : false; 158 | } 159 | 160 | return $deleted; 161 | } 162 | 163 | /** 164 | * Persists a cache item immediately. 165 | * 166 | * @param CacheItemInterface $item 167 | * @return bool 168 | * @throws \Neos\Cache\Exception 169 | */ 170 | public function save(CacheItemInterface $item): bool 171 | { 172 | $lifetime = null; 173 | $expiresAt = null; 174 | if ($item instanceof CacheItem) { 175 | $expiresAt = $item->getExpirationDate(); 176 | } 177 | 178 | if ($expiresAt instanceof \DateTimeInterface) { 179 | $lifetime = $expiresAt->getTimestamp() - (new \DateTime())->getTimestamp(); 180 | } 181 | 182 | $this->backend->set($item->getKey(), serialize($item->get()), [], $lifetime); 183 | return true; 184 | } 185 | 186 | /** 187 | * Sets a cache item to be persisted later. 188 | * 189 | * @param CacheItemInterface $item 190 | * @return bool 191 | */ 192 | public function saveDeferred(CacheItemInterface $item): bool 193 | { 194 | $this->deferredItems[] = $item; 195 | return true; 196 | } 197 | 198 | /** 199 | * Persists any deferred cache items. 200 | * 201 | * @return bool 202 | * @throws \Neos\Cache\Exception 203 | */ 204 | public function commit(): bool 205 | { 206 | foreach ($this->deferredItems as $item) { 207 | $this->save($item); 208 | } 209 | 210 | $this->deferredItems = []; 211 | 212 | return true; 213 | } 214 | 215 | /** 216 | * Checks the validity of an entry identifier. Returns true if it's valid. 217 | * 218 | * @param string $identifier An identifier to be checked for validity 219 | * @return boolean 220 | * @api 221 | */ 222 | public function isValidEntryIdentifier($identifier): bool 223 | { 224 | return preg_match(self::PATTERN_ENTRYIDENTIFIER, $identifier) === 1; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Classes/Psr/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | isValidEntryIdentifier($identifier) === false) { 52 | throw new InvalidArgumentException('"' . $identifier . '" is not a valid cache identifier.', 1515192811703); 53 | } 54 | $this->identifier = $identifier; 55 | $this->backend = $backend; 56 | } 57 | 58 | /** 59 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. 60 | * 61 | * @param string $key An identifier used for this cache entry 62 | * @param mixed $value The variable to cache 63 | * @param null|int|\DateInterval $ttl Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime. 64 | * @return bool 65 | * 66 | * @throws Exception 67 | * @throws InvalidArgumentException 68 | */ 69 | public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool 70 | { 71 | $this->ensureValidEntryIdentifier($key); 72 | 73 | try { 74 | if ($ttl instanceof \DateInterval) { 75 | $lifetime = $this->calculateLifetimeFromDateInterval($ttl); 76 | } else { 77 | $lifetime = $ttl; 78 | } 79 | $this->backend->set($key, serialize($value), [], $lifetime); 80 | } catch (\Throwable $throwable) { 81 | throw new Exception('An exception was thrown in retrieving the key from the cache backend.', 1515193492062, $throwable); 82 | } 83 | return true; 84 | } 85 | 86 | /** 87 | * Finds and returns a variable value from the cache. 88 | * 89 | * @param string $key Identifier of the cache entry to fetch 90 | * @param mixed $default 91 | * @return mixed The value or the defaultValue if entry was not found 92 | * @throws Exception 93 | * @throws InvalidArgumentException 94 | */ 95 | public function get(string $key, mixed $default = null): mixed 96 | { 97 | $this->ensureValidEntryIdentifier($key); 98 | try { 99 | $rawResult = $this->backend->get($key); 100 | } catch (\Throwable $throwable) { 101 | throw new Exception('An exception was thrown in retrieving the key from the cache backend.', 1515193339722, $throwable); 102 | } 103 | if ($rawResult === false) { 104 | return $default; 105 | } 106 | 107 | return unserialize((string)$rawResult); 108 | } 109 | 110 | /** 111 | * @param string $key 112 | * @return bool 113 | * @throws Exception 114 | * @throws InvalidArgumentException 115 | */ 116 | public function delete(string $key): bool 117 | { 118 | $this->ensureValidEntryIdentifier($key); 119 | try { 120 | return $this->backend->remove($key); 121 | } catch (\Throwable $throwable) { 122 | throw new Exception('An exception was thrown in removing the key from the cache backend.', 1515193384076, $throwable); 123 | } 124 | } 125 | 126 | /** 127 | * @return bool 128 | */ 129 | public function clear(): bool 130 | { 131 | $this->backend->flush(); 132 | return true; 133 | } 134 | 135 | /** 136 | * @param iterable $keys 137 | * @param mixed $default 138 | * @return iterable 139 | * @throws InvalidArgumentException 140 | * @throws Exception 141 | */ 142 | public function getMultiple(iterable $keys, mixed $default = null): iterable 143 | { 144 | $result = []; 145 | foreach ($keys as $key) { 146 | $result[$key] = $this->get($key, $default); 147 | } 148 | return $result; 149 | } 150 | 151 | /** 152 | * @param iterable $values 153 | * @param null|int|\DateInterval $ttl 154 | * @return bool 155 | * @throws Exception 156 | * @throws InvalidArgumentException 157 | */ 158 | public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool 159 | { 160 | $allSet = true; 161 | if ($ttl instanceof \DateInterval) { 162 | $lifetime = $this->calculateLifetimeFromDateInterval($ttl); 163 | } else { 164 | $lifetime = $ttl; 165 | } 166 | foreach ($values as $key => $value) { 167 | $allSet = $this->set($key, $value, $lifetime) && $allSet; 168 | } 169 | 170 | return $allSet; 171 | } 172 | 173 | /** 174 | * @param iterable $keys 175 | * @return bool 176 | * @throws InvalidArgumentException 177 | * @throws Exception 178 | */ 179 | public function deleteMultiple(iterable $keys): bool 180 | { 181 | foreach ($keys as $key) { 182 | $this->delete($key); 183 | }; 184 | 185 | return true; 186 | } 187 | 188 | /** 189 | * @param string $key 190 | * @return bool 191 | * @throws InvalidArgumentException 192 | */ 193 | public function has(string $key): bool 194 | { 195 | $this->ensureValidEntryIdentifier($key); 196 | return $this->backend->has($key); 197 | } 198 | 199 | /** 200 | * @param string $key 201 | * @return bool 202 | */ 203 | protected function isValidEntryIdentifier(string $key): bool 204 | { 205 | return (preg_match(self::PATTERN_ENTRYIDENTIFIER, $key) === 1); 206 | } 207 | 208 | /** 209 | * @param string $key 210 | * @return void 211 | * @throws InvalidArgumentException 212 | */ 213 | protected function ensureValidEntryIdentifier($key): void 214 | { 215 | if ($this->isValidEntryIdentifier($key) === false) { 216 | throw new InvalidArgumentException('"' . $key . '" is not a valid cache key.', 1515192768083); 217 | } 218 | } 219 | 220 | /** 221 | * @param \DateInterval $ttl 222 | * @return int 223 | */ 224 | protected function calculateLifetimeFromDateInterval(\DateInterval $ttl): int 225 | { 226 | $lifetime = (int)( 227 | ((int)$ttl->format('a')) * 86400 228 | + $ttl->h * 3600 229 | + $ttl->m * 60 230 | + $ttl->s 231 | ); 232 | return $lifetime; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Classes/Psr/SimpleCache/SimpleCacheFactory.php: -------------------------------------------------------------------------------- 1 | environmentConfiguration = $environmentConfiguration; 43 | } 44 | 45 | /** 46 | * Factory method which creates the specified cache along with the specified kind of backend. 47 | * The identifier uniquely identifiers the specific cache, so that entries inside are unique. 48 | * 49 | * @param string $cacheIdentifier The name / identifier of the cache to create. 50 | * @param string $backendObjectName Object name of the cache backend 51 | * @param array $backendOptions (optional) Array of backend options 52 | * @return CacheInterface 53 | * @throws InvalidBackendException 54 | */ 55 | public function create(string $cacheIdentifier, string $backendObjectName, array $backendOptions = []): CacheInterface 56 | { 57 | $backend = $this->instantiateBackend($backendObjectName, $backendOptions, $this->environmentConfiguration); 58 | $cache = $this->instantiateCache($cacheIdentifier, $backend); 59 | // TODO: Remove this need. 60 | $fakeFrontend = new VariableFrontend($cacheIdentifier, $backend); 61 | $backend->setCache($fakeFrontend); 62 | 63 | return $cache; 64 | } 65 | 66 | /** 67 | * @param string $cacheIdentifier 68 | * @param BackendInterface $backend 69 | * @return CacheInterface 70 | */ 71 | protected function instantiateCache($cacheIdentifier, $backend): CacheInterface 72 | { 73 | return new SimpleCache($cacheIdentifier, $backend); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Neos project contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Neos Cache Framework 2 | 3 | This is a generic cache package for use in projects. 4 | It implements [PSR-6](https://github.com/php-fig/cache) and [PSR-16](https://github.com/php-fig/simple-cache) but 5 | also brings own interfaces used in Flow and Neos which support additional featuers. 6 | 7 | #### Note 8 | 9 | This repository is a **read-only subsplit** of a package that is part of the 10 | Flow framework (learn more on `http://flow.neos.io `_). 11 | 12 | All pull requests and issues should be opened in the [main repository](https://github.com/neos/flow-development-collection). 13 | 14 | The package is usable without the Flow framework, but if you 15 | want to use it, please have a look at the `Flow documentation 16 | `_ 17 | 18 | ## Installation 19 | 20 | Install latest version via composer: 21 | 22 | `composer require neos/cache` 23 | 24 | ## Basic usage 25 | 26 | 27 | $environmentConfiguration = new \Neos\Cache\EnvironmentConfiguration('appIdentifier', __DIR__); 28 | 29 | // This cache factory can be used for PSR-6 caches 30 | // and for the Neos CacheInterface 31 | $cacheFactory = new \Neos\Cache\Psr\Cache\CacheFactory( 32 | $environmentConfiguration 33 | ); 34 | 35 | // Create a PSR-6 compatible cache 36 | $cachePool = $cacheFactory->create( 37 | 'myCache', 38 | \Neos\Cache\Backend\SimpleFileBackend::class 39 | ); 40 | 41 | // Create a PSR-16 compatible cache 42 | $simpleCacheFactory = new \Neos\Cache\Psr\SimpleCache\SimpleCacheFactory( 43 | $environmentConfiguration 44 | ); 45 | 46 | $simpleCache = $simpleCacheFactory->create( 47 | 'myCache', 48 | \Neos\Cache\Backend\SimpleFileBackend::class 49 | ); 50 | 51 | The first argument given to either factory is a unique identifier for the specific cache instance. 52 | If you need different caches you should give them separate identifiers. 53 | 54 | ## Documentation 55 | 56 | Both the PSR-6 CachePool and the PSR-16 SimpleCache are separate implementations with their respective factories, 57 | but both use the existing [backends](https://flowframework.readthedocs.io/en/stable/TheDefinitiveGuide/PartIII/Caching.html#cache-backends) 58 | that can also be used with the `\Neos\Cache\Frontend\FrontendInterface` implementations, which are slightly 59 | different than the PSR caches but also implement additional features like tagging. 60 | 61 | #### Note 62 | 63 | Both PSR implementations are not integrated in Flow yet, so when you use them within a Flow installation 64 | it's your responsibility to flush them correctly as ``./flow flow:cache:flush`` will not do that in this case. 65 | 66 | Contribute 67 | ---------- 68 | 69 | If you want to contribute to this package or the Flow framework, please have a look at 70 | https://github.com/neos/flow-development-collection - it is the repository 71 | used for development and all pull requests should go into it. 72 | -------------------------------------------------------------------------------- /Resources/Private/DDL.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TABLE "###CACHE_TABLE_NAME###" ( 4 | "identifier" VARCHAR(250) NOT NULL, 5 | "cache" VARCHAR(250) NOT NULL, 6 | "context" VARCHAR(150) NOT NULL, 7 | "created" INTEGER UNSIGNED NOT NULL, 8 | "lifetime" INTEGER UNSIGNED DEFAULT '0' NOT NULL, 9 | "content" MEDIUMTEXT, 10 | PRIMARY KEY ("identifier", "cache", "context") 11 | ); 12 | 13 | CREATE TABLE "###TAGS_TABLE_NAME###" ( 14 | "identifier" VARCHAR(250) NOT NULL, 15 | "cache" VARCHAR(250) NOT NULL, 16 | "context" VARCHAR(150) NOT NULL, 17 | "tag" VARCHAR(250) NOT NULL 18 | ); 19 | CREATE INDEX "identifier" ON "###TAGS_TABLE_NAME###" ("identifier", "cache", "context"); 20 | CREATE INDEX "tag" ON "###TAGS_TABLE_NAME###" ("tag"); 21 | 22 | COMMIT; 23 | -------------------------------------------------------------------------------- /Resources/Private/mysql.DDL.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TABLE "###CACHE_TABLE_NAME###" ( 4 | "identifier" VARCHAR(250) NOT NULL, 5 | "cache" VARCHAR(250) NOT NULL, 6 | "context" VARCHAR(150) NOT NULL, 7 | "created" INTEGER UNSIGNED NOT NULL, 8 | "lifetime" INTEGER UNSIGNED DEFAULT '0' NOT NULL, 9 | "content" MEDIUMBLOB, 10 | PRIMARY KEY ("identifier", "cache", "context") 11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 12 | 13 | CREATE TABLE "###TAGS_TABLE_NAME###" ( 14 | "pk" INT NOT NULL AUTO_INCREMENT, 15 | "identifier" VARCHAR(250) NOT NULL, 16 | "cache" VARCHAR(250) NOT NULL, 17 | "context" VARCHAR(150) NOT NULL, 18 | "tag" VARCHAR(250) NOT NULL, 19 | PRIMARY KEY ("pk") 20 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 21 | CREATE INDEX "identifier" ON ###TAGS_TABLE_NAME### ("identifier", "cache", "context"); 22 | CREATE INDEX "tag" ON "###TAGS_TABLE_NAME###" ("tag"); 23 | 24 | COMMIT; 25 | -------------------------------------------------------------------------------- /Resources/Private/pgsql.DDL.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TABLE "###CACHE_TABLE_NAME###" ( 4 | "identifier" VARCHAR(250) NOT NULL, 5 | "cache" VARCHAR(250) NOT NULL, 6 | "context" VARCHAR(150) NOT NULL, 7 | "created" INTEGER NOT NULL, 8 | "lifetime" INTEGER DEFAULT '0' NOT NULL, 9 | "content" BYTEA, 10 | PRIMARY KEY ("identifier", "cache", "context") 11 | ); 12 | 13 | CREATE TABLE "###TAGS_TABLE_NAME###" ( 14 | "identifier" VARCHAR(250) NOT NULL, 15 | "cache" VARCHAR(250) NOT NULL, 16 | "context" VARCHAR(150) NOT NULL, 17 | "tag" VARCHAR(250) NOT NULL 18 | ); 19 | CREATE INDEX "identifier" ON "###TAGS_TABLE_NAME###" ("identifier", "cache", "context"); 20 | CREATE INDEX "tag" ON "###TAGS_TABLE_NAME###" ("tag"); 21 | 22 | COMMIT; 23 | -------------------------------------------------------------------------------- /Tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | hasMethod('set' . $methodNamePart)) { 53 | $methodName = 'set' . $methodNamePart; 54 | $target->$methodName($dependency); 55 | } elseif ($objectReflection->hasMethod('inject' . $methodNamePart)) { 56 | $methodName = 'inject' . $methodNamePart; 57 | $target->$methodName($dependency); 58 | } elseif ($objectReflection->hasProperty($name)) { 59 | $property = $objectReflection->getProperty($name); 60 | $property->setAccessible(true); 61 | $property->setValue($target, $dependency); 62 | } else { 63 | throw new \RuntimeException('Could not inject ' . $name . ' into object of type ' . get_class($target)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/Functional/Backend/PdoBackendTest.php: -------------------------------------------------------------------------------- 1 | backends as $backend) { 46 | $backend->flush(); 47 | } 48 | } 49 | 50 | public function backendsToTest(): array 51 | { 52 | $this->cache = $this->createMock(FrontendInterface::class); 53 | $this->cache->method('getIdentifier')->willReturn('TestCache'); 54 | $this->setupBackends(); 55 | return $this->backends; 56 | } 57 | 58 | /** 59 | * @test 60 | * @dataProvider backendsToTest 61 | */ 62 | public function setAddsCacheEntry(BackendInterface $backend): void 63 | { 64 | $backend->flush(); 65 | 66 | // use data that contains binary junk 67 | $data = random_bytes(2048); 68 | $backend->set('some_entry', $data); 69 | self::assertEquals($data, $backend->get('some_entry')); 70 | } 71 | 72 | /** 73 | * @test 74 | * @dataProvider backendsToTest 75 | */ 76 | public function cacheEntriesCanBeIterated(BackendInterface $backend): void 77 | { 78 | $backend->flush(); 79 | 80 | // use data that contains binary junk 81 | $data = random_bytes(128); 82 | $backend->set('first_entry', $data); 83 | $backend->set('second_entry', $data); 84 | $backend->set('third_entry', $data); 85 | 86 | $entries = 0; 87 | foreach ($backend as $entry) { 88 | self::assertEquals($data, $entry); 89 | $entries++; 90 | } 91 | 92 | self::assertEquals(3, $entries); 93 | } 94 | 95 | private function setupBackends(): void 96 | { 97 | try { 98 | $backend = new PdoBackend( 99 | new EnvironmentConfiguration('PdoBackend a wonderful color Testing', '/some/path', PHP_MAXPATHLEN), 100 | [ 101 | 'dataSourceName' => 'sqlite::memory:', 102 | 'defaultLifetime' => 0 103 | ] 104 | ); 105 | $backend->setup(); 106 | $backend->setCache($this->cache); 107 | $backend->flush(); 108 | $this->backends['sqlite'] = [$backend]; 109 | } catch (\Throwable $t) { 110 | $this->addWarning('SQLite DB is not reachable: ' . $t->getMessage()); 111 | } 112 | 113 | try { 114 | $backend = new PdoBackend( 115 | new EnvironmentConfiguration('PdoBackend a wonderful color Testing', '/some/path', PHP_MAXPATHLEN), 116 | [ 117 | 'dataSourceName' => 'mysql:host=127.0.0.1;dbname=flow_functional_testing', 118 | 'username' => 'neos', 119 | 'password' => 'neos', 120 | 'defaultLifetime' => 0 121 | ] 122 | ); 123 | $backend->setup(); 124 | $backend->setCache($this->cache); 125 | $backend->flush(); 126 | $this->backends['mysql'] = [$backend]; 127 | } catch (\Throwable $t) { 128 | $this->addWarning('MySQL DB server is not reachable: ' . $t->getMessage()); 129 | } 130 | 131 | try { 132 | $backend = new PdoBackend( 133 | new EnvironmentConfiguration('PdoBackend a wonderful color Testing', '/some/path', PHP_MAXPATHLEN), 134 | [ 135 | 'dataSourceName' => 'pgsql:host=127.0.0.1;dbname=flow_functional_testing', 136 | 'username' => 'neos', 137 | 'password' => 'neos', 138 | 'defaultLifetime' => 0 139 | ] 140 | ); 141 | $backend->setup(); 142 | $backend->setCache($this->cache); 143 | $backend->flush(); 144 | $this->backends['pgsql'] = [$backend]; 145 | } catch (\Throwable $t) { 146 | $this->addWarning('PostgreSQL DB server is not reachable: ' . $t->getMessage()); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Tests/Functional/Backend/RedisBackendTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped(sprintf('phpredis extension version %s is not supported. Please update to version 5.0.0+.', $phpredisVersion)); 54 | } 55 | try { 56 | if (!@fsockopen('127.0.0.1', 6379)) { 57 | $this->markTestSkipped('redis server not reachable'); 58 | } 59 | } catch (\Exception $e) { 60 | $this->markTestSkipped('redis server not reachable'); 61 | } 62 | $this->backend = new RedisBackend( 63 | new EnvironmentConfiguration('Redis a wonderful color Testing', '/some/path', PHP_MAXPATHLEN), 64 | ['hostname' => '127.0.0.1', 'database' => 0] 65 | ); 66 | $this->cache = $this->createMock(FrontendInterface::class); 67 | $this->cache->expects(self::any())->method('getIdentifier')->will(self::returnValue('TestCache')); 68 | $this->backend->setCache($this->cache); 69 | $this->backend->flush(); 70 | } 71 | 72 | /** 73 | * Tear down test case 74 | * 75 | * @return void 76 | */ 77 | protected function tearDown(): void 78 | { 79 | if ($this->backend instanceof RedisBackend) { 80 | $this->backend->flush(); 81 | } 82 | } 83 | 84 | /** 85 | * @test 86 | */ 87 | public function setAddsCacheEntry() 88 | { 89 | $this->backend->set('some_entry', 'foo'); 90 | self::assertEquals('foo', $this->backend->get('some_entry')); 91 | } 92 | 93 | /** 94 | * @test 95 | */ 96 | public function setAddsTags() 97 | { 98 | $this->backend->set('some_entry', 'foo', ['tag1', 'tag2']); 99 | $this->backend->set('some_other_entry', 'foo', ['tag2', 'tag3']); 100 | 101 | self::assertEquals(['some_entry'], $this->backend->findIdentifiersByTag('tag1')); 102 | $expected = ['some_entry', 'some_other_entry']; 103 | $actual = $this->backend->findIdentifiersByTag('tag2'); 104 | 105 | // since Redis does not garantuee the order of values in sets, manually sort the array for comparison 106 | natsort($actual); 107 | $actual = array_values($actual); 108 | 109 | self::assertEquals($expected, $actual); 110 | self::assertEquals(['some_other_entry'], $this->backend->findIdentifiersByTag('tag3')); 111 | } 112 | 113 | /** 114 | * @test 115 | */ 116 | public function setDoesNotAddMultipleEntries() 117 | { 118 | $this->backend->set('some_entry', 'foo'); 119 | $this->backend->set('some_entry', 'bar'); 120 | 121 | $entryIdentifiers = []; 122 | foreach ($this->backend as $entryIdentifier => $entryValue) { 123 | $entryIdentifiers[] = $entryIdentifier; 124 | } 125 | 126 | self::assertEquals(['some_entry'], $entryIdentifiers); 127 | } 128 | 129 | /** 130 | * @test 131 | */ 132 | public function cacheIsIterable() 133 | { 134 | for ($i = 0; $i < 100; $i++) { 135 | $this->backend->set('entry_' . $i, 'foo'); 136 | } 137 | $actualEntries = []; 138 | foreach ($this->backend as $key => $value) { 139 | $actualEntries[] = $key; 140 | } 141 | 142 | self::assertCount(100, $actualEntries); 143 | 144 | for ($i = 0; $i < 100; $i++) { 145 | self::assertContains('entry_' . $i, $actualEntries); 146 | } 147 | } 148 | 149 | /** 150 | * @test 151 | */ 152 | public function freezeFreezesTheCache() 153 | { 154 | self::assertFalse($this->backend->isFrozen()); 155 | for ($i = 0; $i < 10; $i++) { 156 | $this->backend->set('entry_' . $i, 'foo'); 157 | } 158 | $this->backend->freeze(); 159 | self::assertTrue($this->backend->isFrozen()); 160 | } 161 | 162 | /** 163 | * @test 164 | */ 165 | public function flushUnfreezesTheCache() 166 | { 167 | self::assertFalse($this->backend->isFrozen()); 168 | $this->backend->freeze(); 169 | self::assertTrue($this->backend->isFrozen()); 170 | $this->backend->flush(); 171 | self::assertFalse($this->backend->isFrozen()); 172 | } 173 | 174 | /** 175 | * @test 176 | */ 177 | public function flushByTagFlushesEntryByTag() 178 | { 179 | for ($i = 0; $i < 10; $i++) { 180 | $this->backend->set('entry_' . $i, 'foo', ['tag1', 'tag2']); 181 | } 182 | for ($i = 10; $i < 20; $i++) { 183 | $this->backend->set('entry_' . $i, 'foo', ['tag2']); 184 | } 185 | self::assertCount(10, $this->backend->findIdentifiersByTag('tag1')); 186 | self::assertCount(20, $this->backend->findIdentifiersByTag('tag2')); 187 | 188 | $count = $this->backend->flushByTag('tag1'); 189 | self::assertEquals(10, $count, 'flushByTag returns amount of flushed entries'); 190 | self::assertCount(0, $this->backend->findIdentifiersByTag('tag1')); 191 | self::assertCount(10, $this->backend->findIdentifiersByTag('tag2')); 192 | } 193 | 194 | /** 195 | * @test 196 | */ 197 | public function flushByTagsFlushesEntryByTags() 198 | { 199 | for ($i = 0; $i < 10; $i++) { 200 | $this->backend->set('entry_' . $i, 'foo', ['tag1', 'tag2']); 201 | } 202 | for ($i = 10; $i < 20; $i++) { 203 | $this->backend->set('entry_' . $i, 'foo', ['tag2']); 204 | } 205 | for ($i = 20; $i < 30; $i++) { 206 | $this->backend->set('entry_' . $i, 'foo', ['tag3']); 207 | } 208 | self::assertCount(10, $this->backend->findIdentifiersByTag('tag1')); 209 | self::assertCount(20, $this->backend->findIdentifiersByTag('tag2')); 210 | self::assertCount(10, $this->backend->findIdentifiersByTag('tag3')); 211 | 212 | $count = $this->backend->flushByTags(['tag1', 'tag3']); 213 | self::assertEquals(20, $count, 'flushByTag returns amount of flushed entries'); 214 | self::assertCount(0, $this->backend->findIdentifiersByTag('tag1')); 215 | self::assertCount(10, $this->backend->findIdentifiersByTag('tag2')); 216 | self::assertCount(0, $this->backend->findIdentifiersByTag('tag3')); 217 | } 218 | 219 | /** 220 | * @test 221 | */ 222 | public function flushByTagRemovesEntries() 223 | { 224 | $this->backend->set('some_entry', 'foo', ['tag1', 'tag2']); 225 | 226 | $this->backend->flushByTag('tag1'); 227 | 228 | $entryIdentifiers = []; 229 | foreach ($this->backend as $entryIdentifier => $entryValue) { 230 | $entryIdentifiers[] = $entryIdentifier; 231 | } 232 | 233 | self::assertEquals([], $entryIdentifiers); 234 | } 235 | 236 | /** 237 | * @test 238 | */ 239 | public function flushByTagsRemovesEntries() 240 | { 241 | $this->backend->set('some_entry', 'foo', ['tag1', 'tag2']); 242 | $this->backend->set('some_other_entry', 'bar', ['tag3']); 243 | 244 | $this->backend->flushByTags(['tag1', 'tag3']); 245 | 246 | $entryIdentifiers = []; 247 | foreach ($this->backend as $entryIdentifier => $entryValue) { 248 | $entryIdentifiers[] = $entryIdentifier; 249 | } 250 | 251 | self::assertEquals([], $entryIdentifiers); 252 | } 253 | 254 | /** 255 | * @test 256 | */ 257 | public function flushFlushesCache() 258 | { 259 | for ($i = 0; $i < 10; $i++) { 260 | $this->backend->set('entry_' . $i, 'foo', ['tag1']); 261 | } 262 | self::assertTrue($this->backend->has('entry_5')); 263 | $this->backend->flush(); 264 | self::assertFalse($this->backend->has('entry_5')); 265 | } 266 | 267 | /** 268 | * @test 269 | */ 270 | public function removeRemovesEntryFromCache() 271 | { 272 | for ($i = 0; $i < 10; $i++) { 273 | $this->backend->set('entry_' . $i, 'foo', ['tag1']); 274 | } 275 | self::assertCount(10, $this->backend->findIdentifiersByTag('tag1')); 276 | self::assertEquals('foo', $this->backend->get('entry_1')); 277 | $actualEntries = []; 278 | foreach ($this->backend as $key => $value) { 279 | $actualEntries[] = $key; 280 | } 281 | self::assertCount(10, $actualEntries); 282 | 283 | $this->backend->remove('entry_3'); 284 | self::assertCount(9, $this->backend->findIdentifiersByTag('tag1')); 285 | self::assertFalse($this->backend->get('entry_3')); 286 | $actualEntries = []; 287 | foreach ($this->backend as $key => $value) { 288 | $actualEntries[] = $key; 289 | } 290 | self::assertCount(9, $actualEntries); 291 | } 292 | 293 | /** 294 | * @test 295 | */ 296 | public function expiredEntriesAreSkippedWhenIterating() 297 | { 298 | $this->backend->set('entry1', 'foo', [], 1); 299 | sleep(2); 300 | self::assertFalse($this->backend->has('entry1')); 301 | 302 | $actualEntries = []; 303 | foreach ($this->backend as $key => $value) { 304 | $actualEntries[] = $key; 305 | } 306 | self::assertEmpty($actualEntries, 'Entries should be empty'); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /Tests/PhpUnitBootstrap.php: -------------------------------------------------------------------------------- 1 | someOption = $value; 51 | } 52 | public function getSomeOption() { 53 | return $this->someOption; 54 | } 55 | } 56 | '); 57 | $this->backend = new $className(new EnvironmentConfiguration('Ultraman Neos Testing', '/some/path', PHP_MAXPATHLEN)); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function theConstructorCallsSetterMethodsForAllSpecifiedOptions() 64 | { 65 | $className = get_class($this->backend); 66 | $backend = new $className(new EnvironmentConfiguration('Ultraman Neos Testing', '/some/path', PHP_MAXPATHLEN), ['someOption' => 'someValue']); 67 | self::assertSame('someValue', $backend->getSomeOption()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/Unit/Backend/FileBackendEntryDtoTest.php: -------------------------------------------------------------------------------- 1 | getData()); 57 | self::assertEquals($tags, $entryDto->getTags()); 58 | self::assertEquals($expiryTime, $entryDto->getExpiryTime()); 59 | } 60 | 61 | /** 62 | * @test 63 | * @return void 64 | */ 65 | public function isExpiredReturnsFalseIfExpiryTimeIsInFuture() 66 | { 67 | $entryDto = new FileBackendEntryDto('data', [], time() + 10); 68 | self::assertFalse($entryDto->isExpired()); 69 | } 70 | 71 | /** 72 | * @test 73 | * @return void 74 | */ 75 | public function isExpiredReturnsTrueIfExpiryTimeIsInPast() 76 | { 77 | $entryDto = new FileBackendEntryDto('data', [], time() - 10); 78 | self::assertTrue($entryDto->isExpired()); 79 | } 80 | 81 | /** 82 | * @dataProvider validEntryConstructorParameters 83 | * @test 84 | * @return void 85 | */ 86 | public function isIdempotent($data, $tags, $expiryTime) 87 | { 88 | $entryDto = new FileBackendEntryDto($data, $tags, $expiryTime); 89 | $entryString = (string)$entryDto; 90 | $entryDtoReconstituted = FileBackendEntryDto::fromString($entryString); 91 | $entryStringFromReconstituted = (string)$entryDtoReconstituted; 92 | self::assertEquals($entryString, $entryStringFromReconstituted); 93 | self::assertEquals($data, $entryDtoReconstituted->getData()); 94 | self::assertEquals($tags, $entryDtoReconstituted->getTags()); 95 | self::assertEquals($expiryTime, $entryDtoReconstituted->getExpiryTime()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tests/Unit/Backend/IterableMultiBackendTest.php: -------------------------------------------------------------------------------- 1 | getEnvironmentConfiguration(), 23 | [ 24 | 'debug' => true, 25 | 'backendConfigurations' => [ 26 | [ 27 | 'backend' => FileBackend::class, 28 | 'backendOptions' => [] 29 | ] 30 | ] 31 | ] 32 | ); 33 | 34 | $cache = new VariableFrontend('TestCache', $multiBackend); 35 | $multiBackend->setCache($cache); 36 | 37 | $cache->set('foo1', 'bar1'); 38 | $cache->set('foo2', 'bar2'); 39 | 40 | 41 | $iterator = $cache->getIterator(); 42 | $iterator->rewind(); 43 | 44 | self::assertSame('foo1', $iterator->key()); 45 | self::assertSame('bar1', $iterator->current()); 46 | 47 | $iterator->next(); 48 | 49 | self::assertSame('foo2', $iterator->key()); 50 | self::assertSame('bar2', $iterator->current()); 51 | } 52 | 53 | public function getEnvironmentConfiguration(): EnvironmentConfiguration 54 | { 55 | return new EnvironmentConfiguration( 56 | __DIR__ . '~Testing', 57 | 'vfs://Foo/', 58 | 255 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Unit/Backend/MemcacheBackendTest.php: -------------------------------------------------------------------------------- 1 | [ 23 | [ 24 | // Will fail as there shouldn't be a redis usually on that port 25 | 'backend' => RedisBackend::class, 26 | 'backendOptions' => [ 27 | 'port' => '60999' 28 | ] 29 | ] 30 | ] 31 | ]; 32 | 33 | $multiBackend = new MultiBackend($this->getEnvironmentConfiguration(), $backendOptions); 34 | // We need to trigger initialization. 35 | $result = $multiBackend->get('foo'); 36 | self::assertFalse($result); 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function debugModeWillBubbleExceptions(): void 43 | { 44 | $this->expectException(\Throwable::class); 45 | $backendOptions = [ 46 | 'debug' => true, 47 | 'backendConfigurations' => [ 48 | [ 49 | // Will fail as there shouldn't be a redis usually on that port 50 | 'backend' => RedisBackend::class, 51 | 'backendOptions' => [ 52 | 'port' => '60999' 53 | ] 54 | ] 55 | ] 56 | ]; 57 | 58 | $multiBackend = new MultiBackend($this->getEnvironmentConfiguration(), $backendOptions); 59 | // We need to trigger initialization. 60 | $result = $multiBackend->get('foo'); 61 | self::assertFalse($result); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function writesToAllBackends(): void 68 | { 69 | $mockBuilder = $this->getMockBuilder(NullBackend::class); 70 | $firstNullBackendMock = $mockBuilder->getMock(); 71 | $secondNullBackendMock = $mockBuilder->getMock(); 72 | 73 | $firstNullBackendMock->expects(self::once())->method('set')->withAnyParameters(); 74 | $secondNullBackendMock->expects(self::once())->method('set')->withAnyParameters(); 75 | 76 | $multiBackend = new MultiBackend($this->getEnvironmentConfiguration(), []); 77 | $this->inject($multiBackend, 'backends', [$firstNullBackendMock, $secondNullBackendMock]); 78 | $this->inject($multiBackend, 'initialized', true); 79 | 80 | $multiBackend->set('foo', 'data'); 81 | } 82 | 83 | /** 84 | * @test 85 | */ 86 | public function fallsBackToSecondaryBackend(): void 87 | { 88 | $mockBuilder = $this->getMockBuilder(NullBackend::class); 89 | $firstNullBackendMock = $mockBuilder->getMock(); 90 | $secondNullBackendMock = $mockBuilder->getMock(); 91 | 92 | $firstNullBackendMock->expects(self::once())->method('get')->with('foo')->willThrowException(new \Exception('Backend failure')); 93 | $secondNullBackendMock->expects(self::once())->method('get')->with('foo')->willReturn(5); 94 | 95 | $multiBackend = new MultiBackend($this->getEnvironmentConfiguration(), []); 96 | $this->inject($multiBackend, 'backends', [$firstNullBackendMock, $secondNullBackendMock]); 97 | $this->inject($multiBackend, 'initialized', true); 98 | 99 | $result = $multiBackend->get('foo'); 100 | self::assertSame(5, $result); 101 | } 102 | 103 | /** 104 | * @test 105 | */ 106 | public function removesUnhealthyBackend(): void 107 | { 108 | $mockBuilder = $this->getMockBuilder(NullBackend::class); 109 | $firstNullBackendMock = $mockBuilder->getMock(); 110 | $secondNullBackendMock = $mockBuilder->getMock(); 111 | 112 | $firstNullBackendMock->expects(self::once())->method('get')->with('foo')->willThrowException(new \Exception('Backend failure')); 113 | $secondNullBackendMock->expects(self::exactly(2))->method('get')->with('foo')->willReturn(5); 114 | 115 | $multiBackend = new MultiBackend($this->getEnvironmentConfiguration(), []); 116 | $multiBackend->setRemoveUnhealthyBackends(true); 117 | 118 | $this->inject($multiBackend, 'backends', [$firstNullBackendMock, $secondNullBackendMock]); 119 | $this->inject($multiBackend, 'initialized', true); 120 | 121 | $result = $multiBackend->get('foo'); 122 | self::assertSame(5, $result); 123 | $result = $multiBackend->get('foo'); 124 | self::assertSame(5, $result); 125 | } 126 | 127 | /** 128 | * @return EnvironmentConfiguration 129 | */ 130 | public function getEnvironmentConfiguration(): EnvironmentConfiguration 131 | { 132 | return new EnvironmentConfiguration( 133 | __DIR__ . '~Testing', 134 | 'vfs://Foo/', 135 | 255 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/Unit/Backend/RedisBackendTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped(sprintf('phpredis extension version %s is not supported. Please update to version 5.0.0+.', $phpredisVersion)); 54 | } 55 | 56 | $this->redis = $this->getMockBuilder(\Redis::class)->disableOriginalConstructor()->getMock(); 57 | $this->cache = $this->createMock(FrontendInterface::class); 58 | $this->cache->method('getIdentifier') 59 | ->willReturn('Foo_Cache'); 60 | 61 | $mockEnvironmentConfiguration = $this->getMockBuilder(EnvironmentConfiguration::class)->setConstructorArgs([ 62 | __DIR__ . '~Testing', 63 | 'vfs://Foo/', 64 | 255 65 | ])->getMock(); 66 | 67 | $this->backend = new RedisBackend($mockEnvironmentConfiguration, ['redis' => $this->redis]); 68 | $this->backend->setCache($this->cache); 69 | 70 | // set this to false manually, since the check in isFrozen leads to null (instead of a boolean) 71 | // as the exists call is not mocked (and cannot easily be mocked, as it is used for different 72 | // things.) 73 | $this->inject($this->backend, 'frozen', false); 74 | } 75 | 76 | /** 77 | * @test 78 | */ 79 | public function findIdentifiersByTagInvokesRedis(): void 80 | { 81 | $this->redis->expects(self::once()) 82 | ->method('sMembers') 83 | ->with('d41d8cd98f00b204e9800998ecf8427e:Foo_Cache:tag:some_tag') 84 | ->willReturn(['entry_1', 'entry_2']); 85 | 86 | $this->assertEquals(['entry_1', 'entry_2'], $this->backend->findIdentifiersByTag('some_tag')); 87 | } 88 | 89 | /** 90 | * @test 91 | */ 92 | public function freezeInvokesRedis(): void 93 | { 94 | $this->redis->method('exec') 95 | ->willReturn($this->redis); 96 | 97 | $this->redis->expects(self::once()) 98 | ->method('keys') 99 | ->willReturn(['entry_1', 'entry_2']); 100 | 101 | $this->redis->expects(self::exactly(2)) 102 | ->method('persist'); 103 | 104 | $this->redis->expects(self::once()) 105 | ->method('set') 106 | ->with('d41d8cd98f00b204e9800998ecf8427e:Foo_Cache:frozen', true); 107 | 108 | $this->backend->freeze(); 109 | } 110 | 111 | /** 112 | * @test 113 | */ 114 | public function setUsesDefaultLifetimeIfNotProvided(): void 115 | { 116 | $defaultLifetime = random_int(1, 9999); 117 | $this->backend->setDefaultLifetime($defaultLifetime); 118 | $expected = ['ex' => $defaultLifetime]; 119 | 120 | $this->redis->method('multi') 121 | ->willReturn($this->redis); 122 | 123 | $this->redis->expects(self::once()) 124 | ->method('set') 125 | ->with($this->anything(), $this->anything(), $expected) 126 | ->willReturn($this->redis); 127 | 128 | $this->backend->set('foo', 'bar'); 129 | } 130 | 131 | /** 132 | * @test 133 | */ 134 | public function setUsesProvidedLifetime(): void 135 | { 136 | $defaultLifetime = 3600; 137 | $this->backend->setDefaultLifetime($defaultLifetime); 138 | $expected = ['ex' => 1600]; 139 | 140 | $this->redis->method('multi') 141 | ->willReturn($this->redis); 142 | 143 | $this->redis->expects(self::once()) 144 | ->method('set') 145 | ->with($this->anything(), $this->anything(), $expected) 146 | ->willReturn($this->redis); 147 | 148 | $this->backend->set('foo', 'bar', [], 1600); 149 | } 150 | 151 | /** 152 | * @test 153 | */ 154 | public function setAddsEntryToRedis(): void 155 | { 156 | $this->redis->method('multi') 157 | ->willReturn($this->redis); 158 | 159 | $this->redis->expects(self::once()) 160 | ->method('set') 161 | ->with('d41d8cd98f00b204e9800998ecf8427e:Foo_Cache:entry:entry_1', 'foo') 162 | ->willReturn($this->redis); 163 | 164 | $this->backend->set('entry_1', 'foo'); 165 | } 166 | 167 | /** 168 | * @test 169 | */ 170 | public function getInvokesRedis(): void 171 | { 172 | $this->redis->expects(self::once()) 173 | ->method('get') 174 | ->with('d41d8cd98f00b204e9800998ecf8427e:Foo_Cache:entry:foo') 175 | ->willReturn('bar'); 176 | 177 | self::assertEquals('bar', $this->backend->get('foo')); 178 | } 179 | 180 | /** 181 | * @test 182 | */ 183 | public function hasInvokesRedis(): void 184 | { 185 | $this->redis->expects(self::once()) 186 | ->method('exists') 187 | ->with('d41d8cd98f00b204e9800998ecf8427e:Foo_Cache:entry:foo') 188 | ->willReturn(true); 189 | 190 | self::assertEquals(true, $this->backend->has('foo')); 191 | } 192 | 193 | /** 194 | * @test 195 | * @dataProvider writingOperationsProvider 196 | * @param string $method 197 | */ 198 | public function writingOperationsThrowAnExceptionIfCacheIsFrozen(string $method): void 199 | { 200 | $this->expectException(\RuntimeException::class); 201 | $this->inject($this->backend, 'frozen', null); 202 | $this->redis->expects(self::once()) 203 | ->method('exists') 204 | ->with('d41d8cd98f00b204e9800998ecf8427e:Foo_Cache:frozen') 205 | ->willReturn(true); 206 | 207 | $this->backend->$method('foo', 'bar'); 208 | } 209 | 210 | /** 211 | * @test 212 | * @dataProvider batchWritingOperationsProvider 213 | * @param string $method 214 | */ 215 | public function batchWritingOperationsThrowAnExceptionIfCacheIsFrozen(string $method): void 216 | { 217 | $this->expectException(\RuntimeException::class); 218 | $this->inject($this->backend, 'frozen', null); 219 | $this->redis->expects(self::once()) 220 | ->method('exists') 221 | ->with('d41d8cd98f00b204e9800998ecf8427e:Foo_Cache:frozen') 222 | ->willReturn(true); 223 | 224 | $this->backend->$method(['foo', 'bar']); 225 | } 226 | 227 | /** 228 | * @return array 229 | */ 230 | public static function writingOperationsProvider(): array 231 | { 232 | return [ 233 | ['set'], 234 | ['remove'], 235 | ['flushByTag'], 236 | ['freeze'] 237 | ]; 238 | } 239 | 240 | /** 241 | * @return array 242 | */ 243 | public static function batchWritingOperationsProvider(): array 244 | { 245 | return [ 246 | ['flushByTags'], 247 | ]; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Tests/Unit/Backend/TaggableMultiBackendTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(NullBackend::class); 21 | $firstNullBackendMock = $mockBuilder->getMock(); 22 | $secondNullBackendMock = $mockBuilder->getMock(); 23 | $thirdNullBackendMock = $mockBuilder->getMock(); 24 | 25 | $firstNullBackendMock->expects(self::once())->method('flushByTag')->with('foo')->willReturn(2); 26 | $secondNullBackendMock->expects(self::once())->method('flushByTag')->with('foo')->willThrowException(new \RuntimeException()); 27 | $thirdNullBackendMock->expects(self::once())->method('flushByTag')->with('foo')->willReturn(3); 28 | 29 | $multiBackend = new TaggableMultiBackend($this->getEnvironmentConfiguration(), []); 30 | $this->inject($multiBackend, 'backends', [$firstNullBackendMock, $secondNullBackendMock, $thirdNullBackendMock]); 31 | $this->inject($multiBackend, 'initialized', true); 32 | 33 | $result = $multiBackend->flushByTag('foo'); 34 | self::assertSame(5, $result); 35 | } 36 | 37 | /** 38 | * @return EnvironmentConfiguration 39 | */ 40 | public function getEnvironmentConfiguration(): EnvironmentConfiguration 41 | { 42 | return new EnvironmentConfiguration( 43 | __DIR__ . '~Testing', 44 | 'vfs://Foo/', 45 | 255 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/Unit/Backend/TransientMemoryBackendTest.php: -------------------------------------------------------------------------------- 1 | expectException(Exception::class); 35 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 36 | 37 | $data = 'Some data'; 38 | $identifier = 'MyIdentifier'; 39 | $backend->set($identifier, $data); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function itIsPossibleToSetAndCheckExistenceInCache(): void 46 | { 47 | $cache = $this->createMock(FrontendInterface::class); 48 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 49 | $backend->setCache($cache); 50 | 51 | $data = 'Some data'; 52 | $identifier = 'MyIdentifier'; 53 | $backend->set($identifier, $data); 54 | $inCache = $backend->has($identifier); 55 | self::assertTrue($inCache); 56 | } 57 | 58 | /** 59 | * @test 60 | */ 61 | public function itIsPossibleToSetAndGetEntry(): void 62 | { 63 | $cache = $this->createMock(FrontendInterface::class); 64 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 65 | $backend->setCache($cache); 66 | 67 | $data = 'Some data'; 68 | $identifier = 'MyIdentifier'; 69 | $backend->set($identifier, $data); 70 | $fetchedData = $backend->get($identifier); 71 | self::assertEquals($data, $fetchedData); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function itIsPossibleToRemoveEntryFromCache(): void 78 | { 79 | $cache = $this->createMock(FrontendInterface::class); 80 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 81 | $backend->setCache($cache); 82 | 83 | $data = 'Some data'; 84 | $identifier = 'MyIdentifier'; 85 | $backend->set($identifier, $data); 86 | $backend->remove($identifier); 87 | $inCache = $backend->has($identifier); 88 | self::assertFalse($inCache); 89 | } 90 | 91 | /** 92 | * @test 93 | */ 94 | public function itIsPossibleToOverwriteAnEntryInTheCache(): void 95 | { 96 | $cache = $this->createMock(FrontendInterface::class); 97 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 98 | $backend->setCache($cache); 99 | 100 | $data = 'Some data'; 101 | $identifier = 'MyIdentifier'; 102 | $backend->set($identifier, $data); 103 | $otherData = 'some other data'; 104 | $backend->set($identifier, $otherData); 105 | $fetchedData = $backend->get($identifier); 106 | self::assertEquals($otherData, $fetchedData); 107 | } 108 | 109 | /** 110 | * @test 111 | */ 112 | public function findIdentifiersByTagFindsCacheEntriesWithSpecifiedTag(): void 113 | { 114 | $cache = $this->createMock(FrontendInterface::class); 115 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 116 | $backend->setCache($cache); 117 | 118 | $data = 'Some data'; 119 | $entryIdentifier = 'MyIdentifier'; 120 | $backend->set($entryIdentifier, $data, ['UnitTestTag%tag1', 'UnitTestTag%tag2']); 121 | 122 | $retrieved = $backend->findIdentifiersByTag('UnitTestTag%tag1'); 123 | self::assertEquals($entryIdentifier, $retrieved[0]); 124 | 125 | $retrieved = $backend->findIdentifiersByTag('UnitTestTag%tag2'); 126 | self::assertEquals($entryIdentifier, $retrieved[0]); 127 | } 128 | 129 | /** 130 | * @test 131 | * @throws Exception\NotSupportedByBackendException 132 | * @throws Exception 133 | */ 134 | public function usingNumbersAsCacheIdentifiersWorksWhenUsingFindByTag(): void 135 | { 136 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 137 | $cache = new StringFrontend('test', $backend); 138 | $backend->setCache($cache); 139 | 140 | $data = 'Some data'; 141 | $entryIdentifier = '12345'; 142 | $backend->set($entryIdentifier, $data, ['UnitTestTag%tag1']); 143 | 144 | $retrieved = $cache->getByTag('UnitTestTag%tag1'); 145 | self::assertEquals($data, current($retrieved)); 146 | } 147 | 148 | /** 149 | * @test 150 | * @throws Exception 151 | */ 152 | public function usingNumbersAsCacheIdentifiersWorksWhenUsingFlushTag(): void 153 | { 154 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 155 | $cache = new StringFrontend('test', $backend); 156 | $backend->setCache($cache); 157 | 158 | $data = 'Some data'; 159 | $entryIdentifier = '12345'; 160 | $backend->set($entryIdentifier, $data, ['UnitTestTag%tag1']); 161 | 162 | $cache->flushByTag('UnitTestTag%tag1'); 163 | self::assertFalse($backend->has('12345')); 164 | } 165 | 166 | /** 167 | * @test 168 | */ 169 | public function hasReturnsFalseIfTheEntryDoesntExist(): void 170 | { 171 | $cache = $this->createMock(FrontendInterface::class); 172 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 173 | $backend->setCache($cache); 174 | 175 | $identifier = 'NonExistingIdentifier'; 176 | $inCache = $backend->has($identifier); 177 | self::assertFalse($inCache); 178 | } 179 | 180 | /** 181 | * @test 182 | */ 183 | public function removeReturnsFalseIfTheEntryDoesntExist(): void 184 | { 185 | $cache = $this->createMock(FrontendInterface::class); 186 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 187 | $backend->setCache($cache); 188 | 189 | $identifier = 'NonExistingIdentifier'; 190 | $inCache = $backend->remove($identifier); 191 | self::assertFalse($inCache); 192 | } 193 | 194 | /** 195 | * @test 196 | */ 197 | public function flushByTagRemovesCacheEntriesWithSpecifiedTag(): void 198 | { 199 | $cache = $this->createMock(FrontendInterface::class); 200 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 201 | $backend->setCache($cache); 202 | 203 | $data = 'some data' . microtime(); 204 | $backend->set('TransientMemoryBackendTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']); 205 | $backend->set('TransientMemoryBackendTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); 206 | $backend->set('TransientMemoryBackendTest3', $data, ['UnitTestTag%test']); 207 | 208 | $backend->flushByTag('UnitTestTag%special'); 209 | 210 | self::assertTrue($backend->has('TransientMemoryBackendTest1'), 'TransientMemoryBackendTest1'); 211 | self::assertFalse($backend->has('TransientMemoryBackendTest2'), 'TransientMemoryBackendTest2'); 212 | self::assertTrue($backend->has('TransientMemoryBackendTest3'), 'TransientMemoryBackendTest3'); 213 | } 214 | 215 | /** 216 | * @test 217 | */ 218 | public function flushByTagsRemovesCacheEntriesWithSpecifiedTags(): void 219 | { 220 | $cache = $this->createMock(FrontendInterface::class); 221 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 222 | $backend->setCache($cache); 223 | 224 | $data = 'some data' . microtime(); 225 | $backend->set('TransientMemoryBackendTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']); 226 | $backend->set('TransientMemoryBackendTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); 227 | $backend->set('TransientMemoryBackendTest3', $data, ['UnitTestTag%test']); 228 | 229 | $backend->flushByTags(['UnitTestTag%boring', 'UnitTestTag%special']); 230 | 231 | self::assertFalse($backend->has('TransientMemoryBackendTest1'), 'TransientMemoryBackendTest1'); 232 | self::assertFalse($backend->has('TransientMemoryBackendTest2'), 'TransientMemoryBackendTest2'); 233 | self::assertTrue($backend->has('TransientMemoryBackendTest3'), 'TransientMemoryBackendTest3'); 234 | } 235 | 236 | /** 237 | * @test 238 | */ 239 | public function flushRemovesAllCacheEntries(): void 240 | { 241 | $cache = $this->createMock(FrontendInterface::class); 242 | $backend = new TransientMemoryBackend($this->getEnvironmentConfiguration()); 243 | $backend->setCache($cache); 244 | 245 | $data = 'some data' . microtime(); 246 | $backend->set('TransientMemoryBackendTest1', $data); 247 | $backend->set('TransientMemoryBackendTest2', $data); 248 | $backend->set('TransientMemoryBackendTest3', $data); 249 | 250 | $backend->flush(); 251 | 252 | self::assertFalse($backend->has('TransientMemoryBackendTest1'), 'TransientMemoryBackendTest1'); 253 | self::assertFalse($backend->has('TransientMemoryBackendTest2'), 'TransientMemoryBackendTest2'); 254 | self::assertFalse($backend->has('TransientMemoryBackendTest3'), 'TransientMemoryBackendTest3'); 255 | } 256 | 257 | /** 258 | * @return EnvironmentConfiguration|\PHPUnit\Framework\MockObject\MockObject 259 | */ 260 | public function getEnvironmentConfiguration() 261 | { 262 | return $this->getMockBuilder(EnvironmentConfiguration::class)->setConstructorArgs([ 263 | __DIR__ . '~Testing', 264 | 'vfs://Foo/', 265 | 255 266 | ])->getMock(); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /Tests/Unit/Frontend/AbstractFrontendTest.php: -------------------------------------------------------------------------------- 1 | mockBackend = $this->getMockBuilder(AbstractBackend::class)->setMethods(['get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage'])->disableOriginalConstructor()->getMock(); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function theConstructorAcceptsValidIdentifiers() 39 | { 40 | foreach (['x', 'someValue', '123fivesixseveneight', 'some&', 'ab_cd%', rawurlencode('resource://some/äöü$&% sadf'), str_repeat('x', 250)] as $identifier) { 41 | $cache = new StringFrontend($identifier, $this->mockBackend); 42 | self::assertInstanceOf(StringFrontend::class, $cache); 43 | } 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function theConstructorRejectsInvalidIdentifiers() 50 | { 51 | foreach (['', 'abc def', 'foo!', 'bar:', 'some/', 'bla*', 'one+', 'äöü', str_repeat('x', 251), 'x$', '\\a', 'b#'] as $identifier) { 52 | try { 53 | new StringFrontend($identifier, $this->mockBackend); 54 | $this->fail('Identifier "' . $identifier . '" was not rejected.'); 55 | } catch (\Exception $exception) { 56 | self::assertInstanceOf(\InvalidArgumentException::class, $exception); 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * @test 63 | */ 64 | public function flushCallsBackend() 65 | { 66 | $identifier = 'someCacheIdentifier'; 67 | $backend = $this->getMockBuilder(AbstractBackend::class) 68 | ->setMethods(['get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage'])->disableOriginalConstructor()->getMock(); 69 | $backend->expects(self::once())->method('flush'); 70 | 71 | $cache = $this->getMockBuilder(StringFrontend::class) 72 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 73 | ->setConstructorArgs([$identifier, $backend]) 74 | ->getMock(); 75 | $cache->flush(); 76 | } 77 | 78 | /** 79 | * @test 80 | */ 81 | public function flushByTagRejectsInvalidTags() 82 | { 83 | $this->expectException(\InvalidArgumentException::class); 84 | $identifier = 'someCacheIdentifier'; 85 | $backend = $this->createMock(TaggableBackendInterface::class); 86 | $backend->expects(self::never())->method('flushByTag'); 87 | 88 | $cache = $this->getMockBuilder(StringFrontend::class) 89 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 90 | ->setConstructorArgs([$identifier, $backend]) 91 | ->getMock(); 92 | $cache->flushByTag('SomeInvalid\Tag'); 93 | } 94 | 95 | /** 96 | * @test 97 | */ 98 | public function flushByTagCallsBackendIfItIsATaggableBackend() 99 | { 100 | $tag = 'sometag'; 101 | $identifier = 'someCacheIdentifier'; 102 | $backend = $this->createMock(TaggableBackendInterface::class); 103 | $backend->expects(self::once())->method('flushByTag')->with($tag); 104 | 105 | $cache = $this->getMockBuilder(StringFrontend::class) 106 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 107 | ->setConstructorArgs([$identifier, $backend]) 108 | ->getMock(); 109 | $cache->flushByTag($tag); 110 | } 111 | 112 | /** 113 | * @test 114 | */ 115 | public function collectGarbageCallsBackend() 116 | { 117 | $identifier = 'someCacheIdentifier'; 118 | $backend = $this->getMockBuilder(AbstractBackend::class) 119 | ->setMethods(['get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage']) 120 | ->disableOriginalConstructor() 121 | ->getMock(); 122 | $backend->expects(self::once())->method('collectGarbage'); 123 | 124 | $cache = $this->getMockBuilder(StringFrontend::class) 125 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 126 | ->setConstructorArgs([$identifier, $backend]) 127 | ->getMock(); 128 | $cache->collectGarbage(); 129 | } 130 | 131 | /** 132 | * @test 133 | */ 134 | public function invalidEntryIdentifiersAreRecognizedAsInvalid() 135 | { 136 | $identifier = 'someCacheIdentifier'; 137 | $backend = $this->createMock(AbstractBackend::class); 138 | 139 | $cache = $this->getMockBuilder(StringFrontend::class) 140 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 141 | ->setConstructorArgs([$identifier, $backend]) 142 | ->getMock(); 143 | 144 | foreach (['', 'abc def', 'foo!', 'bar:', 'some/', 'bla*', 'one+', 'äöü', str_repeat('x', 251), 'x$', '\\a', 'b#'] as $entryIdentifier) { 145 | self::assertFalse($cache->isValidEntryIdentifier($entryIdentifier), 'Invalid identifier "' . $entryIdentifier . '" was not rejected.'); 146 | } 147 | } 148 | 149 | /** 150 | * @test 151 | */ 152 | public function validEntryIdentifiersAreRecognizedAsValid() 153 | { 154 | $identifier = 'someCacheIdentifier'; 155 | $backend = $this->createMock(AbstractBackend::class); 156 | $cache = $this->getMockBuilder(StringFrontend::class) 157 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 158 | ->setConstructorArgs([$identifier, $backend]) 159 | ->getMock(); 160 | 161 | foreach (['_', 'abc-def', 'foo', 'bar123', '3some', '_bl_a', 'some&', 'one%TWO', str_repeat('x', 250)] as $entryIdentifier) { 162 | self::assertTrue($cache->isValidEntryIdentifier($entryIdentifier), 'Valid identifier "' . $entryIdentifier . '" was not accepted.'); 163 | } 164 | } 165 | 166 | /** 167 | * @test 168 | */ 169 | public function invalidTagsAreRecognizedAsInvalid() 170 | { 171 | $identifier = 'someCacheIdentifier'; 172 | $backend = $this->createMock(AbstractBackend::class); 173 | $cache = $this->getMockBuilder(StringFrontend::class) 174 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 175 | ->setConstructorArgs([$identifier, $backend]) 176 | ->getMock(); 177 | 178 | foreach (['', 'abc def', 'foo!', 'bar:', 'some/', 'bla*', 'one+', 'äöü', str_repeat('x', 251), 'x$', '\\a', 'b#'] as $tag) { 179 | self::assertFalse($cache->isValidTag($tag), 'Invalid tag "' . $tag . '" was not rejected.'); 180 | } 181 | } 182 | 183 | /** 184 | * @test 185 | */ 186 | public function validTagsAreRecognizedAsValid() 187 | { 188 | $identifier = 'someCacheIdentifier'; 189 | $backend = $this->createMock(AbstractBackend::class); 190 | $cache = $this->getMockBuilder(StringFrontend::class) 191 | ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) 192 | ->setConstructorArgs([$identifier, $backend]) 193 | ->getMock(); 194 | 195 | foreach (['abcdef', 'foo-bar', 'foo_baar', 'bar123', '3some', 'file%Thing', 'some&', '%x%', str_repeat('x', 250)] as $tag) { 196 | self::assertTrue($cache->isValidTag($tag), 'Valid tag "' . $tag . '" was not accepted.'); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Tests/Unit/Frontend/PhpFrontendTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 34 | $cache = $this->getMockBuilder(StringFrontend::class) 35 | ->setMethods(['isValidEntryIdentifier']) 36 | ->disableOriginalConstructor() 37 | ->getMock(); 38 | $cache->expects(self::once())->method('isValidEntryIdentifier')->with('foo')->will(self::returnValue(false)); 39 | $cache->set('foo', 'bar'); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function setPassesPhpSourceCodeTagsAndLifetimeToBackend() 46 | { 47 | $originalSourceCode = 'return "hello world!";'; 48 | $modifiedSourceCode = 'createMock(PhpCapableBackendInterface::class); 51 | $mockBackend->expects(self::once())->method('set')->with('Foo-Bar', $modifiedSourceCode, ['tags'], 1234); 52 | 53 | $cache = $this->getMockBuilder(PhpFrontend::class) 54 | ->setMethods(null) 55 | ->disableOriginalConstructor() 56 | ->getMock(); 57 | $this->inject($cache, 'backend', $mockBackend); 58 | $cache->set('Foo-Bar', $originalSourceCode, ['tags'], 1234); 59 | } 60 | 61 | /** 62 | * @test 63 | */ 64 | public function setThrowsInvalidDataExceptionOnNonStringValues() 65 | { 66 | $this->expectException(InvalidDataException::class); 67 | $cache = $this->getMockBuilder(PhpFrontend::class) 68 | ->setMethods(null) 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | $cache->set('Foo-Bar', []); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function requireOnceCallsTheBackendsRequireOnceMethod() 78 | { 79 | $mockBackend = $this->createMock(PhpCapableBackendInterface::class); 80 | $mockBackend->expects(self::once())->method('requireOnce')->with('Foo-Bar')->will(self::returnValue('hello world!')); 81 | 82 | $cache = $this->getMockBuilder(PhpFrontend::class) 83 | ->setMethods(null) 84 | ->disableOriginalConstructor() 85 | ->getMock(); 86 | $this->inject($cache, 'backend', $mockBackend); 87 | 88 | $result = $cache->requireOnce('Foo-Bar'); 89 | self::assertSame('hello world!', $result); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/Unit/Frontend/StringFrontendTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 35 | $cache = $this->getMockBuilder(StringFrontend::class) 36 | ->setMethods(['isValidEntryIdentifier']) 37 | ->disableOriginalConstructor() 38 | ->getMock(); 39 | $cache->expects(self::once())->method('isValidEntryIdentifier')->with('foo')->will(self::returnValue(false)); 40 | $cache->set('foo', 'bar'); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function setPassesStringToBackend() 47 | { 48 | $theString = 'Just some value'; 49 | $backend = $this->prepareDefaultBackend(); 50 | $backend->expects(self::once())->method('set')->with(self::equalTo('StringCacheTest'), self::equalTo($theString)); 51 | 52 | $cache = new StringFrontend('StringFrontend', $backend); 53 | $cache->set('StringCacheTest', $theString); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function setPassesLifetimeToBackend() 60 | { 61 | $theString = 'Just some value'; 62 | $theLifetime = 1234; 63 | $backend = $this->prepareDefaultBackend(); 64 | 65 | $backend->expects(self::once())->method('set')->with(self::equalTo('StringCacheTest'), self::equalTo($theString), self::equalTo([]), self::equalTo($theLifetime)); 66 | 67 | $cache = new StringFrontend('StringFrontend', $backend); 68 | $cache->set('StringCacheTest', $theString, [], $theLifetime); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function setThrowsInvalidDataExceptionOnNonStringValues() 75 | { 76 | $this->expectException(InvalidDataException::class); 77 | $backend = $this->prepareDefaultBackend(); 78 | 79 | $cache = new StringFrontend('StringFrontend', $backend); 80 | $cache->set('StringCacheTest', []); 81 | } 82 | 83 | /** 84 | * @test 85 | */ 86 | public function getFetchesStringValueFromBackend() 87 | { 88 | $backend = $this->prepareDefaultBackend(); 89 | 90 | $backend->expects(self::once())->method('get')->will(self::returnValue('Just some value')); 91 | 92 | $cache = new StringFrontend('StringFrontend', $backend); 93 | self::assertEquals('Just some value', $cache->get('StringCacheTest'), 'The returned value was not the expected string.'); 94 | } 95 | 96 | /** 97 | * @test 98 | */ 99 | public function hasReturnsResultFromBackend() 100 | { 101 | $backend = $this->prepareDefaultBackend(); 102 | $backend->expects(self::once())->method('has')->with(self::equalTo('StringCacheTest'))->will(self::returnValue(true)); 103 | 104 | $cache = new StringFrontend('StringFrontend', $backend); 105 | self::assertTrue($cache->has('StringCacheTest'), 'has() did not return true.'); 106 | } 107 | 108 | /** 109 | * @test 110 | */ 111 | public function removeCallsBackend() 112 | { 113 | $cacheIdentifier = 'someCacheIdentifier'; 114 | $backend = $this->prepareDefaultBackend(); 115 | 116 | $backend->expects(self::once())->method('remove')->with(self::equalTo($cacheIdentifier))->will(self::returnValue(true)); 117 | 118 | $cache = new StringFrontend('StringFrontend', $backend); 119 | self::assertTrue($cache->remove($cacheIdentifier), 'remove() did not return true'); 120 | } 121 | 122 | /** 123 | * @test 124 | */ 125 | public function getByTagRejectsInvalidTags() 126 | { 127 | $this->expectException(\InvalidArgumentException::class); 128 | $backend = $this->createMock(TaggableBackendInterface::class); 129 | $backend->expects(self::never())->method('findIdentifiersByTag'); 130 | 131 | $cache = new StringFrontend('StringFrontend', $backend); 132 | $cache->getByTag('SomeInvalid\Tag'); 133 | } 134 | 135 | /** 136 | * @test 137 | */ 138 | public function getByTagThrowAnExceptionWithoutTaggableBackend() 139 | { 140 | $this->expectException(NotSupportedByBackendException::class); 141 | $backend = $this->prepareDefaultBackend(); 142 | $cache = new StringFrontend('VariableFrontend', $backend); 143 | $cache->getByTag('foo'); 144 | } 145 | 146 | /** 147 | * @test 148 | */ 149 | public function getByTagCallsBackendAndReturnsIdentifiersAndValuesOfEntries() 150 | { 151 | $tag = 'sometag'; 152 | $identifiers = ['one', 'two']; 153 | $entries = ['one' => 'one value', 'two' => 'two value']; 154 | $backend = $this->prepareTaggableBackend(); 155 | 156 | $backend->expects(self::once())->method('findIdentifiersByTag')->with(self::equalTo($tag))->will(self::returnValue($identifiers)); 157 | $backend->expects(self::exactly(2))->method('get')->will($this->onConsecutiveCalls('one value', 'two value')); 158 | 159 | $cache = new StringFrontend('StringFrontend', $backend); 160 | self::assertEquals($entries, $cache->getByTag($tag), 'Did not receive the expected entries'); 161 | } 162 | 163 | /** 164 | * @param array $methods 165 | * @return AbstractBackend|\PHPUnit\Framework\MockObject\MockObject 166 | */ 167 | protected function prepareDefaultBackend(array $methods = ['get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage']) 168 | { 169 | return $this->getMockBuilder(AbstractBackend::class) 170 | ->setMethods($methods) 171 | ->disableOriginalConstructor() 172 | ->getMock(); 173 | } 174 | 175 | /** 176 | * @param array $methods 177 | * @return AbstractBackend|\PHPUnit\Framework\MockObject\MockObject 178 | */ 179 | protected function prepareTaggableBackend(array $methods = ['get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage']) 180 | { 181 | return $this->getMockBuilder(NullBackend::class) 182 | ->setMethods($methods) 183 | ->disableOriginalConstructor() 184 | ->getMock(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Tests/Unit/Frontend/VariableFrontendTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 35 | $cache = $this->getMockBuilder(StringFrontend::class) 36 | ->setMethods(['isValidEntryIdentifier']) 37 | ->disableOriginalConstructor() 38 | ->getMock(); 39 | $cache->expects(self::once())->method('isValidEntryIdentifier')->with('foo')->will(self::returnValue(false)); 40 | $cache->set('foo', 'bar'); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function setPassesSerializedStringToBackend() 47 | { 48 | $theString = 'Just some value'; 49 | $backend = $this->prepareDefaultBackend(); 50 | $backend->expects(self::once())->method('set')->with(self::equalTo('VariableCacheTest'), self::equalTo(serialize($theString))); 51 | 52 | $cache = new VariableFrontend('VariableFrontend', $backend); 53 | $cache->set('VariableCacheTest', $theString); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function setPassesSerializedArrayToBackend() 60 | { 61 | $theArray = ['Just some value', 'and another one.']; 62 | $backend = $this->prepareDefaultBackend(); 63 | $backend->expects(self::once())->method('set')->with(self::equalTo('VariableCacheTest'), self::equalTo(serialize($theArray))); 64 | 65 | $cache = new VariableFrontend('VariableFrontend', $backend); 66 | $cache->set('VariableCacheTest', $theArray); 67 | } 68 | 69 | /** 70 | * @test 71 | */ 72 | public function setPassesLifetimeToBackend() 73 | { 74 | $theString = 'Just some value'; 75 | $theLifetime = 1234; 76 | $backend = $this->prepareDefaultBackend(); 77 | $backend->expects(self::once())->method('set')->with(self::equalTo('VariableCacheTest'), self::equalTo(serialize($theString)), self::equalTo([]), self::equalTo($theLifetime)); 78 | 79 | $cache = new VariableFrontend('VariableFrontend', $backend); 80 | $cache->set('VariableCacheTest', $theString, [], $theLifetime); 81 | } 82 | 83 | /** 84 | * @test 85 | * @requires extension igbinary 86 | */ 87 | public function setUsesIgBinarySerializeIfAvailable() 88 | { 89 | $theString = 'Just some value'; 90 | $backend = $this->prepareDefaultBackend(); 91 | $backend->expects(self::once())->method('set')->with(self::equalTo('VariableCacheTest'), self::equalTo(igbinary_serialize($theString))); 92 | 93 | $cache = new VariableFrontend('VariableFrontend', $backend); 94 | $cache->initializeObject(); 95 | $cache->set('VariableCacheTest', $theString); 96 | } 97 | 98 | /** 99 | * @test 100 | */ 101 | public function getFetchesStringValueFromBackend() 102 | { 103 | $backend = $this->prepareDefaultBackend(); 104 | $backend->expects(self::once())->method('get')->will(self::returnValue(serialize('Just some value'))); 105 | 106 | $cache = new VariableFrontend('VariableFrontend', $backend); 107 | self::assertEquals('Just some value', $cache->get('VariableCacheTest'), 'The returned value was not the expected string.'); 108 | } 109 | 110 | /** 111 | * @test 112 | */ 113 | public function getFetchesArrayValueFromBackend() 114 | { 115 | $theArray = ['Just some value', 'and another one.']; 116 | $backend = $this->prepareDefaultBackend(); 117 | $backend->expects(self::once())->method('get')->will(self::returnValue(serialize($theArray))); 118 | 119 | $cache = new VariableFrontend('VariableFrontend', $backend); 120 | self::assertEquals($theArray, $cache->get('VariableCacheTest'), 'The returned value was not the expected unserialized array.'); 121 | } 122 | 123 | /** 124 | * @test 125 | */ 126 | public function getFetchesFalseBooleanValueFromBackend() 127 | { 128 | $backend = $this->prepareDefaultBackend(); 129 | $backend->expects(self::once())->method('get')->will(self::returnValue(serialize(false))); 130 | 131 | $cache = new VariableFrontend('VariableFrontend', $backend); 132 | self::assertFalse($cache->get('VariableCacheTest'), 'The returned value was not the false.'); 133 | } 134 | 135 | /** 136 | * @test 137 | * @requires extension igbinary 138 | */ 139 | public function getUsesIgBinaryIfAvailable() 140 | { 141 | $theArray = ['Just some value', 'and another one.']; 142 | $backend = $this->prepareDefaultBackend(); 143 | $backend->expects(self::once())->method('get')->will(self::returnValue(igbinary_serialize($theArray))); 144 | 145 | $cache = new VariableFrontend('VariableFrontend', $backend); 146 | $cache->initializeObject(); 147 | 148 | self::assertEquals($theArray, $cache->get('VariableCacheTest'), 'The returned value was not the expected unserialized array.'); 149 | } 150 | 151 | /** 152 | * @test 153 | */ 154 | public function hasReturnsResultFromBackend() 155 | { 156 | $backend = $this->prepareDefaultBackend(); 157 | $backend->expects(self::once())->method('has')->with(self::equalTo('VariableCacheTest'))->will(self::returnValue(true)); 158 | 159 | $cache = new VariableFrontend('VariableFrontend', $backend); 160 | self::assertTrue($cache->has('VariableCacheTest'), 'has() did not return true.'); 161 | } 162 | 163 | /** 164 | * @test 165 | */ 166 | public function removeCallsBackend() 167 | { 168 | $cacheIdentifier = 'someCacheIdentifier'; 169 | $backend = $this->prepareDefaultBackend(); 170 | 171 | $backend->expects(self::once())->method('remove')->with(self::equalTo($cacheIdentifier))->will(self::returnValue(true)); 172 | 173 | $cache = new VariableFrontend('VariableFrontend', $backend); 174 | self::assertTrue($cache->remove($cacheIdentifier), 'remove() did not return true'); 175 | } 176 | 177 | /** 178 | * @test 179 | */ 180 | public function getByTagRejectsInvalidTags() 181 | { 182 | $this->expectException(\InvalidArgumentException::class); 183 | $backend = $this->createMock(TaggableBackendInterface::class); 184 | $backend->expects(self::never())->method('findIdentifiersByTag'); 185 | 186 | $cache = new VariableFrontend('VariableFrontend', $backend); 187 | $cache->getByTag('SomeInvalid\Tag'); 188 | } 189 | 190 | /** 191 | * @test 192 | */ 193 | public function getByTagThrowAnExceptionWithoutTaggableBackend() 194 | { 195 | $this->expectException(NotSupportedByBackendException::class); 196 | $backend = $this->prepareDefaultBackend(); 197 | $cache = new VariableFrontend('VariableFrontend', $backend); 198 | $cache->getByTag('foo'); 199 | } 200 | 201 | /** 202 | * @test 203 | */ 204 | public function getByTagCallsBackendAndReturnsIdentifiersAndValuesOfEntries() 205 | { 206 | $tag = 'sometag'; 207 | $identifiers = ['one', 'two']; 208 | $entries = ['one' => 'one value', 'two' => 'two value']; 209 | $backend = $this->prepareTaggableBackend(); 210 | 211 | $backend->expects(self::once())->method('findIdentifiersByTag')->with(self::equalTo($tag))->will(self::returnValue($identifiers)); 212 | $backend->expects(self::exactly(2))->method('get')->will($this->onConsecutiveCalls(serialize('one value'), serialize('two value'))); 213 | 214 | $cache = new VariableFrontend('VariableFrontend', $backend); 215 | self::assertEquals($entries, $cache->getByTag($tag), 'Did not receive the expected entries'); 216 | } 217 | 218 | /** 219 | * @test 220 | * @requires extension igbinary 221 | */ 222 | public function getByTagUsesIgBinaryIfAvailable() 223 | { 224 | $tag = 'sometag'; 225 | $identifiers = ['one', 'two']; 226 | $entries = ['one' => 'one value', 'two' => 'two value']; 227 | $backend = $this->prepareTaggableBackend(); 228 | 229 | $backend->expects(self::once())->method('findIdentifiersByTag')->with(self::equalTo($tag))->will(self::returnValue($identifiers)); 230 | $backend->expects(self::exactly(2))->method('get')->will($this->onConsecutiveCalls(igbinary_serialize('one value'), igbinary_serialize('two value'))); 231 | 232 | $cache = new VariableFrontend('VariableFrontend', $backend); 233 | $cache->initializeObject(); 234 | 235 | self::assertEquals($entries, $cache->getByTag($tag), 'Did not receive the expected entries'); 236 | } 237 | 238 | /** 239 | * @return AbstractBackend|\PHPUnit\Framework\MockObject\MockObject 240 | */ 241 | protected function prepareDefaultBackend() 242 | { 243 | return $this->getMockBuilder(AbstractBackend::class) 244 | ->setMethods(['get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage']) 245 | ->disableOriginalConstructor() 246 | ->getMock(); 247 | } 248 | 249 | /** 250 | * @param array $methods 251 | * @return AbstractBackend|\PHPUnit\Framework\MockObject\MockObject 252 | */ 253 | protected function prepareTaggableBackend(array $methods = ['get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage']) 254 | { 255 | return $this->getMockBuilder(NullBackend::class) 256 | ->setMethods($methods) 257 | ->disableOriginalConstructor() 258 | ->getMock(); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Tests/Unit/Psr/Cache/CachePoolTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(BackendInterface::class)->getMock(); 51 | $cachePool = new CachePool($identifier, $mockBackend); 52 | self::assertInstanceOf(CachePool::class, $cachePool); 53 | } 54 | 55 | public function invalidIdentifiersDataProvider(): array 56 | { 57 | return [ 58 | [''], 59 | ['späcialcharacters'], 60 | ['a-string-that-exceeds-the-maximum-allowed-length-of-twohundredandfifty-characters-which-is-pretty-large-as-it-turns-out-so-i-repeat-a-string-that-exceeds-the-maximum-allowed-length-of-twohundredandfifty-characters-still-not-there-wow-crazy-flow-rocks-though'], 61 | ]; 62 | } 63 | 64 | /** 65 | * @test 66 | * @dataProvider invalidIdentifiersDataProvider 67 | */ 68 | public function invalidIdentifiers(string $identifier): void 69 | { 70 | $mockBackend = $this->getMockBuilder(BackendInterface::class)->getMock(); 71 | 72 | $this->expectException(\InvalidArgumentException::class); 73 | new CachePool($identifier, $mockBackend); 74 | } 75 | 76 | 77 | 78 | /** 79 | * @test 80 | */ 81 | public function getItemChecksIfTheIdentifierIsValid() 82 | { 83 | $this->expectException(InvalidArgumentException::class); 84 | /** @var PsrFrontend|\PHPUnit\Framework\MockObject\MockObject $cache */ 85 | $cache = $this->getMockBuilder(CachePool::class) 86 | ->setMethods(['isValidEntryIdentifier']) 87 | ->disableOriginalConstructor() 88 | ->getMock(); 89 | $cache->expects(self::once())->method('isValidEntryIdentifier')->with('foo')->willReturn(false); 90 | $cache->getItem('foo'); 91 | } 92 | 93 | /** 94 | * @test 95 | */ 96 | public function savePassesSerializedStringToBackend() 97 | { 98 | $theString = 'Just some value'; 99 | $cacheItem = new CacheItem('PsrCacheTest', true, $theString); 100 | $backend = $this->prepareDefaultBackend(); 101 | $backend->expects(self::once())->method('set')->with(self::equalTo('PsrCacheTest'), self::equalTo(serialize($theString))); 102 | 103 | $cache = new CachePool('CachePool', $backend); 104 | $cache->save($cacheItem); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function savePassesSerializedArrayToBackend() 111 | { 112 | $theArray = ['Just some value', 'and another one.']; 113 | $cacheItem = new CacheItem('PsrCacheTest', true, $theArray); 114 | $backend = $this->prepareDefaultBackend(); 115 | $backend->expects(self::once())->method('set')->with(self::equalTo('PsrCacheTest'), self::equalTo(serialize($theArray))); 116 | 117 | $cache = new CachePool('CachePool', $backend); 118 | $cache->save($cacheItem); 119 | } 120 | 121 | /** 122 | * @test 123 | */ 124 | public function savePassesLifetimeToBackend() 125 | { 126 | // Note that this test can fail due to fraction of second problems in the calculation of lifetime vs. expiration date. 127 | $theString = 'Just some value'; 128 | $theLifetime = 1234; 129 | $cacheItem = new CacheItem('PsrCacheTest', true, $theString); 130 | $cacheItem->expiresAfter($theLifetime); 131 | $backend = $this->prepareDefaultBackend(); 132 | $backend->expects(self::once())->method('set')->with(self::equalTo('PsrCacheTest'), self::equalTo(serialize($theString)), self::equalTo([]), self::equalTo($theLifetime, 1)); 133 | 134 | $cache = new CachePool('CachePool', $backend); 135 | $cache->save($cacheItem); 136 | } 137 | 138 | /** 139 | * @test 140 | */ 141 | public function getItemFetchesValueFromBackend() 142 | { 143 | $theString = 'Just some value'; 144 | $backend = $this->prepareDefaultBackend(); 145 | $backend->expects(self::any())->method('get')->willReturn(serialize($theString)); 146 | 147 | $cache = new CachePool('CachePool', $backend); 148 | self::assertEquals(true, $cache->getItem('PsrCacheTest')->isHit(), 'The item should have been a hit but is not'); 149 | self::assertEquals($theString, $cache->getItem('PsrCacheTest')->get(), 'The returned value was not the expected string.'); 150 | } 151 | 152 | /** 153 | * @test 154 | */ 155 | public function getItemFetchesFalseBooleanValueFromBackend() 156 | { 157 | $backend = $this->prepareDefaultBackend(); 158 | $backend->expects(self::once())->method('get')->willReturn(serialize(false)); 159 | 160 | $cache = new CachePool('CachePool', $backend); 161 | $retrievedItem = $cache->getItem('PsrCacheTest'); 162 | self::assertEquals(true, $retrievedItem->isHit(), 'The item should have been a hit but is not'); 163 | self::assertEquals(false, $retrievedItem->get(), 'The returned value was not the false.'); 164 | } 165 | 166 | /** 167 | * @test 168 | */ 169 | public function hasItemReturnsResultFromBackend() 170 | { 171 | $backend = $this->prepareDefaultBackend(); 172 | $backend->expects(self::once())->method('has')->with(self::equalTo('PsrCacheTest'))->willReturn(true); 173 | 174 | $cache = new CachePool('CachePool', $backend); 175 | self::assertTrue($cache->hasItem('PsrCacheTest'), 'hasItem() did not return true.'); 176 | } 177 | 178 | /** 179 | * @test 180 | */ 181 | public function deleteItemCallsBackend() 182 | { 183 | $cacheIdentifier = 'someCacheIdentifier'; 184 | $backend = $this->prepareDefaultBackend(); 185 | 186 | $backend->expects(self::once())->method('remove')->with(self::equalTo($cacheIdentifier))->willReturn(true); 187 | 188 | $cache = new CachePool('CachePool', $backend); 189 | self::assertTrue($cache->deleteItem($cacheIdentifier), 'deleteItem() did not return true'); 190 | } 191 | 192 | /** 193 | * @return AbstractBackend|\PHPUnit\Framework\MockObject\MockObject 194 | */ 195 | protected function prepareDefaultBackend() 196 | { 197 | return $this->getMockBuilder(AbstractBackend::class) 198 | ->setMethods([ 199 | 'get', 200 | 'set', 201 | 'has', 202 | 'remove', 203 | 'findIdentifiersByTag', 204 | 'flush', 205 | 'flushByTag', 206 | 'collectGarbage' 207 | ]) 208 | ->disableOriginalConstructor() 209 | ->getMock(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Tests/Unit/Psr/SimpleCache/SimpleCacheFactoryTest.php: -------------------------------------------------------------------------------- 1 | mockEnvironmentConfiguration = $this->getMockBuilder(EnvironmentConfiguration::class) 29 | ->setMethods(null) 30 | ->setConstructorArgs([ 31 | __DIR__ . '~Testing', 32 | 'vfs://Temporary/Directory/', 33 | 1024 34 | ])->getMock(); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function createConstructsASimpleCache() 41 | { 42 | $simpleCacheFactory = new SimpleCacheFactory($this->mockEnvironmentConfiguration); 43 | $cache = $simpleCacheFactory->create('SimpleCacheTest', NullBackend::class); 44 | self::assertInstanceOf(CacheInterface::class, $cache, 'Cache was not an \Psr\SimpleCache\CacheInterface implementation.'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Unit/Psr/SimpleCache/SimpleCacheTest.php: -------------------------------------------------------------------------------- 1 | mockBackend = $this->getMockBuilder(BackendInterface::class)->getMock(); 26 | } 27 | 28 | /** 29 | * @param string $identifier 30 | * @return SimpleCache 31 | */ 32 | protected function createSimpleCache($identifier = 'SimpleCacheTest') 33 | { 34 | return new SimpleCache($identifier, $this->mockBackend); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function constructingWithInvalidIdentifierThrowsPsrInvalidArgumentException() 41 | { 42 | $this->expectException(InvalidArgumentException::class); 43 | $this->createSimpleCache('Invalid #*<>/()=?!'); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function setThrowsInvalidArgumentExceptionOnInvalidIdentifier() 50 | { 51 | $this->expectException(InvalidArgumentException::class); 52 | $simpleCache = $this->createSimpleCache(); 53 | $simpleCache->set('Invalid #*<>/()=?!', 'does not matter'); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function setThrowsExceptionOnBackendError() 60 | { 61 | $this->expectException(Exception::class); 62 | $this->mockBackend->expects(self::any())->method('set')->willThrowException(new Exception\InvalidDataException('Some other exception', 1234)); 63 | $simpleCache = $this->createSimpleCache(); 64 | $simpleCache->set('validkey', 'valid data'); 65 | } 66 | 67 | /** 68 | * @test 69 | */ 70 | public function setWillSetInBackendAndReturnBackendResponse() 71 | { 72 | $this->mockBackend->expects(self::any())->method('set'); 73 | $simpleCache = $this->createSimpleCache(); 74 | $result = $simpleCache->set('validkey', 'valid data'); 75 | self::assertEquals(true, $result); 76 | } 77 | 78 | /** 79 | * @test 80 | */ 81 | public function getThrowsInvalidArgumentExceptionOnInvalidIdentifier() 82 | { 83 | $this->expectException(InvalidArgumentException::class); 84 | $simpleCache = $this->createSimpleCache(); 85 | $simpleCache->get('Invalid #*<>/()=?!', false); 86 | } 87 | 88 | /** 89 | * @test 90 | */ 91 | public function getThrowsExceptionOnBackendError() 92 | { 93 | $this->expectException(Exception::class); 94 | $this->mockBackend->expects(self::any())->method('get')->willThrowException(new Exception\InvalidDataException('Some other exception', 1234)); 95 | $simpleCache = $this->createSimpleCache(); 96 | $simpleCache->get('validkey', false); 97 | } 98 | 99 | /** 100 | * @test 101 | */ 102 | public function getReturnsDefaultValueIfBackendFoundNoEntry() 103 | { 104 | $defaultValue = 'fallback'; 105 | $this->mockBackend->expects(self::any())->method('get')->willReturn(false); 106 | $simpleCache = $this->createSimpleCache(); 107 | $result = $simpleCache->get('validkey', $defaultValue); 108 | self::assertEquals($defaultValue, $result); 109 | } 110 | 111 | /** 112 | * Somewhat brittle test as we know that the cache serializes. Might want to extract that to a separate Serializer? 113 | * @test 114 | */ 115 | public function getReturnsBackendResponseAfterUnserialising() 116 | { 117 | $cachedValue = [1, 2, 3]; 118 | $this->mockBackend->expects(self::any())->method('get')->willReturn(serialize($cachedValue)); 119 | $simpleCache = $this->createSimpleCache(); 120 | $result = $simpleCache->get('validkey'); 121 | self::assertEquals($cachedValue, $result); 122 | } 123 | 124 | /** 125 | * @test 126 | */ 127 | public function deleteThrowsInvalidArgumentExceptionOnInvalidIdentifier() 128 | { 129 | $this->expectException(InvalidArgumentException::class); 130 | $simpleCache = $this->createSimpleCache(); 131 | $simpleCache->delete('Invalid #*<>/()=?!'); 132 | } 133 | 134 | /** 135 | * @test 136 | */ 137 | public function deleteThrowsExceptionOnBackendError() 138 | { 139 | $this->expectException(Exception::class); 140 | $this->mockBackend->expects(self::any())->method('remove')->willThrowException(new Exception\InvalidDataException('Some other exception', 1234)); 141 | $simpleCache = $this->createSimpleCache(); 142 | $simpleCache->delete('validkey'); 143 | } 144 | 145 | /** 146 | * @test 147 | */ 148 | public function getMultipleThrowsInvalidArgumentExceptionOnInvalidIdentifier() 149 | { 150 | $this->expectException(InvalidArgumentException::class); 151 | $simpleCache = $this->createSimpleCache(); 152 | $simpleCache->getMultiple(['validKey', 'Invalid #*<>/()=?!']); 153 | } 154 | 155 | /** 156 | * @test 157 | */ 158 | public function getMultipleGetsMultipleValues() 159 | { 160 | $this->mockBackend->expects(self::any())->method('get')->willReturnMap([ 161 | ['validKey', serialize('entry1')], 162 | ['another', serialize('entry2')] 163 | ]); 164 | $simpleCache = $this->createSimpleCache(); 165 | $result = $simpleCache->getMultiple(['validKey', 'another']); 166 | self::assertEquals(['validKey' => 'entry1', 'another' => 'entry2'], $result); 167 | } 168 | 169 | /** 170 | * @test 171 | */ 172 | public function getMultipleFillsWithDefault() 173 | { 174 | $this->mockBackend->expects(self::any())->method('get')->willReturnMap([ 175 | ['validKey', serialize('entry1')], 176 | ['notExistingEntry', false] 177 | ]); 178 | $simpleCache = $this->createSimpleCache(); 179 | $result = $simpleCache->getMultiple(['validKey', 'notExistingEntry'], 'FALLBACK'); 180 | self::assertEquals(['validKey' => 'entry1', 'notExistingEntry' => 'FALLBACK'], $result); 181 | } 182 | 183 | /** 184 | * @test 185 | */ 186 | public function setMultipleThrowsInvalidArgumentExceptionOnInvalidIdentifier() 187 | { 188 | $this->expectException(InvalidArgumentException::class); 189 | $simpleCache = $this->createSimpleCache(); 190 | $simpleCache->setMultiple(['validKey' => 'value', 'Invalid #*<>/()=?!' => 'value']); 191 | } 192 | 193 | /** 194 | * Moot test at the momment, as our backends never return so this is always true. 195 | * 196 | * @test 197 | */ 198 | public function setMultipleReturnsResult() 199 | { 200 | $this->mockBackend->expects(self::any())->method('set')->willReturnMap([ 201 | ['validKey', 'value', true], 202 | ['another', 'value', true] 203 | ]); 204 | 205 | $simpleCache = $this->createSimpleCache(); 206 | $result = $simpleCache->setMultiple(['validKey' => 'value', 'another' => 'value']); 207 | self::assertEquals(true, $result); 208 | } 209 | 210 | /** 211 | * @test 212 | */ 213 | public function deleteMultipleThrowsInvalidArgumentExceptionOnInvalidIdentifier() 214 | { 215 | $this->expectException(InvalidArgumentException::class); 216 | $simpleCache = $this->createSimpleCache(); 217 | $simpleCache->deleteMultiple(['validKey', 'Invalid #*<>/()=?!']); 218 | } 219 | 220 | /** 221 | * @test 222 | */ 223 | public function hasThrowsInvalidArgumentExceptionOnInvalidIdentifier() 224 | { 225 | $this->expectException(InvalidArgumentException::class); 226 | $simpleCache = $this->createSimpleCache(); 227 | $simpleCache->has('Invalid #*<>/()=?!'); 228 | } 229 | 230 | /** 231 | * @test 232 | */ 233 | public function hasReturnsWhatTheBackendSays() 234 | { 235 | $this->mockBackend->expects(self::any())->method('has')->willReturnMap([ 236 | ['existing', true], 237 | ['notExisting', false] 238 | ]); 239 | 240 | $simpleCache = $this->createSimpleCache(); 241 | $result = $simpleCache->has('existing'); 242 | self::assertEquals(true, $result); 243 | 244 | $result = $simpleCache->has('notExisting'); 245 | self::assertEquals(false, $result); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neos/cache", 3 | "description": "Neos Cache Framework", 4 | "type": "library", 5 | "homepage": "http://flow.neos.io", 6 | "license": ["MIT"], 7 | "require": { 8 | "php": "^8.0", 9 | "psr/simple-cache": "^2.0 || ^3.0", 10 | "psr/cache": "^2.0 || ^3.0", 11 | "psr/log": "^2.0 || ^3.0", 12 | "neos/utility-files": "self.version", 13 | "neos/utility-pdo": "self.version", 14 | "neos/utility-opcodecache": "self.version" 15 | }, 16 | "require-dev": { 17 | "mikey179/vfsstream": "^1.6.10", 18 | "phpunit/phpunit": "~9.1" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Neos\\Cache\\": "Classes" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Neos\\Cache\\Tests\\": "Tests" 28 | } 29 | }, 30 | "provide": { 31 | "psr/cache-implementation": "2.0.0 || 3.0.0", 32 | "psr/simple-cache-implementation": "2.0.0 || 3.0.0" 33 | }, 34 | "extra": { 35 | "neos": { 36 | "package-key": "Neos.Cache" 37 | } 38 | }, 39 | "suggest": { 40 | "ext-memcache": "If you have a memcache server and want to use it for caching.", 41 | "ext-memcached": "Alternative, if you have a memcache server and want to use it for caching.", 42 | "ext-redis": "If you have a redis server and want to use it for caching.", 43 | "ext-igbinary": "The igbinary extension is a replacement for the standard PHP serializer" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------