Completed
Push — master ( 8bfe41...fce112 )
by Dmitry
01:40
created

Space::createIndex()   B

Complexity

Conditions 11
Paths 122

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 47
rs 7.1333
c 0
b 0
f 0
cc 11
nc 122
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Tarantool\Mapper;
6
7
use Exception;
8
use Tarantool\Client\Schema\Space as ClientSpace;
9
use Tarantool\Client\Schema\Criteria;
10
11
class Space
12
{
13
    private $mapper;
14
15
    private $id;
16
    private $name;
17
    private $engine;
18
    private $format;
19
    private $indexes;
20
21
    private $formatNamesHash = [];
22
    private $formatTypesHash = [];
23
    private $formatReferences = [];
24
25
    private $repository;
26
27
    public function __construct(Mapper $mapper, int $id, string $name, string $engine, array $meta = null)
28
    {
29
        $this->mapper = $mapper;
30
        $this->id = $id;
31
        $this->name = $name;
32
        $this->engine = $engine;
33
34
        if ($meta) {
35
            foreach ($meta as $key => $value) {
36
                $this->$key = $value;
37
            }
38
        }
39
    }
40
41
    public function getEngine() : string
42
    {
43
        return $this->engine;
44
    }
45
46
    public function addProperties(array $config) : self
47
    {
48
        foreach ($config as $name => $type) {
49
            $this->addProperty($name, $type);
50
        }
51
        return $this;
52
    }
53
54
    public function addProperty(string $name, string $type, array $opts = []) : self
55
    {
56
        $format = $this->getFormat();
57
58
        if ($this->getProperty($name, false)) {
59
            throw new Exception("Property $name exists");
60
        }
61
62
        $row = array_merge(compact('name', 'type'), $opts);
63
        if (!array_key_exists('is_nullable', $row)) {
64
            $row['is_nullable'] = true;
65
        }
66
67
        if (array_key_exists('default', $row)) {
68
            $row['defaultValue'] = $row['default'];
69
            unset($row['default']);
70
        }
71
72
        $format[] = $row;
73
74
        return $this->setFormat($format);
75
    }
76
77
    public function hasDefaultValue(string $name) : bool
78
    {
79
        return array_key_exists('defaultValue', $this->getProperty($name));
80
    }
81
82
    public function getDefaultValue(string $name)
83
    {
84
        return $this->getPropertyFlag($name, 'defaultValue');
85
    }
86
87
    public function isPropertyNullable(string $name) : bool
88
    {
89
        return !!$this->getPropertyFlag($name, 'is_nullable');
90
    }
91
92
    public function setFormat(array $format) : self
93
    {
94
        $this->format = $format;
95
        $this->mapper->getClient()->call("box.space.$this->name:format", $format);
96
        return $this->parseFormat();
97
    }
98
99
    public function setPropertyNullable(string $name, bool $nullable = true) : self
100
    {
101
        $format = $this->getFormat();
102
        foreach ($format as $i => $field) {
103
            if ($field['name'] == $name) {
104
                $format[$i]['is_nullable'] = $nullable;
105
            }
106
        }
107
108
        return $this->setFormat($format);
109
    }
110
111
    public function removeProperty(string $name) : self
112
    {
113
        $format = $this->getFormat();
114
        $last = array_pop($format);
115
        if ($last['name'] != $name) {
116
            throw new Exception("Remove only last property");
117
        }
118
119
        return $this->setFormat($format);
120
    }
121
122
    public function removeIndex(string $name) : self
123
    {
124
        $this->mapper->getClient()->call("box.space.$this->name.index.$name:drop");
125
        $this->indexes = [];
126
        $this->mapper->getRepository('_vindex')->flushCache();
127
128
        return $this;
129
    }
130
131
    public function addIndex($config) : self
132
    {
133
        return $this->createIndex($config);
134
    }
135
136
    public function createIndex($config) : self
137
    {
138
        if (!is_array($config)) {
139
            $config = ['fields' => $config];
140
        }
141
142
        if (!array_key_exists('fields', $config)) {
143
            if (array_values($config) != $config) {
144
                throw new Exception("Invalid index configuration");
145
            }
146
            $config = [
147
                'fields' => $config
148
            ];
149
        }
150
151
        if (!is_array($config['fields'])) {
152
            $config['fields'] = [$config['fields']];
153
        }
154
155
        $options = [
156
            'parts' => []
157
        ];
158
159
        foreach ($config as $k => $v) {
160
            if ($k != 'name' && $k != 'fields') {
161
                $options[$k] = $v;
162
            }
163
        }
164
165
        foreach ($config['fields'] as $property) {
166
            if (!$this->getPropertyType($property)) {
167
                throw new Exception("Unknown property $property", 1);
168
            }
169
            $options['parts'][] = $this->getPropertyIndex($property)+1;
170
            $options['parts'][] = $this->getPropertyType($property);
171
            $this->setPropertyNullable($property, false);
172
        }
173
174
        $name = array_key_exists('name', $config) ? $config['name'] : implode('_', $config['fields']);
175
176
        $this->mapper->getClient()->call("box.space.$this->name:create_index", $name, $options);
177
        $this->mapper->getSchema()->getSpace('_vindex')->getRepository()->flushCache();
178
179
        $this->indexes = [];
180
181
        return $this;
182
    }
183
184
    public function getIndex(int $id) : array
185
    {
186
        foreach ($this->getIndexes() as $index) {
187
            if ($index['iid'] == $id) {
188
                return $index;
189
            }
190
        }
191
192
        throw new Exception("Invalid index #$id");
193
    }
194
195
    public function getIndexType(int $id) : string
196
    {
197
        return $this->getIndex($id)['type'];
198
    }
199
200
    public function isSpecial() : bool
201
    {
202
        return in_array($this->id, [ ClientSpace::VSPACE_ID, ClientSpace::VINDEX_ID ]);
203
    }
204
205
    public function isSystem() : bool
206
    {
207
        return $this->id < 512;
208
    }
209
210
    public function getId() : int
211
    {
212
        return $this->id;
213
    }
214
215
    public function getTupleMap()
216
    {
217
        $reverse = [];
218
        foreach ($this->getFormat() as $i => $field) {
219
            $reverse[$field['name']] = $i + 1;
220
        }
221
        return (object) $reverse;
222
    }
223
224
    public function getFormat() : array
225
    {
226
        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...
227
            if ($this->isSpecial()) {
228
                $this->format = $this->mapper->getClient()
229
                    ->getSpaceById(ClientSpace::VSPACE_ID)
230
                    ->select(Criteria::key([$this->id]))[0][6];
231
            } else {
232
                $this->format = $this->mapper->findOrFail('_vspace', ['id' => $this->id])->format;
0 ignored issues
show
Bug introduced by
The property format does not seem to exist in Tarantool\Mapper\Entity.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
233
            }
234
            if (!$this->format) {
235
                $this->format = [];
236
            }
237
            $this->parseFormat();
238
        }
239
240
        return $this->format;
241
    }
242
243
    public function getMapper() : Mapper
244
    {
245
        return $this->mapper;
246
    }
247
248
    public function getName() : string
249
    {
250
        return $this->name;
251
    }
252
253
    private function parseFormat() : self
254
    {
255
        $this->formatTypesHash = [];
256
        $this->formatNamesHash = [];
257
        $this->formatReferences = [];
258
        foreach ($this->format as $key => $row) {
259
            $name = $row['name'];
260
            $this->formatTypesHash[$name] = $row['type'];
261
            $this->formatNamesHash[$name] = $key;
262
            if (array_key_exists('reference', $row)) {
263
                $this->formatReferences[$name] = $row['reference'];
264
            }
265
        }
266
        return $this;
267
    }
268
269
    public function hasProperty(string $name) : bool
270
    {
271
        $this->getFormat();
272
        return array_key_exists($name, $this->formatNamesHash);
273
    }
274
275
    public function getMeta() : array
276
    {
277
        $this->getFormat();
278
        $this->getIndexes();
279
280
        return [
281
            'formatNamesHash' => $this->formatNamesHash,
282
            'formatTypesHash' => $this->formatTypesHash,
283
            'formatReferences' => $this->formatReferences,
284
            'indexes' => $this->indexes,
285
            'format' => $this->format,
286
        ];
287
    }
288
289
    public function getProperty(string $name, bool $required = true) : ?array
290
    {
291
        foreach ($this->getFormat() as $field) {
292
            if ($field['name'] == $name) {
293
                return $field;
294
            }
295
        }
296
297
        if ($required) {
298
            throw new Exception("Invalid property $name");
299
        }
300
301
        return null;
302
    }
303
304
    public function getPropertyFlag(string $name, string $flag)
305
    {
306
        $property = $this->getProperty($name);
307
        if (array_key_exists($flag, $property)) {
308
            return $property[$flag];
309
        }
310
    }
311
312
    public function getPropertyType(string $name)
313
    {
314
        if (!$this->hasProperty($name)) {
315
            throw new Exception("No property $name");
316
        }
317
        return $this->formatTypesHash[$name];
318
    }
319
320
    public function getPropertyIndex(string $name) : int
321
    {
322
        if (!$this->hasProperty($name)) {
323
            throw new Exception("No property $name");
324
        }
325
        return $this->formatNamesHash[$name];
326
    }
327
328
    public function getReference(string $name) : ?string
329
    {
330
        return $this->isReference($name) ? $this->formatReferences[$name] : null;
331
    }
332
333
    public function isReference(string $name) : bool
334
    {
335
        return array_key_exists($name, $this->formatReferences);
336
    }
337
338
    public function getIndexes() : array
339
    {
340
        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...
341
            $this->indexes = [];
342
            if ($this->isSpecial()) {
343
                $indexTuples = $this->mapper->getClient()
344
                    ->getSpaceById(ClientSpace::VINDEX_ID)
345
                    ->select(Criteria::key([$this->id]));
346
347
                $indexFormat = $this->mapper->getSchema()
348
                    ->getSpace(ClientSpace::VINDEX_ID)
349
                    ->getFormat();
350
351
                foreach ($indexTuples as $tuple) {
352
                    $instance = [];
353
                    foreach ($indexFormat as $index => $format) {
354
                        $instance[$format['name']] = $tuple[$index];
355
                    }
356
                    $this->indexes[] = $instance;
357
                }
358
            } else {
359
                $indexes = $this->mapper->find('_vindex', [
360
                    'id' => $this->id,
361
                ]);
362
                foreach ($indexes as $index) {
363
                    $index = get_object_vars($index);
364
                    foreach ($index as $key => $value) {
365
                        if (is_object($value)) {
366
                            unset($index[$key]);
367
                        }
368
                    }
369
                    $this->indexes[] = $index;
370
                }
371
            }
372
        }
373
        return $this->indexes;
374
    }
375
376
    public function castIndex(array $params, bool $suppressException = false) : ?int
377
    {
378
        if (!count($this->getIndexes())) {
379
            return null;
380
        }
381
382
        $keys = [];
383
        foreach ($params as $name => $value) {
384
            $keys[$this->getPropertyIndex($name)] = $name;
385
        }
386
387
        // equals
388
        foreach ($this->getIndexes() as $index) {
389
            $equals = false;
390
            if (count($keys) == count($index['parts'])) {
391
                // same length
392
                $equals = true;
393
                foreach ($index['parts'] as $part) {
394
                    $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
395
                    $equals = $equals && array_key_exists($field, $keys);
396
                }
397
            }
398
399
            if ($equals) {
400
                return $index['iid'];
401
            }
402
        }
403
404
        // index part
405
        foreach ($this->getIndexes() as $index) {
406
            $partial = [];
407
            foreach ($index['parts'] as $n => $part) {
408
                $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
409
                if (!array_key_exists($field, $keys)) {
410
                    break;
411
                }
412
                $partial[] = $keys[$field];
413
            }
414
415
            if (count($partial) == count($keys)) {
416
                return $index['iid'];
417
            }
418
        }
419
420
        if (!$suppressException) {
421
            throw new Exception("No index on ".$this->name.' for ['.implode(', ', array_keys($params)).']');
422
        }
423
424
        return null;
425
    }
426
427
    public function getIndexValues(int $indexId, array $params) : array
428
    {
429
        $index = $this->getIndex($indexId);
430
        $format = $this->getFormat();
431
432
        $values = [];
433
        foreach ($index['parts'] as $part) {
434
            $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
435
            $name = $format[$field]['name'];
436
            if (!array_key_exists($name, $params)) {
437
                break;
438
            }
439
            $type = array_key_exists(1, $part) ? $part[1] : $part['type'];
440
            $value = $this->mapper->getSchema()->formatValue($type, $params[$name]);
441
            if (is_null($value) && !$this->isPropertyNullable($name)) {
442
                $value = $this->mapper->getSchema()->getDefaultValue($format[$field]['type']);
443
            }
444
            $values[] = $value;
445
        }
446
        return $values;
447
    }
448
449
    protected $primaryKey;
450
451
    public function getPrimaryKey() : ?string
452
    {
453
        if (!is_null($this->primaryKey)) {
454
            return $this->primaryKey ?: null;
455
        }
456
        $field = $this->getPrimaryField();
457
        if (!is_null($field)) {
458
            return $this->primaryKey = $this->getFormat()[$field]['name'];
459
        }
460
461
        $this->primaryKey = false;
462
        return null;
463
    }
464
465
    protected $primaryField;
466
467
    public function getPrimaryField() : ?int
468
    {
469
        if (!is_null($this->primaryField)) {
470
            return $this->primaryField ?: null;
471
        }
472
        $primary = $this->getPrimaryIndex();
473
        if (count($primary['parts']) == 1) {
474
            return $this->primaryField = $primary['parts'][0][0];
475
        }
476
477
        $this->primaryField = false;
478
        return null;
479
    }
480
481
    public function getPrimaryIndex() : array
482
    {
483
        return $this->getIndex(0);
484
    }
485
486
    public function getTupleKey(array $tuple)
487
    {
488
        $key = [];
489
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
490
            $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
491
            $key[] = $tuple[$field];
492
        }
493
        return count($key) == 1 ? $key[0] : implode(':', $key);
494
    }
495
496
    public function getInstanceKey(Entity $instance)
497
    {
498
        if ($this->getPrimaryKey()) {
499
            $key = $this->getPrimaryKey();
500
            return $instance->{$key};
501
        }
502
503
        $key = [];
504
505
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
506
            $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
507
            $name = $this->getFormat()[$field]['name'];
508
            if (!property_exists($instance, $name)) {
509
                throw new Exception("Field $name is undefined", 1);
510
            }
511
            $key[] = $instance->$name;
512
        }
513
514
        return count($key) == 1 ? $key[0] : implode(':', $key);
515
    }
516
517
    public function getRepository() : Repository
518
    {
519
        $class = Repository::class;
520
        foreach ($this->mapper->getPlugins() as $plugin) {
521
            $repositoryClass = $plugin->getRepositoryClass($this);
522
            if ($repositoryClass) {
523
                if ($class != Repository::class) {
524
                    throw new Exception('Repository class override');
525
                }
526
                $class = $repositoryClass;
527
            }
528
        }
529
        return $this->repository ?: $this->repository = new $class($this);
530
    }
531
532
    public function repositoryExists() : bool
533
    {
534
        return !!$this->repository;
535
    }
536
}
537