Completed
Push — master ( b45468...dcac8e )
by Sherif
02:16
created

BaseRepository::save()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 1
nc 1
nop 1
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
     * The model implementation.
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 records with relations based on the given params.
29
     *
30
     * @param   string  $relations
31
     * @param   array   $conditions
32
     * @param   integer $perPage
33
     * @param   string  $sortBy
34
     * @param   boolean $desc
35
     * @return collection
36
     */
37
    public function list($relations = [], $conditions = false, $perPage = 15, $sortBy = 'created_at', $desc = true)
38
    {
39
        unset($conditions['page']);
40
        unset($conditions['perPage']);
41
        unset($conditions['sortBy']);
42
        unset($conditions['sort']);
43
        unset($conditions['page']);
44
45
        if (count($conditions)) {
46
            return $this->paginateBy(['and' => $conditions], $perPage ?? 15, $relations, $sortBy ?? 'created_at', $desc ?? true);
0 ignored issues
show
Bug introduced by
It seems like $relations defined by parameter $relations on line 37 can also be of type string; however, App\Modules\Core\BaseCla...epository::paginateBy() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Documentation introduced by
$desc ?? true is of type boolean, but the function expects a integer.

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...
47
        }
48
49
        return $this->paginate($perPage ?? 15, $relations, $sortBy ?? 'created_at', $desc ?? true);
0 ignored issues
show
Bug introduced by
It seems like $relations defined by parameter $relations on line 37 can also be of type string; however, App\Modules\Core\BaseCla...eRepository::paginate() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Documentation introduced by
$desc ?? true is of type boolean, but the function expects a integer.

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...
50
    }
51
52
    /**
53
     * Fetch all records with relations from the storage.
54
     *
55
     * @param  array   $relations
56
     * @param  string  $sortBy
57
     * @param  boolean $desc
58
     * @param  array   $columns
59
     * @return collection
60
     */
61
    public function all($relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
62
    {
63
        $sort = $desc ? 'desc' : 'asc';
64
        return $this->model->with($relations)->orderBy($sortBy, $sort)->get($columns);
65
    }
66
67
    /**
68
     * Fetch all records with relations from storage in pages
69
     * that matche the given query.
70
     *
71
     * @param  string  $query
72
     * @param  integer $perPage
73
     * @param  array   $relations
74
     * @param  string  $sortBy
75
     * @param  boolean $desc
76
     * @param  array   $columns
77
     * @return collection
78
     */
79
    public function search($query, $perPage = 15, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
80
    {
81
        $model            = $this->model->with($relations);
82
        $conditionColumns = $this->model->searchable;
83
        $sort             = $desc ? 'desc' : 'asc';
84
85
        /**
86
         * Construct the select conditions for the model.
87
         */
88
        $model->where(function ($q) use ($query, $conditionColumns, $relations) {
89
90 View Code Duplication
            if (count($conditionColumns)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
91
                $column = 'LOWER('.array_shift($conditionColumns).')';
92
                if (Str::contains($column, '->')) {
93
                    $column = $this->wrapJsonSelector($column);
94
                }
95
96
                /**
97
                 * Use the first element in the model columns to construct the first condition.
98
                 */
99
                $q->where(\DB::raw($column), 'LIKE', '%'.strtolower($query).'%');
100
            }
101
102
            /**
103
             * Loop through the rest of the columns to construct or where conditions.
104
             */
105 View Code Duplication
            foreach ($conditionColumns as $column) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
106
                $column = 'LOWER('.$column.')';
107
                if (Str::contains($column, '->')) {
108
                    $column = $this->wrapJsonSelector($column);
109
                }
110
111
                $q->orWhere(\DB::raw($column), 'LIKE', '%'.strtolower($query).'%');
112
            }
113
114
            /**
115
             * Loop through the model relations.
116
             */
117
            foreach ($relations as $relation) {
118
                /**
119
                 * Remove the sub relation if exists.
120
                 */
121
                $relation = explode('.', $relation)[0];
122
123
                /**
124
                 * Try to fetch the relation repository from the core.
125
                 */
126
                if (\Core::$relation()) {
127
                    /**
128
                     * Construct the relation condition.
129
                     */
130
                    $q->orWhereHas($relation, function ($subModel) use ($query, $relation) {
131
132
                        $subModel->where(function ($q) use ($query, $relation) {
133
134
                            /**
135
                             * Get columns of the relation.
136
                             */
137
                            $subConditionColumns = \Core::$relation()->model->searchable;
138
139 View Code Duplication
                            if (count($subConditionColumns)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
140
                                $column = 'LOWER('.array_shift($subConditionColumns).')';
141
                                if (Str::contains($column, '->')) {
142
                                    $column = $this->wrapJsonSelector($column);
143
                                }
144
145
                                /**
146
                                 * Use the first element in the relation model columns to construct the first condition.
147
                                 */
148
                                $q->where(\DB::raw($column), 'LIKE', '%'.strtolower($query).'%');
149
                            }
150
151
                            /**
152
                             * Loop through the rest of the columns to construct or where conditions.
153
                             */
154 View Code Duplication
                            foreach ($subConditionColumns as $subConditionColumn) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
155
                                $column = 'LOWER('.$subConditionColumn.')';
156
                                if (Str::contains($column, '->')) {
157
                                    $column = $this->wrapJsonSelector($column);
158
                                }
159
                                
160
                                $q->orWhere(\DB::raw($column), 'LIKE', '%'.strtolower($query).'%');
161
                            }
162
                        });
163
                    });
164
                }
165
            }
166
        });
167
        
168
        return $model->orderBy($sortBy, $sort)->paginate($perPage, $columns);
169
    }
170
    
171
    /**
172
     * Fetch all records with relations from storage in pages.
173
     *
174
     * @param  integer $perPage
175
     * @param  array   $relations
176
     * @param  string  $sortBy
177
     * @param  boolean $desc
178
     * @param  array   $columns
179
     * @return collection
180
     */
181
    public function paginate($perPage = 15, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
182
    {
183
        $sort = $desc ? 'desc' : 'asc';
184
        return $this->model->with($relations)->orderBy($sortBy, $sort)->paginate($perPage, $columns);
185
    }
186
187
    /**
188
     * Fetch all records with relations based on
189
     * the given condition from storage in pages.
190
     *
191
     * @param  array   $conditions array of conditions
192
     * @param  integer $perPage
193
     * @param  array   $relations
194
     * @param  string  $sortBy
195
     * @param  boolean $desc
196
     * @param  array   $columns
197
     * @return collection
198
     */
199
    public function paginateBy($conditions, $perPage = 15, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
200
    {
201
        $conditions = $this->constructConditions($conditions, $this->model);
202
        $sort       = $desc ? 'desc' : 'asc';
203
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->orderBy($sortBy, $sort)->paginate($perPage, $columns);
204
    }
205
    
206
    /**
207
     * Save the given model to the storage.
208
     *
209
     * @param  array $data
210
     * @return mixed
211
     */
212
    public function save(array $data)
213
    {
214
        \Session::put('locale', 'all');
215
        $model      = false;
216
        $relations  = [];
217
218
        \DB::transaction(function () use (&$model, $relations, $data) {
219
            
220
            $model     = $this->prepareModel($data);
221
            $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...
222
            $model     = $this->saveModel($model, $relations);
223
        });
224
            
225
        return $model;
226
    }
227
228
    /**
229
     * Delete record from the storage based on the given
230
     * condition.
231
     *
232
     * @param  var $value condition value
233
     * @param  string $attribute condition column name
234
     * @return void
235
     */
236
    public function delete($value, $attribute = 'id')
237
    {
238
        if ($attribute == 'id') {
239 View Code Duplication
            \DB::transaction(function () use ($value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
240
                $model = $this->model->lockForUpdate()->find($value);
241
                if (! $model) {
242
                    \ErrorHandler::notFound(class_basename($this->model).' with id : '.$value);
243
                }
244
                
245
                $model->delete();
246
            });
247
        } else {
248
            \DB::transaction(function () use ($value, $attribute) {
249
                $this->model->where($attribute, '=', $value)->lockForUpdate()->get()->each(function ($model) {
250
                    $model->delete();
251
                });
252
            });
253
        }
254
    }
255
    
256
    /**
257
     * Fetch records from the storage based on the given
258
     * id.
259
     *
260
     * @param  integer $id
261
     * @param  string[]   $relations
262
     * @param  array   $columns
263
     * @return object
264
     */
265
    public function find($id, $relations = [], $columns = ['*'])
266
    {
267
        return $this->model->with($relations)->find($id, $columns);
268
    }
269
    
270
    /**
271
     * Fetch records from the storage based on the given
272
     * condition.
273
     *
274
     * @param  array   $conditions array of conditions
275
     * @param  array   $relations
276
     * @param  string  $sortBy
277
     * @param  boolean $desc
278
     * @param  array   $columns
279
     * @return collection
280
     */
281
    public function findBy($conditions, $relations = [], $sortBy = 'created_at', $desc = 1, $columns = ['*'])
282
    {
283
        $conditions = $this->constructConditions($conditions, $this->model);
284
        $sort       = $desc ? 'desc' : 'asc';
285
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->orderBy($sortBy, $sort)->get($columns);
286
    }
287
288
    /**
289
     * Fetch the first record from the storage based on the given
290
     * condition.
291
     *
292
     * @param  array   $conditions array of conditions
293
     * @param  array   $relations
294
     * @param  array   $columns
295
     * @return object
296
     */
297
    public function first($conditions, $relations = [], $columns = ['*'])
298
    {
299
        $conditions = $this->constructConditions($conditions, $this->model);
300
        return $this->model->with($relations)->whereRaw($conditions['conditionString'], $conditions['conditionValues'])->first($columns);
301
    }
302
303
    /**
304
     * Return the deleted models in pages based on the given conditions.
305
     *
306
     * @param  array   $conditions array of conditions
307
     * @param  integer $perPage
308
     * @param  string  $sortBy
309
     * @param  boolean $desc
310
     * @param  array   $columns
311
     * @return collection
312
     */
313
    public function deleted($conditions, $perPage = 15, $sortBy = 'created_at', $desc = 1, $columns = ['*'])
314
    {
315
        unset($conditions['page']);
316
        unset($conditions['perPage']);
317
        unset($conditions['sortBy']);
318
        unset($conditions['sort']);
319
        $conditions = $this->constructConditions($conditions, $this->model);
320
        $sort       = $desc ? 'desc' : 'asc';
321
        $model      = $this->model->onlyTrashed();
322
323
        if (count($conditions['conditionValues'])) {
324
            $model->whereRaw($conditions['conditionString'], $conditions['conditionValues']);
325
        }
326
327
        return $model->orderBy($sortBy, $sort)->paginate($perPage, $columns);
328
    }
329
330
    /**
331
     * Restore the deleted model.
332
     *
333
     * @param  integer $id
334
     * @return void
335
     */
336 View Code Duplication
    public function restore($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
337
    {
338
        $model = $this->model->onlyTrashed()->find($id);
339
340
        if (! $model) {
341
            \ErrorHandler::notFound(class_basename($this->model).' with id : '.$id);
342
        }
343
344
        $model->restore();
345
    }
346
347
    /**
348
     * Fill the model with the given data.
349
     *
350
     * @param   array  $data
351
     *
352
     * @return  object
353
     */
354
    public function prepareModel($data)
355
    {
356
        $modelClass = $this->model;
357
358
        /**
359
         * If the id is present in the data then select the model for updating,
360
         * else create new model.
361
         * @var array
362
         */
363
        $model = Arr::has($data, 'id') ? $modelClass->lockForUpdate()->find($data['id']) : new $modelClass;
364
        if (! $model) {
365
            \ErrorHandler::notFound(class_basename($modelClass).' with id : '.$data['id']);
366
        }
367
368
        /**
369
         * Construct the model object with the given data,
370
         * and if there is a relation add it to relations array,
371
         * then save the model.
372
         */
373
        foreach ($data as $key => $value) {
374
            if (array_search($key, $model->getFillable(), true) !== false) {
375
                /**
376
                 * If the attribute isn't a relation and prevent attributes not in the fillable.
377
                 */
378
                $model->$key = $value;
379
            }
380
        }
381
382
        return $model;
383
    }
384
    
385
    /**
386
     * Prepare related models based on the given data for the given model.
387
     *
388
     * @param   array  $data
389
     * @param   object $model
390
     *
391
     * @return  array
392
     */
393
    public function prepareRelations($data, $model)
394
    {
395
        /**
396
         * Construct the model object with the given data,
397
         * and if there is a relation add it to relations array,
398
         * then save the model.
399
         */
400
        foreach ($data as $key => $value) {
401
            /**
402
             * If the attribute is a relation.
403
             */
404
            $relation = \Str::camel($key);
405
            if (method_exists($model, $relation) && \Core::$relation()) {
406
                /**
407
                 * Check if the relation is a collection.
408
                 */
409
                if (class_basename($model->$relation) == 'Collection') {
410
                    /**
411
                     * If the relation has no value then marke the relation data
412
                     * related to the model to be deleted.
413
                     */
414
                    if (! $value || ! count($value)) {
415
                        $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...
416
                    }
417
                }
418
                if (is_array($value)) {
419
                    /**
420
                     * Loop through the relation data.
421
                     */
422
                    foreach ($value as $attr => $val) {
423
                        /**
424
                         * Get the relation model.
425
                         */
426
                        $relationBaseModel = \Core::$relation()->model;
427
428
                        /**
429
                         * Check if the relation is a collection.
430
                         */
431
                        if (class_basename($model->$relation) == 'Collection') {
432
                            /**
433
                             * If the id is present in the data then select the relation model for updating,
434
                             * else create new model.
435
                             */
436
                            $relationModel = Arr::has($val, 'id') ? $relationBaseModel->lockForUpdate()->find($val['id']) : new $relationBaseModel;
437
438
                            /**
439
                             * If model doesn't exists.
440
                             */
441
                            if (! $relationModel) {
442
                                \ErrorHandler::notFound(class_basename($relationBaseModel).' with id : '.$val['id']);
443
                            }
444
445
                            /**
446
                             * Loop through the relation attributes.
447
                             */
448
                            foreach ($val as $attr => $val) {
449
                                /**
450
                                 * Prevent the sub relations or attributes not in the fillable.
451
                                 */
452
                                if (gettype($val) !== 'object' && gettype($val) !== 'array' && array_search($attr, $relationModel->getFillable(), true) !== false) {
453
                                    $relationModel->$attr = $val;
454
                                }
455
                            }
456
457
                            $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...
458
                        } else {
459
                            /**
460
                             * Prevent the sub relations.
461
                             */
462
                            if (gettype($val) !== 'object' && gettype($val) !== 'array') {
463
                                /**
464
                                 * If the id is present in the data then select the relation model for updating,
465
                                 * else create new model.
466
                                 */
467
                                $relationModel = Arr::has($value, 'id') ? $relationBaseModel->lockForUpdate()->find($value['id']) : new $relationBaseModel;
468
469
                                /**
470
                                 * If model doesn't exists.
471
                                 */
472
                                if (! $relationModel) {
473
                                    \ErrorHandler::notFound(class_basename($relationBaseModel).' with id : '.$value['id']);
474
                                }
475
476
                                foreach ($value as $relationAttribute => $relationValue) {
477
                                    /**
478
                                     * Prevent attributes not in the fillable.
479
                                     */
480
                                    if (array_search($relationAttribute, $relationModel->getFillable(), true) !== false) {
481
                                        $relationModel->$relationAttribute = $relationValue;
482
                                    }
483
                                }
484
485
                                $relations[$relation] = $relationModel;
486
                            }
487
                        }
488
                    }
489
                }
490
            }
491
        }
492
493
        return $relations;
494
    }
495
496
    /**
497
     * Save the model with related models.
498
     *
499
     * @param   object  $model
500
     * @param   array   $relations
501
     *
502
     * @return  object
503
     */
504
    public function saveModel($model, $relations)
505
    {
506
507
        /**
508
         * Loop through the relations array.
509
         */
510
        foreach ($relations as $key => $value) {
511
            /**
512
             * If the relation is marked for delete then delete it.
513
             */
514
            if ($value == 'delete' && $model->$key()->count()) {
515
                $model->$key()->delete();
516
            } elseif (gettype($value) == 'array') {
517
                /**
518
                 * Save the model.
519
                 */
520
                $model->save();
521
                $ids = [];
522
523
                /**
524
                 * Loop through the relations.
525
                 */
526
                foreach ($value as $val) {
527
                    switch (class_basename($model->$key())) {
528
                        /**
529
                         * If the relation is one to many then update it's foreign key with
530
                         * the model id and save it then add its id to ids array to delete all
531
                         * relations who's id isn't in the ids array.
532
                         */
533
                        case 'HasMany':
534
                            $foreignKeyName       = $model->$key()->getForeignKeyName();
535
                            $val->$foreignKeyName = $model->id;
536
                            $val->save();
537
                            $ids[] = $val->id;
538
                            break;
539
540
                        /**
541
                         * If the relation is many to many then add it's id to the ids array to
542
                         * attache these ids to the model.
543
                         */
544
                        case 'BelongsToMany':
545
                            $val->save();
546
                            $ids[] = $val->id;
547
                            break;
548
                    }
549
                }
550
                switch (class_basename($model->$key())) {
551
                    /**
552
                     * If the relation is one to many then delete all
553
                     * relations who's id isn't in the ids array.
554
                     */
555
                    case 'HasMany':
556
                        $model->$key()->whereNotIn('id', $ids)->delete();
557
                        break;
558
559
                    /**
560
                     * If the relation is many to many then
561
                     * detach the previous data and attach
562
                     * the ids array to the model.
563
                     */
564
                    case 'BelongsToMany':
565
                        $model->$key()->detach();
566
                        $model->$key()->attach($ids);
567
                        break;
568
                }
569
            } else {
570
                switch (class_basename($model->$key())) {
571
                    /**
572
                     * If the relation is one to one.
573
                     */
574
                    case 'HasOne':
575
                        /**
576
                         * Save the model.
577
                         */
578
                        $model->save();
579
                        $foreignKeyName         = $model->$key()->getForeignKeyName();
580
                        $value->$foreignKeyName = $model->id;
581
                        $value->save();
582
                        break;
583
                    case 'BelongsTo':
584
                        /**
585
                         * Save the model.
586
                         */
587
                        $value->save();
588
                        $model->$key()->associate($value);
589
                        break;
590
                }
591
            }
592
        }
593
594
        /**
595
         * Save the model.
596
         */
597
        $model->save();
598
599
        return $model;
600
    }
601
602
    /**
603
     * Build the conditions recursively for the retrieving methods.
604
     * @param  array $conditions
605
     * @return array
606
     */
607
    protected function constructConditions($conditions, $model)
608
    {
609
        $conditionString = '';
610
        $conditionValues = [];
611
        foreach ($conditions as $key => $value) {
612
            if (Str::contains($key, '->')) {
613
                $key = $this->wrapJsonSelector($key);
614
            }
615
616
            if ($key == 'and') {
617
                $conditions       = $this->constructConditions($value, $model);
618
                $conditionString .= str_replace('{op}', 'and', $conditions['conditionString']).' {op} ';
619
                $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
620
            } elseif ($key == 'or') {
621
                $conditions       = $this->constructConditions($value, $model);
622
                $conditionString .= str_replace('{op}', 'or', $conditions['conditionString']).' {op} ';
623
                $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
624
            } else {
625
                if (is_array($value)) {
626
                    $operator = $value['op'];
627
                    if (strtolower($operator) == 'between') {
628
                        $value1 = $value['val1'];
629
                        $value2 = $value['val2'];
630
                    } else {
631
                        $value = Arr::get($value, 'val', '');
632
                    }
633
                } else {
634
                    $operator = '=';
635
                }
636
                
637
                if (strtolower($operator) == 'between') {
638
                    $conditionString  .= $key.' >= ? and ';
639
                    $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...
640
641
                    $conditionString  .= $key.' <= ? {op} ';
642
                    $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...
643
                } elseif (strtolower($operator) == 'in') {
644
                    $conditionValues  = array_merge($conditionValues, $value);
645
                    $inBindingsString = rtrim(str_repeat('?,', count($value)), ',');
646
                    $conditionString .= $key.' in ('.rtrim($inBindingsString, ',').') {op} ';
647
                } elseif (strtolower($operator) == 'null') {
648
                    $conditionString .= $key.' is null {op} ';
649
                } elseif (strtolower($operator) == 'not null') {
650
                    $conditionString .= $key.' is not null {op} ';
651
                } elseif (strtolower($operator) == 'has') {
652
                    $sql              = $model->withTrashed()->has($key)->toSql();
653
                    $conditions       = $this->constructConditions($value, $model->$key()->getRelated());
654
                    $conditionString .= rtrim(substr($sql, strpos($sql, 'exists')), ')').' and '.$conditions['conditionString'].') {op} ';
655
                    $conditionValues  = array_merge($conditionValues, $conditions['conditionValues']);
656
                } else {
657
                    $conditionString  .= $key.' '.$operator.' ? {op} ';
658
                    $conditionValues[] = $value;
659
                }
660
            }
661
        }
662
        $conditionString = '('.rtrim($conditionString, '{op} ').')';
663
        return ['conditionString' => $conditionString, 'conditionValues' => $conditionValues];
664
    }
665
666
    /**
667
     * Wrap the given JSON selector.
668
     *
669
     * @param  string  $value
670
     * @return string
671
     */
672
    protected function wrapJsonSelector($value)
673
    {
674
        $removeLast = strpos($value, ')');
675
        $value      = $removeLast === false ? $value : substr($value, 0, $removeLast);
676
        $path       = explode('->', $value);
677
        $field      = array_shift($path);
678
        $result     = sprintf('%s->\'$.%s\'', $field, collect($path)->map(function ($part) {
679
            return '"'.$part.'"';
680
        })->implode('.'));
681
        
682
        return $removeLast === false ? $result : $result.')';
683
    }
684
}
685