Completed
Push — master ( 84d771...649726 )
by Dmitry
03:12
created

Repository::save()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 6

Importance

Changes 11
Bugs 3 Features 1
Metric Value
c 11
b 3
f 1
dl 0
loc 29
ccs 18
cts 18
cp 1
rs 8.439
cc 6
eloc 20
nc 5
nop 1
crap 6
1
<?php
2
3
namespace Tarantool\Mapper;
4
5
use BadMethodCallException;
6
use LogicException;
7
8
class Repository implements Contracts\Repository
9
{
10
    private $type;
11
    private $entities = [];
12
    private $keyMap = [];
13
    private $findCache = [];
14
15
    private $magicMethodRules = [
16
        'by' => false,
17
        'firstBy' => true,
18
        'oneBy' => true,
19
    ];
20
21 36
    public function __construct(Contracts\Type $type)
22
    {
23 36
        $this->type = $type;
24 36
    }
25
26 36
    public function create($params = null)
27
    {
28 36
        if ($params && !is_array($params)) {
29 36
            $params = [$params];
30
        }
31
32 36
        $data = [];
33 36
        foreach ($params as $k => $v) {
34 36
            if (is_numeric($k)) {
35 36
                if ($v instanceof Contracts\Entity) {
36 4
                    $type = $this->type->getManager()->findRepository($v)->getType();
37 4
                    $k = $this->type->getReferenceProperty($type);
38
                } else {
39 36
                    $primitive = [];
40 36
                    foreach ($this->type->getProperties() as $property) {
41 36
                        if (!$this->type->isReference($property)) {
42 36
                            $primitive[] = $property;
43
                        }
44
                    }
45 36
                    if (count($primitive) == 2) {
46 36
                        $k = $primitive[1];
47
                    } else {
48 1
                        throw new \Exception("Can't calculate key name");
49
                    }
50
                }
51
            }
52 36
            if (!$this->type->hasProperty($k)) {
53 1
                throw new \Exception("Unknown property $k");
54
            }
55 36
            $data[$k] = $this->type->encodeProperty($k, $v);
56
        }
57
58 36
        return $this->register(new Entity($data));
59
    }
60
61 36
    public function __call($method, $arguments)
62
    {
63 36
        foreach ($this->magicMethodRules as $prefix => $oneItem) {
64 36
            if (substr($method, 0, strlen($prefix)) == $prefix) {
65 36
                $tail = substr($method, strlen($prefix));
66 36
                $fields = array_map('strtolower', explode('And', $tail));
67
68 36
                return $this->find(array_combine($fields, $arguments), $oneItem);
69
            }
70
        }
71
72 1
        throw new BadMethodCallException("Method $method not found");
73
    }
74
75 9
    public function findOne($params)
76
    {
77 9
        return $this->find($params, true);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->find($params, true); of type Tarantool\Mapper\Contrac...pper\Contracts\Entity[] adds the type Tarantool\Mapper\Contracts\Entity[] to the return on line 77 which is incompatible with the return type declared by the interface Tarantool\Mapper\Contracts\Repository::findOne of type Tarantool\Mapper\Contracts\Entity.
Loading history...
78
    }
79
80 36
    public function find($params = [], $oneItem = false)
81
    {
82 36
        $query = [];
83
84 36
        if (is_string($params) && 1 * $params == $params) {
85 1
            $params = 1 * $params;
86
        }
87
88 36
        if (is_int($params)) {
89 8
            if (isset($this->keyMap[$params])) {
90 2
                return $this->entities[$this->keyMap[$params]];
91
            }
92
            $query = [
93 6
                'id' => $params,
94
            ];
95 6
            $oneItem = true;
96
        }
97
98 36
        if ($params instanceof Contracts\Entity) {
99 2
            $params = [$params];
100
        }
101
102 36
        if (is_array($params)) {
103 36
            foreach ($params as $key => $value) {
104 36
                if (is_numeric($key) && $value instanceof Contracts\Entity) {
105 2
                    $type = $this->type->getManager()->findRepository($value)->getType();
106 2
                    $key = $this->type->getReferenceProperty($type);
107
                }
108 36
                if ($this->type->hasProperty($key)) {
109 36
                    $query[$key] = $this->type->encodeProperty($key, $value);
110
                }
111
            }
112
        }
113
114 36
        $findKey = md5(json_encode($query));
115 36
        if (array_key_exists($findKey, $this->findCache)) {
116 36
            return $this->findCache[$findKey];
117
        }
118
119 36
        $index = $this->type->findIndex(array_keys($query));
120 36
        if (!is_numeric($index)) {
121 1
            throw new \Exception('No index found for '.json_encode(array_keys($query)));
122
        }
123
124 36
        $values = count($query) ? $this->type->getIndexTuple($index, $query) : [];
125 36
        $data = $this->type->getSpace()->select($values, $index);
126
127 36
        $result = [];
128 36
        if (!empty($data->getData())) {
129 36
            foreach ($data->getData() as $tuple) {
130 36
                $data = $this->type->fromTuple($tuple);
131 36
                if (isset($data['id']) && array_key_exists($data['id'], $this->keyMap)) {
132 36
                    $entity = $this->entities[$this->keyMap[$data['id']]];
133 36
                    $entity->update($data);
134
                } else {
135 36
                    $entity = new Entity($data);
136 36
                    $this->register($entity);
137
                }
138 36
                if ($oneItem) {
139 36
                    return $this->findCache[$findKey] = $entity;
140
                }
141 8
                $result[] = $entity;
142
            }
143
        }
144 36
        if (!$oneItem) {
145 10
            return $this->findCache[$findKey] = $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->findCache[$findKey] = $result; (array) is incompatible with the return type declared by the interface Tarantool\Mapper\Contracts\Repository::find of type Tarantool\Mapper\Contrac...pper\Contracts\Entity[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
146
        }
147 36
    }
148
149
    /**
150
     * @return Entity
151
     */
152 36
    public function knows(Contracts\Entity $entity)
153
    {
154 36
        return in_array($entity, $this->entities);
155
    }
156
157 3
    public function remove(Contracts\Entity $entity)
158
    {
159 3
        unset($this->entities[$this->keyMap[$entity->id]]);
160 3
        unset($this->keyMap[$entity->id]);
161
162 3
        $this->type->getSpace()->delete([$entity->id]);
0 ignored issues
show
Bug introduced by
Accessing id on the interface Tarantool\Mapper\Contracts\Entity suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
163 3
    }
164
165 1
    public function removeAll()
166
    {
167 1
        foreach ($this->find([]) as $entity) {
0 ignored issues
show
Bug introduced by
The expression $this->find(array()) of type object<Tarantool\Mapper\...pper\Contracts\Entity>> 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...
168 1
            $this->remove($entity);
169
        }
170 1
        $this->flushCache();
171 1
    }
172
173 2
    public function flushCache()
174
    {
175 2
        $this->findCache = [];
176 2
    }
177
178 36
    public function save(Contracts\Entity $entity)
179
    {
180 36
        if (!$this->knows($entity)) {
181 1
            throw new LogicException('Entity is not related with this repository');
182
        }
183
184 36
        if (!$entity->getId()) {
185 36
            $this->generateId($entity);
186 36
            $tuple = $this->type->getCompleteTuple($entity->toArray());
187 36
            $this->type->getSpace()->insert($tuple);
188
        } else {
189 10
            $changes = $entity->pullChanges();
190 10
            if (count($changes)) {
191 10
                $operations = [];
192 10
                foreach ($this->type->getTuple($changes) as $key => $value) {
193 10
                    $operations[] = ['=', $key, $value];
194
                }
195
                try {
196 10
                    $this->type->getSpace()->update($entity->getId(), $operations);
197 1
                } catch (\Exception $e) {
198 1
                    $this->type->getSpace()->delete([$entity->getId()]);
199 1
                    $tuple = $this->type->getCompleteTuple($entity->toArray());
200 1
                    $this->type->getSpace()->insert($tuple);
201
                }
202
            }
203
        }
204
205 36
        return $entity;
206
    }
207
208 36
    private function register(Contracts\Entity $entity)
209
    {
210 36
        if (!$this->knows($entity)) {
211 36
            $this->entities[] = $entity;
212
        }
213 36
        if ($entity->getId() && !array_key_exists($entity->getId(), $this->keyMap)) {
214 36
            $this->keyMap[$entity->getId()] = array_search($entity, $this->entities);
215
        }
216
217 36
        return $entity;
218
    }
219
220 36
    private function generateId(Contracts\Entity $entity)
221
    {
222 36
        $manager = $this->type->getManager();
223 36
        $name = $this->type->getName();
0 ignored issues
show
Unused Code introduced by
$name is not used, you could remove the assignment.

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

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

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

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

Loading history...
224 36
        $spaceId = $this->type->getSpaceId();
225
226 36
        $sequence = $manager->get('sequence')->oneBySpace($spaceId);
227 36
        if (!$sequence) {
228 36
            $sequence = $manager->get('sequence')->create([
229 36
                'space' => $spaceId,
230 36
                'value' => 0,
231
            ]);
232 36
            $manager->save($sequence);
233
        }
234
235 36
        $nextValue = +$manager->getMeta()
236 36
            ->get('sequence')
237 36
            ->getSpace()
238 36
            ->update($sequence->id, [['+', 2, 1]])
239 36
            ->getData()[0][2];
240
241 36
        $entity->setId($nextValue);
242
243 36
        $this->register($entity);
244
245 36
        return $entity;
246
    }
247
248 4
    public function getType()
249
    {
250 4
        return $this->type;
251
    }
252
}
253