Completed
Push — master ( aff20d...34dcc5 )
by Dmitry
01:52
created

Repository::findOrCreate()   D

Complexity

Conditions 9
Paths 22

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 41
ccs 21
cts 21
cp 1
rs 4.909
c 0
b 0
f 0
cc 9
eloc 25
nc 22
nop 1
crap 9
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
        $space = $this->getSpace();
102
103 64
        if ($space->getName() != '_procedure') {
104
105 64
            $result = $this->getMapper()
106
                ->getPlugin(Procedure::class)
107 64
                ->get(FindOrCreate::class)
108 1
                ->execute($space, $this->normalize($params));
109
110
            if ($result['created']) {
111 64
                $this->flushCache();
112 10
            }
113 3
114
            $instance = $this->findOrFail($result['key']);
115
            if ($result['created']) {
116 7
                if (method_exists($instance, 'beforeCreate')) {
117
                    $instance->beforeCreate();
118 7
                    $instance->save();
119
                }
120
                foreach ($this->getMapper()->getPlugins() as $plugin) {
121 64
                    $plugin->beforeCreate($instance, $space);
122 2
                }
123
124
                foreach ($this->getMapper()->getPlugins() as $plugin) {
125 64
                    $plugin->afterCreate($instance, $space);
126 64
                }
127
                if (method_exists($instance, 'afterCreate')) {
128 64
                    $instance->afterCreate();
129 64
                }
130 18
                $this->flushCache();
131 64
            }
132
        }
133
134
        $entity = $this->findOne($params);
135 64
        if (!$entity) {
136 1
            $entity = $this->create($params);
137
        }
138
        return $entity;
139 64
    }
140 64
141 2
    public function findOrFail($params = [])
142 2
    {
143
        $entity = $this->findOne($params);
144 64
        if (!$entity) {
145 64
            throw new Exception("No ".$this->getSpace()->getName().' found using '.json_encode($params));
146
        }
147 1
        return $entity;
148 64
    }
149
150
    public function normalize($params)
151
    {
152
        if (!is_array($params)) {
153 64
            $params = [$params];
154 64
        }
155 64
156
        if (count($params) == 1 && array_key_exists(0, $params)) {
157
            $primary = $this->space->getPrimaryIndex();
158 64
            if (count($primary['parts']) == 1) {
159 64
                $formatted = $this->getMapper()->getSchema()->formatValue($primary['parts'][0][1], $params[0]);
160 2
                if ($params[0] == $formatted) {
161
                    $params = [
162
                        $this->space->getFormat()[$primary['parts'][0][0]]['name'] => $params[0]
163 64
                    ];
164
                }
165 64
            }
166 1
        }
167 1
168 1
        return $params;
169 1
    }
170 1
171
    public function find($params = [], $one = false)
172
    {
173 1
        $cacheKey = json_encode(func_get_args());
174
175
        if (array_key_exists($cacheKey, $this->results)) {
176 1
            return $this->results[$cacheKey];
177
        }
178 1
179 1
        $params = $this->normalize($params);
180
181 1
        if (array_key_exists('id', $params)) {
182
            if (array_key_exists($params['id'], $this->persisted)) {
183
                $instance = $this->persisted[$params['id']];
184 64
                return $one ? $instance : [$instance];
185
            }
186 64
        }
187 64
188 64
189 64
        $index = $this->space->castIndex($params);
190 64
        if (is_null($index)) {
191 64
            throw new Exception("No index for params ".json_encode($params));
192 64
        }
193
194 64
        $client = $this->getMapper()->getClient();
195
        $values = $this->space->getIndexValues($index, $params);
196 64
        if ($this->space->getIndextype($index) == 'hash' && !count($values)) {
197 64
            //  iterator box.index.ALL == 2
198
            $data = $client->getSpace($this->space->getId())->select($values, $index, null, null, 2)->getData();
199 14
        } else {
200
            $data = $client->getSpace($this->space->getId())->select($values, $index)->getData();
201
        }
202 64
203 64
        $result = [];
204
        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...
205 17
            $instance = $this->getInstance($tuple);
206
            if ($one) {
207
                return $this->results[$cacheKey] = $instance;
208
            }
209
            $result[] = $instance;
210
        }
211
212 64
        if ($one) {
213
            return $this->results[$cacheKey] = null;
214 64
        }
215
216
        return $this->results[$cacheKey] = $result;
217 6
    }
218
219 6
    public function forget($id)
220 6
    {
221 6
        if (array_key_exists($id, $this->persisted)) {
222
            unset($this->persisted[$id]);
223 6
        }
224 6
    }
225
226 1
    public function getInstance($tuple)
227
    {
228 1
        $key = $this->space->getTupleKey($tuple);
229 1
230 1
        if (array_key_exists($key, $this->persisted)) {
231
            return $this->persisted[$key];
232 1
        }
233 1
234
        $class = Entity::class;
235 64 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...
236
            $entityClass = $plugin->getEntityClass($this->space);
237 64
            if ($entityClass) {
238 64
                if ($class != Entity::class) {
239
                    throw new Exception('Entity class override');
240 64
                }
241
                $class = $entityClass;
242 64
            }
243 1
        }
244
245
        $instance = new $class($this);
246 64
247 64
        $this->original[$key] = $tuple;
248 64
249 64 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...
250
            $instance->{$info['name']} = array_key_exists($index, $tuple) ? $tuple[$index] : null;
251 19
        }
252 19
253 19
        $this->keys->offsetSet($instance, $key);
254 19
255
        foreach ($this->getMapper()->getPlugins() as $plugin) {
256
            $plugin->afterInstantiate($instance);
257 19
        }
258 19
259 4
        return $this->persisted[$key] = $instance;
260 19
    }
261 19
262
    public function getMapper()
263
    {
264
        return $this->space->getMapper();
265
    }
266 19
267 19
    public function getSpace()
268 1
    {
269 19
        return $this->space;
270
    }
271
272
    public function knows($instance)
273 18
    {
274 18
        return $this->keys->offsetExists($instance);
275 18
    }
276 18
277
    public function update(Entity $instance, $operations)
278
    {
279 18
        if (!count($operations)) {
280 17
            return;
281 17
        }
282 17
283 1
        $tupleOperations = [];
284 1
        foreach ($operations as $operation) {
285 1
            $tupleIndex = $this->space->getPropertyIndex($operation[1]);
286 1
            $tupleOperations[] = [$operation[0], $tupleIndex, $operation[2]];
287
        }
288 18
289
        $pk = [];
290
        foreach ($this->space->getPrimaryIndex()['parts'] as $part) {
291
            $pk[] = $instance->{$this->space->getFormat()[$part[0]]['name']};
292 64
        }
293
294 64
        $client = $this->getMapper()->getClient();
295
        $result = $client->getSpace($this->space->getId())->update($pk, $tupleOperations);
296
        foreach ($result->getData() as $tuple) {
297 64 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...
298
                if (array_key_exists($index, $tuple)) {
299 64
                    $instance->{$info['name']} = $tuple[$index];
300 64
                }
301
            }
302 64
        }
303 64
    }
304
305
    public function truncate()
306 64
    {
307 64
        $this->results = [];
308
        $id = $this->space->getId();
309
        $this->getMapper()->getClient()->evaluate("box.space[$id]:truncate()");
310 64
    }
311
312
    public function remove($params = [])
313 64
    {
314
        if ($params instanceof Entity) {
315 64
            return $this->removeEntity($params);
316 64
        }
317 64
318
        if (!count($params)) {
319 64
            throw new Exception("Use truncate to flush space");
320 64
        }
321 64
322 64
        foreach ($this->find($params) as $entity) {
323 64
            $this->removeEntity($entity);
324
        }
325 64
    }
326
327
    public function removeEntity(Entity $instance)
328 64
    {
329 64
        $key = $this->space->getInstanceKey($instance);
330 64
331 64
        if (!array_key_exists($key, $this->original)) {
332 64
            return;
333
        }
334 64
335
        if (array_key_exists($key, $this->persisted)) {
336 64
            unset($this->persisted[$key]);
337
338 64
            $pk = [];
339 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...
340
                $pk[] = $this->original[$key][$part[0]];
341 64
            }
342
343 64
            foreach ($this->getMapper()->getPlugins() as $plugin) {
344
                $plugin->beforeRemove($instance, $this->space);
345
            }
346 3
347
            if (method_exists($instance, 'beforeRemove')) {
348 3
                $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...
349 3
            }
350 3
351 2
            $this->getMapper()->getClient()
352 2
                ->getSpace($this->space->getId())
353 2
                ->delete($pk);
354 2
355
            foreach ($this->getMapper()->getPlugins() as $plugin) {
356
                $plugin->afterRemove($instance, $this->space);
357
            }
358 2
359
            if (method_exists($instance, 'afterRemove')) {
360
                $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...
361 3
            }
362
        }
363
364
        unset($this->original[$key]);
365
        unset($this->keys[$instance]);
366
367
        $this->results = [];
368
    }
369
370
    public function save($instance)
371
    {
372
        $key = $this->space->getInstanceKey($instance);
373
        $client = $this->getMapper()->getClient();
374
375
        if (array_key_exists($key, $this->persisted)) {
376
            // update
377
            $tuple = $this->getTuple($instance);
378
            $update = [];
379
380 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...
381
                if (!array_key_exists($i, $this->original[$key]) || $v !== $this->original[$key][$i]) {
382
                    $update[$i] = $v;
383
                }
384
            }
385
386
            if (!count($update)) {
387
                return $instance;
388
            }
389
390
            foreach ($this->getMapper()->getPlugins() as $plugin) {
391
                $plugin->beforeUpdate($instance, $this->space);
392
            }
393
394
            if (method_exists($instance, 'beforeUpdate')) {
395
                $instance->beforeUpdate();
396
            }
397
398
            $tuple = $this->getTuple($instance);
399
            $update = [];
400
401 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...
402
                if (!array_key_exists($i, $this->original[$key]) || $v !== $this->original[$key][$i]) {
403
                    $update[$i] = $v;
404
                }
405
            }
406
407
            if (!count($update)) {
408
                return $instance;
409
            }
410
411
            $operations = [];
412
            foreach ($update as $index => $value) {
413
                $operations[] = ['=', $index, $value];
414
            }
415
416
            $pk = [];
417 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...
418
                $pk[] = $this->original[$key][$part[0]];
419
            }
420
421
            $client->getSpace($this->space->getId())->update($pk, $operations);
422
            $this->original[$key] = $tuple;
423
424
            foreach ($this->getMapper()->getPlugins() as $plugin) {
425
                $plugin->afterUpdate($instance, $this->space);
426
            }
427
428
            if (method_exists($instance, 'afterUpdate')) {
429
                $instance->afterUpdate();
430
            }
431
        } else {
432
            $this->addDefaultValues($instance);
433
            foreach ($this->getMapper()->getPlugins() as $plugin) {
434
                $plugin->beforeCreate($instance, $this->space);
435
            }
436
437
            if (method_exists($instance, 'beforeCreate')) {
438
                $instance->beforeCreate();
439
            }
440
441
            $tuple = $this->getTuple($instance);
442
            $client->getSpace($this->space->getId())->insert($tuple);
443
            $this->persisted[$key] = $instance;
444
            $this->original[$key] = $tuple;
445
446
            foreach ($this->getMapper()->getPlugins() as $plugin) {
447
                $plugin->afterCreate($instance, $this->space);
448
            }
449
450
            if (method_exists($instance, 'afterCreate')) {
451
                $instance->afterCreate();
452
            }
453
        }
454
455
        $this->flushCache();
456
457
        return $instance;
458
    }
459
460
    private function addDefaultValues(Entity $instance)
461
    {
462
        $format = $this->space->getFormat();
463
464
        // complete format fields
465
        foreach ($format as $info) {
466
            $name = $info['name'];
467
            if (!property_exists($instance, $name)) {
468
                $instance->$name = null;
469
            }
470
        }
471
    }
472
473
    public function getOriginal($instance)
474
    {
475
        return $this->original[$this->space->getInstanceKey($instance)];
476
    }
477
478
    private function getTuple(Entity $instance)
479
    {
480
        $schema = $this->getMapper()->getSchema();
481
        $tuple = [];
482
483
        foreach ($this->space->getFormat() as $index => $info) {
484
            $name = $info['name'];
485
            if (!property_exists($instance, $name)) {
486
                $instance->$name = null;
487
            }
488
489
            $instance->$name = $schema->formatValue($info['type'], $instance->$name);
490
            if (is_null($instance->$name)) {
491
                if (!$this->space->isPropertyNullable($name)) {
492
                    $instance->$name = $schema->getDefaultValue($info['type']);
493
                }
494
            }
495
496
            $tuple[$index] = $instance->$name;
497
        }
498
499
        return $tuple;
500
    }
501
502
    public function sync($id, $fields = null)
503
    {
504
        if (array_key_exists($id, $this->persisted)) {
505
            $tuple = $this->getMapper()->getClient()->getSpace($this->space->getId())->select([$id], 0)->getData()[0];
506
507
            foreach ($this->space->getFormat() as $index => $info) {
508
                if (!$fields || in_array($info['name'], $fields)) {
509
                    $value = array_key_exists($index, $tuple) ? $tuple[$index] : null;
510
                    $this->persisted[$id]->{$info['name']} = $value;
511
                    $this->original[$id][$index] = $value;
512
                }
513
            }
514
        }
515
    }
516
517
    public function flushCache()
518
    {
519
        $this->results = [];
520
    }
521
}
522