Completed
Push — master ( d7f465...3873b4 )
by Dmitry
04:58
created

Space::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 3
nc 3
nop 5
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 = null;
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
            $isNullable = false;
167
            if (is_array($property)) {
168
                if (!array_key_exists('property', $property)) {
169
                    throw new Exception("Invalid property configuration");
170
                }
171
                if (array_key_exists('is_nullable', $property)) {
172
                    $isNullable = $property['is_nullable'];
173
                }
174
                $property = $property['property'];
175
            }
176
            if ($this->isPropertyNullable($property) != $isNullable) {
177
                $this->setPropertyNullable($property, $isNullable);
178
            }
179
            if (!$this->getPropertyType($property)) {
180
                throw new Exception("Unknown property $property", 1);
181
            }
182
            $part = [
183
                'field' => $this->getPropertyIndex($property) + 1,
184
                'type' => $this->getPropertyType($property),
185
            ];
186
            if ($this->isPropertyNullable($property)) {
187
                $part['is_nullable'] = true;
188
            }
189
            $options['parts'][] = $part;
190
        }
191
192
        $name = array_key_exists('name', $config) ? $config['name'] : implode('_', $config['fields']);
193
194
        $this->mapper->getClient()->call("box.space.$this->name:create_index", $name, $options);
195
        $this->mapper->getSchema()->getSpace('_vindex')->getRepository()->flushCache();
196
197
        $this->indexes = null;
198
199
        return $this;
200
    }
201
202
    public function getIndex(int $id): array
203
    {
204
        foreach ($this->getIndexes() as $index) {
205
            if ($index['iid'] == $id) {
206
                return $index;
207
            }
208
        }
209
210
        throw new Exception("Invalid index #$id");
211
    }
212
213
    public function getIndexType(int $id): string
214
    {
215
        return $this->getIndex($id)['type'];
216
    }
217
218
    public function isSpecial(): bool
219
    {
220
        return in_array($this->id, [ ClientSpace::VSPACE_ID, ClientSpace::VINDEX_ID ]);
221
    }
222
223
    public function isSystem(): bool
224
    {
225
        return $this->id < 512;
226
    }
227
228
    public function getId(): int
229
    {
230
        return $this->id;
231
    }
232
233
    public function getTupleMap()
234
    {
235
        $reverse = [];
236
        foreach ($this->getFormat() as $i => $field) {
237
            $reverse[$field['name']] = $i + 1;
238
        }
239
        return (object) $reverse;
240
    }
241
242
    public function getFormat(): array
243
    {
244
        if ($this->format === null) {
245
            if ($this->isSpecial()) {
246
                $this->format = $this->mapper->getClient()
247
                    ->getSpaceById(ClientSpace::VSPACE_ID)
248
                    ->select(Criteria::key([$this->id]))[0][6];
249
            } else {
250
                $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...
251
            }
252
            if (!$this->format) {
253
                $this->format = [];
254
            }
255
            $this->parseFormat();
256
        }
257
258
        return $this->format;
259
    }
260
261
    public function getMapper(): Mapper
262
    {
263
        return $this->mapper;
264
    }
265
266
    public function getName(): string
267
    {
268
        return $this->name;
269
    }
270
271
    private function parseFormat(): self
272
    {
273
        $this->formatTypesHash = [];
274
        $this->formatNamesHash = [];
275
        $this->formatReferences = [];
276
        foreach ($this->format as $key => $row) {
277
            $name = $row['name'];
278
            $this->formatTypesHash[$name] = $row['type'];
279
            $this->formatNamesHash[$name] = $key;
280
            if (array_key_exists('reference', $row)) {
281
                $this->formatReferences[$name] = $row['reference'];
282
            }
283
        }
284
        return $this;
285
    }
286
287
    public function hasProperty(string $name): bool
288
    {
289
        $this->getFormat();
290
        return array_key_exists($name, $this->formatNamesHash);
291
    }
292
293
    public function getMeta(): array
294
    {
295
        $this->getFormat();
296
        $this->getIndexes();
297
298
        return [
299
            'formatNamesHash' => $this->formatNamesHash,
300
            'formatTypesHash' => $this->formatTypesHash,
301
            'formatReferences' => $this->formatReferences,
302
            'indexes' => $this->indexes,
303
            'format' => $this->format,
304
        ];
305
    }
306
307
    public function getProperty(string $name, bool $required = true): ?array
308
    {
309
        foreach ($this->getFormat() as $field) {
310
            if ($field['name'] == $name) {
311
                return $field;
312
            }
313
        }
314
315
        if ($required) {
316
            throw new Exception("Invalid property $name");
317
        }
318
319
        return null;
320
    }
321
322
    public function getPropertyFlag(string $name, string $flag)
323
    {
324
        $property = $this->getProperty($name);
325
        if (array_key_exists($flag, $property)) {
326
            return $property[$flag];
327
        }
328
    }
329
330
    public function getPropertyType(string $name)
331
    {
332
        if (!$this->hasProperty($name)) {
333
            throw new Exception("No property $name");
334
        }
335
        return $this->formatTypesHash[$name];
336
    }
337
338
    public function getPropertyIndex(string $name): int
339
    {
340
        if (!$this->hasProperty($name)) {
341
            throw new Exception("No property $name");
342
        }
343
        return $this->formatNamesHash[$name];
344
    }
345
346
    public function getReference(string $name): ?string
347
    {
348
        return $this->isReference($name) ? $this->formatReferences[$name] : null;
349
    }
350
351
    public function isReference(string $name): bool
352
    {
353
        return array_key_exists($name, $this->formatReferences);
354
    }
355
356
    public function getIndexes(): array
357
    {
358
        if ($this->indexes === null) {
359
            $this->indexes = [];
360
            if ($this->isSpecial()) {
361
                $indexTuples = $this->mapper->getClient()
362
                    ->getSpaceById(ClientSpace::VINDEX_ID)
363
                    ->select(Criteria::key([$this->id]));
364
365
                $indexFormat = $this->mapper->getSchema()
366
                    ->getSpace(ClientSpace::VINDEX_ID)
367
                    ->getFormat();
368
369
                foreach ($indexTuples as $tuple) {
370
                    $instance = [];
371
                    foreach ($indexFormat as $index => $format) {
372
                        $instance[$format['name']] = $tuple[$index];
373
                    }
374
                    $this->indexes[] = $instance;
375
                }
376
            } else {
377
                $indexes = $this->mapper->find('_vindex', [
378
                    'id' => $this->id,
379
                ]);
380
                foreach ($indexes as $index) {
381
                    $index = get_object_vars($index);
382
                    foreach ($index as $key => $value) {
383
                        if (is_object($value)) {
384
                            unset($index[$key]);
385
                        }
386
                    }
387
                    $this->indexes[] = $index;
388
                }
389
            }
390
        }
391
        return $this->indexes;
392
    }
393
394
    public function castIndex(array $params, bool $suppressException = false): ?int
395
    {
396
        if (!count($this->getIndexes())) {
397
            return null;
398
        }
399
400
        $keys = [];
401
        foreach ($params as $name => $value) {
402
            $keys[$this->getPropertyIndex($name)] = $name;
403
        }
404
405
        // equals
406
        foreach ($this->getIndexes() as $index) {
407
            $equals = false;
408
            if (count($keys) == count($index['parts'])) {
409
                // same length
410
                $equals = true;
411
                foreach ($index['parts'] as $part) {
412
                    $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
413
                    $equals = $equals && array_key_exists($field, $keys);
414
                }
415
            }
416
417
            if ($equals) {
418
                return $index['iid'];
419
            }
420
        }
421
422
        // index part
423
        foreach ($this->getIndexes() as $index) {
424
            $partial = [];
425
            foreach ($index['parts'] as $n => $part) {
426
                $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
427
                if (!array_key_exists($field, $keys)) {
428
                    break;
429
                }
430
                $partial[] = $keys[$field];
431
            }
432
433
            if (count($partial) == count($keys)) {
434
                return $index['iid'];
435
            }
436
        }
437
438
        if (!$suppressException) {
439
            throw new Exception("No index on " . $this->name . ' for [' . implode(', ', array_keys($params)) . ']');
440
        }
441
442
        return null;
443
    }
444
445
    public function getIndexValues(int $indexId, array $params): array
446
    {
447
        $index = $this->getIndex($indexId);
448
        $format = $this->getFormat();
449
450
        $values = [];
451
        foreach ($index['parts'] as $part) {
452
            $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
453
            $name = $format[$field]['name'];
454
            if (!array_key_exists($name, $params)) {
455
                break;
456
            }
457
            $type = array_key_exists(1, $part) ? $part[1] : $part['type'];
458
            $value = $this->mapper->getSchema()->formatValue($type, $params[$name]);
459
            if ($value === null && !$this->isPropertyNullable($name)) {
460
                $value = $this->mapper->getSchema()->getDefaultValue($format[$field]['type']);
461
            }
462
            $values[] = $value;
463
        }
464
        return $values;
465
    }
466
467
    protected $primaryKey;
468
469
    public function getPrimaryKey(): ?string
470
    {
471
        if ($this->primaryKey !== null) {
472
            return $this->primaryKey ?: null;
473
        }
474
        $field = $this->getPrimaryField();
475
        if ($field !== null) {
476
            return $this->primaryKey = $this->getFormat()[$field]['name'];
477
        }
478
479
        $this->primaryKey = false;
480
        return null;
481
    }
482
483
    protected $primaryField;
484
485
    public function getPrimaryField(): ?int
486
    {
487
        if ($this->primaryField !== null) {
488
            return $this->primaryField ?: null;
489
        }
490
        $primary = $this->getPrimaryIndex();
491
        if (count($primary['parts']) == 1) {
492
            return $this->primaryField = $primary['parts'][0][0];
493
        }
494
495
        $this->primaryField = false;
496
        return null;
497
    }
498
499
    public function getPrimaryIndex(): array
500
    {
501
        return $this->getIndex(0);
502
    }
503
504
    public function getTupleKey(array $tuple)
505
    {
506
        $key = [];
507
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
508
            $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
509
            $key[] = $tuple[$field];
510
        }
511
        return count($key) == 1 ? $key[0] : implode(':', $key);
512
    }
513
514
    public function getInstanceKey(Entity $instance)
515
    {
516
        if ($this->getPrimaryKey()) {
517
            $key = $this->getPrimaryKey();
518
            return $instance->{$key};
519
        }
520
521
        $key = [];
522
523
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
524
            $field = array_key_exists(0, $part) ? $part[0] : $part['field'];
525
            $name = $this->getFormat()[$field]['name'];
526
            if (!property_exists($instance, $name)) {
527
                throw new Exception("Field $name is undefined", 1);
528
            }
529
            $key[] = $instance->$name;
530
        }
531
532
        return count($key) == 1 ? $key[0] : implode(':', $key);
533
    }
534
535
    public function getRepository(): Repository
536
    {
537
        $class = Repository::class;
538
        foreach ($this->mapper->getPlugins() as $plugin) {
539
            $repositoryClass = $plugin->getRepositoryClass($this);
540
            if ($repositoryClass) {
541
                if ($class !== Repository::class && !is_subclass_of($class, Repository::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Tarantool\Mapper\Repository::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
542
                    throw new Exception('Repository class override');
543
                }
544
                $class = $repositoryClass;
545
            }
546
        }
547
        return $this->repository ?: $this->repository = new $class($this);
548
    }
549
550
    public function repositoryExists(): bool
551
    {
552
        return !!$this->repository;
553
    }
554
}
555