Completed
Push — master ( c17680...20c85b )
by Dmitry
02:46
created

Space::addProperties()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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() : array
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
                    $equals = $equals && array_key_exists($part[0], $keys);
395
                }
396
            }
397
398
            if ($equals) {
399
                return $index['iid'];
400
            }
401
        }
402
403
        // index part
404
        foreach ($this->getIndexes() as $index) {
405
            $partial = [];
406
            foreach ($index['parts'] as $n => $part) {
407
                if (!array_key_exists($part[0], $keys)) {
408
                    break;
409
                }
410
                $partial[] = $keys[$part[0]];
411
            }
412
413
            if (count($partial) == count($keys)) {
414
                return $index['iid'];
415
            }
416
        }
417
418
        if (!$suppressException) {
419
            throw new Exception("No index on ".$this->name.' for ['.implode(', ', array_keys($params)).']');
420
        }
421
422
        return null;
423
    }
424
425
    public function getIndexValues(int $indexId, array $params) : array
426
    {
427
        $index = $this->getIndex($indexId);
428
        $format = $this->getFormat();
429
430
        $values = [];
431
        foreach ($index['parts'] as $part) {
432
            $name = $format[$part[0]]['name'];
433
            if (!array_key_exists($name, $params)) {
434
                break;
435
            }
436
            $value = $this->mapper->getSchema()->formatValue($part[1], $params[$name]);
437
            if (is_null($value) && !$this->isPropertyNullable($name)) {
438
                $value = $this->mapper->getSchema()->getDefaultValue($format[$part[0]]['type']);
439
            }
440
            $values[] = $value;
441
        }
442
        return $values;
443
    }
444
445
    protected $primaryKey;
446
447
    public function getPrimaryKey() : ?string
448
    {
449
        if (!is_null($this->primaryKey)) {
450
            return $this->primaryKey ?: null;
451
        }
452
        $field = $this->getPrimaryField();
453
        if (!is_null($field)) {
454
            return $this->primaryKey = $this->getFormat()[$field]['name'];
455
        }
456
457
        $this->primaryKey = false;
458
        return null;
459
    }
460
461
    protected $primaryField;
462
463
    public function getPrimaryField() : ?int
464
    {
465
        if (!is_null($this->primaryField)) {
466
            return $this->primaryField ?: null;
467
        }
468
        $primary = $this->getPrimaryIndex();
469
        if (count($primary['parts']) == 1) {
470
            return $this->primaryField = $primary['parts'][0][0];
471
        }
472
473
        $this->primaryField = false;
474
        return null;
475
    }
476
477
    public function getPrimaryIndex() : array
478
    {
479
        return $this->getIndex(0);
480
    }
481
482
    public function getTupleKey(array $tuple)
483
    {
484
        $key = [];
485
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
486
            $key[] = $tuple[$part[0]];
487
        }
488
        return count($key) == 1 ? $key[0] : implode(':', $key);
489
    }
490
491
    public function getInstanceKey(Entity $instance)
492
    {
493
        if ($this->getPrimaryKey()) {
494
            $key = $this->getPrimaryKey();
495
            return $instance->{$key};
496
        }
497
498
        $key = [];
499
500
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
501
            $name = $this->getFormat()[$part[0]]['name'];
502
            if (!property_exists($instance, $name)) {
503
                throw new Exception("Field $name is undefined", 1);
504
            }
505
            $key[] = $instance->$name;
506
        }
507
508
        return count($key) == 1 ? $key[0] : implode(':', $key);
509
    }
510
511
    public function getRepository() : Repository
512
    {
513
        $class = Repository::class;
514
        foreach ($this->mapper->getPlugins() as $plugin) {
515
            $repositoryClass = $plugin->getRepositoryClass($this);
516
            if ($repositoryClass) {
517
                if ($class != Repository::class) {
518
                    throw new Exception('Repository class override');
519
                }
520
                $class = $repositoryClass;
521
            }
522
        }
523
        return $this->repository ?: $this->repository = new $class($this);
524
    }
525
526
    public function repositoryExists() : bool
527
    {
528
        return !!$this->repository;
529
    }
530
}
531