Completed
Push — master ( 3b9ac7...e8290d )
by Dmitry
03:08
created

Repository::knows()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Tarantool\Mapper;
6
7
use Exception;
8
use SplObjectStorage;
9
use Tarantool\Client\Schema\Criteria;
10
use Tarantool\Client\Schema\Operations;
11
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...
12
use Tarantool\Mapper\Procedure\FindOrCreate;
13
14
class Repository
15
{
16
    private $space;
17
    private $persisted = [];
18
    private $original = [];
19
    private $keys;
20
21
    private $results = [];
22
23 64
    public function __construct(Space $space)
24
    {
25 64
        $this->space = $space;
26 64
        $this->keys = new SplObjectStorage;
27
    }
28 64
29
    public function create($data) : Entity
30 64
    {
31 64
        $data = (array) $data;
32
        $class = Entity::class;
33
        foreach ($this->getMapper()->getPlugins() as $plugin) {
34 64
            $entityClass = $plugin->getEntityClass($this->space, $data);
35 2
            if ($entityClass) {
36
                if ($class != Entity::class) {
37
                    throw new Exception('Entity class override');
38 64
                }
39 64
                $class = $entityClass;
40 64
            }
41 64
        }
42 4
43 4
        if (array_key_exists(0, $data)) {
44
            $byType = [];
45 64
            foreach ($this->space->getFormat() as $row) {
46 64
                if (!array_key_exists($row['type'], $byType)) {
47 64
                    $byType[$row['type']] = [$row['name']];
48 64
                } else {
49
                    $byType[$row['type']][] = $row['name'];
50
                }
51 64
            }
52 64
            $mapping = [
53
                'is_numeric' => 'unsigned',
54 1
                'is_string' => 'string',
55
                'is_array' => '*',
56
            ];
57
            foreach ($data as $k => $v) {
58 64
                foreach ($mapping as $function => $type) {
59 2
                    if (call_user_func($function, $v)) {
60 2
                        if (array_key_exists($type, $byType) && count($byType[$type]) == 1) {
61
                            $data[$byType[$type][0]] = $v;
62 64
                            unset($data[$k]);
63
                        }
64 64
                    }
65 64
                }
66 64
            }
67 64
        }
68 64
69
        $instance = new $class($this);
70
71
        foreach ($this->space->getFormat() as $row) {
72 64
            if (array_key_exists($row['name'], $data)) {
73
                $instance->{$row['name']} = $data[$row['name']];
74 64
                if ($data[$row['name']] instanceof Entity) {
75
                    $instance->{$row['name']} = $instance->{$row['name']}->id;
76
                }
77 64
            }
78
        }
79 64
80
        foreach ($this->getMapper()->getPlugins() as $plugin) {
81 64
            $plugin->generateKey($instance, $this->space);
82
            $plugin->afterInstantiate($instance, $this->space);
83
        }
84 64
85
        // validate instance key
86 64
        $key = $this->space->getInstanceKey($instance);
87 64
88 64
        foreach ($this->keys as $_) {
89 64
            if ($this->keys[$_] == $key) {
90
                throw new Exception($this->space->getName().' '.json_encode($key).' exists');
91 64
            }
92
        }
93
94
        $this->keys[$instance] = $key;
95 1
        return $instance;
96
    }
97
98 19
    public function findOne($params = []) : ?Entity
99
    {
100 19
        return $this->find($params, true);
101
    }
102
103 64
    public function findOrCreate($params = []) : Entity
104
    {
105 64
        $space = $this->getSpace();
106
107 64
        if ($space->getName() != '_procedure') {
108 1
109
            $result = $this->getMapper()
110
                ->getPlugin(Procedure::class)
111 64
                ->get(FindOrCreate::class)
112 10
                ->execute($space, $this->normalize($params));
113 3
114
            if ($result['created']) {
115
                $this->flushCache();
116 7
            }
117
118 7
            $instance = $this->findOrFail($result['key']);
119
            if ($result['created']) {
120
                if (method_exists($instance, 'beforeCreate')) {
121 64
                    $instance->beforeCreate();
0 ignored issues
show
Documentation Bug introduced by
The method beforeCreate 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...
122 2
                    $instance->save();
123
                }
124
                foreach ($this->getMapper()->getPlugins() as $plugin) {
125 64
                    $plugin->beforeCreate($instance, $space);
126 64
                }
127
128 64
                foreach ($this->getMapper()->getPlugins() as $plugin) {
129 64
                    $plugin->afterCreate($instance, $space);
130 18
                }
131 64
                if (method_exists($instance, 'afterCreate')) {
132
                    $instance->afterCreate();
0 ignored issues
show
Documentation Bug introduced by
The method afterCreate 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...
133
                }
134
                $this->flushCache();
135 64
            }
136 1
        }
137
138
        $entity = $this->findOne($params);
139 64
        if (!$entity) {
140 64
            $entity = $this->create($params);
141 2
        }
142 2
        return $entity;
143
    }
144 64
145 64
    public function findOrFail($params = []) : Entity
146
    {
147 1
        $entity = $this->findOne($params);
148 64
        if (!$entity) {
149
            throw new Exception("No ".$this->getSpace()->getName().' found using '.json_encode($params));
150
        }
151
        return $entity;
152
    }
153 64
154 64
    public function normalize($params) : array
155 64
    {
156
        if (!is_array($params)) {
157
            $params = [$params];
158 64
        }
159 64
160 2
        if (count($params) == 1 && array_key_exists(0, $params)) {
161
            $primary = $this->space->getPrimaryIndex();
162
            if (count($primary['parts']) == 1) {
163 64
                $formatted = $this->getMapper()->getSchema()->formatValue($primary['parts'][0][1], $params[0]);
164
                if ($params[0] == $formatted) {
165 64
                    $params = [
166 1
                        $this->space->getFormat()[$primary['parts'][0][0]]['name'] => $params[0]
167 1
                    ];
168 1
                }
169 1
            }
170 1
        }
171
172
        return $params;
173 1
    }
174
175
    public function find($params = [], $one = false)
176 1
    {
177
        $cacheKey = json_encode(func_get_args());
178 1
179 1
        if (array_key_exists($cacheKey, $this->results)) {
180
            return $this->results[$cacheKey];
181 1
        }
182
183
        $params = $this->normalize($params);
184 64
185
        if (array_key_exists('id', $params)) {
186 64
            if (array_key_exists($params['id'], $this->persisted)) {
187 64
                $instance = $this->persisted[$params['id']];
188 64
                return $one ? $instance : [$instance];
189 64
            }
190 64
        }
191 64
192 64
        $space = $this->space;
193
        $index = $space->castIndex($params);
194 64
        if (is_null($index)) {
195
            throw new Exception("No index for params ".json_encode($params));
196 64
        }
197 64
198
        $criteria = Criteria::index($index)
199 14
            ->andKey($space->getIndexValues($index, $params));
200
201
        if ($space->getIndextype($index) == 'hash' && !count($params)) {
202 64
            $criteria = $criteria->allIterator();
203 64
        }
204
205 17
        $data = $this->getMapper()
206
            ->getClient()
207
            ->getSpace($space->getName())
208
            ->select($criteria);
209
210
        $result = [];
211
        foreach ($data as $tuple) {
212 64
            $instance = $this->getInstance($tuple);
213
            if ($one) {
214 64
                return $this->results[$cacheKey] = $instance;
215
            }
216
            $result[] = $instance;
217 6
        }
218
219 6
        if ($one) {
220 6
            return $this->results[$cacheKey] = null;
221 6
        }
222
223 6
        return $this->results[$cacheKey] = $result;
224 6
    }
225
226 1
    public function forget(int $id) : self
227
    {
228 1
        if (array_key_exists($id, $this->persisted)) {
229 1
            unset($this->persisted[$id]);
230 1
        }
231
232 1
        return $this;
233 1
    }
234
235 64
    public function getInstance(array $tuple) : Entity
236
    {
237 64
        $key = $this->space->getTupleKey($tuple);
238 64
239
        if (array_key_exists($key, $this->persisted)) {
240 64
            return $this->persisted[$key];
241
        }
242 64
243 1
        $data = [];
244
        foreach ($this->space->getFormat() as $index => $info) {
245
            $data[$info['name']] = array_key_exists($index, $tuple) ? $tuple[$index] : null;
246 64
        }
247 64
248 64
        $class = Entity::class;
249 64
        foreach ($this->getMapper()->getPlugins() as $plugin) {
250
            $entityClass = $plugin->getEntityClass($this->space, $data);
251 19
            if ($entityClass) {
252 19
                if ($class != Entity::class) {
253 19
                    throw new Exception('Entity class override');
254 19
                }
255
                $class = $entityClass;
256
            }
257 19
        }
258 19
259 4
        $instance = new $class($this);
260 19
261 19
        $this->original[$key] = $tuple;
262
263
        foreach ($data as $k => $v) {
264
            $instance->$k = $v;
265
        }
266 19
267 19
        $this->keys->offsetSet($instance, $key);
268 1
269 19
        foreach ($this->getMapper()->getPlugins() as $plugin) {
270
            $plugin->afterInstantiate($instance);
271
        }
272
273 18
        return $this->persisted[$key] = $instance;
274 18
    }
275 18
276 18
    public function getMapper() : Mapper
277
    {
278
        return $this->space->getMapper();
279 18
    }
280 17
281 17
    public function getSpace() : Space
282 17
    {
283 1
        return $this->space;
284 1
    }
285 1
286 1
    public function knows(Entity $instance) : bool
287
    {
288 18
        return $this->keys->offsetExists($instance);
289
    }
290
291
    public function truncate() : self
292 64
    {
293
        $this->results = [];
294 64
        $name = $this->space->getName();
295
        $this->getMapper()->getClient()->call("box.space.$name:truncate");
296
297 64
        return $this;
298
    }
299 64
300 64
    public function remove($params = []) : self
301
    {
302 64
        if ($params instanceof Entity) {
303 64
            return $this->removeEntity($params);
304
        }
305
306 64
        if (!count($params)) {
307 64
            throw new Exception("Use truncate to flush space");
308
        }
309
310 64
        foreach ($this->find($params) as $entity) {
311
            $this->removeEntity($entity);
312
        }
313 64
314
        return $this;
315 64
    }
316 64
317 64
    public function removeEntity(Entity $instance) : self
318
    {
319 64
        $key = $this->space->getInstanceKey($instance);
320 64
321 64
        if (!array_key_exists($key, $this->original)) {
322 64
            return $this;
323 64
        }
324
325 64
        if (array_key_exists($key, $this->persisted)) {
326
            unset($this->persisted[$key]);
327
328 64
            $pk = [];
329 64
            foreach ($this->space->getPrimaryIndex()['parts'] as $part) {
330 64
                $pk[] = $this->original[$key][$part[0]];
331 64
            }
332 64
333
            foreach ($this->getMapper()->getPlugins() as $plugin) {
334 64
                $plugin->beforeRemove($instance, $this->space);
335
            }
336 64
337
            if (method_exists($instance, 'beforeRemove')) {
338 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...
339
            }
340
341 64
            $this->getMapper()->getClient()
342
                ->getSpaceById($this->space->getId())
343 64
                ->delete($pk);
344
345
            foreach ($this->getMapper()->getPlugins() as $plugin) {
346 3
                $plugin->afterRemove($instance, $this->space);
347
            }
348 3
349 3
            if (method_exists($instance, 'afterRemove')) {
350 3
                $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...
351 2
            }
352 2
        }
353 2
354 2
        unset($this->original[$key]);
355
        unset($this->keys[$instance]);
356
357
        $this->results = [];
358 2
        return $this;
359
    }
360
361 3
    public function save(Entity $instance) : Entity
362
    {
363
        $key = $this->space->getInstanceKey($instance);
364
        $client = $this->getMapper()->getClient();
365
366
        if (array_key_exists($key, $this->persisted)) {
367
            // update
368
            $tuple = $this->getTuple($instance);
369
            $update = [];
370
371
            foreach ($tuple as $i => $v) {
372
                if (!array_key_exists($i, $this->original[$key]) || $v !== $this->original[$key][$i]) {
373
                    $update[$i] = $v;
374
                }
375
            }
376
377
            if (!count($update)) {
378
                return $instance;
379
            }
380
381
            foreach ($this->getMapper()->getPlugins() as $plugin) {
382
                $plugin->beforeUpdate($instance, $this->space);
383
            }
384
385
            if (method_exists($instance, 'beforeUpdate')) {
386
                $instance->beforeUpdate();
0 ignored issues
show
Documentation Bug introduced by
The method beforeUpdate 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...
387
            }
388
389
            $tuple = $this->getTuple($instance);
390
            $update = [];
391
392
            foreach ($tuple as $i => $v) {
393
                if (!array_key_exists($i, $this->original[$key]) || $v !== $this->original[$key][$i]) {
394
                    $update[$i] = $v;
395
                }
396
            }
397
398
            if (!count($update)) {
399
                return $instance;
400
            }
401
402
            $operations = null;
403
            foreach ($update as $index => $value) {
404
                $operations = $operations ? $operations->set($index, $value) : Operations::set($index, $value);
405
            }
406
407
            $pk = [];
408
            foreach ($this->space->getPrimaryIndex()['parts'] as $part) {
409
                $pk[] = $this->original[$key][$part[0]];
410
            }
411
412
            $client->getSpaceById($this->space->getId())->update($pk, $operations);
413
            $this->original[$key] = $tuple;
414
415
            foreach ($this->getMapper()->getPlugins() as $plugin) {
416
                $plugin->afterUpdate($instance, $this->space);
417
            }
418
419
            if (method_exists($instance, 'afterUpdate')) {
420
                $instance->afterUpdate();
0 ignored issues
show
Documentation Bug introduced by
The method afterUpdate 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...
421
            }
422
        } else {
423
            $this->addDefaultValues($instance);
424
            foreach ($this->getMapper()->getPlugins() as $plugin) {
425
                $plugin->beforeCreate($instance, $this->space);
426
            }
427
428
            if (method_exists($instance, 'beforeCreate')) {
429
                $instance->beforeCreate();
0 ignored issues
show
Documentation Bug introduced by
The method beforeCreate 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...
430
            }
431
432
            $tuple = $this->getTuple($instance);
433
            $client->getSpaceById($this->space->getId())->insert($tuple);
434
            $this->persisted[$key] = $instance;
435
            $this->original[$key] = $tuple;
436
437
            foreach ($this->getMapper()->getPlugins() as $plugin) {
438
                $plugin->afterCreate($instance, $this->space);
439
            }
440
441
            if (method_exists($instance, 'afterCreate')) {
442
                $instance->afterCreate();
0 ignored issues
show
Documentation Bug introduced by
The method afterCreate 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...
443
            }
444
        }
445
446
        $this->flushCache();
447
448
        return $instance;
449
    }
450
451
    private function addDefaultValues(Entity $instance) : Entity
452
    {
453
        $format = $this->space->getFormat();
454
455
        // complete format fields
456
        foreach ($format as $info) {
457
            $name = $info['name'];
458
            if (!property_exists($instance, $name)) {
459
                $instance->$name = null;
460
            }
461
        }
462
463
        return $instance;
464
    }
465
466
    public function getOriginal(Entity $instance) : array
467
    {
468
        return $this->original[$this->space->getInstanceKey($instance)];
469
    }
470
471
    private function getTuple(Entity $instance) : array
472
    {
473
        $schema = $this->getMapper()->getSchema();
474
        $tuple = [];
475
476
        foreach ($this->space->getFormat() as $index => $info) {
477
            $name = $info['name'];
478
            if (!property_exists($instance, $name)) {
479
                $instance->$name = null;
480
            }
481
482
            $instance->$name = $schema->formatValue($info['type'], $instance->$name);
483
            if (is_null($instance->$name)) {
484
                if ($this->space->hasDefaultValue($name)) {
485
                    $instance->$name = $this->space->getDefaultValue($name);
486
                } elseif (!$this->space->isPropertyNullable($name)) {
487
                    $instance->$name = $schema->getDefaultValue($info['type']);
488
                }
489
            }
490
491
            $tuple[$index] = $instance->$name;
492
        }
493
494
        return $tuple;
495
    }
496
497
    public function sync(int $id, string $fields = null) : ?Entity
498
    {
499
        if (array_key_exists($id, $this->persisted)) {
500
            $tuple = $this->getMapper()->getClient()->getSpaceById($this->space->getId())->select(Criteria::key([$id]))[0];
501
502
            foreach ($this->space->getFormat() as $index => $info) {
503
                if (!$fields || in_array($info['name'], $fields)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
504
                    $value = array_key_exists($index, $tuple) ? $tuple[$index] : null;
505
                    $this->persisted[$id]->{$info['name']} = $value;
506
                    $this->original[$id][$index] = $value;
507
                }
508
            }
509
510
            return $this->persisted[$id];
511
        }
512
    }
513
514
    public function flushCache() : self
515
    {
516
        $this->results = [];
517
        return $this;
518
    }
519
}
520