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; |
|
|
|
|
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)) { |
|
|
|
|
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
|
|
|
|
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.