Completed
Push — master ( 39b104...a0772e )
by Sherif
01:57
created

BaseRepository::find()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
1
<?php namespace App\Modules\Core\BaseClasses;
2
3
use App\Modules\Core\Interfaces\BaseRepositoryInterface;
4
use Illuminate\Support\Arr;
5
use Illuminate\Support\Str;
6
7
abstract class BaseRepository implements BaseRepositoryInterface
8
{
9
    /**
10
     * @var object
11
     */
12
    public $model;
13
    
14
    /**
15
     * Init new object.
16
     *
17
     * @var mixed model
18
     * @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...
19
     */
20
    public function __construct($model)
21
    {
22
        $this->model  = $model;
23
    }
24
25
    /**
26
     * Fetch all records with relations from the storage.
27
     *
28
     * @param  array   $relations
29
     * @param  string  $sortBy
30
     * @param  boolean $desc
31
     * @param  array   $columns
32
     * @return collection
33
     */
34
    public function all($relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
35
    {
36
        $sort = $desc ? 'desc' : 'asc';
37
        return $this->model->with($relations)->orderBy($sortBy, $sort)->get($columns);
38
    }
39
    
40
    /**
41
     * Fetch all records with relations from storage in pages.
42
     *
43
     * @param  integer $perPage
44
     * @param  array   $relations
45
     * @param  string  $sortBy
46
     * @param  boolean $desc
47
     * @param  array   $columns
48
     * @return collection
49
     */
50
    public function paginate($perPage = 15, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
51
    {
52
        $sort = $desc ? 'desc' : 'asc';
53
        return $this->model->with($relations)->orderBy($sortBy, $sort)->paginate($perPage, $columns);
54
    }
55
56
    /**
57
     * Fetch all records with relations based on
58
     * the given condition from storage in pages.
59
     *
60
     * @param  array   $conditions array of conditions
61
     * @param  integer $perPage
62
     * @param  array   $relations
63
     * @param  string  $sortBy
64
     * @param  boolean $desc
65
     * @param  array   $columns
66
     * @return collection
67
     */
68
    public function paginateBy($conditions, $perPage = 15, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
69
    {
70
        $conditions = $this->constructConditions($conditions, $this->model);
71
        $sort       = $desc ? 'desc' : 'asc';
72
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->orderBy($sortBy, $sort)->paginate($perPage, $columns);
73
    }
74
    
75
    /**
76
     * Save the given model to the storage.
77
     *
78
     * @param  array $data
79
     * @return mixed
80
     */
81
    public function save(array $data)
82
    {
83
        \Session::put('locale', 'all');
84
        $model      = false;
85
        $relations  = [];
86
87
        \DB::transaction(function () use (&$model, $relations, $data) {
88
            
89
            $model     = $this->prepareModel($data);
90
            $relations = $this->prepareRelations($data, $model);
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $relations, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
91
            $model     = $this->saveModel($model, $relations);
92
        });
93
            
94
        return $model;
95
    }
96
97
    /**
98
     * Delete record from the storage based on the given
99
     * condition.
100
     *
101
     * @param  var $value condition value
102
     * @param  string $attribute condition column name
103
     * @return void
104
     */
105
    public function delete($value, $attribute = 'id')
106
    {
107
        \DB::transaction(function () use ($value, $attribute) {
108
            $this->model->where($attribute, '=', $value)->lockForUpdate()->get()->each(function ($model) {
109
                $model->delete();
110
            });
111
        });
112
    }
113
    
114
    /**
115
     * Fetch records from the storage based on the given
116
     * id.
117
     *
118
     * @param  integer $id
119
     * @param  string[]   $relations
120
     * @param  array   $columns
121
     * @return object
122
     */
123
    public function find($id, $relations = [], $columns = ['*'])
124
    {
125
        return $this->model->with($relations)->find($id, $columns);
126
    }
127
    
128
    /**
129
     * Fetch records from the storage based on the given
130
     * condition.
131
     *
132
     * @param  array   $conditions array of conditions
133
     * @param  array   $relations
134
     * @param  string  $sortBy
135
     * @param  boolean $desc
136
     * @param  array   $columns
137
     * @return collection
138
     */
139
    public function findBy($conditions, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
140
    {
141
        $conditions = $this->constructConditions($conditions, $this->model);
142
        $sort       = $desc ? 'desc' : 'asc';
143
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->orderBy($sortBy, $sort)->get($columns);
144
    }
145
146
    /**
147
     * Fetch the first record from the storage based on the given
148
     * condition.
149
     *
150
     * @param  array   $conditions array of conditions
151
     * @param  array   $relations
152
     * @param  array   $columns
153
     * @return object
154
     */
155
    public function first($conditions, $relations = [], $columns = ['*'])
156
    {
157
        $conditions = $this->constructConditions($conditions, $this->model);
158
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->first($columns);
159
    }
160
161
    /**
162
     * Return the deleted models in pages based on the given conditions.
163
     *
164
     * @param  array   $conditions array of conditions
165
     * @param  integer $perPage
166
     * @param  string  $sortBy
167
     * @param  boolean $desc
168
     * @param  array   $columns
169
     * @return collection
170
     */
171
    public function deleted($conditions, $perPage = 15, $sortBy = 'created_at', $desc = 1, $columns = ['*'])
172
    {
173
        unset($conditions['page']);
174
        unset($conditions['perPage']);
175
        unset($conditions['sortBy']);
176
        unset($conditions['sort']);
177
        $conditions = $this->constructConditions($conditions, $this->model);
178
        $sort       = $desc ? 'desc' : 'asc';
179
        $model      = $this->model->onlyTrashed();
180
181
        if (count($conditions['conditionValues'])) {
182
            $model->whereRaw($conditions['conditionString'], $conditions['conditionValues']);
183
        }
184
185
        return $model->orderBy($sortBy, $sort)->paginate($perPage, $columns);
186
    }
187
188
    /**
189
     * Restore the deleted model.
190
     *
191
     * @param  integer $id
192
     * @return void
193
     */
194
    public function restore($id)
195
    {
196
        $model = $this->model->onlyTrashed()->find($id);
197
198
        if (! $model) {
199
            \Errors::notFound(class_basename($this->model).' with id : '.$id);
200
        }
201
202
        $model->restore();
203
    }
204
205
    /**
206
     * Fill the model with the given data.
207
     *
208
     * @param   array  $data
209
     *
210
     * @return  object
211
     */
212
    public function prepareModel($data)
213
    {
214
        $modelClass = $this->model;
215
216
        /**
217
         * If the id is present in the data then select the model for updating,
218
         * else create new model.
219
         * @var array
220
         */
221
        $model = Arr::has($data, 'id') ? $modelClass->lockForUpdate()->find($data['id']) : new $modelClass;
222
        if (! $model) {
223
            \Errors::notFound(class_basename($modelClass).' with id : '.$data['id']);
224
        }
225
226
        /**
227
         * Construct the model object with the given data,
228
         * and if there is a relation add it to relations array,
229
         * then save the model.
230
         */
231
        foreach ($data as $key => $value) {
232
            if (array_search($key, $model->getFillable(), true) !== false) {
233
                /**
234
                 * If the attribute isn't a relation and prevent attributes not in the fillable.
235
                 */
236
                $model->$key = $value;
237
            }
238
        }
239
240
        return $model;
241
    }
242
    
243
    /**
244
     * Prepare related models based on the given data for the given model.
245
     *
246
     * @param   array  $data
247
     * @param   object $model
248
     *
249
     * @return  array
250
     */
251
    public function prepareRelations($data, $model)
252
    {
253
        /**
254
         * Construct the model object with the given data,
255
         * and if there is a relation add it to relations array,
256
         * then save the model.
257
         */
258
        foreach ($data as $key => $value) {
259
            /**
260
             * If the attribute is a relation.
261
             */
262
            $relation = \Str::camel($key);
263
            if (method_exists($model, $relation) && \Core::$relation()) {
264
                /**
265
                 * Check if the relation is a collection.
266
                 */
267
                if (class_basename($model->$relation) == 'Collection') {
268
                    /**
269
                     * If the relation has no value then marke the relation data
270
                     * related to the model to be deleted.
271
                     */
272
                    if (! $value || ! count($value)) {
273
                        $relations[$relation] = 'delete';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$relations was never initialized. Although not strictly required by PHP, it is generally a good practice to add $relations = 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...
274
                    }
275
                }
276
                if (is_array($value)) {
277
                    /**
278
                     * Loop through the relation data.
279
                     */
280
                    foreach ($value as $attr => $val) {
281
                        /**
282
                         * Get the relation model.
283
                         */
284
                        $relationBaseModel = \Core::$relation()->model;
285
286
                        /**
287
                         * Check if the relation is a collection.
288
                         */
289
                        if (class_basename($model->$relation) == 'Collection') {
290
                            /**
291
                             * If the id is present in the data then select the relation model for updating,
292
                             * else create new model.
293
                             */
294
                            $relationModel = Arr::has($val, 'id') ? $relationBaseModel->lockForUpdate()->find($val['id']) : new $relationBaseModel;
295
296
                            /**
297
                             * If model doesn't exists.
298
                             */
299
                            if (! $relationModel) {
300
                                \Errors::notFound(class_basename($relationBaseModel).' with id : '.$val['id']);
301
                            }
302
303
                            /**
304
                             * Loop through the relation attributes.
305
                             */
306
                            foreach ($val as $attr => $val) {
307
                                /**
308
                                 * Prevent the sub relations or attributes not in the fillable.
309
                                 */
310
                                if (gettype($val) !== 'object' && gettype($val) !== 'array' && array_search($attr, $relationModel->getFillable(), true) !== false) {
311
                                    $relationModel->$attr = $val;
312
                                }
313
                            }
314
315
                            $relations[$relation][] = $relationModel;
0 ignored issues
show
Bug introduced by
The variable $relations 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...
316
                        } else {
317
                            /**
318
                             * Prevent the sub relations.
319
                             */
320
                            if (gettype($val) !== 'object' && gettype($val) !== 'array') {
321
                                /**
322
                                 * If the id is present in the data then select the relation model for updating,
323
                                 * else create new model.
324
                                 */
325
                                $relationModel = Arr::has($value, 'id') ? $relationBaseModel->lockForUpdate()->find($value['id']) : new $relationBaseModel;
326
327
                                /**
328
                                 * If model doesn't exists.
329
                                 */
330
                                if (! $relationModel) {
331
                                    \Errors::notFound(class_basename($relationBaseModel).' with id : '.$value['id']);
332
                                }
333
334
                                foreach ($value as $relationAttribute => $relationValue) {
335
                                    /**
336
                                     * Prevent attributes not in the fillable.
337
                                     */
338
                                    if (array_search($relationAttribute, $relationModel->getFillable(), true) !== false) {
339
                                        $relationModel->$relationAttribute = $relationValue;
340
                                    }
341
                                }
342
343
                                $relations[$relation] = $relationModel;
344
                            }
345
                        }
346
                    }
347
                }
348
            }
349
        }
350
351
        return $relations;
352
    }
353
354
    /**
355
     * Save the model with related models.
356
     *
357
     * @param   object  $model
358
     * @param   array   $relations
359
     *
360
     * @return  object
361
     */
362
    public function saveModel($model, $relations)
363
    {
364
365
        /**
366
         * Loop through the relations array.
367
         */
368
        foreach ($relations as $key => $value) {
369
            /**
370
             * If the relation is marked for delete then delete it.
371
             */
372
            if ($value == 'delete' && $model->$key()->count()) {
373
                $model->$key()->delete();
374
            } elseif (gettype($value) == 'array') {
375
                /**
376
                 * Save the model.
377
                 */
378
                $model->save();
379
                $ids = [];
380
381
                /**
382
                 * Loop through the relations.
383
                 */
384
                foreach ($value as $val) {
385
                    switch (class_basename($model->$key())) {
386
                        /**
387
                         * If the relation is one to many then update it's foreign key with
388
                         * the model id and save it then add its id to ids array to delete all
389
                         * relations who's id isn't in the ids array.
390
                         */
391
                        case 'HasMany':
392
                            $foreignKeyName       = $model->$key()->getForeignKeyName();
393
                            $val->$foreignKeyName = $model->id;
394
                            $val->save();
395
                            $ids[] = $val->id;
396
                            break;
397
398
                        /**
399
                         * If the relation is many to many then add it's id to the ids array to
400
                         * attache these ids to the model.
401
                         */
402
                        case 'BelongsToMany':
403
                            $val->save();
404
                            $ids[] = $val->id;
405
                            break;
406
                    }
407
                }
408
                switch (class_basename($model->$key())) {
409
                    /**
410
                     * If the relation is one to many then delete all
411
                     * relations who's id isn't in the ids array.
412
                     */
413
                    case 'HasMany':
414
                        $model->$key()->whereNotIn('id', $ids)->delete();
415
                        break;
416
417
                    /**
418
                     * If the relation is many to many then
419
                     * detach the previous data and attach
420
                     * the ids array to the model.
421
                     */
422
                    case 'BelongsToMany':
423
                        $model->$key()->detach();
424
                        $model->$key()->attach($ids);
425
                        break;
426
                }
427
            } else {
428
                switch (class_basename($model->$key())) {
429
                    /**
430
                     * If the relation is one to one.
431
                     */
432
                    case 'HasOne':
433
                        /**
434
                         * Save the model.
435
                         */
436
                        $model->save();
437
                        $foreignKeyName         = $model->$key()->getForeignKeyName();
438
                        $value->$foreignKeyName = $model->id;
439
                        $value->save();
440
                        break;
441
                    case 'BelongsTo':
442
                        /**
443
                         * Save the model.
444
                         */
445
                        $value->save();
446
                        $model->$key()->associate($value);
447
                        break;
448
                }
449
            }
450
        }
451
452
        /**
453
         * Save the model.
454
         */
455
        $model->save();
456
457
        return $model;
458
    }
459
460
    /**
461
     * Build the conditions recursively for the retrieving methods.
462
     * @param  array $conditions
463
     * @return array
464
     */
465
    protected function constructConditions($conditions, $model)
466
    {
467
        $conditionString = '';
468
        $conditionValues = [];
469
        foreach ($conditions as $key => $value) {
470
            if (Str::contains($key, '->')) {
471
                $key = $this->wrapJsonSelector($key);
472
            }
473
474
            if ($key == 'and') {
475
                $conditions       = $this->constructConditions($value, $model);
476
                $conditionString .= str_replace('{op}', 'and', $conditions['conditionString']).' {op} ';
477
                $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
478
            } elseif ($key == 'or') {
479
                $conditions       = $this->constructConditions($value, $model);
480
                $conditionString .= str_replace('{op}', 'or', $conditions['conditionString']).' {op} ';
481
                $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
482
            } else {
483
                if (is_array($value)) {
484
                    $operator = $value['op'];
485
                    if (strtolower($operator) == 'between') {
486
                        $value1 = $value['val1'];
487
                        $value2 = $value['val2'];
488
                    } else {
489
                        $value = Arr::get($value, 'val', '');
490
                    }
491
                } else {
492
                    $operator = '=';
493
                }
494
                
495
                if (strtolower($operator) == 'between') {
496
                    $conditionString  .= $key.' >= ? and ';
497
                    $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...
498
499
                    $conditionString  .= $key.' <= ? {op} ';
500
                    $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...
501
                } elseif (strtolower($operator) == 'in') {
502
                    $conditionValues  = array_merge($conditionValues, $value);
503
                    $inBindingsString = rtrim(str_repeat('?,', count($value)), ',');
504
                    $conditionString .= $key.' in ('.rtrim($inBindingsString, ',').') {op} ';
505
                } elseif (strtolower($operator) == 'null') {
506
                    $conditionString .= $key.' is null {op} ';
507
                } elseif (strtolower($operator) == 'not null') {
508
                    $conditionString .= $key.' is not null {op} ';
509
                } elseif (strtolower($operator) == 'has') {
510
                    $sql              = $model->withTrashed()->has($key)->toSql();
511
                    $conditions       = $this->constructConditions($value, $model->$key()->getRelated());
512
                    $conditionString .= rtrim(substr($sql, strpos($sql, 'exists')), ')').' and '.$conditions['conditionString'].') {op} ';
513
                    $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
514
                } else {
515
                    $conditionString  .= $key.' '.$operator.' ? {op} ';
516
                    $conditionValues[] = $value;
517
                }
518
            }
519
        }
520
        $conditionString = '('.rtrim($conditionString, '{op} ').')';
521
        return ['conditionString' => $conditionString, 'conditionValues' => $conditionValues];
522
    }
523
524
    /**
525
     * Wrap the given JSON selector.
526
     *
527
     * @param  string  $value
528
     * @return string
529
     */
530
    protected function wrapJsonSelector($value)
531
    {
532
        $removeLast = strpos($value, ')');
533
        $value      = $removeLast === false ? $value : substr($value, 0, $removeLast);
534
        $path       = explode('->', $value);
535
        $field      = array_shift($path);
536
        $result     = sprintf('%s->\'$.%s\'', $field, collect($path)->map(function ($part) {
537
            return '"'.$part.'"';
538
        })->implode('.'));
539
        
540
        return $removeLast === false ? $result : $result.')';
541
    }
542
}
543