Completed
Push — master ( d8f60e...a72876 )
by Changwan
03:40
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\Repository;
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\IdentifierNotFoundException;
12
use Wandu\Database\Manager;
13
use Wandu\Database\Query\SelectQuery;
14
use Wandu\Database\QueryBuilder;
15
16
class Repository
17
{
18
    /** @var \Wandu\Database\Manager */
19
    protected $manager;
20
    
21
    /** @var \Wandu\Database\Contracts\ConnectionInterface */
22
    protected $connection;
23
    
24
    /** @var \Wandu\Database\Entity\Metadata */
25
    protected $meta;
26
    
27
    /** @var \Wandu\Caster\CastManagerInterface */
28
    protected $caster;
29
    
30
    /** @var \Wandu\Database\QueryBuilder */
31
    protected $queryBuilder;
32
    
33
    /** @var \ReflectionClass */
34
    protected $reflClass;
35
    
36
    /** @var \ReflectionProperty[] */
37
    protected $refProperties = [];
38
    
39
    /** @var array */
40
    protected static $entityCache = [];
41
    
42 16
    public function __construct(Manager $manager, Metadata $meta, CastManagerInterface $caster)
43
    {
44 16
        $this->manager = $manager;
45 16
        $this->connection = $manager->connection($meta->getConnection());
46 16
        $this->meta = $meta;
47 16
        $this->caster = $caster;
48 16
        $this->query = new QueryBuilder($meta->getTable());
0 ignored issues
show
Bug introduced by
The property query does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
49 16
    }
50
51
    /**
52
     * @return \Wandu\Database\Entity\Metadata
53
     */
54
    public function getMeta(): Metadata
55
    {
56
        return $this->meta;
57
    }
58
    
59
    /**
60
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
61
     * @param array $bindings
62
     * @return \Generator
63
     */
64 7
    public function fetch($query = null, array $bindings = [])
65
    {
66 7
        foreach ($this->connection->fetch($this->normalizeSelectQuery($query), $bindings) as $row) {
67 7
            yield $this->hydrate($row);
68
        }
69 7
    }
70
71
    /**
72
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
73
     * @param array $bindings
74
     * @return \Wandu\Collection\Contracts\ListInterface
75
     */
76 2
    public function all($query = null, array $bindings = []): ListInterface
77
    {
78 2
        return new ArrayList($this->fetch($query, $bindings));
79
    }
80
81
    /**
82
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
83
     * @param array $bindings
84
     * @return object
85
     */
86 11
    public function first($query = null, array $bindings = [])
87
    {
88 11
        return $this->hydrate($this->connection->first($this->normalizeSelectQuery($query), $bindings));
89
    }
90
91
    /**
92
     * @param string|int $identifier
93
     * @return object
94
     */
95 4
    public function find($identifier)
96
    {
97
        return $this->first(function (SelectQuery $select) use ($identifier) {
98 4
            return $select->where($this->meta->getPrimaryKey(), $identifier);
99 4
        });
100
    }
101
102
    /**
103
     * @param array $identifiers
104
     * @return \Wandu\Collection\Contracts\ListInterface
105
     */
106
    public function findMany(array $identifiers = []): ListInterface
107
    {
108 1
        return $this->all(function (SelectQuery $select) use ($identifiers) {
109 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...
110 1
        });
111
    }
112
    
113
    /**
114
     * @param object $entity
115
     * @return int
116
     */
117 1
    public function insert($entity)
118
    {
119 1
        $this->assertIsInstance($entity, __METHOD__);
120 1
        $primaryKey = $this->meta->getPrimaryKey();
121 1
        $primaryProperty = null;
122 1
        $columns = $this->meta->getColumns();
123 1
        $attributesToStore = [];
124 1
        foreach ($columns as $propertyName => $column) {
125 1
            if ($primaryKey === $column->name) {
126 1
                $primaryProperty = $propertyName;
127 1
                continue;
128
            }
129 1
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
130
        }
131 1
        $rowAffected = $this->query($this->query->insert($attributesToStore));
132 1
        if ($this->meta->isIncrements()) {
133 1
            $lastInsertId = $this->connection->getLastInsertId();
134 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...
135 1
                $this->injectProperty($this->getPropertyReflection($primaryProperty), $entity, $lastInsertId);
136
            }
137
        }
138 1
        return $rowAffected;
139
    }
140
141
    /**
142
     * @param object $entity
143
     * @return int
144
     */
145 1
    public function update($entity)
146
    {
147 1
        $this->assertIsInstance($entity, __METHOD__);
148 1
        $primaryKey = $this->meta->getPrimaryKey(); 
149
150 1
        $identifier = $this->getIdentifier($entity);
151 1
        $attributesToStore = [];
152 1
        foreach ($this->meta->getColumns() as $propertyName => $column) {
153 1
            if ($primaryKey === $column->name) continue;
154 1
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
155
        }
156
157 1
        return $this->query(
158 1
            $this->query->update($attributesToStore)->where($primaryKey, $identifier)
159
        );
160
    }
161
162
    /**
163
     * @param object $entity
164
     * @return int
165
     */
166 1
    public function delete($entity)
167
    {
168 1
        $this->assertIsInstance($entity, __METHOD__);
169
        
170 1
        $primaryKey = $this->meta->getPrimaryKey();
171 1
        $identifier = $this->getIdentifier($entity);
172
173 1
        $affectedRows = $this->query($this->query->delete()->where($primaryKey, $identifier));
174 1
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
175 1
            $this->injectProperty($this->getPropertyReflection($identifierProperty), $entity, null);
176
        }
177 1
        return $affectedRows;
178
    }
179
180
    /**
181
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
182
     * @param array $bindings
183
     * @return int
184
     */
185 2
    public function query($query, array $bindings = [])
186
    {
187 2
        return $this->connection->query($query, $bindings);
188
    }
189
190
    /**
191
     * @param array $attributes
192
     * @return object
193
     */
194 15
    public function hydrate(array $attributes = null)
195
    {
196 15
        if (!$attributes) {
197 1
            return null;
198
        }
199 15
        $entityCacheId = $this->meta->getClass() . '#' . $attributes[$this->meta->getPrimaryKey()];
200 15
        if (isset(static::$entityCache[$entityCacheId])) return static::$entityCache[$entityCacheId];
201
202 15
        $casts = $this->meta->getCasts();
203 15
        $relations = $this->meta->getRelations();
204 15
        $entity = $this->getClassReflection()->newInstanceWithoutConstructor();
205
206 15
        static::$entityCache[$entityCacheId] = $entity;
207
208 15
        foreach ($this->meta->getColumns() as $propertyName => $column) {
209 15
            if (!isset($attributes[$column->name])) continue;
210 15
            if (isset($relations[$propertyName])) {
211 3
                $value = $relations[$propertyName]->getRelation($this->manager, $attributes[$column->name]);
212 15
            } elseif (isset($casts[$propertyName])) {
213 15
                $value = $this->caster->cast($attributes[$column->name], $casts[$propertyName]->type);
214
            } else {
215 15
                $value = $attributes[$column->name];
216
            }
217 15
            $this->injectProperty($this->getPropertyReflection($propertyName), $entity, $value);
218
        }
219
220 15
        unset(static::$entityCache[$entityCacheId]);
221 15
        return $entity;
222
    }
223
224
    /**
225
     * @param mixed $entity
226
     * @return string|int
227
     */
228 2
    private function getIdentifier($entity)
229
    {
230 2
        $identifier = null;
231 2
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
232 2
            $identifier = $this->pickProperty($this->getPropertyReflection($identifierProperty), $entity);
233
        }
234 2
        if (!$identifier) {
235 1
            throw new IdentifierNotFoundException();
236
        }
237 2
        return $identifier;
238
    }
239
240
    /**
241
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
242
     * @return string|\Wandu\Database\Contracts\QueryInterface
243
     */
244 15
    private function normalizeSelectQuery($query = null)
245
    {
246 15
        if (!isset($query) || is_callable($query)) {
247 10
            $connection = $this->connection;
248 10
            $queryBuilder = $this->query->select();
249 10
            if (!isset($query)) {
250
                return $queryBuilder;
251
            }
252 10
            while (is_callable($query)) {
253 10
                $query = call_user_func($query, $queryBuilder, $connection);
254
            }
255
        }
256 15
        return $query;
257
    }
258
    
259
    /**
260
     * @param \ReflectionProperty $property
261
     * @param object $object
262
     * @param mixed $target
263
     */
264 16
    private function injectProperty(ReflectionProperty $property, $object, $target)
265
    {
266 16
        $property->setAccessible(true);
267 16
        $property->setValue($object, $target);
268 16
    }
269
270
    /**
271
     * @param \ReflectionProperty $property
272
     * @param object $object
273
     * @return mixed
274
     */
275 2
    private function pickProperty(ReflectionProperty $property, $object)
276
    {
277 2
        $property->setAccessible(true);
278 2
        return $property->getValue($object);
279
    }
280
281
    /**
282
     * @param string $name
283
     * @return \ReflectionProperty
284
     */
285 16
    private function getPropertyReflection($name): ReflectionProperty
286
    {
287 16
        if (!isset($this->refProperties[$name])) {
288 16
            return $this->refProperties[$name] = $this->getClassReflection()->getProperty($name);
289
        }
290 9
        return $this->refProperties[$name];
291
    }
292
293
    /**
294
     * @return \ReflectionClass
295
     */
296 16
    private function getClassReflection(): ReflectionClass
297
    {
298 16
        if (!isset($this->reflClass)) {
299 16
            return $this->reflClass = new ReflectionClass($this->meta->getClass());
300
        }
301 16
        return $this->reflClass;
302
    }
303
304
    /**
305
     * @param mixed $entity
306
     * @param string $method
307
     */
308 2
    private function assertIsInstance($entity, $method)
309
    {
310 2
        $class = $this->meta->getClass();
311 2
        if (!$entity instanceof $class) {
312 2
            throw new InvalidArgumentException(
313 2
                "Argument 1 passed to {$method}() must be of the type " . $class
314
            );
315
        }
316 2
    }
317
}
318