Test Setup Failed
Push — master ( 0246f2...eafbab )
by Chauncey
08:12
created

DatabaseSource::saveItem()   C

Complexity

Conditions 7
Paths 36

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 50
rs 6.7272
cc 7
eloc 31
nc 36
nop 1
1
<?php
2
3
namespace Charcoal\Source;
4
5
use PDO;
6
use PDOException;
7
use InvalidArgumentException;
8
use RuntimeException;
9
use UnexpectedValueException;
10
11
// From 'charcoal-core'
12
use Charcoal\Model\ModelInterface;
13
use Charcoal\Source\AbstractSource;
14
use Charcoal\Source\DatabaseSourceConfig;
15
use Charcoal\Source\DatabaseSourceInterface;
16
use Charcoal\Source\Database\DatabaseFilter;
17
use Charcoal\Source\Database\DatabaseOrder;
18
use Charcoal\Source\Database\DatabasePagination;
19
20
/**
21
 * Database Source, through PDO.
22
 */
23
class DatabaseSource extends AbstractSource implements DatabaseSourceInterface
24
{
25
    const DEFAULT_DB_HOSTNAME = 'localhost';
26
    const DEFAULT_DB_TYPE = 'mysql';
27
28
    /**
29
     * @var PDO
30
     */
31
    private $pdo;
32
33
    /**
34
     * @var string $table
35
     */
36
    private $table = null;
37
38
    /**
39
     * @var array $dbs
40
     */
41
    private static $db;
0 ignored issues
show
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
42
43
    /**
44
     * @param array $data Class dependencies.
45
     */
46
    public function __construct(array $data)
47
    {
48
        $this->pdo = $data['pdo'];
49
50
        parent::__construct($data);
51
    }
52
53
    /**
54
     * Set the database's table to use.
55
     *
56
     * @param string $table The source table.
57
     * @throws InvalidArgumentException If argument is not a string or alphanumeric/underscore.
58
     * @return DatabaseSource Chainable
59
     */
60 View Code Duplication
    public function setTable($table)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
61
    {
62
        if (!is_string($table)) {
63
            throw new InvalidArgumentException(
64
                sprintf(
65
                    'DatabaseSource::setTable() expects a string as table. (%s given). [%s]',
66
                    gettype($table),
67
                    get_class($this->model())
68
                )
69
            );
70
        }
71
        // For security reason, only alphanumeric characters (+ underscores) are valid table names.
72
        // Although SQL can support more, there's really no reason to.
73
        if (!preg_match('/[A-Za-z0-9_]/', $table)) {
74
            throw new InvalidArgumentException(
75
                sprintf('Table name "%s" is invalid: must be alphanumeric / underscore.', $table)
76
            );
77
        }
78
        $this->table = $table;
79
80
        return $this;
81
    }
82
83
    /**
84
     * Get the database's current table.
85
     *
86
     * @throws RuntimeException If the table was not set.
87
     * @return string
88
     */
89
    public function table()
90
    {
91
        if ($this->table === null) {
92
            throw new RuntimeException(
93
                'Table was not set.'
94
            );
95
        }
96
        return $this->table;
97
    }
98
99
    /**
100
     * Create a table from a model's metadata.
101
     *
102
     * @return boolean Success / Failure
103
     */
104
    public function createTable()
105
    {
106
        if ($this->tableExists()) {
107
            // Table already exists
108
            return true;
109
        }
110
111
        $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
112
113
        $model = $this->model();
114
        $metadata = $model->metadata();
115
        $fields = $this->getModelFields($model);
116
        $fieldsSql = [];
117
        foreach ($fields as $field) {
118
            $fieldsSql[] = $field->sql();
119
        }
120
121
        $q = 'CREATE TABLE  `'.$this->table().'` ('."\n";
122
        $q .= implode(',', $fieldsSql);
123
        $key = $model->key();
124
        if ($key) {
125
            $q .= ', PRIMARY KEY (`'.$key.'`) '."\n";
126
        }
127
        /** @todo add indexes for all defined list constraints (yea... tough job...) */
128
        if ($dbDriver === 'mysql') {
129
            $engine = 'InnoDB';
130
            $q .= ') ENGINE='.$engine.' DEFAULT CHARSET=utf8 COMMENT=\''.addslashes($metadata['name']).'\';';
131
        } else {
132
            $q .= ');';
133
        }
134
        $this->logger->debug($q);
135
        $this->db()->query($q);
136
137
        return true;
138
    }
139
140
    /**
141
     * Alter an existing table to match the model's metadata.
142
     *
143
     * @return boolean Success / Failure
144
     */
145
    public function alterTable()
146
    {
147
        if (!$this->tableExists()) {
148
            return false;
149
        }
150
151
        $fields = $this->getModelFields($this->model());
152
153
        $cols = $this->tableStructure();
154
155
        foreach ($fields as $field) {
156
            $ident = $field->ident();
157
158
            if (!array_key_exists($ident, $cols)) {
159
                // The key does not exist at all.
160
                $q = 'ALTER TABLE `'.$this->table().'` ADD '.$field->sql();
161
                $this->logger->debug($q);
162
                $this->db()->query($q);
163
            } else {
164
                // The key exists. Validate.
165
                $col = $cols[$ident];
166
                $alter = true;
167
                if (strtolower($col['Type']) != strtolower($field->sqlType())) {
168
                    $alter = true;
169
                }
170
                if ((strtolower($col['Null']) == 'no') && !$field->allowNull()) {
171
                    $alter = true;
172
                }
173
                if ((strtolower($col['Null']) != 'no') && $field->allowNull()) {
174
                    $alter = true;
175
                }
176
                if ($col['Default'] != $field->defaultVal()) {
177
                    $alter = true;
178
                }
179
180
                if ($alter === true) {
181
                    $q = 'ALTER TABLE `'.$this->table().'` CHANGE `'.$ident.'` '.$field->sql();
182
                    $this->logger->debug($q);
183
                    $this->db()->query($q);
184
                }
185
            }
186
        }
187
188
        return true;
189
    }
190
191
    /**
192
     * @return boolean
193
     */
194
    public function tableExists()
195
    {
196
        $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
197 View Code Duplication
        if ($dbDriver === 'sqlite') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
            $q = 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\''.$this->table().'\';';
199
        } else {
200
            $q = 'SHOW TABLES LIKE \''.$this->table().'\'';
201
        }
202
        $this->logger->debug($q);
203
        $res = $this->db()->query($q);
204
        $tableExists = $res->fetchColumn(0);
205
206
        // Return as boolean
207
        return !!$tableExists;
208
    }
209
210
    /**
211
     * Get the table columns information.
212
     *
213
     * @return array
214
     */
215
    public function tableStructure()
216
    {
217
        $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
218 View Code Duplication
        if ($dbDriver === 'sqlite') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
            $q = 'PRAGMA table_info(\''.$this->table().'\') ';
220
        } else {
221
            $q = 'SHOW COLUMNS FROM `'.$this->table().'`';
222
        }
223
        $this->logger->debug($q);
224
        $res = $this->db()->query($q);
225
        $cols = $res->fetchAll((PDO::FETCH_GROUP|PDO::FETCH_UNIQUE|PDO::FETCH_ASSOC));
226
        if ($dbDriver === 'sqlite') {
227
            $ret = [];
228
            foreach ($cols as $c) {
229
                // Normalize SQLite's result (PRAGMA) with mysql's (SHOW COLUMNS)
230
                $ret[$c['name']] = [
231
                    'Type'      => $c['type'],
232
                    'Null'      => !!$c['notnull'] ? 'NO' : 'YES',
233
                    'Default'   => $c['dflt_value'],
234
                    'Key'       => !!$c['pk'] ? 'PRI' : '',
235
                    'Extra'     => ''
236
                ];
237
            }
238
            return $ret;
239
        } else {
240
            return $cols;
241
        }
242
    }
243
244
    /**
245
     * Check wether the source table is empty (`true`) or not (`false`)
246
     *
247
     * @return boolean
248
     */
249
    public function tableIsEmpty()
250
    {
251
        $q = 'SELECT NULL FROM `'.$this->table().'` LIMIT 1';
252
        $this->logger->debug($q);
253
        $res = $this->db()->query($q);
254
        return ($res->rowCount() === 0);
255
    }
256
257
    /**
258
     * @throws Exception If the database can not set.
259
     * @return PDO
260
     */
261
    public function db()
262
    {
263
        return $this->pdo;
264
    }
265
266
    /**
267
     * Get all the fields of a model.
268
     *
269
     * @param ModelInterface $model      The model to get fields from.
270
     * @param array|null     $properties Optional list of properties to get. If null, retrieve all (from metadata).
271
     * @return array
272
     * @todo Move this method in StorableTrait or AbstractModel
273
     */
274
    private function getModelFields(ModelInterface $model, $properties = null)
275
    {
276
        if ($properties === null) {
277
            // No custom properties; use all (from model metadata)
278
            $properties = array_keys($model->metadata()->properties());
279
        } else {
280
            // Ensure the key is always in the required fields.
281
            $properties = array_merge($properties, [ $model->key() ]);
282
        }
283
284
        $fields = [];
285
        foreach ($properties as $propertyIdent) {
286
            $p = $model->p($propertyIdent);
287
            if (!$p || !$p->active() || !$p->storable()) {
288
                continue;
289
            }
290
291
            $v = $model->propertyValue($propertyIdent);
0 ignored issues
show
Bug introduced by
The method propertyValue() does not exist on Charcoal\Model\ModelInterface. Did you maybe mean property()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
292
            foreach ($p->fields($v) as $fieldIdent => $field) {
293
                $fields[$field->ident()] = $field;
294
            }
295
        }
296
        return $fields;
297
    }
298
299
    /**
300
     * @param mixed             $ident Ident can be any scalar value.
301
     * @param StorableInterface $item  Optional item to load into.
302
     * @return StorableInterface
303
     */
304
    public function loadItem($ident, StorableInterface $item = null)
305
    {
306
        $key = $this->model()->key();
307
308
        return $this->loadItemFromKey($key, $ident, $item);
309
    }
310
311
    /**
312
     * Load item from a custom column's name ($key)
313
     *
314
     * @param  string                 $key   Column name.
315
     * @param  mixed                  $ident Value of said column.
316
     * @param  StorableInterface|null $item  Optional. Item (storable object) to load into.
317
     * @throws Exception If the query fails.
318
     * @return StorableInterface             Item
319
     */
320
    public function loadItemFromKey($key, $ident, StorableInterface $item = null)
321
    {
322 View Code Duplication
        if ($item !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
323
            $this->setModel($item);
0 ignored issues
show
Documentation introduced by
$item is of type object<Charcoal\Source\StorableInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
324
        } else {
325
            $class = get_class($this->model());
326
            $item = new $class;
327
        }
328
329
        // Missing parameters
330
        if (!$key || !$ident) {
331
            return $item;
332
        }
333
334
        $q = '
335
            SELECT
336
                *
337
            FROM
338
               `'.$this->table().'`
339
            WHERE
340
               `'.$key.'`=:ident
341
            LIMIT
342
               1';
343
344
        $binds = [
345
            'ident' => $ident
346
        ];
347
348
        return $this->loadItemFromQuery($q, $binds, $item);
349
    }
350
351
    /**
352
     * @param  string            $query The SQL query.
353
     * @param  array             $binds Optional. The query parameters.
354
     * @param  StorableInterface $item  Optional. Item (storable object) to load into.
355
     * @throws PDOException If there is a query error.
356
     * @return StorableInterface Item.
357
     */
358
    public function loadItemFromQuery($query, array $binds = [], StorableInterface $item = null)
359
    {
360 View Code Duplication
        if ($item !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
361
            $this->setModel($item);
0 ignored issues
show
Documentation introduced by
$item is of type object<Charcoal\Source\StorableInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
362
        } else {
363
            $class = get_class($this->model());
364
            $item = new $class;
365
        }
366
367
        // Missing parameters
368
        if (!$query) {
369
            return $item;
370
        }
371
372
        $sth = $this->dbQuery($query, $binds);
373
        if ($sth === false) {
374
            throw new PDOException('Could not load item.');
375
        }
376
377
        $data = $sth->fetch(PDO::FETCH_ASSOC);
378
        if ($data) {
379
            $item->setFlatData($data);
380
        }
381
382
        return $item;
383
    }
384
385
    /**
386
     * @param StorableInterface|null $item Optional item to use as model.
387
     * @see this->loadItemsFromQuery()
388
     * @return array
389
     */
390
    public function loadItems(StorableInterface $item = null)
391
    {
392
        if ($item !== null) {
393
            $this->setModel($item);
0 ignored issues
show
Documentation introduced by
$item is of type object<Charcoal\Source\StorableInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
394
        }
395
396
        $q = $this->sqlLoad();
397
        return $this->loadItemsFromQuery($q, [], $item);
398
    }
399
400
    /**
401
     * Loads items to a list from a query
402
     * Allows external use.
403
     *
404
     * @param  string                 $q     The actual query.
405
     * @param  array                  $binds This has to be done.
406
     * @param  StorableInterface|null $item  Model Item.
407
     * @return array                         Collection of Items | Model
408
     */
409
    public function loadItemsFromQuery($q, array $binds = [], StorableInterface $item = null)
410
    {
411
        if ($item !== null) {
412
            $this->setModel($item);
0 ignored issues
show
Documentation introduced by
$item is of type object<Charcoal\Source\StorableInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
413
        }
414
415
        // Out
416
        $items = [];
417
418
        $model = $this->model();
419
        $db = $this->db();
420
421
        $this->logger->debug($q);
422
        $sth = $db->prepare($q);
423
424
        // @todo Binds
425
        if (!empty($binds)) {
426
            //
427
            unset($binds);
428
        }
429
430
        $sth->execute();
431
        $sth->setFetchMode(PDO::FETCH_ASSOC);
432
433
        $classname = get_class($model);
434
        while ($objData = $sth->fetch()) {
435
            $obj = new $classname;
436
            $obj->setFlatData($objData);
437
            $items[] = $obj;
438
        }
439
440
        return $items;
441
    }
442
443
    /**
444
     * Save an item (create a new row) in storage.
445
     *
446
     * @param  StorableInterface $item The object to save.
447
     * @throws PDOException If a database error occurs.
448
     * @return mixed The created item ID, or false in case of an error.
449
     */
450
    public function saveItem(StorableInterface $item)
451
    {
452
        if ($this->tableExists() === false) {
453
            /** @todo Optionnally turn off for some models */
454
            $this->createTable();
455
        }
456
457
        if ($item !== null) {
458
            $this->setModel($item);
0 ignored issues
show
Documentation introduced by
$item is of type object<Charcoal\Source\StorableInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
459
        }
460
        $model = $this->model();
461
462
        $tableStructure = array_keys($this->tableStructure());
463
464
        $fields = $this->getModelFields($model);
465
466
        $keys   = [];
467
        $values = [];
468
        $binds  = [];
469
        $binds_types = [];
470
        foreach ($fields as $f) {
471
            $k = $f->ident();
472
            if (in_array($k, $tableStructure)) {
473
                $keys[]    = '`'.$k.'`';
474
                $values[]  = ':'.$k.'';
475
                $binds[$k] = $f->val();
476
                $binds_types[$k] = $f->sqlPdoType();
477
            }
478
        }
479
480
        $q = '
481
            INSERT
482
                INTO
483
            `'.$this->table().'`
484
                ('.implode(', ', $keys).')
485
            VALUES
486
                ('.implode(', ', $values).')';
487
488
        $res = $this->dbQuery($q, $binds, $binds_types);
489
490
        if ($res === false) {
491
            throw new PDOException('Could not save item.');
492
        } else {
493
            if ($model->id()) {
494
                return $model->id();
495
            } else {
496
                return $this->db()->lastInsertId();
497
            }
498
        }
499
    }
500
501
    /**
502
     * Update an item in storage.
503
     *
504
     * @param StorableInterface $item       The object to update.
505
     * @param array             $properties The list of properties to update, if not all.
506
     * @return boolean Success / Failure
507
     */
508
    public function updateItem(StorableInterface $item, array $properties = null)
509
    {
510
        if ($item !== null) {
511
            $this->setModel($item);
0 ignored issues
show
Documentation introduced by
$item is of type object<Charcoal\Source\StorableInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
512
        }
513
        $model = $this->model();
514
515
        $tableStructure = array_keys($this->tableStructure());
516
        $fields = $this->getModelFields($model, $properties);
517
518
        $updates = [];
519
        $binds   = [];
520
        $binds_types = [];
521
        foreach ($fields as $f) {
522
            $k = $f->ident();
523
            if (in_array($k, $tableStructure)) {
524
                if ($k !== $model->key()) {
525
                    $updates[] = '`'.$k.'` = :'.$k;
526
                }
527
                $binds[$k] = $f->val();
528
                $binds_types[$k] = $f->sqlPdoType();
529
            } else {
530
                $this->logger->debug(
531
                    sprintf('Field %s not in table structure', $k)
532
                );
533
            }
534
        }
535
        if (empty($updates)) {
536
            $this->logger->warning('Could not update items. No valid fields were set / available in database table.', [
537
                'properties'    => $properties,
538
                'structure'     => $tableStructure
539
            ]);
540
            return false;
541
        }
542
543
        $binds[$model->key()] = $model->id();
544
        $binds_types[$model->key()] = PDO::PARAM_STR;
545
546
        $q = '
547
            UPDATE
548
                `'.$this->table().'`
549
            SET
550
                '.implode(", \n\t", $updates).'
551
            WHERE
552
                `'.$model->key().'`=:'.$model->key().'';
553
554
        $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
555
        if ($dbDriver == 'mysql') {
556
            $q .= "\n".'LIMIT 1';
557
        }
558
559
        $res = $this->dbQuery($q, $binds, $binds_types);
560
561
        if ($res === false) {
562
            return false;
563
        } else {
564
            return true;
565
        }
566
    }
567
568
    /**
569
     * Delete an item from storage
570
     *
571
     * @param  StorableInterface $item Optional item to delete. If none, the current model object will be used.
572
     * @throws UnexpectedValueException If the item does not have an ID.
573
     * @return boolean Success / Failure
574
     */
575
    public function deleteItem(StorableInterface $item = null)
576
    {
577
        if ($item !== null) {
578
            $this->setModel($item);
0 ignored issues
show
Documentation introduced by
$item is of type object<Charcoal\Source\StorableInterface>, but the function expects a object<Charcoal\Model\ModelInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
579
        }
580
581
        $model = $this->model();
582
583
        if (!$model->id()) {
584
            throw new UnexpectedValueException(
585
                sprintf('Can not delete "%s" item. No ID.', get_class($this))
586
            );
587
        }
588
589
        $q = '
590
            DELETE FROM
591
                `'.$this->table().'`
592
            WHERE
593
                `'.$model->key().'` = :id';
594
595
        $dbDriver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
596
        if ($dbDriver == 'mysql') {
597
            $q .= "\n".'LIMIT 1';
598
        }
599
600
        $binds = [
601
            'id' => $model->id()
602
        ];
603
604
        $res = $this->dbQuery($q, $binds);
605
606
        if ($res === false) {
607
            return false;
608
        } else {
609
            return true;
610
        }
611
    }
612
613
    /**
614
     * Execute a SQL query, with PDO, and returns the PDOStatement.
615
     *
616
     * If the query fails, this method will return false.
617
     *
618
     * @param string $q           The SQL query to executed.
619
     * @param array  $binds       Optional. Query parameter binds.
620
     * @param array  $binds_types Optional. Types of parameter bindings.
621
     * @return PDOStatement|false The PDOStatement, or false in case of error
622
     */
623
    public function dbQuery($q, array $binds = [], array $binds_types = [])
624
    {
625
        $this->logger->debug($q, $binds);
626
        $sth = $this->db()->prepare($q);
627
        if (!$sth) {
628
            return false;
629
        }
630
        if (!empty($binds)) {
631
            foreach ($binds as $k => $v) {
632
                if ($binds[$k] === null) {
633
                    $binds_types[$k] = PDO::PARAM_NULL;
634
                } elseif (!is_scalar($binds[$k])) {
635
                    $binds[$k] = json_encode($binds[$k]);
636
                }
637
                $type = (isset($binds_types[$k]) ? $binds_types[$k] : PDO::PARAM_STR);
638
                $sth->bindParam(':'.$k, $binds[$k], $type);
639
            }
640
        }
641
642
        $ret = $sth->execute();
643
        if ($ret === false) {
644
            return false;
645
        }
646
647
        return $sth;
648
    }
649
650
    /**
651
     * @throws UnexpectedValueException If the source does not have a table defined.
652
     * @return string
653
     */
654
    public function sqlLoad()
655
    {
656
        $table = $this->table();
657
        if (!$table) {
658
            throw new UnexpectedValueException(
659
                'Can not get SQL. No table defined.'
660
            );
661
        }
662
663
        $selects = $this->sqlSelect();
664
        $tables  = '`'.$table.'` AS objTable';
665
        $filters = $this->sqlFilters();
666
        $orders  = $this->sqlOrders();
667
        $limits  = $this->sqlPagination();
668
669
        $q = 'SELECT '.$selects.' FROM '.$tables.$filters.$orders.$limits;
670
        return $q;
671
    }
672
673
    /**
674
     * Get a special SQL query for loading the count.
675
     *
676
     * @throws UnexpectedValueException If the source does not have a table defined.
677
     * @return string
678
     */
679
    public function sqlLoadCount()
680
    {
681
        $table = $this->table();
682
        if (!$table) {
683
            throw new UnexpectedValueException(
684
                'Can not get SQL count. No table defined.'
685
            );
686
        }
687
688
        $tables = '`'.$table.'` AS objTable';
689
        $filters = $this->sqlFilters();
690
        $q = 'SELECT COUNT(*) FROM '.$tables.$filters;
691
        return $q;
692
    }
693
694
    /**
695
     * @return string
696
     */
697
    public function sqlSelect()
698
    {
699
        $properties = $this->properties();
700
        if (empty($properties)) {
701
            return 'objTable.*';
702
        }
703
704
        $sql = '';
705
        $propsSql = [];
706
        foreach ($properties as $p) {
707
            $propsSql[] = 'objTable.`'.$p.'`';
708
        }
709
        if (!empty($propsSql)) {
710
            $sql = implode(', ', $propsSql);
711
        }
712
713
        return $sql;
714
    }
715
716
    /**
717
     * @return string
718
     * @todo 2016-02-19 Use bindings for filters value
719
     */
720
    public function sqlFilters()
721
    {
722
        $sql = '';
723
724
        $filters = $this->filters();
725
        if (empty($filters)) {
726
            return '';
727
        }
728
729
        // Process filters
730
        $filtersSql = [];
731
        foreach ($filters as $f) {
732
            $fSql = $f->sql();
733
            if ($fSql) {
734
                $filtersSql[] = [
735
                    'sql'     => $f->sql(),
736
                    'operand' => $f->operand()
737
                ];
738
            }
739
        }
740
        if (empty($filtersSql)) {
741
            return '';
742
        }
743
744
        $sql .= ' WHERE';
745
        $i = 0;
746
747
        foreach ($filtersSql as $f) {
748
            if ($i > 0) {
749
                $sql .= ' '.$f['operand'];
750
            }
751
            $sql .= ' '.$f['sql'];
752
            $i++;
753
        }
754
        return $sql;
755
    }
756
757
    /**
758
     * @return string
759
     */
760
    public function sqlOrders()
761
    {
762
        $sql = '';
763
764
        if (!empty($this->orders)) {
765
            $ordersSql = [];
766
            foreach ($this->orders as $o) {
767
                $ordersSql[] = $o->sql();
768
            }
769
            if (!empty($ordersSql)) {
770
                $sql = ' ORDER BY '.implode(', ', $ordersSql);
771
            }
772
        }
773
774
        return $sql;
775
    }
776
777
    /**
778
     * @return string
779
     */
780
    public function sqlPagination()
781
    {
782
        return $this->pagination()->sql();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\PaginationInterface as the method sql() does only exist in the following implementations of said interface: Charcoal\Source\Database\DatabasePagination.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
783
    }
784
785
    /**
786
     * @return FilterInterface
787
     */
788
    protected function createFilter()
789
    {
790
        $filter = new DatabaseFilter();
791
        return $filter;
792
    }
793
794
    /**
795
     * @return OrderInterface
796
     */
797
    protected function createOrder()
798
    {
799
        $order = new DatabaseOrder();
800
        return $order;
801
    }
802
803
    /**
804
     * @return PaginationInterface
805
     */
806
    protected function createPagination()
807
    {
808
        $pagination = new DatabasePagination();
809
        return $pagination;
810
    }
811
812
    /**
813
     * ConfigurableTrait > createConfig()
814
     *
815
     * Overrides the method defined in AbstractSource to returns a `DatabaseSourceConfig` object.
816
     *
817
     * @param array $data Optional.
818
     * @return DatabaseSourceConfig
819
     */
820
    public function createConfig(array $data = null)
821
    {
822
        $config = new DatabaseSourceConfig($data);
823
        return $config;
824
    }
825
}
826