Completed
Push — master ( c4bd73...d1b940 )
by Dmitry
07:41
created

Space::setFormat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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 $engine;
15
    private $format;
16
    private $indexes;
17
18
    private $formatNamesHash = [];
19
    private $formatTypesHash = [];
20
    private $formatReferences = [];
21
22
    private $repository;
23
24
    public function __construct(Mapper $mapper, $id, $name, $engine, $meta = null)
25
    {
26
        $this->mapper = $mapper;
27
        $this->id = $id;
28
        $this->name = $name;
29
        $this->engine = $engine;
30
31
        if ($meta) {
32
            foreach ($meta as $key => $value) {
33
                $this->$key = $value;
34
            }
35
        }
36
    }
37
38
    public function getEngine()
39
    {
40
        return $this->engine;
41
    }
42
43
    public function addProperties($config)
44
    {
45
        foreach ($config as $name => $type) {
46
            $this->addProperty($name, $type);
47
        }
48
        return $this;
49
    }
50
51
    public function addProperty($name, $type, $opts = [])
52
    {
53
        $format = $this->getFormat();
54
55
        if ($this->getProperty($name, false)) {
56
            throw new Exception("Property $name exists");
57
        }
58
59
        $row = array_merge(compact('name', 'type'), $opts);
60
        if (!array_key_exists('is_nullable', $row)) {
61
            $row['is_nullable'] = true;
62
        }
63
64
        $format[] = $row;
65
66
        return $this->setFormat($format);
67
    }
68
69
    public function hasDefaultValue($name)
70
    {
71
        return array_key_exists('default', $this->getProperty($name));
72
    }
73
74
    public function getDefaultValue($name)
75
    {
76
        return $this->getPropertyFlag($name, 'default');
77
    }
78
79
    public function isPropertyNullable($name)
80
    {
81
        return $this->getPropertyFlag($name, 'is_nullable');
82
    }
83
84
    public function setFormat($format)
85
    {
86
        $this->format = $format;
87
        $this->mapper->getClient()->evaluate("box.space[$this->id]:format(...)", [$format]);
88
        return $this->parseFormat();
89
    }
90
91
    public function setPropertyNullable($name, $nullable = true)
92
    {
93
        $format = $this->getFormat();
94
        foreach ($format as $i => $field) {
95
            if ($field['name'] == $name) {
96
                $format[$i]['is_nullable'] = $nullable;
97
            }
98
        }
99
100
        return $this->setFormat($format);
101
    }
102
103
    public function removeProperty($name)
104
    {
105
        $format = $this->getFormat();
106
        $last = array_pop($format);
107
        if ($last['name'] != $name) {
108
            throw new Exception("Remove only last property");
109
        }
110
111
        return $this->setFormat($format);
112
    }
113
114
    public function removeIndex($name)
115
    {
116
        $this->mapper->getClient()->evaluate("box.space[$this->id].index.$name:drop()");
117
        $this->indexes = [];
118
        $this->mapper->getRepository('_vindex')->flushCache();
119
120
        return $this;
121
    }
122
123
    public function addIndex($config)
124
    {
125
        return $this->createIndex($config);
126
    }
127
128
    public function createIndex($config)
129
    {
130
        if (!is_array($config)) {
131
            $config = ['fields' => $config];
132
        }
133
134
        if (!array_key_exists('fields', $config)) {
135
            if (array_values($config) != $config) {
136
                throw new Exception("Invalid index configuration");
137
            }
138
            $config = [
139
                'fields' => $config
140
            ];
141
        }
142
143
        if (!is_array($config['fields'])) {
144
            $config['fields'] = [$config['fields']];
145
        }
146
147
        $options = [
148
            'parts' => []
149
        ];
150
151
        foreach ($config as $k => $v) {
152
            if ($k != 'name' && $k != 'fields') {
153
                $options[$k] = $v;
154
            }
155
        }
156
157
        foreach ($config['fields'] as $property) {
158
            if (!$this->getPropertyType($property)) {
159
                throw new Exception("Unknown property $property", 1);
160
            }
161
            $options['parts'][] = $this->getPropertyIndex($property)+1;
162
            $options['parts'][] = $this->getPropertyType($property);
163
            $this->setPropertyNullable($property, false);
164
        }
165
166
        $name = array_key_exists('name', $config) ? $config['name'] : implode('_', $config['fields']);
167
168
        $this->mapper->getClient()->evaluate("box.space[$this->id]:create_index('$name', ...)", [$options]);
169
        $this->indexes = [];
170
171
        $this->mapper->getSchema()->getSpace('_vindex')->getRepository()->flushCache();
172
173
        return $this;
174
    }
175
176
    public function getIndex($id)
177
    {
178
        foreach ($this->getIndexes() as $index) {
179
            if ($index['iid'] == $id) {
180
                return $index;
181
            }
182
        }
183
184
        throw new Exception("Invalid index #$index");
185
    }
186
187
    public function getIndexType($id)
188
    {
189
        return $this->getIndex($id)['type'];
190
    }
191
192
    public function isSpecial()
193
    {
194
        return $this->id == ClientSpace::VSPACE || $this->id == ClientSpace::VINDEX;
195
    }
196
197
    public function getId()
198
    {
199
        return $this->id;
200
    }
201
202
    public function getTupleMap()
203
    {
204
        $reverse = [];
205
        foreach ($this->getFormat() as $i => $field) {
206
            $reverse[$field['name']] = $i + 1;
207
        }
208
        return (object) $reverse;
209
    }
210
211
    public function getFormat()
212
    {
213
        if (!$this->format) {
214
            if ($this->isSpecial()) {
215
                $this->format = $this->mapper->getClient()
216
                    ->getSpace(ClientSpace::VSPACE)->select([$this->id])->getData()[0][6];
217
            } else {
218
                $this->format = $this->mapper->findOne('_vspace', ['id' => $this->id])->format;
219
            }
220
            if (!$this->format) {
221
                $this->format = [];
222
            }
223
            $this->parseFormat();
224
        }
225
226
        return $this->format;
227
    }
228
229
    public function getMapper()
230
    {
231
        return $this->mapper;
232
    }
233
234
    public function getName()
235
    {
236
        return $this->name;
237
    }
238
239
    private function parseFormat()
240
    {
241
        $this->formatTypesHash = [];
242
        $this->formatNamesHash = [];
243
        $this->formatReferences = [];
244
        foreach ($this->format as $key => $row) {
245
            $name = $row['name'];
246
            $this->formatTypesHash[$name] = $row['type'];
247
            $this->formatNamesHash[$name] = $key;
248
            if (array_key_exists('reference', $row)) {
249
                $this->formatReferences[$name] = $row['reference'];
250
            }
251
        }
252
        return $this;
253
    }
254
255
    public function hasProperty($name)
256
    {
257
        $this->getFormat();
258
        return array_key_exists($name, $this->formatNamesHash);
259
    }
260
261
    public function getMeta()
262
    {
263
        $this->getFormat();
264
        $this->getIndexes();
265
266
        return [
267
            'formatNamesHash' => $this->formatNamesHash,
268
            'formatTypesHash' => $this->formatTypesHash,
269
            'formatReferences' => $this->formatReferences,
270
            'indexes' => $this->indexes,
271
            'format' => $this->format,
272
        ];
273
    }
274
275
    public function getProperty($name, $required = true)
276
    {
277
        foreach ($this->getFormat() as $field) {
278
            if ($field['name'] == $name) {
279
                return $field;
280
            }
281
        }
282
283
        if ($required) {
284
            throw new Exception("Invalid property $name");
285
        }
286
    }
287
288
    public function getPropertyFlag($name, $flag)
289
    {
290
        $property = $this->getProperty($name);
291
        if (array_key_exists($flag, $property)) {
292
            return $property[$flag];
293
        }
294
    }
295
296
    public function getPropertyType($name)
297
    {
298
        if (!$this->hasProperty($name)) {
299
            throw new Exception("No property $name");
300
        }
301
        return $this->formatTypesHash[$name];
302
    }
303
304
    public function getPropertyIndex($name)
305
    {
306
        if (!$this->hasProperty($name)) {
307
            throw new Exception("No property $name");
308
        }
309
        return $this->formatNamesHash[$name];
310
    }
311
312
    public function getReference($name)
313
    {
314
        return $this->isReference($name) ? $this->formatReferences[$name] : null;
315
    }
316
317
    public function isReference($name)
318
    {
319
        return array_key_exists($name, $this->formatReferences);
320
    }
321
322
    public function getIndexes()
323
    {
324
        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...
325
            if ($this->isSpecial()) {
326
                $this->indexes = [];
327
                $indexTuples = $this->mapper->getClient()->getSpace(ClientSpace::VINDEX)->select([$this->id])->getData();
328
                $indexFormat = $this->mapper->getSchema()->getSpace(ClientSpace::VINDEX)->getFormat();
329
                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...
330
                    $instance = [];
331
                    foreach ($indexFormat as $index => $format) {
332
                        $instance[$format['name']] = $tuple[$index];
333
                    }
334
                    $this->indexes[] = $instance;
335
                }
336
            } else {
337
                $indexes = $this->mapper->find('_vindex', ['id' => $this->id]);
338
                $this->indexes = [];
339
                foreach ($indexes as $index) {
340
                    $index = get_object_vars($index);
341
                    foreach ($index as $key => $value) {
342
                        if (is_object($value)) {
343
                            unset($index[$key]);
344
                        }
345
                    }
346
                    $this->indexes[] = $index;
347
                }
348
            }
349
        }
350
        return $this->indexes;
351
    }
352
353
    public function castIndex($params, $suppressException = false)
354
    {
355
        if (!count($this->getIndexes())) {
356
            return;
357
        }
358
        $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...
359
360
        $keys = [];
361
        foreach ($params as $name => $value) {
362
            $keys[] = $this->getPropertyIndex($name);
363
        }
364
365
        // equals
366
        foreach ($this->getIndexes() as $index) {
367
            $equals = false;
368
            if (count($keys) == count($index['parts'])) {
369
                // same length
370
                $equals = true;
371
                foreach ($index['parts'] as $part) {
372
                    $equals = $equals && in_array($part[0], $keys);
373
                }
374
            }
375
376
            if ($equals) {
377
                return $index['iid'];
378
            }
379
        }
380
381
        // index part
382
        foreach ($this->getIndexes() as $index) {
383
            $partial = [];
384
            foreach ($index['parts'] as $n => $part) {
385
                if (!array_key_exists($n, $keys)) {
386
                    break;
387
                }
388
                if ($keys[$n] != $part[0]) {
389
                    break;
390
                }
391
                $partial[] = $keys[$n];
392
            }
393
394
            if (count($partial) == count($keys)) {
395
                return $index['iid'];
396
            }
397
        }
398
399
        if (!$suppressException) {
400
            throw new Exception("No index on ".$this->name.' for ['.implode(', ', array_keys($params)).']');
401
        }
402
    }
403
404
    public function getIndexValues($indexId, $params)
405
    {
406
        $index = $this->getIndex($indexId);
407
        $format = $this->getFormat();
408
409
        $values = [];
410
        foreach ($index['parts'] as $part) {
411
            $name = $format[$part[0]]['name'];
412
            if (!array_key_exists($name, $params)) {
413
                break;
414
            }
415
            $value = $this->mapper->getSchema()->formatValue($part[1], $params[$name]);
416
            if (is_null($value) && !$this->isPropertyNullable($name)) {
417
                $value = $this->mapper->getSchema()->getDefaultValue($format[$part[0]]['type']);
418
            }
419
            $values[] = $value;
420
        }
421
        return $values;
422
    }
423
424
    public function getPrimaryIndex()
425
    {
426
        return $this->getIndex(0);
427
    }
428
429
    public function getTupleKey($tuple)
430
    {
431
        $key = [];
432
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
433
            $key[] = $tuple[$part[0]];
434
        }
435
        return count($key) == 1 ? $key[0] : implode(':', $key);
436
    }
437
438
    public function getInstanceKey($instance)
439
    {
440
        $key = [];
441
442
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
443
            $name = $this->getFormat()[$part[0]]['name'];
444
            if (!property_exists($instance, $name)) {
445
                throw new Exception("Field $name is undefined", 1);
446
            }
447
            $key[] = $instance->$name;
448
        }
449
450
        return count($key) == 1 ? $key[0] : implode(':', $key);
451
    }
452
453
    public function getRepository()
454
    {
455
        $class = Repository::class;
456
        foreach ($this->mapper->getPlugins() as $plugin) {
457
            $repositoryClass = $plugin->getRepositoryClass($this);
458
            if ($repositoryClass) {
459
                if ($class != Repository::class) {
460
                    throw new Exception('Repository class override');
461
                }
462
                $class = $repositoryClass;
463
            }
464
        }
465
        return $this->repository ?: $this->repository = new $class($this);
466
    }
467
468
    public function repositoryExists()
469
    {
470
        return !!$this->repository;
471
    }
472
}
473