Completed
Push — master ( d594a1...81b2d1 )
by Matthias
02:04
created

SimpleCache::ttl()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 41
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 41
rs 8.439
cc 6
eloc 18
nc 6
nop 1
1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Psr16;
4
5
use DateInterval;
6
use DateTime;
7
use MatthiasMullie\Scrapbook\KeyValueStore;
8
use Psr\SimpleCache\CacheInterface;
9
use Traversable;
10
11
/**
12
 * @author Matthias Mullie <[email protected]>
13
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
14
 * @license LICENSE MIT
15
 */
16
class SimpleCache implements CacheInterface
17
{
18
    /**
19
     * List of invalid (or reserved) key characters.
20
     *
21
     * @var string
22
     */
23
    const KEY_INVALID_CHARACTERS = '{}()/\@:';
24
25
    /**
26
     * @var KeyValueStore
27
     */
28
    protected $store;
29
30
    /**
31
     * @param KeyValueStore $store
32
     */
33
    public function __construct(KeyValueStore $store)
34
    {
35
        $this->store = $store;
36
    }
37
38
    /**
39
     * {@inheritdoc}
40
     */
41
    public function get($key, $default = null)
42
    {
43
        $this->assertValidKey($key);
44
45
        // KeyValueStore::get returns false for cache misses (which could also
46
        // be confused for a `false` value), so we'll check existence with getMulti
47
        $multi = $this->store->getMulti(array($key));
48
49
        return isset($multi[$key]) ? $multi[$key] : $default;
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55
    public function set($key, $value, $ttl = null)
56
    {
57
        $this->assertValidKey($key);
58
        $ttl = $this->ttl($ttl);
59
60
        return $this->store->set($key, $value, $ttl);
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function delete($key)
67
    {
68
        $this->assertValidKey($key);
69
70
        $this->store->delete($key);
71
72
        // as long as the item is gone from the cache (even if it never existed
73
        // and delete failed because of that), we should return `true`
74
        return true;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function clear()
81
    {
82
        return $this->store->flush();
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function getMultiple($keys, $default = null)
89
    {
90
        if ($keys instanceof Traversable) {
91
            $keys = iterator_to_array($keys, false);
92
        }
93
94
        if (!is_array($keys)) {
95
            throw new InvalidArgumentException(
96
                'Invalid keys: '.var_export($keys, true).'. Keys should be an array or Traversable of strings.'
97
            );
98
        }
99
        array_map(array($this, 'assertValidKey'), $keys);
100
101
        $results = $this->store->getMulti($keys);
102
103
        // KeyValueStore omits values that are not in cache, while PSR-16 will
104
        // have them with a default value
105
        $nulls = array_fill_keys($keys, $default);
106
        $results = array_merge($nulls, $results);
107
108
        return $results;
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The return type of return $results; (array) is incompatible with the return type declared by the interface Psr\SimpleCache\CacheInterface::getMultiple of type Psr\SimpleCache\iterable.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function setMultiple($values, $ttl = null)
115
    {
116
        if ($values instanceof Traversable) {
117
            // we also need the keys, and an array is stricter about what it can
118
            // have as keys than a Traversable is, so we can't use
119
            // iterator_to_array...
120
            $array = array();
121
            foreach ($values as $key => $value) {
122
                if (!is_string($key) && !is_int($key)) {
123
                    throw new InvalidArgumentException(
124
                        'Invalid values: '.var_export($values, true).'. Only strings are allowed as keys.'
125
                    );
126
                }
127
                $array[$key] = $value;
128
            }
129
            $values = $array;
130
        }
131
132
        if (!is_array($values)) {
133
            throw new InvalidArgumentException(
134
                'Invalid values: '.var_export($values, true).'. Values should be an array or Traversable with strings as keys.'
135
            );
136
        }
137
138
        foreach ($values as $key => $value) {
139
            // $key is also allowed to be an integer, since ['0' => ...] will
140
            // automatically convert to [0 => ...]
0 ignored issues
show
Unused Code Comprehensibility introduced by Matthias Mullie
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
141
            $key = is_int($key) ? (string) $key : $key;
142
            $this->assertValidKey($key);
143
        }
144
145
        $ttl = $this->ttl($ttl);
146
        $success = $this->store->setMulti($values, $ttl);
147
148
        return !in_array(false, $success);
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154
    public function deleteMultiple($keys)
155
    {
156
        if ($keys instanceof Traversable) {
157
            $keys = iterator_to_array($keys, false);
158
        }
159
160
        if (!is_array($keys)) {
161
            throw new InvalidArgumentException(
162
                'Invalid keys: '.var_export($keys, true).'. Keys should be an array or Traversable of strings.'
163
            );
164
        }
165
        array_map(array($this, 'assertValidKey'), $keys);
166
167
        $this->store->deleteMulti($keys);
168
169
        // as long as the item is gone from the cache (even if it never existed
170
        // and delete failed because of that), we should return `true`
171
        return true;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function has($key)
178
    {
179
        $this->assertValidKey($key);
180
181
        // KeyValueStore::get returns false for cache misses (which could also
182
        // be confused for a `false` value), so we'll check existence with getMulti
183
        $multi = $this->store->getMulti(array($key));
184
185
        return isset($multi[$key]);
186
    }
187
188
    /**
189
     * Throws an exception if $key is invalid.
190
     *
191
     * @param string $key
192
     *
193
     * @throws InvalidArgumentException
194
     */
195
    protected function assertValidKey($key)
196
    {
197
        if (!is_string($key)) {
198
            throw new InvalidArgumentException(
199
                'Invalid key: '.var_export($key, true).'. Key should be a string.'
200
            );
201
        }
202
203
        if ($key === '') {
204
            throw new InvalidArgumentException(
205
                'Invalid key. Key should not be empty.'
206
            );
207
        }
208
209
        // valid key according to PSR-16 rules
210
        $invalid = preg_quote(static::KEY_INVALID_CHARACTERS, '/');
211
        if (preg_match('/['.$invalid.']/', $key)) {
212
            throw new InvalidArgumentException(
213
                'Invalid key: '.$key.'. Contains (a) character(s) reserved '.
214
                'for future extension: '.static::KEY_INVALID_CHARACTERS
215
            );
216
        }
217
    }
218
219
    /**
220
     * Accepts all TTL inputs valid in PSR-16 (null|int|DateInterval) and
221
     * converts them into TTL for KeyValueStore (int).
222
     *
223
     * @param null|int|DateInterval $ttl
224
     *
225
     * @return int
226
     *
227
     * @throws \TypeError
228
     */
229
    protected function ttl($ttl)
230
    {
231
        if ($ttl === null) {
232
            return 0;
233
        } elseif (is_int($ttl)) {
234
            /*
235
             * PSR-16 specifies that if `0` is provided, it must be treated as
236
             * expired, whereas KeyValueStore will interpret 0 to mean "expires
237
             * this second, but not at this exact time" (could vary a few ms).
238
             */
239
            if ($ttl === 0) {
240
                return -1;
241
            }
242
243
            /*
244
             * PSR-16 only accepts relative timestamps, whereas KeyValueStore
245
             * accepts both relative & absolute, depending on what the timestamp
246
             * is. We'll have to convert all ttls here to absolute, to make sure
247
             * KeyValueStore doesn't get confused.
248
             * @see https://github.com/dragoonis/psr-simplecache/issues/3
249
             */
250
            return $ttl + time();
251
        } elseif ($ttl instanceof DateInterval) {
252
            // convert DateInterval to integer by adding it to a 0 DateTime
253
            $datetime = new DateTime();
254
            $datetime->setTimestamp(0);
255
            $datetime->add($ttl);
256
257
            return time() + (int) $datetime->format('U');
258
        }
259
260
        $error = 'Invalid time: '.serialize($ttl).'. Must be integer or '.
261
            'instance of DateInterval.';
262
263
        if (class_exists('\TypeError')) {
264
            throw new \TypeError($error);
265
        }
266
        trigger_error($error, E_USER_ERROR);
267
268
        return 0;
269
    }
270
}
271