=> array( * => array( * 0 => * 1 => * ['tags' => ] * ) * ) * ) */ private array $data = []; /** * Set options. * * @see getOptions() * * @param array|Traversable|MemoryOptions $options * @return Memory */ public function setOptions($options) { if (! $options instanceof MemoryOptions) { $options = new MemoryOptions($options); } return parent::setOptions($options); } /** * Get options. * * @see setOptions() * * @return MemoryOptions */ public function getOptions() { if (! $this->options) { $this->setOptions(new MemoryOptions()); } return $this->options; } /* TotalSpaceCapableInterface */ /** * Get total space in bytes * * @return int */ public function getTotalSpace() { return $this->getOptions()->getMemoryLimit(); } /* AvailableSpaceCapableInterface */ /** * Get available space in bytes * * @return int|float */ public function getAvailableSpace() { $total = $this->getOptions()->getMemoryLimit(); $avail = $total - (float) memory_get_usage(true); return $avail > 0 ? $avail : 0; } /* IterableInterface */ /** * Get the storage iterator * * @return KeyListIterator */ public function getIterator(): Traversable { $ns = $this->getOptions()->getNamespace(); $keys = []; if (isset($this->data[$ns])) { foreach ($this->data[$ns] as $key => &$tmp) { if ($this->internalHasItem($key)) { $keys[] = $key; } } } return new KeyListIterator($this, $keys); } /* FlushableInterface */ /** * Flush the whole storage * * @return bool */ public function flush() { $this->data = []; return true; } /* ClearExpiredInterface */ /** * Remove expired items * * @return bool */ public function clearExpired() { $ttl = $this->getOptions()->getTtl(); if ($ttl <= 0) { return true; } $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns])) { return true; } $data = &$this->data[$ns]; foreach ($data as $key => &$item) { if (microtime(true) >= $data[$key][1] + $ttl) { unset($data[$key]); } } return true; } /** * Remove items of given namespace * * @param string $namespace * @return bool */ public function clearByNamespace($namespace) { $namespace = (string) $namespace; if ($namespace === '') { throw new Exception\InvalidArgumentException('No namespace given'); } unset($this->data[$namespace]); return true; } /** * Remove items matching given prefix * * @param string $prefix * @return bool */ public function clearByPrefix($prefix) { $prefix = (string) $prefix; if ($prefix === '') { throw new Exception\InvalidArgumentException('No prefix given'); } $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns])) { return true; } $data = &$this->data[$ns]; foreach ($data as $key => &$item) { if (strpos($key, $prefix) === 0) { unset($data[$key]); } } return true; } /* TaggableInterface */ /** * Set tags to an item by given key. * An empty array will remove all tags. * * @param string $key * @param string[] $tags * @return bool */ public function setTags($key, array $tags) { $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns][$key])) { return false; } $this->data[$ns][$key]['tags'] = $tags; return true; } /** * Get tags of an item by given key * * @param string $key * @return string[]|FALSE */ public function getTags($key) { $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns][$key])) { return false; } return $this->data[$ns][$key]['tags'] ?? []; } /** * Remove items matching given tags. * * If $disjunction only one of the given tags must match * else all given tags must match. * * @param string[] $tags * @param bool $disjunction * @return bool */ public function clearByTags(array $tags, $disjunction = false) { $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns])) { return true; } $tagCount = count($tags); $data = &$this->data[$ns]; foreach ($data as $key => &$item) { if (isset($item['tags'])) { $diff = array_diff($tags, $item['tags']); if (($disjunction && count($diff) < $tagCount) || (! $disjunction && ! $diff)) { unset($data[$key]); } } } return true; } /* reading */ /** * Internal method to get an item. * * @param string $normalizedKey * @param bool $success * @param mixed $casToken * @return mixed Data on success, null on failure * @throws Exception\ExceptionInterface */ protected function internalGetItem(&$normalizedKey, &$success = null, &$casToken = null) { $options = $this->getOptions(); $ns = $options->getNamespace(); $success = isset($this->data[$ns][$normalizedKey]); if ($success) { $data = &$this->data[$ns][$normalizedKey]; $ttl = $options->getTtl(); if ($ttl && microtime(true) >= $data[1] + $ttl) { $success = false; } } if (! $success) { return; } $casToken = $data[0]; return $data[0]; } /** * Internal method to get multiple items. * * @param array $normalizedKeys * @return array Associative array of keys and values * @throws Exception\ExceptionInterface */ protected function internalGetItems(array &$normalizedKeys) { $options = $this->getOptions(); $ns = $options->getNamespace(); if (! isset($this->data[$ns])) { return []; } $data = &$this->data[$ns]; $ttl = $options->getTtl(); $now = microtime(true); $result = []; foreach ($normalizedKeys as $normalizedKey) { if (isset($data[$normalizedKey])) { if (! $ttl || $now < $data[$normalizedKey][1] + $ttl) { $result[$normalizedKey] = $data[$normalizedKey][0]; } } } return $result; } /** * Internal method to test if an item exists. * * @param string $normalizedKey * @return bool */ protected function internalHasItem(&$normalizedKey) { $options = $this->getOptions(); $ns = $options->getNamespace(); if (! isset($this->data[$ns][$normalizedKey])) { return false; } // check if expired $ttl = $options->getTtl(); if ($ttl && microtime(true) >= $this->data[$ns][$normalizedKey][1] + $ttl) { return false; } return true; } /** * Internal method to test multiple items. * * @param array $normalizedKeys * @return array Array of found keys */ protected function internalHasItems(array &$normalizedKeys) { $options = $this->getOptions(); $ns = $options->getNamespace(); if (! isset($this->data[$ns])) { return []; } $data = &$this->data[$ns]; $ttl = $options->getTtl(); $now = microtime(true); $result = []; foreach ($normalizedKeys as $normalizedKey) { if (isset($data[$normalizedKey])) { if (! $ttl || $now < $data[$normalizedKey][1] + $ttl) { $result[] = $normalizedKey; } } } return $result; } /** * Get metadata of an item. * * @param string $normalizedKey * @return array|bool Metadata on success, false on failure * @throws Exception\ExceptionInterface * @triggers getMetadata.pre(PreEvent) * @triggers getMetadata.post(PostEvent) * @triggers getMetadata.exception(ExceptionEvent) */ protected function internalGetMetadata(&$normalizedKey) { if (! $this->internalHasItem($normalizedKey)) { return false; } $ns = $this->getOptions()->getNamespace(); return [ 'mtime' => $this->data[$ns][$normalizedKey][1], ]; } /* writing */ /** * Internal method to store an item. * * @param string $normalizedKey * @param mixed $value * @return bool * @throws Exception\ExceptionInterface */ protected function internalSetItem(&$normalizedKey, &$value) { $options = $this->getOptions(); if (! $this->hasAvailableSpace()) { $memoryLimit = $options->getMemoryLimit(); throw new Exception\OutOfSpaceException( "Memory usage exceeds limit ({$memoryLimit})." ); } $ns = $options->getNamespace(); $this->data[$ns][$normalizedKey] = [$value, microtime(true)]; return true; } /** * Internal method to store multiple items. * * @param array $normalizedKeyValuePairs * @return array Array of not stored keys * @throws Exception\ExceptionInterface */ protected function internalSetItems(array &$normalizedKeyValuePairs) { $options = $this->getOptions(); if (! $this->hasAvailableSpace()) { $memoryLimit = $options->getMemoryLimit(); throw new Exception\OutOfSpaceException( "Memory usage exceeds limit ({$memoryLimit})." ); } $ns = $options->getNamespace(); if (! isset($this->data[$ns])) { $this->data[$ns] = []; } $data = &$this->data[$ns]; $now = microtime(true); foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { $data[$normalizedKey] = [$value, $now]; } return []; } /** * Add an item. * * @param string $normalizedKey * @param mixed $value * @return bool * @throws Exception\ExceptionInterface */ protected function internalAddItem(&$normalizedKey, &$value) { $options = $this->getOptions(); if (! $this->hasAvailableSpace()) { $memoryLimit = $options->getMemoryLimit(); throw new Exception\OutOfSpaceException( "Memory usage exceeds limit ({$memoryLimit})." ); } $ns = $options->getNamespace(); if (isset($this->data[$ns][$normalizedKey])) { return false; } $this->data[$ns][$normalizedKey] = [$value, microtime(true)]; return true; } /** * Internal method to add multiple items. * * @param array $normalizedKeyValuePairs * @return array Array of not stored keys * @throws Exception\ExceptionInterface */ protected function internalAddItems(array &$normalizedKeyValuePairs) { $options = $this->getOptions(); if (! $this->hasAvailableSpace()) { $memoryLimit = $options->getMemoryLimit(); throw new Exception\OutOfSpaceException( "Memory usage exceeds limit ({$memoryLimit})." ); } $ns = $options->getNamespace(); if (! isset($this->data[$ns])) { $this->data[$ns] = []; } $result = []; $data = &$this->data[$ns]; $now = microtime(true); foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { if (isset($data[$normalizedKey])) { $result[] = $normalizedKey; } else { $data[$normalizedKey] = [$value, $now]; } } return $result; } /** * Internal method to replace an existing item. * * @param string $normalizedKey * @param mixed $value * @return bool * @throws Exception\ExceptionInterface */ protected function internalReplaceItem(&$normalizedKey, &$value) { $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns][$normalizedKey])) { return false; } $this->data[$ns][$normalizedKey] = [$value, microtime(true)]; return true; } /** * Internal method to replace multiple existing items. * * @param array $normalizedKeyValuePairs * @return array Array of not stored keys * @throws Exception\ExceptionInterface */ protected function internalReplaceItems(array &$normalizedKeyValuePairs) { $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns])) { return array_keys($normalizedKeyValuePairs); } $result = []; $data = &$this->data[$ns]; foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { if (! isset($data[$normalizedKey])) { $result[] = $normalizedKey; } else { $data[$normalizedKey] = [$value, microtime(true)]; } } return $result; } /** * Internal method to reset lifetime of an item * * @param string $normalizedKey * @return bool * @throws Exception\ExceptionInterface */ protected function internalTouchItem(&$normalizedKey) { $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns][$normalizedKey])) { return false; } $this->data[$ns][$normalizedKey][1] = microtime(true); return true; } /** * Internal method to remove an item. * * @param string $normalizedKey * @return bool * @throws Exception\ExceptionInterface */ protected function internalRemoveItem(&$normalizedKey) { $ns = $this->getOptions()->getNamespace(); if (! isset($this->data[$ns][$normalizedKey])) { return false; } unset($this->data[$ns][$normalizedKey]); // remove empty namespace if (! $this->data[$ns]) { unset($this->data[$ns]); } return true; } /** * Internal method to increment an item. * * @param string $normalizedKey * @param int $value * @return int|bool The new value on success, false on failure * @throws Exception\ExceptionInterface */ protected function internalIncrementItem(&$normalizedKey, &$value) { $ns = $this->getOptions()->getNamespace(); if (isset($this->data[$ns][$normalizedKey])) { $data = &$this->data[$ns][$normalizedKey]; $data[0] += $value; $data[1] = microtime(true); $newValue = $data[0]; } else { // initial value $newValue = $value; $this->data[$ns][$normalizedKey] = [$newValue, microtime(true)]; } return $newValue; } /** * Internal method to decrement an item. * * @param string $normalizedKey * @param int $value * @return int|bool The new value on success, false on failure * @throws Exception\ExceptionInterface */ protected function internalDecrementItem(&$normalizedKey, &$value) { $ns = $this->getOptions()->getNamespace(); if (isset($this->data[$ns][$normalizedKey])) { $data = &$this->data[$ns][$normalizedKey]; $data[0] -= $value; $data[1] = microtime(true); $newValue = $data[0]; } else { // initial value $newValue = -$value; $this->data[$ns][$normalizedKey] = [$newValue, microtime(true)]; } return $newValue; } /* status */ /** * Internal method to get capabilities of this adapter * * @return Capabilities */ protected function internalGetCapabilities() { if ($this->capabilities === null) { $this->capabilityMarker = new stdClass(); $this->capabilities = new Capabilities( $this, $this->capabilityMarker, [ 'supportedDatatypes' => [ 'NULL' => true, 'boolean' => true, 'integer' => true, 'double' => true, 'string' => true, 'array' => true, 'object' => true, 'resource' => true, ], 'supportedMetadata' => ['mtime'], 'minTtl' => 1, 'maxTtl' => PHP_INT_MAX, 'staticTtl' => false, 'ttlPrecision' => 1, 'maxKeyLength' => 0, 'namespaceIsPrefix' => false, 'namespaceSeparator' => '', ] ); } return $this->capabilities; } /* internal */ /** * Has space available to store items? * * @return bool */ protected function hasAvailableSpace() { $total = $this->getOptions()->getMemoryLimit(); // check memory limit disabled if ($total <= 0) { return true; } $free = $total - (float) memory_get_usage(true); return $free > 0; } }