Completed
Push — master ( af98e0...8c0891 )
by Dmitry
07:01
created

Space::getIndexValues()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 28
rs 5.3846
cc 8
eloc 19
nc 9
nop 2
1
<?php
2
3
namespace Tarantool\Mapper;
4
5
use Exception;
6
use Tarantool\Client\Schema\Space as ClientSpace;
7
8
class Space
9
{
10
    private $mapper;
11
12
    private $id;
13
    private $name;
14
    private $format;
15
    private $indexes;
16
17
    private $formatNamesHash = [];
18
    private $formatTypesHash = [];
19
20
    private $repository;
21
22
    public function __construct(Mapper $mapper, $id, $name, $meta = null)
23
    {
24
        $this->mapper = $mapper;
25
        $this->id = $id;
26
        $this->name = $name;
27
28
        if ($meta) {
29
            foreach ($meta as $key => $value) {
30
                $this->$key = $value;
31
            }
32
        }
33
    }
34
35
    public function addProperties($config)
36
    {
37
        foreach ($config as $name => $type) {
38
            $this->addProperty($name, $type);
39
        }
40
        return $this;
41
    }
42
43
    public function addProperty($name, $type, $is_nullable = true)
44
    {
45
        $format = $this->getFormat();
46
        foreach ($format as $field) {
47
            if ($field['name'] == $name) {
48
                throw new Exception("Property $name exists");
49
            }
50
        }
51
        $format[] = compact('name', 'type', 'is_nullable');
52
        $this->format = $format;
53
        $this->mapper->getClient()->evaluate("box.space[$this->id]:format(...)", [$format]);
54
55
        $this->parseFormat();
56
57
        return $this;
58
    }
59
60
    public function isPropertyNullable($name)
61
    {
62
        foreach($this->getFormat() as $field) {
63
            if ($field['name'] == $name) {
64
                return array_key_exists('is_nullable', $field) ? $field['is_nullable'] : false;
65
            }
66
        }
67
    }
68
69
    public function setPropertyNullable($name, $nullable = true)
70
    {
71
        $format = $this->getFormat();
72
        foreach ($format as $i => $field) {
73
            if ($field['name'] == $name) {
74
                $format[$i]['is_nullable'] = $nullable;
75
            }
76
        }
77
        $this->format = $format;
78
        $this->mapper->getClient()->evaluate("box.space[$this->id]:format(...)", [$format]);
79
80
        $this->parseFormat();
81
82
        return $this;
83
84
    }
85
86
    public function removeProperty($name)
87
    {
88
        $format = $this->getFormat();
89
        $last = array_pop($format);
90
        if ($last['name'] != $name) {
91
            throw new Exception("Remove only last property");
92
        }
93
        $this->mapper->getClient()->evaluate("box.space[$this->id]:format(...)", [$format]);
94
        $this->format = $format;
95
96
        $this->parseFormat();
97
98
        return $this;
99
    }
100
101
    public function removeIndex($name)
102
    {
103
        $this->mapper->getClient()->evaluate("box.space[$this->id].index.$name:drop()");
104
        $this->indexes = [];
105
        $this->mapper->getRepository('_vindex')->flushCache();
106
107
        return $this;
108
    }
109
110
    public function addIndex($config)
111
    {
112
        return $this->createIndex($config);
113
    }
114
115
    public function createIndex($config)
116
    {
117
        if (!is_array($config)) {
118
            $config = ['fields' => $config];
119
        }
120
121
122
        if (!array_key_exists('fields', $config)) {
123
            if (array_values($config) != $config) {
124
                throw new Exception("Invalid index configuration");
125
            }
126
            $config = [
127
                'fields' => $config
128
            ];
129
        }
130
131
        if (!is_array($config['fields'])) {
132
            $config['fields'] = [$config['fields']];
133
        }
134
135
        $options = [
136
            'parts' => []
137
        ];
138
139
        foreach ($config as $k => $v) {
140
            if ($k != 'name' && $k != 'fields') {
141
                $options[$k] = $v;
142
            }
143
        }
144
145
        foreach ($config['fields'] as $property) {
146
            if (!$this->getPropertyType($property)) {
147
                throw new Exception("Unknown property $property", 1);
148
            }
149
            $options['parts'][] = $this->getPropertyIndex($property)+1;
150
            $options['parts'][] = $this->getPropertyType($property);
151
            $this->setPropertyNullable($property, false);
152
        }
153
154
        $name = array_key_exists('name', $config) ? $config['name'] : implode('_', $config['fields']);
155
156
        $this->mapper->getClient()->evaluate("box.space[$this->id]:create_index('$name', ...)", [$options]);
157
        $this->indexes = [];
158
159
        $this->mapper->getSchema()->getSpace('_vindex')->getRepository()->flushCache();
160
161
        return $this;
162
    }
163
164
    public function isSpecial()
165
    {
166
        return $this->id == ClientSpace::VSPACE || $this->id == ClientSpace::VINDEX;
167
    }
168
169
    public function getId()
170
    {
171
        return $this->id;
172
    }
173
174
    public function getTupleMap()
175
    {
176
        $reverse = [];
177
        foreach ($this->getFormat() as $i => $field) {
178
            $reverse[$field['name']] = $i + 1;
179
        }
180
        return (object) $reverse;
181
    }
182
183
    public function getFormat()
184
    {
185
        if (!$this->format) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->format 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...
186
            if ($this->isSpecial()) {
187
                $this->format = $this->mapper->getClient()
188
                    ->getSpace(ClientSpace::VSPACE)->select([$this->id])->getData()[0][6];
189
            } else {
190
                $this->format = $this->mapper->findOne('_vspace', ['id' => $this->id])->format;
191
            }
192
            if (!$this->format) {
193
                $this->format = [];
194
            }
195
            $this->parseFormat();
196
        }
197
198
        return $this->format;
199
    }
200
201
    public function getMapper()
202
    {
203
        return $this->mapper;
204
    }
205
206
    public function getName()
207
    {
208
        return $this->name;
209
    }
210
211
    private function parseFormat()
212
    {
213
        $this->formatTypesHash = [];
214
        $this->formatNamesHash = [];
215
        foreach ($this->format as $key => $row) {
216
            $this->formatTypesHash[$row['name']] = $row['type'];
217
            $this->formatNamesHash[$row['name']] = $key;
218
        }
219
        return $this;
220
    }
221
222
    public function hasProperty($name)
223
    {
224
        $this->getFormat();
225
        return array_key_exists($name, $this->formatNamesHash);
226
    }
227
228
    public function getMeta()
229
    {
230
        $this->getFormat();
231
        $this->getIndexes();
232
233
        return [
234
            'formatNamesHash' => $this->formatNamesHash,
235
            'formatTypesHash' => $this->formatTypesHash,
236
            'indexes' => $this->indexes,
237
            'format' => $this->format,
238
        ];
239
    }
240
241
    public function getPropertyType($name)
242
    {
243
        if (!$this->hasProperty($name)) {
244
            throw new Exception("No property $name");
245
        }
246
        return $this->formatTypesHash[$name];
247
    }
248
249
    public function getPropertyIndex($name)
250
    {
251
        if (!$this->hasProperty($name)) {
252
            throw new Exception("No property $name");
253
        }
254
        return $this->formatNamesHash[$name];
255
    }
256
257
    public function getIndexes()
258
    {
259
        if (!$this->indexes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->indexes 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...
260
            if ($this->isSpecial()) {
261
                $this->indexes = [];
262
                $indexTuples = $this->mapper->getClient()->getSpace(ClientSpace::VINDEX)->select([$this->id])->getData();
263
                $indexFormat = $this->mapper->getSchema()->getSpace(ClientSpace::VINDEX)->getFormat();
264
                foreach ($indexTuples as $tuple) {
0 ignored issues
show
Bug introduced by
The expression $indexTuples of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
265
                    $instance = [];
266
                    foreach ($indexFormat as $index => $format) {
267
                        $instance[$format['name']] = $tuple[$index];
268
                    }
269
                    $this->indexes[] = $instance;
270
                }
271
            } else {
272
                $indexes = $this->mapper->find('_vindex', ['id' => $this->id]);
273
                $this->indexes = [];
274
                foreach ($indexes as $index) {
275
                    $index = get_object_vars($index);
276
                    foreach ($index as $key => $value) {
277
                        if (is_object($value)) {
278
                            unset($index[$key]);
279
                        }
280
                    }
281
                    $this->indexes[] = $index;
282
                }
283
            }
284
        }
285
        return $this->indexes;
286
    }
287
288
    public function castIndex($params, $suppressException = false)
289
    {
290
        if (!count($this->getIndexes())) {
291
            return;
292
        }
293
        $keys = array_keys($params);
0 ignored issues
show
Unused Code introduced by
$keys is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
294
295
        $keys = [];
296
        foreach ($params as $name => $value) {
297
            $keys[] = $this->getPropertyIndex($name);
298
        }
299
300
        // equals
301
        foreach ($this->getIndexes() as $index) {
302
            $equals = false;
303
            if (count($keys) == count($index['parts'])) {
304
                // same length
305
                $equals = true;
306
                foreach ($index['parts'] as $part) {
307
                    $equals = $equals && in_array($part[0], $keys);
308
                }
309
            }
310
311
            if ($equals) {
312
                return $index['iid'];
313
            }
314
        }
315
316
        // index part
317
        foreach ($this->getIndexes() as $index) {
318
            $partial = [];
319
            foreach ($index['parts'] as $n => $part) {
320
                if (!array_key_exists($n, $keys)) {
321
                    break;
322
                }
323
                if ($keys[$n] != $part[0]) {
324
                    break;
325
                }
326
                $partial[] = $keys[$n];
327
            }
328
329
            if (count($partial) == count($keys)) {
330
                return $index['iid'];
331
            }
332
        }
333
334
        if (!$suppressException) {
335
            throw new Exception("No index");
336
        }
337
    }
338
339
    public function getIndexValues($indexId, $params)
340
    {
341
        $index = null;
342
        foreach ($this->getIndexes() as $candidate) {
343
            if ($candidate['iid'] == $indexId) {
344
                $index = $candidate;
345
                break;
346
            }
347
        }
348
        if (!$index) {
349
            throw new Exception("Undefined index: $indexId");
350
        }
351
352
        $format = $this->getFormat();
353
        $values = [];
354
        foreach ($index['parts'] as $part) {
355
            $name = $format[$part[0]]['name'];
356
            if (!array_key_exists($name, $params)) {
357
                break;
358
            }
359
            $value = $this->mapper->getSchema()->formatValue($part[1], $params[$name]);
360
            if(is_null($value) && !$this->isPropertyNullable($name)) {
361
                $value = $this->mapper->getSchema()->getDefaultValue($format[$part[0]]['type']);
362
            }
363
            $values[] = $value;
364
        }
365
        return $values;
366
    }
367
368
    public function getPrimaryIndex()
369
    {
370
        $indexes = $this->getIndexes();
371
        if (!count($indexes)) {
372
            throw new Exception("No primary index");
373
        }
374
        return $indexes[0];
375
    }
376
377
    public function getTupleKey($tuple)
378
    {
379
        $key = [];
380
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
381
            $key[] = $tuple[$part[0]];
382
        }
383
        return count($key) == 1 ? $key[0] : implode(':', $key);
384
    }
385
386
    public function getInstanceKey($instance)
387
    {
388
        $key = [];
389
390
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
391
            $name = $this->getFormat()[$part[0]]['name'];
392
            if (!property_exists($instance, $name)) {
393
                throw new Exception("Field $name is undefined", 1);
394
            }
395
            $key[] = $instance->$name;
396
        }
397
398
        return count($key) == 1 ? $key[0] : implode(':', $key);
399
    }
400
401
    public function getRepository()
402
    {
403
        $class = Repository::class;
404
        foreach ($this->mapper->getPlugins() as $plugin) {
405
            $repositoryClass = $plugin->getRepositoryClass($this);
406
            if ($repositoryClass) {
407
                if ($class != Repository::class) {
408
                    throw new Exception('Repository class override');
409
                }
410
                $class = $repositoryClass;
411
            }
412
        }
413
        return $this->repository ?: $this->repository = new $class($this);
414
    }
415
416
    public function repositoryExists()
417
    {
418
        return !!$this->repository;
419
    }
420
}
421