aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/StudipCachedArray.php
blob: 18bb55b70868d87875eec974c4a8a25f12cff3ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<?php
/**
 * This class represents an array in cache and removes the neccessity to
 * encode/decode and store the data after every change.
 *
 * @author  Jan-Hendrik Willms <tleilax+studip@gmail.com>
 * @license GPL2 or any later version
 * @since   Stud.IP 5.0
 */
class StudipCachedArray implements ArrayAccess
{
    protected $key;
    protected $cache;
    protected $duration;

    protected $data = [];

    protected $hash;

    /**
     * Constructs the cached array
     *
     * @param string $key      Cache key where the array is/should be stored
     *                         an int which will be length of the substring
     *                         of the given chache offset or a callable which
     *                         will return the partition key.
     * @param int    $duration Duration in seconds for which the item shall be
     *                         stored
     */
    public function __construct(string $key, int $duration = StudipCache::DEFAULT_EXPIRATION)
    {
        $this->key = self::class . "/{$key}";
        $this->cache = StudipCacheFactory::getCache();
        $this->duration = $duration;
        $this->hash = $this->getHash();

        $this->reset();
    }

    /**
     * Clears cached values from memory, but does not remove them from the cache.
     */
    public function reset(): void
    {
        $this->data = [];
    }

    /**
     * Removes all values from the cache.
     */
    public function expire(): void
    {
        $this->hash = $this->getHash(true);
        $this->reset();
    }

    /**
     * Determines whether an offset exists in the array.
     *
     * @param string $offset Offset
     *
     * @return bool
     */
    public function offsetExists($offset): bool
    {
        $this->loadData($offset);
        return isset($this->data[$offset]);
    }

    /**
     * Returns the value at given offset or null if it doesn't exist.
     *
     * @param string $offset Offset
     *
     * @return mixed
     *
     * @todo Add mixed return type when Stud.IP requires PHP8 minimal
     */
    #[ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        $this->loadData($offset);
        return $this->data[$offset];
    }

    /**
     * Sets the value for a given offset.
     *
     * @param string $offset Offset
     * @param mixed  $value  Value
     */
    public function offsetSet($offset, $value): void
    {
        if ($offset === null) {
            throw new Exception('Cannot push to cached array, use correct offset instead');
        }

        if (!isset($this->data[$offset]) || $this->data[$offset] !== $value) {
            $this->data[$offset] = $value;

            $this->storeData($offset);
        }
    }

    /**
     * Unsets the value at a given offset
     *
     * @param string $offset Offset
     */
    public function offsetUnset($offset): void
    {
        $this->cache->expire($this->getCacheKey($offset));
        unset($this->data[$offset]);
    }

    /**
     * Loads the data from cache.
     *
     * @param string $offset Offset to load
     */
    protected function loadData(string $offset)
    {
        if (!array_key_exists($offset, $this->data)) {
            $cached = $this->cache->read($this->getCacheKey($offset));
            $this->data[$offset] = $this->swapNullAndFalse($cached);
        }

        return $this->data[$offset];
    }

    /**
     * Stores the data back to the cache.
     * Data needs to be wrapped in another array so that we can correctly read
     * back a value of "false".
     *
     * @param string $offset Offset to store
     */
    protected function storeData(string $offset): void
    {
        $data = $this->swapNullAndFalse($this->data[$offset]);

        $this->cache->write(
            $this->getCacheKey($offset),
            $data,
            $this->duration
        );
    }

    /**
     * Returns the cache key for a specific offset.
     *
     * @param string $offset Offset of the cached item
     *
     * @return string
     */
    private function getCacheKey(string $offset): string
    {
        $key = rtrim($this->key, '/');
        if ($this->hash) {
            $key .= "/{$this->hash}";
        }
        $key .= "/{$offset}";

        return $key;
    }

    /**
     * Swaps null and false for a value because the Stud.IP cache will return
     * false if a cached item is not found instead of null.
     *
     * @param mixed $value Value to swap if appropriate
     *
     * @return mixed
     */
    private function swapNullAndFalse($value)
    {
        if ($value === null) {
            return false;
        }

        if ($value === false) {
            return null;
        }

        return $value;
    }

    /**
     * Loads or creates and stores a hash for this cached array.
     *
     * @return string
     */
    private function getHash(bool $recreate = false): string
    {
        if (!$recreate) {
            $hash = $this->cache->read($this->key);
            return $hash === false ? '' : $hash;
        }

        $hash = md5(uniqid(__CLASS__, true));
        $this->cache->write($this->key, $hash);
        return $hash;
    }
}