Passed
Push — master ( 157485...b7615a )
by Nikolaos
09:23
created

Select::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 21
nc 2
nop 2
dl 0
loc 33
rs 9.584
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by AtlasPHP
12
 *
13
 * @link    https://github.com/atlasphp/Atlas.Query
14
 * @license https://github.com/atlasphp/Atlas.Qyert/blob/1.x/LICENSE.md
15
 */
16
17
declare(strict_types=1);
18
19
namespace Phalcon\DataMapper\Query;
20
21
use BadMethodCallException;
22
use PDO;
23
use Phalcon\Helper\Arr;
24
25
use function array_merge;
26
use function array_shift;
27
use function call_user_func_array;
28
use function func_get_args;
29
use function implode;
30
use function ltrim;
31
use function strtoupper;
32
use function substr;
33
use function trim;
34
35
/**
36
 * Class Select
37
 *
38
 * @property string $asAlias
39
 * @property bool   $forUpdate
40
 *
41
 * @method int    fetchAffected()
42
 * @method array  fetchAll()
43
 * @method array  fetchAssoc()
44
 * @method array  fetchColumn(int $column = 0)
45
 * @method array  fetchGroup(int $flags = PDO::FETCH_ASSOC)
46
 * @method object fetchObject(string $class = 'stdClass', array $arguments = [])
47
 * @method array  fetchObjects(string $class = 'stdClass', array $arguments = [])
48
 * @method array  fetchOne()
49
 * @method array  fetchPairs()
50
 * @method mixed  fetchValue()
51
 */
52
class Select extends AbstractConditions
53
{
54
    public const JOIN_INNER   = "INNER";
55
    public const JOIN_LEFT    = "LEFT";
56
    public const JOIN_NATURAL = "NATURAL";
57
    public const JOIN_RIGHT   = "RIGHT";
58
59
    /**
60
     * @var string
61
     */
62
    protected $asAlias = "";
63
64
    /**
65
     * @var bool
66
     */
67
    protected $forUpdate = false;
68
69
    /**
70
     * Proxied methods to the connection
71
     *
72
     * @param string $method
73
     * @param array  $params
74
     *
75
     * @return mixed
76
     */
77
    public function __call(string $method, array $params)
78
    {
79
        $proxied = [
80
            "fetchAffected" => true,
81
            "fetchAll"      => true,
82
            "fetchAssoc"    => true,
83
            "fetchCol"      => true,
84
            "fetchGroup"    => true,
85
            "fetchObject"   => true,
86
            "fetchObjects"  => true,
87
            "fetchOne"      => true,
88
            "fetchPairs"    => true,
89
            "fetchValue"    => true
90
        ];
91
92
        if (isset($proxied[$method])) {
93
            return call_user_func_array(
94
                [
95
                    $this->connection,
96
                    $method
97
                ],
98
                array_merge(
99
                    [
100
                        $this->getStatement(),
101
                        $this->getBindValues()
102
                    ],
103
                    $params
104
                )
105
            );
106
        }
107
108
        throw new BadMethodCallException(
109
            "Unknown method: [" . $method . "]"
110
        );
111
    }
112
113
    /**
114
     * Sets a `AND` for a `HAVING` condition
115
     *
116
     * @param string     $condition
117
     * @param mixed|null $value
118
     * @param int        $type
119
     *
120
     * @return Select
121
     */
122
    public function andHaving(
123
        string $condition,
124
        $value = null,
125
        int $type = -1
126
    ): Select {
127
        $this->having($condition, $value, $type);
128
129
        return $this;
130
    }
131
132
    /**
133
     * The `AS` statement for the query - useful in sub-queries
134
     *
135
     * @param string $asAlias
136
     *
137
     * @return Select
138
     */
139
    public function asAlias(string $asAlias): Select
140
    {
141
        $this->asAlias = $asAlias;
142
143
        return $this;
144
    }
145
146
    /**
147
     * Concatenates to the most recent `HAVING` clause
148
     *
149
     * @param string     $condition
150
     * @param mixed|null $value
151
     * @param int        $type
152
     *
153
     * @return Select
154
     */
155
    public function appendHaving(
156
        string $condition,
157
        $value = null,
158
        int $type = -1
159
    ): Select {
160
        $this->appendCondition("HAVING", $condition, $value, $type);
161
162
        return $this;
163
    }
164
165
    /**
166
     * Concatenates to the most recent `JOIN` clause
167
     *
168
     * @param string     $condition
169
     * @param mixed|null $value
170
     * @param int        $type
171
     *
172
     * @return Select
173
     */
174
    public function appendJoin(
175
        string $condition,
176
        $value = null,
177
        int $type = -1
178
    ): Select {
179
        if (!empty($value)) {
180
            $condition .= $this->bind->bindInline($value, $type);
181
        }
182
183
        $end = Arr::lastKey($this->store["FROM"]);
184
        $key = Arr::lastKey($this->store["FROM"][$end]);
185
186
        $this->store["FROM"][$end][$key] = $this->store["FROM"][$end][$key]
187
            . $condition;
188
189
        return $this;
190
    }
191
192
    /**
193
     * The columns to select from. If a key is set in an array element, the
194
     * key will be used as the alias
195
     *
196
     * @param string ...$column
197
     *
198
     * @return Select
199
     */
200
    public function columns(): Select
201
    {
202
        $this->store["COLUMNS"] = array_merge(
203
            $this->store["COLUMNS"],
204
            func_get_args()
205
        );
206
207
        return $this;
208
    }
209
210
    /**
211
     * @param bool $enable
212
     *
213
     * @return Select
214
     */
215
    public function distinct(bool $enable = true): Select
216
    {
217
        $this->setFlag("DISTINCT", $enable);
218
219
        return $this;
220
    }
221
222
    /**
223
     * Adds table(s) in the query
224
     *
225
     * @param string $table
226
     *
227
     * @return Select
228
     */
229
    public function from(string $table): Select
230
    {
231
        $this->store["FROM"][] = [$table];
232
233
        return $this;
234
    }
235
236
    /**
237
     * Enable the `FOR UPDATE` for the query
238
     *
239
     * @param bool $enable
240
     *
241
     * @return Select
242
     */
243
    public function forUpdate(bool $enable = true): Select
244
    {
245
        $this->forUpdate = $enable;
246
247
        return $this;
248
    }
249
250
    /**
251
     * Returns the compiled SQL statement
252
     *
253
     * @return string
254
     */
255
    public function getStatement(): string
256
    {
257
        return implode("", $this->store["UNION"])
258
            . $this->getCurrentStatement();
259
    }
260
261
    /**
262
     * Sets the `GROUP BY`
263
     *
264
     * @param array|string $groupBy
265
     *
266
     * @return Select
267
     */
268
    public function groupBy($groupBy): Select
269
    {
270
        $this->processValue("GROUP", $groupBy);
271
272
        return $this;
273
    }
274
275
    /**
276
     * Whether the query has columns or not
277
     *
278
     * @return bool
279
     */
280
    public function hasColumns(): bool
281
    {
282
        return count($this->store["COLUMNS"]) > 0;
283
    }
284
285
    /**
286
     * Sets a `HAVING` condition
287
     *
288
     * @param string     $condition
289
     * @param mixed|null $value
290
     * @param int        $type
291
     *
292
     * @return Select
293
     */
294
    public function having(
295
        string $condition,
296
        $value = null,
297
        int $type = -1
298
    ): Select {
299
        $this->addCondition("HAVING", "AND ", $condition, $value, $type);
300
301
        return $this;
302
    }
303
304
    /**
305
     * Sets a 'JOIN' condition
306
     *
307
     * @param string     $join
308
     * @param string     $table
309
     * @param string     $condition
310
     * @param mixed|null $value
311
     * @param int        $type
312
     *
313
     * @return Select
314
     */
315
    public function join(
316
        string $join,
317
        string $table,
318
        string $condition,
319
        $value = null,
320
        int $type = -1
321
    ): Select {
322
        $join = strtoupper(trim($join));
323
        if (substr($join, -4) !== "JOIN") {
324
            $join .= " JOIN";
325
        }
326
327
        $condition = ltrim($condition);
328
329
        if (
330
            "" !== $condition
331
            && strtoupper(substr($condition, 0, 3)) !== "ON "
332
            && strtoupper(substr($condition, 0, 6)) !== "USING "
333
        ) {
334
            $condition = "ON " . $condition;
335
        }
336
337
        if (!empty($value)) {
338
            $condition .= $this->bind->bindInline($value, $type);
339
        }
340
341
        $key = Arr::lastKey($this->store["FROM"]);
342
343
        $this->store["FROM"][$key][] = $join . " " . $table . " " . $condition;
344
345
        return $this;
346
    }
347
348
    /**
349
     * Sets a `OR` for a `HAVING` condition
350
     *
351
     * @param string     $condition
352
     * @param mixed|null $value
353
     * @param int        $type
354
     *
355
     * @return Select
356
     */
357
    public function orHaving(
358
        string $condition,
359
        $value = null,
360
        int $type = -1
361
    ): Select {
362
        $this->addCondition("HAVING", "OR ", $condition, $value, $type);
363
364
        return $this;
365
    }
366
367
    /**
368
     * Resets the internal collections
369
     *
370
     * @return Select
371
     */
372
    public function reset(): Select
373
    {
374
        parent::reset();
375
376
        $this->asAlias   = "";
377
        $this->forUpdate = false;
378
379
        return $this;
380
    }
381
382
    /**
383
     * Start a sub-select
384
     *
385
     * @return Select
386
     */
387
    public function subSelect(): Select
388
    {
389
        return new Select($this->connection, $this->bind);
390
    }
391
392
    /**
393
     * Start a `UNION`
394
     *
395
     * @return Select
396
     */
397
    public function union(): Select
398
    {
399
        $this->store["UNION"][] = $this->getCurrentStatement(" UNION ");
400
401
        $this->reset();
402
403
        return $this;
404
    }
405
406
    /**
407
     * Start a `UNION ALL`
408
     *
409
     * @return Select
410
     */
411
    public function unionAll(): Select
412
    {
413
        $this->store["UNION"][] = $this->getCurrentStatement(" UNION ALL ");
414
415
        $this->reset();
416
417
        return $this;
418
    }
419
420
    /**
421
     * Statement builder
422
     *
423
     * @param string $suffix
424
     *
425
     * @return string
426
     */
427
    protected function getCurrentStatement(string $suffix = ""): string
428
    {
429
        $forUpdate = "";
430
431
        if ($this->forUpdate) {
432
            $forUpdate = " FOR UPDATE";
433
        }
434
435
        $statement = "SELECT"
436
            . $this->buildFlags()
437
            . $this->buildLimitEarly()
438
            . $this->buildColumns()
439
            . $this->buildFrom()
440
            . $this->buildCondition("WHERE")
441
            . $this->buildBy("GROUP")
442
            . $this->buildCondition("HAVING")
443
            . $this->buildBy("ORDER")
444
            . $this->buildLimit()
445
            . $forUpdate;
446
447
        if ("" !== $this->asAlias) {
448
            $statement = "(" . $statement . ") AS " . $this->asAlias;
449
        }
450
451
        return $statement . $suffix;
452
    }
453
454
    /**
455
     * Builds the columns list
456
     *
457
     * @return string
458
     */
459
    private function buildColumns(): string
460
    {
461
        if (!$this->hasColumns()) {
462
            $columns = ["*"];
463
        } else {
464
            $columns = $this->store["COLUMNS"];
465
        }
466
467
        return $this->indent($columns, ",");
468
    }
469
470
    /**
471
     * Builds the from list
472
     *
473
     * @return string
474
     */
475
    private function buildFrom(): string
476
    {
477
        $from = [];
478
479
        if (empty($this->store["FROM"])) {
480
            return "";
481
        }
482
483
        foreach ($this->store["FROM"] as $table) {
484
            $from[] = array_shift($table) . $this->indent($table);
485
        }
486
487
        return " FROM" . $this->indent($from, ",");
488
    }
489
}
490