Completed
Push — develop ( 468796...759cfc )
by Abdelrahman
01:35
created

Criteriable::instantiateCriterion()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
c 0
b 0
f 0
rs 9.2
cc 4
eloc 10
nc 3
nop 2
1
<?php
2
3
namespace Rinvex\Repository\Traits;
4
5
use Closure;
6
use Illuminate\Support\Arr;
7
use Rinvex\Repository\Contracts\CriterionContract;
8
use Rinvex\Repository\Contracts\RepositoryContract;
9
use Rinvex\Repository\Exceptions\CriterionException;
10
use Rinvex\Repository\Exceptions\RepositoryException;
11
12
trait Criteriable
13
{
14
    /**
15
     * List of repository criteria.
16
     *
17
     * @var array
18
     */
19
    protected $criteria = [];
20
21
    /**
22
     * List of default repository criteria.
23
     *
24
     * @var array
25
     */
26
    protected $defaultCriteria = [];
27
28
    /**
29
     * Skip criteria flag.
30
     * If setted to true criteria will not be apply to the query.
31
     *
32
     * @var bool
33
     */
34
    protected $skipCriteria = false;
35
36
    /**
37
     * Skip default criteria flag.
38
     * If setted to true default criteria will not be added to the criteria list.
39
     *
40
     * @var bool
41
     */
42
    protected $skipDefaultCriteria = false;
43
44
    /**
45
     * Return name for the criterion.
46
     * If as criterion in parameter passed string we assume that is criterion class name.
47
     *
48
     * @param CriterionContract|Closure|string $criteria
49
     *
50
     * @return string
51
     */
52
    public function getCriterionName($criteria)
53
    {
54
        if ($criteria instanceof Closure) {
55
            return spl_object_hash($criteria);
56
        }
57
58
        return is_object($criteria) ? get_class($criteria) : $criteria;
59
    }
60
61
    /**
62
     * Try to instantiate given criterion class name with this arguments.
63
     *
64
     * @param $class
65
     * @param $arguments
66
     *
67
     * @throws CriterionException
68
     *
69
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
70
     */
71
    protected function instantiateCriterion($class, $arguments)
72
    {
73
        $reflection = new \ReflectionClass($class);
74
75
        if (! $reflection->implementsInterface(CriterionContract::class)) {
76
            throw CriterionException::classNotImplementContract($class);
77
        }
78
79
        // If arguments is an associative array we can assume their order and parameter existence
80
        if (Arr::isAssoc($arguments)) {
81
            $parameters = array_column($reflection->getConstructor()->getParameters(), 'name');
82
83
            $arguments = array_filter(array_map(function ($parameter) use ($arguments) {
84
                return isset($arguments[$parameter]) ? $arguments[$parameter] : null;
85
            }, $parameters));
86
        }
87
88
        return $reflection->newInstanceArgs($arguments);
89
    }
90
91
    /**
92
     * Return class and arguments from passed array criterion.
93
     * Extracting class and arguments from array.
94
     *
95
     * @param array $criterion
96
     *
97
     * @throws CriterionException
98
     *
99
     * @return array
100
     */
101
    protected function extractCriterionClassAndArgs(array $criterion)
102
    {
103
        if (count($criterion) > 2 || empty($criterion)) {
104
            throw CriterionException::wrongArraySignature($criterion);
105
        }
106
107
        //If an array is assoc we assume that the key is a class and value is an arguments
108
        if (Arr::isAssoc($criterion)) {
109
            $criterion = [array_keys($criterion)[0], array_values($criterion)[0]];
110
111
        //If an array is not assoc but count is one, we can assume there is a class without arguments.
112
        //Like when a string passed as criterion
113
        } elseif (count($criterion) === 1) {
114
            array_push($criterion, []);
115
        }
116
117
        return $criterion;
118
    }
119
120
    /**
121
     * Add criterion to the specific list.
122
     * low-level implementation of adding criterion to the list.
123
     *
124
     * @param Closure|CriterionContract|array|string $criterion
125
     * @param string                                 $list
126
     *
127
     * @throws CriterionException
128
     * @throws RepositoryException
129
     *
130
     * @return $this
131
     */
132
    protected function addCriterion($criterion, $list)
133
    {
134
        if (! property_exists($this, $list)) {
135
            throw RepositoryException::listNotFound($list, $this);
136
        }
137
138
        if (! $criterion instanceof Closure &&
139
            ! $criterion instanceof CriterionContract &&
140
            ! is_string($criterion) &&
141
            ! is_array($criterion)
142
        ) {
143
            throw CriterionException::wrongCriterionType($criterion);
144
        }
145
146
        //If criterion is a string we will assume it is a class name without arguments
147
        //and we need to normalize signature for instantiation try
148
        if (is_string($criterion)) {
149
            $criterion = [$criterion, []];
150
        }
151
152
        //If the criterion is an array we will assume it is an array of class name with arguments
153
        //and try to instantiate this
154
        if (is_array($criterion)) {
155
            $criterion = call_user_func_array([$this, 'instantiateCriterion'], $this->extractCriterionClassAndArgs($criterion));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
156
        }
157
158
        $this->$list[$this->getCriterionName($criterion)] = $criterion;
159
160
        return $this;
161
    }
162
163
    /**
164
     * Add criteria to the specific list
165
     * low-level implementation of adding criteria to the list.
166
     *
167
     * @param array $criteria
168
     * @param $list
169
     */
170
    protected function addCriteria(array $criteria, $list)
171
    {
172
        array_walk($criteria, function ($value, $key) use ($list) {
173
            $criterion = is_string($key) ? [$key, $value] : $value;
174
            $this->addCriterion($criterion, $list);
175
        });
176
    }
177
178
    /**
179
     * Push criterion to the criteria list.
180
     *
181
     * @param CriterionContract|Closure|array|string $criterion
182
     *
183
     * @return $this
184
     */
185
    public function pushCriterion($criterion)
186
    {
187
        $this->addCriterion($criterion, 'criteria');
188
189
        return $this;
190
    }
191
192
    /**
193
     * Remove provided criterion from criteria list.
194
     *
195
     * @param CriterionContract|Closure|string $criterion
196
     *
197
     * @return $this
198
     */
199
    public function removeCriterion($criterion)
200
    {
201
        unset($this->criteria[$this->getCriterionName($criterion)]);
202
203
        return $this;
204
    }
205
206
    /**
207
     * Remove provided criteria from criteria list.
208
     *
209
     * @param array $criteria
210
     *
211
     * @return RepositoryContract
0 ignored issues
show
Documentation introduced by
Should the return type not be Criteriable?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
212
     */
213
    public function removeCriteria(array $criteria)
214
    {
215
        array_walk($criteria, function ($criterion) {
216
            $this->removeCriterion($criterion);
217
        });
218
219
        return $this;
220
    }
221
222
    /**
223
     * Push array of criteria to the criteria list.
224
     *
225
     * @param array $criteria
226
     *
227
     * @return $this
228
     */
229
    public function pushCriteria(array $criteria)
230
    {
231
        $this->addCriteria($criteria, 'criteria');
232
233
        return $this;
234
    }
235
236
    /**
237
     * Flush criteria list.
238
     * We can flush criteria only when they is not skipped.
239
     *
240
     * @return $this
241
     */
242
    public function flushCriteria()
243
    {
244
        if (! $this->skipCriteria) {
245
            $this->criteria = [];
246
        }
247
248
        return $this;
249
    }
250
251
    /**
252
     * Set default criteria list.
253
     *
254
     * @param array $criteria
255
     *
256
     * @return $this
257
     */
258
    public function setDefaultCriteria(array $criteria)
259
    {
260
        $this->addCriteria($criteria, 'defaultCriteria');
261
262
        return $this;
263
    }
264
265
    /**
266
     * Return default criteria list.
267
     *
268
     * @return array
269
     */
270
    public function getDefaultCriteria()
271
    {
272
        return $this->defaultCriteria;
273
    }
274
275
    /**
276
     * Return current list of criteria.
277
     *
278
     * @return array
279
     */
280
    public function getCriteria()
281
    {
282
        if ($this->skipCriteria) {
283
            return [];
284
        }
285
286
        return $this->skipDefaultCriteria ? $this->criteria : array_merge($this->getDefaultCriteria(), $this->criteria);
287
    }
288
289
    /**
290
     * Set skipCriteria flag.
291
     *
292
     * @param bool|true $flag
293
     *
294
     * @return $this
295
     */
296
    public function skipCriteria($flag = true)
297
    {
298
        $this->skipCriteria = $flag;
0 ignored issues
show
Documentation Bug introduced by
It seems like $flag can also be of type object<Rinvex\Repository\Traits\true>. However, the property $skipCriteria is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
299
300
        return $this;
301
    }
302
303
    /**
304
     * Set skipDefaultCriteria flag.
305
     *
306
     * @param bool|true $flag
307
     *
308
     * @return $this
309
     */
310
    public function skipDefaultCriteria($flag = true)
311
    {
312
        $this->skipDefaultCriteria = $flag;
0 ignored issues
show
Documentation Bug introduced by
It seems like $flag can also be of type object<Rinvex\Repository\Traits\true>. However, the property $skipDefaultCriteria is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
313
314
        return $this;
315
    }
316
317
    /**
318
     * Check if a given criterion name now in the criteria list.
319
     *
320
     * @param CriterionContract|Closure|string $criterion
321
     *
322
     * @return bool
323
     */
324
    public function hasCriterion($criterion)
325
    {
326
        return isset($this->getCriteria()[$this->getCriterionName($criterion)]);
327
    }
328
329
    /**
330
     * Return criterion object or closure from criteria list by name.
331
     *
332
     * @param $criterion
333
     *
334
     * @return CriterionContract|Closure|null
335
     */
336
    public function getCriterion($criterion)
337
    {
338
        if ($this->hasCriterion($criterion)) {
339
            return $this->getCriteria()[$this->getCriterionName($criterion)];
340
        }
341
    }
342
343
    /**
344
     * Apply criteria list to the given query.
345
     *
346
     * @param $query
347
     * @param $repository
348
     *
349
     * @return mixed
350
     */
351
    public function applyCriteria($query, $repository)
352
    {
353
        foreach ($this->getCriteria() as $criterion) {
354
            if ($criterion instanceof CriterionContract) {
355
                $query = $criterion->apply($query, $repository);
356
            } elseif ($criterion instanceof Closure) {
357
                $query = $criterion($query, $repository);
358
            }
359
        }
360
361
        return $query;
362
    }
363
}
364