Completed
Push — master ( 58cc07...1af072 )
by Dmitry
02:35
created

Repository::addDefaultValues()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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