Completed
Push — master ( 063706...c4b070 )
by Dmitry
04:24
created

Repository::knows()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Tarantool\Mapper;
4
5
use BadMethodCallException;
6
use Exception;
7
use LogicException;
8
9
class Repository implements Contracts\Repository
10
{
11
    private $type;
12
    private $entities = [];
13
    private $keyMap = [];
14
    private $findCache = [];
15
    private $original = [];
16
17
    private $magicMethodRules = [
18
        'by' => false,
19
        'firstBy' => true,
20
        'oneBy' => true,
21
    ];
22
23 64
    public function __construct(Contracts\Type $type)
24
    {
25 64
        $this->type = $type;
26 64
    }
27
28 64
    public function create($params = null)
29
    {
30 64
        if ($params && !is_array($params)) {
31 64
            $params = [$params];
32
        }
33
34 64
        if (!is_array($params)) {
35 2
            $params = [];
36
        }
37
38 64
        $data = [];
39 64
        foreach ($params as $k => $v) {
40 64
            if (is_numeric($k)) {
41 64
                if ($v instanceof Contracts\Entity) {
42 4
                    $type = $this->type->getManager()->findRepository($v)->getType();
43 4
                    $k = $this->type->getReferenceProperty($type);
44
                } else {
45 64
                    $primitive = [];
46 64
                    foreach ($this->type->getProperties() as $property) {
47 64
                        if (!$this->type->isReference($property)) {
48 64
                            $primitive[] = $property;
49
                        }
50
                    }
51 64
                    if (count($primitive) == 2) {
52 64
                        $k = $primitive[1];
53
                    } else {
54 1
                        throw new Exception("Can't calculate key name");
55
                    }
56
                }
57
            }
58 64 View Code Duplication
            if (!$this->type->hasProperty($k)) {
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...
59 2
                $name = $this->type->getName();
60 2
                throw new Exception("Unknown property $name.$k");
61
            }
62 64
            $data[$k] = $this->type->encodeProperty($k, $v);
63
        }
64 64
        foreach ($this->type->getRequiredProperties() as $property) {
65 64
            if (!array_key_exists($property, $data)) {
66 64
                $convention = $this->type->getManager()->getMeta()->getConvention();
67 64
                $propertyType = $this->type->getPropertyType($property);
68 64
                $data[$property] = $convention->getDefaultValue($propertyType);
69
            }
70
        }
71
72 64
        $this->flushCache();
73
74 64
        return $this->createInstance($data);
75
    }
76
77 64
    private function createInstance($data)
78
    {
79 64
        $class = $this->getType()->getEntityClass();
80
81 64
        return $this->register(new $class($data));
82
    }
83
84 64
    public function __call($method, $arguments)
85
    {
86 64
        foreach ($this->magicMethodRules as $prefix => $oneItem) {
87 64
            if (substr($method, 0, strlen($prefix)) == $prefix) {
88 64
                $tail = substr($method, strlen($prefix));
89 64
                $fields = array_map('strtolower', explode('And', $tail));
90
91 64
                return $this->find(array_combine($fields, $arguments), $oneItem);
92
            }
93
        }
94
95 1
        throw new BadMethodCallException("Method $method not found");
96
    }
97
98 19
    public function findOne($params)
99
    {
100 19
        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 100 which is incompatible with the return type declared by the interface Tarantool\Mapper\Contracts\Repository::findOne of type Tarantool\Mapper\Contracts\Entity.
Loading history...
101
    }
102
103 64
    public function find($params = [], $oneItem = false)
104
    {
105 64
        $query = [];
106
107 64
        if (is_string($params) && 1 * $params == $params) {
108 1
            $params = 1 * $params;
109
        }
110
111 64
        if (is_int($params)) {
112 10
            if (isset($this->keyMap[$params])) {
113 3
                return $this->entities[$this->keyMap[$params]];
114
            }
115
            $query = [
116 7
                'id' => $params,
117
            ];
118 7
            $oneItem = true;
119
        }
120
121 64
        if ($params instanceof Contracts\Entity) {
122 2
            $params = [$params];
123
        }
124
125 64
        $reject = [];
126 64
        if (is_array($params)) {
127
128 64
            foreach($params as $key => $value) {
129 64
                if(is_string($value)) {
130 18
                    if(substr($value, 0, 1) == '!') {
131 64
                        $reject[$key] = substr($value, 1);
132
                    }
133
                }
134
            }
135 64
            foreach(array_keys($reject) as $key) {
136 1
                unset($params[$key]);
137
            }
138
139 64
            foreach ($params as $key => $value) {
140 64
                if (is_numeric($key) && $value instanceof Contracts\Entity) {
141 2
                    $type = $this->type->getManager()->findRepository($value)->getType();
142 2
                    $key = $this->type->getReferenceProperty($type);
143
                }
144 64
                if ($this->type->hasProperty($key)) {
145 64
                    $query[$key] = $this->type->encodeProperty($key, $value);
146
                } else {
147 1
                    $name = $this->getType()->getName();
148 64
                    throw new Exception("Unknown property $name.$key");
149
                }
150
            }
151
        }
152
153 64
        $findKey = md5(json_encode([$query, $reject]).($oneItem ? 'x' : ''));
154 64
        if (array_key_exists($findKey, $this->findCache)) {
155 64
            return $this->findCache[$findKey];
156
        }
157
158 64
        $index = $this->type->findIndex(array_keys($query));
159 64
        if (!is_numeric($index)) {
160 2
            throw new Exception('No index found for '.json_encode(array_keys($query)));
161
        }
162
163 64
        $values = count($query) ? $this->type->getIndexTuple($index, $query) : [];
164
165 64
        if (count($reject)) {
166 1
            $if = [];
167 1
            $properties = $this->type->getProperties();
168 1
            foreach ($reject as $key => $value) {
169 1
                $num = array_search($key, $properties) + 1;
170 1
                if(!$num) {
171
                    throw new Exception("Unknown property $key");
172
                }
173 1
                $if[] = "tuple[".$num.'] ~= '.(is_numeric($value) ? intval($value):"'$value'");
174
            }
175
176 1
            return $this->evaluate("
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->evaluate('... return 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...
177
                local result = {}
178 1
                for _, tuple in box.space.".$this->type->getName().'.index['.$index.']:pairs{'.implode(',', $values)."} do
179 1
                    if (" . implode(" && ", $if).") then table.insert(result, tuple) end
180
                end
181 1
                return result");
182
        }
183
184 64
        $data = $this->type->getSpace()->select($values, $index);
185
186 64
        $result = [];
187 64
        if (!empty($data->getData())) {
188 64
            foreach ($data->getData() as $tuple) {
189 64
                $data = $this->type->fromTuple($tuple);
190 64 View Code Duplication
                if (isset($data['id']) && array_key_exists($data['id'], $this->keyMap)) {
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...
191 64
                    $entity = $this->entities[$this->keyMap[$data['id']]];
192 64
                    $entity->update($data);
193
                } else {
194 64
                    $entity = $this->createInstance($data);
195
                }
196 64
                if ($oneItem) {
197 64
                    return $this->findCache[$findKey] = $entity;
198
                }
199 14
                $result[] = $entity;
200
            }
201
        }
202 64
        if ($oneItem) {
203 64
            return $this->findCache[$findKey] = null;
204
        } else {
205 17
            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...
206
        }
207
    }
208
209
    /**
210
     * @return Entity
211
     */
212 64
    public function knows(Contracts\Entity $entity)
213
    {
214 64
        return array_key_exists(spl_object_hash($entity), $this->entities);
215
    }
216
217 6
    public function remove(Contracts\Entity $entity)
218
    {
219 6
        unset($this->entities[$this->keyMap[$entity->id]]);
220 6
        unset($this->keyMap[$entity->id]);
221 6
        $this->flushCache();
222
223 6
        $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...
224 6
    }
225
226 1
    public function removeAll()
227
    {
228 1
        $space = $this->type->getSpace();
229 1
        foreach($space->select([])->getData() as $row) {
230 1
            $space->delete([$row[0]]);
231
        }
232 1
        $this->flushCache();
233 1
    }
234
235 64
    public function flushCache()
236
    {
237 64
        $this->findCache = [];
238 64
    }
239
240 64
    public function save(Contracts\Entity $entity)
241
    {
242 64
        if (!$this->knows($entity)) {
243 1
            throw new LogicException('Entity is not related with this repository');
244
        }
245
246 64
        if (!$entity->getId()) {
247 64
            $this->generateId($entity);
248 64
            $tuple = $this->type->getCompleteTuple($entity->toArray());
249 64
            $this->type->getSpace()->insert($tuple);
250
        } else {
251 19
            $array = $entity->toArray(false);
252 19
            $changes = [];
253 19
            $id = $entity->getId();
254 19
            if (!array_key_exists($id, $this->original)) {
255
                $changes = $array;
256
            } else {
257 19
                foreach ($array as $k => $v) {
258 19
                    if (!array_key_exists($k, $this->original[$id])) {
259 4
                        $changes[$k] = $v;
260 19
                    } elseif ($v !== $this->original[$id][$k]) {
261 19
                        $changes[$k] = $v;
262
                    }
263
                }
264
            }
265
266 19
            foreach ($changes as $k => $v) {
267 19 View Code Duplication
                if (!$this->type->hasProperty($k)) {
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...
268 1
                    $name = $this->type->getName();
269 19
                    throw new Exception("Unknown property $name.$k");
270
                }
271
            }
272
273 18
            if (count($changes)) {
274 18
                $operations = [];
275 18
                foreach ($this->type->getTuple($changes) as $key => $value) {
276 18
                    $operations[] = ['=', $key, $value];
277
                }
278
                try {
279 18
                    $result = $this->type->getSpace()->update($id, $operations);
280 17
                    $current = $this->type->fromTuple($result->getData()[0]);
281 17
                    $this->original[$id] = $current;
282 17
                    $entity->update($current);
283 1
                } catch (Exception $e) {
284 1
                    $this->type->getSpace()->delete([$id]);
285 1
                    $tuple = $this->type->getCompleteTuple($entity->toArray());
286 1
                    $this->type->getSpace()->insert($tuple);
287
                }
288 18
                $this->original[$id] = $entity->toArray();
289
            }
290
        }
291
292 64
        $this->flushCache();
293
294 64
        return $entity;
295
    }
296
297 64
    private function register(Contracts\Entity $entity)
298
    {
299 64
        if (!$this->knows($entity)) {
300 64
            $this->entities[spl_object_hash($entity)] = $entity;
301
        }
302 64
        if ($entity->getId() && !array_key_exists($entity->getId(), $this->keyMap)) {
303 64
            $this->keyMap[$entity->getId()] = spl_object_hash($entity);
304
        }
305
306 64
        if ($entity->getId()) {
307 64
            $this->original[$entity->getId()] = $entity->toArray();
308
        }
309
310 64
        return $entity;
311
    }
312
313 64
    private function generateId(Contracts\Entity $entity)
314
    {
315 64
        $manager = $this->type->getManager();
316 64
        $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...
317 64
        $spaceId = $this->type->getSpaceId();
318
319 64
        $sequence = $manager->get('sequence')->oneBySpace($spaceId);
320 64
        if (!$sequence) {
321 64
            $sequence = $manager->get('sequence')->create([
322 64
                'space' => $spaceId,
323 64
                'value' => 0,
324
            ]);
325 64
            $manager->save($sequence);
326
        }
327
328 64
        $nextValue = +$manager->getMeta()
329 64
            ->get('sequence')
330 64
            ->getSpace()
331 64
            ->update($sequence->id, [['+', 2, 1]])
332 64
            ->getData()[0][2];
333
334 64
        $entity->setId($nextValue);
335
336 64
        $this->register($entity);
337
338 64
        return $entity;
339
    }
340
341 64
    public function getType()
342
    {
343 64
        return $this->type;
344
    }
345
346 3
    public function evaluate($query)
347
    {
348 3
        $result = [];
349 3
        $tuples = $this->type->getManager()->getClient()->evaluate($query)->getData()[0];
350 3
        foreach ($tuples as $tuple) {
351 2
            $data = $this->type->fromTuple($tuple);
352 2 View Code Duplication
            if (isset($data['id']) && array_key_exists($data['id'], $this->keyMap)) {
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...
353 2
                $entity = $this->entities[$this->keyMap[$data['id']]];
354 2
                $entity->update($data);
355
            } else {
356
                $entity = $this->createInstance($data);
357
            }
358 2
            $result[] = $entity;
359
        }
360
361 3
        return $result;
362
    }
363
}
364