Issues (590)

src/Query/Query.php (2 issues)

1
<?php
2
3
namespace Bdf\Prime\Query;
4
5
use Bdf\Prime\Connection\ConnectionInterface;
6
use Bdf\Prime\Connection\Result\ResultSetInterface;
7
use Bdf\Prime\Exception\PrimeException;
8
use Bdf\Prime\Query\Compiler\CompilerInterface;
9
use Bdf\Prime\Query\Compiler\Preprocessor\DefaultPreprocessor;
10
use Bdf\Prime\Query\Compiler\Preprocessor\PreprocessorInterface;
11
use Bdf\Prime\Query\Compiler\QuoteCompilerInterface;
12
use Bdf\Prime\Query\Compiler\SqlCompiler;
13
use Bdf\Prime\Query\Contract\Compilable;
14
use Bdf\Prime\Query\Contract\Paginable;
15
use Bdf\Prime\Query\Contract\ReadOperation;
16
use Bdf\Prime\Query\Contract\WriteOperation;
17
use Bdf\Prime\Query\Expression\Raw;
18
use Bdf\Prime\Query\Extension\EntityJoinTrait;
19
use Bdf\Prime\Query\Extension\LimitableTrait;
20
use Bdf\Prime\Query\Extension\LockableTrait;
21
use Bdf\Prime\Query\Extension\OrderableTrait;
22
use Bdf\Prime\Query\Extension\PaginableTrait;
23
use Bdf\Prime\Query\Extension\SimpleJoinTrait;
24
use Doctrine\DBAL\Query\Expression\CompositeExpression;
25
use Stringable;
26
27
/**
28
 * Sql Query
29
 *
30
 * @package Bdf\Prime\Query
31
 *
32
 * @todo comment reset un statement (ex ecraser les orders). Prendre en compte le reset du compiler
33
 *
34
 * @template C as \Bdf\Prime\Connection\ConnectionInterface&\Doctrine\DBAL\Connection
35
 * @template R as object|array
36
 *
37
 * @extends AbstractQuery<C, R>
38
 * @implements SqlQueryInterface<C, R>
39
 * @implements Paginable<R>
40
 */
41
class Query extends AbstractQuery implements SqlQueryInterface, Paginable, Stringable
42
{
43
    use EntityJoinTrait;
44
    /** @use PaginableTrait<R> */
45
    use PaginableTrait;
46
    use LimitableTrait;
47
    use OrderableTrait;
48
    use SimpleJoinTrait;
49
    use LockableTrait;
50
51
    /**
52
     * Initializes a new <tt>Query</tt>.
53
     *
54
     * @param C $connection The DBAL Connection.
55
     * @param PreprocessorInterface|null $preprocessor
56
     */
57 980
    public function __construct(ConnectionInterface $connection, PreprocessorInterface $preprocessor = null)
58
    {
59 980
        parent::__construct($connection, $preprocessor ?: new DefaultPreprocessor());
60
61 980
        $this->statements = [
62 980
            'ignore'     => null,
63 980
            'replace'    => null,
64 980
            'values'     => [],
65 980
            'columns'    => [],
66 980
            'distinct'   => null,
67 980
            'tables'     => [],
68 980
            'joins'      => [],
69 980
            'where'      => [],
70 980
            'groups'     => [],
71 980
            'having'     => [],
72 980
            'orders'     => [],
73 980
            'limit'      => null,
74 980
            'offset'     => null,
75 980
            'aggregate'  => null,
76 980
            'lock'       => null,
77 980
        ];
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     *
83
     * @return CompilerInterface<Query>&QuoteCompilerInterface
84
     */
85 925
    public function compiler(): object
86
    {
87 925
        return parent::compiler();
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 823
    public function getBindings(): array
94
    {
95 823
        return $this->compiler()->getBindings($this);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function quote($value, int $type = null): string
102
    {
103
        return $this->connection->quote($value, $type);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function quoteIdentifier(string $column): string
110
    {
111
        return $this->compiler()->quoteIdentifier($this, $column);
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    #[WriteOperation]
118 27
    public function delete(): int
119
    {
120 27
        return $this->executeUpdate(self::TYPE_DELETE);
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    #[WriteOperation]
127 29
    public function update(array $data = [], array $types = []): int
128
    {
129 29
        if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
130 11
            $this->statements['values'] = [
131 11
                'data' => $data,
132 11
                'types' => $types,
133 11
            ];
134
        }
135
136 29
        return $this->executeUpdate(self::TYPE_UPDATE);
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142
    #[WriteOperation]
143 662
    public function insert(array $data = []): int
144
    {
145 662
        if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
146 662
            $this->statements['values'] = [
147 662
                'data' => $data,
148 662
            ];
149
        }
150
151 662
        return $this->executeUpdate(self::TYPE_INSERT);
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 1
    public function ignore(bool $flag = true)
158
    {
159 1
        $this->statements['ignore'] = $flag;
160
161 1
        return $this;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    #[WriteOperation]
168 2
    public function replace(array $values = []): int
169
    {
170 2
        $this->statements['replace'] = true;
171
172 2
        return $this->insert($values);
173
    }
174
175
    /**
176
     * Set values to insert or update
177
     *
178
     * @todo Remonter sur une interface ?
179
     *
180
     * <code>
181
     * // Perform a INSERT INTO ... SELECT ... query
182
     * $query
183
     *     ->from('users_bck')
184
     *     ->values($connection->builder()->from('users'))
185
     *     ->insert()
186
     * ;
187
     *
188
     * // Perform a REPLACE INTO ... SELECT ... with column mapping
189
     * $query
190
     *     ->from('users_bck')
191
     *     ->values(
192
     *         $connection->builder()
193
     *             ->from('users')
194
     *             ->select([
195
     *                 'backup_name' => 'name', // Map "name" column from select table to "backup_name" column to insert table
196
     *                 'id'                     // Use "id" column without mapping other than ORM data mapping
197
     *             ])
198
     *     )
199
     *     ->replace() // Replace can also be used
200
     * ;
201
     *
202
     * // Simple UPDATE query
203
     * $query->values(['foo' => 'bar'])->update();
204
     * </code>
205
     *
206
     * @param QueryInterface|array $data The values, in form [column] => [value], or the SELECT query
207
     * @param array $types The binding types Do not works with INSERT INTO SELECT query
208
     *
209
     * @return $this
210
     */
211 27
    public function values($data = [], array $types = [])
212
    {
213 27
        $this->statements['values'] = [
214 27
            'data' => $data,
215 27
            'type' => $types
216 27
        ];
217
218 27
        return $this;
219
    }
220
221
    /**
222
     * Executes this query as an update query
223
     *
224
     * @param Compilable::TYPE_* $type The query type
225
     *
226
     * @return int The number of updated rows
227
     *
228
     * @throws PrimeException
229
     */
230 674
    protected function executeUpdate(string $type): int
231
    {
232 674
        $this->setType($type);
233
234 674
        $result = $this->connection->execute($this);
235
236 674
        if ($result->hasWrite()) {
237 665
            $this->clearCacheOnWrite();
238
        }
239
240 674
        return $result->count();
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    #[ReadOperation]
247 739
    public function execute($columns = null): ResultSetInterface
248
    {
249 739
        if (!empty($columns)) {
250 54
            $this->select($columns);
251
        }
252
253 739
        $this->setType(self::TYPE_SELECT);
254
255 739
        return $this->executeCached();
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 10
    protected function cacheKey(): ?string
262
    {
263 10
        return sha1($this->toSql().'-'.serialize($this->getBindings()));
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    #[ReadOperation]
270 15
    public function paginationCount(?string $column = null): int
271
    {
272 15
        $statements = $this->statements;
273
274 15
        $this->compilerState->invalidate(['columns', 'orders']);
275
276 15
        $this->statements['orders'] = [];
277 15
        $this->statements['limit'] = null;
278 15
        $this->statements['offset'] = null;
279 15
        $this->statements['aggregate'] = ['pagination', $this->getPaginationColumns($column)];
280
281 15
        $count = (int)$this->execute()->current()['aggregate'];
282
283 15
        $this->compilerState->invalidate(['columns', 'orders']);
284 15
        $this->statements = $statements;
285
286 15
        return $count;
287
    }
288
289
    /**
290
     * Get the column to count for pagination
291
     * @todo Voir pour count sur PK quand une entité est liée ?
292
     * @todo array ?
293
     *
294
     * @param string|null $column
295
     *
296
     * @return string
297
     */
298 15
    protected function getPaginationColumns(?string $column): string
299
    {
300 15
        if (!empty($column)) {
301
            return $column;
302
        }
303
304
        // If distinct is on and no column are given, we use the current column
305 15
        if ($this->statements['distinct'] && !empty($this->statements['columns'])) {
306 3
            return $this->statements['columns'][0]['column'];
307
        }
308
309
        // If group by we use the columns of the group by
310 13
        if ($this->statements['groups']) {
311 2
            $this->statements['distinct'] = true;
312 2
            $column = $this->statements['groups'][0];
313 2
            $this->statements['groups'] = [];
314
315 2
            return $column;
316
        }
317
318 12
        return '*';
319
    }
320
321
    /**
322
     * {@inheritdoc}
323
     */
324
    #[ReadOperation]
325 563
    public function count(?string $column = null): int
326
    {
327 563
        return (int)$this->aggregate(__FUNCTION__, $column);
328
    }
329
330
    /**
331
     * {@inheritdoc}
332
     */
333
    #[ReadOperation]
334 2
    public function avg(?string $column = null): float
335
    {
336 2
        return (float)$this->aggregate(__FUNCTION__, $column);
337
    }
338
339
    /**
340
     * {@inheritdoc}
341
     */
342
    #[ReadOperation]
343 2
    public function min(?string $column = null)
344
    {
345 2
        return $this->aggregate(__FUNCTION__, $column);
346
    }
347
348
    /**
349
     * {@inheritdoc}
350
     */
351
    #[ReadOperation]
352 3
    public function max(?string $column = null)
353
    {
354 3
        return $this->aggregate(__FUNCTION__, $column);
355
    }
356
357
    /**
358
     * {@inheritdoc}
359
     */
360
    #[ReadOperation]
361 2
    public function sum(?string $column = null): float
362
    {
363 2
        return (float)$this->aggregate(__FUNCTION__, $column);
364
    }
365
366
    /**
367
     * {@inheritdoc}
368
     */
369
    #[ReadOperation]
370 572
    public function aggregate(string $function, ?string $column = null)
371
    {
372 572
        $statements = $this->statements;
373
374 572
        $this->compilerState->invalidate('columns');
375
376 572
        $this->statements['aggregate'] = [$function, $column ?: '*'];
377
378 572
        $aggregate = $this->execute()->current()['aggregate'];
379
380 572
        $this->compilerState->invalidate('columns');
381 572
        $this->statements = $statements;
382
383 572
        return $aggregate;
384
    }
385
386
    /**
387
     * {@inheritdoc}
388
     */
389 18
    public function distinct(bool $flag = true)
390
    {
391 18
        $this->compilerState->invalidate('columns');
392
393 18
        $this->statements['distinct'] = $flag;
394
395 18
        return $this;
396
    }
397
398
    /**
399
     * {@inheritdoc}
400
     *
401
     * @param string|Query $from The table name, or the embedded query
402
     */
403 946
    public function from($from, ?string $alias = null)
404
    {
405 946
        $this->compilerState->invalidate('from');
406
407 946
        $table = [
408 946
            'table' => $from,
409 946
            'alias' => $alias,
410 946
        ];
411 946
        $key = $alias ?: $from;
412
413 946
        if (is_string($key)) {
414 946
            $this->statements['tables'][$key] = $table;
415
        } else {
416
            $this->statements['tables'][] = $table;
417
        }
418
419 946
        return $this;
420
    }
421
422
    /**
423
     * Change a FROM alias for a table (or previously defined alias)
424
     *
425
     * Usage:
426
     * <code>
427
     * // Change alias of the current table : FROM my_table as my_alias
428
     * $query->from('my_table')->fromAlias('my_alias');
429
     *
430
     * // Change alias of the foo table : FROM foo as my_alias, bar
431
     * $query->from('foo')->from('bar')->fromAlias('my_alias', 'foo');
432
     *
433
     * // Redefine alias of foo : FROM foo as my_alias, bar
434
     * $query->from('foo', 'f')->from('bar')->fromAlias('my_alias', 'f');
435
     * </code>
436
     *
437
     * @param string $alias The new alias name
438
     * @param string|null $table The last alias / table name. If null, will define the last table
439
     *
440
     * @return $this
441
     */
442 3
    public function fromAlias(string $alias, ?string $table = null)
443
    {
444 3
        $this->compilerState->invalidate('from');
445
446 3
        $table = $table ?? key($this->statements['tables']);
447
448 3
        $this->statements['tables'][$alias] = $this->statements['tables'][$table];
449 3
        $this->statements['tables'][$alias]['alias'] = $alias;
450
451 3
        unset($this->statements['tables'][$table]);
452
453 3
        return $this;
454
    }
455
456
    /**
457
     * {@inheritdoc}
458
     */
459 5
    public function group(string ...$columns)
460
    {
461 5
        $this->compilerState->invalidate('groups');
462
463 5
        $this->statements['groups'] = $columns;
464
465 5
        return $this;
466
    }
467
468
    /**
469
     * {@inheritdoc}
470
     *
471
     * @no-named-arguments
472
     */
473
    public function addGroup(string ...$columns)
474
    {
475
        $this->compilerState->invalidate('groups');
476
477
        $this->statements['groups'] = [...$this->statements['groups'], ...$columns];
478
479
        return $this;
480
    }
481
482
    /**
483
     * {@inheritdoc}
484
     */
485 2
    public function having($column, $operator = null, $value = null)
486
    {
487 2
        $this->compilerState->invalidate('having');
488
489 2
        return $this->buildClause('having', $column, $operator, $value);
490
    }
491
492
    /**
493
     * {@inheritdoc}
494
     */
495 3
    public function orHaving($column, $operator = null, $value = null)
496
    {
497 3
        $this->compilerState->invalidate('having');
498
499 3
        return $this->buildClause('having', $column, $operator, $value, CompositeExpression::TYPE_OR);
500
    }
501
502
    /**
503
     * {@inheritdoc}
504
     */
505 2
    public function havingNull(string $column, string $type = CompositeExpression::TYPE_AND)
506
    {
507 2
        $this->compilerState->invalidate('having');
508
509 2
        return $this->buildClause('having', $column, '=', null, $type);
510
    }
511
512
    /**
513
     * {@inheritdoc}
514
     */
515 2
    public function havingNotNull(string $column, string $type = CompositeExpression::TYPE_AND)
516
    {
517 2
        $this->compilerState->invalidate('having');
518
519 2
        return $this->buildClause('having', $column, '!=', null, $type);
520
    }
521
522
    /**
523
     * {@inheritdoc}
524
     */
525 1
    public function orHavingNull(string $column)
526
    {
527 1
        return $this->havingNull($column, CompositeExpression::TYPE_OR);
528
    }
529
530
    /**
531
     * {@inheritdoc}
532
     */
533 1
    public function orHavingNotNull(string $column)
534
    {
535 1
        return $this->havingNotNull($column, CompositeExpression::TYPE_OR);
536
    }
537
538
    /**
539
     * {@inheritdoc}
540
     */
541 2
    public function havingRaw($raw, string $type = CompositeExpression::TYPE_AND)
542
    {
543 2
        $this->compilerState->invalidate('having');
544
545 2
        return $this->buildRaw('having', $raw, $type);
546
    }
547
548
    /**
549
     * {@inheritdoc}
550
     */
551 1
    public function orHavingRaw($raw)
552
    {
553 1
        return $this->havingRaw($raw, CompositeExpression::TYPE_OR);
554
    }
555
556
    /**
557
     * {@inheritdoc}
558
     */
559 6
    public function addCommand(string $command, $value)
560
    {
561
        switch ($command) {
562 6
            case ':limit':
563 4
                if (is_array($value)) {
564
                    $this->limit($value[0], $value[1]);
565
                } else {
566 4
                    $this->limit($value);
567
                }
568 4
                break;
569
570 4
            case ':limitPage':
571 1
                if (is_array($value)) {
572 1
                    $this->limitPage($value[0], $value[1]);
573
                } else {
574
                    $this->limitPage($value);
575
                }
576 1
                break;
577
578 3
            case ':offset':
579
                $this->offset($value);
580
                break;
581
582 3
            case ':order':
583 2
                $this->order($value);
584 2
                break;
585
586 1
            case ':distinct':
587 1
                $this->distinct($value);
588 1
                break;
589
590
            case ':group':
591
                $this->group($value);
592
                break;
593
594
            case ':having':
595
                $this->having($value);
596
                break;
597
        }
598
599 6
        return $this;
600
    }
601
602
    /**
603
     * {@inheritdoc}
604
     */
605 146
    public function toSql(): string
606
    {
607 146
        return $this->compile();
608
    }
609
610
    /**
611
     * {@inheritdoc}
612
     *
613
     * @todo A reprendre: utiliser les types des bindings
614
     *
615
     * @return string
616
     */
617 19
    public function toRawSql(): string
618
    {
619 19
        $keys   = [];
620 19
        $sql    = $this->toSql();
621 19
        $values = $this->compiler()->getBindings($this);
622
623
        # build a regular expression for each parameter
624 19
        foreach ($values as $key => $value) {
625 19
            if (is_string($key)) {
626
                $keys[] = '/:' . $key . '/';
627
            } else {
628 19
                $keys[] = '/[?]/';
629
            }
630
631 19
            if (is_array($value)) {
632
                $values[$key] = implode(',', $this->connection->quote($value));
633 19
            } elseif (is_null($value)) {
634
                $values[$key] = 'NULL';
635 19
            } elseif ($value instanceof \DateTimeInterface) {
636
                $values[$key] = $value->format($this->connection->platform()->grammar()->getDateTimeFormatString());
637 19
            } elseif (is_string($value)) {
638 10
                $values[$key] = $this->connection->quote($value);
639
            } else {
640 17
                $values[$key] = $value;
641
            }
642
        }
643
644 19
        return preg_replace($keys, $values, $sql, 1);
645
    }
646
647
    /**
648
     * Gets a string representation of this Query which corresponds to
649
     * the final SQL query being constructed.
650
     *
651
     * @return string The string representation of this Query.
652
     */
653
    public function __toString(): string
654
    {
655
        return $this->toSql();
656
    }
657
}
658