Completed
Push — 7.4 ( abf332 )
by Nikolaos
16:13
created

Stream::getPayload()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 43
ccs 0
cts 27
cp 0
rs 8.6097
c 0
b 0
f 0
cc 6
nc 7
nop 1
crap 42
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Phalcon\Storage\Adapter;
15
16
use DateInterval;
17
use FilesystemIterator;
18
use Iterator;
19
use Phalcon\Factory\Exception as FactoryException;
20
use Phalcon\Helper\Arr;
21
use Phalcon\Helper\Str;
22
use Phalcon\Storage\Exception;
23
use Phalcon\Storage\SerializerFactory;
24
use RecursiveDirectoryIterator;
25
use RecursiveIteratorIterator;
26
27
use function fclose;
28
use function file_exists;
29
use function file_get_contents;
30
use function file_put_contents;
31
use function flock;
32
use function fopen;
33
use function is_array;
34
use function is_dir;
35
use function mkdir;
36
use function restore_error_handler;
37
use function serialize;
38
use function set_error_handler;
39
use function str_replace;
40
use function time;
41
use function unlink;
42
use function unserialize;
43
44
use const E_NOTICE;
45
use const LOCK_EX;
46
use const LOCK_SH;
47
48
/**
49
 * Stream adapter
50
 *
51
 * @property string $storageDir
52
 * @property array  $options
53
 */
54
class Stream extends AbstractAdapter
55
{
56
    /**
57
     * @var string
58
     */
59
    protected $storageDir = "";
60
61
    /**
62
     * @var array
63
     */
64
    protected $options = [];
65
66
    /**
67
     * Stream constructor.
68
     *
69
     * @param SerializerFactory $factory
70
     * @param array             $options = [
71
     *                                   'storageDir'        => '',
72
     *                                   'defaultSerializer' => 'Php',
73
     *                                   'lifetime'          => 3600,
74
     *                                   'prefix'            => ''
75
     *                                   ]
76
     *
77
     * @throws Exception
78
     * @throws FactoryException
79
     */
80
    public function __construct(
81
        SerializerFactory $factory,
82
        array $options = []
83
    ) {
84
        $storageDir = Arr::get($options, "storageDir", "");
85
        if (empty($storageDir)) {
86
            throw new Exception(
87
                "The 'storageDir' must be specified in the options"
88
            );
89
        }
90
91
        /**
92
         * Lets set some defaults and options here
93
         */
94
        $this->storageDir = Str::dirSeparator($storageDir);
95
        $this->prefix     = "ph-strm";
96
        $this->options    = $options;
97
98
        parent::__construct($factory, $options);
99
100
        $this->initSerializer();
101
    }
102
103
    /**
104
     * Flushes/clears the cache
105
     */
106
    public function clear(): bool
107
    {
108
        $result    = true;
109
        $directory = Str::dirSeparator($this->storageDir);
110
        $iterator  = $this->getIterator($directory);
111
112
        foreach ($iterator as $file) {
113
            if ($file->isFile() && !unlink($file->getPathName())) {
114
                $result = false;
115
            }
116
        }
117
118
        return $result;
119
    }
120
121
    /**
122
     * Decrements a stored number
123
     *
124
     * @param string $key
125
     * @param int    $value
126
     *
127
     * @return bool|int
128
     */
129
    public function decrement(string $key, int $value = 1)
130
    {
131
        if (!$this->has($key)) {
132
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Phalcon\Storage\Adapter\...terInterface::decrement of type integer.

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...
133
        }
134
135
        $data = $this->get($key);
136
        $data = (int) $data - $value;
137
138
        return $this->set($key, $data);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->set($key, $data); (boolean) is incompatible with the return type declared by the interface Phalcon\Storage\Adapter\...terInterface::decrement of type integer.

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...
139
    }
140
141
    /**
142
     * Reads data from the adapter
143
     *
144
     * @param string $key
145
     *
146
     * @return bool
147
     */
148
    public function delete(string $key): bool
149
    {
150
        if (!$this->has($key)) {
151
            return false;
152
        }
153
154
        $filepath = $this->getFilepath($key);
155
156
        return unlink($filepath);
157
    }
158
159
    /**
160
     * Reads data from the adapter
161
     *
162
     * @param string     $key
163
     * @param mixed|null $defaultValue
164
     *
165
     * @return mixed|null
166
     */
167
    public function get(string $key, $defaultValue = null)
168
    {
169
        $filepath = $this->getFilepath($key);
170
171
        if (!file_exists($filepath)) {
172
            return $defaultValue;
173
        }
174
175
        $payload = $this->getPayload($filepath);
176
177
        if (empty($payload)) {
178
            return $defaultValue;
179
        }
180
181
        if ($this->isExpired($payload)) {
182
            return $defaultValue;
183
        }
184
185
        $content = Arr::get($payload, "content", null);
186
187
        return $this->getUnserializedData($content, $defaultValue);
188
    }
189
190
    /**
191
     * Always returns null
192
     *
193
     * @return null
194
     */
195
    public function getAdapter()
196
    {
197
        return $this->adapter;
198
    }
199
200
    /**
201
     * Stores data in the adapter
202
     *
203
     * @param string $prefix
204
     *
205
     * @return array
206
     */
207
    public function getKeys(string $prefix = ""): array
208
    {
209
        $files     = [];
210
        $directory = $this->getDir();
211
212
        if (!file_exists($directory)) {
213
            return [];
214
        }
215
216
        $iterator = $this->getIterator($directory);
217
218
        foreach ($iterator as $file) {
219
            if ($file->isFile()) {
220
                $files[] = $this->prefix . $file->getFilename();
221
            }
222
        }
223
224
        return $this->getFilteredKeys($files, $prefix);
225
    }
226
227
    /**
228
     * Checks if an element exists in the cache and is not expired
229
     *
230
     * @param string $key
231
     *
232
     * @return bool
233
     */
234
    public function has(string $key): bool
235
    {
236
        $filepath = $this->getFilepath($key);
237
238
        if (!file_exists($filepath)) {
239
            return false;
240
        }
241
242
        $payload = $this->getPayload($filepath);
243
244
        if (empty($payload)) {
245
            return false;
246
        }
247
248
        return !$this->isExpired($payload);
249
    }
250
251
    /**
252
     * Increments a stored number
253
     *
254
     * @param string $key
255
     * @param int    $value
256
     *
257
     * @return bool|int
258
     * @throws \Exception
259
     */
260
    public function increment(string $key, int $value = 1)
261
    {
262
        if (!$this->has($key)) {
263
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Phalcon\Storage\Adapter\...terInterface::increment of type integer.

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...
264
        }
265
266
        $data = $this->get($key);
267
        $data = (int) $data + $value;
268
269
        return $this->set($key, $data);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->set($key, $data); (boolean) is incompatible with the return type declared by the interface Phalcon\Storage\Adapter\...terInterface::increment of type integer.

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...
270
    }
271
272
    /**
273
     * Stores data in the adapter
274
     *
275
     * @param string                $key
276
     * @param mixed                 $value
277
     * @param DateInterval|int|null $ttl
278
     *
279
     * @return bool
280
     * @throws \Exception
281
     */
282
    public function set(string $key, $value, $ttl = null): bool
283
    {
284
        $payload   = [
285
            "created" => time(),
286
            "ttl"     => $this->getTtl($ttl),
287
            "content" => $this->getSerializedData($value),
288
        ];
289
        $payload   = serialize($payload);
290
        $directory = $this->getDir($key);
291
292
        if (!is_dir($directory)) {
293
            mkdir($directory, 0777, true);
294
        }
295
296
        return false !== file_put_contents($directory . $key, $payload, LOCK_EX);
297
    }
298
299
    /**
300
     * Returns the folder based on the storageDir and the prefix
301
     *
302
     * @param string $key
303
     *
304
     * @return string
305
     */
306
    private function getDir(string $key = ""): string
307
    {
308
        $dirPrefix   = Str::dirSeparator($this->storageDir . $this->prefix);
309
        $dirFromFile = Str::dirFromFile(
310
            str_replace($this->prefix, "", $key)
311
        );
312
313
        return Str::dirSeparator($dirPrefix . $dirFromFile);
314
    }
315
316
    /**
317
     * Returns the full path to the file
318
     *
319
     * @param string $key
320
     *
321
     * @return string
322
     */
323
    private function getFilepath(string $key): string
324
    {
325
        return $this->getDir($key) . str_replace($this->prefix, "", $key);
326
    }
327
328
    /**
329
     * Returns an iterator for the directory contents
330
     *
331
     * @param string $dir
332
     *
333
     * @return Iterator
334
     */
335
    private function getIterator(string $dir): Iterator
336
    {
337
        return new RecursiveIteratorIterator(
338
            new RecursiveDirectoryIterator(
339
                $dir,
340
                FilesystemIterator::SKIP_DOTS
341
            ),
342
            RecursiveIteratorIterator::CHILD_FIRST
343
        );
344
    }
345
346
    /**
347
     * Gets the file contents and returns an array or an error if something
348
     * went wrong
349
     *
350
     * @param string $filepath
351
     *
352
     * @return array
353
     */
354
    private function getPayload(string $filepath): array
355
    {
356
        $warning = false;
357
        $payload = false;
358
        $pointer = fopen($filepath, 'r');
359
360
        /**
361
         * Cannot open file
362
         */
363
        if (false === $pointer) {
364
            return [];
365
        }
366
367
        if (flock($pointer, LOCK_SH)) {
368
            $payload = file_get_contents($filepath);
369
        }
370
371
        fclose($pointer);
372
373
        /**
374
         * No results
375
         */
376
        if (false === $payload) {
377
            return [];
378
        }
379
380
        set_error_handler(
381
            function () use (&$warning) {
382
                $warning = true;
383
            },
384
            E_NOTICE
385
        );
386
387
        $payload = unserialize($payload);
388
389
        restore_error_handler();
390
391
        if ($warning || !is_array($payload)) {
392
            return [];
393
        }
394
395
        return $payload;
396
    }
397
398
    /**
399
     * Returns if the cache has expired for this item or not
400
     *
401
     * @param array $payload
402
     *
403
     * @return bool
404
     */
405
    private function isExpired(array $payload): bool
406
    {
407
        $created = Arr::get($payload, "created", time());
408
        $ttl     = Arr::get($payload, "ttl", 3600);
409
410
        return ($created + $ttl) < time();
411
    }
412
}
413