aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/StudipCachedArray.php
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:07:19 +0200
committerJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:19:12 +0200
commita3da1483a9e689846179159355badfec8073dbec (patch)
tree770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /lib/classes/StudipCachedArray.php
current code from svn, revision 62608
Diffstat (limited to 'lib/classes/StudipCachedArray.php')
-rw-r--r--lib/classes/StudipCachedArray.php281
1 files changed, 281 insertions, 0 deletions
diff --git a/lib/classes/StudipCachedArray.php b/lib/classes/StudipCachedArray.php
new file mode 100644
index 0000000..1d07ecf
--- /dev/null
+++ b/lib/classes/StudipCachedArray.php
@@ -0,0 +1,281 @@
+<?php
+/**
+ * This class represents an array in cache and removes the neccessity to
+ * encode/decode and store the data after every change.
+ *
+ * Caching is handled by splitting the data into partitions based on the key.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license GPL2 or any later version
+ * @since Stud.IP 5.0
+ *
+ * @todo Automagic partition size?
+ */
+class StudipCachedArray implements ArrayAccess, Countable
+{
+ const ENCODE_JSON = 0;
+ const ENCODE_SERIALIZE = 1;
+
+ protected $key;
+ protected $cache;
+ protected $partitions;
+ protected $partition_by;
+ protected $encoding;
+
+ protected $data = [];
+
+ /**
+ * Constructs the cached array
+ *
+ * @param string $key Cache key where the array is/should be stored
+ * @param mixed $partition_by Defines the partitioning, this may either be
+ * an int which will be length of the substring
+ * of the given chache offset or a callable which
+ * will return the partition key.
+ */
+ public function __construct($key, $partition_by = 1, $encoding = self::ENCODE_JSON, StudipCache $cache = null)
+ {
+ if (!is_callable($partition_by) && !is_int($partition_by)) {
+ throw new Exception('Parameter $partition_by may only be a number or a callable if set');
+ }
+ if (ctype_digit($partition_by) && $partition_by <= 1) {
+ throw new Exception('Parameter $partition_by must be positive and not zero');
+ }
+
+ $this->key = $key;
+ $this->partition_by = $partition_by;
+ $this->encoding = $encoding;
+ $this->setCache($cache ?? StudipCacheFactory::getCache());
+ }
+
+ /**
+ * Sets the cache for this array and resets internal states.
+ *
+ * @param StudipCache $cache
+ */
+ public function setCache(StudipCache $cache)
+ {
+ $this->cache = $cache;
+ $this->reset();
+ }
+
+ /**
+ * Determines whether an offset exists in the array.
+ *
+ * @param string $offset Offset
+ * @return bool
+ */
+ public function offsetExists($offset)
+ {
+ return array_key_exists(
+ $offset,
+ $this->loadData($offset)
+ );
+ }
+
+ /**
+ * Returns the value at given offset or null if it doesn't exist.
+ *
+ * @param string $offset Offset
+ * @return mixed
+ */
+ public function offsetGet($offset)
+ {
+ return $this->loadData($offset)[$offset] ?? null;
+ }
+
+ /**
+ * Sets the value for a given offset.
+ *
+ * @param string $offset Offset
+ * @param mixed $value Value
+ */
+ public function offsetSet($offset, $value)
+ {
+ $this->loadData($offset)[$offset] = $value;
+
+ $this->storeData($offset);
+ }
+
+ /**
+ * Unsets the value at a given offset
+ *
+ * @param string $offset Offset
+ */
+ public function offsetUnset($offset)
+ {
+ $data = &$this->loadData($offset);
+
+ if (array_key_exists($offset, $data)) {
+ unset($data[$offset]);
+ $this->storeData($offset);
+ }
+ }
+
+ /**
+ * Counts all values in chache.
+ * @return int
+ */
+ public function count()
+ {
+ return array_sum($this->partitions);
+ }
+
+ /**
+ * Clears all data.
+ */
+ public function clear()
+ {
+ foreach (array_keys($this->partitions) as $partition) {
+ $this->cache->expire($this->getPartitionKey($partition));
+ }
+
+ $this->partitions = [];
+ $this->data = [];
+
+ $this->storeData();
+ }
+
+ /**
+ * Returns all items in cache as an array.
+ * @return array
+ */
+ public function getArrayCopy()
+ {
+ $result = [];
+ foreach (array_keys($this->partitions) as $partition) {
+ foreach ($this->loadData($partition) as $key => $value) {
+ $result[$key] = $value;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Loads all partitions of this cache and resets the data array
+ * @return array
+ */
+ public function reset()
+ {
+ $this->data = [];
+ $this->partitions = [];
+
+ $cached = $this->cache->read($this->key);
+ if ($cached) {
+ $this->partitions = $this->decode($cached);
+ }
+ }
+
+ /**
+ * Loads the data from cache.
+ */
+ protected function &loadData($offset)
+ {
+ $partition = $this->getPartition($offset);
+ if (!isset($this->data[$partition])) {
+ $cached = $this->cache->read($this->getPartitionKey($offset));
+ if ($cached) {
+ $this->data[$partition] = $this->decode($cached);
+ } else {
+ $this->data[$partition] = [];
+ }
+ }
+
+ return $this->data[$partition];
+ }
+
+ /**
+ * Stores the data back to the cache.
+ */
+ protected function storeData($offset = null)
+ {
+ $partition = false;
+
+ if ($offset !== null) {
+ $partition = $this->getPartition($offset);
+ if (!array_key_exists($partition, $this->partitions) || count($this->data[$partition]) > 0) {
+ $this->partitions[$partition] = count($this->data[$partition]);
+ } elseif (array_key_exists($partition, $this->partitions) && count($this->data[$partition]) === 0) {
+ unset($this->partitions[$partition]);
+ }
+ }
+
+ foreach ($this->data as $p => $data) {
+ if ($partition === false || $p == $partition) {
+ $key = $this->getPartitionKey($p);
+
+ if (count($data) === 0) {
+ $this->cache->expire($key);
+ } else {
+ $this->cache->write($key, $this->encode($data));
+ }
+ }
+ }
+
+ if (count($this->partitions) === 0) {
+ $this->cache->expire($this->key);
+ } else {
+ $this->cache->write($this->key, $this->encode($this->partitions));
+ }
+ }
+
+ /**
+ * Encodes the given data based on the set encoding mechanism.
+ * @param mixed $data
+ * @return string
+ */
+ protected function encode($data)
+ {
+ if ($this->encoding === self::ENCODE_JSON) {
+ return json_encode($data);
+ }
+
+ if ($this->encoding === self::ENCODE_SERIALIZE) {
+ return serialize($data);
+ }
+
+ throw new Exception('Unknown encoding type');
+ }
+
+ /**
+ * Decodes the given data based on the set encoding mechanism.
+ * @param string $data
+ * @return mixed
+ */
+ protected function decode($data)
+ {
+ if ($this->encoding === self::ENCODE_JSON) {
+ return json_decode($data, true);
+ }
+
+ if ($this->encoding === self::ENCODE_SERIALIZE) {
+ return unserialize($data);
+ }
+
+ throw new Exception('Unknown decoding type');
+ }
+
+ /**
+ * Extracts the partition from the key.
+ * @param string $offset
+ * @return string partition
+ */
+ protected function getPartition($offset)
+ {
+ if (is_callable($this->partition_by)) {
+ return call_user_func($this->partition_by, $offset);
+ }
+ return mb_substr($offset, 0, $this->partition_by);
+ }
+
+ /**
+ * Returns the partition key for storage.
+ *
+ * @param string $offset
+ * @return string
+ */
+ protected function getPartitionKey($offset)
+ {
+ return rtrim($this->key, '/') . '/' . $this->getPartition($offset);
+ }
+}