BaseRepository::withCount()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Salah3id\Domains\Repository\Eloquent;
4
5
use Closure;
6
use Exception;
7
use Illuminate\Container\Container as Application;
8
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
9
use Illuminate\Database\Eloquent\Builder;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Support\Collection;
12
use Salah3id\Domains\Repository\Contracts\CriteriaInterface;
13
use Salah3id\Domains\Repository\Contracts\Presentable;
14
use Salah3id\Domains\Repository\Contracts\PresenterInterface;
15
use Salah3id\Domains\Repository\Contracts\RepositoryCriteriaInterface;
16
use Salah3id\Domains\Repository\Contracts\RepositoryInterface;
17
use Salah3id\Domains\Repository\Events\RepositoryEntityCreated;
18
use Salah3id\Domains\Repository\Events\RepositoryEntityCreating;
19
use Salah3id\Domains\Repository\Events\RepositoryEntityDeleted;
20
use Salah3id\Domains\Repository\Events\RepositoryEntityDeleting;
21
use Salah3id\Domains\Repository\Events\RepositoryEntityUpdated;
22
use Salah3id\Domains\Repository\Events\RepositoryEntityUpdating;
23
use Salah3id\Domains\Repository\Exceptions\RepositoryException;
24
use Salah3id\Domains\Repository\Traits\ComparesVersionsTrait;
25
use Salah3id\Domains\Validator\Contracts\ValidatorInterface;
26
use Salah3id\Domains\Validator\Exceptions\ValidatorException;
27
28
/**
29
 * Class BaseRepository
30
 *
31
 * @package Salah3id\Domains\Repository\Eloquent
32
 * @author  Anderson Andrade <[email protected]>
33
 */
34
abstract class BaseRepository implements RepositoryInterface, RepositoryCriteriaInterface
35
{
36
    use ComparesVersionsTrait;
37
38
    /**
39
     * @var Application
40
     */
41
    protected $app;
42
43
    /**
44
     * @var Model
45
     */
46
    protected $model;
47
48
    /**
49
     * @var array
50
     */
51
    protected $fieldSearchable = [];
52
53
    /**
54
     * @var PresenterInterface
55
     */
56
    protected $presenter;
57
58
    /**
59
     * @var ValidatorInterface
60
     */
61
    protected $validator;
62
63
    /**
64
     * Validation Rules
65
     *
66
     * @var array
67
     */
68
    protected $rules = null;
69
70
    /**
71
     * Collection of Criteria
72
     *
73
     * @var Collection
74
     */
75
    protected $criteria;
76
77
    /**
78
     * @var bool
79
     */
80
    protected $skipCriteria = false;
81
82
    /**
83
     * @var bool
84
     */
85
    protected $skipPresenter = false;
86
87
    /**
88
     * @var \Closure
89
     */
90
    protected $scopeQuery = null;
91
92
    /**
93
     * @param Application $app
94
     */
95
    public function __construct(Application $app)
96
    {
97
        $this->app = $app;
98
        $this->criteria = new Collection();
99
        $this->makeModel();
100
        $this->makePresenter();
101
        $this->makeValidator();
102
        $this->boot();
103
    }
104
105
    /**
106
     *
107
     */
108
    public function boot()
109
    {
110
        //
111
    }
112
113
    /**
114
     * Returns the current Model instance
115
     *
116
     * @return Model
117
     */
118
    public function getModel()
119
    {
120
        return $this->model;
121
    }
122
123
    /**
124
     * @throws RepositoryException
125
     */
126
    public function resetModel()
127
    {
128
        $this->makeModel();
129
    }
130
131
    /**
132
     * Specify Model class name
133
     *
134
     * @return string
135
     */
136
    abstract public function model();
137
138
    /**
139
     * Specify Presenter class name
140
     *
141
     * @return string
142
     */
143
    public function presenter()
144
    {
145
        return null;
146
    }
147
148
    /**
149
     * Specify Validator class name of Salah3id\Domains\Validator\Contracts\ValidatorInterface
150
     *
151
     * @return null
152
     * @throws Exception
153
     */
154
    public function validator()
155
    {
156
        if (isset($this->rules) && !is_null($this->rules) && is_array($this->rules) && !empty($this->rules)) {
157
            if (class_exists('Salah3id\Domains\Validator\LaravelValidator')) {
158
                $validator = app('Salah3id\Domains\Validator\LaravelValidator');
159
                if ($validator instanceof ValidatorInterface) {
0 ignored issues
show
introduced by
$validator is always a sub-type of Salah3id\Domains\Validat...acts\ValidatorInterface.
Loading history...
160
                    $validator->setRules($this->rules);
161
162
                    return $validator;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $validator returns the type Salah3id\Domains\Validator\LaravelValidator which is incompatible with the documented return type null.
Loading history...
163
                }
164
            } else {
165
                throw new Exception(trans('repository::packages.prettus_laravel_validation_required'));
166
            }
167
        }
168
169
        return null;
170
    }
171
172
    /**
173
     * Set Presenter
174
     *
175
     * @param $presenter
176
     *
177
     * @return $this
178
     */
179
    public function setPresenter($presenter)
180
    {
181
        $this->makePresenter($presenter);
182
183
        return $this;
184
    }
185
186
    /**
187
     * @return Model
188
     * @throws RepositoryException
189
     */
190
    public function makeModel()
191
    {
192
        $model = $this->app->make($this->model());
193
194
        if (!$model instanceof Model) {
195
            throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model");
196
        }
197
198
        return $this->model = $model;
199
    }
200
201
    /**
202
     * @param null $presenter
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $presenter is correct as it would always require null to be passed?
Loading history...
203
     *
204
     * @return PresenterInterface
205
     * @throws RepositoryException
206
     */
207
    public function makePresenter($presenter = null)
208
    {
209
        $presenter = !is_null($presenter) ? $presenter : $this->presenter();
0 ignored issues
show
introduced by
The condition is_null($presenter) is always true.
Loading history...
210
211
        if (!is_null($presenter)) {
0 ignored issues
show
introduced by
The condition is_null($presenter) is always false.
Loading history...
212
            $this->presenter = is_string($presenter) ? $this->app->make($presenter) : $presenter;
0 ignored issues
show
introduced by
The condition is_string($presenter) is always true.
Loading history...
213
214
            if (!$this->presenter instanceof PresenterInterface) {
215
                throw new RepositoryException("Class {$presenter} must be an instance of Salah3id\Domains\\Repository\\Contracts\\PresenterInterface");
216
            }
217
218
            return $this->presenter;
219
        }
220
221
        return null;
222
    }
223
224
    /**
225
     * @param null $validator
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $validator is correct as it would always require null to be passed?
Loading history...
226
     *
227
     * @return null|ValidatorInterface
228
     * @throws RepositoryException
229
     */
230
    public function makeValidator($validator = null)
231
    {
232
        $validator = !is_null($validator) ? $validator : $this->validator();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->validator() targeting Salah3id\Domains\Reposit...Repository::validator() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
introduced by
The condition is_null($validator) is always true.
Loading history...
233
234
        if (!is_null($validator)) {
0 ignored issues
show
introduced by
The condition is_null($validator) is always true.
Loading history...
235
            $this->validator = is_string($validator) ? $this->app->make($validator) : $validator;
236
237
            if (!$this->validator instanceof ValidatorInterface) {
238
                throw new RepositoryException("Class {$validator} must be an instance of Salah3id\Domains\\Validator\\Contracts\\ValidatorInterface");
239
            }
240
241
            return $this->validator;
242
        }
243
244
        return null;
245
    }
246
247
    /**
248
     * Get Searchable Fields
249
     *
250
     * @return array
251
     */
252
    public function getFieldsSearchable()
253
    {
254
        return $this->fieldSearchable;
255
    }
256
257
    /**
258
     * Query Scope
259
     *
260
     * @param \Closure $scope
261
     *
262
     * @return $this
263
     */
264
    public function scopeQuery(\Closure $scope)
265
    {
266
        $this->scopeQuery = $scope;
267
268
        return $this;
269
    }
270
271
    /**
272
     * Retrieve data array for populate field select
273
     *
274
     * @param string      $column
275
     * @param string|null $key
276
     *
277
     * @return \Illuminate\Support\Collection|array
278
     */
279
    public function lists($column, $key = null)
280
    {
281
        $this->applyCriteria();
282
283
        return $this->model->lists($column, $key);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->model->lists($column, $key) also could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the documented return type Illuminate\Support\Collection|array.
Loading history...
284
    }
285
286
    /**
287
     * Retrieve data array for populate field select
288
     * Compatible with Laravel 5.3
289
     *
290
     * @param string      $column
291
     * @param string|null $key
292
     *
293
     * @return \Illuminate\Support\Collection|array
294
     */
295
    public function pluck($column, $key = null)
296
    {
297
        $this->applyCriteria();
298
299
        return $this->model->pluck($column, $key);
300
    }
301
302
    /**
303
     * Sync relations
304
     *
305
     * @param      $id
306
     * @param      $relation
307
     * @param      $attributes
308
     * @param bool $detaching
309
     *
310
     * @return mixed
311
     */
312
    public function sync($id, $relation, $attributes, $detaching = true)
313
    {
314
        return $this->find($id)->{$relation}()->sync($attributes, $detaching);
315
    }
316
317
    /**
318
     * SyncWithoutDetaching
319
     *
320
     * @param $id
321
     * @param $relation
322
     * @param $attributes
323
     *
324
     * @return mixed
325
     */
326
    public function syncWithoutDetaching($id, $relation, $attributes)
327
    {
328
        return $this->sync($id, $relation, $attributes, false);
329
    }
330
331
    /**
332
     * Retrieve all data of repository
333
     *
334
     * @param array $columns
335
     *
336
     * @return mixed
337
     */
338
    public function all($columns = ['*'])
339
    {
340
        $this->applyCriteria();
341
        $this->applyScope();
342
343
        if ($this->model instanceof Builder) {
0 ignored issues
show
introduced by
$this->model is never a sub-type of Illuminate\Database\Eloquent\Builder.
Loading history...
344
            $results = $this->model->get($columns);
345
        } else {
346
            $results = $this->model->all($columns);
347
        }
348
349
        $this->resetModel();
350
        $this->resetScope();
351
352
        return $this->parserResult($results);
353
    }
354
355
    /**
356
     * Count results of repository
357
     *
358
     * @param array  $where
359
     * @param string $columns
360
     *
361
     * @return int
362
     */
363
    public function count(array $where = [], $columns = '*')
364
    {
365
        $this->applyCriteria();
366
        $this->applyScope();
367
368
        if ($where) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $where of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
369
            $this->applyConditions($where);
370
        }
371
372
        $result = $this->model->count($columns);
373
374
        $this->resetModel();
375
        $this->resetScope();
376
377
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the documented return type integer.
Loading history...
378
    }
379
380
    /**
381
     * Alias of All method
382
     *
383
     * @param array $columns
384
     *
385
     * @return mixed
386
     */
387
    public function get($columns = ['*'])
388
    {
389
        return $this->all($columns);
390
    }
391
392
    /**
393
     * Retrieve first data of repository
394
     *
395
     * @param array $columns
396
     *
397
     * @return mixed
398
     */
399
    public function first($columns = ['*'])
400
    {
401
        $this->applyCriteria();
402
        $this->applyScope();
403
404
        $results = $this->model->first($columns);
405
406
        $this->resetModel();
407
408
        return $this->parserResult($results);
409
    }
410
411
    /**
412
     * Retrieve first data of repository, or return new Entity
413
     *
414
     * @param array $attributes
415
     *
416
     * @return mixed
417
     */
418
    public function firstOrNew(array $attributes = [])
419
    {
420
        $this->applyCriteria();
421
        $this->applyScope();
422
423
        $temporarySkipPresenter = $this->skipPresenter;
424
        $this->skipPresenter(true);
425
426
        $model = $this->model->firstOrNew($attributes);
427
        $this->skipPresenter($temporarySkipPresenter);
428
429
        $this->resetModel();
430
431
        return $this->parserResult($model);
432
    }
433
434
    /**
435
     * Retrieve first data of repository, or create new Entity
436
     *
437
     * @param array $attributes
438
     *
439
     * @return mixed
440
     */
441
    public function firstOrCreate(array $attributes = [])
442
    {
443
        $this->applyCriteria();
444
        $this->applyScope();
445
446
        $temporarySkipPresenter = $this->skipPresenter;
447
        $this->skipPresenter(true);
448
449
        $model = $this->model->firstOrCreate($attributes);
450
        $this->skipPresenter($temporarySkipPresenter);
451
452
        $this->resetModel();
453
454
        return $this->parserResult($model);
455
    }
456
457
    /**
458
     * Retrieve data of repository with limit applied
459
     *
460
     * @param int   $limit
461
     * @param array $columns
462
     *
463
     * @return mixed
464
     */
465
    public function limit($limit, $columns = ['*'])
466
    {
467
        // Shortcut to all with `limit` applied on query via `take`
468
        $this->take($limit);
469
470
        return $this->all($columns);
471
    }
472
473
    /**
474
     * Retrieve all data of repository, paginated
475
     *
476
     * @param null|int $limit
477
     * @param array    $columns
478
     * @param string   $method
479
     *
480
     * @return mixed
481
     */
482
    public function paginate($limit = null, $columns = ['*'], $method = "paginate")
483
    {
484
        $this->applyCriteria();
485
        $this->applyScope();
486
        $limit = is_null($limit) ? config('domains.pagination.limit', 15) : $limit;
487
        $results = $this->model->{$method}($limit, $columns);
488
        $results->appends(app('request')->query());
489
        $this->resetModel();
490
491
        return $this->parserResult($results);
492
    }
493
494
    /**
495
     * Retrieve all data of repository, simple paginated
496
     *
497
     * @param null|int $limit
498
     * @param array    $columns
499
     *
500
     * @return mixed
501
     */
502
    public function simplePaginate($limit = null, $columns = ['*'])
503
    {
504
        return $this->paginate($limit, $columns, "simplePaginate");
505
    }
506
507
    /**
508
     * Find data by id
509
     *
510
     * @param       $id
511
     * @param array $columns
512
     *
513
     * @return mixed
514
     */
515
    public function find($id, $columns = ['*'])
516
    {
517
        $this->applyCriteria();
518
        $this->applyScope();
519
        $model = $this->model->findOrFail($id, $columns);
520
        $this->resetModel();
521
522
        return $this->parserResult($model);
523
    }
524
525
    /**
526
     * Find data by field and value
527
     *
528
     * @param       $field
529
     * @param       $value
530
     * @param array $columns
531
     *
532
     * @return mixed
533
     */
534
    public function findByField($field, $value = null, $columns = ['*'])
535
    {
536
        $this->applyCriteria();
537
        $this->applyScope();
538
        $model = $this->model->where($field, '=', $value)->get($columns);
539
        $this->resetModel();
540
541
        return $this->parserResult($model);
542
    }
543
544
    /**
545
     * Find data by multiple fields
546
     *
547
     * @param array $where
548
     * @param array $columns
549
     *
550
     * @return mixed
551
     */
552
    public function findWhere(array $where, $columns = ['*'])
553
    {
554
        $this->applyCriteria();
555
        $this->applyScope();
556
557
        $this->applyConditions($where);
558
559
        $model = $this->model->get($columns);
560
        $this->resetModel();
561
562
        return $this->parserResult($model);
563
    }
564
565
    /**
566
     * Find data by multiple values in one field
567
     *
568
     * @param       $field
569
     * @param array $values
570
     * @param array $columns
571
     *
572
     * @return mixed
573
     */
574
    public function findWhereIn($field, array $values, $columns = ['*'])
575
    {
576
        $this->applyCriteria();
577
        $this->applyScope();
578
        $model = $this->model->whereIn($field, $values)->get($columns);
579
        $this->resetModel();
580
581
        return $this->parserResult($model);
582
    }
583
584
    /**
585
     * Find data by excluding multiple values in one field
586
     *
587
     * @param       $field
588
     * @param array $values
589
     * @param array $columns
590
     *
591
     * @return mixed
592
     */
593
    public function findWhereNotIn($field, array $values, $columns = ['*'])
594
    {
595
        $this->applyCriteria();
596
        $this->applyScope();
597
        $model = $this->model->whereNotIn($field, $values)->get($columns);
598
        $this->resetModel();
599
600
        return $this->parserResult($model);
601
    }
602
603
    /**
604
     * Find data by between values in one field
605
     *
606
     * @param       $field
607
     * @param array $values
608
     * @param array $columns
609
     *
610
     * @return mixed
611
     */
612
    public function findWhereBetween($field, array $values, $columns = ['*'])
613
    {
614
        $this->applyCriteria();
615
        $this->applyScope();
616
        $model = $this->model->whereBetween($field, $values)->get($columns);
617
        $this->resetModel();
618
619
        return $this->parserResult($model);
620
    }
621
622
    /**
623
     * Save a new entity in repository
624
     *
625
     * @param array $attributes
626
     *
627
     * @return mixed
628
     * @throws ValidatorException
629
     *
630
     */
631
    public function create(array $attributes)
632
    {
633
        if (!is_null($this->validator)) {
634
            // we should pass data that has been casts by the model
635
            // to make sure data type are same because validator may need to use
636
            // this data to compare with data that fetch from database.
637
            if ($this->versionCompare($this->app->version(), "5.2.*", ">")) {
0 ignored issues
show
introduced by
The method version() does not exist on Illuminate\Container\Container. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

637
            if ($this->versionCompare($this->app->/** @scrutinizer ignore-call */ version(), "5.2.*", ">")) {
Loading history...
638
                $attributes = $this->model->newInstance()->forceFill($attributes)->makeVisible($this->model->getHidden())->toArray();
639
            } else {
640
                $model = $this->model->newInstance()->forceFill($attributes);
641
                $model->makeVisible($this->model->getHidden());
642
                $attributes = $model->toArray();
643
            }
644
645
            $this->validator->with($attributes)->passesOrFail(ValidatorInterface::RULE_CREATE);
646
        }
647
648
        event(new RepositoryEntityCreating($this, $attributes));
649
650
        $model = $this->model->newInstance($attributes);
651
        $model->save();
652
        $this->resetModel();
653
654
        event(new RepositoryEntityCreated($this, $model));
655
656
        return $this->parserResult($model);
657
    }
658
659
    /**
660
     * Update a entity in repository by id
661
     *
662
     * @param array $attributes
663
     * @param       $id
664
     *
665
     * @return mixed
666
     * @throws ValidatorException
667
     *
668
     */
669
    public function update(array $attributes, $id)
670
    {
671
        $this->applyScope();
672
673
        if (!is_null($this->validator)) {
674
            // we should pass data that has been casts by the model
675
            // to make sure data type are same because validator may need to use
676
            // this data to compare with data that fetch from database.
677
            $model = $this->model->newInstance();
678
            $model->setRawAttributes([]);
679
            $model->setAppends([]);
680
            if ($this->versionCompare($this->app->version(), "5.2.*", ">")) {
681
                $attributes = $model->forceFill($attributes)->makeVisible($this->model->getHidden())->toArray();
682
            } else {
683
                $model->forceFill($attributes);
684
                $model->makeVisible($this->model->getHidden());
685
                $attributes = $model->toArray();
686
            }
687
688
            $this->validator->with($attributes)->setId($id)->passesOrFail(ValidatorInterface::RULE_UPDATE);
689
        }
690
691
        $temporarySkipPresenter = $this->skipPresenter;
692
693
        $this->skipPresenter(true);
694
695
        $model = $this->model->findOrFail($id);
696
697
        event(new RepositoryEntityUpdating($this, $model));
698
699
        $model->fill($attributes);
700
        $model->save();
701
702
        $this->skipPresenter($temporarySkipPresenter);
703
        $this->resetModel();
704
705
        event(new RepositoryEntityUpdated($this, $model));
706
707
        return $this->parserResult($model);
708
    }
709
710
    /**
711
     * Update or Create an entity in repository
712
     *
713
     * @param array $attributes
714
     * @param array $values
715
     *
716
     * @return mixed
717
     * @throws ValidatorException
718
     *
719
     */
720
    public function updateOrCreate(array $attributes, array $values = [])
721
    {
722
        $this->applyScope();
723
724
        if (!is_null($this->validator)) {
725
            $this->validator->with(array_merge($attributes, $values))->passesOrFail(ValidatorInterface::RULE_CREATE);
726
        }
727
728
        $temporarySkipPresenter = $this->skipPresenter;
729
730
        $this->skipPresenter(true);
731
732
        event(new RepositoryEntityCreating($this, $attributes));
733
734
        $model = $this->model->updateOrCreate($attributes, $values);
735
736
        $this->skipPresenter($temporarySkipPresenter);
737
        $this->resetModel();
738
739
        event(new RepositoryEntityUpdated($this, $model));
740
741
        return $this->parserResult($model);
742
    }
743
744
    /**
745
     * Delete a entity in repository by id
746
     *
747
     * @param $id
748
     *
749
     * @return int
750
     */
751
    public function delete($id)
752
    {
753
        $this->applyScope();
754
755
        $temporarySkipPresenter = $this->skipPresenter;
756
        $this->skipPresenter(true);
757
758
        $model = $this->find($id);
759
        $originalModel = clone $model;
760
761
        $this->skipPresenter($temporarySkipPresenter);
762
        $this->resetModel();
763
764
        event(new RepositoryEntityDeleting($this, $model));
765
766
        $deleted = $model->delete();
767
768
        event(new RepositoryEntityDeleted($this, $originalModel));
769
770
        return $deleted;
771
    }
772
773
    /**
774
     * Delete multiple entities by given criteria.
775
     *
776
     * @param array $where
777
     *
778
     * @return int
779
     */
780
    public function deleteWhere(array $where)
781
    {
782
        $this->applyScope();
783
784
        $temporarySkipPresenter = $this->skipPresenter;
785
        $this->skipPresenter(true);
786
787
        $this->applyConditions($where);
788
789
        event(new RepositoryEntityDeleting($this, $this->model->getModel()));
790
791
        $deleted = $this->model->delete();
792
793
        event(new RepositoryEntityDeleted($this, $this->model->getModel()));
794
795
        $this->skipPresenter($temporarySkipPresenter);
796
        $this->resetModel();
797
798
        return $deleted;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $deleted also could return the type boolean which is incompatible with the documented return type integer.
Loading history...
799
    }
800
801
    /**
802
     * Check if entity has relation
803
     *
804
     * @param string $relation
805
     *
806
     * @return $this
807
     */
808
    public function has($relation)
809
    {
810
        $this->model = $this->model->has($relation);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->model->has($relation) can also be of type Illuminate\Database\Eloquent\Builder or Illuminate\Database\Eloquent\Builder. However, the property $model is declared as type Illuminate\Database\Eloquent\Model. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
811
812
        return $this;
813
    }
814
815
    /**
816
     * Load relations
817
     *
818
     * @param array|string $relations
819
     *
820
     * @return $this
821
     */
822
    public function with($relations)
823
    {
824
        $this->model = $this->model->with($relations);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->model->with($relations) of type Illuminate\Database\Eloquent\Builder is incompatible with the declared type Illuminate\Database\Eloquent\Model of property $model.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
825
826
        return $this;
827
    }
828
829
    /**
830
     * Add subselect queries to count the relations.
831
     *
832
     * @param mixed $relations
833
     *
834
     * @return $this
835
     */
836
    public function withCount($relations)
837
    {
838
        $this->model = $this->model->withCount($relations);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->model->withCount($relations) can also be of type Illuminate\Database\Eloquent\Builder. However, the property $model is declared as type Illuminate\Database\Eloquent\Model. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
839
        return $this;
840
    }
841
842
    /**
843
     * Load relation with closure
844
     *
845
     * @param string  $relation
846
     * @param closure $closure
847
     *
848
     * @return $this
849
     */
850
    public function whereHas($relation, $closure)
851
    {
852
        $this->model = $this->model->whereHas($relation, $closure);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->model->whereHas($relation, $closure) can also be of type Illuminate\Database\Eloquent\Builder or Illuminate\Database\Eloquent\Builder. However, the property $model is declared as type Illuminate\Database\Eloquent\Model. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
853
854
        return $this;
855
    }
856
857
    /**
858
     * Set hidden fields
859
     *
860
     * @param array $fields
861
     *
862
     * @return $this
863
     */
864
    public function hidden(array $fields)
865
    {
866
        $this->model->setHidden($fields);
867
868
        return $this;
869
    }
870
871
    /**
872
     * Set the "orderBy" value of the query.
873
     *
874
     * @param mixed  $column
875
     * @param string $direction
876
     *
877
     * @return $this
878
     */
879
    public function orderBy($column, $direction = 'asc')
880
    {
881
        $this->model = $this->model->orderBy($column, $direction);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->model->orderBy($column, $direction) can also be of type Illuminate\Database\Eloquent\Builder or Illuminate\Database\Query\Builder. However, the property $model is declared as type Illuminate\Database\Eloquent\Model. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
882
883
        return $this;
884
    }
885
886
    /**
887
     * Set the "limit" value of the query.
888
     *
889
     * @param int $limit
890
     *
891
     * @return $this
892
     */
893
    public function take($limit)
894
    {
895
        // Internally `take` is an alias to `limit`
896
        $this->model = $this->model->limit($limit);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->model->limit($limit) can also be of type Illuminate\Database\Eloquent\Builder or Illuminate\Database\Query\Builder. However, the property $model is declared as type Illuminate\Database\Eloquent\Model. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
897
898
        return $this;
899
    }
900
901
    /**
902
     * Set visible fields
903
     *
904
     * @param array $fields
905
     *
906
     * @return $this
907
     */
908
    public function visible(array $fields)
909
    {
910
        $this->model->setVisible($fields);
911
912
        return $this;
913
    }
914
915
    /**
916
     * Push Criteria for filter the query
917
     *
918
     * @param $criteria
919
     *
920
     * @return $this
921
     * @throws \Salah3id\Domains\Repository\Exceptions\RepositoryException
922
     */
923
    public function pushCriteria($criteria)
924
    {
925
        if (is_string($criteria)) {
926
            $criteria = new $criteria;
927
        }
928
        if (!$criteria instanceof CriteriaInterface) {
929
            throw new RepositoryException("Class " . get_class($criteria) . " must be an instance of Salah3id\Domains\\Repository\\Contracts\\CriteriaInterface");
930
        }
931
        $this->criteria->push($criteria);
932
933
        return $this;
934
    }
935
936
    /**
937
     * Pop Criteria
938
     *
939
     * @param $criteria
940
     *
941
     * @return $this
942
     */
943
    public function popCriteria($criteria)
944
    {
945
        $this->criteria = $this->criteria->reject(function ($item) use ($criteria) {
946
            if (is_object($item) && is_string($criteria)) {
947
                return get_class($item) === $criteria;
948
            }
949
950
            if (is_string($item) && is_object($criteria)) {
951
                return $item === get_class($criteria);
952
            }
953
954
            return get_class($item) === get_class($criteria);
955
        });
956
957
        return $this;
958
    }
959
960
    /**
961
     * Get Collection of Criteria
962
     *
963
     * @return Collection
964
     */
965
    public function getCriteria()
966
    {
967
        return $this->criteria;
968
    }
969
970
    /**
971
     * Find data by Criteria
972
     *
973
     * @param CriteriaInterface $criteria
974
     *
975
     * @return mixed
976
     */
977
    public function getByCriteria(CriteriaInterface $criteria)
978
    {
979
        $this->model = $criteria->apply($this->model, $this);
980
        $results = $this->model->get();
981
        $this->resetModel();
982
983
        return $this->parserResult($results);
984
    }
985
986
    /**
987
     * Skip Criteria
988
     *
989
     * @param bool $status
990
     *
991
     * @return $this
992
     */
993
    public function skipCriteria($status = true)
994
    {
995
        $this->skipCriteria = $status;
996
997
        return $this;
998
    }
999
1000
    /**
1001
     * Reset all Criterias
1002
     *
1003
     * @return $this
1004
     */
1005
    public function resetCriteria()
1006
    {
1007
        $this->criteria = new Collection();
1008
1009
        return $this;
1010
    }
1011
1012
    /**
1013
     * Reset Query Scope
1014
     *
1015
     * @return $this
1016
     */
1017
    public function resetScope()
1018
    {
1019
        $this->scopeQuery = null;
1020
1021
        return $this;
1022
    }
1023
1024
    /**
1025
     * Apply scope in current Query
1026
     *
1027
     * @return $this
1028
     */
1029
    protected function applyScope()
1030
    {
1031
        if (isset($this->scopeQuery) && is_callable($this->scopeQuery)) {
1032
            $callback = $this->scopeQuery;
1033
            $this->model = $callback($this->model);
1034
        }
1035
1036
        return $this;
1037
    }
1038
1039
    /**
1040
     * Apply criteria in current Query
1041
     *
1042
     * @return $this
1043
     */
1044
    protected function applyCriteria()
1045
    {
1046
        if ($this->skipCriteria === true) {
1047
            return $this;
1048
        }
1049
1050
        $criteria = $this->getCriteria();
1051
1052
        if ($criteria) {
0 ignored issues
show
introduced by
$criteria is of type Illuminate\Support\Collection, thus it always evaluated to true.
Loading history...
1053
            foreach ($criteria as $c) {
1054
                if ($c instanceof CriteriaInterface) {
1055
                    $this->model = $c->apply($this->model, $this);
1056
                }
1057
            }
1058
        }
1059
1060
        return $this;
1061
    }
1062
1063
    /**
1064
     * Applies the given where conditions to the model.
1065
     *
1066
     * @param array $where
1067
     *
1068
     * @return void
1069
     */
1070
    protected function applyConditions(array $where)
1071
    {
1072
        foreach ($where as $field => $value) {
1073
            if (is_array($value)) {
1074
                list($field, $condition, $val) = $value;
1075
                //smooth input
1076
                $condition = preg_replace('/\s\s+/', ' ', trim($condition));
1077
1078
                //split to get operator, syntax: "DATE >", "DATE =", "DAY <"
1079
                $operator = explode(' ', $condition);
1080
                if (count($operator) > 1) {
1081
                    $condition = $operator[0];
1082
                    $operator = $operator[1];
1083
                } else $operator = null;
1084
                switch (strtoupper($condition)) {
1085
                    case 'IN':
1086
                        if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
1087
                        $this->model = $this->model->whereIn($field, $val);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->model->whereIn($field, $val) can also be of type Illuminate\Database\Eloquent\Builder or Illuminate\Database\Query\Builder. However, the property $model is declared as type Illuminate\Database\Eloquent\Model. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1088
                        break;
1089
                    case 'NOTIN':
1090
                        if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
1091
                        $this->model = $this->model->whereNotIn($field, $val);
1092
                        break;
1093
                    case 'DATE':
1094
                        if (!$operator) $operator = '=';
1095
                        $this->model = $this->model->whereDate($field, $operator, $val);
1096
                        break;
1097
                    case 'DAY':
1098
                        if (!$operator) $operator = '=';
1099
                        $this->model = $this->model->whereDay($field, $operator, $val);
1100
                        break;
1101
                    case 'MONTH':
1102
                        if (!$operator) $operator = '=';
1103
                        $this->model = $this->model->whereMonth($field, $operator, $val);
1104
                        break;
1105
                    case 'YEAR':
1106
                        if (!$operator) $operator = '=';
1107
                        $this->model = $this->model->whereYear($field, $operator, $val);
1108
                        break;
1109
                    case 'EXISTS':
1110
                        if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
1111
                        $this->model = $this->model->whereExists($val);
1112
                        break;
1113
                    case 'HAS':
1114
                        if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
1115
                        $this->model = $this->model->whereHas($field, $val);
1116
                        break;
1117
                    case 'HASMORPH':
1118
                        if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
1119
                        $this->model = $this->model->whereHasMorph($field, $val);
1120
                        break;
1121
                    case 'DOESNTHAVE':
1122
                        if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
1123
                        $this->model = $this->model->whereDoesntHave($field, $val);
1124
                        break;
1125
                    case 'DOESNTHAVEMORPH':
1126
                        if (!($val instanceof Closure)) throw new RepositoryException("Input {$val} must be closure function");
1127
                        $this->model = $this->model->whereDoesntHaveMorph($field, $val);
1128
                        break;
1129
                    case 'BETWEEN':
1130
                        if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
1131
                        $this->model = $this->model->whereBetween($field, $val);
1132
                        break;
1133
                    case 'BETWEENCOLUMNS':
1134
                        if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
1135
                        $this->model = $this->model->whereBetweenColumns($field, $val);
1136
                        break;
1137
                    case 'NOTBETWEEN':
1138
                        if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
1139
                        $this->model = $this->model->whereNotBetween($field, $val);
1140
                        break;
1141
                    case 'NOTBETWEENCOLUMNS':
1142
                        if (!is_array($val)) throw new RepositoryException("Input {$val} mus be an array");
1143
                        $this->model = $this->model->whereNotBetweenColumns($field, $val);
1144
                        break;
1145
                    case 'RAW':
1146
                        $this->model = $this->model->whereRaw($val);
1147
                        break;
1148
                    default:
1149
                        $this->model = $this->model->where($field, $condition, $val);
1150
                }
1151
            } else {
1152
                $this->model = $this->model->where($field, '=', $value);
1153
            }
1154
        }
1155
    }
1156
1157
    /**
1158
     * Skip Presenter Wrapper
1159
     *
1160
     * @param bool $status
1161
     *
1162
     * @return $this
1163
     */
1164
    public function skipPresenter($status = true)
1165
    {
1166
        $this->skipPresenter = $status;
1167
1168
        return $this;
1169
    }
1170
1171
    /**
1172
     * Wrapper result data
1173
     *
1174
     * @param mixed $result
1175
     *
1176
     * @return mixed
1177
     */
1178
    public function parserResult($result)
1179
    {
1180
        if ($this->presenter instanceof PresenterInterface) {
0 ignored issues
show
introduced by
$this->presenter is always a sub-type of Salah3id\Domains\Reposit...acts\PresenterInterface.
Loading history...
1181
            if ($result instanceof Collection || $result instanceof LengthAwarePaginator) {
1182
                $result->each(function ($model) {
1183
                    if ($model instanceof Presentable) {
1184
                        $model->setPresenter($this->presenter);
1185
                    }
1186
1187
                    return $model;
1188
                });
1189
            } else if ($result instanceof Presentable) {
1190
                $result = $result->setPresenter($this->presenter);
1191
            }
1192
1193
            if (!$this->skipPresenter) {
1194
                return $this->presenter->present($result);
1195
            }
1196
        }
1197
1198
        return $result;
1199
    }
1200
1201
    /**
1202
     * Trigger static method calls to the model
1203
     *
1204
     * @param $method
1205
     * @param $arguments
1206
     *
1207
     * @return mixed
1208
     */
1209
    public static function __callStatic($method, $arguments)
1210
    {
1211
        return call_user_func_array([new static(), $method], $arguments);
0 ignored issues
show
Bug introduced by
The call to Salah3id\Domains\Reposit...pository::__construct() has too few arguments starting with app. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1211
        return call_user_func_array([/** @scrutinizer ignore-call */ new static(), $method], $arguments);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1212
    }
1213
1214
    /**
1215
     * Trigger method calls to the model
1216
     *
1217
     * @param string $method
1218
     * @param array  $arguments
1219
     *
1220
     * @return mixed
1221
     */
1222
    public function __call($method, $arguments)
1223
    {
1224
        $this->applyCriteria();
1225
        $this->applyScope();
1226
1227
        return call_user_func_array([$this->model, $method], $arguments);
1228
    }
1229
}
1230