Completed
Pull Request — master (#19)
by Michal
02:49
created

RedisDataManager::execute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
ccs 0
cts 15
cp 0
rs 9.4285
cc 2
eloc 14
nc 2
nop 1
crap 6
1
<?php
2
3
namespace Adminerng\Drivers\Redis;
4
5
use Adminerng\Core\DataManager\AbstractDataManager;
6
use Adminerng\Core\Utils\Multisort;
7
use Adminerng\Core\Utils\Filter;
8
use RedisProxy\RedisProxy;
9
10
class RedisDataManager extends AbstractDataManager
11
{
12
    private $connection;
13
14
    private $itemsCountCache = false;
15
16 2
    public function __construct(RedisProxy $connection)
17
    {
18 2
        $this->connection = $connection;
19 2
    }
20
21
    public function databases(array $sorting = [])
22
    {
23
        $numberOfDatabases = $this->connection->config('get', 'databases')['databases'];
24
        $keyspace = $this->connection->info('keyspace');
25
        $databases = [];
26
        for ($i = 0; $i < $numberOfDatabases; ++$i) {
27
            $databases[$i] = $this->databaseInfo($keyspace, $i);
28
        }
29
        return Multisort::sort($databases, $sorting);
30
    }
31
32
    public function tables(array $sorting = [])
33
    {
34
        $tables = [
35
            RedisDriver::TYPE_KEY => [
36
                'all' => [
37
                    'key' => 'Show all keys',
38
                ]
39
            ],
40
            RedisDriver::TYPE_HASH => [],
41
            RedisDriver::TYPE_SET => [],
42
        ];
43
        $commands = [
44
            'hLen' => RedisDriver::TYPE_HASH,
45
            'sCard' => RedisDriver::TYPE_SET,
46
        ];
47
        foreach ($this->connection->keys('*') as $key) {
48
            foreach ($commands as $command => $label) {
49
                $result = $this->connection->$command($key);
50
                if ($this->connection->getLastError() !== null) {
51
                    $this->connection->clearLastError();
52
                    continue;
53
                }
54
                $tables[$label][$key] = [
55
                    'key' => $key
56
                ];
57
                if ($label == RedisDriver::TYPE_HASH) {
58
                    $tables[$label][$key]['number_of_fields'] = $result;
59
                } elseif ($label == RedisDriver::TYPE_SET) {
60
                    $tables[$label][$key]['number_of_members'] = $result;
61
                }
62
                break;
63
            }
64
        }
65
        return [
66
            RedisDriver::TYPE_KEY => Multisort::sort($tables[RedisDriver::TYPE_KEY], $sorting),
67
            RedisDriver::TYPE_HASH => Multisort::sort($tables[RedisDriver::TYPE_HASH], $sorting),
68
            RedisDriver::TYPE_SET => Multisort::sort($tables[RedisDriver::TYPE_SET], $sorting),
69
        ];
70
    }
71
72
    public function itemsCount($type, $table, array $filter = [])
73
    {
74
        if ($this->itemsCountCache !== false) {
75
            return $this->itemsCountCache;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->itemsCountCache; (boolean) is incompatible with the return type declared by the interface Adminerng\Core\DataManag...erInterface::itemsCount 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...
76
        }
77
        if ($type == RedisDriver::TYPE_HASH) {
78
            if (!$filter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filter of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
79
                $this->itemsCountCache = $this->connection->hLen($table);
80
                return $this->itemsCountCache;
81
            } else {
82
                $totalItems = 0;
83
                foreach ($filter as $filterParts) {
84
                    if (isset($filterParts['key'][Filter::OPERATOR_EQUAL])) {
85
                        $res = $this->connection->hget($table, $filterParts['key'][Filter::OPERATOR_EQUAL]);
86
                        if ($res) {
87
                            $item = [
88
                                'key' => $filterParts['key'][Filter::OPERATOR_EQUAL],
89
                                'length' => strlen($res),
90
                                'value' => $res,
91
                            ];
92
                            if (Filter::apply($item, $filter)) {
93
                                $totalItems++;
94
                            }
95
                        }
96
                        $this->itemsCountCache = $totalItems;
0 ignored issues
show
Documentation Bug introduced by
The property $itemsCountCache was declared of type boolean, but $totalItems is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
97
                        return $this->itemsCountCache;
98
                    }
99
                }
100
                $iterator = '';
101
                do {
102
                    $pattern = null;
103
                    $res = $this->connection->hscan($table, $iterator, $pattern, 1000);
104
                    $res = $res ?: [];
105 View Code Duplication
                    foreach ($res as $key => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
106
                        $item = [
107
                            'key' => $key,
108
                            'length' => strlen($value),
109
                            'value' => $value,
110
                        ];
111
                        if (Filter::apply($item, $filter)) {
112
                            $totalItems++;
113
                        }
114
                    }
115
                } while ($iterator !== 0);
116
                $this->itemsCountCache = $totalItems;
117
                return $this->itemsCountCache;
118
            }
119
        }
120
        if ($type == RedisDriver::TYPE_KEY) {
121
            $totalItems = 0;
122
            foreach ($this->connection->keys('*') as $key) {
123
                $result = $this->connection->get($key);
124
                if ($this->connection->getLastError() !== null) {
125
                    $this->connection->clearLastError();
126
                    continue;
127
                }
128
129
                $item = [
130
                    'key' => $key,
131
                    'value' => $result,
132
                    'length' => strlen($result),
133
                ];
134
135
                if (Filter::apply($item, $filter)) {
136
                    $totalItems++;
137
                }
138
            }
139
            return $totalItems;
140
        }
141
        if ($type == RedisDriver::TYPE_SET) {
142
            if (!$filter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filter of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
143
                return $this->connection->sCard($table);
144
            }
145
            $iterator = '';
146
            $totalItems = 0;
147
            do {
148
                $res = $this->connection->sscan($table, $iterator, null, 1000);
149
                $res = $res ?: [];
150 View Code Duplication
                foreach ($res as $member) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
151
                    $item = [
152
                        'member' => $member,
153
                        'length' => strlen($member),
154
                    ];
155
                    if (Filter::apply($item, $filter)) {
156
                        $totalItems++;
157
                    }
158
                }
159
            } while ($iterator !== 0);
160
            return $totalItems;
161
        }
162
        return 0;
163
    }
164
165
    public function items($type, $table, $page, $onPage, array $filter = [], array $sorting = [])
166
    {
167
        $items = [];
168
        $offset = ($page - 1) * $onPage;
169
        $skipped = 0;
170
        if ($type == RedisDriver::TYPE_HASH) {
171
            foreach ($filter as $filterParts) {
172
                if (isset($filterParts['key'][Filter::OPERATOR_EQUAL])) {
173
                    $items = [];
174
                    $res = $this->connection->hget($table, $filterParts['key'][Filter::OPERATOR_EQUAL]);
175
                    if ($res) {
176
                        $item = [
177
                            'key' => $filterParts['key'][Filter::OPERATOR_EQUAL],
178
                            'length' => strlen($res),
179
                            'value' => $res,
180
                        ];
181
                        if (Filter::apply($item, $filter)) {
182
                            $items[$item['key']] = $item;
183
                        }
184
                    }
185
                    return $items;
186
                }
187
            }
188
189
190
            $iterator = '';
191
            do {
192
                $pattern = null;
193
                $res = $this->connection->hscan($table, $iterator, $pattern, $onPage * 10);
194
                $res = $res ?: [];
195
                foreach ($res as $key => $value) {
196
                    $item = [
197
                        'key' => $key,
198
                        'length' => strlen($value),
199
                        'value' => $value,
200
                    ];
201 View Code Duplication
                    if (Filter::apply($item, $filter)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
                        if ($skipped < $offset) {
203
                            $skipped++;
204
                        } else {
205
                            $items[$key] = $item;
206
                            if (count($items) === $onPage) {
207
                                break;
208
                            }
209
                        }
210
                    }
211
                }
212
            } while ($iterator !== 0 && count($items) < $onPage);
213
        } elseif ($type == RedisDriver::TYPE_KEY) {
214
            foreach ($this->connection->keys('*') as $key) {
215
                $result = $this->connection->get($key);
216
                if ($this->connection->getLastError() !== null) {
217
                    $this->connection->clearLastError();
218
                    continue;
219
                }
220
221
                $item = [
222
                    'key' => $key,
223
                    'value' => $result,
224
                    'length' => strlen($result),
225
                ];
226
227 View Code Duplication
                if (Filter::apply($item, $filter)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
228
                    if ($skipped < $offset) {
229
                        $skipped++;
230
                    } else {
231
                        $items[$key] = $item;
232
                        if (count($items) === $onPage) {
233
                            break;
234
                        }
235
                    }
236
                }
237
            }
238
        } elseif ($type == RedisDriver::TYPE_SET) {
239
            $iterator = '';
240
            do {
241
                $pattern = null;
242
                $res = $this->connection->sscan($table, $iterator, $pattern, $onPage * 10);
243
                $res = $res ?: [];
244
                foreach ($res as $member) {
245
                    $item = [
246
                        'member' => $member,
247
                        'length' => strlen($member),
248
                    ];
249
                    if (Filter::apply($item, $filter)) {
250
                        $items[$member] = $item;
251
                        if (count($items) === $onPage) {
252
                            break;
253
                        }
254
                    }
255
                }
256
            } while ($iterator !== 0 && count($items) < $onPage);
257
        }
258
259
        if ($this->itemsCount($type, $table, $filter) <= $onPage) {
260
            $items = Multisort::sort($items, $sorting);
261
        } elseif ($sorting) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sorting of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
262
            $this->addMessage('Sorting has not been applied because the number of items is greater then the limit. Increase the limit or modify the filter.');
263
        }
264
265
        return $items;
266
    }
267
268
    public function deleteItem($type, $table, $item)
269
    {
270
        if ($type == RedisDriver::TYPE_HASH) {
271
            return $this->connection->hdel($table, $item);
272
        }
273
        if ($type == RedisDriver::TYPE_KEY) {
274
            return $this->connection->del($item);
275
        }
276
        if ($type == RedisDriver::TYPE_SET) {
277
            return $this->connection->srem($table, $item);
278
        }
279
        return parent::deleteItem($type, $table, $item);
280
    }
281
282
    public function deleteTable($type, $table)
283
    {
284
        return $this->connection->del($table);
285
    }
286
287
    public function selectDatabase($database)
288
    {
289
        $this->connection->select($database);
290
    }
291
292
    private function databaseInfo($keyspace, $db)
293
    {
294
        $info = [
295
            'database' => $db,
296
            'keys' => 0,
297
            'expires' => null,
298
            'avg_ttl' => null,
299
        ];
300
        if (isset($keyspace['db' . $db])) {
301
            $dbKeyspace = explode(',', $keyspace['db' . $db]);
302
            $info['keys'] = explode('=', $dbKeyspace[0])[1];
303
            $info['expires'] = explode('=', $dbKeyspace[1])[1];
304
            $info['avg_ttl'] = explode('=', $dbKeyspace[2])[1];
305
        }
306
        return $info;
307
    }
308
309
    public function execute($commands)
310
    {
311
        $listOfCommands = array_filter(array_map('trim', explode("\n", $commands)), function ($command) {
312
            return $command;
313
        });
314
315
        $results = [];
316
        foreach ($listOfCommands as $command) {
317
            $commandParts = explode(' ', $command);
318
            $function = array_shift($commandParts);
319
            $function = strtolower($function);
320
            $results[$command]['headers'] = $this->headers($function);
321
            $rows = call_user_func_array([$this->connection, $function], $commandParts);
322
            $items = $this->getItems($function, $rows);
323
            $results[$command]['items'] = $items;
324
            $results[$command]['count'] = count($items);
325
        }
326
        return $results;
327
    }
328
329
    private function headers($function)
330
    {
331
        if ($function === 'get' || $function === 'hget') {
332
            return ['value'];
333
        }
334
        if ($function === 'keys') {
335
            return ['key'];
336
        }
337
        if ($function === 'hgetall') {
338
            return ['key', 'value'];
339
        }
340
        if ($function === 'hlen') {
341
            return ['items_count'];
342
        }
343
        return [];
344
    }
345
346
    private function getItems($function, $rows)
347
    {
348
        $items = [];
349
        if ($function === 'keys') {
350
            foreach ($rows as $key) {
351
                $items[] = [$key];
352
            }
353
        } elseif ($function === 'hgetall') {
354
            foreach ($rows as $key => $value) {
355
                $items[] = [$key, $value];
356
            }
357
        } else {
358
            return [[$rows]];
359
        }
360
        return $items;
361
    }
362
}
363