Completed
Push — master ( b819b4...8115f1 )
by Sherif
14:17
created

BaseRepository::paginateBy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 6
1
<?php
2
3
namespace App\Modules\Core\BaseClasses;
4
5
use App\Modules\Core\Interfaces\BaseRepositoryInterface;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Str;
8
9
abstract class BaseRepository implements BaseRepositoryInterface
10
{
11
    /**
12
     * @var object
13
     */
14
    public $model;
15
    
16
    /**
17
     * Init new object.
18
     *
19
     * @var mixed model
20
     * @return  void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
21
     */
22
    public function __construct($model)
23
    {
24
        $this->model  = $model;
25
    }
26
27
    /**
28
     * Fetch all records with relations from the storage.
29
     *
30
     * @param  array   $relations
31
     * @param  string  $sortBy
32
     * @param  boolean $desc
33
     * @param  array   $columns
34
     * @return collection
35
     */
36
    public function all($relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
37
    {
38
        $sort = $desc ? 'desc' : 'asc';
39
        return $this->model->with($relations)->orderBy($sortBy, $sort)->get($columns);
40
    }
41
    
42
    /**
43
     * Fetch all records with relations from storage in pages.
44
     *
45
     * @param  integer $perPage
46
     * @param  array   $relations
47
     * @param  string  $sortBy
48
     * @param  boolean $desc
49
     * @param  array   $columns
50
     * @return collection
51
     */
52
    public function paginate($perPage = 15, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
53
    {
54
        $sort = $desc ? 'desc' : 'asc';
55
        return $this->model->with($relations)->orderBy($sortBy, $sort)->paginate($perPage, $columns);
56
    }
57
58
    /**
59
     * Fetch all records with relations based on
60
     * the given condition from storage in pages.
61
     *
62
     * @param  array   $conditions array of conditions
63
     * @param  integer $perPage
64
     * @param  array   $relations
65
     * @param  string  $sortBy
66
     * @param  boolean $desc
67
     * @param  array   $columns
68
     * @return collection
69
     */
70
    public function paginateBy($conditions, $perPage = 15, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
71
    {
72
        $conditions = $this->constructConditions($conditions, $this->model);
73
        $sort       = $desc ? 'desc' : 'asc';
74
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->orderBy($sortBy, $sort)->paginate($perPage, $columns);
75
    }
76
77
    /**
78
     * Count all records based on the given condition from storage.
79
     *
80
     * @param  array   $conditions array of conditions
81
     * @return collection
82
     */
83
    public function count($conditions)
84
    {
85
        $conditions = $this->constructConditions($conditions, $this->model);
86
        return $this->model->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->count();
87
    }
88
    
89
    /**
90
     * Save the given model to the storage.
91
     *
92
     * @param  array $data
93
     * @return mixed
94
     */
95
    public function save(array $data)
96
    {
97
        \Session::put('locale', 'all');
98
        $model      = false;
99
        $relations  = [];
100
101
        \DB::transaction(function () use (&$model, &$relations, $data) {
102
            
103
            $model     = $this->prepareModel($data);
104
            $relations = $this->prepareRelations($data, $model);
105
            $model     = $this->saveModel($model, $relations);
106
        });
107
        
108
        if (count($relations)) {
109
            $model->load(...array_keys($relations));
0 ignored issues
show
Bug introduced by
The method load cannot be called on $model (of type false).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
110
        }
111
112
        return $model;
113
    }
114
115
    /**
116
     * Delete record from the storage based on the given
117
     * condition.
118
     *
119
     * @param  var $value condition value
120
     * @param  string $attribute condition column name
121
     * @return void
122
     */
123
    public function delete($value, $attribute = 'id')
124
    {
125
        \DB::transaction(function () use ($value, $attribute) {
126
            $this->model->where($attribute, '=', $value)->lockForUpdate()->get()->each(function ($model) {
127
                $model->delete();
128
            });
129
        });
130
    }
131
    
132
    /**
133
     * Fetch records from the storage based on the given
134
     * id.
135
     *
136
     * @param  integer $id
137
     * @param  string[]   $relations
138
     * @param  array   $columns
139
     * @return object
140
     */
141
    public function find($id, $relations = [], $columns = ['*'])
142
    {
143
        return $this->model->with($relations)->find($id, $columns);
144
    }
145
    
146
    /**
147
     * Fetch records from the storage based on the given
148
     * condition.
149
     *
150
     * @param  array   $conditions array of conditions
151
     * @param  array   $relations
152
     * @param  string  $sortBy
153
     * @param  boolean $desc
154
     * @param  array   $columns
155
     * @return collection
156
     */
157
    public function findBy($conditions, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
158
    {
159
        $conditions = $this->constructConditions($conditions, $this->model);
160
        $sort       = $desc ? 'desc' : 'asc';
161
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->orderBy($sortBy, $sort)->get($columns);
162
    }
163
164
    /**
165
     * Fetch the first record from the storage based on the given
166
     * condition.
167
     *
168
     * @param  array   $conditions array of conditions
169
     * @param  array   $relations
170
     * @param  array   $columns
171
     * @return object
172
     */
173
    public function first($conditions, $relations = [], $columns = ['*'])
174
    {
175
        $conditions = $this->constructConditions($conditions, $this->model);
176
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->first($columns);
177
    }
178
179
    /**
180
     * Return the deleted models in pages based on the given conditions.
181
     *
182
     * @param  array   $conditions array of conditions
183
     * @param  integer $perPage
184
     * @param  string  $sortBy
185
     * @param  boolean $desc
186
     * @param  array   $columns
187
     * @return collection
188
     */
189
    public function deleted($conditions, $perPage = 15, $sortBy = 'created_at', $desc = 1, $columns = ['*'])
190
    {
191
        unset($conditions['page']);
192
        unset($conditions['perPage']);
193
        unset($conditions['sortBy']);
194
        unset($conditions['sort']);
195
        $conditions = $this->constructConditions($conditions, $this->model);
196
        $sort       = $desc ? 'desc' : 'asc';
197
        $model      = $this->model->onlyTrashed();
198
199
        if (count($conditions['conditionValues'])) {
200
            $model->whereRaw($conditions['conditionString'], $conditions['conditionValues']);
201
        }
202
203
        return $model->orderBy($sortBy, $sort)->paginate($perPage, $columns);
204
    }
205
206
    /**
207
     * Restore the deleted model.
208
     *
209
     * @param  integer $id
210
     * @return void
211
     */
212
    public function restore($id)
213
    {
214
        $model = $this->model->onlyTrashed()->find($id);
215
216
        if (! $model) {
217
            \Errors::notFound(class_basename($this->model).' with id : '.$id);
218
        }
219
220
        $model->restore();
221
    }
222
223
    /**
224
     * Fill the model with the given data.
225
     *
226
     * @param   array  $data
227
     *
228
     * @return  object
229
     */
230
    public function prepareModel($data)
231
    {
232
        $modelClass = $this->model;
233
234
        /**
235
         * If the id is present in the data then select the model for updating,
236
         * else create new model.
237
         * @var array
238
         */
239
        $model = Arr::has($data, 'id') ? $modelClass->lockForUpdate()->find($data['id']) : new $modelClass;
240
        if (! $model) {
241
            \Errors::notFound(class_basename($modelClass).' with id : '.$data['id']);
242
        }
243
244
        /**
245
         * Construct the model object with the given data,
246
         * and if there is a relation add it to relations array,
247
         * then save the model.
248
         */
249
        foreach ($data as $key => $value) {
250
            if (array_search($key, $model->getFillable(), true) !== false) {
251
                /**
252
                 * If the attribute isn't a relation and prevent attributes not in the fillable.
253
                 */
254
                $model->$key = $value;
255
            }
256
        }
257
258
        return $model;
259
    }
260
    
261
    /**
262
     * Prepare related models based on the given data for the given model.
263
     *
264
     * @param   array  $data
265
     * @param   object $model
266
     *
267
     * @return  array
268
     */
269
    public function prepareRelations($data, $model)
270
    {
271
        /**
272
         * Init the relation array
273
         *
274
         * @var array
275
         */
276
        $relations = [];
277
278
        /**
279
         * Construct the model object with the given data,
280
         * and if there is a relation add it to relations array,
281
         * then save the model.
282
         */
283
        foreach ($data as $key => $value) {
284
            /**
285
             * If the attribute is a relation.
286
             */
287
            $relation = \Str::camel($key);
288
            if (method_exists($model, $relation) && \Core::$relation()) {
289
                /**
290
                 * Check if the relation is a collection.
291
                 */
292
                if (class_basename($model->$relation) == 'Collection') {
293
                    /**
294
                     * If the relation has no value then marke the relation data
295
                     * related to the model to be deleted.
296
                     */
297
                    if (! $value || ! count($value)) {
298
                        $relations[$relation] = 'delete';
299
                    }
300
                }
301
                if (is_array($value)) {
302
                    /**
303
                     * Loop through the relation data.
304
                     */
305
                    foreach ($value as $attr => $val) {
306
                        /**
307
                         * Get the relation model.
308
                         */
309
                        $relationBaseModel = \Core::$relation()->model;
310
311
                        /**
312
                         * Check if the relation is a collection.
313
                         */
314
                        if (class_basename($model->$relation) == 'Collection') {
315
                            /**
316
                             * If the id is present in the data then select the relation model for updating,
317
                             * else create new model.
318
                             */
319
                            $relationModel = Arr::has($val, 'id') ? $relationBaseModel->lockForUpdate()->find($val['id']) : new $relationBaseModel;
320
321
                            /**
322
                             * If model doesn't exists.
323
                             */
324
                            if (! $relationModel) {
325
                                \Errors::notFound(class_basename($relationBaseModel).' with id : '.$val['id']);
326
                            }
327
328
                            /**
329
                             * Loop through the relation attributes.
330
                             */
331
                            foreach ($val as $attr => $val) {
332
                                /**
333
                                 * Prevent the sub relations or attributes not in the fillable.
334
                                 */
335
                                if (gettype($val) !== 'object' && gettype($val) !== 'array' && array_search($attr, $relationModel->getFillable(), true) !== false) {
336
                                    $relationModel->$attr = $val;
337
                                } elseif(gettype($val) !== 'object' && gettype($val) !== 'array' && $attr !== 'id') {
338
                                    $extra[$attr] = $val;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$extra was never initialized. Although not strictly required by PHP, it is generally a good practice to add $extra = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
339
                                }
340
                            }
341
342
                            if(isset($extra)) $relationModel->extra = $extra;
343
                            $relations[$relation][] = $relationModel;
344
                        } else {
345
                            /**
346
                             * Prevent the sub relations.
347
                             */
348
                            if (gettype($val) !== 'object' && gettype($val) !== 'array') {
349
                                /**
350
                                 * If the id is present in the data then select the relation model for updating,
351
                                 * else create new model.
352
                                 */
353
                                $relationModel = Arr::has($value, 'id') ? $relationBaseModel->lockForUpdate()->find($value['id']) : new $relationBaseModel;
354
355
                                /**
356
                                 * If model doesn't exists.
357
                                 */
358
                                if (! $relationModel) {
359
                                    \Errors::notFound(class_basename($relationBaseModel).' with id : '.$value['id']);
360
                                }
361
362
                                foreach ($value as $relationAttribute => $relationValue) {
363
                                    /**
364
                                     * Prevent attributes not in the fillable.
365
                                     */
366
                                    if (array_search($relationAttribute, $relationModel->getFillable(), true) !== false) {
367
                                        $relationModel->$relationAttribute = $relationValue;
368
                                    }
369
                                }
370
371
                                $relations[$relation] = $relationModel;
372
                            }
373
                        }
374
                    }
375
                }
376
            }
377
        }
378
379
        return $relations;
380
    }
381
382
    /**
383
     * Save the model with related models.
384
     *
385
     * @param   object  $model
386
     * @param   array   $relations
387
     *
388
     * @return  object
389
     */
390
    public function saveModel($model, $relations)
391
    {
392
393
        /**
394
         * Loop through the relations array.
395
         */
396
        foreach ($relations as $key => $value) {
397
            /**
398
             * If the relation is marked for delete then delete it.
399
             */
400
            if ($value == 'delete' && $model->$key()->count()) {
401
                $model->$key()->delete();
402
            } elseif (gettype($value) == 'array') {
403
                /**
404
                 * Save the model.
405
                 */
406
                $model->save();
407
                $ids = [];
408
409
                /**
410
                 * Loop through the relations.
411
                 */
412
                foreach ($value as $val) {
413
                    switch (class_basename($model->$key())) {
414
                        /**
415
                         * If the relation is one to many then update it's foreign key with
416
                         * the model id and save it then add its id to ids array to delete all
417
                         * relations who's id isn't in the ids array.
418
                         */
419
                        case 'HasMany':
420
                            $foreignKeyName       = $model->$key()->getForeignKeyName();
421
                            $val->$foreignKeyName = $model->id;
422
                            $val->save();
423
                            $ids[] = $val->id;
424
                            break;
425
426
                        /**
427
                         * If the relation is many to many then add it's id to the ids array to
428
                         * attache these ids to the model.
429
                         */
430
                        case 'BelongsToMany':
431
                            $extra = $val->extra;
432
                            unset($val->extra);
433
                            $val->save();
434
                            $ids[$val->id] = $extra ?? [];
435
                            break;
436
                    }
437
                }
438
                switch (class_basename($model->$key())) {
439
                    /**
440
                     * If the relation is one to many then delete all
441
                     * relations who's id isn't in the ids array.
442
                     */
443
                    case 'HasMany':
444
                        $model->$key()->whereNotIn('id', $ids)->delete();
445
                        break;
446
447
                    /**
448
                     * If the relation is many to many then
449
                     * detach the previous data and attach
450
                     * the ids array to the model.
451
                     */
452
                    case 'BelongsToMany':
453
                        $model->$key()->detach();
454
                        $model->$key()->attach($ids);
455
                        break;
456
                }
457
            } else {
458
                switch (class_basename($model->$key())) {
459
                    /**
460
                     * If the relation is one to one.
461
                     */
462
                    case 'HasOne':
463
                        /**
464
                         * Save the model.
465
                         */
466
                        $model->save();
467
                        $foreignKeyName         = $model->$key()->getForeignKeyName();
468
                        $value->$foreignKeyName = $model->id;
469
                        $value->save();
470
                        break;
471
                    case 'BelongsTo':
472
                        /**
473
                         * Save the model.
474
                         */
475
                        $value->save();
476
                        $model->$key()->associate($value);
477
                        break;
478
                }
479
            }
480
        }
481
482
        /**
483
         * Save the model.
484
         */
485
        $model->save();
486
487
        return $model;
488
    }
489
490
    /**
491
     * Build the conditions recursively for the retrieving methods.
492
     * @param  array $conditions
493
     * @return array
494
     */
495
    protected function constructConditions($conditions, $model)
496
    {
497
        $conditionString = '';
498
        $conditionValues = [];
499
        foreach ($conditions as $key => $value) {
500
            if (Str::contains($key, '->')) {
501
                $key = $this->wrapJsonSelector($key);
502
            }
503
504
            if ($key == 'and') {
505
                $conditions       = $this->constructConditions($value, $model);
506
                $conditionString .= str_replace('{op}', 'and', $conditions['conditionString']).' {op} ';
507
                $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
508
            } elseif ($key == 'or') {
509
                $conditions       = $this->constructConditions($value, $model);
510
                $conditionString .= str_replace('{op}', 'or', $conditions['conditionString']).' {op} ';
511
                $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
512
            } else {
513
                if (is_array($value)) {
514
                    $operator = $value['op'];
515
                    if (strtolower($operator) == 'between') {
516
                        $value1 = $value['val1'];
517
                        $value2 = $value['val2'];
518
                    } else {
519
                        $value = Arr::get($value, 'val', '');
520
                    }
521
                } else {
522
                    $operator = '=';
523
                }
524
                
525
                if (strtolower($operator) == 'between') {
526
                    $conditionString  .= $key.' >= ? and ';
527
                    $conditionValues[] = $value1;
0 ignored issues
show
Bug introduced by
The variable $value1 does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
528
529
                    $conditionString  .= $key.' <= ? {op} ';
530
                    $conditionValues[] = $value2;
0 ignored issues
show
Bug introduced by
The variable $value2 does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
531
                } elseif (strtolower($operator) == 'in') {
532
                    $conditionValues  = array_merge($conditionValues, $value);
533
                    $inBindingsString = rtrim(str_repeat('?,', count($value)), ',');
534
                    $conditionString .= $key.' in ('.rtrim($inBindingsString, ',').') {op} ';
535
                } elseif (strtolower($operator) == 'null') {
536
                    $conditionString .= $key.' is null {op} ';
537
                } elseif (strtolower($operator) == 'not null') {
538
                    $conditionString .= $key.' is not null {op} ';
539
                } elseif (strtolower($operator) == 'has') {
540
                    $sql              = $model->withTrashed()->has($key)->toSql();
541
                    $conditions       = $this->constructConditions($value, $model->$key()->getRelated());
542
                    $conditionString .= rtrim(substr($sql, strpos($sql, 'exists')), ')').' and '.$conditions['conditionString'].') {op} ';
543
                    $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
544
                } else {
545
                    $conditionString  .= $key.' '.$operator.' ? {op} ';
546
                    $conditionValues[] = $value;
547
                }
548
            }
549
        }
550
        $conditionString = '('.rtrim($conditionString, '{op} ').')';
551
        return ['conditionString' => $conditionString, 'conditionValues' => $conditionValues];
552
    }
553
554
    /**
555
     * Wrap the given JSON selector.
556
     *
557
     * @param  string  $value
558
     * @return string
559
     */
560
    protected function wrapJsonSelector($value)
561
    {
562
        $removeLast = strpos($value, ')');
563
        $value      = $removeLast === false ? $value : substr($value, 0, $removeLast);
564
        $path       = explode('->', $value);
565
        $field      = array_shift($path);
566
        $result     = sprintf('%s->\'$.%s\'', $field, collect($path)->map(function ($part) {
567
            return '"'.$part.'"';
568
        })->implode('.'));
569
        
570
        return $removeLast === false ? $result : $result.')';
571
    }
572
}
573