Completed
Push — master ( 8eb315...5aaba6 )
by Changwan
04:29
created

Repository   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 88.81%

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 338
ccs 119
cts 134
cp 0.8881
rs 8.6206
wmc 50
lcom 1
cbo 13

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A fetch() 0 6 2
A all() 0 4 1
A first() 0 7 2
A firstOrFail() 0 7 2
A find() 0 6 1
A findOrFail() 0 6 1
A findMany() 0 6 1
A persist() 0 9 2
B executeInsert() 0 22 5
A executeUpdate() 0 15 3
A delete() 0 16 3
A query() 0 11 3
C hydrate() 0 29 7
A getIdentifier() 0 8 2
B normalizeSelectQuery() 0 18 6
A injectProperty() 0 5 1
A pickProperty() 0 5 1
A getPropertyReflection() 0 7 2
A getClassReflection() 0 7 2
A assertIsInstance() 0 9 2

How to fix   Complexity   

Complex Class

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
namespace Wandu\Database;
3
4
use InvalidArgumentException;
5
use ReflectionClass;
6
use ReflectionProperty;
7
use Wandu\Caster\CastManagerInterface;
8
use Wandu\Collection\ArrayList;
9
use Wandu\Collection\Contracts\ListInterface;
10
use Wandu\Database\Contracts\Connection;
11
use Wandu\Database\Contracts\QueryInterface;
12
use Wandu\Database\Entity\Metadata;
13
use Wandu\Database\Exception\EntityNotFoundException;
14
use Wandu\Database\Query\SelectQuery;
15
16
class Repository
17
{
18
    /** @var \Wandu\Database\Contracts\Connection */
19
    protected $connection;
20
    
21
    /** @var \Wandu\Database\Entity\Metadata */
22
    protected $meta;
23
    
24
    /** @var \Wandu\Caster\CastManagerInterface */
25
    protected $caster;
26
    
27
    /** @var \Wandu\Database\QueryBuilder */
28
    protected $queryBuilder;
29
    
30
    /** @var \ReflectionClass */
31
    protected $reflClass;
32
    
33
    /** @var \ReflectionProperty[] */
34
    protected $refProperties = [];
35
    
36
    /** @var array */
37
    protected static $entityCache = [];
38
    
39 12
    public function __construct(Connection $connection, QueryBuilder $queryBuilder, Metadata $meta, Configuration $config)
40
    {
41 12
        $this->connection = $connection;
42 12
        $this->caster = $config->getCaster();
43 12
        $this->queryBuilder = $queryBuilder;
44 12
        $this->meta = $meta;
45 12
    }
46
47
    /**
48
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
49
     * @param array $bindings
50
     * @return \Generator
51
     */
52 5
    public function fetch($query = null, array $bindings = [])
53
    {
54 5
        foreach ($this->connection->fetch(...$this->normalizeSelectQuery($query, $bindings)) as $row) {
0 ignored issues
show
Documentation introduced by
$this->normalizeSelectQuery($query, $bindings) is of type object<Wandu\Database\Qu...,{"0":"*","1":"array"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
55 5
            yield $this->hydrate($row);
56
        }
57 5
    }
58
59
    /**
60
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
61
     * @param array $bindings
62
     * @return \Wandu\Collection\Contracts\ListInterface
63
     */
64
    public function all($query = null, array $bindings = []): ListInterface
65
    {
66
        return new ArrayList($this->fetch($query, $bindings));
67
    }
68
69
    /**
70
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
71
     * @param array $bindings
72
     * @return ?object
0 ignored issues
show
Documentation introduced by
The doc-type ?object could not be parsed: Unknown type name "?object" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
73
     */
74 7
    public function first($query = null, array $bindings = [])
75
    {
76 7
        $entity = $this->connection->first(...$this->normalizeSelectQuery($query, $bindings));
0 ignored issues
show
Documentation introduced by
$this->normalizeSelectQuery($query, $bindings) is of type object<Wandu\Database\Qu...,{"0":"*","1":"array"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
77 7
        if ($entity) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entity of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
78 7
            return $this->hydrate($entity);
79
        }
80 1
    }
81
82
    /**
83
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
84
     * @param array $bindings
85
     * @return object
86
     * @throws \Wandu\Database\Exception\EntityNotFoundException
87
     */
88
    public function firstOrFail($query = null, array $bindings = [])
89
    {
90
        if ($entity = $this->first($query, $bindings)) {
91
            return $entity;
92
        }
93
        throw new EntityNotFoundException();
94
    }
95
96
    /**
97
     * @param string|int $identifier
98
     * @return object
99
     */
100 1
    public function find($identifier)
101
    {
102
        return $this->first(function (SelectQuery $select) use ($identifier) {
103 1
            return $select->where($this->meta->getPrimaryKey(), $identifier);
104 1
        });
105
    }
106
107
    /**
108
     * @param string|int $identifier
109
     * @return object
110
     * @throws \Wandu\Database\Exception\EntityNotFoundException
111
     */
112
    public function findOrFail($identifier)
113
    {
114
        return $this->firstOrFail(function (SelectQuery $select) use ($identifier) {
115
            return $select->where($this->meta->getPrimaryKey(), $identifier);
116
        });
117
    }
118
119
    /**
120
     * @param array $identifiers
121
     * @return \Wandu\Collection\Contracts\ListInterface
122
     */
123
    public function findMany(array $identifiers = []): ListInterface
124
    {
125
        return $this->all(function (SelectQuery $select) use ($identifiers) {
126
            return $select->where($this->meta->getPrimaryKey(), 'IN', $identifiers);
0 ignored issues
show
Documentation introduced by
$identifiers is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
127
        });
128
    }
129
130
    /**
131
     * @param object $entity
132
     * @return int
133
     */
134 2
    public function persist($entity)
135
    {
136 2
        $this->assertIsInstance($entity, __METHOD__);
137 2
        $identifier = $this->getIdentifier($entity);
138 2
        if ($identifier) {
139 1
            return $this->executeUpdate($entity);
140
        }
141 1
        return $this->executeInsert($entity);
142
    }
143
144
    /**
145
     * @param object $entity
146
     * @return int
147
     */
148 1
    protected function executeInsert($entity)
149
    {
150 1
        $primaryKey = $this->meta->getPrimaryKey();
151 1
        $primaryProperty = null;
152 1
        $columns = $this->meta->getColumns();
153 1
        $attributesToStore = [];
154 1
        foreach ($columns as $propertyName => $column) {
155 1
            if ($primaryKey === $column->name) {
156 1
                $primaryProperty = $propertyName;
157 1
                continue;
158
            }
159 1
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
160
        }
161 1
        $rowAffected = $this->query($this->queryBuilder->insert($attributesToStore));
162 1
        if ($this->meta->isIncrements()) {
163 1
            $lastInsertId = $this->connection->getLastInsertId();
164 1
            if ($primaryProperty) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $primaryProperty of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
165 1
                $this->injectProperty($this->getPropertyReflection($primaryProperty), $entity, $lastInsertId);
166
            }
167
        }
168 1
        return $rowAffected;
169
    }
170
171
    /**
172
     * @param object $entity
173
     * @return int
174
     */
175 1
    protected function executeUpdate($entity)
176
    {
177 1
        $primaryKey = $this->meta->getPrimaryKey(); 
178
179 1
        $identifier = $this->getIdentifier($entity);
180 1
        $attributesToStore = [];
181 1
        foreach ($this->meta->getColumns() as $propertyName => $column) {
182 1
            if ($primaryKey === $column->name) continue;
183 1
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
184
        }
185
186 1
        return $this->query(
187 1
            $this->queryBuilder->update($attributesToStore)->where($primaryKey, $identifier)
188
        );
189
    }
190
191
    /**
192
     * @param object $entity
193
     * @return int
194
     */
195 1
    public function delete($entity)
196
    {
197 1
        $this->assertIsInstance($entity, __METHOD__);
198
        
199 1
        $primaryKey = $this->meta->getPrimaryKey();
200 1
        $identifier = $this->getIdentifier($entity);
201 1
        if (!$identifier) {
202 1
            return 0;
203
        }
204
205 1
        $affectedRows = $this->query($this->queryBuilder->delete()->where($primaryKey, $identifier));
206 1
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
207 1
            $this->injectProperty($this->getPropertyReflection($identifierProperty), $entity, null);
208
        }
209 1
        return $affectedRows;
210
    }
211
212
    /**
213
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
214
     * @param array $bindings
215
     * @return int
216
     */
217 2
    protected function query($query, array $bindings = [])
218
    {
219 2
        while (is_callable($query)) {
220
            $query = call_user_func($query);
221
        }
222 2
        if ($query instanceof QueryInterface) {
223 2
            $bindings = $query->getBindings();
224 2
            $query = $query->toSql();
225
        }
226 2
        return $this->connection->query($query, $bindings);
227
    }
228
229
    /**
230
     * @param array $attributes
231
     * @return object
232
     */
233 11
    public function hydrate(array $attributes = null)
234
    {
235 11
        if (!$attributes) {
236
            return null;
237
        }
238 11
        $entityCacheId = $this->meta->getClass() . '#' . $attributes[$this->meta->getPrimaryKey()];
239 11
        if (isset(static::$entityCache[$entityCacheId])) return static::$entityCache[$entityCacheId];
240
241 11
        $casts = $this->meta->getCasts();
242 11
        $relations = $this->meta->getRelations();
243 11
        $entity = $this->getClassReflection()->newInstanceWithoutConstructor();
244
245 11
        static::$entityCache[$entityCacheId] = $entity;
246
247 11
        foreach ($this->meta->getColumns() as $propertyName => $column) {
248 11
            if (!isset($attributes[$column->name])) continue;
249 11
            if (isset($relations[$propertyName])) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
250
//                $value = $relations[$propertyName]->getRelation($this->manager, $attributes[$column->name]);
0 ignored issues
show
Unused Code Comprehensibility introduced by
68% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
251 11
            } elseif (isset($casts[$propertyName])) {
252 11
                $value = $this->caster->cast($attributes[$column->name], $casts[$propertyName]->type);
253
            } else {
254 11
                $value = $attributes[$column->name];
255
            }
256 11
            $this->injectProperty($this->getPropertyReflection($propertyName), $entity, $value);
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
257
        }
258
259 11
        unset(static::$entityCache[$entityCacheId]);
260 11
        return $entity;
261
    }
262
263
    /**
264
     * @param mixed $entity
265
     * @return string|int
266
     */
267 2
    private function getIdentifier($entity)
268
    {
269 2
        $identifier = null;
270 2
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
271 2
            $identifier = $this->pickProperty($this->getPropertyReflection($identifierProperty), $entity);
272
        }
273 2
        return $identifier;
274
    }
275
    
276 11
    private function normalizeSelectQuery($query = null, array $bindings = [])
277
    {
278 11
        if (!isset($query) || is_callable($query)) {
279 6
            $connection = $this->connection;
280 6
            $queryBuilder = $this->queryBuilder->select();
281 6
            if (!isset($query)) {
282
                return $queryBuilder;
283
            }
284 6
            while (is_callable($query)) {
285 6
                $query = call_user_func($query, $queryBuilder, $connection);
286
            }
287
        }
288 11
        if ($query instanceof QueryInterface) {
289 6
            $bindings = $query->getBindings();
290 6
            $query = $query->toSql();
291
        }
292 11
        return [$query, $bindings];
293
    }
294
    
295
    /**
296
     * @param \ReflectionProperty $property
297
     * @param object $object
298
     * @param mixed $target
299
     */
300 12
    private function injectProperty(ReflectionProperty $property, $object, $target)
301
    {
302 12
        $property->setAccessible(true);
303 12
        $property->setValue($object, $target);
304 12
    }
305
306
    /**
307
     * @param \ReflectionProperty $property
308
     * @param object $object
309
     * @return mixed
310
     */
311 2
    private function pickProperty(ReflectionProperty $property, $object)
312
    {
313 2
        $property->setAccessible(true);
314 2
        return $property->getValue($object);
315
    }
316
317
    /**
318
     * @param string $name
319
     * @return \ReflectionProperty
320
     */
321 12
    private function getPropertyReflection($name): ReflectionProperty
322
    {
323 12
        if (!isset($this->refProperties[$name])) {
324 12
            return $this->refProperties[$name] = $this->getClassReflection()->getProperty($name);
325
        }
326 7
        return $this->refProperties[$name];
327
    }
328
329
    /**
330
     * @return \ReflectionClass
331
     */
332 12
    private function getClassReflection(): ReflectionClass
333
    {
334 12
        if (!isset($this->reflClass)) {
335 12
            return $this->reflClass = new ReflectionClass($this->meta->getClass());
336
        }
337 12
        return $this->reflClass;
338
    }
339
340
    /**
341
     * @param mixed $entity
342
     * @param string $method
343
     */
344 2
    private function assertIsInstance($entity, $method)
345
    {
346 2
        $class = $this->meta->getClass();
347 2
        if (!$entity instanceof $class) {
348 2
            throw new InvalidArgumentException(
349 2
                "Argument 1 passed to {$method}() must be of the type " . $class
350
            );
351
        }
352 2
    }
353
}
354