Completed
Push — master ( dc7866...8155ff )
by Changwan
06:46
created

Repository::firstOrFail()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
ccs 2
cts 2
cp 1
crap 2
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\EntityNotFoundException;
12
use Wandu\Database\Exception\IdentifierNotFoundException;
13
use Wandu\Database\Manager;
14
use Wandu\Database\Query\SelectQuery;
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 = $connection = $manager->connection($meta->getConnection());
46 16
        $this->meta = $meta;
47 16
        $this->caster = $caster;
48 16
        $this->queryBuilder = $connection->createQueryBuilder($meta->getTable());
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|callable|\Wandu\Database\Contracts\QueryInterface $query
93
     * @param array $bindings
94
     * @return object
95 4
     * @throws \Wandu\Database\Exception\EntityNotFoundException
96
     */
97
    public function firstOrFail($query = null, array $bindings = [])
98 4
    {
99 4
        $attributes = $this->connection->first($this->normalizeSelectQuery($query), $bindings);
100
        if (isset($attributes)) {
101
            return $this->hydrate($attributes);
102
        }
103
        throw new EntityNotFoundException();
104
    }
105
106
    /**
107
     * @param string|int $identifier
108 1
     * @return object
109 1
     */
110 1
    public function find($identifier)
111
    {
112
        return $this->first(function (SelectQuery $select) use ($identifier) {
113
            return $select->where($this->meta->getPrimaryKey(), $identifier);
114
        });
115
    }
116
117 1
    /**
118
     * @param string|int $identifier
119 1
     * @return object
120 1
     * @throws \Wandu\Database\Exception\EntityNotFoundException
121 1
     */
122 1
    public function findOrFail($identifier)
123 1
    {
124 1
        return $this->firstOrFail(function (SelectQuery $select) use ($identifier) {
125 1
            return $select->where($this->meta->getPrimaryKey(), $identifier);
126 1
        });
127 1
    }
128
129 1
    /**
130
     * @param array $identifiers
131 1
     * @return \Wandu\Collection\Contracts\ListInterface
132 1
     */
133 1
    public function findMany(array $identifiers = []): ListInterface
134 1
    {
135 1
        return $this->all(function (SelectQuery $select) use ($identifiers) {
136
            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...
137
        });
138 1
    }
139
    
140
    /**
141
     * @param object $entity
142
     * @return int
143
     */
144
    public function insert($entity)
145 1
    {
146
        $this->assertIsInstance($entity, __METHOD__);
147 1
        $primaryKey = $this->meta->getPrimaryKey();
148 1
        $primaryProperty = null;
149
        $columns = $this->meta->getColumns();
150 1
        $attributesToStore = [];
151 1
        foreach ($columns as $propertyName => $column) {
152 1
            if ($primaryKey === $column->name) {
153 1
                $primaryProperty = $propertyName;
154 1
                continue;
155
            }
156
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
157 1
        }
158 1
        $rowAffected = $this->query($this->queryBuilder->insert($attributesToStore));
159
        if ($this->meta->isIncrements()) {
160
            $lastInsertId = $this->connection->getLastInsertId();
161
            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...
162
                $this->injectProperty($this->getPropertyReflection($primaryProperty), $entity, $lastInsertId);
163
            }
164
        }
165
        return $rowAffected;
166 1
    }
167
168 1
    /**
169
     * @param object $entity
170 1
     * @return int
171 1
     */
172
    public function update($entity)
173 1
    {
174 1
        $this->assertIsInstance($entity, __METHOD__);
175 1
        $primaryKey = $this->meta->getPrimaryKey(); 
176
177 1
        $identifier = $this->getIdentifier($entity);
178
        $attributesToStore = [];
179
        foreach ($this->meta->getColumns() as $propertyName => $column) {
180
            if ($primaryKey === $column->name) continue;
181
            $attributesToStore[$column->name] = $this->pickProperty($this->getPropertyReflection($propertyName), $entity);
182
        }
183
184
        return $this->query(
185 2
            $this->queryBuilder->update($attributesToStore)->where($primaryKey, $identifier)
186
        );
187 2
    }
188
189
    /**
190
     * @param object $entity
191
     * @return int
192
     */
193
    public function delete($entity)
194 15
    {
195
        $this->assertIsInstance($entity, __METHOD__);
196 15
        
197 1
        $primaryKey = $this->meta->getPrimaryKey();
198
        $identifier = $this->getIdentifier($entity);
199 15
200 15
        $affectedRows = $this->query($this->queryBuilder->delete()->where($primaryKey, $identifier));
201
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
202 15
            $this->injectProperty($this->getPropertyReflection($identifierProperty), $entity, null);
203 15
        }
204 15
        return $affectedRows;
205
    }
206 15
207
    /**
208 15
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
209 15
     * @param array $bindings
210 15
     * @return int
211 3
     */
212 15
    public function query($query, array $bindings = [])
213 15
    {
214
        return $this->connection->query($query, $bindings);
215 15
    }
216
217 15
    /**
218
     * @param array $attributes
219
     * @return object
220 15
     */
221 15
    public function hydrate(array $attributes = null)
222
    {
223
        if (!$attributes) {
224
            return null;
225
        }
226
        $entityCacheId = $this->meta->getClass() . '#' . $attributes[$this->meta->getPrimaryKey()];
227
        if (isset(static::$entityCache[$entityCacheId])) return static::$entityCache[$entityCacheId];
228 2
229
        $casts = $this->meta->getCasts();
230 2
        $relations = $this->meta->getRelations();
231 2
        $entity = $this->getClassReflection()->newInstanceWithoutConstructor();
232 2
233
        static::$entityCache[$entityCacheId] = $entity;
234 2
235 1
        foreach ($this->meta->getColumns() as $propertyName => $column) {
236
            if (!isset($attributes[$column->name])) continue;
237 2
            if (isset($relations[$propertyName])) {
238
                $value = $relations[$propertyName]->getRelation($this->manager, $attributes[$column->name]);
239
            } elseif (isset($casts[$propertyName])) {
240
                $value = $this->caster->cast($attributes[$column->name], $casts[$propertyName]->type);
241
            } else {
242
                $value = $attributes[$column->name];
243
            }
244 15
            $this->injectProperty($this->getPropertyReflection($propertyName), $entity, $value);
245
        }
246 15
247 10
        unset(static::$entityCache[$entityCacheId]);
248 10
        return $entity;
249 10
    }
250
251
    /**
252 10
     * @param mixed $entity
253 10
     * @return string|int
254
     */
255
    private function getIdentifier($entity)
256 15
    {
257
        $identifier = null;
258
        if ($identifierProperty = $this->meta->getPrimaryProperty()) {
259
            $identifier = $this->pickProperty($this->getPropertyReflection($identifierProperty), $entity);
260
        }
261
        if (!$identifier) {
262
            throw new IdentifierNotFoundException();
263
        }
264 16
        return $identifier;
265
    }
266 16
267 16
    /**
268 16
     * @param string|callable|\Wandu\Database\Contracts\QueryInterface $query
269
     * @return string|\Wandu\Database\Contracts\QueryInterface
270
     */
271
    private function normalizeSelectQuery($query = null)
272
    {
273
        if (!isset($query) || is_callable($query)) {
274
            $connection = $this->connection;
275 2
            $queryBuilder = $this->queryBuilder->select();
276
            if (!isset($query)) {
277 2
                return $queryBuilder;
278 2
            }
279
            while (is_callable($query)) {
280
                $query = call_user_func($query, $queryBuilder, $connection);
281
            }
282
        }
283
        return $query;
284
    }
285 16
    
286
    /**
287 16
     * @param \ReflectionProperty $property
288 16
     * @param object $object
289
     * @param mixed $target
290 9
     */
291
    private function injectProperty(ReflectionProperty $property, $object, $target)
292
    {
293
        $property->setAccessible(true);
294
        $property->setValue($object, $target);
295
    }
296 16
297
    /**
298 16
     * @param \ReflectionProperty $property
299 16
     * @param object $object
300
     * @return mixed
301 16
     */
302
    private function pickProperty(ReflectionProperty $property, $object)
303
    {
304
        $property->setAccessible(true);
305
        return $property->getValue($object);
306
    }
307
308 2
    /**
309
     * @param string $name
310 2
     * @return \ReflectionProperty
311 2
     */
312 2
    private function getPropertyReflection($name): ReflectionProperty
313 2
    {
314
        if (!isset($this->refProperties[$name])) {
315
            return $this->refProperties[$name] = $this->getClassReflection()->getProperty($name);
316 2
        }
317
        return $this->refProperties[$name];
318
    }
319
320
    /**
321
     * @return \ReflectionClass
322
     */
323
    private function getClassReflection(): ReflectionClass
324
    {
325
        if (!isset($this->reflClass)) {
326
            return $this->reflClass = new ReflectionClass($this->meta->getClass());
327
        }
328
        return $this->reflClass;
329
    }
330
331
    /**
332
     * @param mixed $entity
333
     * @param string $method
334
     */
335
    private function assertIsInstance($entity, $method)
336
    {
337
        $class = $this->meta->getClass();
338
        if (!$entity instanceof $class) {
339
            throw new InvalidArgumentException(
340
                "Argument 1 passed to {$method}() must be of the type " . $class
341
            );
342
        }
343
    }
344
}
345