Issues (21)

src/Query.php (18 issues)

1
<?php
2
3
/**
4
 * Created by PhpStorm.
5
 * User: Goldoni
6
 * Date: 31/10/2018
7
 * Time: 00:53.
8
 */
9
10
namespace Goldoni\Builder;
11
12
use Pagerfanta\Pagerfanta;
13
use PDO;
14
15
/**
16
 * Class Builder.
17
 */
18
class Query implements \IteratorAggregate
19
{
20
    /**
21
     * @var array
22
     */
23
    private $select;
24
    /**
25
     * @var array
26
     */
27
    private $insert;
28
    /**
29
     * @var array
30
     */
31
    private $update;
32
    /**
33
     * @var array
34
     */
35
    private $delete;
36
    /**
37
     * @var array
38
     */
39
    private $values;
40
    /**
41
     * @var array
42
     */
43
    private $set;
44
    /**
45
     * @var array
46
     */
47
    private $from;
48
    /**
49
     * @var array
50
     */
51
    private $where = [];
52
    /**
53
     * @var array
54
     */
55
    private $joins;
56
    /**
57
     * @var string
58
     */
59
    private $entity;
60
    /**
61
     * @var string
62
     */
63
    private $group;
64
    /**
65
     * @var array
66
     */
67
    private $order;
68
    /**
69
     * @var int
70
     */
71
    private $limit;
72
73
    /**
74
     * @var \PDO
75
     */
76
    private $pdo;
77
78
    /**
79
     * @var array
80
     */
81
    private $params = [];
82
83
    /**
84
     * Query constructor.
85
     *
86
     * @param null|\PDO $pdo
87
     */
88
    public function __construct(?PDO $pdo = null)
89
    {
90
        $this->pdo = $pdo;
91
    }
92
93
    /**
94
     * @param string $table
95
     * @param string $alias
96
     *
97
     * @return \Goldoni\Builder\Query
98
     */
99
    public function from(string $table, ?string $alias = null): self
100
    {
101
        if ($alias) {
102
            $this->from[$table] = $alias;
103
        } else {
104
            $this->from[] = $table;
105
        }
106
107
        return $this;
108
    }
109
110
    /**
111
     * @param string ...$fields
112
     *
113
     * @return \Goldoni\Builder\Query
114
     */
115
    public function select(string ...$fields): self
116
    {
117
        $this->select = $fields;
118
119
        return $this;
120
    }
121
122
    /**
123
     * @param string $table
124
     * @param array  $attributes
125
     *
126
     * @return \Goldoni\Builder\Query
127
     */
128
    public function insert(string $table, ?array $attributes = null): self
129
    {
130
        $this->insert = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table of type string is incompatible with the declared type array of property $insert.

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...
131
132
        if ($attributes) {
133
            $this->values = $attributes;
134
        }
135
136
        return $this;
137
    }
138
139
    public function value(array $attributes): self
140
    {
141
        $this->values = $attributes;
142
143
        return $this;
144
    }
145
146
    /**
147
     * @param string $table
148
     * @param array  $attributes
149
     * @param int    $id
150
     *
151
     * @return \Goldoni\Builder\Query
152
     */
153
    public function update(string $table, ?array $attributes = null, ?int $id = null): self
154
    {
155
        $this->update = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table of type string is incompatible with the declared type array of property $update.

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...
156
157
        if ($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
158
            $this->where('id = :id');
159
            $this->params(['id' => $id]);
160
        }
161
162
        if ($attributes) {
163
            $this->set = $attributes;
164
        }
165
166
        return $this;
167
    }
168
169
    public function set(array $attributes): self
170
    {
171
        $this->set = $attributes;
172
173
        return $this;
174
    }
175
176
    public function delete(string $table, ?int $id = null): self
177
    {
178
        $this->delete = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table of type string is incompatible with the declared type array of property $delete.

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...
179
180
        if ($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
181
            $this->where('id = :id');
182
            $this->params(['id' => $id]);
183
        }
184
185
        return $this;
186
    }
187
188
    /**
189
     * @param string ...$conditions
190
     *
191
     * @return \Goldoni\Builder\Query
192
     */
193
    public function where(string ...$conditions): self
194
    {
195
        $this->where = array_merge($this->where, $conditions);
196
197
        return $this;
198
    }
199
200
    /**
201
     * @param string $table
202
     * @param string $condition
203
     * @param string $type
204
     *
205
     * @return \Goldoni\Builder\Query
206
     */
207
    public function join(string $table, string $condition, string  $type = 'left'): self
208
    {
209
        $this->joins[$type][] = [$table, $condition];
210
211
        return $this;
212
    }
213
214
    /**
215
     * @throws \Exception
216
     *
217
     * @return int
218
     */
219
    public function count(): int
220
    {
221
        $query = clone $this;
222
        $table = current($this->from);
223
224
        return $query->select("COUNT({$table}.id)")->execute()->fetchColumn();
225
    }
226
227
    /**
228
     * @param string      $column
229
     * @param null|string $direction
230
     *
231
     * @return \Goldoni\Builder\Query
232
     */
233
    public function orderBy(string $column, ?string $direction = 'ASC'): self
234
    {
235
        $this->order[$column] = $direction;
236
237
        return $this;
238
    }
239
240
    /**
241
     * @param string $column
242
     *
243
     * @return \Goldoni\Builder\Query
244
     */
245
    public function groupBy(string $column): self
246
    {
247
        $this->group = $column;
248
249
        return $this;
250
    }
251
252
    /**
253
     * @param int $limit
254
     * @param int $offset
255
     *
256
     * @return \Goldoni\Builder\Query
257
     */
258
    public function limit(int $limit, int $offset = 0): self
259
    {
260
        $this->limit = "$offset, $limit";
0 ignored issues
show
Documentation Bug introduced by
The property $limit was declared of type integer, but $offset.', '.$limit is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
261
262
        return $this;
263
    }
264
265
    /**
266
     * @param string $entity
267
     *
268
     * @return \Goldoni\Builder\Query
269
     */
270
    public function into(string $entity): self
271
    {
272
        $this->entity = $entity;
273
274
        return $this;
275
    }
276
277
    public function fetchAll(): QueryResult
278
    {
279
        return new QueryResult($this->execute()->fetchAll(\PDO::FETCH_ASSOC), $this->entity);
280
    }
281
282
    public function paginate(int $perPage, int $currentPage = 1)
283
    {
284
        $paginator = new Paginator($this, $perPage, $currentPage);
0 ignored issues
show
The call to Goldoni\Builder\Paginator::__construct() has too many arguments starting with $perPage. ( Ignorable by Annotation )

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

284
        $paginator = /** @scrutinizer ignore-call */ new Paginator($this, $perPage, $currentPage);

This check compares calls to functions or methods with their respective definitions. If the call has more 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...
285
286
        return (new Pagerfanta($paginator))
287
            ->setMaxPerPage($perPage)
288
            ->setCurrentPage($currentPage);
289
    }
290
291
    public function fetch()
292
    {
293
        $record = $this->execute()->fetch(\PDO::FETCH_ASSOC);
294
295
        if (false === $record) {
296
            return false;
297
        }
298
299
        if ($this->entity) {
300
            return Builder::hydrate($record, $this->entity);
301
        }
302
303
        return $record;
304
    }
305
306
    /**
307
     * @throws \Exception
308
     */
309
    public function fetchOrFail()
310
    {
311
        $record = $this->fetch();
312
313
        if (false === $record) {
314
            throw new \Exception('No query results for model');
315
        }
316
317
        return $record;
318
    }
319
320
    /**
321
     * @param array $params
322
     * @param bool  $merge
323
     *
324
     * @return \Goldoni\Builder\Query
325
     */
326
    public function params(array $params, bool $merge = true): self
327
    {
328
        if ($merge) {
329
            $this->params = array_merge($this->params, $params);
330
        } else {
331
            $this->params = $params;
332
        }
333
334
        return $this;
335
    }
336
337
    /**
338
     * @return string
339
     */
340
    public function __toString()
341
    {
342
        $parts = ['SELECT'];
343
344
        if ($this->select) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->select 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...
345
            $parts[] = implode(', ', $this->select);
346
        } else {
347
            $parts[] = '*';
348
        }
349
350
        if ($this->insert) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->insert 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...
351
            $parts = ['INSERT INTO ' . $this->insert];
0 ignored issues
show
Are you sure $this->insert of type array can be used in concatenation? ( Ignorable by Annotation )

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

351
            $parts = ['INSERT INTO ' . /** @scrutinizer ignore-type */ $this->insert];
Loading history...
352
        }
353
354
        if ($this->values) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->values 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...
355
            $parts[] = '(' . implode(', ', array_keys($this->values)) . ')';
356
            $parts[] = 'VALUES';
357
            $parts[] = '(' . implode(', ', array_values($this->values)) . ')';
358
        }
359
360
        if ($this->update) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->update 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...
361
            $parts = ['UPDATE ' . $this->update . ' SET'];
0 ignored issues
show
Are you sure $this->update of type array can be used in concatenation? ( Ignorable by Annotation )

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

361
            $parts = ['UPDATE ' . /** @scrutinizer ignore-type */ $this->update . ' SET'];
Loading history...
362
        }
363
364
        if ($this->set) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->set 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...
365
            $sets = [];
366
367
            foreach ($this->set as $key => $value) {
368
                $sets[] = "$key = $value";
369
            }
370
            $parts[] = implode(', ', $sets);
371
        }
372
373
        if ($this->delete) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->delete 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...
374
            $parts = ['DELETE FROM ' . $this->delete];
0 ignored issues
show
Are you sure $this->delete of type array can be used in concatenation? ( Ignorable by Annotation )

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

374
            $parts = ['DELETE FROM ' . /** @scrutinizer ignore-type */ $this->delete];
Loading history...
375
        }
376
377
        if ($this->from) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->from 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...
378
            $parts[] = 'FROM';
379
            $parts[] = $this->buildFrom();
380
        }
381
382
        if (!empty($this->where)) {
383
            $parts[] = 'WHERE';
384
            $parts[] = '(' . implode(') AND (', $this->where) . ')';
385
        }
386
387
        if (!empty($this->joins)) {
388
            foreach ($this->joins as $type => $joins) {
389
                foreach ($joins as [$table, $condition]) {
390
                    $parts[] = mb_strtoupper($type) . " JOIN $table ON $condition";
391
                }
392
            }
393
        }
394
395
        if ($this->order) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->order 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...
396
            foreach ($this->order as $key => $value) {
397
                $parts[] = "ORDER BY $key $value";
398
            }
399
        }
400
401
        if ($this->group) {
402
            $parts[] = 'GROUP BY ' . $this->group;
403
        }
404
405
        if ($this->limit) {
406
            $parts[] = 'LIMIT ' . $this->limit;
407
        }
408
409
        return implode(' ', $parts);
410
    }
411
412
    private function buildFrom(): string
413
    {
414
        $from = [];
415
416
        foreach ($this->from as $key => $value) {
417
            if (\is_string($key)) {
418
                $from[] = "$key as $value";
419
            } else {
420
                $from[] = $value;
421
            }
422
        }
423
424
        return implode(', ', $from);
425
    }
426
427
    public function execute()
428
    {
429
        if (!empty($this->params)) {
430
            $statement = $this->pdo->prepare($this->__toString());
431
432
            if (!$statement->execute($this->params)) {
433
                throw new \Exception("Sql Error by execute query: {$this->__toString()}");
434
            }
435
436
            return $statement;
437
        }
438
439
        return $this->pdo->query($this->__toString());
440
    }
441
442
    /**
443
     * @return \Goldoni\Builder\QueryResult|\Traversable
444
     */
445
    public function getIterator()
446
    {
447
        return $this->fetchAll();
448
    }
449
}
450