Completed
Push — master ( 2dc4cf...1e0c3d )
by Terry
04:09
created

FluentPdoModel::getTableName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 4
c 2
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * @package Terah\FluentPdoModel
4
 *
5
 * Original work Copyright (c) 2014 Mardix (http://github.com/mardix)
6
 * Modified work Copyright (c) 2015 Terry Cullen (http://github.com/terah)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
13
 */
14
namespace Terah\FluentPdoModel;
15
16
use Closure;
17
use PDOException;
18
use Exception;
19
use PDO;
20
use PDOStatement;
21
use stdClass;
22
use DateTime;
23
use Terah\FluentPdoModel\Drivers\AbstractPdo;
24
use Psr\Log\AbstractLogger;
25
use Terah\RedisCache\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
        if ( preg_match('/ as /i', $keyedOn) )
878
        {
879
            list($keyedOn, $keyedOnAlias)   = preg_split('/ as /i', $keyedOn);
880
            $keyedOn                        = trim($keyedOn);
881
            $keyedOnAlias                   = trim($keyedOnAlias);
882
        }
883
        if ( preg_match('/ as /i', $valueField) )
884
        {
885
            list($valueField, $valueFieldAlias) = preg_split('/ as /i', $valueField);
886
            $valueField                         = trim($valueField);
887
            $valueFieldAlias                    = trim($valueFieldAlias);
888
        }
889
890
        $this->_cacheTtl($cacheTtl);
891
        $fnCallback         = function() use ($keyedOn, $keyedOnAlias, $valueField, $valueFieldAlias) {
892
893
            $rows = [];
894
            $stmt = $this->select(null)
895
                ->select($keyedOn, $keyedOnAlias)
896
                ->select($valueField, $valueFieldAlias)
897
                ->fetchStmt();
898
            while ( $record = $this->fetchRow($stmt) )
899
            {
900
                $rows[$record->$keyedOnAlias] = $record->$valueFieldAlias;
901
            }
902
            return $rows;
903
        };
904
        if ( $this->_cache_ttl === self::CACHE_NO )
905
        {
906
            $result = $fnCallback();
907
            unset($cacheKey, $fnCallback);
908
            return $result;
909
        }
910
        $table              = $this->getTableName();
911
        $cacheKey           = md5(json_encode([
912
            'sql'               => $this->fetchSqlQuery(),
913
            'keyed_on'          => $keyedOn,
914
            'keyed_on_alias'    => $keyedOnAlias,
915
            'value_field'       => $valueField,
916
            'value_fieldAlias'  => $valueFieldAlias,
917
        ]));
918
        return $this->_cacheData("/{$table}/list/{$cacheKey}", $fnCallback, $this->_cache_ttl);
919
    }
920
921
    /**
922
     * @param string $column
923
     * @param int $cacheTtl
924
     * @param bool|true $unique
925
     * @return array|mixed
926
     */
927
    public function fetchColumn($column, $cacheTtl=self::CACHE_NO, $unique=true)
928
    {
929
        $list = $this->select($column)->fetch(null, $cacheTtl);
930
        foreach ( $list as $idx => $obj )
931
        {
932
            $list[$idx] = $obj->{$column};
933
        }
934
        return $unique ? array_unique($list) : $list;
935
    }
936
937
    /**
938
     * @param null|string $field
939
     * @param null|int $itemId
940
     * @param int $cacheTtl
941
     * @return mixed|null
942
     */
943
    public function fetchField($field=null, $itemId=null, $cacheTtl=self::CACHE_NO)
944
    {
945
        $field  = !is_null($field) ? $field : $this->getPrimaryKeyName();
946
        $this->_cacheTtl($cacheTtl);
947
        $object = $this->select(null)->select($field)->fetchOne($itemId);
948
        if ( ! $object )
949
        {
950
            return null;
951
        }
952
        // Handle aliases
953
        if ( preg_match('/ as /i', $field) )
954
        {
955
            list($expression, $alias) = preg_split('/ as /i', $field);
956
            unset($expression);
957
            $field = trim($alias);
958
        }
959
        if ( strpos($field, '.') !== false )
960
        {
961
            list($tableAlias, $field) = explode('.', $field);
962
            unset($tableAlias);
963
        }
964
        return property_exists($object, $field) ? $object->$field : null;
965
    }
966
967
    /**
968
     * @param int|null $id
969
     * @param int $cacheTtl
970
     * @return \stdClass|bool
971
     */
972
    public function fetchOne($id=null, $cacheTtl=self::CACHE_NO)
973
    {
974
        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...
975
        {
976
            $this->wherePK($id, true);
977
        }
978
        $this->_cacheTtl($cacheTtl);
979
        $this->limit(1);
980
        $fetchAll = $this->fetch();
981
        return $fetchAll ? array_shift($fetchAll) : false;
982
    }
983
984
    /**
985
     * @param int|null $id
986
     * @param int $cacheTtl
987
     * @return \stdClass|bool
988
     */
989
    public function fetchExists($id=null, $cacheTtl=self::CACHE_NO)
990
    {
991
        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...
992
        {
993
            $this->wherePK($id, true);
994
        }
995
        $this->_cacheTtl($cacheTtl);
996
        return $this->count() !== 0;
997
    }
998
999
    /*------------------------------------------------------------------------------
1000
                                    Fluent Query Builder
1001
    *-----------------------------------------------------------------------------*/
1002
1003
    /**
1004
     * Create the select clause
1005
     *
1006
     * @param  mixed    $columns  - the column to select. Can be string or array of fields
1007
     * @param  string   $alias - an alias to the column
1008
     * @param bool|true $explicitSelect
1009
     * @return $this
1010
     */
1011
    public function select($columns='*', $alias=null, $explicitSelect=true)
1012
    {
1013
        if ( $explicitSelect )
1014
        {
1015
            $this->explicitSelectMode();
1016
        }
1017
        if ( ! empty($alias) && !is_array($columns) & $columns !== $alias )
1018
        {
1019
            $columns .= " AS {$alias} ";
1020
        }
1021
        if ( $columns === '*' && !empty($this->_schema) )
1022
        {
1023
            $columns = $this->getColumns();
1024
        }
1025
        // Reset the select list
1026
        if ( is_null($columns) )
1027
        {
1028
            $this->_select_fields = [];
1029
            return $this;
1030
        }
1031
        $columns = is_array($columns) ? $columns : [$columns];
1032
1033
//        if ( empty($this->_select_fields) && $addAllIfEmpty )
1034
//        {
1035
//            $this->select('*');
1036
//        }
1037
        if ( $this->_table_alias )
1038
        {
1039
            $schema = $this->columns();
1040
            foreach ( $columns as $idx => $col )
1041
            {
1042
                if ( in_array($col, $schema) )
1043
                {
1044
                    $columns[$idx] = "{$this->_table_alias}.{$col}";
1045
                }
1046
            }
1047
        }
1048
        $this->_select_fields = array_merge($this->_select_fields, $columns);
1049
        return $this;
1050
    }
1051
1052
    public function selectRaw($select)
1053
    {
1054
        $this->_select_fields[] = $select;
1055
        return $this;
1056
    }
1057
1058
    /**
1059
     * @param bool $log_queries
1060
     *
1061
     * @return $this
1062
     */
1063
    public function logQueries($log_queries=true)
1064
    {
1065
        $this->_log_queries = $log_queries;
1066
        return $this;
1067
    }
1068
1069
    /**
1070
     * @param bool $include_count
1071
     *
1072
     * @return $this
1073
     */
1074
    public function includeCount($include_count=true)
1075
    {
1076
        $this->_include_count = $include_count;
1077
        return $this;
1078
    }
1079
1080
    /**
1081
     * @param bool $distinct
1082
     *
1083
     * @return $this
1084
     */
1085
    public function distinct($distinct=true)
1086
    {
1087
        $this->_distinct = $distinct;
1088
        return $this;
1089
    }
1090
1091
    /**
1092
     * @param array $fields
1093
     * @return $this
1094
     */
1095
    public function withBelongsTo($fields=[])
1096
    {
1097
        if ( ! empty($this->_associations['belongsTo']) )
1098
        {
1099
            foreach ( $this->_associations['belongsTo'] as $alias => $config )
1100
            {
1101
                $addFieldsForJoins = empty($fields) || in_array($config[3], $fields);
1102
                $this->autoJoin($alias, self::LEFT_JOIN, $addFieldsForJoins);
1103
            }
1104
        }
1105
        return $this;
1106
    }
1107
1108
    /**
1109
     * @param string $alias
1110
     * @param string $type
1111
     * @param bool   $addSelectField
1112
     * @return $this
1113
     */
1114
    public function autoJoin($alias, $type=self::LEFT_JOIN, $addSelectField=true)
1115
    {
1116
        Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid join... the alias does not exists");
1117
        list($table, $join_col, $field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
1118
        $condition = "{$alias}.id = {$this->_table_alias}.{$join_col}";
1119
        if ( in_array($alias, $this->_join_aliases) )
1120
        {
1121
            return $this;
1122
        }
1123
        $this->join($table, $condition, $alias, $type);
1124
        if ( $addSelectField )
1125
        {
1126
            $this->select($field, $fieldAlias, false);
1127
        }
1128
        return $this;
1129
    }
1130
1131
    /**
1132
     * Add where condition, more calls appends with AND
1133
     *
1134
     * @param string $condition possibly containing ? or :name
1135
     * @param mixed $parameters accepted by PDOStatement::execute or a scalar value
1136
     * @param mixed ...
1137
     * @return $this
1138
     */
1139
    public function where($condition, $parameters = [])
1140
    {
1141
        // By default the and_or_operator and wrap operator is AND,
1142
        if ( $this->_wrap_open || ! $this->_and_or_operator )
1143
        {
1144
            $this->_and();
1145
        }
1146
1147
        // where(array("column1" => 1, "column2 > ?" => 2))
1148
        if ( is_array($condition) )
1149
        {
1150
            foreach ($condition as $key => $val)
1151
            {
1152
                $this->where($key, $val);
1153
            }
1154
            return $this;
1155
        }
1156
1157
        $args = func_num_args();
1158
        if ( $args != 2 || strpbrk($condition, '?:') )
1159
        { // where('column < ? OR column > ?', array(1, 2))
1160
            if ( $args != 2 || !is_array($parameters) )
1161
            { // where('column < ? OR column > ?', 1, 2)
1162
                $parameters = func_get_args();
1163
                array_shift($parameters);
1164
            }
1165
        }
1166
        else if ( !is_array($parameters) )
1167
        {//where(column,value) => column=value
1168
            $condition .= ' = ?';
1169
            $parameters = [$parameters];
1170
        }
1171
        else if ( is_array($parameters) )
1172
        { // where('column', array(1, 2)) => column IN (?,?)
1173
            $placeholders = $this->_makePlaceholders(count($parameters));
1174
            $condition = "({$condition} IN ({$placeholders}))";
1175
        }
1176
1177
        $this->_where_conditions[] = [
1178
            'STATEMENT'   => $condition,
1179
            'PARAMS'      => $parameters,
1180
            'OPERATOR'    => $this->_and_or_operator
1181
        ];
1182
        // Reset the where operator to AND. To use OR, you must call _or()
1183
        $this->_and();
1184
        return $this;
1185
    }
1186
1187
    /**
1188
     * Create an AND operator in the where clause
1189
     *
1190
     * @return $this
1191
     */
1192
    public function _and()
1193
    {
1194
        if ( $this->_wrap_open )
1195
        {
1196
            $this->_where_conditions[] = self::OPERATOR_AND;
1197
            $this->_last_wrap_position = count($this->_where_conditions);
1198
            $this->_wrap_open = false;
1199
            return $this;
1200
        }
1201
        $this->_and_or_operator = self::OPERATOR_AND;
1202
        return $this;
1203
    }
1204
1205
1206
    /**
1207
     * Create an OR operator in the where clause
1208
     *
1209
     * @return $this
1210
     */
1211
    public function _or()
1212
    {
1213
        if ( $this->_wrap_open )
1214
        {
1215
            $this->_where_conditions[] = self::OPERATOR_OR;
1216
            $this->_last_wrap_position = count($this->_where_conditions);
1217
            $this->_wrap_open = false;
1218
            return $this;
1219
        }
1220
        $this->_and_or_operator = self::OPERATOR_OR;
1221
        return $this;
1222
    }
1223
1224
    /**
1225
     * To group multiple where clauses together.
1226
     *
1227
     * @return $this
1228
     */
1229
    public function wrap()
1230
    {
1231
        $this->_wrap_open = true;
1232
        $spliced = array_splice($this->_where_conditions, $this->_last_wrap_position, count($this->_where_conditions), '(');
1233
        $this->_where_conditions = array_merge($this->_where_conditions, $spliced);
1234
        array_push($this->_where_conditions,')');
1235
        $this->_last_wrap_position = count($this->_where_conditions);
1236
        return $this;
1237
    }
1238
1239
    /**
1240
     * Where Primary key
1241
     *
1242
     * @param      integer $id
1243
     * @param bool $addAlias
1244
     *
1245
     * @return $this
1246
     */
1247
    public function wherePK($id, $addAlias=true)
1248
    {
1249
        $alias = $addAlias && !empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1250
        return $this->where($alias . $this->getPrimaryKeyName(), $id);
1251
    }
1252
1253
    /**
1254
     * WHERE $columnName != $value
1255
     *
1256
     * @param  string   $columnName
1257
     * @param  mixed    $value
1258
     * @return $this
1259
     */
1260
    public function whereNot($columnName, $value)
1261
    {
1262
        return $this->where("$columnName != ?", $value);
1263
    }
1264
    /**
1265
     * WHERE $columnName != $value
1266
     *
1267
     * @param  string   $columnName
1268
     * @param  mixed    $value
1269
     * @return $this
1270
     */
1271
    public function whereCoercedNot($columnName, $value)
1272
    {
1273
        return $this->where("IFNULL({$columnName}, '') != ?", $value);
1274
    }
1275
1276
    /**
1277
     * WHERE $columnName LIKE $value
1278
     *
1279
     * @param  string   $columnName
1280
     * @param  mixed    $value
1281
     * @return $this
1282
     */
1283
    public function whereLike($columnName, $value)
1284
    {
1285
        return $this->where("$columnName LIKE ?", $value);
1286
    }
1287
1288
    /**
1289
     * @param string $columnName
1290
     * @param mixed $value1
1291
     * @param mixed $value2
1292
     * @return $this
1293
     */
1294
    public function whereBetween($columnName, $value1, $value2)
1295
    {
1296
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1297
    }
1298
1299
    /**
1300
     * @param string $columnName
1301
     * @param mixed $value1
1302
     * @param mixed $value2
1303
     * @return $this
1304
     */
1305
    public function whereNotBetween($columnName, $value1, $value2)
1306
    {
1307
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1308
    }
1309
1310
    /**
1311
     * @param string $columnName
1312
     * @param string $regex
1313
     * @return $this
1314
     */
1315
    public function whereRegex($columnName, $regex)
1316
    {
1317
        return $this->where("$columnName REGEXP ?", $regex);
1318
    }
1319
1320
    /**
1321
     * @param string $columnName
1322
     * @param string $regex
1323
     * @return $this
1324
     */
1325
    public function whereNotRegex($columnName, $regex)
1326
    {
1327
        return $this->where("$columnName NOT REGEXP ?", $regex);
1328
    }
1329
1330
    /**
1331
     * WHERE $columnName NOT LIKE $value
1332
     *
1333
     * @param  string   $columnName
1334
     * @param  mixed    $value
1335
     * @return $this
1336
     */
1337
    public function whereNotLike($columnName, $value)
1338
    {
1339
        return $this->where("$columnName NOT LIKE ?", $value);
1340
    }
1341
1342
    /**
1343
     * WHERE $columnName > $value
1344
     *
1345
     * @param  string   $columnName
1346
     * @param  mixed    $value
1347
     * @return $this
1348
     */
1349
    public function whereGt($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 whereGte($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 whereLt($columnName, $value)
1374
    {
1375
        return $this->where("$columnName < ?", $value);
1376
    }
1377
1378
    /**
1379
     * WHERE $columnName <= $value
1380
     *
1381
     * @param  string   $columnName
1382
     * @param  mixed    $value
1383
     * @return $this
1384
     */
1385
    public function whereLte($columnName, $value)
1386
    {
1387
        return $this->where("$columnName <= ?", $value);
1388
    }
1389
1390
    /**
1391
     * WHERE $columnName IN (?,?,?,...)
1392
     *
1393
     * @param  string   $columnName
1394
     * @param  array    $values
1395
     * @return $this
1396
     */
1397
    public function whereIn($columnName, array $values)
1398
    {
1399
        return $this->where($columnName,$values);
1400
    }
1401
1402
    /**
1403
     * WHERE $columnName NOT IN (?,?,?,...)
1404
     *
1405
     * @param  string   $columnName
1406
     * @param  array    $values
1407
     * @return $this
1408
     */
1409
    public function whereNotIn($columnName, array $values)
1410
    {
1411
        $placeholders = $this->_makePlaceholders(count($values));
1412
        return $this->where("({$columnName} NOT IN ({$placeholders}))", $values);
1413
    }
1414
1415
    /**
1416
     * WHERE $columnName IS NULL
1417
     *
1418
     * @param  string   $columnName
1419
     * @return $this
1420
     */
1421
    public function whereNull($columnName)
1422
    {
1423
        return $this->where("({$columnName} IS NULL)");
1424
    }
1425
1426
    /**
1427
     * WHERE $columnName IS NOT NULL
1428
     *
1429
     * @param  string   $columnName
1430
     * @return $this
1431
     */
1432
    public function whereNotNull($columnName)
1433
    {
1434
        return $this->where("({$columnName} IS NOT NULL)");
1435
    }
1436
1437
    /**
1438
     * @param string $statement
1439
     * @param string $operator
1440
     * @return $this
1441
     */
1442
    public function having($statement, $operator = self::OPERATOR_AND)
1443
    {
1444
        $this->_having[] = [
1445
            'STATEMENT'   => $statement,
1446
            'OPERATOR'    => $operator
1447
        ];
1448
        return $this;
1449
    }
1450
1451
    /**
1452
     * ORDER BY $columnName (ASC | DESC)
1453
     *
1454
     * @param  string   $columnName - The name of the column or an expression
1455
     * @param  string   $ordering   (DESC | ASC)
1456
     * @return $this
1457
     */
1458
    public function orderBy($columnName, $ordering=null)
1459
    {
1460
        Assert($ordering)->nullOr()->inArray(['DESC', 'desc', 'ASC', 'asc']);
1461
        if ( is_null($columnName) )
1462
        {
1463
            $this->_order_by = [];
1464
            return $this;
1465
        }
1466
        $this->_order_by[] = trim("{$columnName} {$ordering}");
1467
        return $this;
1468
    }
1469
1470
    /**
1471
     * GROUP BY $columnName
1472
     *
1473
     * @param  string   $columnName
1474
     * @return $this
1475
     */
1476
    public function groupBy($columnName)
1477
    {
1478
        $columnName = is_array($columnName) ? $columnName : [$columnName];
1479
        foreach ( $columnName as $col )
1480
        {
1481
            $this->_group_by[] = $col;
1482
        }
1483
        return $this;
1484
    }
1485
1486
1487
    /**
1488
     * LIMIT $limit
1489
     *
1490
     * @param  int      $limit
1491
     * @param  int|null $offset
1492
     * @return $this
1493
     */
1494
    public function limit($limit, $offset=null)
1495
    {
1496
        $this->_limit =  (int)$limit;
1497
        if ( ! is_null($offset) )
1498
        {
1499
            $this->offset($offset);
1500
        }
1501
        return $this;
1502
    }
1503
1504
    /**
1505
     * Return the limit
1506
     *
1507
     * @return integer
1508
     */
1509
    public function getLimit()
1510
    {
1511
        return $this->_limit;
1512
    }
1513
1514
    /**
1515
     * OFFSET $offset
1516
     *
1517
     * @param  int      $offset
1518
     * @return $this
1519
     */
1520
    public function offset($offset)
1521
    {
1522
        $this->_offset = (int)$offset;
1523
        return $this;
1524
    }
1525
1526
    /**
1527
     * Return the offset
1528
     *
1529
     * @return integer
1530
     */
1531
    public function getOffset()
1532
    {
1533
        return $this->_offset;
1534
    }
1535
1536
    /**
1537
     * Build a join
1538
     *
1539
     * @param  string    $table         - The table name
1540
     * @param  string   $constraint    -> id = profile.user_id
1541
     * @param  string   $tableAlias   - The alias of the table name
1542
     * @param  string   $joinOperator - LEFT | INNER | etc...
1543
     * @return $this
1544
     */
1545
    public function join($table, $constraint=null, $tableAlias = null, $joinOperator = '')
1546
    {
1547
        if ( is_null($constraint) )
1548
        {
1549
            return $this->autoJoin($table, $joinOperator);
1550
        }
1551
        $join                   = [$joinOperator ? "{$joinOperator} " : ''];
1552
        $join[]                 = "JOIN {$table} ";
1553
        $tableAlias            = is_null($tableAlias) ? Inflector::classify($table) : $tableAlias;
1554
        $join[]                 = $tableAlias ? "AS {$tableAlias} " : '';
1555
        $join[]                 = "ON {$constraint}";
1556
        $this->_join_sources[]  = implode('', $join);
1557
        if ( $tableAlias )
1558
        {
1559
            $this->_join_aliases[]  = $tableAlias;
1560
        }
1561
        return $this;
1562
    }
1563
1564
    /**
1565
     * Create a left join
1566
     *
1567
     * @param  string   $table
1568
     * @param  string   $constraint
1569
     * @param  string   $tableAlias
1570
     * @return $this
1571
     */
1572
    public function leftJoin($table, $constraint, $tableAlias=null)
1573
    {
1574
        return $this->join($table, $constraint, $tableAlias, self::LEFT_JOIN);
1575
    }
1576
1577
1578
    /**
1579
     * Return the build select query
1580
     *
1581
     * @return string
1582
     */
1583
    public function getSelectQuery()
1584
    {
1585
        if ( ! is_null($this->_raw_sql) )
1586
        {
1587
            return $this->_raw_sql;
1588
        }
1589
        if ( empty($this->_select_fields) || ! $this->_explicit_select_mode )
1590
        {
1591
            $this->select('*', null, false);
1592
        }
1593
        foreach ( $this->_select_fields as $idx => $cols )
1594
        {
1595
            if ( strpos(trim(strtolower($cols)), 'distinct ') === 0 )
1596
            {
1597
                $this->_distinct = true;
1598
                $this->_select_fields[$idx] = str_ireplace('distinct ', '', $cols);
1599
            }
1600
        }
1601
        if ( $this->_include_count )
1602
        {
1603
            $this->select('COUNT(*) as __cnt');
1604
        }
1605
        $query  = 'SELECT ';
1606
        $query .= $this->_distinct ? 'DISTINCT ' : '';
1607
        $query .= implode(', ', $this->_prepareColumns($this->_select_fields));
1608
        $query .= " FROM {$this->_table_name}" . ( $this->_table_alias ? " {$this->_table_alias}" : '' );
1609
        if ( count($this->_join_sources ) )
1610
        {
1611
            $query .= (' ').implode(' ',$this->_join_sources);
1612
        }
1613
        $query .= $this->_getWhereString(); // WHERE
1614
        if ( count($this->_group_by) )
1615
        {
1616
            $query .= ' GROUP BY ' . implode(', ', array_unique($this->_group_by));
1617
        }
1618
        if ( count($this->_order_by ) )
1619
        {
1620
            $query .= ' ORDER BY ' . implode(', ', array_unique($this->_order_by));
1621
        }
1622
        $query .= $this->_getHavingString(); // HAVING
1623
        return $this->_connection->setLimit($query, $this->_limit, $this->_offset);
1624
    }
1625
1626
    /**
1627
     * Prepare columns to include the table alias name
1628
     * @param array $columns
1629
     * @return array
1630
     */
1631
    protected function _prepareColumns(array $columns)
1632
    {
1633
        if ( ! $this->_table_alias )
1634
        {
1635
            return $columns;
1636
        }
1637
        $newColumns = [];
1638
        foreach ($columns as $column)
1639
        {
1640
            if ( strpos($column, ',') && ! preg_match('/^[a-zA-Z]{2,200}\(.{1,500}\)/', trim($column)) )
1641
            {
1642
                $newColumns = array_merge($this->_prepareColumns(explode(',', $column)), $newColumns);
1643
            }
1644
            elseif ( preg_match('/^(AVG|SUM|MAX|MIN|COUNT|CONCAT)/', $column) )
1645
            {
1646
                $newColumns[] = trim($column);
1647
            }
1648
            elseif (strpos($column, '.') === false && strpos(strtoupper($column), 'NULL') === false)
1649
            {
1650
                $column         = trim($column);
1651
                $newColumns[]   = preg_match('/^[0-9]/', $column) ? trim($column) : "{$this->_table_alias}.{$column}";
1652
            }
1653
            else
1654
            {
1655
                $newColumns[] = trim($column);
1656
            }
1657
        }
1658
        return $newColumns;
1659
    }
1660
1661
    /**
1662
     * Build the WHERE clause(s)
1663
     *
1664
     * @param bool $purgeAliases
1665
     * @return string
1666
     */
1667
    protected function _getWhereString($purgeAliases=false)
1668
    {
1669
        // If there are no WHERE clauses, return empty string
1670
        if ( empty($this->_where_conditions) )
1671
        {
1672
            return '';
1673
        }
1674
        $where_condition = '';
1675
        $last_condition = '';
1676
        foreach ( $this->_where_conditions as $condition )
1677
        {
1678
            if ( is_array($condition) )
1679
            {
1680
                if ( $where_condition && $last_condition != '(' && !preg_match('/\)\s+(OR|AND)\s+$/i', $where_condition))
1681
                {
1682
                    $where_condition .= $condition['OPERATOR'];
1683
                }
1684
                if ( $purgeAliases && ! empty($condition['STATEMENT']) && strpos($condition['STATEMENT'], '.') !== false && ! empty($this->_table_alias) )
1685
                {
1686
                    $condition['STATEMENT'] = preg_replace("/{$this->_table_alias}\./", '', $condition['STATEMENT']);
1687
                }
1688
                $where_condition .= $condition['STATEMENT'];
1689
                $this->_where_parameters = array_merge($this->_where_parameters, $condition['PARAMS']);
1690
            }
1691
            else
1692
            {
1693
                $where_condition .= $condition;
1694
            }
1695
            $last_condition = $condition;
1696
        }
1697
        return " WHERE {$where_condition}" ;
1698
    }
1699
1700
    /**
1701
     * Return the HAVING clause
1702
     *
1703
     * @return string
1704
     */
1705
    protected function _getHavingString()
1706
    {
1707
        // If there are no WHERE clauses, return empty string
1708
        if ( empty($this->_having) )
1709
        {
1710
            return '';
1711
        }
1712
        $having_condition = '';
1713
        foreach ( $this->_having as $condition )
1714
        {
1715
            if ( $having_condition && !preg_match('/\)\s+(OR|AND)\s+$/i', $having_condition) )
1716
            {
1717
                $having_condition .= $condition['OPERATOR'];
1718
            }
1719
            $having_condition .= $condition['STATEMENT'];
1720
        }
1721
        return " HAVING {$having_condition}" ;
1722
    }
1723
1724
    /**
1725
     * Return the values to be bound for where
1726
     *
1727
     * @param bool $purgeAliases
1728
     * @return array
1729
     */
1730
    protected function _getWhereParameters($purgeAliases=false)
1731
    {
1732
        unset($purgeAliases);
1733
        return $this->_where_parameters;
1734
    }
1735
1736
    /**
1737
     * Insert new rows
1738
     * $records can be a stdClass or an array of stdClass to add a bulk insert
1739
     * If a single row is inserted, it will return it's row instance
1740
     *
1741
     * @param stdClass|array $records
1742
     * @return int|stdClass|bool
1743
     * @throws Exception
1744
     */
1745
    public function insert($records)
1746
    {
1747
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
1748
        $records = is_array($records) ? $records : [$records];
1749
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1750
1751
        foreach ( $records as $key => $record )
1752
        {
1753
            $records[$key] = $this->beforeSave($records[$key], self::SAVE_INSERT);
1754
            if ( ! empty($this->_errors) )
1755
            {
1756
                return false;
1757
            }
1758
        }
1759
        list($sql, $insert_values) = $this->insertSqlQuery($records);
1760
        $this->execute($sql, $insert_values);
1761
        $rowCount = $this->rowCount();
1762
        if ( $rowCount === 1 )
1763
        {
1764
            $primaryKeyName                 = $this->getPrimaryKeyName();
1765
            $records[0]->$primaryKeyName    = $this->getLastInsertId($primaryKeyName);
1766
        }
1767
        foreach ( $records as $key => $record )
1768
        {
1769
            $records[$key] = $this->afterSave($record, self::SAVE_INSERT);
1770
        }
1771
        $this->destroy();
1772
        // On single element return the object
1773
        return $rowCount === 1 ? $records[0] : $rowCount;
1774
    }
1775
1776
    /**
1777
     * @param null $name
1778
     * @return int
1779
     */
1780
    public function getLastInsertId($name=null)
1781
    {
1782
        return (int)$this->getPdo()->lastInsertId($name);
1783
    }
1784
1785
    /**
1786
     * @param $records
1787
     * @return array
1788
     */
1789
    public function insertSqlQuery($records)
1790
    {
1791
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
1792
        $records = is_array($records) ? $records : [$records];
1793
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1794
1795
        $insert_values  = [];
1796
        $question_marks = [];
1797
        $properties     = [];
1798
        foreach ( $records as $record )
1799
        {
1800
            $properties         = !empty($properties) ? $properties : array_keys(get_object_vars($record));
1801
            $question_marks[]   = '('  . $this->_makePlaceholders(count($properties)) . ')';
1802
            $insert_values      = array_merge($insert_values, array_values((array)$record));
1803
        }
1804
        $properties     = implode(', ', $properties);
1805
        $question_marks = implode(', ', $question_marks);
1806
        $sql            = "INSERT INTO {$this->_table_name} ({$properties}) VALUES {$question_marks}";
1807
        return [$sql, $insert_values];
1808
    }
1809
1810
    /**
1811
     * @param       $data
1812
     * @param array $matchOn
1813
     * @param bool  $returnObj
1814
     * @return bool|int|stdClass
1815
     */
1816
    public function upsert($data, array $matchOn=[], $returnObj=false)
1817
    {
1818
        if ( ! is_array($data) )
1819
        {
1820
            return $this->upsertOne($data, $matchOn, $returnObj);
1821
        }
1822
        Assert($data)
1823
            ->notEmpty("The data passed to insert does not contain any data")
1824
            ->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1825
        $num_success    = 0;
1826
        foreach ( $data as $row )
1827
        {
1828
            $clone = clone $this;
1829
            if ( $clone->upsertOne($row, $matchOn) )
1830
            {
1831
                $num_success++;
1832
            }
1833
            unset($clone->_handlers, $clone); // hhvm mem leak
1834
        }
1835
        return $num_success;
1836
    }
1837
1838
    /**
1839
     * @param stdClass $object
1840
     * @param array    $matchOn
1841
     * @param bool     $returnObj
1842
     * @return bool|int|stdClass
1843
     */
1844
    public function upsertOne(stdClass $object, array $matchOn=[], $returnObj=false)
1845
    {
1846
        $primary_key    = $this->getPrimaryKeyName();
1847
        $matchOn       = empty($matchOn) && property_exists($object, $primary_key) ? [$primary_key] : $matchOn;
1848
        foreach ( $matchOn as $column )
1849
        {
1850
            Assert( ! property_exists($object, $column) && $column !== $primary_key)->false('The match on value for upserts is missing.');
1851
            if ( property_exists($object, $column) )
1852
            {
1853
                if ( is_null($object->$column) )
1854
                {
1855
                    $this->whereNull($column);
1856
                }
1857
                else
1858
                {
1859
                    $this->where($column, $object->$column);
1860
                }
1861
            }
1862
        }
1863
        if ( count($this->_where_conditions) < 1 )
1864
        {
1865
            return $this->insert($object);
1866
        }
1867
        if ( ( $id = (int)$this->fetchField($primary_key) ) )
1868
        {
1869
            if ( property_exists($object, $primary_key) && is_null($object->$primary_key) )
1870
            {
1871
                $object->$primary_key = $id;
1872
            }
1873
            $rows_affected = $this->reset()->wherePk($id)->update($object);
1874
            if ( $rows_affected === false )
1875
            {
1876
                return false;
1877
            }
1878
            return $returnObj ? $this->reset()->fetchOne($id) : $id;
1879
        }
1880
        return $this->insert($object);
1881
    }
1882
1883
    /**
1884
     * @param array      $data
1885
     * @param array      $matchOn
1886
     * @param bool|false $returnObj
1887
     * @return bool|int|stdClass
1888
     */
1889
    public function upsertArr(array $data, array $matchOn=[], $returnObj=false)
1890
    {
1891
        return $this->upsert((object)$data, $matchOn, $returnObj);
1892
    }
1893
1894
    /**
1895
     * Update entries
1896
     * Use the query builder to create the where clause
1897
     *
1898
     * @param stdClass $record
1899
     * @param bool     $updateAll
1900
     * @return bool|int
1901
     * @throws Exception
1902
     */
1903
    public function update(stdClass $record, $updateAll=false)
1904
    {
1905
        Assert($record)
1906
            ->notEmpty("The data passed to update does not contain any data")
1907
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
1908
1909
        if ( empty($this->_where_conditions) && ! $updateAll )
1910
        {
1911
            throw new Exception("You cannot update an entire table without calling update with updateAll=true", 500);
1912
        }
1913
        $record = $this->beforeSave($record, self::SAVE_UPDATE);
1914
        if ( ! empty($this->_errors) )
1915
        {
1916
            return false;
1917
        }
1918
        list($sql, $values) = $this->updateSqlQuery($record);
1919
        $this->execute($sql, $values);
1920
        $this->afterSave($record, self::SAVE_UPDATE);
1921
        $row_count = $this->rowCount();
1922
        $this->destroy();
1923
        return $row_count;
1924
    }
1925
1926
    /**
1927
     * @param array      $record
1928
     * @param bool|false $updateAll
1929
     * @return bool|int
1930
     * @throws Exception
1931
     */
1932
    public function updateArr(array $record, $updateAll=false)
1933
    {
1934
        return $this->update((object)$record, $updateAll);
1935
    }
1936
1937
    /**
1938
     * @param array $record
1939
     * @return bool|int|stdClass
1940
     */
1941
    public function insertArr(array $record)
1942
    {
1943
        return $this->insert((object)$record);
1944
    }
1945
1946
    /**
1947
     * @param int     $field
1948
     * @param mixed   $value
1949
     * @param null $id
1950
     * @param bool|false $updateAll
1951
     * @return bool|int
1952
     * @throws Exception
1953
     */
1954
    public function updateField($field, $value, $id=null, $updateAll=false)
1955
    {
1956
        if ( ! is_null($id) )
1957
        {
1958
            $this->wherePk($id);
1959
        }
1960
        return $this->update((object)[$field => $value], $updateAll);
1961
    }
1962
1963
    /**
1964
     * @param int     $field
1965
     * @param mixed   $value
1966
     * @param null $id
1967
     * @param bool|false $updateAll
1968
     * @return bool|int
1969
     * @throws Exception
1970
     */
1971
    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...
1972
    {
1973
1974
    }
1975
1976
    /**
1977
     * @param stdClass $record
1978
     * @return bool|int
1979
     * @throws Exception
1980
     */
1981
    public function updateChanged(stdClass $record)
1982
    {
1983
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
1984
        {
1985
            if ( is_null($value) )
1986
            {
1987
                $this->whereNotNull($field);
1988
                continue;
1989
            }
1990
            $this->whereCoercedNot($field, $value);
1991
        }
1992
        return $this->update($record);
1993
    }
1994
1995
    /**
1996
     * @param string    $expression
1997
     * @param array     $params
1998
     * @return $this
1999
     */
2000
    public function updateByExpression($expression, array $params)
2001
    {
2002
        $this->_update_raw[] = [$expression, $params];
2003
        return $this;
2004
    }
2005
2006
    /**
2007
     * @param array $data
2008
     * @return int
2009
     * @throws Exception
2010
     */
2011
    public function rawUpdate(array $data=[])
2012
    {
2013
        list($sql, $values) = $this->updateSql($data);
2014
        $this->execute($sql, $values);
2015
        $row_count = $this->rowCount();
2016
        $this->destroy();
2017
        return $row_count;
2018
    }
2019
2020
    /**
2021
     * @param stdClass $record
2022
     * @return array
2023
     */
2024
    public function updateSqlQuery(stdClass $record)
2025
    {
2026
        Assert($record)
2027
            ->notEmpty("The data passed to update does not contain any data")
2028
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2029
2030
        // Make sure we remove the primary key
2031
        $record = (array)$record;
2032
        return $this->updateSql($record);
2033
    }
2034
2035
    /**
2036
     * @param $record
2037
     * @return array
2038
     */
2039
    protected function updateSql(array $record)
2040
    {
2041
        unset($record[$this->getPrimaryKeyName()]);
2042
        // Sqlite needs a null primary key
2043
        //$record[$this->getPrimaryKeyName()] = null;
2044
        $field_list = [];
2045
        foreach ( $record as $key => $value )
2046
        {
2047
            if ( is_numeric($key) )
2048
            {
2049
                $field_list[] = $value;
2050
                unset($record[$key]);
2051
                continue;
2052
            }
2053
            $field_list[] = "{$key} = ?";
2054
        }
2055
        $rawParams  = [];
2056
        foreach ( $this->_update_raw as $rawUpdate )
2057
        {
2058
            $field_list[]   = $rawUpdate[0];
2059
            $rawParams      = array_merge($rawParams, $rawUpdate[1]);
2060
        }
2061
        $field_list     = implode(', ', $field_list);
2062
        $where_str      = $this->_getWhereString();
2063
        $joins          = ! empty($this->_join_sources) ? (' ').implode(' ',$this->_join_sources) : '';
2064
        $alias          = ! empty($this->_table_alias) ? " AS {$this->_table_alias}" : '';
2065
        $sql            = "UPDATE {$this->_table_name}{$alias}{$joins} SET {$field_list}{$where_str}";
2066
        $values         = array_merge(array_values($record), $rawParams, $this->_getWhereParameters());
2067
        return [$sql, $values];
2068
    }
2069
2070
    /**
2071
     * Delete rows
2072
     * Use the query builder to create the where clause
2073
     * @param bool $deleteAll = When there is no where condition, setting to true will delete all
2074
     * @return int - total affected rows
2075
     * @throws Exception
2076
     */
2077
    public function delete($deleteAll=false)
2078
    {
2079
        list($sql, $params) = $this->deleteSqlQuery();
2080
        if ( empty($this->_where_conditions) && ! $deleteAll )
2081
        {
2082
            throw new Exception("You cannot update an entire table without calling update with deleteAll=true");
2083
        }
2084
        $this->execute($sql, $params);
2085
        return $this->rowCount();
2086
    }
2087
2088
    /**
2089
     * @param bool|false $force
2090
     * @return $this
2091
     * @throws Exception
2092
     */
2093
    public function truncate($force=false)
2094
    {
2095
        if ( $force )
2096
        {
2097
            $this->execute('SET FOREIGN_KEY_CHECKS = 0');
2098
        }
2099
        $this->execute("TRUNCATE TABLE {$this->_table_name}");
2100
        if ( $force )
2101
        {
2102
            $this->execute('SET FOREIGN_KEY_CHECKS = 1');
2103
        }
2104
        return $this;
2105
    }
2106
2107
    /**
2108
     * @return array
2109
     */
2110
    public function deleteSqlQuery()
2111
    {
2112
        $query  = "DELETE FROM {$this->_table_name}";
2113
        if ( !empty($this->_where_conditions) )
2114
        {
2115
            $query .= $this->_getWhereString(true);
2116
            return [$query, $this->_getWhereParameters()];
2117
        }
2118
        return [$query, []];
2119
    }
2120
2121
2122
    /**
2123
     * Return the aggregate count of column
2124
     *
2125
     * @param string $column
2126
     * @param int $cacheTtl
2127
     * @return int
2128
     */
2129
    public function count($column='*', $cacheTtl=self::CACHE_NO)
2130
    {
2131
        $this->explicitSelectMode();
2132
        $this->select("COUNT({$column})", 'cnt');
2133
        $result             = $this->fetchOne(null, $cacheTtl);
2134
        return ($result !== false && isset($result->cnt)) ? (float)$result->cnt : 0;
2135
    }
2136
2137
2138
    /**
2139
     * Return the aggregate max count of column
2140
     *
2141
     * @param string $column
2142
     * @param int $cacheTtl
2143
     * @return mixed|null
2144
     */
2145
    public function max($column, $cacheTtl=self::CACHE_NO)
2146
    {
2147
        $this->explicitSelectMode();
2148
        $this->select("MAX({$column})", 'max');
2149
        $result = $this->fetchOne(null, $cacheTtl);
2150
        return ( $result !== false && isset($result->max) ) ? $result->max : null;
2151
    }
2152
2153
2154
    /**
2155
     * Return the aggregate min count of column
2156
     *
2157
     * @param string $column
2158
     * @param int $cacheTtl
2159
     * @return mixed|null
2160
     */
2161
    public function min($column, $cacheTtl=self::CACHE_NO)
2162
    {
2163
        $this->explicitSelectMode();
2164
        $this->select("MIN({$column})", 'min');
2165
        $result = $this->fetchOne(null, $cacheTtl);
2166
        return ( $result !== false && isset($result->min) ) ? $result->min : null;
2167
    }
2168
2169
    /**
2170
     * Return the aggregate sum count of column
2171
     *
2172
     * @param string $column
2173
     * @param int $cacheTtl
2174
     * @return mixed|null
2175
     */
2176
    public function sum($column, $cacheTtl=self::CACHE_NO)
2177
    {
2178
        $this->explicitSelectMode();
2179
        $this->select("SUM({$column})", 'sum');
2180
        $result = $this->fetchOne(null, $cacheTtl);
2181
        return ( $result !== false && isset($result->sum) ) ? $result->sum : null;
2182
    }
2183
2184
    /**
2185
     * Return the aggregate average count of column
2186
     *
2187
     * @param string $column
2188
     * @param int $cacheTtl
2189
     * @return mixed|null
2190
     */
2191
    public function avg($column, $cacheTtl=self::CACHE_NO)
2192
    {
2193
        $this->explicitSelectMode();
2194
        $this->select("AVG({$column})", 'avg');
2195
        $result = $this->fetchOne(null, $cacheTtl);
2196
        return ( $result !== false && isset($result->avg) ) ? $result->avg : null;
2197
    }
2198
2199
    /*******************************************************************************/
2200
// Utilities methods
2201
2202
    /**
2203
     * Reset fields
2204
     *
2205
     * @return $this
2206
     */
2207
    public function reset()
2208
    {
2209
        $this->_where_parameters        = [];
2210
        $this->_select_fields           = [];
2211
        $this->_join_sources            = [];
2212
        $this->_join_aliases            = [];
2213
        $this->_where_conditions        = [];
2214
        $this->_limit                   = null;
2215
        $this->_offset                  = null;
2216
        $this->_order_by                = [];
2217
        $this->_group_by                = [];
2218
        $this->_and_or_operator         = self::OPERATOR_AND;
2219
        $this->_having                  = [];
2220
        $this->_wrap_open               = false;
2221
        $this->_last_wrap_position      = 0;
2222
        $this->_pdo_stmt                = null;
2223
        $this->_distinct                = false;
2224
        $this->_requested_fields        = null;
2225
        $this->_filter_meta             = null;
2226
        $this->_cache_ttl               = -1;
2227
        $this->_timer                   = [];
2228
        $this->_built_query             = null;
2229
        $this->_paging_meta             = [];
2230
        $this->_raw_sql                 = null;
2231
        $this->_explicit_select_mode    = false;
2232
        return $this;
2233
    }
2234
2235
    /**
2236
     * Return a YYYY-MM-DD HH:II:SS date format
2237
     *
2238
     * @param string $datetime - An english textual datetime description
2239
     *          now, yesterday, 3 days ago, +1 week
2240
     *          http://php.net/manual/en/function.strtotime.php
2241
     * @return string YYYY-MM-DD HH:II:SS
2242
     */
2243
    public static function NOW($datetime = 'now')
2244
    {
2245
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
2246
    }
2247
2248
    /**
2249
     * Return a string containing the given number of question marks,
2250
     * separated by commas. Eg '?, ?, ?'
2251
     *
2252
     * @param int - total of placeholder to insert
2253
     * @return string
2254
     */
2255
    protected function _makePlaceholders($number_of_placeholders=1)
2256
    {
2257
        return implode(', ', array_fill(0, $number_of_placeholders, '?'));
2258
    }
2259
2260
    /**
2261
     * Format the table{Primary|Foreign}KeyName
2262
     *
2263
     * @param  string $pattern
2264
     * @param  string $tableName
2265
     * @return string
2266
     */
2267
    protected function _formatKeyName($pattern, $tableName)
2268
    {
2269
        return sprintf($pattern, $tableName);
2270
    }
2271
2272
    /**
2273
     * @param string $query
2274
     * @param array $parameters
2275
     *
2276
     * @return array
2277
     */
2278
    protected function _logQuery($query, array $parameters)
2279
    {
2280
        $query = $this->buildQuery($query, $parameters);
2281
        if ( ! $this->_log_queries )
2282
        {
2283
            return [null, null];
2284
        }
2285
        $ident = substr(str_shuffle(md5($query)), 0, 10);
2286
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
2287
        $this->_timer['start'] = microtime(true);
2288
        return [$query, $ident];
2289
    }
2290
2291
    /**
2292
     * @param $ident
2293
     * @param $builtQuery
2294
     */
2295
    protected function _logSlowQueries($ident, $builtQuery)
2296
    {
2297
        if ( ! $this->_log_queries )
2298
        {
2299
            return ;
2300
        }
2301
        $this->_timer['end']    = microtime(true);
2302
        $seconds_taken          = round($this->_timer['end'] - $this->_timer['start'], 3);
2303
        if ( $seconds_taken > $this->_slow_query_secs )
2304
        {
2305
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$seconds_taken} seconds:\n{$builtQuery}");
2306
        }
2307
    }
2308
2309
    /**
2310
     * @return float
2311
     */
2312
    public function getTimeTaken()
2313
    {
2314
        $seconds_taken = $this->_timer['end'] - $this->_timer['start'];
2315
        return $seconds_taken;
2316
    }
2317
2318
    /**
2319
     * @param $secs
2320
     * @return $this
2321
     */
2322
    public function slowQuerySeconds($secs)
2323
    {
2324
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
2325
        $this->_slow_query_secs = $secs;
2326
        return $this;
2327
    }
2328
2329
2330
    /**
2331
     * @param       $field
2332
     * @param array $values
2333
     * @param null  $placeholder_prefix
2334
     *
2335
     * @return array
2336
     */
2337
    public function getNamedWhereIn($field, array $values, $placeholder_prefix=null)
2338
    {
2339
        Assert($field)->string()->notEmpty();
2340
        Assert($values)->isArray();
2341
        if ( empty($values) )
2342
        {
2343
            return ['', []];
2344
        }
2345
        $placeholder_prefix     = !is_null($placeholder_prefix) ? $placeholder_prefix : strtolower(str_replace('.', '__', $field));
2346
        $params = $placeholders = [];
2347
        $count                  = 1;
2348
        foreach ( $values as $val )
2349
        {
2350
            $name           = "{$placeholder_prefix}_{$count}";
2351
            $params[$name]  = $val;
2352
            $placeholders[] = ":{$name}";
2353
            $count++;
2354
        }
2355
        $placeholders = implode(',', $placeholders);
2356
        return ["AND {$field} IN ({$placeholders})\n", $params];
2357
    }
2358
2359
    /**
2360
     * @param        $field
2361
     * @param string $delimiter
2362
     *
2363
     * @return array
2364
     */
2365
    protected function _getColumnAliasParts($field, $delimiter=':')
2366
    {
2367
        $parts = explode($delimiter, $field);
2368
        if ( count($parts) === 2 )
2369
        {
2370
            return $parts;
2371
        }
2372
        return ['', $field];
2373
    }
2374
2375
    /**
2376
     * @param string $column
2377
     * @param string $term
2378
     * @return $this
2379
     */
2380
    protected function _addWhereClause($column, $term)
2381
    {
2382
        $modifiers = [
2383
            'whereLike'         => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
2384
            'whereNotLike'      => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
2385
            'whereLt'           => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
2386
            'whereLte'          => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
2387
            'whereGt'           => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
2388
            'whereGte'          => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
2389
            'whereBetween'      => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
2390
            'whereNotBetween'  => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
2391
        ];
2392
        foreach ( $modifiers as $func => $regex )
2393
        {
2394
            if ( preg_match($regex, $term, $matches) )
2395
            {
2396
                array_shift($matches);
2397
                switch ($func)
2398
                {
2399
                    case 'whereLike':
2400
2401
                        return $this->whereLike($column, $matches[0]);
2402
2403
                    case 'whereNotLike':
2404
2405
                        return $this->whereNotLike($column, $matches[0]);
2406
2407
                    case 'whereLt':
2408
2409
                        return $this->whereLt($column, $matches[0]);
2410
2411
                    case 'whereLte':
2412
2413
                        return $this->whereLte($column, $matches[0]);
2414
2415
                    case 'whereGt':
2416
2417
                        return $this->whereGt($column, $matches[0]);
2418
2419
                    case 'whereGte':
2420
2421
                        return $this->whereGte($column, $matches[0]);
2422
2423
                    case 'whereBetween':
2424
2425
                        return $this->whereBetween($column, $matches[0], $matches[1]);
2426
2427
                    case 'whereNotBetween':
2428
2429
                        return $this->whereNotBetween($column, $matches[0], $matches[1]);
2430
2431
                }
2432
            }
2433
        }
2434
        return $this->where($column, $term);
2435
    }
2436
2437
    public function destroy()
2438
    {
2439
        if ( !is_null($this->_pdo_stmt) )
2440
        {
2441
            $this->_pdo_stmt->closeCursor();
2442
        }
2443
        $this->_pdo_stmt    = null;
2444
        $this->_handlers    = [];
2445
    }
2446
2447
    public function __destruct()
2448
    {
2449
        $this->destroy();
2450
    }
2451
2452
    /** @var string */
2453
    static protected $_model_namespace = '';
2454
2455
    /** @var bool */
2456
    protected $_validation_exceptions = true;
2457
2458
    /** @var array */
2459
    protected $_paging_meta           = [];
2460
2461
    /** @var int */
2462
    protected $_default_max           = 100;
2463
2464
    /**
2465
     * Load a model
2466
     *
2467
     * @param string $model_name
2468
     * @param AbstractPdo $connection
2469
     * @return Model
2470
     * @throws ModelNotFoundException
2471
     */
2472
    public static function loadModel($model_name, AbstractPdo $connection=null)
2473
    {
2474
        $model_name = static::$_model_namespace . $model_name;
2475
        if ( ! class_exists($model_name) )
2476
        {
2477
            throw new ModelNotFoundException("Failed to find model class {$model_name}.");
2478
        }
2479
        return new $model_name($connection);
2480
    }
2481
2482
    /**
2483
     * Load a model
2484
     *
2485
     * @param string      $table_name
2486
     * @param AbstractPdo $connection
2487
     * @return $this
2488
     */
2489
    public static function loadTable($table_name, AbstractPdo $connection=null)
2490
    {
2491
        $model_name = Inflector::classify($table_name);
2492
        Assert($model_name)->notEmpty("Could not resolve model name from table name.");
2493
        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 2491 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...
2494
    }
2495
2496
    /**
2497
     * @param string   $columnName
2498
     * @param int $cacheTtl
2499
     * @param bool $flushCache
2500
     * @return bool
2501
     */
2502
    public function columnExists($columnName, $cacheTtl=self::CACHE_NO, $flushCache=false)
2503
    {
2504
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
2505
        return array_key_exists($columnName, $columns);
2506
    }
2507
2508
    /**
2509
     * @param int $cacheTtl
2510
     * @param bool $flushCache
2511
     * @return $this
2512
     */
2513
    public function loadSchemaFromDb($cacheTtl=self::CACHE_NO, $flushCache=false)
2514
    {
2515
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
2516
        $this->schema($schema);
2517
        return $this;
2518
    }
2519
2520
    /**
2521
     * @param int $cacheTtl
2522
     * @param bool $flushCache
2523
     * @return array
2524
     * @throws \Terah\Assert\AssertionFailedException
2525
     */
2526
    public function getSchemaFromDb($cacheTtl=self::CACHE_NO, $flushCache=false)
2527
    {
2528
        $table      = $this->getTableName();
2529
        Assert($table)->string()->notEmpty();
2530
        $schema     = [];
2531
        $columns    = $this->_getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
2532
        foreach ( $columns[$table] as $column => $meta )
2533
        {
2534
            $schema[$column] = $meta->data_type;
2535
        }
2536
        return $schema;
2537
    }
2538
2539
    /**
2540
     * @param string $table
2541
     * @param int $cacheTtl
2542
     * @param bool $flushCache
2543
     * @return array
2544
     */
2545
    protected function _getColumnsByTableFromDb($table, $cacheTtl=self::CACHE_NO, $flushCache=false)
2546
    {
2547
        Assert($table)->string()->notEmpty();
2548
2549
        $callback = function() use ($table) {
2550
2551
            return $this->_connection->getColumns(true, $table);
2552
        };
2553
        $cacheKey   = '/column_schema/' . $table;
2554
        if ( $flushCache === true )
2555
        {
2556
            $this->clearCache($cacheKey);
2557
        }
2558
        return $this->_cacheData($cacheKey, $callback, $cacheTtl);
2559
    }
2560
2561
    /**
2562
     * @param string $table
2563
     * @return bool
2564
     */
2565
    public function clearSchemaCache($table)
2566
    {
2567
        return $this->clearCache('/column_schema/' . $table);
2568
    }
2569
2570
    /**
2571
     * @param stdClass $record
2572
     * @return stdClass
2573
     */
2574
    public function onFetch(stdClass $record)
2575
    {
2576
        $record = $this->_trimAndLowerCaseKeys($record);
2577
        if ( $this->_filter_on_fetch )
2578
        {
2579
            $record = $this->cleanseRecord($record);
2580
        }
2581
        return $this->fixTypes($record);
2582
    }
2583
2584
    /**
2585
     * @param stdClass $record
2586
     * @return stdClass
2587
     */
2588
    public function cleanseRecord(stdClass $record)
2589
    {
2590
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2591
        {
2592
            if ( is_string($record->$field) )
2593
            {
2594
                $record->$field = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", filter_var($record->$field, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES));
2595
                if ( $this->_log_filter_changes && $value !== $record->$field )
2596
                {
2597
                    $table = $this->_table_name ? $this->_table_name : '';
2598
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->$field]);
2599
                }
2600
            }
2601
        }
2602
        return $record;
2603
    }
2604
2605
    /**
2606
     * @param stdClass $record
2607
     * @param string   $type
2608
     * @return stdClass
2609
     */
2610
    public function beforeSave(stdClass $record, $type)
2611
    {
2612
        $record = $this->addDefaultFields($record, $type);
2613
        $record = $this->applyGlobalModifiers($record, $type);
2614
        $record = $this->applyHandlers($record, $type);
2615
        $record = $this->removeUnneededFields($record, $type);
2616
        return $record;
2617
    }
2618
2619
    /**
2620
     * @param stdClass $record
2621
     * @param string   $type
2622
     * @return stdClass
2623
     */
2624
    public function afterSave(stdClass $record, $type)
2625
    {
2626
        unset($type);
2627
        $this->clearCacheByTable();
2628
        return $record;
2629
    }
2630
2631
    /**
2632
     * @param stdClass $record
2633
     * @param string   $type
2634
     * @return stdClass
2635
     */
2636
    public function addDefaultFields(stdClass $record, $type)
2637
    {
2638
        unset($type);
2639
        return $record;
2640
    }
2641
2642
    public function applyGlobalModifiers(stdClass $record, $type)
2643
    {
2644
        unset($type);
2645
        return $record;
2646
    }
2647
2648
    public function removeUnneededFields(\stdClass $record, $type)
2649
    {
2650
        unset($type);
2651
        // remove un-needed fields
2652
        $columns = $this->getColumns(true);
2653
        if ( empty($columns) )
2654
        {
2655
            return $record;
2656
        }
2657
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2658
        {
2659
            if ( ! in_array($name, $columns) || in_array($name, $this->_virtual_fields) )
2660
            {
2661
                unset($record->$name);
2662
            }
2663
        }
2664
        return $record;
2665
    }
2666
2667
    /**
2668
     * @param array $data
2669
     * @param string $saveType
2670
     * @return array
2671
     */
2672
    public function cleanseWebData($data, $saveType)
2673
    {
2674
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
2675
        $columns = $this->getColumns(false);
2676
        if ( empty($columns) )
2677
        {
2678
            return $data;
2679
        }
2680
        foreach ( $data as $field => $val )
2681
        {
2682
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
2683
        }
2684
        return array_intersect_key($data, $columns);
2685
    }
2686
2687
    /**
2688
     * @return array
2689
     */
2690
    public function skeleton()
2691
    {
2692
        $skel       = [];
2693
        $columns    = $this->columns(false);
2694
        foreach ( $columns as $column => $type )
2695
        {
2696
            $skel[$column] = null;
2697
        }
2698
        return $skel;
2699
    }
2700
2701
2702
    /**
2703
     * @return Closure[]
2704
     */
2705
    protected function _getFieldHandlers()
2706
    {
2707
        return [];
2708
    }
2709
2710
    /**
2711
     * @param bool $toString
2712
     * @return array|string
2713
     */
2714
    public function getErrors($toString=false)
2715
    {
2716
        if ( $toString )
2717
        {
2718
            $errors = [];
2719
            foreach ( $this->_errors as $field => $error )
2720
            {
2721
                $errors[] = implode("\n", $error);
2722
            }
2723
            return implode("\n", $errors);
2724
        }
2725
        return $this->_errors;
2726
    }
2727
2728
    /**
2729
     * @param bool $throw
2730
     * @return $this
2731
     */
2732
    public function validationExceptions($throw=true)
2733
    {
2734
        $this->_validation_exceptions = $throw;
2735
        return $this;
2736
    }
2737
2738
    /**
2739
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
2740
     *
2741
     * @return $this
2742
     * @throws Exception
2743
     */
2744
    public function paginate(array $query=[])
2745
    {
2746
        $_fields = $_order = $_limit = $_offset = null;
2747
        extract($query);
2748
        $this->_setLimit($_limit, $_offset);
2749
        $this->_setOrderBy($_order);
2750
        $this->_setFields($_fields);
2751
        return $this;
2752
    }
2753
2754
    /**
2755
     * @param null|string $limit
2756
     * @param null|string|int $offset
2757
     * @return $this
2758
     */
2759
    protected function _setLimit($limit=null, $offset=null)
2760
    {
2761
        $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...
2762
        if ( ! is_numeric($limit) )
2763
        {
2764
            return $this;
2765
        }
2766
        $this->limit((int)$limit);
2767
        if ( $offset && is_numeric($offset) )
2768
        {
2769
            $this->offset((int)$offset);
2770
        }
2771
        return $this;
2772
    }
2773
2774
    /**
2775
     * @param null|string|array $fields
2776
     * @return $this
2777
     * @throws Exception
2778
     */
2779
    protected function _setFields($fields=null)
2780
    {
2781
        if ( ! $fields )
2782
        {
2783
            return $this;
2784
        }
2785
        $this->explicitSelectMode();
2786
        $columns    = $this->getColumns();
2787
        $fields     = is_array($fields) ? $fields : explode('|', $fields);
2788
2789
        foreach ( $fields as $idx => $field )
2790
        {
2791
            list($alias, $field) = $this->_getColumnAliasParts($field);
2792
            $field = $field === '_display_field' ? $this->_display_column : $field;
2793
            // Regular primary table field
2794
            if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
2795
            {
2796
                $this->select("{$this->_table_alias}.{$field}");
2797
                $this->_requested_fields[] = "{$this->_table_alias}.{$field}";
2798
                continue;
2799
            }
2800
            // Reference table field with alias
2801
            if ( ! empty($alias) )
2802
            {
2803
                Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
2804
                Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
2805
                list(, , $join_field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
2806
                $this->autoJoin($alias, static::LEFT_JOIN, false);
2807
                $this->select($join_field, $fieldAlias);
2808
                $this->_requested_fields[] = "{$join_field} AS {$fieldAlias}";
2809
                continue;
2810
            }
2811
            // Reference table select field without alias
2812
            foreach ( $this->_associations['belongsTo'] as $joinAlias => $config )
2813
            {
2814
                list(, , $join_field, $fieldAlias) = $config;
2815
                if ( $field === $config[3] )
2816
                {
2817
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
2818
                    $this->select($join_field, $fieldAlias);
2819
                    $this->_requested_fields[] = "{$join_field} AS {$fieldAlias}";
2820
                    continue;
2821
                }
2822
            }
2823
        }
2824
        return $this;
2825
    }
2826
2827
    /**
2828
     * @param null|string $orderBy
2829
     * @return $this|FluentPdoModel
2830
     */
2831
    protected function _setOrderBy($orderBy=null)
2832
    {
2833
        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...
2834
        {
2835
            return $this;
2836
        }
2837
        $columns                    = $this->getColumns();
2838
        list($order, $direction)    = strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
2839
        list($alias, $field)        = $this->_getColumnAliasParts(trim($order), '.');
2840
        $field                      = explode(' ', $field);
2841
        $field                      = trim($field[0]);
2842
        $direction                  = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
2843
2844
        // Regular primary table order by
2845
        if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
2846
        {
2847
            return $this->orderBy("{$this->_table_alias}.{$field}", $direction);
2848
        }
2849
        // Reference table order by with alias
2850
        if ( ! empty($alias) )
2851
        {
2852
            Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
2853
            Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
2854
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
2855
        }
2856
        // Reference table order by without alias
2857
        foreach ( $this->_associations['belongsTo'] as $joinAlias => $config )
2858
        {
2859
            if ( $field === $config[3] )
2860
            {
2861
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
2862
            }
2863
        }
2864
        return $this;
2865
    }
2866
2867
    /**
2868
     * @param null $type
2869
     * @return array
2870
     */
2871
    public function getPagingMeta($type=null)
2872
    {
2873
        if ( empty($this->_paging_meta) )
2874
        {
2875
            $this->setPagingMeta();
2876
        }
2877
        return is_null($type) ? $this->_paging_meta : $this->_paging_meta[$type];
2878
    }
2879
2880
    /**
2881
     * @return $this
2882
     */
2883
    public function setPagingMeta()
2884
    {
2885
        $model      = clone $this;
2886
        $limit      = intval($this->getLimit());
2887
        $offset     = intval($this->getOffset());
2888
        $total      = intval($model->withBelongsTo()->select(null)->offset(null)->limit(null)->orderBy(null)->count());
2889
        unset($model->_handlers, $model); //hhmv mem leak
2890
        $order_bys  = !is_array($this->_order_by) ? [] : $this->_order_by;
2891
        $this->_paging_meta  = [
2892
            'limit'     => $limit,
2893
            'offset'    => $offset,
2894
            'page'      => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
2895
            'pages'     => $limit === 0 ? 1 : intval(ceil($total / $limit)),
2896
            'order'     => $order_bys,
2897
            'total'     => $total = $limit === 1 && $total > 1 ? 1 : $total,
2898
            'filters'   => is_null($this->_filter_meta) ? [] : $this->_filter_meta,
2899
            'fields'    => is_null($this->_requested_fields) ? [] : $this->_requested_fields,
2900
            'perms'     => [],
2901
        ];
2902
        return $this;
2903
    }
2904
2905
    /**
2906
     * Take a web request and format a query
2907
     *
2908
     * @param array $query
2909
     *
2910
     * @return $this
2911
     * @throws Exception
2912
     */
2913
    public function filter(array $query=[])
2914
    {
2915
        $columns   = $this->getColumns(false);
2916
        $alias     = '';
2917
        foreach ( $query as $column => $value )
2918
        {
2919
            if ( in_array($column, $this->_pagination_attribs) )
2920
            {
2921
                continue;
2922
            }
2923
            $field = $this->_findFieldByQuery($column, $this->_display_column);
2924
            if ( is_null($field) )
2925
            {
2926
                continue;
2927
            }
2928
            $this->_filter_meta[$field]     = $value;
2929
            $where                          = !is_array($value) && mb_stripos($value, '|') !== false ? explode('|', $value) : $value;
2930
            if ( is_array($where) )
2931
            {
2932
                $this->whereIn($field, $where);
2933
            }
2934
            else
2935
            {
2936
                $this->_addWhereClause($field, $where);
2937
            }
2938
        }
2939
        if ( empty($query['_search']) )
2940
        {
2941
            return $this;
2942
        }
2943
        $alias          = !empty($alias) ? $alias : $this->_table_alias;
2944
        $string_cols    = array_filter($columns, function($type) {
2945
2946
            return in_array($type, ['varchar', 'text', 'enum']);
2947
        });
2948
        $terms          = explode('|', $query['_search']);
2949
        $where_likes    = [];
2950
        foreach ( $string_cols as $column => $type )
2951
        {
2952
            foreach ( $terms as $term )
2953
            {
2954
                $where_likes["{$alias}.{$column}"] = "%{$term}%";
2955
            }
2956
        }
2957
        // Reference fields...
2958
        $belongsTo = $this->getSearchableAssociations();
2959
        foreach ( $belongsTo as $alias => $config )
2960
        {
2961
            foreach ( $terms as $term )
2962
            {
2963
                $where_likes[$config[2]] = "%{$term}%";
2964
            }
2965
        }
2966
        if ( empty($where_likes) )
2967
        {
2968
            return $this;
2969
        }
2970
        $this->where([1=>1])->wrap()->_and();
2971
        foreach ( $where_likes as $column => $term )
2972
        {
2973
            $this->_or()->whereLike($column, $term);
2974
        }
2975
        $this->wrap();
2976
        return $this;
2977
    }
2978
2979
    /**
2980
     * @param string $column
2981
     * @param $displayCol
2982
     * @return string|null
2983
     */
2984
    protected function _findFieldByQuery($column, $displayCol)
2985
    {
2986
        list($alias, $field)    = $this->_getColumnAliasParts($column);
2987
        $field                  = $field === '_display_field' ? $displayCol : $field;
2988
        $columns                = $this->getColumns();
2989
        $tableAlias             = $this->getTableAlias();
2990
        if ( ! empty($alias) && $alias === $tableAlias )
2991
        {
2992
            // Alias is set but the field isn't correct
2993
            if ( ! in_array($field, $columns) )
2994
            {
2995
                return null;
2996
            }
2997
            return "{$alias}.{$field}";
2998
        }
2999
        // Alias isn't passed in but the field is ok
3000
        if ( empty($alias) && in_array($field, $columns) )
3001
        {
3002
            return "{$tableAlias}.{$field}";
3003
        }
3004
        // Alias is passed but not this table in but there is a matching field on this table
3005
        if ( ! empty($alias) && in_array($field, $columns) )
3006
        {
3007
            return null;
3008
        }
3009
        // Now search the associations for the field
3010
        $associations = $this->getSearchableAssociations();
3011
        if ( ! empty($alias) )
3012
        {
3013
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
3014
            {
3015
                return "{$alias}.{$field}";
3016
            }
3017
            return null;
3018
        }
3019
        foreach ( $associations as $assocAlias => $config )
3020
        {
3021
            list(, , $assocField, $fieldAlias) = $config;
3022
            if ( $fieldAlias === $field )
3023
            {
3024
                return $assocField;
3025
            }
3026
        }
3027
        return null;
3028
    }
3029
3030
    public function getSearchableAssociations()
3031
    {
3032
        $belongsTo = ! empty($this->_associations['belongsTo']) ? $this->_associations['belongsTo'] : [];
3033
        return $belongsTo;
3034
    }
3035
3036
    /**
3037
     * @param $keys_only
3038
     * @return array
3039
     */
3040
3041
    public function columns($keys_only=true)
3042
    {
3043
        return $keys_only ? array_keys($this->_schema) : $this->_schema;
3044
    }
3045
3046
    /**
3047
     * @param string $field
3048
     * @param mixed $value
3049
     * @param bool|false $permissive
3050
     * @return float|int|null|string
3051
     */
3052
    protected function _fixType($field, $value, $permissive=false)
3053
    {
3054
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
3055
        if ( empty($this->_schema) || ( ! array_key_exists($field, $this->_schema) && $permissive ) )
3056
        {
3057
            return $value;
3058
        }
3059
        $columns    = $this->getColumns(false);
3060
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
3061
3062
        $field_type = ! empty($columns[$field]) ? $columns[$field] : null;
3063
3064
        if ( is_null($value) )
3065
        {
3066
            return null;
3067
        }
3068
        // return on null, '' but not 0
3069
        if ( ! is_numeric($value) && empty($value) )
3070
        {
3071
            return null;
3072
        }
3073
        // Don't cast invalid values... only those that can be cast cleanly
3074
        switch ($field_type)
3075
        {
3076
            case 'varchar':
3077
            case 'text';
3078
            case 'date':
3079
            case 'datetime':
3080
            case 'timestamp':
3081
                return (string)$value;
3082
            case 'int':
3083
            case 'tinyint':
3084
                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...
3085
            case 'decimal':
3086
                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...
3087
            default:
3088
                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...
3089
        }
3090
    }
3091
3092
    /**
3093
     * @param stdClass $record
3094
     * @param string $type
3095
     * @return stdClass
3096
     */
3097
    public function fixTypes(stdClass $record, $type=null)
3098
    {
3099
        foreach ( $this->getColumns(false) as $column => $field_type )
3100
        {
3101
            if ( ! property_exists($record, $column) || is_null($record->$column) )
3102
            {
3103
                continue;
3104
            }
3105
            $record->$column = $this->_fixType($column, $record->$column);
3106
        }
3107
        unset($type);
3108
        return $record;
3109
    }
3110
3111
    /**
3112
     * @param stdClass $record
3113
     * @param string   $type
3114
     * @return stdClass
3115
     * @throws Exception
3116
     */
3117
    public function applyHandlers(stdClass $record, $type='INSERT')
3118
    {
3119
        $this->_compileHandlers();
3120
        $this->_errors                  = [];
3121
        // Disable per field exceptions so we can capture all errors for the record
3122
        $tmpExceptions                  = $this->_validation_exceptions;
3123
        $this->_validation_exceptions   = false;
3124
        foreach ( $this->_handlers as $field => $fn_validator )
3125
        {
3126
            if ( ! property_exists($record, $field) )
3127
            {
3128
                // If the operation is an update it can be a partial update
3129
                if ( $type === self::SAVE_UPDATE )
3130
                {
3131
                    continue;
3132
                }
3133
                $record->$field = null;
3134
            }
3135
            $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...
3136
        }
3137
        $this->_validation_exceptions = $tmpExceptions;
3138
        if ( $this->_validation_exceptions && ! empty($this->_errors) )
3139
        {
3140
            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...
3141
        }
3142
        return $record;
3143
    }
3144
3145
    /**
3146
     * @param string $field
3147
     * @param mixed $value
3148
     * @param null $type
3149
     * @param null $record
3150
     * @return null
3151
     * @throws Exception
3152
     */
3153
    protected function applyHandler($field, $value, $type=null, $record=null)
3154
    {
3155
        $this->_compileHandlers();
3156
        $fnHandler = ! empty($this->_handlers[$field]) ? $this->_handlers[$field] : null;
3157
        if ( is_callable($fnHandler) )
3158
        {
3159
            try
3160
            {
3161
                $value = $fnHandler($field, $value, $type, $record);
3162
            }
3163
            catch( Exception $e )
3164
            {
3165
                $this->_errors[$field][] = $e->getMessage();
3166
                if ( $this->_validation_exceptions && ! empty($this->_errors) )
3167
                {
3168
                    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...
3169
                }
3170
                return null;
3171
            }
3172
        }
3173
        return $value;
3174
    }
3175
3176
    protected function _compileHandlers()
3177
    {
3178
        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...
3179
        {
3180
            return;
3181
        }
3182
        $this->_handlers    = array_merge([], $this->_getFieldHandlers());
3183
    }
3184
}
3185
3186
class ModelNotFoundException extends \Exception{}
3187
3188
class ModelFailedValidationException extends \Exception
3189
{
3190
    private $validationErrors;
3191
3192
    /**
3193
     * @return array
3194
     */
3195
    public function getValidationErrors($asString=false)
3196
    {
3197
        $errors = is_array($this->validationErrors) ? $this->validationErrors : [];
3198
        return $asString ? json_encode($errors, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) : $errors;
3199
    }
3200
3201
    /**
3202
     * @param string         $message
3203
     * @param array          $validationErrors
3204
     * @param int            $code
3205
     * @param Exception|null $previous
3206
     */
3207
    public function __construct($message="", array $validationErrors=[], $code=0, Exception $previous = null)
3208
    {
3209
        $this->validationErrors = $validationErrors;
3210
        parent::__construct($message, $code, $previous);
3211
    }
3212
}
3213