Completed
Push — master ( 762e93...98e9e2 )
by Dmitry
32:47 queued 02:55
created

Repository   C

Complexity

Total Complexity 77

Size/Duplication

Total Lines 352
Duplicated Lines 5.68 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 0%

Importance

Changes 51
Bugs 10 Features 17
Metric Value
wmc 77
c 51
b 10
f 17
lcom 1
cbo 2
dl 20
loc 352
ccs 0
cts 286
cp 0
rs 5.4715

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C create() 4 48 13
A createInstance() 0 6 1
A __call() 0 13 3
A findOne() 0 4 1
F find() 6 103 29
A knows() 0 4 1
A remove() 0 8 1
A removeAll() 0 7 2
A flushCache() 0 4 1
C save() 4 56 12
B register() 0 15 5
B generateId() 0 27 2
A getType() 0 4 1
A evaluate() 6 17 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Repository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Repository, and based on these observations, apply Extract Interface, too.

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