Completed
Push — master ( e3c0b5...539798 )
by Dmitry
03:14
created

Repository::normalize()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
rs 8.8571
ccs 11
cts 11
cp 1
cc 6
eloc 11
nc 8
nop 1
crap 6
1
<?php
2
3
namespace Tarantool\Mapper;
4
5
use Exception;
6
use SplObjectStorage;
7
use Tarantool\Mapper\Plugin\Procedure;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Tarantool\Mapper\Procedure.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
8
use Tarantool\Mapper\Procedure\FindOrCreate;
9
10
class Repository
11
{
12
    private $space;
13
    private $persisted = [];
14
    private $original = [];
15
    private $keys;
16
17
    private $results = [];
18
19
    public function __construct(Space $space)
20
    {
21
        $this->space = $space;
22
        $this->keys = new SplObjectStorage;
23 64
    }
24
25 64
    public function create($data)
26 64
    {
27
        $data = (array) $data;
28 64
        $class = Entity::class;
29 View Code Duplication
        foreach ($this->getMapper()->getPlugins() as $plugin) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
30 64
            $entityClass = $plugin->getEntityClass($this->space);
31 64
            if ($entityClass) {
32
                if ($class != Entity::class) {
33
                    throw new Exception('Entity class override');
34 64
                }
35 2
                $class = $entityClass;
36
            }
37
        }
38 64
39 64
        if (array_key_exists(0, $data)) {
40 64
            $byType = [];
41 64
            foreach ($this->space->getFormat() as $row) {
42 4
                if (!array_key_exists($row['type'], $byType)) {
43 4
                    $byType[$row['type']] = [$row['name']];
44
                } else {
45 64
                    $byType[$row['type']][] = $row['name'];
46 64
                }
47 64
            }
48 64
            $mapping = [
49
                'is_numeric' => 'unsigned',
50
                'is_string' => 'string',
51 64
                'is_array' => '*',
52 64
            ];
53
            foreach ($data as $k => $v) {
54 1
                foreach ($mapping as $function => $type) {
55
                    if (call_user_func($function, $v)) {
56
                        if (array_key_exists($type, $byType) && count($byType[$type]) == 1) {
57
                            $data[$byType[$type][0]] = $v;
58 64
                            unset($data[$k]);
59 2
                        }
60 2
                    }
61
                }
62 64
            }
63
        }
64 64
65 64
        $instance = new $class($this);
66 64
67 64
        foreach ($this->space->getFormat() as $row) {
68 64
            if (array_key_exists($row['name'], $data)) {
69
                $instance->{$row['name']} = $data[$row['name']];
70
                if ($data[$row['name']] instanceof Entity) {
71
                    $instance->{$row['name']} = $instance->{$row['name']}->id;
72 64
                }
73
            }
74 64
        }
75
76
        foreach ($this->getMapper()->getPlugins() as $plugin) {
77 64
            $plugin->generateKey($instance, $this->space);
78
            $plugin->afterInstantiate($instance, $this->space);
79 64
        }
80
81 64
        // validate instance key
82
        $key = $this->space->getInstanceKey($instance);
83
84 64
        foreach ($this->keys as $_) {
85
            if ($this->keys[$_] == $key) {
86 64
                throw new Exception($this->space->getName().' '.json_encode($key).' exists');
87 64
            }
88 64
        }
89 64
90
        $this->keys[$instance] = $key;
91 64
        return $instance;
92
    }
93
94
    public function findOne($params = [])
95 1
    {
96
        return $this->find($params, true);
97
    }
98 19
99
    public function findOrCreate($params = [])
100 19
    {
101
        if ($this->getSpace()->getName() != '_procedure') {
102
103 64
            $key = $this->getMapper()
104
                ->getPlugin(Procedure::class)
105 64
                ->get(FindOrCreate::class)
106
                ->execute($this->getSpace(), $this->normalize($params));
107 64
108 1
            if (!$key) {
109
                throw new Exception("Invalid key");
110
            }
111 64
112 10
            return $this->findOrFail($key);
113 3
        }
114
115
        $entity = $this->findOne($params);
116 7
        if (!$entity) {
117
            $entity = $this->create($params);
118 7
        }
119
        return $entity;
120
    }
121 64
122 2
    public function findOrFail($params = [])
123
    {
124
        $entity = $this->findOne($params);
125 64
        if (!$entity) {
126 64
            throw new Exception("No ".$this->getSpace()->getName().' found using '.json_encode($params));
127
        }
128 64
        return $entity;
129 64
    }
130 18
131 64
    public function normalize($params)
132
    {
133
        if (!is_array($params)) {
134
            $params = [$params];
135 64
        }
136 1
137
        if (count($params) == 1 && array_key_exists(0, $params)) {
138
            $primary = $this->space->getPrimaryIndex();
139 64
            if (count($primary['parts']) == 1) {
140 64
                $formatted = $this->getMapper()->getSchema()->formatValue($primary['parts'][0][1], $params[0]);
141 2
                if ($params[0] == $formatted) {
142 2
                    $params = [
143
                        $this->space->getFormat()[$primary['parts'][0][0]]['name'] => $params[0]
144 64
                    ];
145 64
                }
146
            }
147 1
        }
148 64
149
        return $params;
150
    }
151
152
    public function find($params = [], $one = false)
153 64
    {
154 64
        $cacheKey = json_encode(func_get_args());
155 64
156
        if (array_key_exists($cacheKey, $this->results)) {
157
            return $this->results[$cacheKey];
158 64
        }
159 64
160 2
        $params = $this->normalize($params);
161
162
        if (array_key_exists('id', $params)) {
163 64
            if (array_key_exists($params['id'], $this->persisted)) {
164
                $instance = $this->persisted[$params['id']];
165 64
                return $one ? $instance : [$instance];
166 1
            }
167 1
        }
168 1
169 1
170 1
        $index = $this->space->castIndex($params);
171
        if (is_null($index)) {
172
            throw new Exception("No index for params ".json_encode($params));
173 1
        }
174
175
        $client = $this->getMapper()->getClient();
176 1
        $values = $this->space->getIndexValues($index, $params);
177
        if ($this->space->getIndextype($index) == 'hash' && !count($values)) {
178 1
            //  iterator box.index.ALL == 2
179 1
            $data = $client->getSpace($this->space->getId())->select($values, $index, null, null, 2)->getData();
180
        } else {
181 1
            $data = $client->getSpace($this->space->getId())->select($values, $index)->getData();
182
        }
183
184 64
        $result = [];
185
        foreach ($data as $tuple) {
0 ignored issues
show
Bug introduced by
The expression $data 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...
186 64
            $instance = $this->getInstance($tuple);
187 64
            if ($one) {
188 64
                return $this->results[$cacheKey] = $instance;
189 64
            }
190 64
            $result[] = $instance;
191 64
        }
192 64
193
        if ($one) {
194 64
            return $this->results[$cacheKey] = null;
195
        }
196 64
197 64
        return $this->results[$cacheKey] = $result;
198
    }
199 14
200
    public function forget($id)
201
    {
202 64
        if (array_key_exists($id, $this->persisted)) {
203 64
            unset($this->persisted[$id]);
204
        }
205 17
    }
206
207
    public function getInstance($tuple)
208
    {
209
        $key = $this->space->getTupleKey($tuple);
210
211
        if (array_key_exists($key, $this->persisted)) {
212 64
            return $this->persisted[$key];
213
        }
214 64
215
        $class = Entity::class;
216 View Code Duplication
        foreach ($this->getMapper()->getPlugins() as $plugin) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217 6
            $entityClass = $plugin->getEntityClass($this->space);
218
            if ($entityClass) {
219 6
                if ($class != Entity::class) {
220 6
                    throw new Exception('Entity class override');
221 6
                }
222
                $class = $entityClass;
223 6
            }
224 6
        }
225
226 1
        $instance = new $class($this);
227
228 1
        $this->original[$key] = $tuple;
229 1
230 1 View Code Duplication
        foreach ($this->space->getFormat() as $index => $info) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
            $instance->{$info['name']} = array_key_exists($index, $tuple) ? $tuple[$index] : null;
232 1
        }
233 1
234
        $this->keys->offsetSet($instance, $key);
235 64
236
        foreach ($this->getMapper()->getPlugins() as $plugin) {
237 64
            $plugin->afterInstantiate($instance);
238 64
        }
239
240 64
        return $this->persisted[$key] = $instance;
241
    }
242 64
243 1
    public function getMapper()
244
    {
245
        return $this->space->getMapper();
246 64
    }
247 64
248 64
    public function getSpace()
249 64
    {
250
        return $this->space;
251 19
    }
252 19
253 19
    public function knows($instance)
254 19
    {
255
        return $this->keys->offsetExists($instance);
256
    }
257 19
258 19
    public function update(Entity $instance, $operations)
259 4
    {
260 19
        if (!count($operations)) {
261 19
            return;
262
        }
263
264
        $tupleOperations = [];
265
        foreach ($operations as $operation) {
266 19
            $tupleIndex = $this->space->getPropertyIndex($operation[1]);
267 19
            $tupleOperations[] = [$operation[0], $tupleIndex, $operation[2]];
268 1
        }
269 19
270
        $pk = [];
271
        foreach ($this->space->getPrimaryIndex()['parts'] as $part) {
272
            $pk[] = $instance->{$this->space->getFormat()[$part[0]]['name']};
273 18
        }
274 18
275 18
        $client = $this->getMapper()->getClient();
276 18
        $result = $client->getSpace($this->space->getId())->update($pk, $tupleOperations);
277
        foreach ($result->getData() as $tuple) {
278 View Code Duplication
            foreach ($this->space->getFormat() as $index => $info) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279 18
                if (array_key_exists($index, $tuple)) {
280 17
                    $instance->{$info['name']} = $tuple[$index];
281 17
                }
282 17
            }
283 1
        }
284 1
    }
285 1
286 1
    public function truncate()
287
    {
288 18
        $this->results = [];
289
        $id = $this->space->getId();
290
        $this->getMapper()->getClient()->evaluate("box.space[$id]:truncate()");
291
    }
292 64
293
    public function remove($params = [])
294 64
    {
295
        if ($params instanceof Entity) {
296
            return $this->removeEntity($params);
297 64
        }
298
299 64
        if (!count($params)) {
300 64
            throw new Exception("Use truncate to flush space");
301
        }
302 64
303 64
        foreach ($this->find($params) as $entity) {
304
            $this->removeEntity($entity);
305
        }
306 64
    }
307 64
308
    public function removeEntity(Entity $instance)
309
    {
310 64
        $key = $this->space->getInstanceKey($instance);
311
312
        if (!array_key_exists($key, $this->original)) {
313 64
            return;
314
        }
315 64
316 64
        if (array_key_exists($key, $this->persisted)) {
317 64
            unset($this->persisted[$key]);
318
319 64
            $pk = [];
320 64 View Code Duplication
            foreach ($this->space->getPrimaryIndex()['parts'] as $part) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321 64
                $pk[] = $this->original[$key][$part[0]];
322 64
            }
323 64
324
            foreach ($this->getMapper()->getPlugins() as $plugin) {
325 64
                $plugin->beforeRemove($instance, $this->space);
326
            }
327
328 64
            if (method_exists($instance, 'beforeRemove')) {
329 64
                $instance->beforeRemove();
0 ignored issues
show
Documentation Bug introduced by
The method beforeRemove does not exist on object<Tarantool\Mapper\Entity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
330 64
            }
331 64
332 64
            $this->getMapper()->getClient()
333
                ->getSpace($this->space->getId())
334 64
                ->delete($pk);
335
336 64
            foreach ($this->getMapper()->getPlugins() as $plugin) {
337
                $plugin->afterRemove($instance, $this->space);
338 64
            }
339
340
            if (method_exists($instance, 'afterRemove')) {
341 64
                $instance->afterRemove();
0 ignored issues
show
Documentation Bug introduced by
The method afterRemove does not exist on object<Tarantool\Mapper\Entity>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
342
            }
343 64
        }
344
345
        unset($this->original[$key]);
346 3
        unset($this->keys[$instance]);
347
348 3
        $this->results = [];
349 3
    }
350 3
351 2
    public function save($instance)
352 2
    {
353 2
        $key = $this->space->getInstanceKey($instance);
354 2
        $client = $this->getMapper()->getClient();
355
356
        if (array_key_exists($key, $this->persisted)) {
357
            // update
358 2
            $tuple = $this->getTuple($instance);
359
            $update = [];
360
361 3 View Code Duplication
            foreach ($tuple as $i => $v) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
362
                if (!array_key_exists($i, $this->original[$key]) || $v !== $this->original[$key][$i]) {
363
                    $update[$i] = $v;
364
                }
365
            }
366
367
            if (!count($update)) {
368
                return $instance;
369
            }
370
371
            foreach ($this->getMapper()->getPlugins() as $plugin) {
372
                $plugin->beforeUpdate($instance, $this->space);
373
            }
374
375
            if (method_exists($instance, 'beforeUpdate')) {
376
                $instance->beforeUpdate();
377
            }
378
379
            $tuple = $this->getTuple($instance);
380
            $update = [];
381
382 View Code Duplication
            foreach ($tuple as $i => $v) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
383
                if (!array_key_exists($i, $this->original[$key]) || $v !== $this->original[$key][$i]) {
384
                    $update[$i] = $v;
385
                }
386
            }
387
388
            if (!count($update)) {
389
                return $instance;
390
            }
391
392
            $operations = [];
393
            foreach ($update as $index => $value) {
394
                $operations[] = ['=', $index, $value];
395
            }
396
397
            $pk = [];
398 View Code Duplication
            foreach ($this->space->getPrimaryIndex()['parts'] as $part) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
399
                $pk[] = $this->original[$key][$part[0]];
400
            }
401
402
            $client->getSpace($this->space->getId())->update($pk, $operations);
403
            $this->original[$key] = $tuple;
404
405
            foreach ($this->getMapper()->getPlugins() as $plugin) {
406
                $plugin->afterUpdate($instance, $this->space);
407
            }
408
409
            if (method_exists($instance, 'afterUpdate')) {
410
                $instance->afterUpdate();
411
            }
412
        } else {
413
            $this->addDefaultValues($instance);
414
            foreach ($this->getMapper()->getPlugins() as $plugin) {
415
                $plugin->beforeCreate($instance, $this->space);
416
            }
417
418
            if (method_exists($instance, 'beforeCreate')) {
419
                $instance->beforeCreate();
420
            }
421
422
            $tuple = $this->getTuple($instance);
423
            $client->getSpace($this->space->getId())->insert($tuple);
424
            $this->persisted[$key] = $instance;
425
            $this->original[$key] = $tuple;
426
427
            foreach ($this->getMapper()->getPlugins() as $plugin) {
428
                $plugin->afterCreate($instance, $this->space);
429
            }
430
431
            if (method_exists($instance, 'afterCreate')) {
432
                $instance->afterCreate();
433
            }
434
        }
435
436
        $this->flushCache();
437
438
        return $instance;
439
    }
440
441
    private function addDefaultValues(Entity $instance)
442
    {
443
        $format = $this->space->getFormat();
444
445
        // complete format fields
446
        foreach ($format as $info) {
447
            $name = $info['name'];
448
            if (!property_exists($instance, $name)) {
449
                $instance->$name = null;
450
            }
451
        }
452
    }
453
454
    public function getOriginal($instance)
455
    {
456
        return $this->original[$this->space->getInstanceKey($instance)];
457
    }
458
459
    private function getTuple(Entity $instance)
460
    {
461
        $schema = $this->getMapper()->getSchema();
462
        $tuple = [];
463
464
        foreach ($this->space->getFormat() as $index => $info) {
465
            $name = $info['name'];
466
            if (!property_exists($instance, $name)) {
467
                $instance->$name = null;
468
            }
469
470
            $instance->$name = $schema->formatValue($info['type'], $instance->$name);
471
            if (is_null($instance->$name)) {
472
                if (!$this->space->isPropertyNullable($name)) {
473
                    $instance->$name = $schema->getDefaultValue($info['type']);
474
                }
475
            }
476
477
            $tuple[$index] = $instance->$name;
478
        }
479
480
        return $tuple;
481
    }
482
483
    public function sync($id, $fields = null)
484
    {
485
        if (array_key_exists($id, $this->persisted)) {
486
            $tuple = $this->getMapper()->getClient()->getSpace($this->space->getId())->select([$id], 0)->getData()[0];
487
488
            foreach ($this->space->getFormat() as $index => $info) {
489
                if (!$fields || in_array($info['name'], $fields)) {
490
                    $value = array_key_exists($index, $tuple) ? $tuple[$index] : null;
491
                    $this->persisted[$id]->{$info['name']} = $value;
492
                    $this->original[$id][$index] = $value;
493
                }
494
            }
495
        }
496
    }
497
498
    public function flushCache()
499
    {
500
        $this->results = [];
501
    }
502
}
503