Completed
Push — master ( 059e5b...d67629 )
by Changwan
03:22
created

Repository::hydrate()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 20
nc 7
nop 1
dl 0
loc 29
ccs 19
cts 19
cp 1
crap 7
rs 6.7272
c 0
b 0
f 0
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\Entity\Metadata;
11
use Wandu\Database\Exception\EntityNotFoundException;
12
use Wandu\Database\Query\SelectQuery;
13
14
class Repository
15
{
16
    /** @var \Wandu\Database\Manager */
17
    protected $manager;
18
    
19
    /** @var \Wandu\Database\Contracts\ConnectionInterface */
20
    protected $connection;
21
    
22
    /** @var \Wandu\Database\Entity\Metadata */
23
    protected $meta;
24
    
25
    /** @var \Wandu\Caster\CastManagerInterface */
26
    protected $caster;
27
    
28
    /** @var \Wandu\Database\QueryBuilder */
29
    protected $queryBuilder;
30
    
31
    /** @var \ReflectionClass */
32
    protected $reflClass;
33
    
34
    /** @var \ReflectionProperty[] */
35
    protected $refProperties = [];
36
    
37
    /** @var array */
38
    protected static $entityCache = [];
39
    
40 17
    public function __construct(Manager $manager, Metadata $meta, CastManagerInterface $caster)
41
    {
42 17
        $this->manager = $manager;
43 17
        $this->connection = $connection = $manager->connection($meta->getConnection());
44 17
        $this->meta = $meta;
45 17
        $this->caster = $caster;
46 17
        $this->queryBuilder = $connection->createQueryBuilder($meta->getTable());
47 17
    }
48
49
    /**
50
     * @return \Wandu\Database\Entity\Metadata
51
     */
52
    public function getMeta(): Metadata
53
    {
54
        return $this->meta;
55
    }
56
    
57
    /**
58
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
59
     * @param array $bindings
60
     * @return \Generator
61
     */
62 7
    public function fetch($query = null, array $bindings = [])
63
    {
64 7
        foreach ($this->connection->fetch($this->normalizeSelectQuery($query), $bindings) as $row) {
65 7
            yield $this->hydrate($row);
66
        }
67 7
    }
68
69
    /**
70
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
71
     * @param array $bindings
72
     * @return \Wandu\Collection\Contracts\ListInterface
73
     */
74 2
    public function all($query = null, array $bindings = []): ListInterface
75
    {
76 2
        return new ArrayList($this->fetch($query, $bindings));
77
    }
78
79
    /**
80
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
81
     * @param array $bindings
82
     * @return object
83
     */
84 11
    public function first($query = null, array $bindings = [])
85
    {
86 11
        return $this->hydrate($this->connection->first($this->normalizeSelectQuery($query), $bindings));
87
    }
88
89
    /**
90
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
91
     * @param array $bindings
92
     * @return object
93
     * @throws \Wandu\Database\Exception\EntityNotFoundException
94
     */
95
    public function firstOrFail($query = null, array $bindings = [])
96
    {
97
        $attributes = $this->connection->first($this->normalizeSelectQuery($query), $bindings);
98
        if (isset($attributes)) {
99
            return $this->hydrate($attributes);
100
        }
101
        throw new EntityNotFoundException();
102
    }
103
104
    /**
105
     * @param string|int $identifier
106
     * @return object
107
     */
108 4
    public function find($identifier)
109
    {
110
        return $this->first(function (SelectQuery $select) use ($identifier) {
111 4
            return $select->where($this->meta->getPrimaryKey(), $identifier);
112 4
        });
113
    }
114
115
    /**
116
     * @param string|int $identifier
117
     * @return object
118
     * @throws \Wandu\Database\Exception\EntityNotFoundException
119
     */
120
    public function findOrFail($identifier)
121
    {
122
        return $this->firstOrFail(function (SelectQuery $select) use ($identifier) {
123
            return $select->where($this->meta->getPrimaryKey(), $identifier);
124
        });
125
    }
126
127
    /**
128
     * @param array $identifiers
129
     * @return \Wandu\Collection\Contracts\ListInterface
130
     */
131
    public function findMany(array $identifiers = []): ListInterface
132
    {
133 1
        return $this->all(function (SelectQuery $select) use ($identifiers) {
134 1
            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...
135 1
        });
136
    }
137
138
    /**
139
     * @param object $entity
140
     * @return int
141
     */
142 3
    public function persist($entity)
143
    {
144 3
        $this->assertIsInstance($entity, __METHOD__);
145 3
        $identifier = $this->getIdentifier($entity);
146 3
        if ($identifier) {
147 2
            return $this->executeUpdate($entity);
148
        }
149 2
        return $this->executeInsert($entity);
150
    }
151
152
    /**
153
     * @param array $attributes
154
     * @return object
155
     */
156
    public function create(array $attributes = [])
0 ignored issues
show
Unused Code introduced by
The parameter $attributes is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
157
    {
158
    }
159
    
160
    /**
161
     * @param object $entity
162
     * @return int
163
     */
164 2
    protected function executeInsert($entity)
165
    {
166 2
        $primaryKey = $this->meta->getPrimaryKey();
167 2
        $primaryProperty = null;
168 2
        $columns = $this->meta->getColumns();
169 2
        $attributesToStore = [];
170 2
        foreach ($columns as $propertyName => $column) {
171 2
            if ($primaryKey === $column->name) {
172 2
                $primaryProperty = $propertyName;
173 2
                continue;
174
            }
175 2
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
176
        }
177 2
        $rowAffected = $this->query($this->queryBuilder->insert($attributesToStore));
178 2
        if ($this->meta->isIncrements()) {
179 2
            $lastInsertId = $this->connection->getLastInsertId();
180 2
            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...
181 2
                $this->injectProperty($this->getPropertyReflection($primaryProperty), $entity, $lastInsertId);
182
            }
183
        }
184 2
        return $rowAffected;
185
    }
186
187
    /**
188
     * @param object $entity
189
     * @return int
190
     */
191 2
    protected function executeUpdate($entity)
192
    {
193 2
        $primaryKey = $this->meta->getPrimaryKey(); 
194
195 2
        $identifier = $this->getIdentifier($entity);
196 2
        $attributesToStore = [];
197 2
        foreach ($this->meta->getColumns() as $propertyName => $column) {
198 2
            if ($primaryKey === $column->name) continue;
199 2
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
200
        }
201
202 2
        return $this->query(
203 2
            $this->queryBuilder->update($attributesToStore)->where($primaryKey, $identifier)
204
        );
205
    }
206
207
    /**
208
     * @param object $entity
209
     * @return int
210
     */
211 2
    public function delete($entity)
212
    {
213 2
        $this->assertIsInstance($entity, __METHOD__);
214
        
215 2
        $primaryKey = $this->meta->getPrimaryKey();
216 2
        $identifier = $this->getIdentifier($entity);
217 2
        if (!$identifier) {
218 1
            return 0;
219
        }
220
221 2
        $affectedRows = $this->query($this->queryBuilder->delete()->where($primaryKey, $identifier));
222 2
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
223 2
            $this->injectProperty($this->getPropertyReflection($identifierProperty), $entity, null);
224
        }
225 2
        return $affectedRows;
226
    }
227
228
    /**
229
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
230
     * @param array $bindings
231
     * @return int
232
     */
233 3
    public function query($query, array $bindings = [])
234
    {
235 3
        return $this->connection->query($query, $bindings);
236
    }
237
238
    /**
239
     * @param array $attributes
240
     * @return object
241
     */
242 15
    public function hydrate(array $attributes = null)
243
    {
244 15
        if (!$attributes) {
245 1
            return null;
246
        }
247 15
        $entityCacheId = $this->meta->getClass() . '#' . $attributes[$this->meta->getPrimaryKey()];
248 15
        if (isset(static::$entityCache[$entityCacheId])) return static::$entityCache[$entityCacheId];
249
250 15
        $casts = $this->meta->getCasts();
251 15
        $relations = $this->meta->getRelations();
252 15
        $entity = $this->getClassReflection()->newInstanceWithoutConstructor();
253
254 15
        static::$entityCache[$entityCacheId] = $entity;
255
256 15
        foreach ($this->meta->getColumns() as $propertyName => $column) {
257 15
            if (!isset($attributes[$column->name])) continue;
258 15
            if (isset($relations[$propertyName])) {
259 3
                $value = $relations[$propertyName]->getRelation($this->manager, $attributes[$column->name]);
260 15
            } elseif (isset($casts[$propertyName])) {
261 15
                $value = $this->caster->cast($attributes[$column->name], $casts[$propertyName]->type);
262
            } else {
263 15
                $value = $attributes[$column->name];
264
            }
265 15
            $this->injectProperty($this->getPropertyReflection($propertyName), $entity, $value);
266
        }
267
268 15
        unset(static::$entityCache[$entityCacheId]);
269 15
        return $entity;
270
    }
271
272
    /**
273
     * @param mixed $entity
274
     * @return string|int
275
     */
276 3
    private function getIdentifier($entity)
277
    {
278 3
        $identifier = null;
279 3
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
280 3
            $identifier = $this->pickProperty($this->getPropertyReflection($identifierProperty), $entity);
281
        }
282 3
        return $identifier;
283
    }
284
285
    /**
286
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
287
     * @return string|\Wandu\Database\Contracts\QueryInterface
288
     */
289 15
    private function normalizeSelectQuery($query = null)
290
    {
291 15
        if (!isset($query) || is_callable($query)) {
292 10
            $connection = $this->connection;
293 10
            $queryBuilder = $this->queryBuilder->select();
294 10
            if (!isset($query)) {
295
                return $queryBuilder;
296
            }
297 10
            while (is_callable($query)) {
298 10
                $query = call_user_func($query, $queryBuilder, $connection);
299
            }
300
        }
301 15
        return $query;
302
    }
303
    
304
    /**
305
     * @param \ReflectionProperty $property
306
     * @param object $object
307
     * @param mixed $target
308
     */
309 17
    private function injectProperty(ReflectionProperty $property, $object, $target)
310
    {
311 17
        $property->setAccessible(true);
312 17
        $property->setValue($object, $target);
313 17
    }
314
315
    /**
316
     * @param \ReflectionProperty $property
317
     * @param object $object
318
     * @return mixed
319
     */
320 3
    private function pickProperty(ReflectionProperty $property, $object)
321
    {
322 3
        $property->setAccessible(true);
323 3
        return $property->getValue($object);
324
    }
325
326
    /**
327
     * @param string $name
328
     * @return \ReflectionProperty
329
     */
330 17
    private function getPropertyReflection($name): ReflectionProperty
331
    {
332 17
        if (!isset($this->refProperties[$name])) {
333 17
            return $this->refProperties[$name] = $this->getClassReflection()->getProperty($name);
334
        }
335 10
        return $this->refProperties[$name];
336
    }
337
338
    /**
339
     * @return \ReflectionClass
340
     */
341 17
    private function getClassReflection(): ReflectionClass
342
    {
343 17
        if (!isset($this->reflClass)) {
344 17
            return $this->reflClass = new ReflectionClass($this->meta->getClass());
345
        }
346 17
        return $this->reflClass;
347
    }
348
349
    /**
350
     * @param mixed $entity
351
     * @param string $method
352
     */
353 3
    private function assertIsInstance($entity, $method)
354
    {
355 3
        $class = $this->meta->getClass();
356 3
        if (!$entity instanceof $class) {
357 2
            throw new InvalidArgumentException(
358 2
                "Argument 1 passed to {$method}() must be of the type " . $class
359
            );
360
        }
361 3
    }
362
}
363