Rule::build()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 19
ccs 8
cts 8
cp 1
rs 10
cc 3
nc 4
nop 2
crap 3
1
<?php
2
3
namespace App\Models;
4
5
use App\Factories\SmartPlaylistRuleParameterFactory;
6
use Illuminate\Database\Eloquent\Builder;
7
use InvalidArgumentException;
8
9
class Rule
10
{
11
    public const OPERATOR_IS = 'is';
12
    public const OPERATOR_IS_NOT = 'isNot';
13
    public const OPERATOR_CONTAINS = 'contains';
14
    public const OPERATOR_NOT_CONTAIN = 'notContain';
15
    public const OPERATOR_IS_BETWEEN = 'isBetween';
16
    public const OPERATOR_IS_GREATER_THAN = 'isGreaterThan';
17
    public const OPERATOR_IS_LESS_THAN = 'isLessThan';
18
    public const OPERATOR_BEGINS_WITH = 'beginsWith';
19
    public const OPERATOR_ENDS_WITH = 'endsWith';
20
    public const OPERATOR_IN_LAST = 'inLast';
21
    public const OPERATOR_NOT_IN_LAST = 'notInLast';
22
23
    public const VALID_OPERATORS = [
24
        self::OPERATOR_BEGINS_WITH,
25
        self::OPERATOR_CONTAINS,
26
        self::OPERATOR_ENDS_WITH,
27
        self::OPERATOR_IN_LAST,
28
        self::OPERATOR_IS,
29
        self::OPERATOR_IS_BETWEEN,
30
        self::OPERATOR_IS_GREATER_THAN,
31
        self::OPERATOR_IS_LESS_THAN,
32
        self::OPERATOR_IS_NOT,
33
        self::OPERATOR_NOT_CONTAIN,
34
        self::OPERATOR_NOT_IN_LAST,
35
    ];
36
37
    private $operator;
38
    private $value;
39
    private $model;
40
    private $parameterFactory;
41
42 13
    private function __construct(array $config)
43
    {
44 13
        $this->validateOperator($config['operator']);
45
46 13
        $this->value = $config['value'];
47 13
        $this->model = $config['model'];
48 13
        $this->operator = $config['operator'];
49
50 13
        $this->parameterFactory = new SmartPlaylistRuleParameterFactory();
51 13
    }
52
53 13
    public static function create(array $config): self
54
    {
55 13
        return new static($config);
56
    }
57
58 13
    public function build(Builder $query, ?string $model = null): Builder
59
    {
60 13
        if (!$model) {
61 13
            $model = $this->model;
62
        }
63
64 13
        $fragments = explode('.', $model, 2);
65
66 13
        if (count($fragments) === 1) {
67 13
            return $query->{$this->resolveLogic()}(
68 13
                ...$this->parameterFactory->createParameters($model, $this->operator, $this->value)
69
            );
70
        }
71
72
        // If the model is something like 'artist.name' or 'interactions.play_count', we have a subquery to deal with.
73
        // We handle such a case with a recursive call which, in theory, should work with an unlimited level of nesting,
74
        // though in practice we only have one level max.
75
        return $query->whereHas($fragments[0], function (Builder $subQuery) use ($fragments): Builder {
76 3
            return $this->build($subQuery, $fragments[1]);
77 3
        });
78
    }
79
80
    /**
81
     * Resolve the logic of a (sub)query base on the configured operator.
82
     * Basically, if the operator is "between," we use "whereBetween." Otherwise, it's "where." Simple.
83
     */
84 13
    private function resolveLogic(): string
85
    {
86 13
        return $this->operator === self::OPERATOR_IS_BETWEEN ? 'whereBetween' : 'where';
87
    }
88
89 13
    private function validateOperator(string $operator): void
90
    {
91 13
        if (!in_array($operator, self::VALID_OPERATORS, true)) {
92
            throw new InvalidArgumentException(
93
                sprintf(
94
                    '%s is not a valid value for operators. Valid values are: %s',
95
                    $operator,
96
                    implode(', ', self::VALID_OPERATORS)
97
                )
98
            );
99
        }
100 13
    }
101
}
102