Completed
Push — master ( ac8697...846245 )
by Terry
03:03
created

FluentPdoModel::getPdo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 4
c 1
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * @package Terah\FluentPdoModel
4
 *
5
 * Original work Copyright (c) 2014 Mardix (http://github.com/mardix)
6
 * Modified work Copyright (c) 2015 Terry Cullen (http://github.com/terah)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
13
 */
14
namespace Terah\FluentPdoModel;
15
16
use Closure;
17
use PDOException;
18
use Exception;
19
use PDO;
20
use PDOStatement;
21
use stdClass;
22
use DateTime;
23
use Terah\FluentPdoModel\Drivers\AbstractPdo;
24
use Psr\Log\AbstractLogger;
25
use Terah\RedisCache\CacheInterface;
26
use function Terah\Assert\Assert;
27
use function Terah\Assert\Validate;
28
29
30
/**
31
 * Class FluentPdoModel
32
 *
33
 * @package Terah\FluentPdoModel
34
 * @author  Terry Cullen - [email protected]
35
 */
36
class FluentPdoModel
37
{
38
    const OPERATOR_AND              = ' AND ';
39
    const OPERATOR_OR               = ' OR ';
40
    const ORDERBY_ASC               = 'ASC';
41
    const ORDERBY_DESC              = 'DESC';
42
    const SAVE_INSERT               = 'INSERT';
43
    const SAVE_UPDATE               = 'UPDATE';
44
    const LEFT_JOIN                 = 'LEFT';
45
    const INNER_JOIN                = 'INNER';
46
    const ONE_DAY                   = 86400;
47
    const ONE_WEEK                  = 60480060;
48
    const ONE_HOUR                  = 3600;
49
    const TEN_MINS                  = 600;
50
    const CACHE_NO                  = -1;
51
    const CACHE_DEFAULT             = 0;
52
53
    /** @var AbstractPdo $_connection */
54
    protected $_connection          = null;
55
56
    /** @var string */
57
    protected $_primary_key         = 'id';
58
59
    /** @var array */
60
    protected $_where_parameters    = [];
61
62
    /** @var array */
63
    protected $_select_fields       = [];
64
65
    /** @var array */
66
    protected $_join_sources        = [];
67
68
    /** @var array */
69
    protected $_join_aliases        = [];
70
71
    /** @var array $_associations */
72
    protected $_associations        = [
73
        'belongsTo' => [],
74
    ];
75
76
    /** @var array */
77
    protected $_where_conditions    = [];
78
79
    protected $_raw_sql             = null;
80
81
    /** @var int */
82
    protected $_limit               = null;
83
84
    /** @var int */
85
    protected $_offset              = null;
86
87
    /** @var array */
88
    protected $_order_by            = [];
89
90
    /** @var array */
91
    protected $_group_by            = [];
92
93
    /** @var string */
94
    protected $_and_or_operator     = self::OPERATOR_AND;
95
96
    /** @var array */
97
    protected $_having              = [];
98
99
    /** @var bool */
100
    protected $_wrap_open           = false;
101
102
    /** @var int */
103
    protected $_last_wrap_position  = 0;
104
105
    /** @var PDOStatement $_pdo_stmt */
106
    protected $_pdo_stmt            = null;
107
108
    /** @var bool */
109
    protected $_distinct            = false;
110
111
    /** @var null */
112
    protected $_requested_fields    = [];
113
114
    /** @var null */
115
    protected $_filter_meta         = [];
116
117
    /** @var bool */
118
    protected $_log_queries         = false;
119
120
    /** @var array */
121
    protected $_timer               = [];
122
123
    /** @var int */
124
    protected $_slow_query_secs     = 5;
125
126
    /** @var array */
127
    protected $_pagination_attribs  = [
128
        '_limit',
129
        '_offset',
130
        '_order',
131
        '_fields',
132
        '_search'
133
    ];
134
135
    /** @var string $_table_name */
136
    protected $_table_name          = null;
137
138
    /** @var string $_table_alias */
139
    protected $_table_alias         = null;
140
141
    /** @var string $_display_column */
142
    protected $_display_column      = null;
143
144
    /** @var string $_connection_name */
145
    protected $_connection_name     = null;
146
147
    /** @var array $_schema */
148
    protected $_schema              = [];
149
150
    /** @var array $_virtual_fields */
151
    protected $_virtual_fields      = [];
152
153
    /** @var array $_errors */
154
    protected $_errors              = [];
155
156
    /**
157
     * @var int - true  = connection default x days
158
     *          - false = no cache
159
     *          - int   = a specific amount
160
     */
161
    protected $_cache_ttl               = self::CACHE_NO;
162
163
    /** @var string */
164
    protected $_tmp_table_prefix        = 'tmp_';
165
166
    /** @var null|string */
167
    protected $_built_query             = null;
168
169
    // Make handlers public so whe can delete them externally to reduce memory in hhmv
170
    //protected $_handlers              = [];
171
    public $_handlers                   = [];
172
173
    /** @var bool User want to directly specify the fields */
174
    protected $_explicit_select_mode    = false;
175
176
    /** @var string[] */
177
    protected $_update_raw              = [];
178
179
    protected $_max_callback_failures   = null;
180
181
    protected $_num_callback_failures   = 0;
182
183
    protected $_filter_on_fetch         = false;
184
185
    protected $_log_filter_changes      = true;
186
187
    protected $_include_count           = false;
188
189
    /**
190
     * @param AbstractPdo|null $connection
191
     */
192
    public function __construct(AbstractPdo $connection=null)
193
    {
194
        $connection = !is_null($connection) ? $connection : ConnectionPool::get($this->_connection_name);
195
        $this->_connection  = $connection;
196
        $this->_log_queries = $connection->logQueries();
197
        $this->init();
198
    }
199
200
    public function init()
201
    {}
202
203
    /**
204
     * @return AbstractPdo
205
     * @throws Exception
206
     */
207
    public function getPdo()
208
    {
209
        return $this->_connection;
210
    }
211
212
    /**
213
     * @return AbstractLogger
214
     */
215
    public function getLogger()
216
    {
217
        return $this->_connection->getLogger();
218
    }
219
220
    /**
221
     * @return CacheInterface
222
     */
223
    public function getCache()
224
    {
225
        return $this->_connection->getCache();
226
    }
227
228
    /**
229
     * Define the working table and create a new instance
230
     *
231
     * @param string $tableName - Table name
232
     * @param string $alias     - The table alias name
233
     * @param string $displayColumn
234
     * @param string $primaryKeyName
235
     *
236
     * @return $this
237
     */
238
    public function table($tableName, $alias='', $displayColumn='', $primaryKeyName='id')
239
    {
240
        return $this->reset()
241
            ->tableName($tableName)
242
            ->tableAlias($alias)
243
            ->displayColumn($displayColumn)
244
            ->primaryKeyName($primaryKeyName);
245
    }
246
247
    /**
248
     * @param string $primaryKeyName
249
     * @return $this
250
     */
251
    public function primaryKeyName($primaryKeyName)
252
    {
253
        $this->_primary_key = $primaryKeyName;
254
        return $this;
255
    }
256
257
    /**
258
     * @param string $tableName
259
     *
260
     * @return $this
261
     */
262
    public function tableName($tableName)
263
    {
264
        $this->_table_name  = $tableName;
265
        return $this;
266
    }
267
268
    /**
269
     * @param $explicitSelect
270
     *
271
     * @return $this
272
     */
273
    public function explicitSelectMode($explicitSelect=true)
274
    {
275
        $this->_explicit_select_mode  = (bool)$explicitSelect;
276
        return $this;
277
    }
278
279
    /**
280
     * @param bool $filterOnFetch
281
     *
282
     * @return $this
283
     */
284
    public function filterOnFetch($filterOnFetch=true)
285
    {
286
        $this->_filter_on_fetch  = (bool)$filterOnFetch;
287
        return $this;
288
    }
289
290
    /**
291
     * @param bool $logFilterChanges
292
     *
293
     * @return $this
294
     */
295
    public function logFilterChanges($logFilterChanges=true)
296
    {
297
        $this->_log_filter_changes  = (bool)$logFilterChanges;
298
        return $this;
299
    }
300
301
    /**
302
     * Return the name of the table
303
     *
304
     * @return string
305
     */
306
    public function getTableName()
307
    {
308
        return $this->_table_name;
309
    }
310
311
    /**
312
     * @return string
313
     */
314
    public function getDisplayColumn()
315
    {
316
        return $this->_display_column;
317
    }
318
319
    /**
320
     * Set the display column
321
     *
322
     * @param string $column
323
     *
324
     * @return $this
325
     */
326
    public function displayColumn($column)
327
    {
328
        $this->_display_column = $column;
329
        return $this;
330
    }
331
    /**
332
     * Set the table alias
333
     *
334
     * @param string $alias
335
     *
336
     * @return $this
337
     */
338
    public function tableAlias($alias)
339
    {
340
        $this->_table_alias = $alias;
341
        return $this;
342
    }
343
344
    /**
345
     * @param int $cacheTtl
346
     * @return $this
347
     * @throws Exception
348
     */
349
    protected function _cacheTtl($cacheTtl)
350
    {
351
        Assert($cacheTtl)->int('Cache ttl must be either -1 for no cache, 0 for default ttl or an integer for a custom ttl');
352
        if ( $cacheTtl !== self::CACHE_NO && ! is_null($this->_pdo_stmt) )
353
        {
354
            throw new Exception("You cannot cache pre-executed queries");
355
        }
356
        $this->_cache_ttl = $cacheTtl;
357
        return $this;
358
    }
359
360
    /**
361
     * @return string
362
     */
363
    public function getTableAlias()
364
    {
365
        return $this->_table_alias;
366
    }
367
368
    /**
369
     * @param array $associations
370
     *
371
     * @return $this
372
     */
373
    public function associations(array $associations)
374
    {
375
        $this->_associations = $associations;
376
        return $this;
377
    }
378
379
    /**
380
     * @param string $alias
381
     * @param array $definition
382
     * @return $this
383
     */
384
    public function setBelongsTo($alias, array $definition)
385
    {
386
        Assert($alias)->notEmpty();
387
        Assert($definition)->isArray()->count(4);
388
389
        $this->_associations['belongsTo'][$alias] = $definition;
390
        return $this;
391
    }
392
393
    public function setBelongsToDisplayField($alias, $displayField)
394
    {
395
        Assert($alias)->notEmpty();
396
        Assert($this->_associations['belongsTo'])->keyExists($alias);
397
        Assert($displayField)->notEmpty();
398
399
        $this->_associations['belongsTo'][$alias][2] = $displayField;
400
        return $this;
401
    }
402
403
    /**
404
     * @param PDOStatement $stmt
405
     *
406
     * @param PDOStatement $stmt
407
     * @param Closure $fnCallback
408
     * @return bool|stdClass
409
     */
410
    public function fetchRow(PDOStatement $stmt, Closure $fnCallback=null)
411
    {
412
        if ( ! ( $record = $stmt->fetch(PDO::FETCH_OBJ) ) )
413
        {
414
            return false;
415
        }
416
        $record = $this->onFetch($record);
417
        if ( empty($fnCallback) )
418
        {
419
            return $record;
420
        }
421
        $record = $fnCallback($record);
422
        if ( is_null($record) )
423
        {
424
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
425
        }
426
        unset($fnCallback);
427
        return $record;
428
    }
429
430
    /**
431
     * @param array $schema
432
     *
433
     * @return $this
434
     */
435
    public function schema(array $schema)
436
    {
437
        $this->_schema = $schema;
438
        return $this;
439
    }
440
441
    /**
442
     * @param string|array $field
443
     * @param $type
444
     * @return $this
445
     */
446
    public function addSchema($field, $type)
447
    {
448
        if ( is_array($field) )
449
        {
450
            foreach ( $field as $field_name => $type_def )
451
            {
452
                $this->addSchema($field_name, $type_def);
453
            }
454
            return $this;
455
        }
456
        $this->_schema[$field] = $type;
457
        return $this;
458
    }
459
460
    /**
461
     * @param $keys_only
462
     * @return array
463
     */
464
    public function getColumns($keys_only=true)
465
    {
466
        return $keys_only ? array_keys($this->_schema) : $this->_schema;
467
    }
468
469
    /**
470
     * Get the primary key name
471
     *
472
     * @return string
473
     */
474
    public function getPrimaryKeyName()
475
    {
476
        return $this->_formatKeyName($this->_primary_key, $this->_table_name);
477
    }
478
479
    /**
480
     * @param string $query
481
     * @param array $parameters
482
     *
483
     * @return bool
484
     * @throws Exception
485
     */
486
    public function execute($query, array $parameters=[])
487
    {
488
        list($this->_built_query, $ident)  = $this->_logQuery($query, $parameters);
489
        try
490
        {
491
            $this->_pdo_stmt        = $this->getPdo()->prepare($query);
492
            $result                 = $this->_pdo_stmt->execute($parameters);
493
            if ( false === $result )
494
            {
495
                $this->_pdo_stmt = null;
496
                throw new PDOException("The query failed to execute.");
497
            }
498
        }
499
        catch(Exception $e)
500
        {
501
            $built_query = $this->_built_query ? $this->_built_query : $this->buildQuery($query, $parameters);
502
            $this->getLogger()->error("FAILED: \n\n{$built_query}\n WITH ERROR:\n" . $e->getMessage());
503
            $this->_pdo_stmt = null;
504
            throw $e;
505
        }
506
        $this->_logSlowQueries($ident, $this->_built_query);
507
        return $result;
508
    }
509
510
    /**
511
     * @param string $query
512
     * @param array $params
513
     * @return $this
514
     */
515
    public function query($query, array $params=[])
516
    {
517
        $this->_raw_sql             = $query;
518
        $this->_where_parameters    = $params;
519
        return $this;
520
    }
521
522
    /**
523
     * @return bool
524
     */
525
    public function begin()
526
    {
527
        return $this->getPdo()->beginTransaction();
528
    }
529
530
    /**
531
     * @return bool
532
     */
533
    public function commit()
534
    {
535
        return $this->getPdo()->commit();
536
    }
537
538
    /**
539
     * @return bool
540
     */
541
    public function rollback()
542
    {
543
        $this->getLogger()->debug("Calling db transaction rollback.");
544
        return $this->getPdo()->rollback();
545
    }
546
547
548
    /**
549
     * @param string $sql
550
     * @param array $params
551
     *
552
     * @return string
553
     */
554
    public function buildQuery($sql, array $params=[])
555
    {
556
        $indexed = $params == array_values($params);
557
        if ( $indexed )
558
        {
559
            foreach ( $params as $key => $val )
560
            {
561
                $val    = is_string($val) ? "'{$val}'" : $val;
562
                $val    = is_null($val) ? 'NULL' : $val;
563
                $sql    = preg_replace('/\?/', $val, $sql, 1);
564
            }
565
            return $sql;
566
        }
567
568
        uksort($params, function ($a, $b) {
569
            return strlen($b) - strlen($a);
570
        });
571
        foreach ( $params as $key => $val )
572
        {
573
            $val    = is_string($val) ? "'{$val}'" : $val;
574
            $val    = is_null($val) ? 'NULL' : $val;
575
            $sql    = str_replace(":$key", $val, $sql);
576
            //$sql    = str_replace("$key", $val, $sql);
577
        }
578
        return $sql;
579
    }
580
581
    /**
582
     * @param stdClass $record
583
     *
584
     * @return stdClass
585
     */
586
    protected function _trimAndLowerCaseKeys(stdClass $record)
587
    {
588
        $fnTrimStrings = function($value) {
589
            return is_string($value) ? trim($value) : $value;
590
        };
591
        $record = array_map($fnTrimStrings, array_change_key_case((array)$record, CASE_LOWER));
592
        unset($fnTrimStrings);
593
        return (object)$record;
594
    }
595
596
    /**
597
     * Return the number of affected row by the last statement
598
     *
599
     * @return int
600
     */
601
    public function rowCount()
602
    {
603
        $stmt = $this->fetchStmt();
604
        return $stmt ? $stmt->rowCount() : 0;
605
    }
606
607
    /**
608
     * @return PDOStatement
609
     * @throws PDOException
610
     */
611
    public function fetchStmt()
612
    {
613
        if ( null === $this->_pdo_stmt )
614
        {
615
            $this->execute($this->getSelectQuery(), $this->_getWhereParameters());
616
        }
617
        return $this->_pdo_stmt;
618
    }
619
620
    /**
621
     * @param bool $build
622
     * @return array
623
     */
624
    public function fetchSqlQuery($build=false)
625
    {
626
        $clone  = clone $this;
627
        $query  = $clone->getSelectQuery();
628
        $params = $clone->_getWhereParameters();
629
        $result = $build ? $clone->buildQuery($query, $params) : [$query, $params];
630
        unset($clone->_handlers, $clone, $query, $params, $build);
631
        return $result;
632
    }
633
634
    /**
635
     * @param string $table_name
636
     * @param bool  $drop_if_exists
637
     * @param array $indexes
638
     * @return boolean
639
     * @throws Exception
640
     */
641
    public function fetchIntoMemoryTable($table_name, $drop_if_exists=true, array $indexes=[])
642
    {
643
        $table_name = preg_replace('/[^A-Za-z0-9_]+/', '', $table_name);
644
        $table_name = $this->_tmp_table_prefix . preg_replace('/^' . $this->_tmp_table_prefix . '/', '', $table_name);
645
        if ( $drop_if_exists )
646
        {
647
            $this->execute("DROP TABLE IF EXISTS {$table_name}");
648
        }
649
        $indexSql = [];
650
        foreach ( $indexes as $name => $column )
651
        {
652
            $indexSql[] = "INDEX {$name} ({$column})";
653
        }
654
        $indexSql = implode(", ", $indexSql);
655
        $indexSql = empty($indexSql) ? '' : "({$indexSql})";
656
        list($querySql, $params) = $this->fetchSqlQuery();
657
        $sql = <<<SQL
658
        CREATE TEMPORARY TABLE {$table_name} {$indexSql} ENGINE=MEMORY {$querySql}
659
SQL;
660
        return $this->execute($sql, $params);
661
    }
662
663
    /**
664
     * @param null $keyedOn
665
     * @param int $cacheTtl
666
     * @return \stdClass[]|null
667
     */
668
    public function fetch($keyedOn=null, $cacheTtl=self::CACHE_NO)
669
    {
670
        $this->_cacheTtl($cacheTtl);
671
        $fnCallback = function() use ($keyedOn) {
672
673
            $stmt   = $this->fetchStmt();
674
            $rows   = [];
675
            while ( $record = $this->fetchRow($stmt) )
676
            {
677
                if ( $record === false ) continue; // For scrutinizer...
678
                if ( $keyedOn && property_exists($record, $keyedOn) )
679
                {
680
                    $rows[$record->$keyedOn] = $record;
681
                    continue;
682
                }
683
                $rows[] = $record;
684
            }
685
            $this->reset();
686
            return $rows;
687
        };
688
        if ( $this->_cache_ttl === self::CACHE_NO )
689
        {
690
            return $fnCallback();
691
        }
692
        $table              = $this->getTableName();
693
        $id                 = $this->_parseWhereForPrimaryLookup();
694
        $id                 = $id ? "/{$id}" : '';
695
        list($sql, $params) = $this->fetchSqlQuery();
696
        $sql                = $this->buildQuery($sql, $params);
697
        $cacheKey           = "/{$table}{$id}/" . md5(json_encode([
698
            'sql'       => $sql,
699
            'keyed_on'  => $keyedOn,
700
        ]));
701
        return $this->_cacheData($cacheKey, $fnCallback, $this->_cache_ttl);
702
    }
703
704
    protected function _parseWhereForPrimaryLookup()
705
    {
706
        if ( ! ( $alias = $this->getTableAlias() ) )
707
        {
708
            return null;
709
        }
710
        foreach ( $this->_where_conditions as $idx => $conds )
711
        {
712
            if ( ! empty($conds['STATEMENT']) && $conds['STATEMENT'] === "{$alias}.id = ?" )
713
            {
714
                return ! empty($conds['PARAMS'][0]) ? $conds['PARAMS'][0] : null;
715
            }
716
        }
717
        return null;
718
    }
719
720
    /**
721
     * @param string $cacheKey
722
     * @param Closure $func
723
     * @param int $cacheTtl - 0 for default ttl, -1 for no cache or int for custom ttl
724
     * @return mixed|null
725
     */
726
    protected function _cacheData($cacheKey, Closure $func, $cacheTtl=self::CACHE_DEFAULT)
727
    {
728
        if ( $cacheTtl === self::CACHE_NO )
729
        {
730
            /** @noinspection PhpVoidFunctionResultUsedInspection */
731
            return $func->__invoke();
732
        }
733
        $data = $this->getCache()->get($cacheKey);
734
        if ( $data && is_object($data) && property_exists($data, 'results') )
735
        {
736
            $this->getLogger()->debug("Cache hit on {$cacheKey}");
737
            return $data->results;
738
        }
739
        $this->getLogger()->debug("Cache miss on {$cacheKey}");
740
        /** @noinspection PhpVoidFunctionResultUsedInspection */
741
        $data = (object)[
742
            // Watch out... invoke most likely calls reset
743
            // which clears the model params like _cache_ttl
744
            'results' => $func->__invoke(),
745
        ];
746
        try
747
        {
748
            // The cache engine expects null for the default cache value
749
            $cacheTtl = $cacheTtl === self::CACHE_DEFAULT ? null : $cacheTtl;
750
            /** @noinspection PhpMethodParametersCountMismatchInspection */
751
            if ( ! $this->getCache()->set($cacheKey, $data, $cacheTtl) )
752
            {
753
                throw new \Exception("Could not save data to cache");
754
            }
755
            return $data->results;
756
        }
757
        catch (\Exception $e)
758
        {
759
            $this->getLogger()->error($e->getMessage(), $e->getTrace());
760
            return $data->results;
761
        }
762
    }
763
764
    /**
765
     * @param string $cacheKey
766
     * @return bool
767
     */
768
    public function clearCache($cacheKey)
769
    {
770
        return $this->getCache()->delete($cacheKey);
771
    }
772
773
    /**
774
     * @param string|null $table
775
     * @return bool
776
     */
777
    public function clearCacheByTable($table=null)
778
    {
779
        $table = ! is_null($table) ? $table : $this->getTableName();
780
        if ( empty($table) )
781
        {
782
            return true;
783
        }
784
        return $this->clearCache("/{$table}/");
785
    }
786
787
    /**
788
     * @param Closure $fnCallback
789
     * @return int
790
     */
791
    public function fetchCallback(Closure $fnCallback)
792
    {
793
        $successCnt    = 0;
794
        $stmt           = $this->fetchStmt();
795
        while ( $this->_tallySuccessCount($stmt, $fnCallback, $successCnt) )
796
        {}
797
        return $successCnt;
798
    }
799
800
    /**
801
     * @param Closure $fnCallback
802
     * @param string  $keyedOn
803
     * @return array
804
     */
805
    public function fetchObjectsByCallback(Closure $fnCallback, $keyedOn=null)
806
    {
807
        $stmt       = $this->fetchStmt();
808
        $rows       = [];
809
        while ( $record = $this->fetchRow($stmt, $fnCallback) )
810
        {
811
            if ( $keyedOn && property_exists($record, $keyedOn) )
0 ignored issues
show
Bug Best Practice introduced by
The expression $keyedOn of type string|null is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
812
            {
813
                $rows[$record->$keyedOn] = $record;
814
                continue;
815
            }
816
            $rows[] = $record;
817
        }
818
        $this->reset();
819
        return $rows;
820
    }
821
822
    /**
823
     * @param $numFailures
824
     * @return $this
825
     */
826
    public function maxCallbackFailures($numFailures)
827
    {
828
        Assert($numFailures)->int();
829
        $this->_max_callback_failures = $numFailures;
830
        return $this;
831
    }
832
833
    /**
834
     * @param PDOStatement $stmt
835
     * @param Closure $fnCallback
836
     * @param int $successCnt
837
     * @return bool|null|stdClass
838
     */
839
    protected function _tallySuccessCount($stmt, Closure $fnCallback, &$successCnt)
840
    {
841
        $record = $this->fetchRow($stmt);
842
        if ( $record === false )
843
        {
844
            return false;
845
        }
846
        $record = $fnCallback($record);
847
        // Callback return null then we want to exit the fetch loop
848
        if ( is_null($record) )
849
        {
850
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
851
            return null;
852
        }
853
        // The not record then don't bump the tally
854
        if ( ! $record )
855
        {
856
            $this->_num_callback_failures++;
857
            if ( ! is_null($this->_max_callback_failures) && $this->_num_callback_failures >= $this->_max_callback_failures )
858
            {
859
                $this->getLogger()->error("The callback has failed {$this->_max_callback_failures} times... aborting...");
860
                $successCnt = null;
861
                return null;
862
            }
863
            return true;
864
        }
865
        $successCnt++;
866
        return $record;
867
    }
868
869
    /**
870
     * @param null|string $keyedOn
871
     * @param null|mixed $valueField
872
     * @param int $cacheTtl
873
     * @return mixed
874
     */
875
    public function fetchList($keyedOn=null, $valueField=null, $cacheTtl=self::CACHE_NO)
876
    {
877
        $keyedOn            = ! is_null($keyedOn) ? $keyedOn : $this->getPrimaryKeyName();
878
        $valueField         = ! is_null($valueField) ? $valueField : $this->getDisplayColumn();
879
        $keyedOnAlias       = strtolower(str_replace('.', '_', $keyedOn));
880
        $valueFieldAlias    = strtolower(str_replace('.', '_', $valueField));
881
        if ( preg_match('/ as /i', $keyedOn) )
882
        {
883
            list($keyedOn, $keyedOnAlias)   = preg_split('/ as /i', $keyedOn);
884
            $keyedOn                        = trim($keyedOn);
885
            $keyedOnAlias                   = trim($keyedOnAlias);
886
        }
887
        if ( preg_match('/ as /i', $valueField) )
888
        {
889
            list($valueField, $valueFieldAlias) = preg_split('/ as /i', $valueField);
890
            $valueField                         = trim($valueField);
891
            $valueFieldAlias                    = trim($valueFieldAlias);
892
        }
893
894
        $this->_cacheTtl($cacheTtl);
895
        $fnCallback         = function() use ($keyedOn, $keyedOnAlias, $valueField, $valueFieldAlias) {
896
897
            $rows = [];
898
            $stmt = $this->select(null)
899
                ->select($keyedOn, $keyedOnAlias)
900
                ->select($valueField, $valueFieldAlias)
901
                ->fetchStmt();
902
            while ( $record = $this->fetchRow($stmt) )
903
            {
904
                $rows[$record->$keyedOnAlias] = $record->$valueFieldAlias;
905
            }
906
            return $rows;
907
        };
908
        if ( $this->_cache_ttl === self::CACHE_NO )
909
        {
910
            $result = $fnCallback();
911
            unset($cacheKey, $fnCallback);
912
            return $result;
913
        }
914
        $table              = $this->getTableName();
915
        $cacheKey           = md5(json_encode([
916
            'sql'               => $this->fetchSqlQuery(),
917
            'keyed_on'          => $keyedOn,
918
            'keyed_on_alias'    => $keyedOnAlias,
919
            'value_field'       => $valueField,
920
            'value_fieldAlias'  => $valueFieldAlias,
921
        ]));
922
        return $this->_cacheData("/{$table}/list/{$cacheKey}", $fnCallback, $this->_cache_ttl);
923
    }
924
925
    /**
926
     * @param string $column
927
     * @param int $cacheTtl
928
     * @param bool|true $unique
929
     * @return array|mixed
930
     */
931
    public function fetchColumn($column, $cacheTtl=self::CACHE_NO, $unique=true)
932
    {
933
        $list = $this->select($column)->fetch(null, $cacheTtl);
934
        foreach ( $list as $idx => $obj )
0 ignored issues
show
Bug introduced by
The expression $list of type array<integer,object<stdClass>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
935
        {
936
            $list[$idx] = $obj->{$column};
937
        }
938
        return $unique ? array_unique($list) : $list;
939
    }
940
941
    /**
942
     * @param null|string $field
943
     * @param null|int $itemId
944
     * @param int $cacheTtl
945
     * @return mixed|null
946
     */
947
    public function fetchField($field=null, $itemId=null, $cacheTtl=self::CACHE_NO)
948
    {
949
        $field  = !is_null($field) ? $field : $this->getPrimaryKeyName();
950
        $object = $this->select(null)->select($field)->fetchOne($itemId, $cacheTtl);
951
        if ( ! $object )
952
        {
953
            return null;
954
        }
955
        // Handle aliases
956
        if ( preg_match('/ as /i', $field) )
957
        {
958
            list($expression, $alias) = preg_split('/ as /i', $field);
959
            unset($expression);
960
            $field = trim($alias);
961
        }
962
        if ( strpos($field, '.') !== false )
963
        {
964
            list($tableAlias, $field) = explode('.', $field);
965
            unset($tableAlias);
966
        }
967
        return property_exists($object, $field) ? $object->$field : null;
968
    }
969
970
    /**
971
     * @param int|null $id
972
     * @param int $cacheTtl
973
     * @return \stdClass|bool
974
     */
975
    public function fetchOne($id=null, $cacheTtl=self::CACHE_NO)
976
    {
977
        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 zero. 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...
978
        {
979
            $this->wherePk($id, true);
980
        }
981
        $this->limit(1);
982
        $fetchAll = $this->fetch(null, $cacheTtl);
983
        return $fetchAll ? array_shift($fetchAll) : false;
984
    }
985
986
    /**
987
     * @param int|null $id
988
     * @param int $cacheTtl
989
     * @return boolean
990
     */
991
    public function fetchExists($id=null, $cacheTtl=self::CACHE_NO)
992
    {
993
        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 zero. 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...
994
        {
995
            $this->wherePk($id, true);
996
        }
997
        return $this->count('*', $cacheTtl) !== 0;
998
    }
999
1000
    /*------------------------------------------------------------------------------
1001
                                    Fluent Query Builder
1002
    *-----------------------------------------------------------------------------*/
1003
1004
    /**
1005
     * Create the select clause
1006
     *
1007
     * @param  mixed    $columns  - the column to select. Can be string or array of fields
1008
     * @param  string   $alias - an alias to the column
1009
     * @param boolean $explicitSelect
1010
     * @return $this
1011
     */
1012
    public function select($columns='*', $alias=null, $explicitSelect=true)
1013
    {
1014
        if ( $explicitSelect )
1015
        {
1016
            $this->explicitSelectMode();
1017
        }
1018
        if ( ! empty($alias) && !is_array($columns) & $columns !== $alias )
1019
        {
1020
            $columns .= " AS {$alias} ";
1021
        }
1022
        if ( $columns === '*' && !empty($this->_schema) )
1023
        {
1024
            $columns = $this->getColumns();
1025
        }
1026
        // Reset the select list
1027
        if ( is_null($columns) )
1028
        {
1029
            $this->_select_fields = [];
1030
            return $this;
1031
        }
1032
        $columns = is_array($columns) ? $columns : [$columns];
1033
1034
//        if ( empty($this->_select_fields) && $addAllIfEmpty )
1035
//        {
1036
//            $this->select('*');
1037
//        }
1038
        if ( $this->_table_alias )
1039
        {
1040
            $schema = $this->columns();
1041
            foreach ( $columns as $idx => $col )
1042
            {
1043
                if ( in_array($col, $schema) )
1044
                {
1045
                    $columns[$idx] = "{$this->_table_alias}.{$col}";
1046
                }
1047
            }
1048
        }
1049
        $this->_select_fields = array_merge($this->_select_fields, $columns);
1050
        return $this;
1051
    }
1052
1053
    /**
1054
     * @param string $select
1055
     * @return $this
1056
     */
1057
    public function selectRaw($select)
1058
    {
1059
        $this->_select_fields[] = $select;
1060
        return $this;
1061
    }
1062
1063
    /**
1064
     * @param bool $log_queries
1065
     *
1066
     * @return $this
1067
     */
1068
    public function logQueries($log_queries=true)
1069
    {
1070
        $this->_log_queries = $log_queries;
1071
        return $this;
1072
    }
1073
1074
    /**
1075
     * @param bool $include_count
1076
     *
1077
     * @return $this
1078
     */
1079
    public function includeCount($include_count=true)
1080
    {
1081
        $this->_include_count = $include_count;
1082
        return $this;
1083
    }
1084
1085
    /**
1086
     * @param bool $distinct
1087
     *
1088
     * @return $this
1089
     */
1090
    public function distinct($distinct=true)
1091
    {
1092
        $this->_distinct = $distinct;
1093
        return $this;
1094
    }
1095
1096
    /**
1097
     * @param array $fields
1098
     * @return $this
1099
     */
1100
    public function withBelongsTo($fields=[])
1101
    {
1102
        if ( ! empty($this->_associations['belongsTo']) )
1103
        {
1104
            foreach ( $this->_associations['belongsTo'] as $alias => $config )
1105
            {
1106
                $addFieldsForJoins = empty($fields) || in_array($config[3], $fields);
1107
                $this->autoJoin($alias, self::LEFT_JOIN, $addFieldsForJoins);
1108
            }
1109
        }
1110
        return $this;
1111
    }
1112
1113
    /**
1114
     * @param string $alias
1115
     * @param string $type
1116
     * @param bool   $addSelectField
1117
     * @return $this
1118
     */
1119
    public function autoJoin($alias, $type=self::LEFT_JOIN, $addSelectField=true)
1120
    {
1121
        Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid join... the alias does not exists");
1122
        list($table, $join_col, $field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
1123
        $condition = "{$alias}.id = {$this->_table_alias}.{$join_col}";
1124
        if ( in_array($alias, $this->_join_aliases) )
1125
        {
1126
            return $this;
1127
        }
1128
        $this->join($table, $condition, $alias, $type);
1129
        if ( $addSelectField )
1130
        {
1131
            $this->select($field, $fieldAlias, false);
1132
        }
1133
        return $this;
1134
    }
1135
1136
    /**
1137
     * Add where condition, more calls appends with AND
1138
     *
1139
     * @param string $condition possibly containing ? or :name
1140
     * @param mixed $parameters accepted by PDOStatement::execute or a scalar value
1141
     * @param mixed ...
1142
     * @return $this
1143
     */
1144
    public function where($condition, $parameters = [])
1145
    {
1146
        // By default the and_or_operator and wrap operator is AND,
1147
        if ( $this->_wrap_open || ! $this->_and_or_operator )
1148
        {
1149
            $this->_and();
1150
        }
1151
1152
        // where(array("column1" => 1, "column2 > ?" => 2))
1153
        if ( is_array($condition) )
1154
        {
1155
            foreach ($condition as $key => $val)
1156
            {
1157
                $this->where($key, $val);
1158
            }
1159
            return $this;
1160
        }
1161
1162
        $args = func_num_args();
1163
        if ( $args != 2 || strpbrk($condition, '?:') )
1164
        { // where('column < ? OR column > ?', array(1, 2))
1165
            if ( $args != 2 || !is_array($parameters) )
1166
            { // where('column < ? OR column > ?', 1, 2)
1167
                $parameters = func_get_args();
1168
                array_shift($parameters);
1169
            }
1170
        }
1171
        else if ( !is_array($parameters) )
1172
        {//where(column,value) => column=value
1173
            $condition .= ' = ?';
1174
            $parameters = [$parameters];
1175
        }
1176
        else if ( is_array($parameters) )
1177
        { // where('column', array(1, 2)) => column IN (?,?)
1178
            $placeholders = $this->_makePlaceholders(count($parameters));
1179
            $condition = "({$condition} IN ({$placeholders}))";
1180
        }
1181
1182
        $this->_where_conditions[] = [
1183
            'STATEMENT'   => $condition,
1184
            'PARAMS'      => $parameters,
1185
            'OPERATOR'    => $this->_and_or_operator
1186
        ];
1187
        // Reset the where operator to AND. To use OR, you must call _or()
1188
        $this->_and();
1189
        return $this;
1190
    }
1191
1192
    /**
1193
     * Create an AND operator in the where clause
1194
     *
1195
     * @return $this
1196
     */
1197
    public function _and()
1198
    {
1199
        if ( $this->_wrap_open )
1200
        {
1201
            $this->_where_conditions[] = self::OPERATOR_AND;
1202
            $this->_last_wrap_position = count($this->_where_conditions);
1203
            $this->_wrap_open = false;
1204
            return $this;
1205
        }
1206
        $this->_and_or_operator = self::OPERATOR_AND;
1207
        return $this;
1208
    }
1209
1210
1211
    /**
1212
     * Create an OR operator in the where clause
1213
     *
1214
     * @return $this
1215
     */
1216
    public function _or()
1217
    {
1218
        if ( $this->_wrap_open )
1219
        {
1220
            $this->_where_conditions[] = self::OPERATOR_OR;
1221
            $this->_last_wrap_position = count($this->_where_conditions);
1222
            $this->_wrap_open = false;
1223
            return $this;
1224
        }
1225
        $this->_and_or_operator = self::OPERATOR_OR;
1226
        return $this;
1227
    }
1228
1229
    /**
1230
     * To group multiple where clauses together.
1231
     *
1232
     * @return $this
1233
     */
1234
    public function wrap()
1235
    {
1236
        $this->_wrap_open = true;
1237
        $spliced = array_splice($this->_where_conditions, $this->_last_wrap_position, count($this->_where_conditions), '(');
1238
        $this->_where_conditions = array_merge($this->_where_conditions, $spliced);
1239
        array_push($this->_where_conditions,')');
1240
        $this->_last_wrap_position = count($this->_where_conditions);
1241
        return $this;
1242
    }
1243
1244
    /**
1245
     * Where Primary key
1246
     *
1247
     * @param      integer $id
1248
     * @param bool $addAlias
1249
     *
1250
     * @return $this
1251
     */
1252
    public function wherePk($id, $addAlias=true)
1253
    {
1254
        $alias = $addAlias && !empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1255
        return $this->where($alias . $this->getPrimaryKeyName(), $id);
1256
    }
1257
1258
    /**
1259
     * WHERE $columnName != $value
1260
     *
1261
     * @param  string   $columnName
1262
     * @param  mixed    $value
1263
     * @return $this
1264
     */
1265
    public function whereNot($columnName, $value)
1266
    {
1267
        return $this->where("$columnName != ?", $value);
1268
    }
1269
    /**
1270
     * WHERE $columnName != $value
1271
     *
1272
     * @param  string   $columnName
1273
     * @param  mixed    $value
1274
     * @return $this
1275
     */
1276
    public function whereCoercedNot($columnName, $value)
1277
    {
1278
        return $this->where("IFNULL({$columnName}, '') != ?", $value);
1279
    }
1280
1281
    /**
1282
     * WHERE $columnName LIKE $value
1283
     *
1284
     * @param  string   $columnName
1285
     * @param  mixed    $value
1286
     * @return $this
1287
     */
1288
    public function whereLike($columnName, $value)
1289
    {
1290
        return $this->where("$columnName LIKE ?", $value);
1291
    }
1292
1293
    /**
1294
     * @param string $columnName
1295
     * @param mixed $value1
1296
     * @param mixed $value2
1297
     * @return $this
1298
     */
1299
    public function whereBetween($columnName, $value1, $value2)
1300
    {
1301
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1302
    }
1303
1304
    /**
1305
     * @param string $columnName
1306
     * @param mixed $value1
1307
     * @param mixed $value2
1308
     * @return $this
1309
     */
1310
    public function whereNotBetween($columnName, $value1, $value2)
1311
    {
1312
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1313
    }
1314
1315
    /**
1316
     * @param string $columnName
1317
     * @param string $regex
1318
     * @return $this
1319
     */
1320
    public function whereRegex($columnName, $regex)
1321
    {
1322
        return $this->where("$columnName REGEXP ?", $regex);
1323
    }
1324
1325
    /**
1326
     * @param string $columnName
1327
     * @param string $regex
1328
     * @return $this
1329
     */
1330
    public function whereNotRegex($columnName, $regex)
1331
    {
1332
        return $this->where("$columnName NOT REGEXP ?", $regex);
1333
    }
1334
1335
    /**
1336
     * WHERE $columnName NOT LIKE $value
1337
     *
1338
     * @param  string   $columnName
1339
     * @param  mixed    $value
1340
     * @return $this
1341
     */
1342
    public function whereNotLike($columnName, $value)
1343
    {
1344
        return $this->where("$columnName NOT LIKE ?", $value);
1345
    }
1346
1347
    /**
1348
     * WHERE $columnName > $value
1349
     *
1350
     * @param  string   $columnName
1351
     * @param  mixed    $value
1352
     * @return $this
1353
     */
1354
    public function whereGt($columnName, $value)
1355
    {
1356
        return $this->where("$columnName > ?", $value);
1357
    }
1358
1359
    /**
1360
     * WHERE $columnName >= $value
1361
     *
1362
     * @param  string   $columnName
1363
     * @param  mixed    $value
1364
     * @return $this
1365
     */
1366
    public function whereGte($columnName, $value)
1367
    {
1368
        return $this->where("$columnName >= ?", $value);
1369
    }
1370
1371
    /**
1372
     * WHERE $columnName < $value
1373
     *
1374
     * @param  string   $columnName
1375
     * @param  mixed    $value
1376
     * @return $this
1377
     */
1378
    public function whereLt($columnName, $value)
1379
    {
1380
        return $this->where("$columnName < ?", $value);
1381
    }
1382
1383
    /**
1384
     * WHERE $columnName <= $value
1385
     *
1386
     * @param  string   $columnName
1387
     * @param  mixed    $value
1388
     * @return $this
1389
     */
1390
    public function whereLte($columnName, $value)
1391
    {
1392
        return $this->where("$columnName <= ?", $value);
1393
    }
1394
1395
    /**
1396
     * WHERE $columnName IN (?,?,?,...)
1397
     *
1398
     * @param  string   $columnName
1399
     * @param  array    $values
1400
     * @return $this
1401
     */
1402
    public function whereIn($columnName, array $values)
1403
    {
1404
        return $this->where($columnName,$values);
1405
    }
1406
1407
    /**
1408
     * WHERE $columnName NOT IN (?,?,?,...)
1409
     *
1410
     * @param  string   $columnName
1411
     * @param  array    $values
1412
     * @return $this
1413
     */
1414
    public function whereNotIn($columnName, array $values)
1415
    {
1416
        $placeholders = $this->_makePlaceholders(count($values));
1417
        return $this->where("({$columnName} NOT IN ({$placeholders}))", $values);
1418
    }
1419
1420
    /**
1421
     * WHERE $columnName IS NULL
1422
     *
1423
     * @param  string   $columnName
1424
     * @return $this
1425
     */
1426
    public function whereNull($columnName)
1427
    {
1428
        return $this->where("({$columnName} IS NULL)");
1429
    }
1430
1431
    /**
1432
     * WHERE $columnName IS NOT NULL
1433
     *
1434
     * @param  string   $columnName
1435
     * @return $this
1436
     */
1437
    public function whereNotNull($columnName)
1438
    {
1439
        return $this->where("({$columnName} IS NOT NULL)");
1440
    }
1441
1442
    /**
1443
     * @param string $statement
1444
     * @param string $operator
1445
     * @return $this
1446
     */
1447
    public function having($statement, $operator = self::OPERATOR_AND)
1448
    {
1449
        $this->_having[] = [
1450
            'STATEMENT'   => $statement,
1451
            'OPERATOR'    => $operator
1452
        ];
1453
        return $this;
1454
    }
1455
1456
    /**
1457
     * ORDER BY $columnName (ASC | DESC)
1458
     *
1459
     * @param  string   $columnName - The name of the column or an expression
1460
     * @param  string   $ordering   (DESC | ASC)
1461
     * @return $this
1462
     */
1463
    public function orderBy($columnName, $ordering=null)
1464
    {
1465
        Assert($ordering)->nullOr()->inArray(['DESC', 'desc', 'ASC', 'asc']);
1466
        if ( is_null($columnName) )
1467
        {
1468
            $this->_order_by = [];
1469
            return $this;
1470
        }
1471
        $this->_order_by[] = trim("{$columnName} {$ordering}");
1472
        return $this;
1473
    }
1474
1475
    /**
1476
     * GROUP BY $columnName
1477
     *
1478
     * @param  string   $columnName
1479
     * @return $this
1480
     */
1481
    public function groupBy($columnName)
1482
    {
1483
        $columnName = is_array($columnName) ? $columnName : [$columnName];
1484
        foreach ( $columnName as $col )
1485
        {
1486
            $this->_group_by[] = $col;
1487
        }
1488
        return $this;
1489
    }
1490
1491
1492
    /**
1493
     * LIMIT $limit
1494
     *
1495
     * @param  int      $limit
1496
     * @param  int|null $offset
1497
     * @return $this
1498
     */
1499
    public function limit($limit, $offset=null)
1500
    {
1501
        $this->_limit =  (int)$limit;
1502
        if ( ! is_null($offset) )
1503
        {
1504
            $this->offset($offset);
1505
        }
1506
        return $this;
1507
    }
1508
1509
    /**
1510
     * Return the limit
1511
     *
1512
     * @return integer
1513
     */
1514
    public function getLimit()
1515
    {
1516
        return $this->_limit;
1517
    }
1518
1519
    /**
1520
     * OFFSET $offset
1521
     *
1522
     * @param  int      $offset
1523
     * @return $this
1524
     */
1525
    public function offset($offset)
1526
    {
1527
        $this->_offset = (int)$offset;
1528
        return $this;
1529
    }
1530
1531
    /**
1532
     * Return the offset
1533
     *
1534
     * @return integer
1535
     */
1536
    public function getOffset()
1537
    {
1538
        return $this->_offset;
1539
    }
1540
1541
    /**
1542
     * Build a join
1543
     *
1544
     * @param  string    $table         - The table name
1545
     * @param  string   $constraint    -> id = profile.user_id
1546
     * @param  string   $tableAlias   - The alias of the table name
1547
     * @param  string   $joinOperator - LEFT | INNER | etc...
1548
     * @return $this
1549
     */
1550
    public function join($table, $constraint=null, $tableAlias = null, $joinOperator = '')
1551
    {
1552
        if ( is_null($constraint) )
1553
        {
1554
            return $this->autoJoin($table, $joinOperator);
1555
        }
1556
        $join                   = [$joinOperator ? "{$joinOperator} " : ''];
1557
        $join[]                 = "JOIN {$table} ";
1558
        $tableAlias            = is_null($tableAlias) ? Inflector::classify($table) : $tableAlias;
1559
        $join[]                 = $tableAlias ? "AS {$tableAlias} " : '';
1560
        $join[]                 = "ON {$constraint}";
1561
        $this->_join_sources[]  = implode('', $join);
1562
        if ( $tableAlias )
1563
        {
1564
            $this->_join_aliases[]  = $tableAlias;
1565
        }
1566
        return $this;
1567
    }
1568
1569
    /**
1570
     * Create a left join
1571
     *
1572
     * @param  string   $table
1573
     * @param  string   $constraint
1574
     * @param  string   $tableAlias
1575
     * @return $this
1576
     */
1577
    public function leftJoin($table, $constraint, $tableAlias=null)
1578
    {
1579
        return $this->join($table, $constraint, $tableAlias, self::LEFT_JOIN);
1580
    }
1581
1582
1583
    /**
1584
     * Return the build select query
1585
     *
1586
     * @return string
1587
     */
1588
    public function getSelectQuery()
1589
    {
1590
        if ( ! is_null($this->_raw_sql) )
1591
        {
1592
            return $this->_raw_sql;
1593
        }
1594
        if ( empty($this->_select_fields) || ! $this->_explicit_select_mode )
1595
        {
1596
            $this->select('*', null, false);
1597
        }
1598
        foreach ( $this->_select_fields as $idx => $cols )
1599
        {
1600
            if ( strpos(trim(strtolower($cols)), 'distinct ') === 0 )
1601
            {
1602
                $this->_distinct = true;
1603
                $this->_select_fields[$idx] = str_ireplace('distinct ', '', $cols);
1604
            }
1605
        }
1606
        if ( $this->_include_count )
1607
        {
1608
            $this->select('COUNT(*) as __cnt');
1609
        }
1610
        $query  = 'SELECT ';
1611
        $query .= $this->_distinct ? 'DISTINCT ' : '';
1612
        $query .= implode(', ', $this->_prepareColumns($this->_select_fields));
1613
        $query .= " FROM {$this->_table_name}" . ( $this->_table_alias ? " {$this->_table_alias}" : '' );
1614
        if ( count($this->_join_sources ) )
1615
        {
1616
            $query .= (' ').implode(' ',$this->_join_sources);
1617
        }
1618
        $query .= $this->_getWhereString(); // WHERE
1619
        if ( count($this->_group_by) )
1620
        {
1621
            $query .= ' GROUP BY ' . implode(', ', array_unique($this->_group_by));
1622
        }
1623
        if ( count($this->_order_by ) )
1624
        {
1625
            $query .= ' ORDER BY ' . implode(', ', array_unique($this->_order_by));
1626
        }
1627
        $query .= $this->_getHavingString(); // HAVING
1628
        return $this->_connection->setLimit($query, $this->_limit, $this->_offset);
1629
    }
1630
1631
    /**
1632
     * Prepare columns to include the table alias name
1633
     * @param array $columns
1634
     * @return array
1635
     */
1636
    protected function _prepareColumns(array $columns)
1637
    {
1638
        if ( ! $this->_table_alias )
1639
        {
1640
            return $columns;
1641
        }
1642
        $newColumns = [];
1643
        foreach ($columns as $column)
1644
        {
1645
            if ( strpos($column, ',') && ! preg_match('/^[a-zA-Z]{2,200}\(.{1,500}\)/', trim($column)) )
1646
            {
1647
                $newColumns = array_merge($this->_prepareColumns(explode(',', $column)), $newColumns);
1648
            }
1649
            elseif ( preg_match('/^(AVG|SUM|MAX|MIN|COUNT|CONCAT)/', $column) )
1650
            {
1651
                $newColumns[] = trim($column);
1652
            }
1653
            elseif (strpos($column, '.') === false && strpos(strtoupper($column), 'NULL') === false)
1654
            {
1655
                $column         = trim($column);
1656
                $newColumns[]   = preg_match('/^[0-9]/', $column) ? trim($column) : "{$this->_table_alias}.{$column}";
1657
            }
1658
            else
1659
            {
1660
                $newColumns[] = trim($column);
1661
            }
1662
        }
1663
        return $newColumns;
1664
    }
1665
1666
    /**
1667
     * Build the WHERE clause(s)
1668
     *
1669
     * @param bool $purgeAliases
1670
     * @return string
1671
     */
1672
    protected function _getWhereString($purgeAliases=false)
1673
    {
1674
        // If there are no WHERE clauses, return empty string
1675
        if ( empty($this->_where_conditions) )
1676
        {
1677
            return '';
1678
        }
1679
        $where_condition = '';
1680
        $last_condition = '';
1681
        foreach ( $this->_where_conditions as $condition )
1682
        {
1683
            if ( is_array($condition) )
1684
            {
1685
                if ( $where_condition && $last_condition != '(' && !preg_match('/\)\s+(OR|AND)\s+$/i', $where_condition))
1686
                {
1687
                    $where_condition .= $condition['OPERATOR'];
1688
                }
1689
                if ( $purgeAliases && ! empty($condition['STATEMENT']) && strpos($condition['STATEMENT'], '.') !== false && ! empty($this->_table_alias) )
1690
                {
1691
                    $condition['STATEMENT'] = preg_replace("/{$this->_table_alias}\./", '', $condition['STATEMENT']);
1692
                }
1693
                $where_condition .= $condition['STATEMENT'];
1694
                $this->_where_parameters = array_merge($this->_where_parameters, $condition['PARAMS']);
1695
            }
1696
            else
1697
            {
1698
                $where_condition .= $condition;
1699
            }
1700
            $last_condition = $condition;
1701
        }
1702
        return " WHERE {$where_condition}" ;
1703
    }
1704
1705
    /**
1706
     * Return the HAVING clause
1707
     *
1708
     * @return string
1709
     */
1710
    protected function _getHavingString()
1711
    {
1712
        // If there are no WHERE clauses, return empty string
1713
        if ( empty($this->_having) )
1714
        {
1715
            return '';
1716
        }
1717
        $having_condition = '';
1718
        foreach ( $this->_having as $condition )
1719
        {
1720
            if ( $having_condition && !preg_match('/\)\s+(OR|AND)\s+$/i', $having_condition) )
1721
            {
1722
                $having_condition .= $condition['OPERATOR'];
1723
            }
1724
            $having_condition .= $condition['STATEMENT'];
1725
        }
1726
        return " HAVING {$having_condition}" ;
1727
    }
1728
1729
    /**
1730
     * Return the values to be bound for where
1731
     *
1732
     * @param bool $purgeAliases
1733
     * @return array
1734
     */
1735
    protected function _getWhereParameters($purgeAliases=false)
1736
    {
1737
        unset($purgeAliases);
1738
        return $this->_where_parameters;
1739
    }
1740
1741
    /**
1742
     * Insert new rows
1743
     * $records can be a stdClass or an array of stdClass to add a bulk insert
1744
     * If a single row is inserted, it will return it's row instance
1745
     *
1746
     * @param stdClass|array $records
1747
     * @return int|stdClass|bool
1748
     * @throws Exception
1749
     */
1750
    public function insert($records)
1751
    {
1752
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
1753
        $records = is_array($records) ? $records : [$records];
1754
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1755
1756
        foreach ( $records as $key => $record )
1757
        {
1758
            $records[$key] = $this->beforeSave($records[$key], self::SAVE_INSERT);
1759
            if ( ! empty($this->_errors) )
1760
            {
1761
                return false;
1762
            }
1763
        }
1764
        list($sql, $insert_values) = $this->insertSqlQuery($records);
1765
        $this->execute($sql, $insert_values);
1766
        $rowCount = $this->rowCount();
1767
        if ( $rowCount === 1 )
1768
        {
1769
            $primaryKeyName                 = $this->getPrimaryKeyName();
1770
            $records[0]->$primaryKeyName    = $this->getLastInsertId($primaryKeyName);
1771
        }
1772
        foreach ( $records as $key => $record )
1773
        {
1774
            $records[$key] = $this->afterSave($record, self::SAVE_INSERT);
1775
        }
1776
        $this->destroy();
1777
        // On single element return the object
1778
        return $rowCount === 1 ? $records[0] : $rowCount;
1779
    }
1780
1781
    /**
1782
     * @param string $name
1783
     * @return int
1784
     */
1785
    public function getLastInsertId($name=null)
1786
    {
1787
        return (int)$this->getPdo()->lastInsertId($name);
1788
    }
1789
1790
    /**
1791
     * @param $records
1792
     * @return array
1793
     */
1794
    public function insertSqlQuery($records)
1795
    {
1796
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
1797
        $records = is_array($records) ? $records : [$records];
1798
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1799
1800
        $insert_values  = [];
1801
        $question_marks = [];
1802
        $properties     = [];
1803
        foreach ( $records as $record )
1804
        {
1805
            $properties         = !empty($properties) ? $properties : array_keys(get_object_vars($record));
1806
            $question_marks[]   = '('  . $this->_makePlaceholders(count($properties)) . ')';
1807
            $insert_values      = array_merge($insert_values, array_values((array)$record));
1808
        }
1809
        $properties     = implode(', ', $properties);
1810
        $question_marks = implode(', ', $question_marks);
1811
        $sql            = "INSERT INTO {$this->_table_name} ({$properties}) VALUES {$question_marks}";
1812
        return [$sql, $insert_values];
1813
    }
1814
1815
    /**
1816
     * @param       $data
1817
     * @param array $matchOn
1818
     * @param bool  $returnObj
1819
     * @return bool|int|stdClass
1820
     */
1821
    public function upsert($data, array $matchOn=[], $returnObj=false)
1822
    {
1823
        if ( ! is_array($data) )
1824
        {
1825
            return $this->upsertOne($data, $matchOn, $returnObj);
1826
        }
1827
        Assert($data)
1828
            ->notEmpty("The data passed to insert does not contain any data")
1829
            ->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1830
        $num_success    = 0;
1831
        foreach ( $data as $row )
1832
        {
1833
            $clone = clone $this;
1834
            if ( $clone->upsertOne($row, $matchOn) )
1835
            {
1836
                $num_success++;
1837
            }
1838
            unset($clone->_handlers, $clone); // hhvm mem leak
1839
        }
1840
        return $num_success;
1841
    }
1842
1843
    /**
1844
     * @param stdClass $object
1845
     * @param array    $matchOn
1846
     * @param bool     $returnObj
1847
     * @return bool|int|stdClass
1848
     */
1849
    public function upsertOne(stdClass $object, array $matchOn=[], $returnObj=false)
1850
    {
1851
        $primary_key    = $this->getPrimaryKeyName();
1852
        $matchOn       = empty($matchOn) && property_exists($object, $primary_key) ? [$primary_key] : $matchOn;
1853
        foreach ( $matchOn as $column )
1854
        {
1855
            Assert( ! property_exists($object, $column) && $column !== $primary_key)->false('The match on value for upserts is missing.');
1856
            if ( property_exists($object, $column) )
1857
            {
1858
                if ( is_null($object->$column) )
1859
                {
1860
                    $this->whereNull($column);
1861
                }
1862
                else
1863
                {
1864
                    $this->where($column, $object->$column);
1865
                }
1866
            }
1867
        }
1868
        if ( count($this->_where_conditions) < 1 )
1869
        {
1870
            return $this->insert($object);
1871
        }
1872
        if ( ( $id = (int)$this->fetchField($primary_key) ) )
1873
        {
1874
            if ( property_exists($object, $primary_key) && is_null($object->$primary_key) )
1875
            {
1876
                $object->$primary_key = $id;
1877
            }
1878
            $rows_affected = $this->reset()->wherePk($id)->update($object);
1879
            if ( $rows_affected === false )
1880
            {
1881
                return false;
1882
            }
1883
            return $returnObj ? $this->reset()->fetchOne($id) : $id;
1884
        }
1885
        return $this->insert($object);
1886
    }
1887
1888
    /**
1889
     * @param array      $data
1890
     * @param array      $matchOn
1891
     * @param bool|false $returnObj
1892
     * @return bool|int|stdClass
1893
     */
1894
    public function upsertArr(array $data, array $matchOn=[], $returnObj=false)
1895
    {
1896
        return $this->upsert((object)$data, $matchOn, $returnObj);
1897
    }
1898
1899
    /**
1900
     * Update entries
1901
     * Use the query builder to create the where clause
1902
     *
1903
     * @param stdClass $record
1904
     * @param bool     $updateAll
1905
     * @return bool|int
1906
     * @throws Exception
1907
     */
1908
    public function update(stdClass $record, $updateAll=false)
1909
    {
1910
        Assert($record)
1911
            ->notEmpty("The data passed to update does not contain any data")
1912
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
1913
1914
        if ( empty($this->_where_conditions) && ! $updateAll )
1915
        {
1916
            throw new Exception("You cannot update an entire table without calling update with updateAll=true", 500);
1917
        }
1918
        $record = $this->beforeSave($record, self::SAVE_UPDATE);
1919
        if ( ! empty($this->_errors) )
1920
        {
1921
            return false;
1922
        }
1923
        list($sql, $values) = $this->updateSqlQuery($record);
1924
        $this->execute($sql, $values);
1925
        $this->afterSave($record, self::SAVE_UPDATE);
1926
        $row_count = $this->rowCount();
1927
        $this->destroy();
1928
        return $row_count;
1929
    }
1930
1931
    /**
1932
     * @param array      $record
1933
     * @param bool|false $updateAll
1934
     * @return bool|int
1935
     * @throws Exception
1936
     */
1937
    public function updateArr(array $record, $updateAll=false)
1938
    {
1939
        return $this->update((object)$record, $updateAll);
1940
    }
1941
1942
    /**
1943
     * @param array $record
1944
     * @return bool|int|stdClass
1945
     */
1946
    public function insertArr(array $record)
1947
    {
1948
        return $this->insert((object)$record);
1949
    }
1950
1951
    /**
1952
     * @param int     $field
1953
     * @param mixed   $value
1954
     * @param null $id
1955
     * @param bool|false $updateAll
1956
     * @return bool|int
1957
     * @throws Exception
1958
     */
1959
    public function updateField($field, $value, $id=null, $updateAll=false)
1960
    {
1961
        if ( ! is_null($id) )
1962
        {
1963
            $this->wherePk($id);
1964
        }
1965
        return $this->update((object)[$field => $value], $updateAll);
1966
    }
1967
1968
    /**
1969
     * @param int     $field
1970
     * @param mixed   $value
1971
     * @param null $id
1972
     * @param bool|false $updateAll
1973
     * @return bool|int
1974
     * @throws Exception
1975
     */
1976
    public function concatField($field, $value, $id=null, $updateAll=false)
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $updateAll is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1977
    {
1978
1979
    }
1980
1981
    /**
1982
     * @param stdClass $record
1983
     * @return bool|int
1984
     * @throws Exception
1985
     */
1986
    public function updateChanged(stdClass $record)
1987
    {
1988
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
1989
        {
1990
            if ( is_null($value) )
1991
            {
1992
                $this->whereNotNull($field);
1993
                continue;
1994
            }
1995
            $this->whereCoercedNot($field, $value);
1996
        }
1997
        return $this->update($record);
1998
    }
1999
2000
    /**
2001
     * @param string    $expression
2002
     * @param array     $params
2003
     * @return $this
2004
     */
2005
    public function updateByExpression($expression, array $params)
2006
    {
2007
        $this->_update_raw[] = [$expression, $params];
2008
        return $this;
2009
    }
2010
2011
    /**
2012
     * @param array $data
2013
     * @return int
2014
     * @throws Exception
2015
     */
2016
    public function rawUpdate(array $data=[])
2017
    {
2018
        list($sql, $values) = $this->updateSql($data);
2019
        $this->execute($sql, $values);
2020
        $row_count = $this->rowCount();
2021
        $this->destroy();
2022
        return $row_count;
2023
    }
2024
2025
    /**
2026
     * @param stdClass $record
2027
     * @return array
2028
     */
2029
    public function updateSqlQuery(stdClass $record)
2030
    {
2031
        Assert($record)
2032
            ->notEmpty("The data passed to update does not contain any data")
2033
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2034
2035
        // Make sure we remove the primary key
2036
        $record = (array)$record;
2037
        return $this->updateSql($record);
2038
    }
2039
2040
    /**
2041
     * @param $record
2042
     * @return array
2043
     */
2044
    protected function updateSql(array $record)
2045
    {
2046
        unset($record[$this->getPrimaryKeyName()]);
2047
        // Sqlite needs a null primary key
2048
        //$record[$this->getPrimaryKeyName()] = null;
2049
        $field_list = [];
2050
        foreach ( $record as $key => $value )
2051
        {
2052
            if ( is_numeric($key) )
2053
            {
2054
                $field_list[] = $value;
2055
                unset($record[$key]);
2056
                continue;
2057
            }
2058
            $field_list[] = "{$key} = ?";
2059
        }
2060
        $rawParams  = [];
2061
        foreach ( $this->_update_raw as $rawUpdate )
2062
        {
2063
            $field_list[]   = $rawUpdate[0];
2064
            $rawParams      = array_merge($rawParams, $rawUpdate[1]);
2065
        }
2066
        $field_list     = implode(', ', $field_list);
2067
        $where_str      = $this->_getWhereString();
2068
        $joins          = ! empty($this->_join_sources) ? (' ').implode(' ',$this->_join_sources) : '';
2069
        $alias          = ! empty($this->_table_alias) ? " AS {$this->_table_alias}" : '';
2070
        $sql            = "UPDATE {$this->_table_name}{$alias}{$joins} SET {$field_list}{$where_str}";
2071
        $values         = array_merge(array_values($record), $rawParams, $this->_getWhereParameters());
2072
        return [$sql, $values];
2073
    }
2074
2075
    /**
2076
     * Delete rows
2077
     * Use the query builder to create the where clause
2078
     * @param bool $deleteAll = When there is no where condition, setting to true will delete all
2079
     * @return int - total affected rows
2080
     * @throws Exception
2081
     */
2082
    public function delete($deleteAll=false)
2083
    {
2084
        list($sql, $params) = $this->deleteSqlQuery();
2085
        if ( empty($this->_where_conditions) && ! $deleteAll )
2086
        {
2087
            throw new Exception("You cannot update an entire table without calling update with deleteAll=true");
2088
        }
2089
        $this->execute($sql, $params);
2090
        return $this->rowCount();
2091
    }
2092
2093
    /**
2094
     * @param bool|false $force
2095
     * @return $this
2096
     * @throws Exception
2097
     */
2098
    public function truncate($force=false)
2099
    {
2100
        if ( $force )
2101
        {
2102
            $this->execute('SET FOREIGN_KEY_CHECKS = 0');
2103
        }
2104
        $this->execute("TRUNCATE TABLE {$this->_table_name}");
2105
        if ( $force )
2106
        {
2107
            $this->execute('SET FOREIGN_KEY_CHECKS = 1');
2108
        }
2109
        return $this;
2110
    }
2111
2112
    /**
2113
     * @return array
2114
     */
2115
    public function deleteSqlQuery()
2116
    {
2117
        $query  = "DELETE FROM {$this->_table_name}";
2118
        if ( !empty($this->_where_conditions) )
2119
        {
2120
            $query .= $this->_getWhereString(true);
2121
            return [$query, $this->_getWhereParameters()];
2122
        }
2123
        return [$query, []];
2124
    }
2125
2126
2127
    /**
2128
     * Return the aggregate count of column
2129
     *
2130
     * @param string $column
2131
     * @param int $cacheTtl
2132
     * @return int
2133
     */
2134
    public function count($column='*', $cacheTtl=self::CACHE_NO)
2135
    {
2136
        $this->explicitSelectMode();
2137
        $this->select("COUNT({$column})", 'cnt');
2138
        $result             = $this->fetchOne(null, $cacheTtl);
2139
        return ($result !== false && isset($result->cnt)) ? (float)$result->cnt : 0;
2140
    }
2141
2142
2143
    /**
2144
     * Return the aggregate max count of column
2145
     *
2146
     * @param string $column
2147
     * @param int $cacheTtl
2148
     * @return mixed|null
2149
     */
2150
    public function max($column, $cacheTtl=self::CACHE_NO)
2151
    {
2152
        $this->explicitSelectMode();
2153
        $this->select("MAX({$column})", 'max');
2154
        $result = $this->fetchOne(null, $cacheTtl);
2155
        return ( $result !== false && isset($result->max) ) ? $result->max : null;
2156
    }
2157
2158
2159
    /**
2160
     * Return the aggregate min count of column
2161
     *
2162
     * @param string $column
2163
     * @param int $cacheTtl
2164
     * @return mixed|null
2165
     */
2166
    public function min($column, $cacheTtl=self::CACHE_NO)
2167
    {
2168
        $this->explicitSelectMode();
2169
        $this->select("MIN({$column})", 'min');
2170
        $result = $this->fetchOne(null, $cacheTtl);
2171
        return ( $result !== false && isset($result->min) ) ? $result->min : null;
2172
    }
2173
2174
    /**
2175
     * Return the aggregate sum count of column
2176
     *
2177
     * @param string $column
2178
     * @param int $cacheTtl
2179
     * @return mixed|null
2180
     */
2181
    public function sum($column, $cacheTtl=self::CACHE_NO)
2182
    {
2183
        $this->explicitSelectMode();
2184
        $this->select("SUM({$column})", 'sum');
2185
        $result = $this->fetchOne(null, $cacheTtl);
2186
        return ( $result !== false && isset($result->sum) ) ? $result->sum : null;
2187
    }
2188
2189
    /**
2190
     * Return the aggregate average count of column
2191
     *
2192
     * @param string $column
2193
     * @param int $cacheTtl
2194
     * @return mixed|null
2195
     */
2196
    public function avg($column, $cacheTtl=self::CACHE_NO)
2197
    {
2198
        $this->explicitSelectMode();
2199
        $this->select("AVG({$column})", 'avg');
2200
        $result = $this->fetchOne(null, $cacheTtl);
2201
        return ( $result !== false && isset($result->avg) ) ? $result->avg : null;
2202
    }
2203
2204
    /*******************************************************************************/
2205
// Utilities methods
2206
2207
    /**
2208
     * Reset fields
2209
     *
2210
     * @return $this
2211
     */
2212
    public function reset()
2213
    {
2214
        $this->_where_parameters        = [];
2215
        $this->_select_fields           = [];
2216
        $this->_join_sources            = [];
2217
        $this->_join_aliases            = [];
2218
        $this->_where_conditions        = [];
2219
        $this->_limit                   = null;
2220
        $this->_offset                  = null;
2221
        $this->_order_by                = [];
2222
        $this->_group_by                = [];
2223
        $this->_and_or_operator         = self::OPERATOR_AND;
2224
        $this->_having                  = [];
2225
        $this->_wrap_open               = false;
2226
        $this->_last_wrap_position      = 0;
2227
        $this->_pdo_stmt                = null;
2228
        $this->_distinct                = false;
2229
        $this->_requested_fields        = null;
2230
        $this->_filter_meta             = null;
2231
        $this->_cache_ttl               = -1;
2232
        $this->_timer                   = [];
2233
        $this->_built_query             = null;
2234
        $this->_paging_meta             = [];
2235
        $this->_raw_sql                 = null;
2236
        $this->_explicit_select_mode    = false;
2237
        return $this;
2238
    }
2239
2240
    /**
2241
     * Return a YYYY-MM-DD HH:II:SS date format
2242
     *
2243
     * @param string $datetime - An english textual datetime description
2244
     *          now, yesterday, 3 days ago, +1 week
2245
     *          http://php.net/manual/en/function.strtotime.php
2246
     * @return string YYYY-MM-DD HH:II:SS
2247
     */
2248
    public static function NOW($datetime = 'now')
2249
    {
2250
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
2251
    }
2252
2253
    /**
2254
     * Return a string containing the given number of question marks,
2255
     * separated by commas. Eg '?, ?, ?'
2256
     *
2257
     * @param int - total of placeholder to insert
2258
     * @return string
2259
     */
2260
    protected function _makePlaceholders($number_of_placeholders=1)
2261
    {
2262
        return implode(', ', array_fill(0, $number_of_placeholders, '?'));
2263
    }
2264
2265
    /**
2266
     * Format the table{Primary|Foreign}KeyName
2267
     *
2268
     * @param  string $pattern
2269
     * @param  string $tableName
2270
     * @return string
2271
     */
2272
    protected function _formatKeyName($pattern, $tableName)
2273
    {
2274
        return sprintf($pattern, $tableName);
2275
    }
2276
2277
    /**
2278
     * @param string $query
2279
     * @param array $parameters
2280
     *
2281
     * @return array
2282
     */
2283
    protected function _logQuery($query, array $parameters)
2284
    {
2285
        $query = $this->buildQuery($query, $parameters);
2286
        if ( ! $this->_log_queries )
2287
        {
2288
            return [null, null];
2289
        }
2290
        $ident = substr(str_shuffle(md5($query)), 0, 10);
2291
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
2292
        $this->_timer['start'] = microtime(true);
2293
        return [$query, $ident];
2294
    }
2295
2296
    /**
2297
     * @param $ident
2298
     * @param $builtQuery
2299
     */
2300
    protected function _logSlowQueries($ident, $builtQuery)
2301
    {
2302
        if ( ! $this->_log_queries )
2303
        {
2304
            return ;
2305
        }
2306
        $this->_timer['end']    = microtime(true);
2307
        $seconds_taken          = round($this->_timer['end'] - $this->_timer['start'], 3);
2308
        if ( $seconds_taken > $this->_slow_query_secs )
2309
        {
2310
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$seconds_taken} seconds:\n{$builtQuery}");
2311
        }
2312
    }
2313
2314
    /**
2315
     * @return float
2316
     */
2317
    public function getTimeTaken()
2318
    {
2319
        $seconds_taken = $this->_timer['end'] - $this->_timer['start'];
2320
        return $seconds_taken;
2321
    }
2322
2323
    /**
2324
     * @param $secs
2325
     * @return $this
2326
     */
2327
    public function slowQuerySeconds($secs)
2328
    {
2329
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
2330
        $this->_slow_query_secs = $secs;
2331
        return $this;
2332
    }
2333
2334
2335
    /**
2336
     * @param       $field
2337
     * @param array $values
2338
     * @param null  $placeholder_prefix
2339
     *
2340
     * @return array
2341
     */
2342
    public function getNamedWhereIn($field, array $values, $placeholder_prefix=null)
2343
    {
2344
        Assert($field)->string()->notEmpty();
2345
        Assert($values)->isArray();
2346
        if ( empty($values) )
2347
        {
2348
            return ['', []];
2349
        }
2350
        $placeholder_prefix     = !is_null($placeholder_prefix) ? $placeholder_prefix : strtolower(str_replace('.', '__', $field));
2351
        $params = $placeholders = [];
2352
        $count                  = 1;
2353
        foreach ( $values as $val )
2354
        {
2355
            $name           = "{$placeholder_prefix}_{$count}";
2356
            $params[$name]  = $val;
2357
            $placeholders[] = ":{$name}";
2358
            $count++;
2359
        }
2360
        $placeholders = implode(',', $placeholders);
2361
        return ["AND {$field} IN ({$placeholders})\n", $params];
2362
    }
2363
2364
    /**
2365
     * @param        $field
2366
     * @param string $delimiter
2367
     *
2368
     * @return array
2369
     */
2370
    protected function _getColumnAliasParts($field, $delimiter=':')
2371
    {
2372
        $parts = explode($delimiter, $field);
2373
        if ( count($parts) === 2 )
2374
        {
2375
            return $parts;
2376
        }
2377
        return ['', $field];
2378
    }
2379
2380
    /**
2381
     * @param string $column
2382
     * @param string $term
2383
     * @return $this
2384
     */
2385
    protected function _addWhereClause($column, $term)
2386
    {
2387
        $modifiers = [
2388
            'whereLike'         => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
2389
            'whereNotLike'      => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
2390
            'whereLt'           => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
2391
            'whereLte'          => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
2392
            'whereGt'           => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
2393
            'whereGte'          => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
2394
            'whereBetween'      => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
2395
            'whereNotBetween'  => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
2396
        ];
2397
        foreach ( $modifiers as $func => $regex )
2398
        {
2399
            if ( preg_match($regex, $term, $matches) )
2400
            {
2401
                array_shift($matches);
2402
                switch ($func)
2403
                {
2404
                    case 'whereLike':
2405
2406
                        return $this->whereLike($column, $matches[0]);
2407
2408
                    case 'whereNotLike':
2409
2410
                        return $this->whereNotLike($column, $matches[0]);
2411
2412
                    case 'whereLt':
2413
2414
                        return $this->whereLt($column, $matches[0]);
2415
2416
                    case 'whereLte':
2417
2418
                        return $this->whereLte($column, $matches[0]);
2419
2420
                    case 'whereGt':
2421
2422
                        return $this->whereGt($column, $matches[0]);
2423
2424
                    case 'whereGte':
2425
2426
                        return $this->whereGte($column, $matches[0]);
2427
2428
                    case 'whereBetween':
2429
2430
                        return $this->whereBetween($column, $matches[0], $matches[1]);
2431
2432
                    case 'whereNotBetween':
2433
2434
                        return $this->whereNotBetween($column, $matches[0], $matches[1]);
2435
2436
                }
2437
            }
2438
        }
2439
        return $this->where($column, $term);
2440
    }
2441
2442
    public function destroy()
2443
    {
2444
        if ( !is_null($this->_pdo_stmt) )
2445
        {
2446
            $this->_pdo_stmt->closeCursor();
2447
        }
2448
        $this->_pdo_stmt    = null;
2449
        $this->_handlers    = [];
2450
    }
2451
2452
    public function __destruct()
2453
    {
2454
        $this->destroy();
2455
    }
2456
2457
    /** @var string */
2458
    static protected $_model_namespace = '';
2459
2460
    /** @var bool */
2461
    protected $_validation_exceptions = true;
2462
2463
    /** @var array */
2464
    protected $_paging_meta           = [];
2465
2466
    /** @var int */
2467
    protected $_default_max           = 100;
2468
2469
    /**
2470
     * Load a model
2471
     *
2472
     * @param string $model_name
2473
     * @param AbstractPdo $connection
2474
     * @return FluentPdoModel
2475
     * @throws ModelNotFoundException
2476
     */
2477
    public static function loadModel($model_name, AbstractPdo $connection=null)
2478
    {
2479
        $model_name = static::$_model_namespace . $model_name;
2480
        if ( ! class_exists($model_name) )
2481
        {
2482
            throw new ModelNotFoundException("Failed to find model class {$model_name}.");
2483
        }
2484
        return new $model_name($connection);
2485
    }
2486
2487
    /**
2488
     * Load a model
2489
     *
2490
     * @param string      $table_name
2491
     * @param AbstractPdo $connection
2492
     * @return $this
2493
     */
2494
    public static function loadTable($table_name, AbstractPdo $connection=null)
2495
    {
2496
        $model_name = Inflector::classify($table_name);
2497
        Assert($model_name)->notEmpty("Could not resolve model name from table name.");
2498
        return static::loadModel($model_name, $connection);
0 ignored issues
show
Bug introduced by
It seems like $model_name defined by \Terah\FluentPdoModel\In...::classify($table_name) on line 2496 can also be of type boolean; however, Terah\FluentPdoModel\FluentPdoModel::loadModel() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2499
    }
2500
2501
    /**
2502
     * @param string   $columnName
2503
     * @param int $cacheTtl
2504
     * @param bool $flushCache
2505
     * @return bool
2506
     */
2507
    public function columnExists($columnName, $cacheTtl=self::CACHE_NO, $flushCache=false)
2508
    {
2509
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
2510
        return array_key_exists($columnName, $columns);
2511
    }
2512
2513
    /**
2514
     * @param int $cacheTtl
2515
     * @param bool $flushCache
2516
     * @return $this
2517
     */
2518
    public function loadSchemaFromDb($cacheTtl=self::CACHE_NO, $flushCache=false)
2519
    {
2520
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
2521
        $this->schema($schema);
2522
        return $this;
2523
    }
2524
2525
    /**
2526
     * @param int $cacheTtl
2527
     * @param bool $flushCache
2528
     * @return array
2529
     * @throws \Terah\Assert\AssertionFailedException
2530
     */
2531
    public function getSchemaFromDb($cacheTtl=self::CACHE_NO, $flushCache=false)
2532
    {
2533
        $table      = $this->getTableName();
2534
        Assert($table)->string()->notEmpty();
2535
        $schema     = [];
2536
        $columns    = $this->_getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
2537
        foreach ( $columns[$table] as $column => $meta )
2538
        {
2539
            $schema[$column] = $meta->data_type;
2540
        }
2541
        return $schema;
2542
    }
2543
2544
    /**
2545
     * @param string $table
2546
     * @param int $cacheTtl
2547
     * @param bool $flushCache
2548
     * @return array
2549
     */
2550
    protected function _getColumnsByTableFromDb($table, $cacheTtl=self::CACHE_NO, $flushCache=false)
2551
    {
2552
        Assert($table)->string()->notEmpty();
2553
2554
        $callback = function() use ($table) {
2555
2556
            return $this->_connection->getColumns(true, $table);
2557
        };
2558
        $cacheKey   = '/column_schema/' . $table;
2559
        if ( $flushCache === true )
2560
        {
2561
            $this->clearCache($cacheKey);
2562
        }
2563
        return $this->_cacheData($cacheKey, $callback, $cacheTtl);
2564
    }
2565
2566
    /**
2567
     * @param string $table
2568
     * @return bool
2569
     */
2570
    public function clearSchemaCache($table)
2571
    {
2572
        return $this->clearCache('/column_schema/' . $table);
2573
    }
2574
2575
    /**
2576
     * @param stdClass $record
2577
     * @return stdClass
2578
     */
2579
    public function onFetch(stdClass $record)
2580
    {
2581
        $record = $this->_trimAndLowerCaseKeys($record);
2582
        if ( $this->_filter_on_fetch )
2583
        {
2584
            $record = $this->cleanseRecord($record);
2585
        }
2586
        return $this->fixTypes($record);
2587
    }
2588
2589
    /**
2590
     * @param stdClass $record
2591
     * @return stdClass
2592
     */
2593
    public function cleanseRecord(stdClass $record)
2594
    {
2595
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2596
        {
2597
            if ( is_string($record->$field) )
2598
            {
2599
                $record->$field = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", filter_var($record->$field, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES));
2600
                if ( $this->_log_filter_changes && $value !== $record->$field )
2601
                {
2602
                    $table = $this->_table_name ? $this->_table_name : '';
2603
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->$field]);
2604
                }
2605
            }
2606
        }
2607
        return $record;
2608
    }
2609
2610
    /**
2611
     * @param stdClass $record
2612
     * @param string   $type
2613
     * @return stdClass
2614
     */
2615
    public function beforeSave(stdClass $record, $type)
2616
    {
2617
        $record = $this->addDefaultFields($record, $type);
2618
        $record = $this->applyGlobalModifiers($record, $type);
2619
        $record = $this->applyHandlers($record, $type);
2620
        $record = $this->removeUnneededFields($record, $type);
2621
        return $record;
2622
    }
2623
2624
    /**
2625
     * @param stdClass $record
2626
     * @param string   $type
2627
     * @return stdClass
2628
     */
2629
    public function afterSave(stdClass $record, $type)
2630
    {
2631
        unset($type);
2632
        $this->clearCacheByTable();
2633
        return $record;
2634
    }
2635
2636
    /**
2637
     * @param stdClass $record
2638
     * @param string   $type
2639
     * @return stdClass
2640
     */
2641
    public function addDefaultFields(stdClass $record, $type)
2642
    {
2643
        unset($type);
2644
        return $record;
2645
    }
2646
2647
    /**
2648
     * @param stdClass $record
2649
     * @param string   $type
2650
     * @return stdClass
2651
     */
2652
    public function applyGlobalModifiers(stdClass $record, $type)
2653
    {
2654
        unset($type);
2655
        return $record;
2656
    }
2657
2658
    /**
2659
     * @param stdClass $record
2660
     * @param string   $type
2661
     * @return stdClass
2662
     */
2663
    public function removeUnneededFields(\stdClass $record, $type)
2664
    {
2665
        unset($type);
2666
        // remove un-needed fields
2667
        $columns = $this->getColumns(true);
2668
        if ( empty($columns) )
2669
        {
2670
            return $record;
2671
        }
2672
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2673
        {
2674
            if ( ! in_array($name, $columns) || in_array($name, $this->_virtual_fields) )
2675
            {
2676
                unset($record->$name);
2677
            }
2678
        }
2679
        return $record;
2680
    }
2681
2682
    /**
2683
     * @param array $data
2684
     * @param string $saveType
2685
     * @return array
2686
     */
2687
    public function cleanseWebData($data, $saveType)
2688
    {
2689
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
2690
        $columns = $this->getColumns(false);
2691
        if ( empty($columns) )
2692
        {
2693
            return $data;
2694
        }
2695
        foreach ( $data as $field => $val )
2696
        {
2697
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
2698
        }
2699
        return array_intersect_key($data, $columns);
2700
    }
2701
2702
    /**
2703
     * @return array
2704
     */
2705
    public function skeleton()
2706
    {
2707
        $skel       = [];
2708
        $columns    = $this->columns(false);
2709
        foreach ( $columns as $column => $type )
2710
        {
2711
            $skel[$column] = null;
2712
        }
2713
        return $skel;
2714
    }
2715
2716
2717
    /**
2718
     * @return Closure[]
2719
     */
2720
    protected function _getFieldHandlers()
2721
    {
2722
        return [];
2723
    }
2724
2725
    /**
2726
     * @param bool $toString
2727
     * @return array|string
2728
     */
2729
    public function getErrors($toString=false)
2730
    {
2731
        if ( $toString )
2732
        {
2733
            $errors = [];
2734
            foreach ( $this->_errors as $field => $error )
2735
            {
2736
                $errors[] = implode("\n", $error);
2737
            }
2738
            return implode("\n", $errors);
2739
        }
2740
        return $this->_errors;
2741
    }
2742
2743
    /**
2744
     * @param bool $throw
2745
     * @return $this
2746
     */
2747
    public function validationExceptions($throw=true)
2748
    {
2749
        $this->_validation_exceptions = $throw;
2750
        return $this;
2751
    }
2752
2753
    /**
2754
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
2755
     *
2756
     * @return $this
2757
     * @throws Exception
2758
     */
2759
    public function paginate(array $query=[])
2760
    {
2761
        $_fields = $_order = $_limit = $_offset = null;
2762
        extract($query);
2763
        $this->_setLimit($_limit, $_offset);
2764
        $this->_setOrderBy($_order);
2765
        $this->_setFields($_fields);
2766
        return $this;
2767
    }
2768
2769
    /**
2770
     * @param null|string $limit
2771
     * @param null|string|int $offset
2772
     * @return $this
2773
     */
2774
    protected function _setLimit($limit=null, $offset=null)
2775
    {
2776
        $limit      = ! $limit || (int)$limit > (int)$this->_default_max ? (int)$this->_default_max : (int)$limit;
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type null|string is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2777
        if ( ! is_numeric($limit) )
2778
        {
2779
            return $this;
2780
        }
2781
        $this->limit((int)$limit);
2782
        if ( $offset && is_numeric($offset) )
2783
        {
2784
            $this->offset((int)$offset);
2785
        }
2786
        return $this;
2787
    }
2788
2789
    /**
2790
     * @param null|string|array $fields
2791
     * @return $this
2792
     * @throws Exception
2793
     */
2794
    protected function _setFields($fields=null)
2795
    {
2796
        if ( ! $fields )
2797
        {
2798
            return $this;
2799
        }
2800
        $this->explicitSelectMode();
2801
        $columns    = $this->getColumns();
2802
        $fields     = is_array($fields) ? $fields : explode('|', $fields);
2803
2804
        foreach ( $fields as $idx => $field )
2805
        {
2806
            list($alias, $field) = $this->_getColumnAliasParts($field);
2807
            $field = $field === '_display_field' ? $this->_display_column : $field;
2808
            // Regular primary table field
2809
            if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
2810
            {
2811
                $this->select("{$this->_table_alias}.{$field}");
2812
                $this->_requested_fields[] = "{$this->_table_alias}.{$field}";
2813
                continue;
2814
            }
2815
            // Reference table field with alias
2816
            if ( ! empty($alias) )
2817
            {
2818
                Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
2819
                Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
2820
                list(, , $join_field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
2821
                $this->autoJoin($alias, static::LEFT_JOIN, false);
2822
                $this->select($join_field, $fieldAlias);
2823
                $this->_requested_fields[] = "{$join_field} AS {$fieldAlias}";
2824
                continue;
2825
            }
2826
            // Reference table select field without alias
2827
            foreach ( $this->_associations['belongsTo'] as $joinAlias => $config )
2828
            {
2829
                list(, , $join_field, $fieldAlias) = $config;
2830
                if ( $field === $config[3] )
2831
                {
2832
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
2833
                    $this->select($join_field, $fieldAlias);
2834
                    $this->_requested_fields[] = "{$join_field} AS {$fieldAlias}";
2835
                    continue;
2836
                }
2837
            }
2838
        }
2839
        return $this;
2840
    }
2841
2842
    /**
2843
     * @param null|string $orderBy
2844
     * @return $this|FluentPdoModel
2845
     */
2846
    protected function _setOrderBy($orderBy=null)
2847
    {
2848
        if ( ! $orderBy )
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderBy of type null|string is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2849
        {
2850
            return $this;
2851
        }
2852
        $columns                    = $this->getColumns();
2853
        list($order, $direction)    = strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
2854
        list($alias, $field)        = $this->_getColumnAliasParts(trim($order), '.');
2855
        $field                      = explode(' ', $field);
2856
        $field                      = trim($field[0]);
2857
        $direction                  = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
2858
2859
        // Regular primary table order by
2860
        if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
2861
        {
2862
            return $this->orderBy("{$this->_table_alias}.{$field}", $direction);
2863
        }
2864
        // Reference table order by with alias
2865
        if ( ! empty($alias) )
2866
        {
2867
            Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
2868
            Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
2869
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
2870
        }
2871
        // Reference table order by without alias
2872
        foreach ( $this->_associations['belongsTo'] as $joinAlias => $config )
2873
        {
2874
            if ( $field === $config[3] )
2875
            {
2876
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
2877
            }
2878
        }
2879
        return $this;
2880
    }
2881
2882
    /**
2883
     * @param null $type
2884
     * @return array
2885
     */
2886
    public function getPagingMeta($type=null)
2887
    {
2888
        if ( empty($this->_paging_meta) )
2889
        {
2890
            $this->setPagingMeta();
2891
        }
2892
        return is_null($type) ? $this->_paging_meta : $this->_paging_meta[$type];
2893
    }
2894
2895
    /**
2896
     * @return $this
2897
     */
2898
    public function setPagingMeta()
2899
    {
2900
        $model      = clone $this;
2901
        $limit      = intval($this->getLimit());
2902
        $offset     = intval($this->getOffset());
2903
        $total      = intval($model->withBelongsTo()->select(null)->offset(null)->limit(null)->orderBy(null)->count());
2904
        unset($model->_handlers, $model); //hhmv mem leak
2905
        $order_bys  = !is_array($this->_order_by) ? [] : $this->_order_by;
2906
        $this->_paging_meta  = [
2907
            'limit'     => $limit,
2908
            'offset'    => $offset,
2909
            'page'      => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
2910
            'pages'     => $limit === 0 ? 1 : intval(ceil($total / $limit)),
2911
            'order'     => $order_bys,
2912
            'total'     => $total = $limit === 1 && $total > 1 ? 1 : $total,
2913
            'filters'   => is_null($this->_filter_meta) ? [] : $this->_filter_meta,
2914
            'fields'    => is_null($this->_requested_fields) ? [] : $this->_requested_fields,
2915
            'perms'     => [],
2916
        ];
2917
        return $this;
2918
    }
2919
2920
    /**
2921
     * Take a web request and format a query
2922
     *
2923
     * @param array $query
2924
     *
2925
     * @return $this
2926
     * @throws Exception
2927
     */
2928
    public function filter(array $query=[])
2929
    {
2930
        $columns   = $this->getColumns(false);
2931
        $alias     = '';
2932
        foreach ( $query as $column => $value )
2933
        {
2934
            if ( in_array($column, $this->_pagination_attribs) )
2935
            {
2936
                continue;
2937
            }
2938
            $field = $this->_findFieldByQuery($column, $this->_display_column);
2939
            if ( is_null($field) )
2940
            {
2941
                continue;
2942
            }
2943
            $this->_filter_meta[$field]     = $value;
2944
            $where                          = !is_array($value) && mb_stripos($value, '|') !== false ? explode('|', $value) : $value;
2945
            if ( is_array($where) )
2946
            {
2947
                $this->whereIn($field, $where);
2948
            }
2949
            else
2950
            {
2951
                $this->_addWhereClause($field, $where);
2952
            }
2953
        }
2954
        if ( empty($query['_search']) )
2955
        {
2956
            return $this;
2957
        }
2958
        $alias          = !empty($alias) ? $alias : $this->_table_alias;
2959
        $string_cols    = array_filter($columns, function($type) {
2960
2961
            return in_array($type, ['varchar', 'text', 'enum']);
2962
        });
2963
        $terms          = explode('|', $query['_search']);
2964
        $where_likes    = [];
2965
        foreach ( $string_cols as $column => $type )
2966
        {
2967
            foreach ( $terms as $term )
2968
            {
2969
                $where_likes["{$alias}.{$column}"] = "%{$term}%";
2970
            }
2971
        }
2972
        // Reference fields...
2973
        $belongsTo = $this->getSearchableAssociations();
2974
        foreach ( $belongsTo as $alias => $config )
2975
        {
2976
            foreach ( $terms as $term )
2977
            {
2978
                $where_likes[$config[2]] = "%{$term}%";
2979
            }
2980
        }
2981
        if ( empty($where_likes) )
2982
        {
2983
            return $this;
2984
        }
2985
        $this->where([1=>1])->wrap()->_and();
2986
        foreach ( $where_likes as $column => $term )
2987
        {
2988
            $this->_or()->whereLike($column, $term);
2989
        }
2990
        $this->wrap();
2991
        return $this;
2992
    }
2993
2994
    /**
2995
     * @param string $column
2996
     * @param string $displayCol
2997
     * @return string|null
2998
     */
2999
    protected function _findFieldByQuery($column, $displayCol)
3000
    {
3001
        list($alias, $field)    = $this->_getColumnAliasParts($column);
3002
        $field                  = $field === '_display_field' ? $displayCol : $field;
3003
        $columns                = $this->getColumns();
3004
        $tableAlias             = $this->getTableAlias();
3005
        if ( ! empty($alias) && $alias === $tableAlias )
3006
        {
3007
            // Alias is set but the field isn't correct
3008
            if ( ! in_array($field, $columns) )
3009
            {
3010
                return null;
3011
            }
3012
            return "{$alias}.{$field}";
3013
        }
3014
        // Alias isn't passed in but the field is ok
3015
        if ( empty($alias) && in_array($field, $columns) )
3016
        {
3017
            return "{$tableAlias}.{$field}";
3018
        }
3019
        // Alias is passed but not this table in but there is a matching field on this table
3020
        if ( ! empty($alias) && in_array($field, $columns) )
3021
        {
3022
            return null;
3023
        }
3024
        // Now search the associations for the field
3025
        $associations = $this->getSearchableAssociations();
3026
        if ( ! empty($alias) )
3027
        {
3028
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
3029
            {
3030
                return "{$alias}.{$field}";
3031
            }
3032
            return null;
3033
        }
3034
        foreach ( $associations as $assocAlias => $config )
3035
        {
3036
            list(, , $assocField, $fieldAlias) = $config;
3037
            if ( $fieldAlias === $field )
3038
            {
3039
                return $assocField;
3040
            }
3041
        }
3042
        return null;
3043
    }
3044
3045
    public function getSearchableAssociations()
3046
    {
3047
        $belongsTo = ! empty($this->_associations['belongsTo']) ? $this->_associations['belongsTo'] : [];
3048
        return $belongsTo;
3049
    }
3050
3051
    /**
3052
     * @param $keys_only
3053
     * @return array
3054
     */
3055
3056
    public function columns($keys_only=true)
3057
    {
3058
        return $keys_only ? array_keys($this->_schema) : $this->_schema;
3059
    }
3060
3061
    /**
3062
     * @param string $field
3063
     * @param mixed $value
3064
     * @param bool|false $permissive
3065
     * @return float|int|null|string
3066
     */
3067
    protected function _fixType($field, $value, $permissive=false)
3068
    {
3069
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
3070
        if ( empty($this->_schema) || ( ! array_key_exists($field, $this->_schema) && $permissive ) )
3071
        {
3072
            return $value;
3073
        }
3074
        $columns    = $this->getColumns(false);
3075
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
3076
3077
        $field_type = ! empty($columns[$field]) ? $columns[$field] : null;
3078
3079
        if ( is_null($value) )
3080
        {
3081
            return null;
3082
        }
3083
        // return on null, '' but not 0
3084
        if ( ! is_numeric($value) && empty($value) )
3085
        {
3086
            return null;
3087
        }
3088
        // Don't cast invalid values... only those that can be cast cleanly
3089
        switch ($field_type)
3090
        {
3091
            case 'varchar':
3092
            case 'text';
3093
            case 'date':
3094
            case 'datetime':
3095
            case 'timestamp':
3096
                return (string)$value;
3097
            case 'int':
3098
            case 'tinyint':
3099
                return ! is_numeric($value) ? $value : (int)$value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return !is_numeric($valu... $value : (int) $value; (string|object|array|boolean|integer) is incompatible with the return type documented by Terah\FluentPdoModel\FluentPdoModel::_fixType of type double|integer|null|string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
3100
            case 'decimal':
3101
                return ! is_numeric($value) ? $value : (float)$value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return !is_numeric($valu...alue : (double) $value; (string|object|array|boolean|double) is incompatible with the return type documented by Terah\FluentPdoModel\FluentPdoModel::_fixType of type double|integer|null|string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
3102
            default:
3103
                return $value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $value; (integer|double|string|object|array|boolean) is incompatible with the return type documented by Terah\FluentPdoModel\FluentPdoModel::_fixType of type double|integer|null|string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
3104
        }
3105
    }
3106
3107
    /**
3108
     * @param stdClass $record
3109
     * @param string $type
3110
     * @return stdClass
3111
     */
3112
    public function fixTypes(stdClass $record, $type=null)
3113
    {
3114
        foreach ( $this->getColumns(false) as $column => $field_type )
3115
        {
3116
            if ( ! property_exists($record, $column) || is_null($record->$column) )
3117
            {
3118
                continue;
3119
            }
3120
            $record->$column = $this->_fixType($column, $record->$column);
3121
        }
3122
        unset($type);
3123
        return $record;
3124
    }
3125
3126
    /**
3127
     * @param stdClass $record
3128
     * @param string   $type
3129
     * @return stdClass
3130
     * @throws Exception
3131
     */
3132
    public function applyHandlers(stdClass $record, $type='INSERT')
3133
    {
3134
        $this->_compileHandlers();
3135
        $this->_errors                  = [];
3136
        // Disable per field exceptions so we can capture all errors for the record
3137
        $tmpExceptions                  = $this->_validation_exceptions;
3138
        $this->_validation_exceptions   = false;
3139
        foreach ( $this->_handlers as $field => $fn_validator )
3140
        {
3141
            if ( ! property_exists($record, $field) )
3142
            {
3143
                // If the operation is an update it can be a partial update
3144
                if ( $type === self::SAVE_UPDATE )
3145
                {
3146
                    continue;
3147
                }
3148
                $record->$field = null;
3149
            }
3150
            $record->$field = $this->applyHandler($field, $record->$field, $type, $record);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $record->{$field} is correct as $this->applyHandler($fie...field}, $type, $record) (which targets Terah\FluentPdoModel\Flu...doModel::applyHandler()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3151
        }
3152
        $this->_validation_exceptions = $tmpExceptions;
3153
        if ( $this->_validation_exceptions && ! empty($this->_errors) )
3154
        {
3155
            throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
0 ignored issues
show
Bug introduced by
It seems like $this->getErrors() targeting Terah\FluentPdoModel\FluentPdoModel::getErrors() can also be of type string; however, Terah\FluentPdoModel\Mod...xception::__construct() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
3156
        }
3157
        return $record;
3158
    }
3159
3160
    /**
3161
     * @param string $field
3162
     * @param mixed $value
3163
     * @param string $type
3164
     * @param stdClass $record
3165
     * @return null
3166
     * @throws Exception
3167
     */
3168
    protected function applyHandler($field, $value, $type=null, $record=null)
3169
    {
3170
        $this->_compileHandlers();
3171
        $fnHandler = ! empty($this->_handlers[$field]) ? $this->_handlers[$field] : null;
3172
        if ( is_callable($fnHandler) )
3173
        {
3174
            try
3175
            {
3176
                $value = $fnHandler($field, $value, $type, $record);
3177
            }
3178
            catch( Exception $e )
3179
            {
3180
                $this->_errors[$field][] = $e->getMessage();
3181
                if ( $this->_validation_exceptions && ! empty($this->_errors) )
3182
                {
3183
                    throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
0 ignored issues
show
Bug introduced by
It seems like $this->getErrors() targeting Terah\FluentPdoModel\FluentPdoModel::getErrors() can also be of type string; however, Terah\FluentPdoModel\Mod...xception::__construct() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
3184
                }
3185
                return null;
3186
            }
3187
        }
3188
        return $value;
3189
    }
3190
3191
    protected function _compileHandlers()
3192
    {
3193
        if ( $this->_handlers )
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_handlers 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...
3194
        {
3195
            return;
3196
        }
3197
        $this->_handlers    = array_merge([], $this->_getFieldHandlers());
3198
    }
3199
}
3200