Completed
Push — master ( ee6d65...c2027d )
by Woody
03:25
created

Conditions::hasStatementParam()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 1
crap 3
1
<?php
2
declare(strict_types=1);
3
4
namespace Latitude\QueryBuilder;
5
6
class Conditions implements Statement
7
{
8
    use Traits\CanUseDefaultIdentifier;
9
10
    /**
11
     * Create a new conditions instance.
12
     */
13 28
    public static function make(string $condition = null, ...$params): Conditions
14
    {
15 28
        $statment = new static();
16 28
        if ($condition) {
17 21
            $statment->with($condition, ...$params);
18
        }
19 28
        return $statment;
20
    }
21
22
    /**
23
     * Alias of andWith().
24
     */
25 27
    public function with(string $condition, ...$params): self
26
    {
27 27
        return $this->andWith($condition, ...$params);
28
    }
29
30
    /**
31
     * Add a condition that will be applied with a logical "AND".
32
     */
33 27
    public function andWith(string $condition, ...$params): self
34
    {
35 27
        return $this->addCondition('AND', $condition, $params);
36
    }
37
38
    /**
39
     * Add a condition that will be applied with a logical "OR".
40
     */
41 4
    public function orWith(string $condition, ...$params): self
42
    {
43 4
        return $this->addCondition('OR', $condition, $params);
44
    }
45
46
    /**
47
     * Alias for andGroup().
48
     */
49 3
    public function group(): Conditions
50
    {
51 3
        return $this->andGroup();
52
    }
53
54
    /**
55
     * Start a new grouping that will be applied with a logical "AND".
56
     *
57
     * Exit the group with end().
58
     */
59 3
    public function andGroup(): Conditions
60
    {
61 3
        return $this->addConditionGroup('AND');
62
    }
63
64
    /**
65
     * Start a new grouping that will be applied with a logical "OR".
66
     *
67
     * Exit the group with end().
68
     */
69 2
    public function orGroup(): Conditions
70
    {
71 2
        return $this->addConditionGroup('OR');
72
    }
73
74
    /**
75
     * Exit the current grouping and return the parent statement.
76
     *
77
     * If no parent exists, the current conditions will be returned.
78
     *
79
     * @return Conditions
80
     */
81 4
    public function end(): Conditions
82
    {
83 4
        return $this->parent ?: $this;
84
    }
85
86
    // Statement
87 26
    public function sql(Identifier $identifier = null): string
88
    {
89 26
        $identifier = $this->getDefaultIdentifier($identifier);
90 26
        $expression = \array_reduce($this->parts, $this->sqlReducer(), '');
91 26
        return $identifier->escapeExpression($expression);
92
    }
93
94
    // Statement
95 16
    public function params(): array
96
    {
97 16
        return \array_reduce($this->parts, $this->paramReducer(), []);
98
    }
99
100
    /**
101
     * @var array
102
     */
103
    protected $parts = [];
104
105
    /**
106
     * @var Conditions
107
     */
108
    protected $parent;
109
110 28
    protected function __construct(Conditions $parent = null)
111
    {
112 28
        $this->parent = $parent;
113 28
    }
114
115
    /**
116
     * Add a condition to the current conditions, expanding IN values.
117
     */
118 27
    protected function addCondition(string $type, string $condition, array $params): self
119
    {
120 27
        $this->parts[] = compact('type', 'condition', 'params');
121 27
        return $this;
122
    }
123
124
    /**
125
     * Add a condition group to the current conditions.
126
     */
127 4
    protected function addConditionGroup(string $type): Conditions
128
    {
129 4
        $condition = new static($this);
130 4
        $this->parts[] = compact('type', 'condition');
131 4
        return $condition;
132
    }
133
134
    /**
135
     * Get a function to reduce condition parts to a SQL string.
136
     */
137 26
    protected function sqlReducer(): callable
138
    {
139
        return function (string $sql, array $part): string {
140 26
            if ($this->isCondition($part['condition'])) {
141
                // (...)
142 3
                $statement = "({$part['condition']->sql()})";
143
            } else {
144
                // foo = ?
145 26
                $statement = $this->replaceStatementParams($part['condition'], $part['params']);
146
            }
147 26
            if ($sql) {
148
                // AND ...
149 6
                $statement = "{$part['type']} $statement";
150
            }
151 26
            return \trim($sql . ' ' . $statement);
152 26
        };
153
    }
154
155
156
    /**
157
     * Get a function to reduce parameters to a single list.
158
     */
159 16
    protected function paramReducer(): callable
160
    {
161
        return function (array $params, array $part): array {
162 16
            if ($this->isCondition($part['condition'])) {
163
                // Conditions have a parameter list already
164 2
                return \array_merge($params, $part['condition']->params());
165
            }
166
            // Otherwise convert the list to a list of lists for flattening
167 16
            return \array_merge($params, ...\array_map($this->paramLister(), $part['params']));
168 16
        };
169
    }
170
171
    /**
172
     * Convert all parameters to an array for flattening.
173
     */
174 16
    protected function paramLister(): callable
175
    {
176
        return function ($param): array {
177 15
            if ($this->isStatement($param)) {
178
                // Statements have a parameter list already
179 3
                return $param->params();
180
            }
181
            // Otherwise convert to a list
182 14
            return [$param];
183 16
        };
184
    }
185
186
    /**
187
     * Check if a condition is a sub-condition.
188
     */
189 27
    protected function isCondition($condition): bool
190
    {
191 27
        if (\is_object($condition) === false) {
192 27
            return false;
193
        }
194 3
        return $condition instanceof Conditions;
195
    }
196
197
    /**
198
     * Check if a parameter is a statement.
199
     */
200 15
    protected function isStatement($param): bool
201
    {
202 15
        if (\is_object($param) === false) {
203 14
            return false;
204
        }
205 3
        return $param instanceof Statement;
206
    }
207
208
    /**
209
     * Check if any parameter is a statement.
210
     */
211 26
    protected function hasStatementParam(array $params): bool
212
    {
213 26
        foreach ($params as $param) {
214 14
            if ($this->isStatement($param)) {
215 14
                return true;
216
            }
217
        }
218 24
        return false;
219
    }
220
221
    /**
222
     * Replacement statement parameters with SQL expression.
223
     */
224 26
    protected function replaceStatementParams(string $statement, array $params): string
225
    {
226 26
        if ($this->hasStatementParam($params) === false) {
227 24
            return $statement;
228
        }
229
        // Maintain an offset position, as preg_replace_callback() does not provide one
230 3
        $index = 0;
231 3
        return \preg_replace_callback('/\?/', function ($matches) use (&$index, $params) {
232
            try {
233 3
                if ($this->isStatement($params[$index])) {
234
                    // Replace any statement placeholder with the generated SQL
235 3
                    return $params[$index]->sql();
236
                } else {
237
                    // And leave all other placeholders intact
238 1
                    return $matches[0];
239
                }
240
            } finally {
241
                // This funky usage of finally allows us to increment the offset
242
                // after all other code in the block has been executed.
243 3
                $index++;
244
            }
245 3
        }, $statement);
246
    }
247
}
248