Completed
Push — master ( 7d6fbb...2dc4cf )
by Terry
02:18
created

FluentPdoModel::clearCacheByTable()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 3
eloc 5
nc 4
nop 1
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\RedisCache;
26
use function Terah\Assert\Assert;
27
use function Terah\Assert\Validate;
28
29
use Prism\Models\Model;
30
31
/**
32
 * Class FluentPdoModel
33
 *
34
 * @package Terah\FluentPdoModel
35
 * @author  Terry Cullen - [email protected]
36
 */
37
class FluentPdoModel
38
{
39
    const OPERATOR_AND              = ' AND ';
40
    const OPERATOR_OR               = ' OR ';
41
    const ORDERBY_ASC               = 'ASC';
42
    const ORDERBY_DESC              = 'DESC';
43
    const SAVE_INSERT               = 'INSERT';
44
    const SAVE_UPDATE               = 'UPDATE';
45
    const LEFT_JOIN                 = 'LEFT';
46
    const INNER_JOIN                = 'INNER';
47
    const ONE_DAY                   = 86400;
48
    const ONE_WEEK                  = 60480060;
49
    const ONE_HOUR                  = 3600;
50
    const TEN_MINS                  = 600;
51
    const CACHE_NO                  = -1;
52
    const CACHE_DEFAULT             = 0;
53
54
    /** @var AbstractPdo $_connection */
55
    protected $_connection          = null;
56
57
    /** @var string */
58
    protected $_primary_key         = 'id';
59
60
    /** @var array */
61
    protected $_where_parameters    = [];
62
63
    /** @var array */
64
    protected $_select_fields       = [];
65
66
    /** @var array */
67
    protected $_join_sources        = [];
68
69
    /** @var array */
70
    protected $_join_aliases        = [];
71
72
    /** @var array $_associations */
73
    protected $_associations        = [
74
        'belongsTo' => [],
75
    ];
76
77
    /** @var array */
78
    protected $_where_conditions    = [];
79
80
    protected $_raw_sql             = null;
81
82
    /** @var int */
83
    protected $_limit               = null;
84
85
    /** @var int */
86
    protected $_offset              = null;
87
88
    /** @var array */
89
    protected $_order_by            = [];
90
91
    /** @var array */
92
    protected $_group_by            = [];
93
94
    /** @var string */
95
    protected $_and_or_operator     = self::OPERATOR_AND;
96
97
    /** @var array */
98
    protected $_having              = [];
99
100
    /** @var bool */
101
    protected $_wrap_open           = false;
102
103
    /** @var int */
104
    protected $_last_wrap_position  = 0;
105
106
    /** @var PDOStatement $_pdo_stmt */
107
    protected $_pdo_stmt            = null;
108
109
    /** @var bool */
110
    protected $_distinct            = false;
111
112
    /** @var null */
113
    protected $_requested_fields    = [];
114
115
    /** @var null */
116
    protected $_filter_meta         = [];
117
118
    /** @var bool */
119
    protected $_log_queries         = false;
120
121
    /** @var array */
122
    protected $_timer               = [];
123
124
    /** @var int */
125
    protected $_slow_query_secs     = 5;
126
127
    /** @var array */
128
    protected $_pagination_attribs  = [
129
        '_limit',
130
        '_offset',
131
        '_order',
132
        '_fields',
133
        '_search'
134
    ];
135
136
    /** @var string $_table_name */
137
    protected $_table_name          = null;
138
139
    /** @var string $_table_alias */
140
    protected $_table_alias         = null;
141
142
    /** @var string $_display_column */
143
    protected $_display_column      = null;
144
145
    /** @var string $_connection_name */
146
    protected $_connection_name     = null;
147
148
    /** @var array $_schema */
149
    protected $_schema              = [];
150
151
    /** @var array $_virtual_fields */
152
    protected $_virtual_fields      = [];
153
154
    /** @var array $_errors */
155
    protected $_errors              = [];
156
157
    /**
158
     * @var int - true  = connection default x days
159
     *          - false = no cache
160
     *          - int   = a specific amount
161
     */
162
    protected $_cache_ttl               = self::CACHE_NO;
163
164
    /** @var string */
165
    protected $_tmp_table_prefix        = 'tmp_';
166
167
    /** @var null|string */
168
    protected $_built_query             = null;
169
170
    // Make handlers public so whe can delete them externally to reduce memory in hhmv
171
    //protected $_handlers              = [];
172
    public $_handlers                   = [];
173
174
    /** @var bool User want to directly specify the fields */
175
    protected $_explicit_select_mode    = false;
176
177
    /** @var string[] */
178
    protected $_update_raw              = [];
179
180
    protected $_max_callback_failures   = null;
181
182
    protected $_num_callback_failures   = 0;
183
184
    protected $_filter_on_fetch         = false;
185
186
    protected $_log_filter_changes      = true;
187
188
    protected $_include_count           = false;
189
190
    /**
191
     * @param AbstractPdo|null $connection
192
     */
193
    public function __construct(AbstractPdo $connection=null)
194
    {
195
        $connection = !is_null($connection) ? $connection : ConnectionPool::get($this->_connection_name);
196
        $this->_connection  = $connection;
197
        $this->_log_queries = $connection->logQueries();
198
        $this->init();
199
    }
200
201
    public function init()
202
    {}
203
204
    /**
205
     * @return AbstractPdo
206
     * @throws Exception
207
     */
208
    public function getPdo()
209
    {
210
        return $this->_connection;
211
    }
212
213
    /**
214
     * @return AbstractLogger
215
     */
216
    public function getLogger()
217
    {
218
        return $this->_connection->getLogger();
219
    }
220
221
    /**
222
     * @return RedisCache
223
     */
224
    public function getCache()
225
    {
226
        return $this->_connection->getCache();
227
    }
228
229
    /**
230
     * Define the working table and create a new instance
231
     *
232
     * @param string $tableName - Table name
233
     * @param string $alias     - The table alias name
234
     * @param string $displayColumn
235
     * @param string $primaryKeyName
236
     *
237
     * @return $this
238
     */
239
    public function table($tableName, $alias='', $displayColumn='', $primaryKeyName='id')
240
    {
241
        return $this->reset()
242
            ->tableName($tableName)
243
            ->tableAlias($alias)
244
            ->displayColumn($displayColumn)
245
            ->primaryKeyName($primaryKeyName);
246
    }
247
248
    /**
249
     * @param $primaryKeyName
250
     * @return $this
251
     */
252
    public function primaryKeyName($primaryKeyName)
253
    {
254
        $this->_primary_key = $primaryKeyName;
255
        return $this;
256
    }
257
258
    /**
259
     * @param $tableName
260
     *
261
     * @return $this
262
     */
263
    public function tableName($tableName)
264
    {
265
        $this->_table_name  = $tableName;
266
        return $this;
267
    }
268
269
    /**
270
     * @param $explicitSelect
271
     *
272
     * @return $this
273
     */
274
    public function explicitSelectMode($explicitSelect=true)
275
    {
276
        $this->_explicit_select_mode  = (bool)$explicitSelect;
277
        return $this;
278
    }
279
280
    /**
281
     * @param bool $filterOnFetch
282
     *
283
     * @return $this
284
     */
285
    public function filterOnFetch($filterOnFetch=true)
286
    {
287
        $this->_filter_on_fetch  = (bool)$filterOnFetch;
288
        return $this;
289
    }
290
291
    /**
292
     * @param bool $logFilterChanges
293
     *
294
     * @return $this
295
     */
296
    public function logFilterChanges($logFilterChanges=true)
297
    {
298
        $this->_log_filter_changes  = (bool)$logFilterChanges;
299
        return $this;
300
    }
301
302
    /**
303
     * Return the name of the table
304
     *
305
     * @return string
306
     */
307
    public function getTableName()
308
    {
309
        return $this->_table_name;
310
    }
311
312
    /**
313
     * @return string
314
     */
315
    public function getDisplayColumn()
316
    {
317
        return $this->_display_column;
318
    }
319
320
    /**
321
     * Set the display column
322
     *
323
     * @param string $column
324
     *
325
     * @return $this
326
     */
327
    public function displayColumn($column)
328
    {
329
        $this->_display_column = $column;
330
        return $this;
331
    }
332
    /**
333
     * Set the table alias
334
     *
335
     * @param string $alias
336
     *
337
     * @return $this
338
     */
339
    public function tableAlias($alias)
340
    {
341
        $this->_table_alias = $alias;
342
        return $this;
343
    }
344
345
    /**
346
     * @param int $cacheTtl
347
     * @return $this
348
     * @throws Exception
349
     */
350
    protected function _cacheTtl($cacheTtl)
351
    {
352
        Assert($cacheTtl)->int('Cache ttl must be either -1 for no cache, 0 for default ttl or an integer for a custom ttl');
353
        if ( $cacheTtl !== self::CACHE_NO && ! is_null($this->_pdo_stmt) )
354
        {
355
            throw new Exception("You cannot cache pre-executed queries");
356
        }
357
        $this->_cache_ttl = $cacheTtl;
358
        return $this;
359
    }
360
361
    /**
362
     * @return string
363
     */
364
    public function getTableAlias()
365
    {
366
        return $this->_table_alias;
367
    }
368
369
    /**
370
     * @param array $associations
371
     *
372
     * @return $this
373
     */
374
    public function associations(array $associations)
375
    {
376
        $this->_associations = $associations;
377
        return $this;
378
    }
379
380
    /**
381
     * @param string $alias
382
     * @param array $definition
383
     * @return $this
384
     */
385
    public function setBelongsTo($alias, array $definition)
386
    {
387
        Assert($alias)->notEmpty();
388
        Assert($definition)->isArray()->count(4);
389
390
        $this->_associations['belongsTo'][$alias] = $definition;
391
        return $this;
392
    }
393
394
    public function setBelongsToDisplayField($alias, $displayField)
395
    {
396
        Assert($alias)->notEmpty();
397
        Assert($this->_associations['belongsTo'])->keyExists($alias);
398
        Assert($displayField)->notEmpty();
399
400
        $this->_associations['belongsTo'][$alias][2] = $displayField;
401
        return $this;
402
    }
403
404
    /**
405
     * @param PDOStatement $stmt
406
     *
407
     * @param PDOStatement $stmt
408
     * @param Closure $fnCallback
409
     * @return bool|stdClass
410
     */
411
    public function fetchRow(PDOStatement $stmt, Closure $fnCallback=null)
412
    {
413
        if ( ! ( $record = $stmt->fetch(PDO::FETCH_OBJ) ) )
414
        {
415
            return false;
416
        }
417
        $record = $this->onFetch($record);
418
        if ( empty($fnCallback) )
419
        {
420
            return $record;
421
        }
422
        $record = $fnCallback($record);
423
        if ( is_null($record) )
424
        {
425
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
426
        }
427
        unset($fnCallback);
428
        return $record;
429
    }
430
431
    /**
432
     * @param array $schema
433
     *
434
     * @return $this
435
     */
436
    public function schema(array $schema)
437
    {
438
        $this->_schema = $schema;
439
        return $this;
440
    }
441
442
    /**
443
     * @param string|array $field
444
     * @param $type
445
     * @return $this
446
     */
447
    public function addSchema($field, $type)
448
    {
449
        if ( is_array($field) )
450
        {
451
            foreach ( $field as $field_name => $type_def )
452
            {
453
                $this->addSchema($field_name, $type_def);
454
            }
455
            return $this;
456
        }
457
        $this->_schema[$field] = $type;
458
        return $this;
459
    }
460
461
    /**
462
     * @param $keys_only
463
     * @return array
464
     */
465
    public function getColumns($keys_only=true)
466
    {
467
        return $keys_only ? array_keys($this->_schema) : $this->_schema;
468
    }
469
470
    /**
471
     * Get the primary key name
472
     *
473
     * @return string
474
     */
475
    public function getPrimaryKeyName()
476
    {
477
        return $this->_formatKeyName($this->_primary_key, $this->_table_name);
478
    }
479
480
    /**
481
     * @param string $query
482
     * @param array $parameters
483
     *
484
     * @return bool
485
     * @throws Exception
486
     */
487
    public function execute($query, array $parameters=[])
488
    {
489
        list($this->_built_query, $ident)  = $this->_logQuery($query, $parameters);
490
        try
491
        {
492
            $this->_pdo_stmt        = $this->getPdo()->prepare($query);
493
            $result                 = $this->_pdo_stmt->execute($parameters);
494
            if ( false === $result )
495
            {
496
                $this->_pdo_stmt = null;
497
                throw new PDOException("The query failed to execute.");
498
            }
499
        }
500
        catch(Exception $e)
501
        {
502
            $built_query = $this->_built_query ? $this->_built_query : $this->buildQuery($query, $parameters);
503
            $this->getLogger()->error("FAILED: \n\n{$built_query}\n WITH ERROR:\n" . $e->getMessage());
504
            $this->_pdo_stmt = null;
505
            throw $e;
506
        }
507
        $this->_logSlowQueries($ident, $this->_built_query);
508
        return $result;
509
    }
510
511
    /**
512
     * @param string $query
513
     * @param array $params
514
     * @return $this
515
     */
516
    public function query($query, array $params=[])
517
    {
518
        $this->_raw_sql             = $query;
519
        $this->_where_parameters    = $params;
520
        return $this;
521
    }
522
523
    /**
524
     * @return bool
525
     */
526
    public function begin()
527
    {
528
        return $this->getPdo()->beginTransaction();
529
    }
530
531
    /**
532
     * @return bool
533
     */
534
    public function commit()
535
    {
536
        return $this->getPdo()->commit();
537
    }
538
539
    /**
540
     * @return bool
541
     */
542
    public function rollback()
543
    {
544
        $this->getLogger()->debug("Calling db transaction rollback.");
545
        return $this->getPdo()->rollBack();
546
    }
547
548
549
    /**
550
     * @param string $sql
551
     * @param array $params
552
     *
553
     * @return string
554
     */
555
    public function buildQuery($sql, array $params=[])
556
    {
557
        $indexed = $params == array_values($params);
558
        if ( $indexed )
559
        {
560
            foreach ( $params as $key => $val )
561
            {
562
                $val    = is_string($val) ? "'{$val}'" : $val;
563
                $val    = is_null($val) ? 'NULL' : $val;
564
                $sql    = preg_replace('/\?/', $val, $sql, 1);
565
            }
566
            return $sql;
567
        }
568
569
        uksort($params, function ($a, $b) {
570
            return strlen($b) - strlen($a);
571
        });
572
        foreach ( $params as $key => $val )
573
        {
574
            $val    = is_string($val) ? "'{$val}'" : $val;
575
            $val    = is_null($val) ? 'NULL' : $val;
576
            $sql    = str_replace(":$key", $val, $sql);
577
            //$sql    = str_replace("$key", $val, $sql);
578
        }
579
        return $sql;
580
    }
581
582
    /**
583
     * @param stdClass $record
584
     *
585
     * @return stdClass
586
     */
587
    protected function _trimAndLowerCaseKeys(stdClass $record)
588
    {
589
        $fnTrimStrings = function($value) {
590
            return is_string($value) ? trim($value) : $value;
591
        };
592
        $record = array_map($fnTrimStrings, array_change_key_case((array)$record, CASE_LOWER));
593
        unset($fnTrimStrings);
594
        return (object)$record;
595
    }
596
597
    /**
598
     * Return the number of affected row by the last statement
599
     *
600
     * @return int
601
     */
602
    public function rowCount()
603
    {
604
        $stmt = $this->fetchStmt();
605
        return $stmt ? $stmt->rowCount() : 0;
606
    }
607
608
    /**
609
     * @return PDOStatement
610
     * @throws PDOException
611
     */
612
    public function fetchStmt()
613
    {
614
        if ( null === $this->_pdo_stmt )
615
        {
616
            $this->execute($this->getSelectQuery(), $this->_getWhereParameters());
617
        }
618
        return $this->_pdo_stmt;
619
    }
620
621
    /**
622
     * @param bool $build
623
     * @return array
624
     */
625
    public function fetchSqlQuery($build=false)
626
    {
627
        $clone  = clone $this;
628
        $query  = $clone->getSelectQuery();
629
        $params = $clone->_getWhereParameters();
630
        $result = $build ? $clone->buildQuery($query, $params) : [$query, $params];
631
        unset($clone->_handlers, $clone, $query, $params, $build);
632
        return $result;
633
    }
634
635
    /**
636
     * @param string $table_name
637
     * @param bool  $drop_if_exists
638
     * @param array $indexes
639
     * @return $this
640
     * @throws Exception
641
     */
642
    public function fetchIntoMemoryTable($table_name, $drop_if_exists=true, array $indexes=[])
643
    {
644
        $table_name = preg_replace('/[^A-Za-z0-9_]+/', '', $table_name);
645
        $table_name = $this->_tmp_table_prefix . preg_replace('/^' . $this->_tmp_table_prefix . '/', '', $table_name);
646
        if ( $drop_if_exists )
647
        {
648
            $this->execute("DROP TABLE IF EXISTS {$table_name}");
649
        }
650
        $indexSql = [];
651
        foreach ( $indexes as $name => $column )
652
        {
653
            $indexSql[] = "INDEX {$name} ({$column})";
654
        }
655
        $indexSql = implode(", ", $indexSql);
656
        $indexSql = empty($indexSql) ? '' : "({$indexSql})";
657
        list($querySql, $params) = $this->fetchSqlQuery();
658
        $sql = <<<SQL
659
        CREATE TEMPORARY TABLE {$table_name} {$indexSql} ENGINE=MEMORY {$querySql}
660
SQL;
661
        return $this->execute($sql, $params);
662
    }
663
664
    /**
665
     * @param null $keyedOn
666
     * @param int $cacheTtl
667
     * @return mixed
668
     */
669
    public function fetch($keyedOn=null, $cacheTtl=self::CACHE_NO)
670
    {
671
        $this->_cacheTtl($cacheTtl);
672
        $fnCallback = function() use ($keyedOn) {
673
674
            $stmt   = $this->fetchStmt();
675
            $rows   = [];
676
            while ( $record = $this->fetchRow($stmt) )
677
            {
678
                if ( $record === false ) continue; // For scrutinizer...
679
                if ( $keyedOn && property_exists($record, $keyedOn) )
680
                {
681
                    $rows[$record->$keyedOn] = $record;
682
                    continue;
683
                }
684
                $rows[] = $record;
685
            }
686
            $this->reset();
687
            return $rows;
688
        };
689
        if ( $this->_cache_ttl === self::CACHE_NO )
690
        {
691
            return $fnCallback();
692
        }
693
        $table              = $this->getTableName();
694
        $id                 = $this->_parseWhereForPrimaryLookup();
695
        $id                 = $id ? "/{$id}" : '';
696
        list($sql, $params) = $this->fetchSqlQuery();
697
        $sql                = $this->buildQuery($sql, $params);
698
        $cacheKey           = "/{$table}{$id}/" . md5(json_encode([
699
            'sql'       => $sql,
700
            'keyed_on'  => $keyedOn,
701
        ]));
702
        return $this->_cacheData($cacheKey, $fnCallback, $this->_cache_ttl);
703
    }
704
705
    protected function _parseWhereForPrimaryLookup()
706
    {
707
        if ( ! ( $alias = $this->getTableAlias() ) )
708
        {
709
            return null;
710
        }
711
        foreach ( $this->_where_conditions as $idx => $conds )
712
        {
713
            if ( ! empty($conds['STATEMENT']) && $conds['STATEMENT'] === "{$alias}.id = ?" )
714
            {
715
                return ! empty($conds['PARAMS'][0]) ? $conds['PARAMS'][0] : null;
716
            }
717
        }
718
        return null;
719
    }
720
721
    /**
722
     * @param string $cacheKey
723
     * @param Closure $func
724
     * @param int $cacheTtl - 0 for default ttl, -1 for no cache or int for custom ttl
725
     * @return mixed|null
726
     */
727
    protected function _cacheData($cacheKey, Closure $func, $cacheTtl=self::CACHE_DEFAULT)
728
    {
729
        if ( $cacheTtl === self::CACHE_NO )
730
        {
731
            /** @noinspection PhpVoidFunctionResultUsedInspection */
732
            return $func->__invoke();
733
        }
734
        $data = $this->getCache()->get($cacheKey);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $data is correct as $this->getCache()->get($cacheKey) (which targets Terah\RedisCache\RedisCache::get()) 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...
735
        if ( $data && is_object($data) && property_exists($data, 'results') )
736
        {
737
            $this->getLogger()->debug("Cache hit on {$cacheKey}");
738
            return $data->results;
739
        }
740
        $this->getLogger()->debug("Cache miss on {$cacheKey}");
741
        /** @noinspection PhpVoidFunctionResultUsedInspection */
742
        $data = (object)[
743
            // Watch out... invoke most likely calls reset
744
            // which clears the model params like _cache_ttl
745
            'results' => $func->__invoke(),
746
        ];
747
        try
748
        {
749
            // The cache engine expects null for the default cache value
750
            $cacheTtl = $cacheTtl === self::CACHE_DEFAULT ? null : $cacheTtl;
751
            /** @noinspection PhpMethodParametersCountMismatchInspection */
752
            if ( ! $this->getCache()->set($cacheKey, $data, $cacheTtl) )
753
            {
754
                throw new \Exception("Could not save data to cache");
755
            }
756
            return $data->results;
757
        }
758
        catch (\Exception $e)
759
        {
760
            $this->getLogger()->error($e->getMessage(), $e->getTrace());
761
            return $data->results;
762
        }
763
    }
764
765
    /**
766
     * @param string $cacheKey
767
     * @return bool
768
     */
769
    public function clearCache($cacheKey)
770
    {
771
        return $this->getCache()->delete($cacheKey);
772
    }
773
774
    /**
775
     * @param string|null $table
776
     * @return bool
777
     */
778
    public function clearCacheByTable($table=null)
779
    {
780
        $table = ! is_null($table) ? $table : $this->getTableName();
781
        if ( empty($table) )
782
        {
783
            return true;
784
        }
785
        return $this->clearCache("/{$table}/");
786
    }
787
788
    /**
789
     * @param Closure $fnCallback
790
     * @param null     $keyedOn
791
     * @param bool     $returnSuccessCnt
792
     * @return stdClass[]|int
793
     */
794
    public function fetchCallback(Closure $fnCallback, $keyedOn=null, $returnSuccessCnt=true)
795
    {
796
        $successCnt    = 0;
797
        $stmt           = $this->fetchStmt();
798
        if ( ! $returnSuccessCnt )
799
        {
800
            $rows   = [];
801
            while ( $record = $this->fetchRow($stmt, $fnCallback) )
802
            {
803
                if ( $keyedOn && property_exists($record, $keyedOn) )
804
                {
805
                    $rows[$record->$keyedOn] = $record;
806
                    continue;
807
                }
808
                $rows[] = $record;
809
            }
810
            $this->reset();
811
            return $rows;
812
        }
813
        while ( $this->_tallySuccessCount($stmt, $fnCallback, $successCnt) )
814
        {}
815
        return $successCnt;
816
    }
817
818
    /**
819
     * @param $numFailures
820
     * @return $this
821
     */
822
    public function maxCallbackFailures($numFailures)
823
    {
824
        Assert($numFailures)->int();
825
        $this->_max_callback_failures = $numFailures;
826
        return $this;
827
    }
828
829
    /**
830
     * @param PDOStatement $stmt
831
     * @param Closure $fnCallback
832
     * @param int $successCnt
833
     * @return bool|null|stdClass
834
     */
835
    protected function _tallySuccessCount($stmt, Closure $fnCallback, &$successCnt)
836
    {
837
        $record = $this->fetchRow($stmt);
838
        if ( $record === false )
839
        {
840
            return false;
841
        }
842
        $record = $fnCallback($record);
843
        // Callback return null then we want to exit the fetch loop
844
        if ( is_null($record) )
845
        {
846
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
847
            return null;
848
        }
849
        // The not record then don't bump the tally
850
        if ( ! $record )
851
        {
852
            $this->_num_callback_failures++;
853
            if ( ! is_null($this->_max_callback_failures) && $this->_num_callback_failures >= $this->_max_callback_failures )
854
            {
855
                $this->getLogger()->error("The callback has failed {$this->_max_callback_failures} times... aborting...");
856
                $successCnt = null;
857
                return null;
858
            }
859
            return true;
860
        }
861
        $successCnt++;
862
        return $record;
863
    }
864
865
    /**
866
     * @param null|string $keyedOn
867
     * @param null|mixed $valueField
868
     * @param int $cacheTtl
869
     * @return mixed
870
     */
871
    public function fetchList($keyedOn=null, $valueField=null, $cacheTtl=self::CACHE_NO)
872
    {
873
        $keyedOn           = ! is_null($keyedOn) ? $keyedOn : $this->getPrimaryKeyName();
874
        $valueField        = ! is_null($valueField) ? $valueField : $this->getDisplayColumn();
875
        $keyedOnAlias     = strtolower(str_replace('.', '_', $keyedOn));
876
        $valueFieldAlias  = strtolower(str_replace('.', '_', $valueField));
877
878
        $this->_cacheTtl($cacheTtl);
879
        $fnCallback         = function() use ($keyedOn, $keyedOnAlias, $valueField, $valueFieldAlias) {
880
881
            $rows = [];
882
            $stmt = $this->select(null)
883
                ->select($keyedOn, $keyedOnAlias)
884
                ->select($valueField, $valueFieldAlias)
885
                ->fetchStmt();
886
            while ( $record = $this->fetchRow($stmt) )
887
            {
888
                $rows[$record->$keyedOnAlias] = $record->$valueFieldAlias;
889
            }
890
            return $rows;
891
        };
892
        if ( $this->_cache_ttl === self::CACHE_NO )
893
        {
894
            $result = $fnCallback();
895
            unset($cacheKey, $fnCallback);
896
            return $result;
897
        }
898
        $table              = $this->getTableName();
899
        $cacheKey           = md5(json_encode([
900
            'sql'               => $this->fetchSqlQuery(),
901
            'keyed_on'          => $keyedOn,
902
            'keyed_on_alias'    => $keyedOnAlias,
903
            'value_field'       => $valueField,
904
            'value_fieldAlias'  => $valueFieldAlias,
905
        ]));
906
        return $this->_cacheData("/{$table}/list/{$cacheKey}", $fnCallback, $this->_cache_ttl);
907
    }
908
909
    /**
910
     * @param string $column
911
     * @param int $cacheTtl
912
     * @param bool|true $unique
913
     * @return array|mixed
914
     */
915
    public function fetchColumn($column, $cacheTtl=self::CACHE_NO, $unique=true)
916
    {
917
        $list = $this->select($column)->fetch(null, $cacheTtl);
918
        foreach ( $list as $idx => $obj )
919
        {
920
            $list[$idx] = $obj->{$column};
921
        }
922
        return $unique ? array_unique($list) : $list;
923
    }
924
925
    /**
926
     * @param null|string $field
927
     * @param null|int $itemId
928
     * @param int $cacheTtl
929
     * @return mixed|null
930
     */
931
    public function fetchField($field=null, $itemId=null, $cacheTtl=self::CACHE_NO)
932
    {
933
        $field  = !is_null($field) ? $field : $this->getPrimaryKeyName();
934
        $this->_cacheTtl($cacheTtl);
935
        $object = $this->select(null)->select($field)->fetchOne($itemId);
936
        if ( ! $object )
937
        {
938
            return null;
939
        }
940
        // Handle aliases
941
        if ( preg_match('/ as /i', $field) )
942
        {
943
            list($expression, $alias) = preg_split('/ as /i', $field);
944
            unset($expression);
945
            $field = trim($alias);
946
        }
947
        if ( strpos($field, '.') !== false )
948
        {
949
            list($tableAlias, $field) = explode('.', $field);
950
            unset($tableAlias);
951
        }
952
        return property_exists($object, $field) ? $object->$field : null;
953
    }
954
955
    /**
956
     * @param int|null $id
957
     * @param int $cacheTtl
958
     * @return \stdClass|bool
959
     */
960
    public function fetchOne($id=null, $cacheTtl=self::CACHE_NO)
961
    {
962
        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...
963
        {
964
            $this->wherePK($id, true);
965
        }
966
        $this->_cacheTtl($cacheTtl);
967
        $this->limit(1);
968
        $fetchAll = $this->fetch();
969
        return $fetchAll ? array_shift($fetchAll) : false;
970
    }
971
972
    /**
973
     * @param int|null $id
974
     * @param int $cacheTtl
975
     * @return \stdClass|bool
976
     */
977
    public function fetchExists($id=null, $cacheTtl=self::CACHE_NO)
978
    {
979
        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...
980
        {
981
            $this->wherePK($id, true);
982
        }
983
        $this->_cacheTtl($cacheTtl);
984
        return $this->count() !== 0;
985
    }
986
987
    /*------------------------------------------------------------------------------
988
                                    Fluent Query Builder
989
    *-----------------------------------------------------------------------------*/
990
991
    /**
992
     * Create the select clause
993
     *
994
     * @param  mixed    $columns  - the column to select. Can be string or array of fields
995
     * @param  string   $alias - an alias to the column
996
     * @param bool|true $explicitSelect
997
     * @return $this
998
     */
999
    public function select($columns='*', $alias=null, $explicitSelect=true)
1000
    {
1001
        if ( $explicitSelect )
1002
        {
1003
            $this->explicitSelectMode();
1004
        }
1005
        if ( ! empty($alias) && !is_array($columns) & $columns !== $alias )
1006
        {
1007
            $columns .= " AS {$alias} ";
1008
        }
1009
        if ( $columns === '*' && !empty($this->_schema) )
1010
        {
1011
            $columns = $this->getColumns();
1012
        }
1013
        // Reset the select list
1014
        if ( is_null($columns) )
1015
        {
1016
            $this->_select_fields = [];
1017
            return $this;
1018
        }
1019
        $columns = is_array($columns) ? $columns : [$columns];
1020
1021
//        if ( empty($this->_select_fields) && $addAllIfEmpty )
1022
//        {
1023
//            $this->select('*');
1024
//        }
1025
        if ( $this->_table_alias )
1026
        {
1027
            $schema = $this->columns();
1028
            foreach ( $columns as $idx => $col )
1029
            {
1030
                if ( in_array($col, $schema) )
1031
                {
1032
                    $columns[$idx] = "{$this->_table_alias}.{$col}";
1033
                }
1034
            }
1035
        }
1036
        $this->_select_fields = array_merge($this->_select_fields, $columns);
1037
        return $this;
1038
    }
1039
1040
    public function selectRaw($select)
1041
    {
1042
        $this->_select_fields[] = $select;
1043
        return $this;
1044
    }
1045
1046
    /**
1047
     * @param bool $log_queries
1048
     *
1049
     * @return $this
1050
     */
1051
    public function logQueries($log_queries=true)
1052
    {
1053
        $this->_log_queries = $log_queries;
1054
        return $this;
1055
    }
1056
1057
    /**
1058
     * @param bool $include_count
1059
     *
1060
     * @return $this
1061
     */
1062
    public function includeCount($include_count=true)
1063
    {
1064
        $this->_include_count = $include_count;
1065
        return $this;
1066
    }
1067
1068
    /**
1069
     * @param bool $distinct
1070
     *
1071
     * @return $this
1072
     */
1073
    public function distinct($distinct=true)
1074
    {
1075
        $this->_distinct = $distinct;
1076
        return $this;
1077
    }
1078
1079
    /**
1080
     * @param array $fields
1081
     * @return $this
1082
     */
1083
    public function withBelongsTo($fields=[])
1084
    {
1085
        if ( ! empty($this->_associations['belongsTo']) )
1086
        {
1087
            foreach ( $this->_associations['belongsTo'] as $alias => $config )
1088
            {
1089
                $addFieldsForJoins = empty($fields) || in_array($config[3], $fields);
1090
                $this->autoJoin($alias, self::LEFT_JOIN, $addFieldsForJoins);
1091
            }
1092
        }
1093
        return $this;
1094
    }
1095
1096
    /**
1097
     * @param string $alias
1098
     * @param string $type
1099
     * @param bool   $addSelectField
1100
     * @return $this
1101
     */
1102
    public function autoJoin($alias, $type=self::LEFT_JOIN, $addSelectField=true)
1103
    {
1104
        Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid join... the alias does not exists");
1105
        list($table, $join_col, $field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
1106
        $condition = "{$alias}.id = {$this->_table_alias}.{$join_col}";
1107
        if ( in_array($alias, $this->_join_aliases) )
1108
        {
1109
            return $this;
1110
        }
1111
        $this->join($table, $condition, $alias, $type);
1112
        if ( $addSelectField )
1113
        {
1114
            $this->select($field, $fieldAlias, false);
1115
        }
1116
        return $this;
1117
    }
1118
1119
    /**
1120
     * Add where condition, more calls appends with AND
1121
     *
1122
     * @param string $condition possibly containing ? or :name
1123
     * @param mixed $parameters accepted by PDOStatement::execute or a scalar value
1124
     * @param mixed ...
1125
     * @return $this
1126
     */
1127
    public function where($condition, $parameters = [])
1128
    {
1129
        // By default the and_or_operator and wrap operator is AND,
1130
        if ( $this->_wrap_open || ! $this->_and_or_operator )
1131
        {
1132
            $this->_and();
1133
        }
1134
1135
        // where(array("column1" => 1, "column2 > ?" => 2))
1136
        if ( is_array($condition) )
1137
        {
1138
            foreach ($condition as $key => $val)
1139
            {
1140
                $this->where($key, $val);
1141
            }
1142
            return $this;
1143
        }
1144
1145
        $args = func_num_args();
1146
        if ( $args != 2 || strpbrk($condition, '?:') )
1147
        { // where('column < ? OR column > ?', array(1, 2))
1148
            if ( $args != 2 || !is_array($parameters) )
1149
            { // where('column < ? OR column > ?', 1, 2)
1150
                $parameters = func_get_args();
1151
                array_shift($parameters);
1152
            }
1153
        }
1154
        else if ( !is_array($parameters) )
1155
        {//where(column,value) => column=value
1156
            $condition .= ' = ?';
1157
            $parameters = [$parameters];
1158
        }
1159
        else if ( is_array($parameters) )
1160
        { // where('column', array(1, 2)) => column IN (?,?)
1161
            $placeholders = $this->_makePlaceholders(count($parameters));
1162
            $condition = "({$condition} IN ({$placeholders}))";
1163
        }
1164
1165
        $this->_where_conditions[] = [
1166
            'STATEMENT'   => $condition,
1167
            'PARAMS'      => $parameters,
1168
            'OPERATOR'    => $this->_and_or_operator
1169
        ];
1170
        // Reset the where operator to AND. To use OR, you must call _or()
1171
        $this->_and();
1172
        return $this;
1173
    }
1174
1175
    /**
1176
     * Create an AND operator in the where clause
1177
     *
1178
     * @return $this
1179
     */
1180
    public function _and()
1181
    {
1182
        if ( $this->_wrap_open )
1183
        {
1184
            $this->_where_conditions[] = self::OPERATOR_AND;
1185
            $this->_last_wrap_position = count($this->_where_conditions);
1186
            $this->_wrap_open = false;
1187
            return $this;
1188
        }
1189
        $this->_and_or_operator = self::OPERATOR_AND;
1190
        return $this;
1191
    }
1192
1193
1194
    /**
1195
     * Create an OR operator in the where clause
1196
     *
1197
     * @return $this
1198
     */
1199
    public function _or()
1200
    {
1201
        if ( $this->_wrap_open )
1202
        {
1203
            $this->_where_conditions[] = self::OPERATOR_OR;
1204
            $this->_last_wrap_position = count($this->_where_conditions);
1205
            $this->_wrap_open = false;
1206
            return $this;
1207
        }
1208
        $this->_and_or_operator = self::OPERATOR_OR;
1209
        return $this;
1210
    }
1211
1212
    /**
1213
     * To group multiple where clauses together.
1214
     *
1215
     * @return $this
1216
     */
1217
    public function wrap()
1218
    {
1219
        $this->_wrap_open = true;
1220
        $spliced = array_splice($this->_where_conditions, $this->_last_wrap_position, count($this->_where_conditions), '(');
1221
        $this->_where_conditions = array_merge($this->_where_conditions, $spliced);
1222
        array_push($this->_where_conditions,')');
1223
        $this->_last_wrap_position = count($this->_where_conditions);
1224
        return $this;
1225
    }
1226
1227
    /**
1228
     * Where Primary key
1229
     *
1230
     * @param      integer $id
1231
     * @param bool $addAlias
1232
     *
1233
     * @return $this
1234
     */
1235
    public function wherePK($id, $addAlias=true)
1236
    {
1237
        $alias = $addAlias && !empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1238
        return $this->where($alias . $this->getPrimaryKeyName(), $id);
1239
    }
1240
1241
    /**
1242
     * WHERE $columnName != $value
1243
     *
1244
     * @param  string   $columnName
1245
     * @param  mixed    $value
1246
     * @return $this
1247
     */
1248
    public function whereNot($columnName, $value)
1249
    {
1250
        return $this->where("$columnName != ?", $value);
1251
    }
1252
    /**
1253
     * WHERE $columnName != $value
1254
     *
1255
     * @param  string   $columnName
1256
     * @param  mixed    $value
1257
     * @return $this
1258
     */
1259
    public function whereCoercedNot($columnName, $value)
1260
    {
1261
        return $this->where("IFNULL({$columnName}, '') != ?", $value);
1262
    }
1263
1264
    /**
1265
     * WHERE $columnName LIKE $value
1266
     *
1267
     * @param  string   $columnName
1268
     * @param  mixed    $value
1269
     * @return $this
1270
     */
1271
    public function whereLike($columnName, $value)
1272
    {
1273
        return $this->where("$columnName LIKE ?", $value);
1274
    }
1275
1276
    /**
1277
     * @param string $columnName
1278
     * @param mixed $value1
1279
     * @param mixed $value2
1280
     * @return $this
1281
     */
1282
    public function whereBetween($columnName, $value1, $value2)
1283
    {
1284
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1285
    }
1286
1287
    /**
1288
     * @param string $columnName
1289
     * @param mixed $value1
1290
     * @param mixed $value2
1291
     * @return $this
1292
     */
1293
    public function whereNotBetween($columnName, $value1, $value2)
1294
    {
1295
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1296
    }
1297
1298
    /**
1299
     * @param string $columnName
1300
     * @param string $regex
1301
     * @return $this
1302
     */
1303
    public function whereRegex($columnName, $regex)
1304
    {
1305
        return $this->where("$columnName REGEXP ?", $regex);
1306
    }
1307
1308
    /**
1309
     * @param string $columnName
1310
     * @param string $regex
1311
     * @return $this
1312
     */
1313
    public function whereNotRegex($columnName, $regex)
1314
    {
1315
        return $this->where("$columnName NOT REGEXP ?", $regex);
1316
    }
1317
1318
    /**
1319
     * WHERE $columnName NOT LIKE $value
1320
     *
1321
     * @param  string   $columnName
1322
     * @param  mixed    $value
1323
     * @return $this
1324
     */
1325
    public function whereNotLike($columnName, $value)
1326
    {
1327
        return $this->where("$columnName NOT LIKE ?", $value);
1328
    }
1329
1330
    /**
1331
     * WHERE $columnName > $value
1332
     *
1333
     * @param  string   $columnName
1334
     * @param  mixed    $value
1335
     * @return $this
1336
     */
1337
    public function whereGt($columnName, $value)
1338
    {
1339
        return $this->where("$columnName > ?", $value);
1340
    }
1341
1342
    /**
1343
     * WHERE $columnName >= $value
1344
     *
1345
     * @param  string   $columnName
1346
     * @param  mixed    $value
1347
     * @return $this
1348
     */
1349
    public function whereGte($columnName, $value)
1350
    {
1351
        return $this->where("$columnName >= ?", $value);
1352
    }
1353
1354
    /**
1355
     * WHERE $columnName < $value
1356
     *
1357
     * @param  string   $columnName
1358
     * @param  mixed    $value
1359
     * @return $this
1360
     */
1361
    public function whereLt($columnName, $value)
1362
    {
1363
        return $this->where("$columnName < ?", $value);
1364
    }
1365
1366
    /**
1367
     * WHERE $columnName <= $value
1368
     *
1369
     * @param  string   $columnName
1370
     * @param  mixed    $value
1371
     * @return $this
1372
     */
1373
    public function whereLte($columnName, $value)
1374
    {
1375
        return $this->where("$columnName <= ?", $value);
1376
    }
1377
1378
    /**
1379
     * WHERE $columnName IN (?,?,?,...)
1380
     *
1381
     * @param  string   $columnName
1382
     * @param  array    $values
1383
     * @return $this
1384
     */
1385
    public function whereIn($columnName, array $values)
1386
    {
1387
        return $this->where($columnName,$values);
1388
    }
1389
1390
    /**
1391
     * WHERE $columnName NOT IN (?,?,?,...)
1392
     *
1393
     * @param  string   $columnName
1394
     * @param  array    $values
1395
     * @return $this
1396
     */
1397
    public function whereNotIn($columnName, array $values)
1398
    {
1399
        $placeholders = $this->_makePlaceholders(count($values));
1400
        return $this->where("({$columnName} NOT IN ({$placeholders}))", $values);
1401
    }
1402
1403
    /**
1404
     * WHERE $columnName IS NULL
1405
     *
1406
     * @param  string   $columnName
1407
     * @return $this
1408
     */
1409
    public function whereNull($columnName)
1410
    {
1411
        return $this->where("({$columnName} IS NULL)");
1412
    }
1413
1414
    /**
1415
     * WHERE $columnName IS NOT NULL
1416
     *
1417
     * @param  string   $columnName
1418
     * @return $this
1419
     */
1420
    public function whereNotNull($columnName)
1421
    {
1422
        return $this->where("({$columnName} IS NOT NULL)");
1423
    }
1424
1425
    /**
1426
     * @param string $statement
1427
     * @param string $operator
1428
     * @return $this
1429
     */
1430
    public function having($statement, $operator = self::OPERATOR_AND)
1431
    {
1432
        $this->_having[] = [
1433
            'STATEMENT'   => $statement,
1434
            'OPERATOR'    => $operator
1435
        ];
1436
        return $this;
1437
    }
1438
1439
    /**
1440
     * ORDER BY $columnName (ASC | DESC)
1441
     *
1442
     * @param  string   $columnName - The name of the column or an expression
1443
     * @param  string   $ordering   (DESC | ASC)
1444
     * @return $this
1445
     */
1446
    public function orderBy($columnName, $ordering=null)
1447
    {
1448
        Assert($ordering)->nullOr()->inArray(['DESC', 'desc', 'ASC', 'asc']);
1449
        if ( is_null($columnName) )
1450
        {
1451
            $this->_order_by = [];
1452
            return $this;
1453
        }
1454
        $this->_order_by[] = trim("{$columnName} {$ordering}");
1455
        return $this;
1456
    }
1457
1458
    /**
1459
     * GROUP BY $columnName
1460
     *
1461
     * @param  string   $columnName
1462
     * @return $this
1463
     */
1464
    public function groupBy($columnName)
1465
    {
1466
        $columnName = is_array($columnName) ? $columnName : [$columnName];
1467
        foreach ( $columnName as $col )
1468
        {
1469
            $this->_group_by[] = $col;
1470
        }
1471
        return $this;
1472
    }
1473
1474
1475
    /**
1476
     * LIMIT $limit
1477
     *
1478
     * @param  int      $limit
1479
     * @param  int|null $offset
1480
     * @return $this
1481
     */
1482
    public function limit($limit, $offset=null)
1483
    {
1484
        $this->_limit =  (int)$limit;
1485
        if ( ! is_null($offset) )
1486
        {
1487
            $this->offset($offset);
1488
        }
1489
        return $this;
1490
    }
1491
1492
    /**
1493
     * Return the limit
1494
     *
1495
     * @return integer
1496
     */
1497
    public function getLimit()
1498
    {
1499
        return $this->_limit;
1500
    }
1501
1502
    /**
1503
     * OFFSET $offset
1504
     *
1505
     * @param  int      $offset
1506
     * @return $this
1507
     */
1508
    public function offset($offset)
1509
    {
1510
        $this->_offset = (int)$offset;
1511
        return $this;
1512
    }
1513
1514
    /**
1515
     * Return the offset
1516
     *
1517
     * @return integer
1518
     */
1519
    public function getOffset()
1520
    {
1521
        return $this->_offset;
1522
    }
1523
1524
    /**
1525
     * Build a join
1526
     *
1527
     * @param  string    $table         - The table name
1528
     * @param  string   $constraint    -> id = profile.user_id
1529
     * @param  string   $tableAlias   - The alias of the table name
1530
     * @param  string   $joinOperator - LEFT | INNER | etc...
1531
     * @return $this
1532
     */
1533
    public function join($table, $constraint=null, $tableAlias = null, $joinOperator = '')
1534
    {
1535
        if ( is_null($constraint) )
1536
        {
1537
            return $this->autoJoin($table, $joinOperator);
1538
        }
1539
        $join                   = [$joinOperator ? "{$joinOperator} " : ''];
1540
        $join[]                 = "JOIN {$table} ";
1541
        $tableAlias            = is_null($tableAlias) ? Inflector::classify($table) : $tableAlias;
1542
        $join[]                 = $tableAlias ? "AS {$tableAlias} " : '';
1543
        $join[]                 = "ON {$constraint}";
1544
        $this->_join_sources[]  = implode('', $join);
1545
        if ( $tableAlias )
1546
        {
1547
            $this->_join_aliases[]  = $tableAlias;
1548
        }
1549
        return $this;
1550
    }
1551
1552
    /**
1553
     * Create a left join
1554
     *
1555
     * @param  string   $table
1556
     * @param  string   $constraint
1557
     * @param  string   $tableAlias
1558
     * @return $this
1559
     */
1560
    public function leftJoin($table, $constraint, $tableAlias=null)
1561
    {
1562
        return $this->join($table, $constraint, $tableAlias, self::LEFT_JOIN);
1563
    }
1564
1565
1566
    /**
1567
     * Return the build select query
1568
     *
1569
     * @return string
1570
     */
1571
    public function getSelectQuery()
1572
    {
1573
        if ( ! is_null($this->_raw_sql) )
1574
        {
1575
            return $this->_raw_sql;
1576
        }
1577
        if ( empty($this->_select_fields) || ! $this->_explicit_select_mode )
1578
        {
1579
            $this->select('*', null, false);
1580
        }
1581
        foreach ( $this->_select_fields as $idx => $cols )
1582
        {
1583
            if ( strpos(trim(strtolower($cols)), 'distinct ') === 0 )
1584
            {
1585
                $this->_distinct = true;
1586
                $this->_select_fields[$idx] = str_ireplace('distinct ', '', $cols);
1587
            }
1588
        }
1589
        if ( $this->_include_count )
1590
        {
1591
            $this->select('COUNT(*) as __cnt');
1592
        }
1593
        $query  = 'SELECT ';
1594
        $query .= $this->_distinct ? 'DISTINCT ' : '';
1595
        $query .= implode(', ', $this->_prepareColumns($this->_select_fields));
1596
        $query .= " FROM {$this->_table_name}" . ( $this->_table_alias ? " {$this->_table_alias}" : '' );
1597
        if ( count($this->_join_sources ) )
1598
        {
1599
            $query .= (' ').implode(' ',$this->_join_sources);
1600
        }
1601
        $query .= $this->_getWhereString(); // WHERE
1602
        if ( count($this->_group_by) )
1603
        {
1604
            $query .= ' GROUP BY ' . implode(', ', array_unique($this->_group_by));
1605
        }
1606
        if ( count($this->_order_by ) )
1607
        {
1608
            $query .= ' ORDER BY ' . implode(', ', array_unique($this->_order_by));
1609
        }
1610
        $query .= $this->_getHavingString(); // HAVING
1611
        return $this->_connection->setLimit($query, $this->_limit, $this->_offset);
1612
    }
1613
1614
    /**
1615
     * Prepare columns to include the table alias name
1616
     * @param array $columns
1617
     * @return array
1618
     */
1619
    protected function _prepareColumns(array $columns)
1620
    {
1621
        if ( ! $this->_table_alias )
1622
        {
1623
            return $columns;
1624
        }
1625
        $newColumns = [];
1626
        foreach ($columns as $column)
1627
        {
1628
            if ( strpos($column, ',') && ! preg_match('/^[a-zA-Z]{2,200}\(.{1,500}\)/', trim($column)) )
1629
            {
1630
                $newColumns = array_merge($this->_prepareColumns(explode(',', $column)), $newColumns);
1631
            }
1632
            elseif ( preg_match('/^(AVG|SUM|MAX|MIN|COUNT|CONCAT)/', $column) )
1633
            {
1634
                $newColumns[] = trim($column);
1635
            }
1636
            elseif (strpos($column, '.') === false && strpos(strtoupper($column), 'NULL') === false)
1637
            {
1638
                $column         = trim($column);
1639
                $newColumns[]   = preg_match('/^[0-9]/', $column) ? trim($column) : "{$this->_table_alias}.{$column}";
1640
            }
1641
            else
1642
            {
1643
                $newColumns[] = trim($column);
1644
            }
1645
        }
1646
        return $newColumns;
1647
    }
1648
1649
    /**
1650
     * Build the WHERE clause(s)
1651
     *
1652
     * @param bool $purgeAliases
1653
     * @return string
1654
     */
1655
    protected function _getWhereString($purgeAliases=false)
1656
    {
1657
        // If there are no WHERE clauses, return empty string
1658
        if ( empty($this->_where_conditions) )
1659
        {
1660
            return '';
1661
        }
1662
        $where_condition = '';
1663
        $last_condition = '';
1664
        foreach ( $this->_where_conditions as $condition )
1665
        {
1666
            if ( is_array($condition) )
1667
            {
1668
                if ( $where_condition && $last_condition != '(' && !preg_match('/\)\s+(OR|AND)\s+$/i', $where_condition))
1669
                {
1670
                    $where_condition .= $condition['OPERATOR'];
1671
                }
1672
                if ( $purgeAliases && ! empty($condition['STATEMENT']) && strpos($condition['STATEMENT'], '.') !== false && ! empty($this->_table_alias) )
1673
                {
1674
                    $condition['STATEMENT'] = preg_replace("/{$this->_table_alias}\./", '', $condition['STATEMENT']);
1675
                }
1676
                $where_condition .= $condition['STATEMENT'];
1677
                $this->_where_parameters = array_merge($this->_where_parameters, $condition['PARAMS']);
1678
            }
1679
            else
1680
            {
1681
                $where_condition .= $condition;
1682
            }
1683
            $last_condition = $condition;
1684
        }
1685
        return " WHERE {$where_condition}" ;
1686
    }
1687
1688
    /**
1689
     * Return the HAVING clause
1690
     *
1691
     * @return string
1692
     */
1693
    protected function _getHavingString()
1694
    {
1695
        // If there are no WHERE clauses, return empty string
1696
        if ( empty($this->_having) )
1697
        {
1698
            return '';
1699
        }
1700
        $having_condition = '';
1701
        foreach ( $this->_having as $condition )
1702
        {
1703
            if ( $having_condition && !preg_match('/\)\s+(OR|AND)\s+$/i', $having_condition) )
1704
            {
1705
                $having_condition .= $condition['OPERATOR'];
1706
            }
1707
            $having_condition .= $condition['STATEMENT'];
1708
        }
1709
        return " HAVING {$having_condition}" ;
1710
    }
1711
1712
    /**
1713
     * Return the values to be bound for where
1714
     *
1715
     * @param bool $purgeAliases
1716
     * @return array
1717
     */
1718
    protected function _getWhereParameters($purgeAliases=false)
1719
    {
1720
        unset($purgeAliases);
1721
        return $this->_where_parameters;
1722
    }
1723
1724
    /**
1725
     * Insert new rows
1726
     * $records can be a stdClass or an array of stdClass to add a bulk insert
1727
     * If a single row is inserted, it will return it's row instance
1728
     *
1729
     * @param stdClass|array $records
1730
     * @return int|stdClass|bool
1731
     * @throws Exception
1732
     */
1733
    public function insert($records)
1734
    {
1735
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
1736
        $records = is_array($records) ? $records : [$records];
1737
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1738
1739
        foreach ( $records as $key => $record )
1740
        {
1741
            $records[$key] = $this->beforeSave($records[$key], self::SAVE_INSERT);
1742
            if ( ! empty($this->_errors) )
1743
            {
1744
                return false;
1745
            }
1746
        }
1747
        list($sql, $insert_values) = $this->insertSqlQuery($records);
1748
        $this->execute($sql, $insert_values);
1749
        $rowCount = $this->rowCount();
1750
        if ( $rowCount === 1 )
1751
        {
1752
            $primaryKeyName                 = $this->getPrimaryKeyName();
1753
            $records[0]->$primaryKeyName    = $this->getLastInsertId($primaryKeyName);
1754
        }
1755
        foreach ( $records as $key => $record )
1756
        {
1757
            $records[$key] = $this->afterSave($record, self::SAVE_INSERT);
1758
        }
1759
        $this->destroy();
1760
        // On single element return the object
1761
        return $rowCount === 1 ? $records[0] : $rowCount;
1762
    }
1763
1764
    /**
1765
     * @param null $name
1766
     * @return int
1767
     */
1768
    public function getLastInsertId($name=null)
1769
    {
1770
        return (int)$this->getPdo()->lastInsertId($name);
1771
    }
1772
1773
    /**
1774
     * @param $records
1775
     * @return array
1776
     */
1777
    public function insertSqlQuery($records)
1778
    {
1779
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
1780
        $records = is_array($records) ? $records : [$records];
1781
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1782
1783
        $insert_values  = [];
1784
        $question_marks = [];
1785
        $properties     = [];
1786
        foreach ( $records as $record )
1787
        {
1788
            $properties         = !empty($properties) ? $properties : array_keys(get_object_vars($record));
1789
            $question_marks[]   = '('  . $this->_makePlaceholders(count($properties)) . ')';
1790
            $insert_values      = array_merge($insert_values, array_values((array)$record));
1791
        }
1792
        $properties     = implode(', ', $properties);
1793
        $question_marks = implode(', ', $question_marks);
1794
        $sql            = "INSERT INTO {$this->_table_name} ({$properties}) VALUES {$question_marks}";
1795
        return [$sql, $insert_values];
1796
    }
1797
1798
    /**
1799
     * @param       $data
1800
     * @param array $matchOn
1801
     * @param bool  $returnObj
1802
     * @return bool|int|stdClass
1803
     */
1804
    public function upsert($data, array $matchOn=[], $returnObj=false)
1805
    {
1806
        if ( ! is_array($data) )
1807
        {
1808
            return $this->upsertOne($data, $matchOn, $returnObj);
1809
        }
1810
        Assert($data)
1811
            ->notEmpty("The data passed to insert does not contain any data")
1812
            ->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1813
        $num_success    = 0;
1814
        foreach ( $data as $row )
1815
        {
1816
            $clone = clone $this;
1817
            if ( $clone->upsertOne($row, $matchOn) )
1818
            {
1819
                $num_success++;
1820
            }
1821
            unset($clone->_handlers, $clone); // hhvm mem leak
1822
        }
1823
        return $num_success;
1824
    }
1825
1826
    /**
1827
     * @param stdClass $object
1828
     * @param array    $matchOn
1829
     * @param bool     $returnObj
1830
     * @return bool|int|stdClass
1831
     */
1832
    public function upsertOne(stdClass $object, array $matchOn=[], $returnObj=false)
1833
    {
1834
        $primary_key    = $this->getPrimaryKeyName();
1835
        $matchOn       = empty($matchOn) && property_exists($object, $primary_key) ? [$primary_key] : $matchOn;
1836
        foreach ( $matchOn as $column )
1837
        {
1838
            Assert( ! property_exists($object, $column) && $column !== $primary_key)->false('The match on value for upserts is missing.');
1839
            if ( property_exists($object, $column) )
1840
            {
1841
                if ( is_null($object->$column) )
1842
                {
1843
                    $this->whereNull($column);
1844
                }
1845
                else
1846
                {
1847
                    $this->where($column, $object->$column);
1848
                }
1849
            }
1850
        }
1851
        if ( count($this->_where_conditions) < 1 )
1852
        {
1853
            return $this->insert($object);
1854
        }
1855
        if ( ( $id = (int)$this->fetchField($primary_key) ) )
1856
        {
1857
            if ( property_exists($object, $primary_key) && is_null($object->$primary_key) )
1858
            {
1859
                $object->$primary_key = $id;
1860
            }
1861
            $rows_affected = $this->reset()->wherePk($id)->update($object);
1862
            if ( $rows_affected === false )
1863
            {
1864
                return false;
1865
            }
1866
            return $returnObj ? $this->reset()->fetchOne($id) : $id;
1867
        }
1868
        return $this->insert($object);
1869
    }
1870
1871
    /**
1872
     * @param array      $data
1873
     * @param array      $matchOn
1874
     * @param bool|false $returnObj
1875
     * @return bool|int|stdClass
1876
     */
1877
    public function upsertArr(array $data, array $matchOn=[], $returnObj=false)
1878
    {
1879
        return $this->upsert((object)$data, $matchOn, $returnObj);
1880
    }
1881
1882
    /**
1883
     * Update entries
1884
     * Use the query builder to create the where clause
1885
     *
1886
     * @param stdClass $record
1887
     * @param bool     $updateAll
1888
     * @return bool|int
1889
     * @throws Exception
1890
     */
1891
    public function update(stdClass $record, $updateAll=false)
1892
    {
1893
        Assert($record)
1894
            ->notEmpty("The data passed to update does not contain any data")
1895
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
1896
1897
        if ( empty($this->_where_conditions) && ! $updateAll )
1898
        {
1899
            throw new Exception("You cannot update an entire table without calling update with updateAll=true", 500);
1900
        }
1901
        $record = $this->beforeSave($record, self::SAVE_UPDATE);
1902
        if ( ! empty($this->_errors) )
1903
        {
1904
            return false;
1905
        }
1906
        list($sql, $values) = $this->updateSqlQuery($record);
1907
        $this->execute($sql, $values);
1908
        $this->afterSave($record, self::SAVE_UPDATE);
1909
        $row_count = $this->rowCount();
1910
        $this->destroy();
1911
        return $row_count;
1912
    }
1913
1914
    /**
1915
     * @param array      $record
1916
     * @param bool|false $updateAll
1917
     * @return bool|int
1918
     * @throws Exception
1919
     */
1920
    public function updateArr(array $record, $updateAll=false)
1921
    {
1922
        return $this->update((object)$record, $updateAll);
1923
    }
1924
1925
    /**
1926
     * @param array $record
1927
     * @return bool|int|stdClass
1928
     */
1929
    public function insertArr(array $record)
1930
    {
1931
        return $this->insert((object)$record);
1932
    }
1933
1934
    /**
1935
     * @param int     $field
1936
     * @param mixed   $value
1937
     * @param null $id
1938
     * @param bool|false $updateAll
1939
     * @return bool|int
1940
     * @throws Exception
1941
     */
1942
    public function updateField($field, $value, $id=null, $updateAll=false)
1943
    {
1944
        if ( ! is_null($id) )
1945
        {
1946
            $this->wherePk($id);
1947
        }
1948
        return $this->update((object)[$field => $value], $updateAll);
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 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...
1960
    {
1961
1962
    }
1963
1964
    /**
1965
     * @param stdClass $record
1966
     * @return bool|int
1967
     * @throws Exception
1968
     */
1969
    public function updateChanged(stdClass $record)
1970
    {
1971
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
1972
        {
1973
            if ( is_null($value) )
1974
            {
1975
                $this->whereNotNull($field);
1976
                continue;
1977
            }
1978
            $this->whereCoercedNot($field, $value);
1979
        }
1980
        return $this->update($record);
1981
    }
1982
1983
    /**
1984
     * @param string    $expression
1985
     * @param array     $params
1986
     * @return $this
1987
     */
1988
    public function updateByExpression($expression, array $params)
1989
    {
1990
        $this->_update_raw[] = [$expression, $params];
1991
        return $this;
1992
    }
1993
1994
    /**
1995
     * @param array $data
1996
     * @return int
1997
     * @throws Exception
1998
     */
1999
    public function rawUpdate(array $data=[])
2000
    {
2001
        list($sql, $values) = $this->updateSql($data);
2002
        $this->execute($sql, $values);
2003
        $row_count = $this->rowCount();
2004
        $this->destroy();
2005
        return $row_count;
2006
    }
2007
2008
    /**
2009
     * @param stdClass $record
2010
     * @return array
2011
     */
2012
    public function updateSqlQuery(stdClass $record)
2013
    {
2014
        Assert($record)
2015
            ->notEmpty("The data passed to update does not contain any data")
2016
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2017
2018
        // Make sure we remove the primary key
2019
        $record = (array)$record;
2020
        return $this->updateSql($record);
2021
    }
2022
2023
    /**
2024
     * @param $record
2025
     * @return array
2026
     */
2027
    protected function updateSql(array $record)
2028
    {
2029
        unset($record[$this->getPrimaryKeyName()]);
2030
        // Sqlite needs a null primary key
2031
        //$record[$this->getPrimaryKeyName()] = null;
2032
        $field_list = [];
2033
        foreach ( $record as $key => $value )
2034
        {
2035
            if ( is_numeric($key) )
2036
            {
2037
                $field_list[] = $value;
2038
                unset($record[$key]);
2039
                continue;
2040
            }
2041
            $field_list[] = "{$key} = ?";
2042
        }
2043
        $rawParams  = [];
2044
        foreach ( $this->_update_raw as $rawUpdate )
2045
        {
2046
            $field_list[]   = $rawUpdate[0];
2047
            $rawParams      = array_merge($rawParams, $rawUpdate[1]);
2048
        }
2049
        $field_list     = implode(', ', $field_list);
2050
        $where_str      = $this->_getWhereString();
2051
        $joins          = ! empty($this->_join_sources) ? (' ').implode(' ',$this->_join_sources) : '';
2052
        $alias          = ! empty($this->_table_alias) ? " AS {$this->_table_alias}" : '';
2053
        $sql            = "UPDATE {$this->_table_name}{$alias}{$joins} SET {$field_list}{$where_str}";
2054
        $values         = array_merge(array_values($record), $rawParams, $this->_getWhereParameters());
2055
        return [$sql, $values];
2056
    }
2057
2058
    /**
2059
     * Delete rows
2060
     * Use the query builder to create the where clause
2061
     * @param bool $deleteAll = When there is no where condition, setting to true will delete all
2062
     * @return int - total affected rows
2063
     * @throws Exception
2064
     */
2065
    public function delete($deleteAll=false)
2066
    {
2067
        list($sql, $params) = $this->deleteSqlQuery();
2068
        if ( empty($this->_where_conditions) && ! $deleteAll )
2069
        {
2070
            throw new Exception("You cannot update an entire table without calling update with deleteAll=true");
2071
        }
2072
        $this->execute($sql, $params);
2073
        return $this->rowCount();
2074
    }
2075
2076
    /**
2077
     * @param bool|false $force
2078
     * @return $this
2079
     * @throws Exception
2080
     */
2081
    public function truncate($force=false)
2082
    {
2083
        if ( $force )
2084
        {
2085
            $this->execute('SET FOREIGN_KEY_CHECKS = 0');
2086
        }
2087
        $this->execute("TRUNCATE TABLE {$this->_table_name}");
2088
        if ( $force )
2089
        {
2090
            $this->execute('SET FOREIGN_KEY_CHECKS = 1');
2091
        }
2092
        return $this;
2093
    }
2094
2095
    /**
2096
     * @return array
2097
     */
2098
    public function deleteSqlQuery()
2099
    {
2100
        $query  = "DELETE FROM {$this->_table_name}";
2101
        if ( !empty($this->_where_conditions) )
2102
        {
2103
            $query .= $this->_getWhereString(true);
2104
            return [$query, $this->_getWhereParameters()];
2105
        }
2106
        return [$query, []];
2107
    }
2108
2109
2110
    /**
2111
     * Return the aggregate count of column
2112
     *
2113
     * @param string $column
2114
     * @param int $cacheTtl
2115
     * @return int
2116
     */
2117
    public function count($column='*', $cacheTtl=self::CACHE_NO)
2118
    {
2119
        $this->explicitSelectMode();
2120
        $this->select("COUNT({$column})", 'cnt');
2121
        $result             = $this->fetchOne(null, $cacheTtl);
2122
        return ($result !== false && isset($result->cnt)) ? (float)$result->cnt : 0;
2123
    }
2124
2125
2126
    /**
2127
     * Return the aggregate max count of column
2128
     *
2129
     * @param string $column
2130
     * @param int $cacheTtl
2131
     * @return mixed|null
2132
     */
2133
    public function max($column, $cacheTtl=self::CACHE_NO)
2134
    {
2135
        $this->explicitSelectMode();
2136
        $this->select("MAX({$column})", 'max');
2137
        $result = $this->fetchOne(null, $cacheTtl);
2138
        return ( $result !== false && isset($result->max) ) ? $result->max : null;
2139
    }
2140
2141
2142
    /**
2143
     * Return the aggregate min count of column
2144
     *
2145
     * @param string $column
2146
     * @param int $cacheTtl
2147
     * @return mixed|null
2148
     */
2149
    public function min($column, $cacheTtl=self::CACHE_NO)
2150
    {
2151
        $this->explicitSelectMode();
2152
        $this->select("MIN({$column})", 'min');
2153
        $result = $this->fetchOne(null, $cacheTtl);
2154
        return ( $result !== false && isset($result->min) ) ? $result->min : null;
2155
    }
2156
2157
    /**
2158
     * Return the aggregate sum count of column
2159
     *
2160
     * @param string $column
2161
     * @param int $cacheTtl
2162
     * @return mixed|null
2163
     */
2164
    public function sum($column, $cacheTtl=self::CACHE_NO)
2165
    {
2166
        $this->explicitSelectMode();
2167
        $this->select("SUM({$column})", 'sum');
2168
        $result = $this->fetchOne(null, $cacheTtl);
2169
        return ( $result !== false && isset($result->sum) ) ? $result->sum : null;
2170
    }
2171
2172
    /**
2173
     * Return the aggregate average count of column
2174
     *
2175
     * @param string $column
2176
     * @param int $cacheTtl
2177
     * @return mixed|null
2178
     */
2179
    public function avg($column, $cacheTtl=self::CACHE_NO)
2180
    {
2181
        $this->explicitSelectMode();
2182
        $this->select("AVG({$column})", 'avg');
2183
        $result = $this->fetchOne(null, $cacheTtl);
2184
        return ( $result !== false && isset($result->avg) ) ? $result->avg : null;
2185
    }
2186
2187
    /*******************************************************************************/
2188
// Utilities methods
2189
2190
    /**
2191
     * Reset fields
2192
     *
2193
     * @return $this
2194
     */
2195
    public function reset()
2196
    {
2197
        $this->_where_parameters        = [];
2198
        $this->_select_fields           = [];
2199
        $this->_join_sources            = [];
2200
        $this->_join_aliases            = [];
2201
        $this->_where_conditions        = [];
2202
        $this->_limit                   = null;
2203
        $this->_offset                  = null;
2204
        $this->_order_by                = [];
2205
        $this->_group_by                = [];
2206
        $this->_and_or_operator         = self::OPERATOR_AND;
2207
        $this->_having                  = [];
2208
        $this->_wrap_open               = false;
2209
        $this->_last_wrap_position      = 0;
2210
        $this->_pdo_stmt                = null;
2211
        $this->_distinct                = false;
2212
        $this->_requested_fields        = null;
2213
        $this->_filter_meta             = null;
2214
        $this->_cache_ttl               = -1;
2215
        $this->_timer                   = [];
2216
        $this->_built_query             = null;
2217
        $this->_paging_meta             = [];
2218
        $this->_raw_sql                 = null;
2219
        $this->_explicit_select_mode    = false;
2220
        return $this;
2221
    }
2222
2223
    /**
2224
     * Return a YYYY-MM-DD HH:II:SS date format
2225
     *
2226
     * @param string $datetime - An english textual datetime description
2227
     *          now, yesterday, 3 days ago, +1 week
2228
     *          http://php.net/manual/en/function.strtotime.php
2229
     * @return string YYYY-MM-DD HH:II:SS
2230
     */
2231
    public static function NOW($datetime = 'now')
2232
    {
2233
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
2234
    }
2235
2236
    /**
2237
     * Return a string containing the given number of question marks,
2238
     * separated by commas. Eg '?, ?, ?'
2239
     *
2240
     * @param int - total of placeholder to insert
2241
     * @return string
2242
     */
2243
    protected function _makePlaceholders($number_of_placeholders=1)
2244
    {
2245
        return implode(', ', array_fill(0, $number_of_placeholders, '?'));
2246
    }
2247
2248
    /**
2249
     * Format the table{Primary|Foreign}KeyName
2250
     *
2251
     * @param  string $pattern
2252
     * @param  string $tableName
2253
     * @return string
2254
     */
2255
    protected function _formatKeyName($pattern, $tableName)
2256
    {
2257
        return sprintf($pattern, $tableName);
2258
    }
2259
2260
    /**
2261
     * @param string $query
2262
     * @param array $parameters
2263
     *
2264
     * @return array
2265
     */
2266
    protected function _logQuery($query, array $parameters)
2267
    {
2268
        $query = $this->buildQuery($query, $parameters);
2269
        if ( ! $this->_log_queries )
2270
        {
2271
            return [null, null];
2272
        }
2273
        $ident = substr(str_shuffle(md5($query)), 0, 10);
2274
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
2275
        $this->_timer['start'] = microtime(true);
2276
        return [$query, $ident];
2277
    }
2278
2279
    /**
2280
     * @param $ident
2281
     * @param $builtQuery
2282
     */
2283
    protected function _logSlowQueries($ident, $builtQuery)
2284
    {
2285
        if ( ! $this->_log_queries )
2286
        {
2287
            return ;
2288
        }
2289
        $this->_timer['end']    = microtime(true);
2290
        $seconds_taken          = round($this->_timer['end'] - $this->_timer['start'], 3);
2291
        if ( $seconds_taken > $this->_slow_query_secs )
2292
        {
2293
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$seconds_taken} seconds:\n{$builtQuery}");
2294
        }
2295
    }
2296
2297
    /**
2298
     * @return float
2299
     */
2300
    public function getTimeTaken()
2301
    {
2302
        $seconds_taken = $this->_timer['end'] - $this->_timer['start'];
2303
        return $seconds_taken;
2304
    }
2305
2306
    /**
2307
     * @param $secs
2308
     * @return $this
2309
     */
2310
    public function slowQuerySeconds($secs)
2311
    {
2312
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
2313
        $this->_slow_query_secs = $secs;
2314
        return $this;
2315
    }
2316
2317
2318
    /**
2319
     * @param       $field
2320
     * @param array $values
2321
     * @param null  $placeholder_prefix
2322
     *
2323
     * @return array
2324
     */
2325
    public function getNamedWhereIn($field, array $values, $placeholder_prefix=null)
2326
    {
2327
        Assert($field)->string()->notEmpty();
2328
        Assert($values)->isArray();
2329
        if ( empty($values) )
2330
        {
2331
            return ['', []];
2332
        }
2333
        $placeholder_prefix     = !is_null($placeholder_prefix) ? $placeholder_prefix : strtolower(str_replace('.', '__', $field));
2334
        $params = $placeholders = [];
2335
        $count                  = 1;
2336
        foreach ( $values as $val )
2337
        {
2338
            $name           = "{$placeholder_prefix}_{$count}";
2339
            $params[$name]  = $val;
2340
            $placeholders[] = ":{$name}";
2341
            $count++;
2342
        }
2343
        $placeholders = implode(',', $placeholders);
2344
        return ["AND {$field} IN ({$placeholders})\n", $params];
2345
    }
2346
2347
    /**
2348
     * @param        $field
2349
     * @param string $delimiter
2350
     *
2351
     * @return array
2352
     */
2353
    protected function _getColumnAliasParts($field, $delimiter=':')
2354
    {
2355
        $parts = explode($delimiter, $field);
2356
        if ( count($parts) === 2 )
2357
        {
2358
            return $parts;
2359
        }
2360
        return ['', $field];
2361
    }
2362
2363
    /**
2364
     * @param string $column
2365
     * @param string $term
2366
     * @return $this
2367
     */
2368
    protected function _addWhereClause($column, $term)
2369
    {
2370
        $modifiers = [
2371
            'whereLike'         => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
2372
            'whereNotLike'      => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
2373
            'whereLt'           => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
2374
            'whereLte'          => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
2375
            'whereGt'           => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
2376
            'whereGte'          => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
2377
            'whereBetween'      => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
2378
            'whereNotBetween'  => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
2379
        ];
2380
        foreach ( $modifiers as $func => $regex )
2381
        {
2382
            if ( preg_match($regex, $term, $matches) )
2383
            {
2384
                array_shift($matches);
2385
                switch ($func)
2386
                {
2387
                    case 'whereLike':
2388
2389
                        return $this->whereLike($column, $matches[0]);
2390
2391
                    case 'whereNotLike':
2392
2393
                        return $this->whereNotLike($column, $matches[0]);
2394
2395
                    case 'whereLt':
2396
2397
                        return $this->whereLt($column, $matches[0]);
2398
2399
                    case 'whereLte':
2400
2401
                        return $this->whereLte($column, $matches[0]);
2402
2403
                    case 'whereGt':
2404
2405
                        return $this->whereGt($column, $matches[0]);
2406
2407
                    case 'whereGte':
2408
2409
                        return $this->whereGte($column, $matches[0]);
2410
2411
                    case 'whereBetween':
2412
2413
                        return $this->whereBetween($column, $matches[0], $matches[1]);
2414
2415
                    case 'whereNotBetween':
2416
2417
                        return $this->whereNotBetween($column, $matches[0], $matches[1]);
2418
2419
                }
2420
            }
2421
        }
2422
        return $this->where($column, $term);
2423
    }
2424
2425
    public function destroy()
2426
    {
2427
        if ( !is_null($this->_pdo_stmt) )
2428
        {
2429
            $this->_pdo_stmt->closeCursor();
2430
        }
2431
        $this->_pdo_stmt    = null;
2432
        $this->_handlers    = [];
2433
    }
2434
2435
    public function __destruct()
2436
    {
2437
        $this->destroy();
2438
    }
2439
2440
    /** @var string */
2441
    static protected $_model_namespace = '';
2442
2443
    /** @var bool */
2444
    protected $_validation_exceptions = true;
2445
2446
    /** @var array */
2447
    protected $_paging_meta           = [];
2448
2449
    /** @var int */
2450
    protected $_default_max           = 100;
2451
2452
    /**
2453
     * Load a model
2454
     *
2455
     * @param string $model_name
2456
     * @param AbstractPdo $connection
2457
     * @return Model
2458
     * @throws ModelNotFoundException
2459
     */
2460
    public static function loadModel($model_name, AbstractPdo $connection=null)
2461
    {
2462
        $model_name = static::$_model_namespace . $model_name;
2463
        if ( ! class_exists($model_name) )
2464
        {
2465
            throw new ModelNotFoundException("Failed to find model class {$model_name}.");
2466
        }
2467
        return new $model_name($connection);
2468
    }
2469
2470
    /**
2471
     * Load a model
2472
     *
2473
     * @param string      $table_name
2474
     * @param AbstractPdo $connection
2475
     * @return $this
2476
     */
2477
    public static function loadTable($table_name, AbstractPdo $connection=null)
2478
    {
2479
        $model_name = Inflector::classify($table_name);
2480
        Assert($model_name)->notEmpty("Could not resolve model name from table name.");
2481
        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 2479 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...
2482
    }
2483
2484
    /**
2485
     * @param string   $columnName
2486
     * @param int $cacheTtl
2487
     * @param bool $flushCache
2488
     * @return bool
2489
     */
2490
    public function columnExists($columnName, $cacheTtl=self::CACHE_NO, $flushCache=false)
2491
    {
2492
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
2493
        return array_key_exists($columnName, $columns);
2494
    }
2495
2496
    /**
2497
     * @param int $cacheTtl
2498
     * @param bool $flushCache
2499
     * @return $this
2500
     */
2501
    public function loadSchemaFromDb($cacheTtl=self::CACHE_NO, $flushCache=false)
2502
    {
2503
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
2504
        $this->schema($schema);
2505
        return $this;
2506
    }
2507
2508
    /**
2509
     * @param int $cacheTtl
2510
     * @param bool $flushCache
2511
     * @return array
2512
     * @throws \Terah\Assert\AssertionFailedException
2513
     */
2514
    public function getSchemaFromDb($cacheTtl=self::CACHE_NO, $flushCache=false)
2515
    {
2516
        $table      = $this->getTableName();
2517
        Assert($table)->string()->notEmpty();
2518
        $schema     = [];
2519
        $columns    = $this->_getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
2520
        foreach ( $columns[$table] as $column => $meta )
2521
        {
2522
            $schema[$column] = $meta->data_type;
2523
        }
2524
        return $schema;
2525
    }
2526
2527
    /**
2528
     * @param string $table
2529
     * @param int $cacheTtl
2530
     * @param bool $flushCache
2531
     * @return array
2532
     */
2533
    protected function _getColumnsByTableFromDb($table, $cacheTtl=self::CACHE_NO, $flushCache=false)
2534
    {
2535
        Assert($table)->string()->notEmpty();
2536
2537
        $callback = function() use ($table) {
2538
2539
            return $this->_connection->getColumns(true, $table);
2540
        };
2541
        $cacheKey   = '/column_schema/' . $table;
2542
        if ( $flushCache === true )
2543
        {
2544
            $this->clearCache($cacheKey);
2545
        }
2546
        return $this->_cacheData($cacheKey, $callback, $cacheTtl);
2547
    }
2548
2549
    /**
2550
     * @param string $table
2551
     * @return bool
2552
     */
2553
    public function clearSchemaCache($table)
2554
    {
2555
        return $this->clearCache('/column_schema/' . $table);
2556
    }
2557
2558
    /**
2559
     * @param stdClass $record
2560
     * @return stdClass
2561
     */
2562
    public function onFetch(stdClass $record)
2563
    {
2564
        $record = $this->_trimAndLowerCaseKeys($record);
2565
        if ( $this->_filter_on_fetch )
2566
        {
2567
            $record = $this->cleanseRecord($record);
2568
        }
2569
        return $this->fixTypes($record);
2570
    }
2571
2572
    /**
2573
     * @param stdClass $record
2574
     * @return stdClass
2575
     */
2576
    public function cleanseRecord(stdClass $record)
2577
    {
2578
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2579
        {
2580
            if ( is_string($record->$field) )
2581
            {
2582
                $record->$field = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", filter_var($record->$field, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES));
2583
                if ( $this->_log_filter_changes && $value !== $record->$field )
2584
                {
2585
                    $table = $this->_table_name ? $this->_table_name : '';
2586
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->$field]);
2587
                }
2588
            }
2589
        }
2590
        return $record;
2591
    }
2592
2593
    /**
2594
     * @param stdClass $record
2595
     * @param string   $type
2596
     * @return stdClass
2597
     */
2598
    public function beforeSave(stdClass $record, $type)
2599
    {
2600
        $record = $this->addDefaultFields($record, $type);
2601
        $record = $this->applyGlobalModifiers($record, $type);
2602
        $record = $this->applyHandlers($record, $type);
2603
        $record = $this->removeUnneededFields($record, $type);
2604
        return $record;
2605
    }
2606
2607
    /**
2608
     * @param stdClass $record
2609
     * @param string   $type
2610
     * @return stdClass
2611
     */
2612
    public function afterSave(stdClass $record, $type)
2613
    {
2614
        unset($type);
2615
        $this->clearCacheByTable();
2616
        return $record;
2617
    }
2618
2619
    /**
2620
     * @param stdClass $record
2621
     * @param string   $type
2622
     * @return stdClass
2623
     */
2624
    public function addDefaultFields(stdClass $record, $type)
2625
    {
2626
        unset($type);
2627
        return $record;
2628
    }
2629
2630
    public function applyGlobalModifiers(stdClass $record, $type)
2631
    {
2632
        unset($type);
2633
        return $record;
2634
    }
2635
2636
    public function removeUnneededFields(\stdClass $record, $type)
2637
    {
2638
        unset($type);
2639
        // remove un-needed fields
2640
        $columns = $this->getColumns(true);
2641
        if ( empty($columns) )
2642
        {
2643
            return $record;
2644
        }
2645
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2646
        {
2647
            if ( ! in_array($name, $columns) || in_array($name, $this->_virtual_fields) )
2648
            {
2649
                unset($record->$name);
2650
            }
2651
        }
2652
        return $record;
2653
    }
2654
2655
    /**
2656
     * @param array $data
2657
     * @param string $saveType
2658
     * @return array
2659
     */
2660
    public function cleanseWebData($data, $saveType)
2661
    {
2662
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
2663
        $columns = $this->getColumns(false);
2664
        if ( empty($columns) )
2665
        {
2666
            return $data;
2667
        }
2668
        foreach ( $data as $field => $val )
2669
        {
2670
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
2671
        }
2672
        return array_intersect_key($data, $columns);
2673
    }
2674
2675
    /**
2676
     * @return array
2677
     */
2678
    public function skeleton()
2679
    {
2680
        $skel       = [];
2681
        $columns    = $this->columns(false);
2682
        foreach ( $columns as $column => $type )
2683
        {
2684
            $skel[$column] = null;
2685
        }
2686
        return $skel;
2687
    }
2688
2689
2690
    /**
2691
     * @return Closure[]
2692
     */
2693
    protected function _getFieldHandlers()
2694
    {
2695
        return [];
2696
    }
2697
2698
    /**
2699
     * @param bool $toString
2700
     * @return array|string
2701
     */
2702
    public function getErrors($toString=false)
2703
    {
2704
        if ( $toString )
2705
        {
2706
            $errors = [];
2707
            foreach ( $this->_errors as $field => $error )
2708
            {
2709
                $errors[] = implode("\n", $error);
2710
            }
2711
            return implode("\n", $errors);
2712
        }
2713
        return $this->_errors;
2714
    }
2715
2716
    /**
2717
     * @param bool $throw
2718
     * @return $this
2719
     */
2720
    public function validationExceptions($throw=true)
2721
    {
2722
        $this->_validation_exceptions = $throw;
2723
        return $this;
2724
    }
2725
2726
    /**
2727
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
2728
     *
2729
     * @return $this
2730
     * @throws Exception
2731
     */
2732
    public function paginate(array $query=[])
2733
    {
2734
        $_fields = $_order = $_limit = $_offset = null;
2735
        extract($query);
2736
        $this->_setLimit($_limit, $_offset);
2737
        $this->_setOrderBy($_order);
2738
        $this->_setFields($_fields);
2739
        return $this;
2740
    }
2741
2742
    /**
2743
     * @param null|string $limit
2744
     * @param null|string|int $offset
2745
     * @return $this
2746
     */
2747
    protected function _setLimit($limit=null, $offset=null)
2748
    {
2749
        $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...
2750
        if ( ! is_numeric($limit) )
2751
        {
2752
            return $this;
2753
        }
2754
        $this->limit((int)$limit);
2755
        if ( $offset && is_numeric($offset) )
2756
        {
2757
            $this->offset((int)$offset);
2758
        }
2759
        return $this;
2760
    }
2761
2762
    /**
2763
     * @param null|string|array $fields
2764
     * @return $this
2765
     * @throws Exception
2766
     */
2767
    protected function _setFields($fields=null)
2768
    {
2769
        if ( ! $fields )
2770
        {
2771
            return $this;
2772
        }
2773
        $this->explicitSelectMode();
2774
        $columns    = $this->getColumns();
2775
        $fields     = is_array($fields) ? $fields : explode('|', $fields);
2776
2777
        foreach ( $fields as $idx => $field )
2778
        {
2779
            list($alias, $field) = $this->_getColumnAliasParts($field);
2780
            $field = $field === '_display_field' ? $this->_display_column : $field;
2781
            // Regular primary table field
2782
            if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
2783
            {
2784
                $this->select("{$this->_table_alias}.{$field}");
2785
                $this->_requested_fields[] = "{$this->_table_alias}.{$field}";
2786
                continue;
2787
            }
2788
            // Reference table field with alias
2789
            if ( ! empty($alias) )
2790
            {
2791
                Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
2792
                Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
2793
                list(, , $join_field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
2794
                $this->autoJoin($alias, static::LEFT_JOIN, false);
2795
                $this->select($join_field, $fieldAlias);
2796
                $this->_requested_fields[] = "{$join_field} AS {$fieldAlias}";
2797
                continue;
2798
            }
2799
            // Reference table select field without alias
2800
            foreach ( $this->_associations['belongsTo'] as $joinAlias => $config )
2801
            {
2802
                list(, , $join_field, $fieldAlias) = $config;
2803
                if ( $field === $config[3] )
2804
                {
2805
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
2806
                    $this->select($join_field, $fieldAlias);
2807
                    $this->_requested_fields[] = "{$join_field} AS {$fieldAlias}";
2808
                    continue;
2809
                }
2810
            }
2811
        }
2812
        return $this;
2813
    }
2814
2815
    /**
2816
     * @param null|string $orderBy
2817
     * @return $this|FluentPdoModel
2818
     */
2819
    protected function _setOrderBy($orderBy=null)
2820
    {
2821
        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...
2822
        {
2823
            return $this;
2824
        }
2825
        $columns                    = $this->getColumns();
2826
        list($order, $direction)    = strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
2827
        list($alias, $field)        = $this->_getColumnAliasParts(trim($order), '.');
2828
        $field                      = explode(' ', $field);
2829
        $field                      = trim($field[0]);
2830
        $direction                  = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
2831
2832
        // Regular primary table order by
2833
        if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
2834
        {
2835
            return $this->orderBy("{$this->_table_alias}.{$field}", $direction);
2836
        }
2837
        // Reference table order by with alias
2838
        if ( ! empty($alias) )
2839
        {
2840
            Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
2841
            Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
2842
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
2843
        }
2844
        // Reference table order by without alias
2845
        foreach ( $this->_associations['belongsTo'] as $joinAlias => $config )
2846
        {
2847
            if ( $field === $config[3] )
2848
            {
2849
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
2850
            }
2851
        }
2852
        return $this;
2853
    }
2854
2855
    /**
2856
     * @param null $type
2857
     * @return array
2858
     */
2859
    public function getPagingMeta($type=null)
2860
    {
2861
        if ( empty($this->_paging_meta) )
2862
        {
2863
            $this->setPagingMeta();
2864
        }
2865
        return is_null($type) ? $this->_paging_meta : $this->_paging_meta[$type];
2866
    }
2867
2868
    /**
2869
     * @return $this
2870
     */
2871
    public function setPagingMeta()
2872
    {
2873
        $model      = clone $this;
2874
        $limit      = intval($this->getLimit());
2875
        $offset     = intval($this->getOffset());
2876
        $total      = intval($model->withBelongsTo()->select(null)->offset(null)->limit(null)->orderBy(null)->count());
2877
        unset($model->_handlers, $model); //hhmv mem leak
2878
        $order_bys  = !is_array($this->_order_by) ? [] : $this->_order_by;
2879
        $this->_paging_meta  = [
2880
            'limit'     => $limit,
2881
            'offset'    => $offset,
2882
            'page'      => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
2883
            'pages'     => $limit === 0 ? 1 : intval(ceil($total / $limit)),
2884
            'order'     => $order_bys,
2885
            'total'     => $total = $limit === 1 && $total > 1 ? 1 : $total,
2886
            'filters'   => is_null($this->_filter_meta) ? [] : $this->_filter_meta,
2887
            'fields'    => is_null($this->_requested_fields) ? [] : $this->_requested_fields,
2888
            'perms'     => [],
2889
        ];
2890
        return $this;
2891
    }
2892
2893
    /**
2894
     * Take a web request and format a query
2895
     *
2896
     * @param array $query
2897
     *
2898
     * @return $this
2899
     * @throws Exception
2900
     */
2901
    public function filter(array $query=[])
2902
    {
2903
        $columns   = $this->getColumns(false);
2904
        $alias     = '';
2905
        foreach ( $query as $column => $value )
2906
        {
2907
            if ( in_array($column, $this->_pagination_attribs) )
2908
            {
2909
                continue;
2910
            }
2911
            $field = $this->_findFieldByQuery($column, $this->_display_column);
2912
            if ( is_null($field) )
2913
            {
2914
                continue;
2915
            }
2916
            $this->_filter_meta[$field]     = $value;
2917
            $where                          = !is_array($value) && mb_stripos($value, '|') !== false ? explode('|', $value) : $value;
2918
            if ( is_array($where) )
2919
            {
2920
                $this->whereIn($field, $where);
2921
            }
2922
            else
2923
            {
2924
                $this->_addWhereClause($field, $where);
2925
            }
2926
        }
2927
        if ( empty($query['_search']) )
2928
        {
2929
            return $this;
2930
        }
2931
        $alias          = !empty($alias) ? $alias : $this->_table_alias;
2932
        $string_cols    = array_filter($columns, function($type) {
2933
2934
            return in_array($type, ['varchar', 'text', 'enum']);
2935
        });
2936
        $terms          = explode('|', $query['_search']);
2937
        $where_likes    = [];
2938
        foreach ( $string_cols as $column => $type )
2939
        {
2940
            foreach ( $terms as $term )
2941
            {
2942
                $where_likes["{$alias}.{$column}"] = "%{$term}%";
2943
            }
2944
        }
2945
        // Reference fields...
2946
        $belongsTo = $this->getSearchableAssociations();
2947
        foreach ( $belongsTo as $alias => $config )
2948
        {
2949
            foreach ( $terms as $term )
2950
            {
2951
                $where_likes[$config[2]] = "%{$term}%";
2952
            }
2953
        }
2954
        if ( empty($where_likes) )
2955
        {
2956
            return $this;
2957
        }
2958
        $this->where([1=>1])->wrap()->_and();
2959
        foreach ( $where_likes as $column => $term )
2960
        {
2961
            $this->_or()->whereLike($column, $term);
2962
        }
2963
        $this->wrap();
2964
        return $this;
2965
    }
2966
2967
    /**
2968
     * @param string $column
2969
     * @param $displayCol
2970
     * @return string|null
2971
     */
2972
    protected function _findFieldByQuery($column, $displayCol)
2973
    {
2974
        list($alias, $field)    = $this->_getColumnAliasParts($column);
2975
        $field                  = $field === '_display_field' ? $displayCol : $field;
2976
        $columns                = $this->getColumns();
2977
        $tableAlias             = $this->getTableAlias();
2978
        if ( ! empty($alias) && $alias === $tableAlias )
2979
        {
2980
            // Alias is set but the field isn't correct
2981
            if ( ! in_array($field, $columns) )
2982
            {
2983
                return null;
2984
            }
2985
            return "{$alias}.{$field}";
2986
        }
2987
        // Alias isn't passed in but the field is ok
2988
        if ( empty($alias) && in_array($field, $columns) )
2989
        {
2990
            return "{$tableAlias}.{$field}";
2991
        }
2992
        // Alias is passed but not this table in but there is a matching field on this table
2993
        if ( ! empty($alias) && in_array($field, $columns) )
2994
        {
2995
            return null;
2996
        }
2997
        // Now search the associations for the field
2998
        $associations = $this->getSearchableAssociations();
2999
        if ( ! empty($alias) )
3000
        {
3001
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
3002
            {
3003
                return "{$alias}.{$field}";
3004
            }
3005
            return null;
3006
        }
3007
        foreach ( $associations as $assocAlias => $config )
3008
        {
3009
            list(, , $assocField, $fieldAlias) = $config;
3010
            if ( $fieldAlias === $field )
3011
            {
3012
                return $assocField;
3013
            }
3014
        }
3015
        return null;
3016
    }
3017
3018
    public function getSearchableAssociations()
3019
    {
3020
        $belongsTo = ! empty($this->_associations['belongsTo']) ? $this->_associations['belongsTo'] : [];
3021
        return $belongsTo;
3022
    }
3023
3024
    /**
3025
     * @param $keys_only
3026
     * @return array
3027
     */
3028
3029
    public function columns($keys_only=true)
3030
    {
3031
        return $keys_only ? array_keys($this->_schema) : $this->_schema;
3032
    }
3033
3034
    /**
3035
     * @param string $field
3036
     * @param mixed $value
3037
     * @param bool|false $permissive
3038
     * @return float|int|null|string
3039
     */
3040
    protected function _fixType($field, $value, $permissive=false)
3041
    {
3042
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
3043
        if ( empty($this->_schema) || ( ! array_key_exists($field, $this->_schema) && $permissive ) )
3044
        {
3045
            return $value;
3046
        }
3047
        $columns    = $this->getColumns(false);
3048
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
3049
3050
        $field_type = ! empty($columns[$field]) ? $columns[$field] : null;
3051
3052
        if ( is_null($value) )
3053
        {
3054
            return null;
3055
        }
3056
        // return on null, '' but not 0
3057
        if ( ! is_numeric($value) && empty($value) )
3058
        {
3059
            return null;
3060
        }
3061
        // Don't cast invalid values... only those that can be cast cleanly
3062
        switch ($field_type)
3063
        {
3064
            case 'varchar':
3065
            case 'text';
3066
            case 'date':
3067
            case 'datetime':
3068
            case 'timestamp':
3069
                return (string)$value;
3070
            case 'int':
3071
            case 'tinyint':
3072
                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...
3073
            case 'decimal':
3074
                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...
3075
            default:
3076
                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...
3077
        }
3078
    }
3079
3080
    /**
3081
     * @param stdClass $record
3082
     * @param string $type
3083
     * @return stdClass
3084
     */
3085
    public function fixTypes(stdClass $record, $type=null)
3086
    {
3087
        foreach ( $this->getColumns(false) as $column => $field_type )
3088
        {
3089
            if ( ! property_exists($record, $column) || is_null($record->$column) )
3090
            {
3091
                continue;
3092
            }
3093
            $record->$column = $this->_fixType($column, $record->$column);
3094
        }
3095
        unset($type);
3096
        return $record;
3097
    }
3098
3099
    /**
3100
     * @param stdClass $record
3101
     * @param string   $type
3102
     * @return stdClass
3103
     * @throws Exception
3104
     */
3105
    public function applyHandlers(stdClass $record, $type='INSERT')
3106
    {
3107
        $this->_compileHandlers();
3108
        $this->_errors                  = [];
3109
        // Disable per field exceptions so we can capture all errors for the record
3110
        $tmpExceptions                  = $this->_validation_exceptions;
3111
        $this->_validation_exceptions   = false;
3112
        foreach ( $this->_handlers as $field => $fn_validator )
3113
        {
3114
            if ( ! property_exists($record, $field) )
3115
            {
3116
                // If the operation is an update it can be a partial update
3117
                if ( $type === self::SAVE_UPDATE )
3118
                {
3119
                    continue;
3120
                }
3121
                $record->$field = null;
3122
            }
3123
            $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...
3124
        }
3125
        $this->_validation_exceptions = $tmpExceptions;
3126
        if ( $this->_validation_exceptions && ! empty($this->_errors) )
3127
        {
3128
            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...
3129
        }
3130
        return $record;
3131
    }
3132
3133
    /**
3134
     * @param string $field
3135
     * @param mixed $value
3136
     * @param null $type
3137
     * @param null $record
3138
     * @return null
3139
     * @throws Exception
3140
     */
3141
    protected function applyHandler($field, $value, $type=null, $record=null)
3142
    {
3143
        $this->_compileHandlers();
3144
        $fnHandler = ! empty($this->_handlers[$field]) ? $this->_handlers[$field] : null;
3145
        if ( is_callable($fnHandler) )
3146
        {
3147
            try
3148
            {
3149
                $value = $fnHandler($field, $value, $type, $record);
3150
            }
3151
            catch( Exception $e )
3152
            {
3153
                $this->_errors[$field][] = $e->getMessage();
3154
                if ( $this->_validation_exceptions && ! empty($this->_errors) )
3155
                {
3156
                    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...
3157
                }
3158
                return null;
3159
            }
3160
        }
3161
        return $value;
3162
    }
3163
3164
    protected function _compileHandlers()
3165
    {
3166
        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...
3167
        {
3168
            return;
3169
        }
3170
        $this->_handlers    = array_merge([], $this->_getFieldHandlers());
3171
    }
3172
}
3173
3174
class ModelNotFoundException extends \Exception{}
3175
3176
class ModelFailedValidationException extends \Exception
3177
{
3178
    private $validationErrors;
3179
3180
    /**
3181
     * @return array
3182
     */
3183
    public function getValidationErrors($asString=false)
3184
    {
3185
        $errors = is_array($this->validationErrors) ? $this->validationErrors : [];
3186
        return $asString ? json_encode($errors, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) : $errors;
3187
    }
3188
3189
    /**
3190
     * @param string         $message
3191
     * @param array          $validationErrors
3192
     * @param int            $code
3193
     * @param Exception|null $previous
3194
     */
3195
    public function __construct($message="", array $validationErrors=[], $code=0, Exception $previous = null)
3196
    {
3197
        $this->validationErrors = $validationErrors;
3198
        parent::__construct($message, $code, $previous);
3199
    }
3200
}
3201