Repository::findOrFail()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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