Completed
Push — master ( 1e0c3d...ec22e7 )
by Terry
04:39
created

FluentPdoModel::wherePk()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
eloc 3
nc 4
nop 2
dl 0
loc 5
rs 9.4285
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 \stdClass[]|null
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 )
0 ignored issues
show
Bug introduced by
The expression $list of type array<integer,object<stdClass>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

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