Completed
Push — master ( 51c809...50c783 )
by Dmitry
03:19
created

Repository::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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
    private $original = [];
15
16
    private $magicMethodRules = [
17
        'by' => false,
18
        'firstBy' => true,
19
        'oneBy' => true,
20
    ];
21
22 52
    public function __construct(Contracts\Type $type)
23
    {
24 52
        $this->type = $type;
25 52
    }
26
27 52
    public function create($params = null)
28
    {
29 52
        if ($params && !is_array($params)) {
30 52
            $params = [$params];
31
        }
32
33 52
        if (!is_array($params)) {
34 1
            $params = [];
35
        }
36
37 52
        $data = [];
38 52
        foreach ($params as $k => $v) {
39 52
            if (is_numeric($k)) {
40 52
                if ($v instanceof Contracts\Entity) {
41 4
                    $type = $this->type->getManager()->findRepository($v)->getType();
42 4
                    $k = $this->type->getReferenceProperty($type);
43
                } else {
44 52
                    $primitive = [];
45 52
                    foreach ($this->type->getProperties() as $property) {
46 52
                        if (!$this->type->isReference($property)) {
47 52
                            $primitive[] = $property;
48
                        }
49
                    }
50 52
                    if (count($primitive) == 2) {
51 52
                        $k = $primitive[1];
52
                    } else {
53 1
                        throw new \Exception("Can't calculate key name");
54
                    }
55
                }
56
            }
57 52
            if (!$this->type->hasProperty($k)) {
58 1
                throw new \Exception("Unknown property $k");
59
            }
60 52
            $data[$k] = $this->type->encodeProperty($k, $v);
61
        }
62
63 52
        $this->flushCache();
64
65 52
        return $this->register(new Entity($data));
66
    }
67
68 52
    public function __call($method, $arguments)
69
    {
70 52
        foreach ($this->magicMethodRules as $prefix => $oneItem) {
71 52
            if (substr($method, 0, strlen($prefix)) == $prefix) {
72 52
                $tail = substr($method, strlen($prefix));
73 52
                $fields = array_map('strtolower', explode('And', $tail));
74
75 52
                return $this->find(array_combine($fields, $arguments), $oneItem);
76
            }
77
        }
78
79 1
        throw new BadMethodCallException("Method $method not found");
80
    }
81
82 16
    public function findOne($params)
83
    {
84 16
        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 84 which is incompatible with the return type declared by the interface Tarantool\Mapper\Contracts\Repository::findOne of type Tarantool\Mapper\Contracts\Entity.
Loading history...
85
    }
86
87 52
    public function find($params = [], $oneItem = false)
88
    {
89 52
        $query = [];
90
91 52
        if (is_string($params) && 1 * $params == $params) {
92 1
            $params = 1 * $params;
93
        }
94
95 52
        if (is_int($params)) {
96 9
            if (isset($this->keyMap[$params])) {
97 3
                return $this->entities[$this->keyMap[$params]];
98
            }
99
            $query = [
100 6
                'id' => $params,
101
            ];
102 6
            $oneItem = true;
103
        }
104
105 52
        if ($params instanceof Contracts\Entity) {
106 2
            $params = [$params];
107
        }
108
109 52
        if (is_array($params)) {
110 52
            foreach ($params as $key => $value) {
111 52
                if (is_numeric($key) && $value instanceof Contracts\Entity) {
112 2
                    $type = $this->type->getManager()->findRepository($value)->getType();
113 2
                    $key = $this->type->getReferenceProperty($type);
114
                }
115 52
                if ($this->type->hasProperty($key)) {
116 52
                    $query[$key] = $this->type->encodeProperty($key, $value);
117
                }
118
            }
119
        }
120
121 52
        $findKey = md5(json_encode($query).($oneItem ? 'x' : ''));
122 52
        if (array_key_exists($findKey, $this->findCache)) {
123 52
            return $this->findCache[$findKey];
124
        }
125
126 52
        $index = $this->type->findIndex(array_keys($query));
127 52
        if (!is_numeric($index)) {
128 2
            throw new \Exception('No index found for '.json_encode(array_keys($query)));
129
        }
130
131 52
        $values = count($query) ? $this->type->getIndexTuple($index, $query) : [];
132 52
        $data = $this->type->getSpace()->select($values, $index);
133
134 52
        $result = [];
135 52
        if (!empty($data->getData())) {
136 52
            foreach ($data->getData() as $tuple) {
137 52
                $data = $this->type->fromTuple($tuple);
138 52
                if (isset($data['id']) && array_key_exists($data['id'], $this->keyMap)) {
139 52
                    $entity = $this->entities[$this->keyMap[$data['id']]];
140 52
                    $entity->update($data);
141
                } else {
142 52
                    $entity = $this->create($data);
143
                }
144 52
                if ($oneItem) {
145 52
                    return $this->findCache[$findKey] = $entity;
146
                }
147 13
                $result[] = $entity;
148
            }
149
        }
150 52
        if (!$oneItem) {
151 16
            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...
152
        }
153 52
    }
154
155
    /**
156
     * @return Entity
157
     */
158 52
    public function knows(Contracts\Entity $entity)
159
    {
160 52
        return in_array($entity, $this->entities);
161
    }
162
163 7
    public function remove(Contracts\Entity $entity)
164
    {
165 7
        unset($this->entities[$this->keyMap[$entity->id]]);
166 7
        unset($this->keyMap[$entity->id]);
167 7
        $this->flushCache();
168
169 7
        $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...
170 7
    }
171
172 1
    public function removeAll()
173
    {
174 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...
175 1
            $this->remove($entity);
176
        }
177 1
        $this->flushCache();
178 1
    }
179
180 52
    public function flushCache()
181
    {
182 52
        $this->findCache = [];
183 52
    }
184
185 52
    public function save(Contracts\Entity $entity)
186
    {
187 52
        if (!$this->knows($entity)) {
188 1
            throw new LogicException('Entity is not related with this repository');
189
        }
190
191 52
        if (!$entity->getId()) {
192 52
            $this->generateId($entity);
193 52
            $tuple = $this->type->getCompleteTuple($entity->toArray());
194 52
            $this->type->getSpace()->insert($tuple);
195
        } else {
196 13
            $array = $entity->toArray(false);
197 13
            $changes = [];
198 13
            $id = $entity->getId();
199 13
            if (!array_key_exists($id, $this->original)) {
200
                $changes = $array;
201
            } else {
202 13
                foreach ($array as $k => $v) {
203 13
                    if (!array_key_exists($k, $this->original[$id])) {
204 2
                        $changes[$k] = $v;
205 13
                    } elseif ($v !== $this->original[$id][$k]) {
206 13
                        $changes[$k] = $v;
207
                    }
208
                }
209
            }
210 13
            if (count($changes)) {
211 13
                $operations = [];
212 13
                foreach ($this->type->getTuple($changes) as $key => $value) {
213 13
                    $operations[] = ['=', $key, $value];
214
                }
215
                try {
216 13
                    $this->type->getSpace()->update($id, $operations);
217 1
                } catch (\Exception $e) {
218 1
                    $this->type->getSpace()->delete([$id]);
219 1
                    $tuple = $this->type->getCompleteTuple($entity->toArray());
220 1
                    $this->type->getSpace()->insert($tuple);
221
                }
222 13
                $this->original[$id] = $entity->toArray();
223
            }
224
        }
225
226 52
        return $entity;
227
    }
228
229 52
    private function register(Contracts\Entity $entity)
230
    {
231 52
        if (!$this->knows($entity)) {
232 52
            $this->entities[] = $entity;
233
        }
234 52
        if ($entity->getId() && !array_key_exists($entity->getId(), $this->keyMap)) {
235 52
            $this->keyMap[$entity->getId()] = array_search($entity, $this->entities);
236
        }
237
238 52
        if ($entity->getId()) {
239 52
            $this->original[$entity->getId()] = $entity->toArray();
240
        }
241
242 52
        return $entity;
243
    }
244
245 52
    private function generateId(Contracts\Entity $entity)
246
    {
247 52
        $manager = $this->type->getManager();
248 52
        $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...
249 52
        $spaceId = $this->type->getSpaceId();
250
251 52
        $sequence = $manager->get('sequence')->oneBySpace($spaceId);
252 52
        if (!$sequence) {
253 52
            $sequence = $manager->get('sequence')->create([
254 52
                'space' => $spaceId,
255 52
                'value' => 0,
256
            ]);
257 52
            $manager->save($sequence);
258
        }
259
260 52
        $nextValue = +$manager->getMeta()
261 52
            ->get('sequence')
262 52
            ->getSpace()
263 52
            ->update($sequence->id, [['+', 2, 1]])
264 52
            ->getData()[0][2];
265
266 52
        $entity->setId($nextValue);
267
268 52
        $this->register($entity);
269
270 52
        return $entity;
271
    }
272
273 4
    public function getType()
274
    {
275 4
        return $this->type;
276
    }
277
}
278