Passed
Branch master (018ba4)
by Mathieu
06:43
created

DatabaseSource   F

Complexity

Total Complexity 114

Size/Duplication

Total Lines 901
Duplicated Lines 4.88 %

Importance

Changes 0
Metric Value
dl 44
loc 901
rs 1.263
c 0
b 0
f 0
wmc 114

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A sqlLoad() 0 16 2
B updateItem() 0 61 8
B tableStructure() 5 30 6
A hasTable() 0 3 1
A sqlLoadCount() 0 13 2
A createFilter() 0 7 2
A loadItems() 0 8 2
A createConfig() 0 4 1
A createPagination() 0 7 2
A createOrder() 0 7 2
A sqlSelect() 0 21 4
A tableIsEmpty() 0 7 1
B createTable() 0 38 5
B loadItemFromQuery() 6 25 5
C dbQuery() 0 27 8
C getModelFields() 0 24 7
C alterTable() 0 47 11
B loadItemsFromQuery() 0 30 4
C saveItem() 0 46 7
A sqlPagination() 0 13 3
A loadItem() 0 5 1
A sqlFrom() 0 10 2
B setTable() 22 24 3
A table() 0 8 2
A db() 0 8 2
A tableExists() 5 16 2
A sqlFilters() 0 16 3
B deleteItem() 0 37 5
B loadItemFromKey() 6 30 4
B sqlOrders() 0 23 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DatabaseSource often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DatabaseSource, and based on these observations, apply Extract Interface, too.

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-property'
12
use Charcoal\Property\PropertyField;
13
14
// From 'charcoal-core'
15
use Charcoal\Model\ModelInterface;
16
use Charcoal\Source\AbstractSource;
17
use Charcoal\Source\DatabaseSourceConfig;
18
use Charcoal\Source\DatabaseSourceInterface;
19
use Charcoal\Source\Database\DatabaseFilter;
20
use Charcoal\Source\Database\DatabaseOrder;
21
use Charcoal\Source\Database\DatabasePagination;
22
use Charcoal\Source\Expression;
23
24
/**
25
 * Database Source Handler, through PDO.
26
 */
27
class DatabaseSource extends AbstractSource implements
28
    DatabaseSourceInterface
29
{
30
    const DEFAULT_DB_HOSTNAME = 'localhost';
31
    const DEFAULT_TABLE_ALIAS = 'objTable';
32
    const MYSQL_DRIVER_NAME   = 'mysql';
33
    const SQLITE_DRIVER_NAME  = 'sqlite';
34
35
    /**
36
     * The database connector.
37
     *
38
     * @var PDO
39
     */
40
    private $pdo;
41
42
    /**
43
     * The {@see self::$model}'s table name.
44
     *
45
     * @var string
46
     */
47
    private $table;
48
49
    /**
50
     * Create a new database handler.
51
     *
52
     * @param array $data Class dependencies.
53
     */
54
    public function __construct(array $data)
55
    {
56
        $this->pdo = $data['pdo'];
57
58
        parent::__construct($data);
59
    }
60
61
    /**
62
     * Retrieve the database connector.
63
     *
64
     * @throws RuntimeException If the datahase was not set.
65
     * @return PDO
66
     */
67
    public function db()
68
    {
69
        if ($this->pdo === null) {
70
            throw new RuntimeException(
71
                'Database Connector was not set.'
72
            );
73
        }
74
        return $this->pdo;
75
    }
76
77
    /**
78
     * Set the database's table to use.
79
     *
80
     * @param  string $table The source table.
81
     * @throws InvalidArgumentException If argument is not a string or alphanumeric/underscore.
82
     * @return self
83
     */
84 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...
85
    {
86
        if (!is_string($table)) {
87
            throw new InvalidArgumentException(sprintf(
88
                'DatabaseSource::setTable() expects a string as table. (%s given). [%s]',
89
                gettype($table),
90
                get_class($this->model())
91
            ));
92
        }
93
94
        /**
95
         * For security reason, only alphanumeric characters (+ underscores)
96
         * are valid table names; Although SQL can support more,
97
         * there's really no reason to.
98
         */
99
        if (!preg_match('/[A-Za-z0-9_]/', $table)) {
100
            throw new InvalidArgumentException(sprintf(
101
                'Table name "%s" is invalid: must be alphanumeric / underscore.',
102
                $table
103
            ));
104
        }
105
106
        $this->table = $table;
107
        return $this;
108
    }
109
110
    /**
111
     * Determine if a table is assigned.
112
     *
113
     * @return boolean
114
     */
115
    public function hasTable()
116
    {
117
        return !empty($this->table);
118
    }
119
120
    /**
121
     * Get the database's current table.
122
     *
123
     * @throws RuntimeException If the table was not set.
124
     * @return string
125
     */
126
    public function table()
127
    {
128
        if ($this->table === null) {
129
            throw new RuntimeException(
130
                'Table was not set.'
131
            );
132
        }
133
        return $this->table;
134
    }
135
136
    /**
137
     * Create a table from a model's metadata.
138
     *
139
     * @return boolean TRUE if the table was created, otherwise FALSE.
140
     */
141
    public function createTable()
142
    {
143
        if ($this->tableExists() === true) {
144
            return true;
145
        }
146
147
        $dbh      = $this->db();
148
        $driver   = $dbh->getAttribute(PDO::ATTR_DRIVER_NAME);
149
        $model    = $this->model();
150
        $metadata = $model->metadata();
151
152
        $table   = $this->table();
153
        $fields  = $this->getModelFields($model);
154
        $columns = [];
155
        foreach ($fields as $field) {
156
            $columns[] = $field->sql();
157
        }
158
159
        $query  = 'CREATE TABLE  `'.$table.'` ('."\n";
160
        $query .= implode(',', $columns);
161
162
        $key = $model->key();
163
        if ($key) {
164
            $query .= ', PRIMARY KEY (`'.$key.'`) '."\n";
165
        }
166
167
        /** @todo Add indexes for all defined list constraints (yea... tough job...) */
168
        if ($driver === self::MYSQL_DRIVER_NAME) {
169
            $engine = 'InnoDB';
170
            $query .= ') ENGINE='.$engine.' DEFAULT CHARSET=utf8 COMMENT="'.addslashes($metadata['name']).'";';
171
        } else {
172
            $query .= ');';
173
        }
174
175
        $this->logger->debug($query);
176
        $dbh->query($query);
177
178
        return true;
179
    }
180
181
    /**
182
     * Alter an existing table to match the model's metadata.
183
     *
184
     * @return boolean TRUE if the table was altered, otherwise FALSE.
185
     */
186
    public function alterTable()
187
    {
188
        if ($this->tableExists() === false) {
189
            return false;
190
        }
191
192
        $dbh    = $this->db();
193
        $table  = $this->table();
194
        $fields = $this->getModelFields($this->model());
195
        $cols   = $this->tableStructure();
196
        foreach ($fields as $field) {
197
            $ident = $field->ident();
198
199
            if (!array_key_exists($ident, $cols)) {
200
                // The key does not exist at all.
201
                $query = 'ALTER TABLE `'.$table.'` ADD '.$field->sql();
202
                $this->logger->debug($query);
203
                $dbh->query($query);
204
            } else {
205
                // The key exists. Validate.
206
                $col   = $cols[$ident];
207
                $alter = true;
208
                if (strtolower($col['Type']) !== strtolower($field->sqlType())) {
209
                    $alter = true;
210
                }
211
212
                if ((strtolower($col['Null']) === 'no') && !$field->allowNull()) {
213
                    $alter = true;
214
                }
215
216
                if ((strtolower($col['Null']) !== 'no') && $field->allowNull()) {
217
                    $alter = true;
218
                }
219
220
                if ($col['Default'] !== $field->defaultVal()) {
221
                    $alter = true;
222
                }
223
224
                if ($alter === true) {
225
                    $query = 'ALTER TABLE `'.$table.'` CHANGE `'.$ident.'` '.$field->sql();
226
                    $this->logger->debug($query);
227
                    $dbh->query($query);
228
                }
229
            }
230
        }
231
232
        return true;
233
    }
234
235
    /**
236
     * Determine if the source table exists.
237
     *
238
     * @return boolean TRUE if the table exists, otherwise FALSE.
239
     */
240
    public function tableExists()
241
    {
242
        $dbh    = $this->db();
243
        $table  = $this->table();
244
        $driver = $dbh->getAttribute(PDO::ATTR_DRIVER_NAME);
245 View Code Duplication
        if ($driver === self::SQLITE_DRIVER_NAME) {
246
            $query = 'SELECT name FROM sqlite_master WHERE type = "table" AND name = "'.$table.'";';
247
        } else {
248
            $query = 'SHOW TABLES LIKE "'.$table.'"';
249
        }
250
251
        $this->logger->debug($query);
252
        $sth    = $dbh->query($query);
253
        $exists = $sth->fetchColumn(0);
254
255
        return !!$exists;
256
    }
257
258
    /**
259
     * Get the table columns information.
260
     *
261
     * @return array An associative array.
262
     */
263
    public function tableStructure()
264
    {
265
        $dbh    = $this->db();
266
        $table  = $this->table();
267
        $driver = $dbh->getAttribute(PDO::ATTR_DRIVER_NAME);
268 View Code Duplication
        if ($driver === self::SQLITE_DRIVER_NAME) {
269
            $query = 'PRAGMA table_info("'.$table.'") ';
270
        } else {
271
            $query = 'SHOW COLUMNS FROM `'.$table.'`';
272
        }
273
274
        $this->logger->debug($query);
275
        $sth = $dbh->query($query);
276
277
        $cols = $sth->fetchAll((PDO::FETCH_GROUP | PDO::FETCH_UNIQUE | PDO::FETCH_ASSOC));
278
        if ($driver === self::SQLITE_DRIVER_NAME) {
279
            $struct = [];
280
            foreach ($cols as $col) {
281
                // Normalize SQLite's result (PRAGMA) with mysql's (SHOW COLUMNS)
282
                $struct[$col['name']] = [
283
                    'Type'      => $col['type'],
284
                    'Null'      => !!$col['notnull'] ? 'NO' : 'YES',
285
                    'Default'   => $col['dflt_value'],
286
                    'Key'       => !!$col['pk'] ? 'PRI' : '',
287
                    'Extra'     => ''
288
                ];
289
            }
290
            return $struct;
291
        } else {
292
            return $cols;
293
        }
294
    }
295
296
    /**
297
     * Determine if the source table is empty or not.
298
     *
299
     * @return boolean TRUE if the table has no data, otherwise FALSE.
300
     */
301
    public function tableIsEmpty()
302
    {
303
        $table = $this->table();
304
        $query = 'SELECT NULL FROM `'.$table.'` LIMIT 1';
305
        $this->logger->debug($query);
306
        $sth = $this->db()->query($query);
307
        return ($sth->rowCount() === 0);
308
    }
309
310
    /**
311
     * Retrieve all fields from a model.
312
     *
313
     * @todo   Move this method in StorableTrait or AbstractModel
314
     * @param  ModelInterface $model      The model to get fields from.
315
     * @param  array|null     $properties Optional list of properties to get.
316
     *     If NULL, retrieve all (from metadata).
317
     * @return PropertyField[]
318
     */
319
    private function getModelFields(ModelInterface $model, $properties = null)
320
    {
321
        if ($properties === null) {
322
            // No custom properties; use all (from model metadata)
323
            $properties = array_keys($model->metadata()->properties());
324
        } else {
325
            // Ensure the key is always in the required fields.
326
            $properties = array_unique(array_merge([ $model->key() ], $properties));
327
        }
328
329
        $fields = [];
330
        foreach ($properties as $propertyIdent) {
331
            $prop = $model->property($propertyIdent);
332
            if (!$prop || !$prop->active() || !$prop->storable()) {
333
                continue;
334
            }
335
336
            $val = $model->propertyValue($propertyIdent);
337
            foreach ($prop->fields($val) as $fieldIdent => $field) {
338
                $fields[$field->ident()] = $field;
339
            }
340
        }
341
342
        return $fields;
343
    }
344
345
    /**
346
     * Load item by the primary column.
347
     *
348
     * @param  mixed             $ident Ident can be any scalar value.
349
     * @param  StorableInterface $item  Optional item to load into.
350
     * @return StorableInterface
351
     */
352
    public function loadItem($ident, StorableInterface $item = null)
353
    {
354
        $key = $this->model()->key();
355
356
        return $this->loadItemFromKey($key, $ident, $item);
357
    }
358
359
    /**
360
     * Load item by the given column.
361
     *
362
     * @param  string                 $key   Column name.
363
     * @param  mixed                  $ident Value of said column.
364
     * @param  StorableInterface|null $item  Optional. Item (storable object) to load into.
365
     * @throws Exception If the query fails.
366
     * @return StorableInterface
367
     */
368
    public function loadItemFromKey($key, $ident, StorableInterface $item = null)
369
    {
370 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...
371
            $this->setModel($item);
372
        } else {
373
            $class = get_class($this->model());
374
            $item  = new $class;
375
        }
376
377
        // Missing parameters
378
        if (!$key || !$ident) {
379
            return $item;
380
        }
381
382
        $table = $this->table();
383
        $query = '
384
            SELECT
385
                *
386
            FROM
387
               `'.$table.'`
388
            WHERE
389
               `'.$key.'` = :ident
390
            LIMIT
391
               1';
392
393
        $binds = [
394
            'ident' => $ident
395
        ];
396
397
        return $this->loadItemFromQuery($query, $binds, $item);
398
    }
399
400
    /**
401
     * Load item by the given query statement.
402
     *
403
     * @param  string            $query The SQL SELECT statement.
404
     * @param  array             $binds Optional. The query parameters.
405
     * @param  StorableInterface $item  Optional. Item (storable object) to load into.
406
     * @throws PDOException If there is a query error.
407
     * @return StorableInterface
408
     */
409
    public function loadItemFromQuery($query, array $binds = [], StorableInterface $item = null)
410
    {
411 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...
412
            $this->setModel($item);
413
        } else {
414
            $class = get_class($this->model());
415
            $item = new $class;
416
        }
417
418
        // Missing parameters
419
        if (!$query) {
420
            return $item;
421
        }
422
423
        $sth = $this->dbQuery($query, $binds);
424
        if ($sth === false) {
425
            throw new PDOException('Could not load item.');
426
        }
427
428
        $data = $sth->fetch(PDO::FETCH_ASSOC);
429
        if ($data) {
430
            $item->setFlatData($data);
431
        }
432
433
        return $item;
434
    }
435
436
    /**
437
     * Load items for the given model.
438
     *
439
     * @param  StorableInterface|null $item Optional model.
440
     * @return StorableInterface[]
441
     */
442
    public function loadItems(StorableInterface $item = null)
443
    {
444
        if ($item !== null) {
445
            $this->setModel($item);
446
        }
447
448
        $query = $this->sqlLoad();
449
        return $this->loadItemsFromQuery($query, [], $item);
450
    }
451
452
    /**
453
     * Load items for the given query statement.
454
     *
455
     * @param  string                 $query The SQL SELECT statement.
456
     * @param  array                  $binds This has to be done.
457
     * @param  StorableInterface|null $item  Model Item.
458
     * @return StorableInterface[]
459
     */
460
    public function loadItemsFromQuery($query, array $binds = [], StorableInterface $item = null)
461
    {
462
        if ($item !== null) {
463
            $this->setModel($item);
464
        }
465
466
        $items = [];
467
468
        $model = $this->model();
469
        $dbh   = $this->db();
470
471
        $this->logger->debug($query);
472
        $sth = $dbh->prepare($query);
473
474
        // @todo Binds
475
        if (!empty($binds)) {
476
            unset($binds);
477
        }
478
479
        $sth->execute();
480
        $sth->setFetchMode(PDO::FETCH_ASSOC);
481
482
        $className = get_class($model);
483
        while ($objData = $sth->fetch()) {
484
            $obj = new $className;
485
            $obj->setFlatData($objData);
486
            $items[] = $obj;
487
        }
488
489
        return $items;
490
    }
491
492
    /**
493
     * Save an item (create a new row) in storage.
494
     *
495
     * @param  StorableInterface $item The object to save.
496
     * @throws PDOException If a database error occurs.
497
     * @return mixed The created item ID, otherwise FALSE.
498
     */
499
    public function saveItem(StorableInterface $item)
500
    {
501
        if ($this->tableExists() === false) {
502
            /** @todo Optionnally turn off for some models */
503
            $this->createTable();
504
        }
505
506
        if ($item !== null) {
507
            $this->setModel($item);
508
        }
509
        $model  = $this->model();
510
        $table  = $this->table();
511
        $struct = array_keys($this->tableStructure());
512
        $fields = $this->getModelFields($model);
513
514
        $keys   = [];
515
        $values = [];
516
        $binds  = [];
517
        $types  = [];
518
        foreach ($fields as $field) {
519
            $key = $field->ident();
520
            if (in_array($key, $struct)) {
521
                $keys[]      = '`'.$key.'`';
522
                $values[]    = ':'.$key.'';
523
                $binds[$key] = $field->val();
524
                $types[$key] = $field->sqlPdoType();
525
            }
526
        }
527
528
        $query = '
529
            INSERT
530
                INTO
531
            `'.$table.'`
532
                ('.implode(', ', $keys).')
533
            VALUES
534
                ('.implode(', ', $values).')';
535
536
        $result = $this->dbQuery($query, $binds, $types);
537
538
        if ($result === false) {
539
            throw new PDOException('Could not save item.');
540
        } else {
541
            if ($model->id()) {
542
                return $model->id();
543
            } else {
544
                return $this->db()->lastInsertId();
545
            }
546
        }
547
    }
548
549
    /**
550
     * Update an item in storage.
551
     *
552
     * @param  StorableInterface $item       The object to update.
553
     * @param  array             $properties The list of properties to update, if not all.
554
     * @return boolean TRUE if the item was updated, otherwise FALSE.
555
     */
556
    public function updateItem(StorableInterface $item, array $properties = null)
557
    {
558
        if ($item !== null) {
559
            $this->setModel($item);
560
        }
561
        $model  = $this->model();
562
        $table  = $this->table();
563
        $struct = array_keys($this->tableStructure());
564
        $fields = $this->getModelFields($model, $properties);
565
566
        $updates = [];
567
        $binds   = [];
568
        $types   = [];
569
        foreach ($fields as $field) {
570
            $key = $field->ident();
571
            if (in_array($key, $struct)) {
572
                if ($key !== $model->key()) {
573
                    $param = ':'.$key;
574
                    $updates[] = '`'.$key.'` = '.$param;
575
                }
576
                $binds[$key] = $field->val();
577
                $types[$key] = $field->sqlPdoType();
578
            } else {
579
                $this->logger->debug(
580
                    sprintf('Field "%s" not in table structure', $key)
581
                );
582
            }
583
        }
584
        if (empty($updates)) {
585
            $this->logger->warning(
586
                'Could not update items. No valid fields were set / available in database table.',
587
                [
588
                    'properties' => $properties,
589
                    'structure'  => $struct
590
                ]
591
            );
592
            return false;
593
        }
594
595
        $binds[$model->key()] = $model->id();
596
        $types[$model->key()] = PDO::PARAM_STR;
597
598
        $query = '
599
            UPDATE
600
                `'.$table.'`
601
            SET
602
                '.implode(", \n\t", $updates).'
603
            WHERE
604
                `'.$model->key().'`=:'.$model->key().'';
605
606
        $driver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
607
        if ($driver == self::MYSQL_DRIVER_NAME) {
608
            $query .= "\n".'LIMIT 1';
609
        }
610
611
        $result = $this->dbQuery($query, $binds, $types);
612
613
        if ($result === false) {
614
            return false;
615
        } else {
616
            return true;
617
        }
618
    }
619
620
    /**
621
     * Delete an item from storage.
622
     *
623
     * @param  StorableInterface $item Optional item to delete. If none, the current model object will be used.
624
     * @throws UnexpectedValueException If the item does not have an ID.
625
     * @return boolean TRUE if the item was deleted, otherwise FALSE.
626
     */
627
    public function deleteItem(StorableInterface $item = null)
628
    {
629
        if ($item !== null) {
630
            $this->setModel($item);
631
        }
632
633
        $model = $this->model();
634
635
        if (!$model->id()) {
636
            throw new UnexpectedValueException(
637
                sprintf('Can not delete "%s" item. No ID.', get_class($this))
638
            );
639
        }
640
641
        $key   = $model->key();
642
        $table = $this->table();
643
        $query = '
644
            DELETE FROM
645
                `'.$table.'`
646
            WHERE
647
                `'.$key.'` = :id';
648
649
        $driver = $this->db()->getAttribute(PDO::ATTR_DRIVER_NAME);
650
        if ($driver == self::MYSQL_DRIVER_NAME) {
651
            $query .= "\n".'LIMIT 1';
652
        }
653
654
        $binds = [
655
            'id' => $model->id()
656
        ];
657
658
        $result = $this->dbQuery($query, $binds);
659
660
        if ($result === false) {
661
            return false;
662
        } else {
663
            return true;
664
        }
665
    }
666
667
    /**
668
     * Execute a SQL query, with PDO, and returns the PDOStatement.
669
     *
670
     * If the query fails, this method will return false.
671
     *
672
     * @param  string $query The SQL query to executed.
673
     * @param  array  $binds Optional. Query parameter binds.
674
     * @param  array  $types Optional. Types of parameter bindings.
675
     * @return PDOStatement|false The PDOStatement, otherwise FALSE.
0 ignored issues
show
Bug introduced by
The type Charcoal\Source\PDOStatement was not found. Did you mean PDOStatement? If so, make sure to prefix the type with \.
Loading history...
676
     */
677
    public function dbQuery($query, array $binds = [], array $types = [])
678
    {
679
        $this->logger->debug($query, $binds);
680
        $sth = $this->db()->prepare($query);
681
        if (!$sth) {
682
            return false;
683
        }
684
685
        if (!empty($binds)) {
686
            foreach ($binds as $key => $val) {
687
                if ($binds[$key] === null) {
688
                    $types[$key] = PDO::PARAM_NULL;
689
                } elseif (!is_scalar($binds[$key])) {
690
                    $binds[$key] = json_encode($binds[$key]);
691
                }
692
                $type  = (isset($types[$key]) ? $types[$key] : PDO::PARAM_STR);
693
                $param = ':'.$key;
694
                $sth->bindParam($param, $binds[$key], $type);
695
            }
696
        }
697
698
        $result = $sth->execute();
699
        if ($result === false) {
700
            return false;
701
        }
702
703
        return $sth;
704
    }
705
706
    /**
707
     * Compile the SELECT statement for fetching one or more objects.
708
     *
709
     * @throws UnexpectedValueException If the source does not have a table defined.
710
     * @return string
711
     */
712
    public function sqlLoad()
713
    {
714
        if (!$this->hasTable()) {
715
            throw new UnexpectedValueException(
716
                'Can not get SQL SELECT clause. No table defined.'
717
            );
718
        }
719
720
        $selects = $this->sqlSelect();
721
        $tables  = $this->sqlFrom();
722
        $filters = $this->sqlFilters();
723
        $orders  = $this->sqlOrders();
724
        $limits  = $this->sqlPagination();
725
726
        $query = 'SELECT '.$selects.' FROM '.$tables.$filters.$orders.$limits;
727
        return $query;
728
    }
729
730
    /**
731
     * Compile the SELECT statement for fetching the number of objects.
732
     *
733
     * @throws UnexpectedValueException If the source does not have a table defined.
734
     * @return string
735
     */
736
    public function sqlLoadCount()
737
    {
738
        if (!$this->hasTable()) {
739
            throw new UnexpectedValueException(
740
                'Can not get SQL count. No table defined.'
741
            );
742
        }
743
744
        $tables  = $this->sqlFrom();
745
        $filters = $this->sqlFilters();
746
747
        $query = 'SELECT COUNT(*) FROM '.$tables.$filters;
748
        return $query;
749
    }
750
751
    /**
752
     * Compile the SELECT clause.
753
     *
754
     * @throws UnexpectedValueException If the clause has no selectable fields.
755
     * @return string
756
     */
757
    public function sqlSelect()
758
    {
759
        $properties = $this->properties();
760
        if (empty($properties)) {
761
            return self::DEFAULT_TABLE_ALIAS.'.*';
762
        }
763
764
        $parts = [];
765
        foreach ($properties as $key) {
766
            $parts[] = Expression::quoteIdentifier($key, self::DEFAULT_TABLE_ALIAS);
767
        }
768
769
        if (empty($parts)) {
770
            throw new UnexpectedValueException(
771
                'Can not get SQL SELECT clause. No valid properties.'
772
            );
773
        }
774
775
        $clause = implode(', ', $parts);
776
777
        return $clause;
778
    }
779
780
    /**
781
     * Compile the FROM clause.
782
     *
783
     * @throws UnexpectedValueException If the source does not have a table defined.
784
     * @return string
785
     */
786
    public function sqlFrom()
787
    {
788
        if (!$this->hasTable()) {
789
            throw new UnexpectedValueException(
790
                'Can not get SQL FROM clause. No table defined.'
791
            );
792
        }
793
794
        $table = $this->table();
795
        return '`'.$table.'` AS `'.self::DEFAULT_TABLE_ALIAS.'`';
796
    }
797
798
    /**
799
     * Compile the WHERE clause.
800
     *
801
     * @todo   [2016-02-19] Use bindings for filters value
802
     * @return string
803
     */
804
    public function sqlFilters()
805
    {
806
        if (!$this->hasFilters()) {
807
            return '';
808
        }
809
810
        $criteria = $this->createFilter([
811
            'filters' => $this->filters()
812
        ]);
813
814
        $sql = $criteria->sql();
815
        if (strlen($sql) > 0) {
816
            $sql = ' WHERE '.$sql;
817
        }
818
819
        return $sql;
820
    }
821
822
    /**
823
     * Compile the ORDER BY clause.
824
     *
825
     * @return string
826
     */
827
    public function sqlOrders()
828
    {
829
        if (!$this->hasOrders()) {
830
            return '';
831
        }
832
833
        $parts = [];
834
        foreach ($this->orders() as $order) {
835
            if (!$order instanceof DatabaseOrder) {
836
                $order = $this->createOrder($order->data());
837
            }
838
839
            $sql = $order->sql();
840
            if (strlen($sql) > 0) {
841
                $parts[] = $sql;
842
            }
843
        }
844
845
        if (empty($parts)) {
846
            return '';
847
        }
848
849
        return ' ORDER BY '.implode(', ', $parts);
850
    }
851
852
    /**
853
     * Compile the LIMIT clause.
854
     *
855
     * @return string
856
     */
857
    public function sqlPagination()
858
    {
859
        $pager = $this->pagination();
860
        if (!$pager instanceof DatabasePagination) {
861
            $pager = $this->createPagination($pager->data());
0 ignored issues
show
Bug introduced by
The method data() does not exist on Charcoal\Source\PaginationInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Charcoal\Source\PaginationInterface. ( Ignorable by Annotation )

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

861
            $pager = $this->createPagination($pager->/** @scrutinizer ignore-call */ data());
Loading history...
862
        }
863
864
        $sql = $pager->sql();
865
        if (strlen($sql) > 0) {
866
            $sql = ' '.$sql;
867
        }
868
869
        return $sql;
870
    }
871
872
    /**
873
     * Create a new filter expression.
874
     *
875
     * @param  array $data Optional expression data.
876
     * @return DatabaseFilter
877
     */
878
    protected function createFilter(array $data = null)
879
    {
880
        $filter = new DatabaseFilter();
881
        if ($data !== null) {
882
            $filter->setData($data);
883
        }
884
        return $filter;
885
    }
886
887
    /**
888
     * Create a new order expression.
889
     *
890
     * @param  array $data Optional expression data.
891
     * @return DatabaseOrder
892
     */
893
    protected function createOrder(array $data = null)
894
    {
895
        $order = new DatabaseOrder();
896
        if ($data !== null) {
897
            $order->setData($data);
898
        }
899
        return $order;
900
    }
901
902
    /**
903
     * Create a new pagination clause.
904
     *
905
     * @param  array $data Optional clause data.
906
     * @return DatabasePagination
907
     */
908
    protected function createPagination(array $data = null)
909
    {
910
        $pagination = new DatabasePagination();
911
        if ($data !== null) {
912
            $pagination->setData($data);
913
        }
914
        return $pagination;
915
    }
916
917
    /**
918
     * Create a new database source config.
919
     *
920
     * @see    \Charcoal\Config\ConfigurableTrait
921
     * @param  array $data Optional data.
922
     * @return DatabaseSourceConfig
923
     */
924
    public function createConfig(array $data = null)
925
    {
926
        $config = new DatabaseSourceConfig($data);
927
        return $config;
928
    }
929
}
930