Passed
Pull Request — master (#16)
by Rodinei
02:04
created

DataLayer::clauseJoins()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 17
rs 8.8333
cc 7
nc 12
nop 0
1
<?php
2
3
namespace CoffeeCode\DataLayer;
4
5
use Exception;
6
use PDO;
7
use PDOException;
8
use stdClass;
9
10
/**
11
 * Class DataLayer
12
 * @package CoffeeCode\DataLayer
13
 */
14
abstract class DataLayer
15
{
16
    use CrudTrait;
17
18
    /** @var string $entity database table */
19
    private $entity;
20
21
    /** @var string $primary table primary key field */
22
    private $primary;
23
24
    /** @var array $required table required fields */
25
    private $required;
26
27
    /** @var string $timestamps control created and updated at */
28
    private $timestamps;
29
30
    /** @var string */
31
    protected $statement;
32
33
    /** @var string */
34
    protected $params;
35
36
    /** @var string */
37
    protected $group;
38
39
    /** @var string */
40
    protected $order;
41
42
    /** @var int */
43
    protected $limit;
44
45
    /** @var int */
46
    protected $offset;
47
48
    /** @var \PDOException|null */
49
    protected $fail;
50
51
    /** @var object|null */
52
    protected $data;
53
54
    /** @var array */
55
    protected $join;
56
57
    /** @var array */
58
    protected $leftJoin;
59
60
    /** @var array */
61
    protected $rightJoin;
62
63
    /** @var array */
64
    protected $where;
65
66
    /**
67
     * DataLayer constructor.
68
     * @param string $entity
69
     * @param array $required
70
     * @param string $primary
71
     * @param bool $timestamps
72
     */
73
    public function __construct(string $entity, array $required, string $primary = 'id', bool $timestamps = true)
74
    {
75
        $this->entity = $entity;
76
        $this->primary = $primary;
77
        $this->required = $required;
78
        $this->timestamps = $timestamps;
0 ignored issues
show
Documentation Bug introduced by
The property $timestamps was declared of type string, but $timestamps is of type boolean. 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...
79
    }
80
81
    /**
82
     * @param $name
83
     * @param $value
84
     */
85
    public function __set($name, $value)
86
    {
87
        if (empty($this->data)) {
88
            $this->data = new stdClass();
89
        }
90
91
        $this->data->$name = $value;
92
    }
93
94
    /**
95
     * @param $name
96
     * @return bool
97
     */
98
    public function __isset($name)
99
    {
100
        return isset($this->data->$name);
101
    }
102
103
    /**
104
     * @param $name
105
     * @return string|null
106
     */
107
    public function __get($name)
108
    {
109
        return ($this->data->$name ?? null);
110
    }
111
112
    /**
113
     * @return object|null
114
     */
115
    public function data(): ?object
116
    {
117
        return $this->data;
118
    }
119
120
    /**
121
     * @return PDOException|Exception|null
122
     */
123
    public function fail()
124
    {
125
        return $this->fail;
126
    }
127
128
    /**
129
     * @param string|null $terms
130
     * @param string|null $params
131
     * @param string $columns
132
     * @return DataLayer
133
     */
134
    public function find(?string $terms = null, ?string $params = null, string $columns = "*"): DataLayer
135
    {
136
        if ($terms) {
137
            $this->statement = "SELECT {$columns} FROM {$this->entity} WHERE {$terms}";
138
            parse_str($params, $this->params);
0 ignored issues
show
Bug introduced by
$this->params of type string is incompatible with the type array|null expected by parameter $arr of parse_str(). ( Ignorable by Annotation )

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

138
            parse_str($params, /** @scrutinizer ignore-type */ $this->params);
Loading history...
139
            return $this;
140
        }
141
142
        $this->statement = "SELECT {$columns} FROM {$this->entity}";
143
        return $this;
144
    }
145
146
    /**
147
     * @param int $id
148
     * @param string $columns
149
     * @return DataLayer|null
150
     */
151
    public function findById(int $id, string $columns = "*"): ?DataLayer
152
    {
153
        $find = $this->find($this->primary . " = :id", "id={$id}", $columns);
154
        return $find->fetch();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $find->fetch() could return the type array which is incompatible with the type-hinted return CoffeeCode\DataLayer\DataLayer|null. Consider adding an additional type-check to rule them out.
Loading history...
155
    }
156
157
    /**
158
     * for use with the new functions:
159
     * join(), leftJoin(), rightJoin(), where(), whereRaw(), whereIn()
160
     *
161
     * @param bool $all
162
     * @return null|array|mixed|DataLayer
163
     */
164
    public function get(bool $all = false)
165
    {
166
        $find = $this->find();
167
        return $find->fetch($all);
168
    }
169
170
    /**
171
     * @param string $column
172
     * @return DataLayer|null
173
     */
174
    public function group(string $column): ?DataLayer
175
    {
176
        $this->group = " GROUP BY {$column}";
177
        return $this;
178
    }
179
180
    /**
181
     * @param string $columnOrder
182
     * @return DataLayer|null
183
     */
184
    public function order(string $columnOrder): ?DataLayer
185
    {
186
        $this->order = " ORDER BY {$columnOrder}";
187
        return $this;
188
    }
189
190
    /**
191
     * @param int $limit
192
     * @return DataLayer|null
193
     */
194
    public function limit(int $limit): ?DataLayer
195
    {
196
        $this->limit = " LIMIT {$limit}";
0 ignored issues
show
Documentation Bug introduced by
The property $limit was declared of type integer, but ' LIMIT '.$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...
197
        return $this;
198
    }
199
200
    /**
201
     * @param int $offset
202
     * @return DataLayer|null
203
     */
204
    public function offset(int $offset): ?DataLayer
205
    {
206
        $this->offset = " OFFSET {$offset}";
0 ignored issues
show
Documentation Bug introduced by
The property $offset was declared of type integer, but ' OFFSET '.$offset 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...
207
        return $this;
208
    }
209
210
    /**
211
     * @param string $table
212
     * @param mixed ...$args
213
     * @return DataLayer
214
     */
215
    public function join(string $table, ...$args): DataLayer
216
    {
217
        if (!$args[0] instanceof \Closure && $args[0] && $args[1]) {
218
            $this->join[] = " INNER JOIN {$table} ON ({$table}.{$args[0]} = {$this->entity}.{$args[1]}) ";
219
        }
220
        return $this;
221
    }
222
223
    /**
224
     * @param string $table
225
     * @param mixed ...$args
226
     * @return DataLayer
227
     */
228
    public function leftJoin(string $table, ...$args): DataLayer
229
    {
230
        if (!$args[0] instanceof \Closure && $args[0] && $args[1]) {
231
            $this->leftJoin[] = " LEFT OUTER JOIN {$table} ON ({$table}.{$args[0]} = {$this->entity}.{$args[1]}) ";
232
        }
233
        return $this;
234
    }
235
236
    /**
237
     * @param string $table
238
     * @param mixed ...$args
239
     * @return DataLayer
240
     */
241
    public function rightJoin(string $table, ...$args): DataLayer
242
    {
243
        if (!$args[0] instanceof \Closure && $args[0] && $args[1]) {
244
            $this->rightJoin[] = " RIGHT OUTER JOIN {$table} ON ({$table}.{$args[0]} = {$this->entity}.{$args[1]}) ";
245
        }
246
        return $this;
247
    }
248
249
    /**
250
     * @param string $whereRaw
251
     * @return DataLayer
252
     */
253
    public function whereRaw(string $whereRaw): DataLayer
254
    {
255
        $this->where[] = " {$whereRaw} ";
256
        return $this;
257
    }
258
259
    /**
260
     * @param string $field
261
     * @param array $values
262
     * @return DataLayer
263
     */
264
    public function whereIn(string $field, array $values = []): DataLayer
265
    {
266
        $this->where[] = " {$field} IN (" . implode(",", $values) . ")";
267
        return $this;
268
    }
269
270
    /**
271
     * @param string $field
272
     * @param string $operator
273
     * @param $value
274
     * @return DataLayer
275
     */
276
    public function where(string $field, string $operator, $value): DataLayer
277
    {
278
        $this->where[] = " {$field} {$operator} :" . str_replace(".", "_", $field);
279
        $params = "{$field}={$value}";
280
        $this->concatParams($params);
281
        parse_str($params, $this->params);
0 ignored issues
show
Bug introduced by
$this->params of type string is incompatible with the type array|null expected by parameter $arr of parse_str(). ( Ignorable by Annotation )

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

281
        parse_str($params, /** @scrutinizer ignore-type */ $this->params);
Loading history...
282
        return $this;
283
    }
284
285
    /**
286
     * @param string|null $params
287
     */
288
    private function concatParams(?string &$params): void
289
    {
290
        if ($this->params) {
291
            foreach ($this->params as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $this->params of type string is not traversable.
Loading history...
292
                $params .= "&{$key}={$value}";
293
            }
294
        }
295
    }
296
297
    /**
298
     * clauseWhere
299
     * @return void
300
     */
301
    private function clauseWhere(): void
302
    {
303
        if ($this->where) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->where 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...
304
            foreach ($this->where as $key => $value) {
305
                if (strpos($this->statement, "WHERE") === false) {
306
                    $this->statement .= " WHERE {$value} ";
307
                } else {
308
                    $this->statement .= " AND {$value} ";
309
                }
310
            }
311
        }
312
    }
313
314
    /**
315
     * clauseJoins
316
     * @return void
317
     */
318
    private function clauseJoins(): void
319
    {
320
        if ($this->join) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->join 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...
321
            foreach ($this->join as $key => $value) {
322
                $this->statement .= $value;
323
            }
324
        }
325
326
        if ($this->leftJoin) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->leftJoin 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...
327
            foreach ($this->leftJoin as $key => $value) {
328
                $this->statement .= $value;
329
            }
330
        }
331
332
        if ($this->rightJoin) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rightJoin 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...
333
            foreach ($this->rightJoin as $key => $value) {
334
                $this->statement .= $value;
335
            }
336
        }
337
    }
338
339
    /**
340
     * @param bool $all
341
     * @return array|mixed|null
342
     */
343
    public function fetch(bool $all = false)
344
    {
345
        try {
346
347
            $this->clauseJoins();
348
            $this->clauseWhere();
349
350
            $stmt = Connect::getInstance()->prepare($this->statement . $this->group . $this->order . $this->limit . $this->offset);
351
            $stmt->execute($this->params);
0 ignored issues
show
Bug introduced by
$this->params of type string is incompatible with the type array expected by parameter $input_parameters of PDOStatement::execute(). ( Ignorable by Annotation )

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

351
            $stmt->execute(/** @scrutinizer ignore-type */ $this->params);
Loading history...
352
353
            if (!$stmt->rowCount()) {
354
                return null;
355
            }
356
357
            if ($all) {
358
                return $stmt->fetchAll(PDO::FETCH_CLASS, static::class);
359
            }
360
361
            return $stmt->fetchObject(static::class);
362
        } catch (PDOException $exception) {
363
            $this->fail = $exception;
364
            return null;
365
        }
366
    }
367
368
    /**
369
     * @return int
370
     */
371
    public function count(): int
372
    {
373
        $stmt = Connect::getInstance()->prepare($this->statement);
374
        $stmt->execute($this->params);
0 ignored issues
show
Bug introduced by
$this->params of type string is incompatible with the type array expected by parameter $input_parameters of PDOStatement::execute(). ( Ignorable by Annotation )

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

374
        $stmt->execute(/** @scrutinizer ignore-type */ $this->params);
Loading history...
375
        return $stmt->rowCount();
376
    }
377
378
    /**
379
     * @return bool
380
     */
381
    public function save(): bool
382
    {
383
        $primary = $this->primary;
384
        $id = null;
385
386
        try {
387
            if (!$this->required()) {
388
                throw new Exception("Preencha os campos necessários");
389
            }
390
391
            /** Update */
392
            if (!empty($this->data->$primary)) {
393
                $id = $this->data->$primary;
394
                $this->update($this->safe(), $this->primary . " = :id", "id={$id}");
395
            }
396
397
            /** Create */
398
            if (empty($this->data->$primary)) {
399
                $id = $this->create($this->safe());
400
            }
401
402
            if (!$id) {
403
                return false;
404
            }
405
406
            $this->data = $this->findById($id)->data();
407
            return true;
408
        } catch (Exception $exception) {
409
            $this->fail = $exception;
0 ignored issues
show
Documentation Bug introduced by
$exception is of type Exception, but the property $fail was declared to be of type PDOException|null. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
410
            return false;
411
        }
412
    }
413
414
    /**
415
     * @return bool
416
     */
417
    public function destroy(): bool
418
    {
419
        $primary = $this->primary;
420
        $id = $this->data->$primary;
421
422
        if (empty($id)) {
423
            return false;
424
        }
425
426
        $destroy = $this->delete($this->primary . " = :id", "id={$id}");
427
        return $destroy;
428
    }
429
430
    /**
431
     * @return bool
432
     */
433
    protected function required(): bool
434
    {
435
        $data = (array)$this->data();
436
        foreach ($this->required as $field) {
437
            if (empty($data[$field])) {
438
                return false;
439
            }
440
        }
441
        return true;
442
    }
443
444
    /**
445
     * @return array|null
446
     */
447
    protected function safe(): ?array
448
    {
449
        $safe = (array)$this->data;
450
        unset($safe[$this->primary]);
451
452
        return $safe;
453
    }
454
}
455