Completed
Push — develop ( f34a65...a0b033 )
by Abdelrahman
02:30
created

Criteriable::applyCriteria()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
c 0
b 0
f 0
rs 9.2
cc 4
eloc 7
nc 4
nop 2
1
<?php
2
3
namespace Rinvex\Repository\Traits;
4
5
use Closure;
6
use Rinvex\Repository\Contracts\CriterionContract;
7
use Rinvex\Repository\Contracts\RepositoryContract;
8
use Rinvex\Repository\Exceptions\CriterionException;
9
use Rinvex\Repository\Exceptions\RepositoryException;
10
11
trait Criteriable
12
{
13
    /**
14
     * List of repository criteria.
15
     *
16
     * @var array
17
     */
18
    protected $criteria = [];
19
20
    /**
21
     * List of default repository criteria.
22
     *
23
     * @var array
24
     */
25
    protected $defaultCriteria = [];
26
27
    /**
28
     * Skip criteria flag.
29
     * If setted to true criteria will not be apply to the query.
30
     *
31
     * @var bool
32
     */
33
    protected $skipCriteria = false;
34
35
    /**
36
     * Skip default criteria flag.
37
     * If setted to true default criteria will not be added to the criteria list.
38
     *
39
     * @var bool
40
     */
41
    protected $skipDefaultCriteria = false;
42
43
    /**
44
     * Return name for the criterion.
45
     * If as criterion in parameter passed string we assume that is criterion class name.
46
     *
47
     * @param CriterionContract|Closure|string $criteria
48
     *
49
     * @return string
50
     */
51
    public function getCriterionName($criteria)
52
    {
53
        if ($criteria instanceof Closure) {
54
            return spl_object_hash($criteria);
55
        }
56
57
        return is_object($criteria) ? get_class($criteria) : $criteria;
58
    }
59
60
    /**
61
     * Try to instantiate given criterion class name with this arguments.
62
     *
63
     * @param $class
64
     * @param $arguments
65
     *
66
     * @throws CriterionException
67
     *
68
     * @return object
69
     */
70
    protected function instantiateCriterion($class, $arguments)
71
    {
72
        $reflection = new \ReflectionClass($class);
73
74
        if (! $reflection->implementsInterface(CriterionContract::class)) {
75
            throw CriterionException::classNotImplementContract($class);
76
        }
77
78
        //If arguments is an associative array we can assume their order and parameter existence
79
        if (count($arguments) && array_keys($arguments) !== range(0, count($arguments) - 1)) {
80
            $parameters = array_column($reflection->getConstructor()->getParameters(), 'name');
81
82
            $arguments = array_filter(array_map(function ($parameter) use ($arguments) {
83
                return isset($arguments[$parameter]) ? $arguments[$parameter] : null;
84
            }, $parameters));
85
        }
86
87
        return $reflection->newInstanceArgs($arguments);
88
    }
89
90
    /**
91
     * Return class and arguments from passed array criterion.
92
     * Extracting class and arguments from array.
93
     *
94
     * @param array $criterion
95
     *
96
     * @throws CriterionException
97
     *
98
     * @return array
99
     */
100
    protected function extractCriterionClassAndArgs(array $criterion)
101
    {
102
        if (count($criterion) > 2 || empty($criterion)) {
103
            throw CriterionException::wrongArraySignature($criterion);
104
        }
105
106
        //If an array is assoc we assume that the key is a class and value is an arguments
107
        if (array_keys($criterion) !== range(0, count($criterion) - 1)) {
108
            $criterion = [array_keys($criterion)[0], array_values($criterion)[0]];
109
110
        //If an array is not assoc but count is one, we can assume there is a class without arguments.
111
        //Like when a string passed as criterion
112
        } elseif (count($criterion) === 1) {
113
            array_push($criterion, []);
114
        }
115
116
        return $criterion;
117
    }
118
119
    /**
120
     * Add criterion to the specific list.
121
     * low-level implementation of adding criterion to the list.
122
     *
123
     * @param Closure|CriterionContract|array|string $criterion
124
     * @param string                                 $list
125
     *
126
     * @throws CriterionException
127
     * @throws RepositoryException
128
     *
129
     * @return $this
130
     */
131
    protected function addCriterion($criterion, $list)
132
    {
133
        if (! property_exists($this, $list)) {
134
            throw RepositoryException::listNotFound($list, $this);
135
        }
136
137
        if (! $criterion instanceof Closure &&
138
            ! $criterion instanceof CriterionContract &&
139
            ! is_string($criterion) &&
140
            ! is_array($criterion)
141
        ) {
142
            throw CriterionException::wrongCriterionType($criterion);
143
        }
144
145
        //If criterion is a string we will assume it is a class name without arguments
146
        //and we need to normalize signature for instantiation try
147
        if (is_string($criterion)) {
148
            $criterion = [$criterion, []];
149
        }
150
151
        //If the criterion is an array we will assume it is an array of class name with arguments
152
        //and try to instantiate this
153
        if (is_array($criterion)) {
154
            $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...
155
        }
156
157
        $this->$list[$this->getCriterionName($criterion)] = $criterion;
158
159
        return $this;
160
    }
161
162
    /**
163
     * Add criteria to the specific list
164
     * low-level implementation of adding criteria to the list.
165
     *
166
     * @param array $criteria
167
     * @param $list
168
     */
169
    protected function addCriteria(array $criteria, $list)
170
    {
171
        array_walk($criteria, function ($value, $key) use ($list) {
172
            $criterion = is_string($key) ? [$key, $value] : $value;
173
            $this->addCriterion($criterion, $list);
174
        });
175
    }
176
177
    /**
178
     * Push criterion to the criteria list.
179
     *
180
     * @param CriterionContract|Closure|array|string $criterion
181
     *
182
     * @return $this
183
     */
184
    public function pushCriterion($criterion)
185
    {
186
        $this->addCriterion($criterion, 'criteria');
187
188
        return $this;
189
    }
190
191
    /**
192
     * Remove provided criterion from criteria list.
193
     *
194
     * @param CriterionContract|Closure|string $criterion
195
     *
196
     * @return $this
197
     */
198
    public function removeCriterion($criterion)
199
    {
200
        unset($this->criteria[$this->getCriterionName($criterion)]);
201
202
        return $this;
203
    }
204
205
    /**
206
     * Remove provided criteria from criteria list.
207
     *
208
     * @param array $criteria
209
     *
210
     * @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...
211
     */
212
    public function removeCriteria(array $criteria)
213
    {
214
        array_walk($criteria, function ($criterion) {
215
            $this->removeCriterion($criterion);
216
        });
217
218
        return $this;
219
    }
220
221
    /**
222
     * Push array of criteria to the criteria list.
223
     *
224
     * @param array $criteria
225
     *
226
     * @return $this
227
     */
228
    public function pushCriteria(array $criteria)
229
    {
230
        $this->addCriteria($criteria, 'criteria');
231
232
        return $this;
233
    }
234
235
    /**
236
     * Flush criteria list.
237
     * We can flush criteria only when they is not skipped.
238
     *
239
     * @return $this
240
     */
241
    public function flushCriteria()
242
    {
243
        if (! $this->skipCriteria) {
244
            $this->criteria = [];
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Set default criteria list.
252
     *
253
     * @param array $criteria
254
     *
255
     * @return $this
256
     */
257
    public function setDefaultCriteria(array $criteria)
258
    {
259
        $this->addCriteria($criteria, 'defaultCriteria');
260
261
        return $this;
262
    }
263
264
    /**
265
     * Return default criteria list.
266
     *
267
     * @return array
268
     */
269
    public function getDefaultCriteria()
270
    {
271
        return $this->defaultCriteria;
272
    }
273
274
    /**
275
     * Return current list of criteria.
276
     *
277
     * @return array
278
     */
279
    public function getCriteria()
280
    {
281
        if ($this->skipCriteria) {
282
            return [];
283
        }
284
285
        return $this->skipDefaultCriteria ? $this->criteria : array_merge($this->getDefaultCriteria(), $this->criteria);
286
    }
287
288
    /**
289
     * Set skipCriteria flag.
290
     *
291
     * @param bool|true $flag
292
     *
293
     * @return $this
294
     */
295
    public function skipCriteria($flag = true)
296
    {
297
        $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...
298
299
        return $this;
300
    }
301
302
    /**
303
     * Set skipDefaultCriteria flag.
304
     *
305
     * @param bool|true $flag
306
     *
307
     * @return $this
308
     */
309
    public function skipDefaultCriteria($flag = true)
310
    {
311
        $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...
312
313
        return $this;
314
    }
315
316
    /**
317
     * Check if a given criterion name now in the criteria list.
318
     *
319
     * @param CriterionContract|Closure|string $criterion
320
     *
321
     * @return bool
322
     */
323
    public function hasCriterion($criterion)
324
    {
325
        return isset($this->getCriteria()[$this->getCriterionName($criterion)]);
326
    }
327
328
    /**
329
     * Return criterion object or closure from criteria list by name.
330
     *
331
     * @param $criterion
332
     *
333
     * @return CriterionContract|Closure|null
334
     */
335
    public function getCriterion($criterion)
336
    {
337
        if ($this->hasCriterion($criterion)) {
338
            return $this->getCriteria()[$this->getCriterionName($criterion)];
339
        }
340
    }
341
342
    /**
343
     * Apply criteria list to the given query.
344
     *
345
     * @param $query
346
     * @param $repository
347
     *
348
     * @return mixed
349
     */
350
    public function applyCriteria($query, $repository)
351
    {
352
        foreach ($this->getCriteria() as $criterion) {
353
            if ($criterion instanceof CriterionContract) {
354
                $query = $criterion->apply($query, $repository);
355
            } elseif ($criterion instanceof Closure) {
356
                $query = $criterion($query, $repository);
357
            }
358
        }
359
360
        return $query;
361
    }
362
}
363