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