Completed
Push — master ( 43d7fb...779a1c )
by Dmitry
03:34
created

Space   D

Complexity

Total Complexity 103

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 103
lcom 1
cbo 4
dl 0
loc 451
rs 4.8717
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
A getEngine() 0 4 1
A addProperties() 0 7 2
A addProperty() 0 20 4
A isPropertyNullable() 0 8 4
A setPropertyNullable() 0 15 3
A removeProperty() 0 14 2
A removeIndex() 0 8 1
A addIndex() 0 4 1
D createIndex() 0 48 11
A getIndexType() 0 10 3
A isSpecial() 0 4 2
A getId() 0 4 1
A getTupleMap() 0 8 2
A getFormat() 0 17 4
A getMapper() 0 4 1
A getName() 0 4 1
A parseFormat() 0 15 3
A hasProperty() 0 5 1
A getMeta() 0 13 1
A getPropertyType() 0 7 2
A getPropertyIndex() 0 7 2
A getReference() 0 4 2
A isReference() 0 4 1
C getIndexes() 0 30 8
C castIndex() 0 50 14
C getIndexValues() 0 28 8
A getPrimaryIndex() 0 8 2
A getTupleKey() 0 8 3
A getInstanceKey() 0 14 4
B getRepository() 0 14 5
A repositoryExists() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Space often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Space, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Tarantool\Mapper;
4
5
use Exception;
6
use Tarantool\Client\Schema\Space as ClientSpace;
7
8
class Space
9
{
10
    private $mapper;
11
12
    private $id;
13
    private $name;
14
    private $engine;
15
    private $format;
16
    private $indexes;
17
18
    private $formatNamesHash = [];
19
    private $formatTypesHash = [];
20
    private $formatReferences = [];
21
22
    private $repository;
23
24
    public function __construct(Mapper $mapper, $id, $name, $engine, $meta = null)
25
    {
26
        $this->mapper = $mapper;
27
        $this->id = $id;
28
        $this->name = $name;
29
        $this->engine = $engine;
30
31
        if ($meta) {
32
            foreach ($meta as $key => $value) {
33
                $this->$key = $value;
34
            }
35
        }
36
    }
37
38
    public function getEngine()
39
    {
40
        return $this->engine;
41
    }
42
43
    public function addProperties($config)
44
    {
45
        foreach ($config as $name => $type) {
46
            $this->addProperty($name, $type);
47
        }
48
        return $this;
49
    }
50
51
    public function addProperty($name, $type, $is_nullable = true, $reference = null)
52
    {
53
        $format = $this->getFormat();
54
        foreach ($format as $field) {
55
            if ($field['name'] == $name) {
56
                throw new Exception("Property $name exists");
57
            }
58
        }
59
        $row = compact('name', 'type', 'is_nullable');
60
        if ($reference) {
61
            $row['reference'] = $reference;
62
        }
63
        $format[] = $row;
64
        $this->format = $format;
65
        $this->mapper->getClient()->evaluate("box.space[$this->id]:format(...)", [$format]);
66
67
        $this->parseFormat();
68
69
        return $this;
70
    }
71
72
    public function isPropertyNullable($name)
73
    {
74
        foreach ($this->getFormat() as $field) {
75
            if ($field['name'] == $name) {
76
                return array_key_exists('is_nullable', $field) ? $field['is_nullable'] : false;
77
            }
78
        }
79
    }
80
81
    public function setPropertyNullable($name, $nullable = true)
82
    {
83
        $format = $this->getFormat();
84
        foreach ($format as $i => $field) {
85
            if ($field['name'] == $name) {
86
                $format[$i]['is_nullable'] = $nullable;
87
            }
88
        }
89
        $this->format = $format;
90
        $this->mapper->getClient()->evaluate("box.space[$this->id]:format(...)", [$format]);
91
92
        $this->parseFormat();
93
94
        return $this;
95
    }
96
97
    public function removeProperty($name)
98
    {
99
        $format = $this->getFormat();
100
        $last = array_pop($format);
101
        if ($last['name'] != $name) {
102
            throw new Exception("Remove only last property");
103
        }
104
        $this->mapper->getClient()->evaluate("box.space[$this->id]:format(...)", [$format]);
105
        $this->format = $format;
106
107
        $this->parseFormat();
108
109
        return $this;
110
    }
111
112
    public function removeIndex($name)
113
    {
114
        $this->mapper->getClient()->evaluate("box.space[$this->id].index.$name:drop()");
115
        $this->indexes = [];
116
        $this->mapper->getRepository('_vindex')->flushCache();
117
118
        return $this;
119
    }
120
121
    public function addIndex($config)
122
    {
123
        return $this->createIndex($config);
124
    }
125
126
    public function createIndex($config)
127
    {
128
        if (!is_array($config)) {
129
            $config = ['fields' => $config];
130
        }
131
132
133
        if (!array_key_exists('fields', $config)) {
134
            if (array_values($config) != $config) {
135
                throw new Exception("Invalid index configuration");
136
            }
137
            $config = [
138
                'fields' => $config
139
            ];
140
        }
141
142
        if (!is_array($config['fields'])) {
143
            $config['fields'] = [$config['fields']];
144
        }
145
146
        $options = [
147
            'parts' => []
148
        ];
149
150
        foreach ($config as $k => $v) {
151
            if ($k != 'name' && $k != 'fields') {
152
                $options[$k] = $v;
153
            }
154
        }
155
156
        foreach ($config['fields'] as $property) {
157
            if (!$this->getPropertyType($property)) {
158
                throw new Exception("Unknown property $property", 1);
159
            }
160
            $options['parts'][] = $this->getPropertyIndex($property)+1;
161
            $options['parts'][] = $this->getPropertyType($property);
162
            $this->setPropertyNullable($property, false);
163
        }
164
165
        $name = array_key_exists('name', $config) ? $config['name'] : implode('_', $config['fields']);
166
167
        $this->mapper->getClient()->evaluate("box.space[$this->id]:create_index('$name', ...)", [$options]);
168
        $this->indexes = [];
169
170
        $this->mapper->getSchema()->getSpace('_vindex')->getRepository()->flushCache();
171
172
        return $this;
173
    }
174
175
    public function getIndexType($id)
176
    {
177
        foreach ($this->getIndexes() as $index) {
178
            if ($index['iid'] == $id) {
179
                return $index['type'];
180
            }
181
        }
182
183
        throw new Exception("Invalid index #$index");
184
    }
185
186
    public function isSpecial()
187
    {
188
        return $this->id == ClientSpace::VSPACE || $this->id == ClientSpace::VINDEX;
189
    }
190
191
    public function getId()
192
    {
193
        return $this->id;
194
    }
195
196
    public function getTupleMap()
197
    {
198
        $reverse = [];
199
        foreach ($this->getFormat() as $i => $field) {
200
            $reverse[$field['name']] = $i + 1;
201
        }
202
        return (object) $reverse;
203
    }
204
205
    public function getFormat()
206
    {
207
        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...
208
            if ($this->isSpecial()) {
209
                $this->format = $this->mapper->getClient()
210
                    ->getSpace(ClientSpace::VSPACE)->select([$this->id])->getData()[0][6];
211
            } else {
212
                $this->format = $this->mapper->findOne('_vspace', ['id' => $this->id])->format;
213
            }
214
            if (!$this->format) {
215
                $this->format = [];
216
            }
217
            $this->parseFormat();
218
        }
219
220
        return $this->format;
221
    }
222
223
    public function getMapper()
224
    {
225
        return $this->mapper;
226
    }
227
228
    public function getName()
229
    {
230
        return $this->name;
231
    }
232
233
    private function parseFormat()
234
    {
235
        $this->formatTypesHash = [];
236
        $this->formatNamesHash = [];
237
        $this->formatReferences = [];
238
        foreach ($this->format as $key => $row) {
239
            $name = $row['name'];
240
            $this->formatTypesHash[$name] = $row['type'];
241
            $this->formatNamesHash[$name] = $key;
242
            if (array_key_exists('reference', $row)) {
243
                $this->formatReferences[$name] = $row['reference'];
244
            }
245
        }
246
        return $this;
247
    }
248
249
    public function hasProperty($name)
250
    {
251
        $this->getFormat();
252
        return array_key_exists($name, $this->formatNamesHash);
253
    }
254
255
    public function getMeta()
256
    {
257
        $this->getFormat();
258
        $this->getIndexes();
259
260
        return [
261
            'formatNamesHash' => $this->formatNamesHash,
262
            'formatTypesHash' => $this->formatTypesHash,
263
            'formatReferences' => $this->formatReferences,
264
            'indexes' => $this->indexes,
265
            'format' => $this->format,
266
        ];
267
    }
268
269
    public function getPropertyType($name)
270
    {
271
        if (!$this->hasProperty($name)) {
272
            throw new Exception("No property $name");
273
        }
274
        return $this->formatTypesHash[$name];
275
    }
276
277
    public function getPropertyIndex($name)
278
    {
279
        if (!$this->hasProperty($name)) {
280
            throw new Exception("No property $name");
281
        }
282
        return $this->formatNamesHash[$name];
283
    }
284
285
    public function getReference($name)
286
    {
287
        return $this->isReference($name) ? $this->formatReferences[$name] : null;
288
    }
289
290
    public function isReference($name)
291
    {
292
        return array_key_exists($name, $this->formatReferences);
293
    }
294
295
    public function getIndexes()
296
    {
297
        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...
298
            if ($this->isSpecial()) {
299
                $this->indexes = [];
300
                $indexTuples = $this->mapper->getClient()->getSpace(ClientSpace::VINDEX)->select([$this->id])->getData();
301
                $indexFormat = $this->mapper->getSchema()->getSpace(ClientSpace::VINDEX)->getFormat();
302
                foreach ($indexTuples as $tuple) {
0 ignored issues
show
Bug introduced by
The expression $indexTuples of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
303
                    $instance = [];
304
                    foreach ($indexFormat as $index => $format) {
305
                        $instance[$format['name']] = $tuple[$index];
306
                    }
307
                    $this->indexes[] = $instance;
308
                }
309
            } else {
310
                $indexes = $this->mapper->find('_vindex', ['id' => $this->id]);
311
                $this->indexes = [];
312
                foreach ($indexes as $index) {
313
                    $index = get_object_vars($index);
314
                    foreach ($index as $key => $value) {
315
                        if (is_object($value)) {
316
                            unset($index[$key]);
317
                        }
318
                    }
319
                    $this->indexes[] = $index;
320
                }
321
            }
322
        }
323
        return $this->indexes;
324
    }
325
326
    public function castIndex($params, $suppressException = false)
327
    {
328
        if (!count($this->getIndexes())) {
329
            return;
330
        }
331
        $keys = array_keys($params);
0 ignored issues
show
Unused Code introduced by
$keys is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
332
333
        $keys = [];
334
        foreach ($params as $name => $value) {
335
            $keys[] = $this->getPropertyIndex($name);
336
        }
337
338
        // equals
339
        foreach ($this->getIndexes() as $index) {
340
            $equals = false;
341
            if (count($keys) == count($index['parts'])) {
342
                // same length
343
                $equals = true;
344
                foreach ($index['parts'] as $part) {
345
                    $equals = $equals && in_array($part[0], $keys);
346
                }
347
            }
348
349
            if ($equals) {
350
                return $index['iid'];
351
            }
352
        }
353
354
        // index part
355
        foreach ($this->getIndexes() as $index) {
356
            $partial = [];
357
            foreach ($index['parts'] as $n => $part) {
358
                if (!array_key_exists($n, $keys)) {
359
                    break;
360
                }
361
                if ($keys[$n] != $part[0]) {
362
                    break;
363
                }
364
                $partial[] = $keys[$n];
365
            }
366
367
            if (count($partial) == count($keys)) {
368
                return $index['iid'];
369
            }
370
        }
371
372
        if (!$suppressException) {
373
            throw new Exception("No index on ".$this->name.' for ['.implode(', ', array_keys($params)).']');
374
        }
375
    }
376
377
    public function getIndexValues($indexId, $params)
378
    {
379
        $index = null;
380
        foreach ($this->getIndexes() as $candidate) {
381
            if ($candidate['iid'] == $indexId) {
382
                $index = $candidate;
383
                break;
384
            }
385
        }
386
        if (!$index) {
387
            throw new Exception("Undefined index: $indexId");
388
        }
389
390
        $format = $this->getFormat();
391
        $values = [];
392
        foreach ($index['parts'] as $part) {
393
            $name = $format[$part[0]]['name'];
394
            if (!array_key_exists($name, $params)) {
395
                break;
396
            }
397
            $value = $this->mapper->getSchema()->formatValue($part[1], $params[$name]);
398
            if (is_null($value) && !$this->isPropertyNullable($name)) {
399
                $value = $this->mapper->getSchema()->getDefaultValue($format[$part[0]]['type']);
400
            }
401
            $values[] = $value;
402
        }
403
        return $values;
404
    }
405
406
    public function getPrimaryIndex()
407
    {
408
        $indexes = $this->getIndexes();
409
        if (!count($indexes)) {
410
            throw new Exception("No primary index");
411
        }
412
        return $indexes[0];
413
    }
414
415
    public function getTupleKey($tuple)
416
    {
417
        $key = [];
418
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
419
            $key[] = $tuple[$part[0]];
420
        }
421
        return count($key) == 1 ? $key[0] : implode(':', $key);
422
    }
423
424
    public function getInstanceKey($instance)
425
    {
426
        $key = [];
427
428
        foreach ($this->getPrimaryIndex()['parts'] as $part) {
429
            $name = $this->getFormat()[$part[0]]['name'];
430
            if (!property_exists($instance, $name)) {
431
                throw new Exception("Field $name is undefined", 1);
432
            }
433
            $key[] = $instance->$name;
434
        }
435
436
        return count($key) == 1 ? $key[0] : implode(':', $key);
437
    }
438
439
    public function getRepository()
440
    {
441
        $class = Repository::class;
442
        foreach ($this->mapper->getPlugins() as $plugin) {
443
            $repositoryClass = $plugin->getRepositoryClass($this);
444
            if ($repositoryClass) {
445
                if ($class != Repository::class) {
446
                    throw new Exception('Repository class override');
447
                }
448
                $class = $repositoryClass;
449
            }
450
        }
451
        return $this->repository ?: $this->repository = new $class($this);
452
    }
453
454
    public function repositoryExists()
455
    {
456
        return !!$this->repository;
457
    }
458
}
459