Completed
Push — master ( f7f9d7...9d7b5e )
by Terry
04:34
created

FluentPdoModel::whereLt()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
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\LoggerInterface;
25
use Terah\RedisCache\CacheInterface;
26
use function Terah\Assert\Assert;
27
use function Terah\Assert\Validate;
28
/**
29
 * Class FluentPdoModel
30
 *
31
 * @package Terah\FluentPdoModel
32
 * @author  Terry Cullen - [email protected]
33
 */
34
class FluentPdoModel
35
{
36
    const ACTIVE                        = 1;
37
    const INACTIVE                      = 0;
38
//    const ARCHIVED                      = -1;
39
    const GZIP_PREFIX                   = 'gzipped|';
40
    const OPERATOR_AND              = ' AND ';
41
    const OPERATOR_OR               = ' OR ';
42
    const ORDERBY_ASC               = 'ASC';
43
    const ORDERBY_DESC              = 'DESC';
44
    const SAVE_INSERT               = 'INSERT';
45
    const SAVE_UPDATE               = 'UPDATE';
46
    const LEFT_JOIN                 = 'LEFT';
47
    const INNER_JOIN                = 'INNER';
48
    const ONE_DAY                   = 86400;
49
    const ONE_WEEK                  = 60480060;
50
    const ONE_HOUR                  = 3600;
51
    const TEN_MINS                  = 600;
52
    const CACHE_NO                  = -1;
53
    const CACHE_DEFAULT             = 0;
54
55
    const CREATOR_ID_FIELD          = 'creator_id';
56
    const CREATOR_FIELD             = 'creator';
57
    const CREATED_TS_FIELD          = 'created_ts';
58
    const MODIFIER_ID_FIELD         = 'modifier_id';
59
    const MODIFIER_FIELD            = 'modifier';
60
    const MODIFIED_TS_FIELD         = 'modified_ts';
61
    const DELETER_ID_FIELD          = 'deleter_id';
62
    const DELETER_FIELD             = 'deleter';
63
    const DELETED_TS_FIELD          = 'deleted_ts';
64
    const STATUS_FIELD              = 'active';
65
66
    /** @var AbstractPdo $_connection */
67
    protected $_connection          = null;
68
69
    /** @var string */
70
    protected $_primary_key         = 'id';
71
72
    /** @var array */
73
    protected $_where_parameters    = [];
74
75
    /** @var array */
76
    protected $_select_fields       = [];
77
78
    /** @var array */
79
    protected $_join_sources        = [];
80
81
    /** @var array */
82
    protected $_join_aliases        = [];
83
84
    /** @var array $_associations */
85
    protected $_associations        = [
86
        'belongsTo' => [],
87
    ];
88
89
    /** @var array */
90
    protected $_where_conditions    = [];
91
92
    protected $_raw_sql             = '';
93
94
    /** @var int */
95
    protected $_limit               = 0;
96
97
    /** @var int */
98
    protected $_offset              = 0;
99
100
    /** @var array */
101
    protected $_order_by            = [];
102
103
    /** @var array */
104
    protected $_group_by            = [];
105
106
    /** @var string */
107
    protected $_and_or_operator     = self::OPERATOR_AND;
108
109
    /** @var array */
110
    protected $_having              = [];
111
112
    /** @var bool */
113
    protected $_wrap_open           = false;
114
115
    /** @var int */
116
    protected $_last_wrap_position  = 0;
117
118
    /** @var PDOStatement $_pdo_stmt */
119
    protected $_pdo_stmt            = null;
120
121
    /** @var bool */
122
    protected $_distinct            = false;
123
124
    /** @var null */
125
    protected $_requested_fields    = [];
126
127
    /** @var null */
128
    protected $_filter_meta         = [];
129
130
    /** @var bool */
131
    protected $_log_queries         = false;
132
133
    /** @var array */
134
    protected $_timer               = [];
135
136
    /** @var int */
137
    protected $_slow_query_secs     = 5;
138
139
    /** @var array */
140
    protected $_pagination_attribs  = [
141
        '_limit',
142
        '_offset',
143
        '_order',
144
        '_fields',
145
        '_search'
146
    ];
147
148
    /** @var string $_table_name */
149
    protected $_table_name          = '';
150
151
    /** @var string $_table_alias */
152
    protected $_table_alias         = '';
153
154
    /** @var string $_display_column */
155
    protected $_display_column      = '';
156
157
    /** @var string $_connection_name */
158
    protected $_connection_name     = '';
159
160
    /** @var array $_schema */
161
    protected $_schema              = [];
162
163
    /** @var array $_virtual_fields */
164
    protected $_virtual_fields      = [];
165
166
    /** @var array $_errors */
167
    protected $_errors              = [];
168
169
    /**
170
     * @var int - true  = connection default x days
171
     *          - false = no cache
172
     *          - int   = a specific amount
173
     */
174
    protected $_cache_ttl               = self::CACHE_NO;
175
176
    /** @var array */
177
    protected $_flush_cache_tables      = [];
178
179
    /** @var string */
180
    protected $_tmp_table_prefix        = 'tmp_';
181
182
    /** @var null|string */
183
    protected $_built_query             = '';
184
185
    /** @var array  */
186
    protected $_handlers                = [];
187
188
    /** @var bool User want to directly specify the fields */
189
    protected $_explicit_select_mode    = false;
190
191
    /** @var string[] */
192
    protected $_update_raw              = [];
193
194
    protected $_max_callback_failures   = -1;
195
196
    protected $_num_callback_failures   = 0;
197
198
    protected $_filter_on_fetch         = false;
199
200
    protected $_log_filter_changes      = true;
201
202
    protected $_include_count           = false;
203
204
    /** @var string */
205
    static protected $_model_namespace  = '';
206
207
    /** @var bool */
208
    protected $_validation_exceptions   = true;
209
210
    /** @var array */
211
    protected $_paging_meta             = [];
212
213
    /** @var bool */
214
    protected $_soft_deletes            = true;
215
216
217
    /** @var bool  */
218
    protected $_allow_meta_override     = false;
219
220
    /** @var bool  */
221
    protected $_skip_meta_updates       = false;
222
223
    /** @var  bool */
224
    protected $_add_update_alias        = false;
225
226
    /** @var int  */
227
    protected $_default_max             = 250;
228
229
    /** @var array  */
230
    protected $removeUnauthorisedFields = [];
231
232
    protected $_can_generic_update      = true;
233
    protected $_can_generic_add         = true;
234
    protected $_can_generic_delete      = true;
235
236
    /** @var  array */
237
    protected $row_meta_data            = [];
238
239
    protected $excluded_search_cols     = [];
240
241
242
    /** @var array  */
243
    protected $globalRemoveUnauthorisedFields = [
244
        '/global_table_meta#view'               => [
245
            self::CREATOR_ID_FIELD,
246
            self::CREATOR_FIELD,
247
            self::CREATED_TS_FIELD,
248
            self::MODIFIER_ID_FIELD,
249
            self::MODIFIER_FIELD,
250
            self::MODIFIED_TS_FIELD,
251
            self::STATUS_FIELD,
252
        ],
253
    ];
254
255
256
    /**
257
     * @param AbstractPdo|null $connection
258
     */
259
    public function __construct(AbstractPdo $connection=null)
260
    {
261
        $connection         = $connection ?: ConnectionPool::get($this->_connection_name);
262
        $this->_connection  = $connection;
263
        $this->_log_queries = $connection->logQueries();
264
        $this->init();
265
    }
266
267
    public function init()
268
    {}
269
270
    /**
271
     * @return AbstractPdo
272
     * @throws Exception
273
     */
274
    public function getPdo() : AbstractPdo
275
    {
276
        return $this->_connection;
277
    }
278
279
    /**
280
     * @return LoggerInterface
281
     */
282
    public function getLogger() : LoggerInterface
283
    {
284
        return $this->_connection->getLogger();
285
    }
286
287
    /**
288
     * @return CacheInterface
289
     */
290
    public function getCache() : CacheInterface
291
    {
292
        return $this->_connection->getCache();
293
    }
294
295
    /**
296
     * Define the working table and create a new instance
297
     *
298
     * @param string $tableName - Table name
299
     * @param string $alias     - The table alias name
300
     * @param string $displayColumn
301
     * @param string $primaryKeyName
302
     *
303
     * @return FluentPdoModel|$this
304
     */
305
    public function table(string $tableName, string $alias='', string $displayColumn='', string $primaryKeyName='id') : FluentPdoModel
306
    {
307
        return $this->reset()
308
            ->tableName($tableName)
309
            ->tableAlias($alias)
310
            ->displayColumn($displayColumn)
311
            ->primaryKeyName($primaryKeyName);
312
    }
313
314
    /**
315
     * @param string $primaryKeyName
316
     * @return FluentPdoModel|$this
317
     */
318
    public function primaryKeyName(string $primaryKeyName) : FluentPdoModel
319
    {
320
        $this->_primary_key = $primaryKeyName;
321
322
        return $this;
323
    }
324
325
    /**
326
     * @param string $tableName
327
     *
328
     * @return FluentPdoModel|$this
329
     */
330
    public function tableName(string $tableName) : FluentPdoModel
331
    {
332
        $this->_table_name  = $tableName;
333
334
        return $this;
335
    }
336
337
    /**
338
     * @param $explicitSelect
339
     *
340
     * @return FluentPdoModel|$this
341
     */
342
    public function explicitSelectMode(bool $explicitSelect=true) : FluentPdoModel
343
    {
344
        $this->_explicit_select_mode  = (bool)$explicitSelect;
345
346
        return $this;
347
    }
348
349
    /**
350
     * @param bool $filterOnFetch
351
     *
352
     * @return FluentPdoModel|$this
353
     */
354
    public function filterOnFetch(bool $filterOnFetch=true) : FluentPdoModel
355
    {
356
        $this->_filter_on_fetch  = (bool)$filterOnFetch;
357
358
        return $this;
359
    }
360
361
    /**
362
     * @param bool $logFilterChanges
363
     *
364
     * @return FluentPdoModel|$this
365
     */
366
    public function logFilterChanges(bool $logFilterChanges=true) : FluentPdoModel
367
    {
368
        $this->_log_filter_changes  = (bool)$logFilterChanges;
369
370
        return $this;
371
    }
372
373
    /**
374
     * Return the name of the table
375
     *
376
     * @return string
377
     */
378
    public function getTableName() : string
379
    {
380
        return $this->_table_name;
381
    }
382
383
    /**
384
     * @return string
385
     */
386
    public function getDisplayColumn() : string
387
    {
388
        return $this->_display_column;
389
    }
390
391
    /**
392
     * Set the display column
393
     *
394
     * @param string $column
395
     *
396
     * @return FluentPdoModel|$this
397
     */
398
    public function displayColumn(string $column) : FluentPdoModel
399
    {
400
        $this->_display_column = $column;
401
402
        return $this;
403
    }
404
    /**
405
     * Set the table alias
406
     *
407
     * @param string $alias
408
     *
409
     * @return FluentPdoModel|$this
410
     */
411
    public function tableAlias(string $alias) : FluentPdoModel
412
    {
413
        $this->_table_alias = $alias;
414
415
        return $this;
416
    }
417
418
    /**
419
     * @param int $cacheTtl
420
     * @return FluentPdoModel|$this
421
     * @throws Exception
422
     */
423
    protected function _cacheTtl(int $cacheTtl) : FluentPdoModel
424
    {
425
        Assert($cacheTtl)->int('Cache ttl must be either -1 for no cache, 0 for default ttl or an integer for a custom ttl');
426
        if ( $cacheTtl !== self::CACHE_NO && ! is_null($this->_pdo_stmt) )
427
        {
428
            throw new Exception("You cannot cache pre-executed queries");
429
        }
430
        $this->_cache_ttl = $cacheTtl;
431
432
        return $this;
433
    }
434
435
    /**
436
     * @return string
437
     */
438
    public function getTableAlias() : string
439
    {
440
        return $this->_table_alias;
441
    }
442
443
    /**
444
     * @param array $associations
445
     *
446
     * @return FluentPdoModel|$this
447
     */
448
    public function associations(array $associations) : FluentPdoModel
449
    {
450
        $this->_associations = $associations;
451
452
        return $this;
453
    }
454
455
    /**
456
     * @param string $alias
457
     * @param array $definition
458
     * @return FluentPdoModel|$this
459
     */
460
    public function setBelongsTo(string $alias, array $definition) : FluentPdoModel
461
    {
462
        Assert($alias)->notEmpty();
463
        Assert($definition)->isArray()->count(4);
464
465
        $this->_associations['belongsTo'][$alias] = $definition;
466
467
        return $this;
468
    }
469
470
    /**
471
     * @param $alias
472
     * @param $displayField
473
     * @return FluentPdoModel|$this
474
     * @throws \Terah\Assert\AssertionFailedException
475
     */
476
    public function setBelongsToDisplayField(string $alias, string $displayField) : FluentPdoModel
477
    {
478
        Assert($alias)->notEmpty();
479
        Assert($this->_associations['belongsTo'])->keyExists($alias);
480
        Assert($displayField)->notEmpty();
481
482
        $this->_associations['belongsTo'][$alias][2] = $displayField;
483
484
        return $this;
485
    }
486
487
    /**
488
     * @param PDOStatement $stmt
489
     * @param Closure $fnCallback
490
     * @return bool|stdClass
491
     */
492
    public function fetchRow(PDOStatement $stmt, Closure $fnCallback=null)
493
    {
494
        if ( ! ( $record = $stmt->fetch(PDO::FETCH_OBJ) ) )
495
        {
496
            $this->row_meta_data    = [];
497
498
            return false;
499
        }
500
        $this->row_meta_data  = $this->row_meta_data  ?: $this->getColumnMeta($stmt, $record);
501
        $record = $this->onFetch($record);
502
        if ( empty($fnCallback) )
503
        {
504
            return $record;
505
        }
506
        $record             = $fnCallback($record);
507
        if ( is_null($record) )
508
        {
509
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
510
        }
511
        unset($fnCallback);
512
513
        return $record;
514
    }
515
516
    /**
517
     * @param PDOStatement $stmt
518
     * @param $record
519
     * @return array
520
     */
521
    protected function getColumnMeta(PDOStatement $stmt, $record) : array
522
    {
523
        $meta                   = [];
524
        if ( ! $this->_connection->supportsColumnMeta() )
525
        {
526
            return $meta;
527
        }
528
        foreach(range(0, $stmt->columnCount() - 1) as $index)
529
        {
530
            $data                   = $stmt->getColumnMeta($index);
531
            $meta[$data['name']]    = $data;
532
        }
533
        foreach ( $record as $field => $value )
534
        {
535
            Assert($meta)->keyExists($field);
536
        }
537
538
        return $meta;
539
    }
540
541
    /**
542
     * @param array $schema
543
     *
544
     * @return FluentPdoModel|$this
545
     */
546
    public function schema(array $schema) : FluentPdoModel
547
    {
548
        $this->_schema = $schema;
549
550
        return $this;
551
    }
552
553
    /**
554
     * @param string|array $field
555
     * @param $type
556
     * @return FluentPdoModel|$this
557
     */
558
    public function addSchema($field, string $type) : FluentPdoModel
559
    {
560
        if ( is_array($field) )
561
        {
562
            foreach ( $field as $field_name => $type_def )
563
            {
564
                $this->addSchema($field_name, $type_def);
565
            }
566
567
            return $this;
568
        }
569
        Assert($field)->string()->notEmpty();
570
        $this->_schema[$field] = $type;
571
572
        return $this;
573
    }
574
575
    /**
576
     * @param $keysOnly
577
     * @return array
578
     */
579
    public function getColumns(bool $keysOnly=true) : array
580
    {
581
        return $keysOnly ? array_keys($this->_schema) : $this->_schema;
582
    }
583
584
    /**
585
     * Get the primary key name
586
     *
587
     * @return string
588
     */
589
    public function getPrimaryKeyName() : string
590
    {
591
        return $this->_formatKeyName($this->_primary_key, $this->_table_name);
592
    }
593
594
    /**
595
     * @param string $query
596
     * @param array $parameters
597
     *
598
     * @return bool
599
     * @throws Exception
600
     */
601
    public function execute(string $query, array $parameters=[]) : bool
602
    {
603
        list($this->_built_query, $ident)  = $this->_logQuery($query, $parameters);
604
        try
605
        {
606
            $this->_pdo_stmt        = $this->getPdo()->prepare($query);
607
            $result                 = $this->_pdo_stmt->execute($parameters);
608
            if ( false === $result )
609
            {
610
                $this->_pdo_stmt        = null;
611
                throw new PDOException("The query failed to execute.");
612
            }
613
        }
614
        catch( Exception $e )
615
        {
616
            $built_query        = $this->_built_query ? $this->_built_query : $this->buildQuery($query, $parameters);
617
            $this->getLogger()->error("FAILED: \n\n{$built_query}\n WITH ERROR:\n" . $e->getMessage());
618
            $this->_pdo_stmt    = null;
619
620
            throw $e;
621
        }
622
        $this->_logSlowQueries($ident, $this->_built_query);
623
624
        return $result;
625
    }
626
627
    /**
628
     * @param string $query
629
     * @param array $params
630
     * @return FluentPdoModel|$this
631
     */
632
    public function query(string $query, array $params=[]) : FluentPdoModel
633
    {
634
        $this->_raw_sql             = $query;
635
        $this->_where_parameters    = $params;
636
637
        return $this;
638
    }
639
640
    /**
641
     * @param string $sql
642
     * @param array $params
643
     *
644
     * @return string
645
     */
646
    public function buildQuery(string $sql, array $params=[]) : string
647
    {
648
        $indexed = $params == array_values($params);
649
        if ( $indexed )
650
        {
651
            foreach ( $params as $key => $val )
652
            {
653
                $val    = is_string($val) ? "'{$val}'" : $val;
654
                $val    = is_null($val) ? 'NULL' : $val;
655
                $sql    = preg_replace('/\?/', $val, $sql, 1);
656
            }
657
658
            return $sql;
659
        }
660
661
        uksort($params, function ($a, $b) {
662
            return strlen($b) - strlen($a);
663
        });
664
        foreach ( $params as $key => $val )
665
        {
666
            $val    = is_string($val) ? "'{$val}'" : $val;
667
            $val    = is_null($val) ? 'NULL' : $val;
668
            $sql    = str_replace(":$key", $val, $sql);
669
            //$sql    = str_replace("$key", $val, $sql);
670
        }
671
672
        return $sql;
673
    }
674
675
    /**
676
     * @param stdClass $record
677
     *
678
     * @return stdClass
679
     */
680
    protected function _trimAndLowerCaseKeys(stdClass $record) : stdClass
681
    {
682
        $fnTrimStrings = function($value) {
683
            return is_string($value) ? trim($value) : $value;
684
        };
685
        $record = array_map($fnTrimStrings, array_change_key_case((array)$record, CASE_LOWER));
686
        unset($fnTrimStrings);
687
688
        return (object)$record;
689
    }
690
691
    /**
692
     * Return the number of affected row by the last statement
693
     *
694
     * @return int
695
     */
696
    public function rowCount() : int
697
    {
698
        $stmt = $this->fetchStmt();
699
700
        return $stmt ? $stmt->rowCount() : 0;
701
    }
702
703
    /**
704
     * @return PDOStatement
705
     * @throws PDOException
706
     */
707
    public function fetchStmt()
708
    {
709
        if ( null === $this->_pdo_stmt )
710
        {
711
            $this->execute($this->getSelectQuery(), $this->_getWhereParameters());
712
        }
713
714
        return $this->_pdo_stmt;
715
    }
716
717
    /**
718
     * @return array
719
     */
720
    public function fetchSqlQuery() : array
721
    {
722
        $clone      = clone $this;
723
        $query      = $clone->getSelectQuery();
724
        $params     = $clone->_getWhereParameters();
725
        $result     = [$query, $params];
726
        unset($clone->_handlers, $clone, $query, $params);
727
728
        return $result;
729
    }
730
731
    /**
732
     * @param string $tableName
733
     * @param bool  $dropIfExists
734
     * @param array $indexes
735
     * @return boolean
736
     * @throws Exception
737
     */
738
    public function fetchIntoMemoryTable(string $tableName, bool $dropIfExists=true, array $indexes=[]) : bool
739
    {
740
        $tableName      = preg_replace('/[^A-Za-z0-9_]+/', '', $tableName);
741
        $tableName      = $this->_tmp_table_prefix . preg_replace('/^' . $this->_tmp_table_prefix . '/', '', $tableName);
742
        if ( $dropIfExists )
743
        {
744
            $this->execute("DROP TABLE IF EXISTS {$tableName}");
745
        }
746
        $indexSql = [];
747
        foreach ( $indexes as $name => $column )
748
        {
749
            $indexSql[] = "INDEX {$name} ({$column})";
750
        }
751
        $indexSql       = implode(", ", $indexSql);
752
        $indexSql       = empty($indexSql) ? '' : "({$indexSql})";
753
        list($querySql, $params) = $this->fetchSqlQuery();
754
        $sql = <<<SQL
755
        CREATE TEMPORARY TABLE {$tableName} {$indexSql} ENGINE=MEMORY {$querySql}
756
SQL;
757
758
        return $this->execute($sql, $params);
759
    }
760
761
    /**
762
     * @param string $keyedOn
763
     * @param int $cacheTtl
764
     * @return stdClass[]
765
     */
766
    public function fetch(string $keyedOn='', int $cacheTtl=self::CACHE_NO) : array
767
    {
768
        $this->_cacheTtl($cacheTtl);
769
        $fnCallback         = function() use ($keyedOn) {
770
771
            $stmt               = $this->fetchStmt();
772
            $rows               = [];
773
            while ( $record = $this->fetchRow($stmt) )
774
            {
775
                if ( $record === false ) continue; // For scrutinizer...
776
                if ( $keyedOn && property_exists($record, $keyedOn) )
777
                {
778
                    $rows[$record->{$keyedOn}] = $record;
779
                    continue;
780
                }
781
                $rows[]         = $record;
782
            }
783
            $this->reset();
784
785
            return $rows;
786
        };
787
        if ( $this->_cache_ttl === self::CACHE_NO )
788
        {
789
            return $fnCallback();
790
        }
791
        $table              = $this->getTableName();
792
        $id                 = $this->_parseWhereForPrimaryLookup();
793
        $id                 = $id ? "/{$id}" : '';
794
        list($sql, $params) = $this->fetchSqlQuery();
795
        $sql                = $this->buildQuery($sql, $params);
796
        $cacheKey           = "/{$table}{$id}/" . md5(json_encode([
797
            'sql'       => $sql,
798
            'keyed_on'  => $keyedOn,
799
        ]));
800
        $data = $this->_cacheData($cacheKey, $fnCallback, $this->_cache_ttl);
801
802
        return is_array($data) ? $data : [];
803
    }
804
805
    /**
806
     * @return string
807
     */
808
    protected function _parseWhereForPrimaryLookup() : string
809
    {
810
        if ( ! ( $alias = $this->getTableAlias() ) )
811
        {
812
            return '';
813
        }
814
        foreach ( $this->_where_conditions as $idx => $conds )
815
        {
816
            if ( ! empty($conds['STATEMENT']) && $conds['STATEMENT'] === "{$alias}.id = ?" )
817
            {
818
                return ! empty($conds['PARAMS'][0]) ? (string)$conds['PARAMS'][0] : '';
819
            }
820
        }
821
822
        return '';
823
    }
824
825
    /**
826
     * @param string $cacheKey
827
     * @param Closure $func
828
     * @param int $cacheTtl - 0 for default ttl, -1 for no cache or int for custom ttl
829
     * @return mixed
830
     */
831
    protected function _cacheData(string $cacheKey, Closure $func, int $cacheTtl=self::CACHE_DEFAULT)
832
    {
833
        if ( $cacheTtl === self::CACHE_NO )
834
        {
835
            /** @noinspection PhpVoidFunctionResultUsedInspection */
836
            return $func->__invoke();
837
        }
838
        $data = $this->getCache()->get($cacheKey);
839
        if ( $data && is_object($data) && property_exists($data, 'results') )
840
        {
841
            $this->getLogger()->debug("Cache hit on {$cacheKey}");
842
            return $data->results;
843
        }
844
        $this->getLogger()->debug("Cache miss on {$cacheKey}");
845
        /** @noinspection PhpVoidFunctionResultUsedInspection */
846
        $data = (object)[
847
            // Watch out... invoke most likely calls reset
848
            // which clears the model params like _cache_ttl
849
            'results' => $func->__invoke(),
850
        ];
851
        try
852
        {
853
            // The cache engine expects null for the default cache value
854
            $cacheTtl = $cacheTtl === self::CACHE_DEFAULT ? 0 : $cacheTtl;
855
            /** @noinspection PhpMethodParametersCountMismatchInspection */
856
            if ( ! $this->getCache()->set($cacheKey, $data, $cacheTtl) )
857
            {
858
                throw new \Exception("Could not save data to cache");
859
            }
860
            return $data->results;
861
        }
862
        catch (\Exception $e)
863
        {
864
            $this->getLogger()->error($e->getMessage(), $e->getTrace());
865
866
            return $data->results;
867
        }
868
    }
869
870
    /**
871
     * @param string $cacheKey
872
     * @return bool
873
     */
874
    public function clearCache(string $cacheKey) : bool
875
    {
876
        return $this->getCache()->delete($cacheKey);
877
    }
878
879
    /**
880
     * @param string $table
881
     * @return bool
882
     */
883
    public function clearCacheByTable(string $table='') : bool
884
    {
885
        $tables  = $table ? [$table] : $this->getFlushCacheTables();
886
        foreach ( $tables as $table )
887
        {
888
            $this->clearCache("/{$table}/");
889
        }
890
891
        return true;
892
    }
893
894
    /**
895
     * @return string[]
896
     */
897
    public function getFlushCacheTables() : array
898
    {
899
        return ! empty($this->_flush_cache_tables) ? $this->_flush_cache_tables : [$this->getTableName()];
900
    }
901
902
    /**
903
     * @param Closure $fnCallback
904
     * @return int
905
     */
906
    public function fetchCallback(Closure $fnCallback) : int
907
    {
908
        $successCnt    = 0;
909
        $stmt           = $this->fetchStmt();
910
        while ( $this->_tallySuccessCount($stmt, $fnCallback, $successCnt) )
911
        {}
912
913
        return $successCnt;
914
    }
915
916
    /**
917
     * @param Closure $fnCallback
918
     * @param string  $keyedOn
919
     * @return array
920
     */
921
    public function fetchObjectsByCallback(Closure $fnCallback, string $keyedOn='') : array
922
    {
923
        $stmt       = $this->fetchStmt();
924
        $rows       = [];
925
        while ( $record = $this->fetchRow($stmt, $fnCallback) )
926
        {
927
            if ( $keyedOn && property_exists($record, $keyedOn) )
928
            {
929
                $rows[$record->{$keyedOn}] = $record;
930
                continue;
931
            }
932
            $rows[] = $record;
933
        }
934
        $this->reset();
935
936
        return $rows;
937
    }
938
939
    /**
940
     * @param $numFailures
941
     * @return FluentPdoModel|$this
942
     */
943
    public function maxCallbackFailures(int $numFailures) : FluentPdoModel
944
    {
945
        Assert($numFailures)->int();
946
        $this->_max_callback_failures = $numFailures;
947
948
        return $this;
949
    }
950
951
    /**
952
     * @param PDOStatement $stmt
953
     * @param Closure $fnCallback
954
     * @param int $successCnt
955
     * @return bool|null|stdClass
956
     */
957
    protected function _tallySuccessCount(PDOStatement $stmt, Closure $fnCallback, int &$successCnt)
958
    {
959
        $record = $this->fetchRow($stmt);
960
        if ( $record === false )
961
        {
962
            return false;
963
        }
964
        $record = $fnCallback($record);
965
        // Callback return null then we want to exit the fetch loop
966
        if ( is_null($record) )
967
        {
968
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
969
970
            return null;
971
        }
972
        // The not record then don't bump the tally
973
        if ( ! $record )
974
        {
975
            $this->_num_callback_failures++;
976
            if ( $this->_max_callback_failures !== -1 && $this->_num_callback_failures >= $this->_max_callback_failures )
977
            {
978
                $this->getLogger()->error("The callback has failed {$this->_max_callback_failures} times... aborting...");
979
                $successCnt = null;
980
981
                return null;
982
            }
983
984
            return true;
985
        }
986
        $successCnt++;
987
988
        return $record;
989
    }
990
991
    /**
992
     * @return bool
993
     */
994
    public function canGenericUpdate() : bool
995
    {
996
        return $this->_can_generic_update;
997
    }
998
999
    /**
1000
     * @return bool
1001
     */
1002
    public function canGenericAdd() : bool
1003
    {
1004
        return $this->_can_generic_add;
1005
    }
1006
1007
    /**
1008
     * @return bool
1009
     */
1010
    public function canGenericDelete() : bool
1011
    {
1012
        return $this->_can_generic_delete;
1013
    }
1014
1015
    /**
1016
     * @param string $keyedOn
1017
     * @param string $valueField
1018
     * @param int $cacheTtl
1019
     * @return mixed
1020
     */
1021
    public function fetchList(string $keyedOn='', string $valueField='', int $cacheTtl=self::CACHE_NO) : array
1022
    {
1023
        $keyedOn            = $keyedOn ?: $this->getPrimaryKeyName();
1024
        $valueField         = $valueField ?: $this->getDisplayColumn();
1025
        $keyedOnAlias       = strtolower(str_replace('.', '_', $keyedOn));
1026
        $valueFieldAlias    = strtolower(str_replace('.', '_', $valueField));
1027
        if ( preg_match('/ as /i', $keyedOn) )
1028
        {
1029
            list($keyedOn, $keyedOnAlias)   = preg_split('/ as /i', $keyedOn);
1030
            $keyedOn                        = trim($keyedOn);
1031
            $keyedOnAlias                   = trim($keyedOnAlias);
1032
        }
1033
        if ( preg_match('/ as /i', $valueField) )
1034
        {
1035
            list($valueField, $valueFieldAlias) = preg_split('/ as /i', $valueField);
1036
            $valueField                         = trim($valueField);
1037
            $valueFieldAlias                    = trim($valueFieldAlias);
1038
        }
1039
1040
        $this->_cacheTtl($cacheTtl);
1041
        $fnCallback         = function() use ($keyedOn, $keyedOnAlias, $valueField, $valueFieldAlias) {
1042
1043
            $rows = [];
1044
            $stmt = $this->select(null)
1045
                ->select($keyedOn, $keyedOnAlias)
1046
                ->select($valueField, $valueFieldAlias)
1047
                ->fetchStmt();
1048
            while ( $record = $this->fetchRow($stmt) )
1049
            {
1050
                $rows[$record->{$keyedOnAlias}] = $record->{$valueFieldAlias};
1051
            }
1052
1053
            return $rows;
1054
        };
1055
        if ( $this->_cache_ttl === self::CACHE_NO )
1056
        {
1057
            $result = $fnCallback();
1058
            unset($cacheKey, $fnCallback);
1059
1060
            return $result;
1061
        }
1062
        $table              = $this->getTableName();
1063
        $cacheData          = [
1064
            'sql'               => $this->fetchSqlQuery(),
1065
            'keyed_on'          => $keyedOn,
1066
            'keyed_on_alias'    => $keyedOnAlias,
1067
            'value_field'       => $valueField,
1068
            'value_fieldAlias'  => $valueFieldAlias,
1069
        ];
1070
        $cacheKey           = json_encode($cacheData);
1071
        if ( ! $cacheKey )
1072
        {
1073
            $this->getLogger()->warning('Could not generate cache key from data', $cacheData);
1074
            $result = $fnCallback();
1075
            unset($cacheKey, $fnCallback);
1076
1077
            return $result;
1078
        }
1079
        $cacheKey           = md5($cacheKey);
1080
1081
        return $this->_cacheData("/{$table}/list/{$cacheKey}", $fnCallback, $this->_cache_ttl);
1082
    }
1083
1084
    /**
1085
     * @param string $column
1086
     * @param int $cacheTtl
1087
     * @param bool|true $unique
1088
     * @return array
1089
     */
1090
    public function fetchColumn(string $column, int $cacheTtl=self::CACHE_NO, bool $unique=true) : array
1091
    {
1092
        $list = $this->select($column)->fetch('', $cacheTtl);
1093
        foreach ( $list as $idx => $obj )
1094
        {
1095
            $list[$idx] = $obj->{$column};
1096
        }
1097
1098
        return $unique ? array_unique($list) : $list;
1099
    }
1100
1101
    /**
1102
     * @param string $field
1103
     * @param int $itemId
1104
     * @param int $cacheTtl
1105
     * @return mixed|null
1106
     */
1107
    public function fetchField(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO)
1108
    {
1109
        $field      = $field ?: $this->getPrimaryKeyName();
1110
        $object     = $this->select(null)->select($field)->fetchOne($itemId, $cacheTtl);
1111
        if ( ! $object )
1112
        {
1113
            return null;
1114
        }
1115
        // Handle aliases
1116
        if ( preg_match('/ as /i', $field) )
1117
        {
1118
            list($expression, $alias) = preg_split('/ as /i', $field);
1119
            unset($expression);
1120
            $field = trim($alias);
1121
        }
1122
        if ( strpos($field, '.') !== false )
1123
        {
1124
            list($tableAlias, $field) = explode('.', $field);
1125
            unset($tableAlias);
1126
        }
1127
1128
        return property_exists($object, $field) ? $object->{$field} : null;
1129
    }
1130
1131
    /**
1132
     * @param string $field
1133
     * @param int $itemId
1134
     * @param int $cacheTtl
1135
     * @return string
1136
     */
1137
    public function fetchStr(string $field='', $itemId=0, int $cacheTtl=self::CACHE_NO) : string
1138
    {
1139
        return (string)$this->fetchField($field, $itemId, $cacheTtl);
1140
    }
1141
1142
    /**
1143
     * @param int $cacheTtl
1144
     * @return int
1145
     */
1146
    public function fetchId(int $cacheTtl=self::CACHE_NO) : int
1147
    {
1148
        return $this->fetchInt($this->getPrimaryKeyName(), 0, $cacheTtl);
1149
    }
1150
1151
    /**
1152
     * @param string $field
1153
     * @param int $itemId
1154
     * @param int $cacheTtl
1155
     * @return int
1156
     */
1157
    public function fetchInt(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO) : int
1158
    {
1159
        return (int)$this->fetchField($field, $itemId, $cacheTtl);
1160
    }
1161
1162
    /**
1163
     * @param string $field
1164
     * @param int $itemId
1165
     * @param int $cacheTtl
1166
     * @return float
1167
     */
1168
    public function fetchFloat(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO) : float
1169
    {
1170
        return (float)$this->fetchField($field, $itemId, $cacheTtl);
1171
    }
1172
1173
    /**
1174
     * @param string $field
1175
     * @param int $itemId
1176
     * @param int $cacheTtl
1177
     * @return bool
1178
     */
1179
    public function fetchBool(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO) : bool
1180
    {
1181
        return (bool)$this->fetchField($field, $itemId, $cacheTtl);
1182
    }
1183
1184
    /**
1185
     * @param int|null $id
1186
     * @param int $cacheTtl
1187
     * @return stdClass|bool
1188
     */
1189
    public function fetchOne(int $id=0, int $cacheTtl=self::CACHE_NO)
1190
    {
1191
        if ( $id > 0 )
1192
        {
1193
            $this->wherePk($id, true);
1194
        }
1195
        $this->limit(1);
1196
        $fetchAll   = $this->fetch('', $cacheTtl);
1197
1198
        return $fetchAll ? array_shift($fetchAll) : false;
1199
    }
1200
1201
    /**
1202
     * @param int|null $id
1203
     * @param int $cacheTtl
1204
     * @return boolean
1205
     */
1206
    public function fetchExists(int $id=0, int $cacheTtl=self::CACHE_NO) : bool
1207
    {
1208
        if ( $id > 0 )
1209
        {
1210
            $this->wherePk($id, true);
1211
        }
1212
        $cnt = $this->count('*', $cacheTtl);
1213
1214
        return $cnt > 0;
1215
    }
1216
1217
    /*------------------------------------------------------------------------------
1218
                                    Fluent Query Builder
1219
    *-----------------------------------------------------------------------------*/
1220
1221
    /**
1222
     * Create the select clause
1223
     *
1224
     * @param  mixed    $columns  - the column to select. Can be string or array of fields
1225
     * @param  string   $alias - an alias to the column
1226
     * @param boolean $explicitSelect
1227
     * @return FluentPdoModel|$this
1228
     */
1229
    public function select($columns='*', string $alias='', bool $explicitSelect=true) : FluentPdoModel
1230
    {
1231
        if ( $explicitSelect )
1232
        {
1233
            $this->explicitSelectMode();
1234
        }
1235
        if ( $alias && ! is_array($columns) & $columns !== $alias )
1236
        {
1237
            $columns .= " AS {$alias} ";
1238
        }
1239
        if ( $columns === '*' && !empty($this->_schema) )
1240
        {
1241
            $columns = $this->getColumns();
1242
        }
1243
        // Reset the select list
1244
        if ( is_null($columns) || $columns === '' )
1245
        {
1246
            $this->_select_fields = [];
1247
1248
            return $this;
1249
        }
1250
        $columns = is_array($columns) ? $columns : [$columns];
1251
1252
//        if ( empty($this->_select_fields) && $addAllIfEmpty )
1253
//        {
1254
//            $this->select('*');
1255
//        }
1256
        if ( $this->_table_alias )
1257
        {
1258
            $schema = $this->columns();
1259
            foreach ( $columns as $idx => $col )
1260
            {
1261
                if ( in_array($col, $schema) )
1262
                {
1263
                    $columns[$idx] = "{$this->_table_alias}.{$col}";
1264
                }
1265
            }
1266
        }
1267
        $this->_select_fields = array_merge($this->_select_fields, $columns);
1268
1269
        return $this;
1270
    }
1271
1272
    /**
1273
     * @param string $select
1274
     * @return FluentPdoModel|$this
1275
     */
1276
    public function selectRaw(string $select) : FluentPdoModel
1277
    {
1278
        $this->_select_fields[] = $select;
1279
1280
        return $this;
1281
    }
1282
1283
    /**
1284
     * @param bool $logQueries
1285
     *
1286
     * @return FluentPdoModel|$this
1287
     */
1288
    public function logQueries(bool $logQueries=true) : FluentPdoModel
1289
    {
1290
        $this->_log_queries = $logQueries;
1291
1292
        return $this;
1293
    }
1294
1295
    /**
1296
     * @param bool $includeCnt
1297
     *
1298
     * @return FluentPdoModel|$this
1299
     */
1300
    public function includeCount(bool $includeCnt=true) : FluentPdoModel
1301
    {
1302
        $this->_include_count = $includeCnt;
1303
1304
        return $this;
1305
    }
1306
1307
    /**
1308
     * @param bool $distinct
1309
     *
1310
     * @return FluentPdoModel|$this
1311
     */
1312
    public function distinct(bool $distinct=true) : FluentPdoModel
1313
    {
1314
        $this->_distinct = $distinct;
1315
1316
        return $this;
1317
    }
1318
1319
    /**
1320
     * @param array $fields
1321
     * @return FluentPdoModel|$this
1322
     */
1323
    public function withBelongsTo(array $fields=[]) : FluentPdoModel
1324
    {
1325
        if ( ! empty($this->_associations['belongsTo']) )
1326
        {
1327
            foreach ( $this->_associations['belongsTo'] as $alias => $config )
1328
            {
1329
                $addFieldsForJoins = empty($fields) || in_array($config[3], $fields);
1330
                $this->autoJoin($alias, self::LEFT_JOIN, $addFieldsForJoins);
1331
            }
1332
        }
1333
1334
        return $this;
1335
    }
1336
1337
    /**
1338
     * @param string $alias
1339
     * @param bool   $addSelectField
1340
     * @return FluentPdoModel
1341
     */
1342
    public function autoInnerJoin(string $alias, bool $addSelectField=true) : FluentPdoModel
1343
    {
1344
        return $this->autoJoin($alias, self::INNER_JOIN, $addSelectField);
1345
    }
1346
1347
    /**
1348
     * @param string $alias
1349
     * @param string $type
1350
     * @param bool   $addSelectField
1351
     * @return FluentPdoModel|$this
1352
     */
1353
    public function autoJoin(string $alias, string $type=self::LEFT_JOIN, bool $addSelectField=true) : FluentPdoModel
1354
    {
1355
        Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid join... the alias does not exists");
1356
        list($table, $join_col, $field, $fieldAlias)    = $this->_associations['belongsTo'][$alias];
1357
        $tableJoinField                                 = "{$this->_table_alias}.{$join_col}";
1358
        // Extra join onto another second level table
1359
        if ( strpos($join_col, '.') !== false )
1360
        {
1361
            $tableJoinField =  $join_col;
1362
            if ( $addSelectField )
1363
            {
1364
                $this->select($join_col, '', false);
1365
            }
1366
        }
1367
1368
1369
        $condition = "{$alias}.id = {$tableJoinField}";
1370
        if ( in_array($alias, $this->_join_aliases) )
1371
        {
1372
            return $this;
1373
        }
1374
        $this->join($table, $condition, $alias, $type);
1375
        if ( $addSelectField )
1376
        {
1377
            $this->select($field, $fieldAlias, false);
1378
        }
1379
1380
        return $this;
1381
    }
1382
1383
    /**
1384
     * @param array $conditions
1385
     * @return FluentPdoModel
1386
     */
1387
    public function whereArr(array $conditions) : FluentPdoModel
1388
    {
1389
        foreach ($conditions as $key => $val)
1390
        {
1391
            $this->where($key, $val);
1392
        }
1393
1394
        return $this;
1395
    }
1396
    /**
1397
     * Add where condition, more calls appends with AND
1398
     *
1399
     * @param string $condition possibly containing ? or :name
1400
     * @param mixed $parameters accepted by PDOStatement::execute or a scalar value
1401
     * @param mixed ...
1402
     * @return FluentPdoModel|$this
1403
     */
1404
    public function where($condition, $parameters=[]) : FluentPdoModel
1405
    {
1406
        // By default the and_or_operator and wrap operator is AND,
1407
        if ( $this->_wrap_open || ! $this->_and_or_operator )
1408
        {
1409
            $this->_and();
1410
        }
1411
1412
        // where(array("column1" => 1, "column2 > ?" => 2))
1413
        if ( is_array($condition) )
1414
        {
1415
            foreach ($condition as $key => $val)
1416
            {
1417
                $this->where($key, $val);
1418
            }
1419
1420
            return $this;
1421
        }
1422
1423
        $args = func_num_args();
1424
        if ( $args != 2 || strpbrk((string)$condition, '?:') )
1425
        { // where('column < ? OR column > ?', array(1, 2))
1426
            if ( $args != 2 || !is_array($parameters) )
1427
            { // where('column < ? OR column > ?', 1, 2)
1428
                $parameters = func_get_args();
1429
                array_shift($parameters);
1430
            }
1431
        }
1432
        else if ( ! is_array($parameters) )
1433
        {//where(column,value) => column=value
1434
            $condition .= ' = ?';
1435
            $parameters = [$parameters];
1436
        }
1437
        else if ( is_array($parameters) )
1438
        { // where('column', array(1, 2)) => column IN (?,?)
1439
            $placeholders = $this->_makePlaceholders(count($parameters));
1440
            $condition = "({$condition} IN ({$placeholders}))";
1441
        }
1442
1443
        $this->_where_conditions[] = [
1444
            'STATEMENT'   => $condition,
1445
            'PARAMS'      => $parameters,
1446
            'OPERATOR'    => $this->_and_or_operator
1447
        ];
1448
        // Reset the where operator to AND. To use OR, you must call _or()
1449
        $this->_and();
1450
1451
        return $this;
1452
    }
1453
1454
    /**
1455
     * Create an AND operator in the where clause
1456
     *
1457
     * @return FluentPdoModel|$this
1458
     */
1459
    public function _and() : FluentPdoModel
1460
    {
1461
        if ( $this->_wrap_open )
1462
        {
1463
            $this->_where_conditions[] = self::OPERATOR_AND;
1464
            $this->_last_wrap_position = count($this->_where_conditions);
1465
            $this->_wrap_open = false;
1466
1467
            return $this;
1468
        }
1469
        $this->_and_or_operator = self::OPERATOR_AND;
1470
1471
        return $this;
1472
    }
1473
1474
1475
    /**
1476
     * Create an OR operator in the where clause
1477
     *
1478
     * @return FluentPdoModel|$this
1479
     */
1480
    public function _or() : FluentPdoModel
1481
    {
1482
        if ( $this->_wrap_open )
1483
        {
1484
            $this->_where_conditions[]  = self::OPERATOR_OR;
1485
            $this->_last_wrap_position  = count($this->_where_conditions);
1486
            $this->_wrap_open           = false;
1487
1488
            return $this;
1489
        }
1490
        $this->_and_or_operator     = self::OPERATOR_OR;
1491
1492
        return $this;
1493
    }
1494
1495
    /**
1496
     * To group multiple where clauses together.
1497
     *
1498
     * @return FluentPdoModel|$this
1499
     */
1500
    public function wrap() : FluentPdoModel
1501
    {
1502
        $this->_wrap_open           = true;
1503
        $spliced                    = array_splice($this->_where_conditions, $this->_last_wrap_position, count($this->_where_conditions), '(');
1504
        $this->_where_conditions    = array_merge($this->_where_conditions, $spliced);
1505
        array_push($this->_where_conditions,')');
1506
        $this->_last_wrap_position = count($this->_where_conditions);
1507
1508
        return $this;
1509
    }
1510
1511
    /**
1512
     * Where Primary key
1513
     *
1514
     * @param int  $id
1515
     * @param bool $addAlias
1516
     *
1517
     * @return FluentPdoModel|$this
1518
     */
1519
    public function wherePk(int $id, bool $addAlias=true) : FluentPdoModel
1520
    {
1521
        $alias = $addAlias && ! empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1522
1523
        return $this->where($alias . $this->getPrimaryKeyName(), $id);
1524
    }
1525
1526
    /**
1527
     * @param string $name
1528
     * @param bool   $addAlias
1529
     * @return FluentPdoModel
1530
     */
1531
    public function whereDisplayName(string $name, bool $addAlias=true) : FluentPdoModel
1532
    {
1533
        $alias = $addAlias && ! empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1534
1535
        return $this->where($alias . $this->getDisplayColumn(), $name);
1536
    }
1537
1538
    /**
1539
     * WHERE $columnName != $value
1540
     *
1541
     * @param  string   $columnName
1542
     * @param  mixed    $value
1543
     * @return FluentPdoModel|$this
1544
     */
1545
    public function whereNot(string $columnName, $value) : FluentPdoModel
1546
    {
1547
        return $this->where("$columnName != ?", $value);
1548
    }
1549
    /**
1550
     * WHERE $columnName != $value
1551
     *
1552
     * @param  string   $columnName
1553
     * @param  mixed    $value
1554
     * @return FluentPdoModel|$this
1555
     */
1556
    public function whereCoercedNot(string $columnName, $value) : FluentPdoModel
1557
    {
1558
        return $this->where("IFNULL({$columnName}, '') != ?", $value);
1559
    }
1560
1561
    /**
1562
     * WHERE $columnName LIKE $value
1563
     *
1564
     * @param  string   $columnName
1565
     * @param  mixed    $value
1566
     * @return FluentPdoModel|$this
1567
     */
1568
    public function whereLike(string $columnName, $value) : FluentPdoModel
1569
    {
1570
        return $this->where("$columnName LIKE ?", $value);
1571
    }
1572
1573
    /**
1574
     * @param string $columnName
1575
     * @param mixed $value1
1576
     * @param mixed $value2
1577
     * @return FluentPdoModel|$this
1578
     */
1579
    public function whereBetween(string $columnName, $value1, $value2) : FluentPdoModel
1580
    {
1581
        $value1 = is_string($value1) ? trim($value1) : $value1;
1582
        $value2 = is_string($value2) ? trim($value2) : $value2;
1583
1584
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1585
    }
1586
1587
    /**
1588
     * @param string $columnName
1589
     * @param mixed $value1
1590
     * @param mixed $value2
1591
     * @return FluentPdoModel|$this
1592
     */
1593
    public function whereNotBetween(string $columnName, $value1, $value2) : FluentPdoModel
1594
    {
1595
        $value1 = is_string($value1) ? trim($value1) : $value1;
1596
        $value2 = is_string($value2) ? trim($value2) : $value2;
1597
1598
        return $this->where("$columnName NOT BETWEEN ? AND ?", [$value1, $value2]);
1599
    }
1600
1601
    /**
1602
     * @param string $columnName
1603
     * @param string $regex
1604
     * @return FluentPdoModel|$this
1605
     */
1606
    public function whereRegex(string $columnName, string $regex) : FluentPdoModel
1607
    {
1608
        return $this->where("$columnName REGEXP ?", $regex);
1609
    }
1610
1611
    /**
1612
     * @param string $columnName
1613
     * @param string $regex
1614
     * @return FluentPdoModel|$this
1615
     */
1616
    public function whereNotRegex(string $columnName, string $regex) : FluentPdoModel
1617
    {
1618
        return $this->where("$columnName NOT REGEXP ?", $regex);
1619
    }
1620
1621
    /**
1622
     * WHERE $columnName NOT LIKE $value
1623
     *
1624
     * @param  string   $columnName
1625
     * @param  string   $value
1626
     * @return FluentPdoModel|$this
1627
     */
1628
    public function whereNotLike(string $columnName, string $value) : FluentPdoModel
1629
    {
1630
        return $this->where("$columnName NOT LIKE ?", $value);
1631
    }
1632
1633
    /**
1634
     * WHERE $columnName > $value
1635
     *
1636
     * @param  string   $columnName
1637
     * @param  mixed    $value
1638
     * @return FluentPdoModel|$this
1639
     */
1640
    public function whereGt(string $columnName, $value) : FluentPdoModel
1641
    {
1642
        return $this->where("$columnName > ?", $value);
1643
    }
1644
1645
    /**
1646
     * WHERE $columnName >= $value
1647
     *
1648
     * @param  string   $columnName
1649
     * @param  mixed    $value
1650
     * @return FluentPdoModel|$this
1651
     */
1652
    public function whereGte(string $columnName, $value) : FluentPdoModel
1653
    {
1654
        return $this->where("$columnName >= ?", $value);
1655
    }
1656
1657
    /**
1658
     * WHERE $columnName < $value
1659
     *
1660
     * @param  string   $columnName
1661
     * @param  mixed    $value
1662
     * @return FluentPdoModel|$this
1663
     */
1664
    public function whereLt(string $columnName, $value) : FluentPdoModel
1665
    {
1666
        return $this->where("$columnName < ?", $value);
1667
    }
1668
1669
    /**
1670
     * WHERE $columnName <= $value
1671
     *
1672
     * @param  string   $columnName
1673
     * @param  mixed    $value
1674
     * @return FluentPdoModel|$this
1675
     */
1676
    public function whereLte(string $columnName, $value) : FluentPdoModel
1677
    {
1678
        return $this->where("$columnName <= ?", $value);
1679
    }
1680
1681
    /**
1682
     * WHERE $columnName IN (?,?,?,...)
1683
     *
1684
     * @param  string   $columnName
1685
     * @param  array    $values
1686
     * @return FluentPdoModel|$this
1687
     */
1688
    public function whereIn(string $columnName, array $values) : FluentPdoModel
1689
    {
1690
        return $this->where($columnName, array_values($values));
1691
    }
1692
1693
    /**
1694
     * WHERE $columnName NOT IN (?,?,?,...)
1695
     *
1696
     * @param  string   $columnName
1697
     * @param  array    $values
1698
     * @return FluentPdoModel|$this
1699
     */
1700
    public function whereNotIn(string $columnName, array $values) : FluentPdoModel
1701
    {
1702
        $placeholders = $this->_makePlaceholders(count($values));
1703
1704
        return $this->where("({$columnName} NOT IN ({$placeholders}))", $values);
1705
    }
1706
1707
    /**
1708
     * WHERE $columnName IS NULL
1709
     *
1710
     * @param  string   $columnName
1711
     * @return FluentPdoModel|$this
1712
     */
1713
    public function whereNull(string $columnName) : FluentPdoModel
1714
    {
1715
        return $this->where("({$columnName} IS NULL)");
1716
    }
1717
1718
    /**
1719
     * WHERE $columnName IS NOT NULL
1720
     *
1721
     * @param  string   $columnName
1722
     * @return FluentPdoModel|$this
1723
     */
1724
    public function whereNotNull(string $columnName) : FluentPdoModel
1725
    {
1726
        return $this->where("({$columnName} IS NOT NULL)");
1727
    }
1728
1729
    /**
1730
     * @param string $statement
1731
     * @param string $operator
1732
     * @return FluentPdoModel|$this
1733
     */
1734
    public function having(string $statement, string $operator=self::OPERATOR_AND) : FluentPdoModel
1735
    {
1736
        $this->_having[] = [
1737
            'STATEMENT'   => $statement,
1738
            'OPERATOR'    => $operator
1739
        ];
1740
1741
        return $this;
1742
    }
1743
1744
    /**
1745
     * ORDER BY $columnName (ASC | DESC)
1746
     *
1747
     * @param  string   $columnName - The name of the column or an expression
1748
     * @param  string   $ordering   (DESC | ASC)
1749
     * @return FluentPdoModel|$this
1750
     */
1751
    public function orderBy(string $columnName='', string $ordering='ASC') : FluentPdoModel
1752
    {
1753
        $ordering       = strtoupper($ordering);
1754
        Assert($ordering)->inArray(['DESC', 'ASC']);
1755
        if ( ! $columnName )
1756
        {
1757
            $this->_order_by = [];
1758
1759
            return $this;
1760
        }
1761
        $this->_order_by[] = trim("{$columnName} {$ordering}");
1762
1763
        return $this;
1764
    }
1765
1766
    /**
1767
     * GROUP BY $columnName
1768
     *
1769
     * @param  string   $columnName
1770
     * @return FluentPdoModel|$this
1771
     */
1772
    public function groupBy(string $columnName) : FluentPdoModel
1773
    {
1774
        $columnName = is_array($columnName) ? $columnName : [$columnName];
1775
        foreach ( $columnName as $col )
1776
        {
1777
            $this->_group_by[] = $col;
1778
        }
1779
1780
        return $this;
1781
    }
1782
1783
1784
    /**
1785
     * LIMIT $limit
1786
     *
1787
     * @param  int      $limit
1788
     * @param  int|null $offset
1789
     * @return FluentPdoModel|$this
1790
     */
1791
    public function limit(int $limit, int $offset=0) : FluentPdoModel
1792
    {
1793
        $this->_limit =  $limit;
1794
        if ( $offset )
1795
        {
1796
            $this->offset($offset);
1797
        }
1798
        return $this;
1799
    }
1800
1801
    /**
1802
     * Return the limit
1803
     *
1804
     * @return integer
1805
     */
1806
    public function getLimit() : int
1807
    {
1808
        return $this->_limit;
1809
    }
1810
1811
    /**
1812
     * OFFSET $offset
1813
     *
1814
     * @param  int      $offset
1815
     * @return FluentPdoModel|$this
1816
     */
1817
    public function offset(int $offset) : FluentPdoModel
1818
    {
1819
        $this->_offset = (int)$offset;
1820
1821
        return $this;
1822
    }
1823
1824
    /**
1825
     * Return the offset
1826
     *
1827
     * @return integer
1828
     */
1829
    public function getOffset() : int
1830
    {
1831
        return $this->_offset;
1832
    }
1833
1834
    /**
1835
     * Build a join
1836
     *
1837
     * @param  string    $table         - The table name
1838
     * @param  string   $constraint    -> id = profile.user_id
1839
     * @param  string   $tableAlias   - The alias of the table name
1840
     * @param  string   $joinOperator - LEFT | INNER | etc...
1841
     * @return FluentPdoModel|$this
1842
     */
1843
    public function join(string $table, string $constraint='', string $tableAlias='', string $joinOperator='') : FluentPdoModel
1844
    {
1845
        if ( ! $constraint )
1846
        {
1847
            return $this->autoJoin($table, $joinOperator);
1848
        }
1849
        $join                   = [$joinOperator ? "{$joinOperator} " : ''];
1850
        $join[]                 = "JOIN {$table} ";
1851
        $tableAlias             = $tableAlias ?: Inflector::classify($table);
1852
        $join[]                 = $tableAlias ? "AS {$tableAlias} " : '';
1853
        $join[]                 = "ON {$constraint}";
1854
        $this->_join_sources[]  = implode('', $join);
1855
        if ( $tableAlias )
1856
        {
1857
            $this->_join_aliases[]  = $tableAlias;
1858
        }
1859
1860
        return $this;
1861
    }
1862
1863
    /**
1864
     * Create a left join
1865
     *
1866
     * @param  string   $table
1867
     * @param  string   $constraint
1868
     * @param  string   $tableAlias
1869
     * @return FluentPdoModel|$this
1870
     */
1871
    public function leftJoin(string $table, string $constraint, string $tableAlias='') : FluentPdoModel
1872
    {
1873
        return $this->join($table, $constraint, $tableAlias, self::LEFT_JOIN);
1874
    }
1875
1876
1877
    /**
1878
     * Return the build select query
1879
     *
1880
     * @return string
1881
     */
1882
    public function getSelectQuery() : string
1883
    {
1884
        if ( $this->_raw_sql )
1885
        {
1886
            return $this->_raw_sql;
1887
        }
1888
        if ( empty($this->_select_fields) || ! $this->_explicit_select_mode )
1889
        {
1890
            $this->select('*', '', false);
1891
        }
1892
        foreach ( $this->_select_fields as $idx => $cols )
1893
        {
1894
            if ( strpos(trim(strtolower($cols)), 'distinct ') === 0 )
1895
            {
1896
                $this->_distinct = true;
1897
                $this->_select_fields[$idx] = str_ireplace('distinct ', '', $cols);
1898
            }
1899
        }
1900
        if ( $this->_include_count )
1901
        {
1902
            $this->select('COUNT(*) as __cnt');
1903
        }
1904
        $query  = 'SELECT ';
1905
        $query .= $this->_distinct ? 'DISTINCT ' : '';
1906
        $query .= implode(', ', $this->_prepareColumns($this->_select_fields));
1907
        $query .= " FROM {$this->_table_name}" . ( $this->_table_alias ? " {$this->_table_alias}" : '' );
1908
        if ( count($this->_join_sources ) )
1909
        {
1910
            $query .= (' ').implode(' ',$this->_join_sources);
1911
        }
1912
        $query .= $this->_getWhereString(); // WHERE
1913
        if ( count($this->_group_by) )
1914
        {
1915
            $query .= ' GROUP BY ' . implode(', ', array_unique($this->_group_by));
1916
        }
1917
        if ( count($this->_order_by ) )
1918
        {
1919
            $query .= ' ORDER BY ' . implode(', ', array_unique($this->_order_by));
1920
        }
1921
        $query .= $this->_getHavingString(); // HAVING
1922
1923
        return $this->_connection->setLimit($query, $this->_limit, $this->_offset);
1924
    }
1925
1926
    /**
1927
     * @param string $field
1928
     * @param string $column
1929
     * @return string
1930
     */
1931
    public function getFieldComment(string $field, string $column) : string
1932
    {
1933
        return $this->_connection->getFieldComment($field, $column);
1934
    }
1935
1936
    /**
1937
     * Prepare columns to include the table alias name
1938
     * @param array $columns
1939
     * @return array
1940
     */
1941
    protected function _prepareColumns(array $columns) : array
1942
    {
1943
        if ( ! $this->_table_alias )
1944
        {
1945
            return $columns;
1946
        }
1947
        $newColumns = [];
1948
        foreach ($columns as $column)
1949
        {
1950
            if ( strpos($column, ',') && ! preg_match('/^[a-zA-Z_]{2,200}\(.{1,500}\)/', trim($column)) )
1951
            {
1952
                $newColumns = array_merge($this->_prepareColumns(explode(',', $column)), $newColumns);
1953
            }
1954
            elseif ( preg_match('/^(AVG|SUM|MAX|MIN|COUNT|CONCAT)/', $column) )
1955
            {
1956
                $newColumns[] = trim($column);
1957
            }
1958
            elseif (strpos($column, '.') === false && strpos(strtoupper($column), 'NULL') === false)
1959
            {
1960
                $column         = trim($column);
1961
                $newColumns[]   = preg_match('/^[0-9]/', $column) ? trim($column) : "{$this->_table_alias}.{$column}";
1962
            }
1963
            else
1964
            {
1965
                $newColumns[] = trim($column);
1966
            }
1967
        }
1968
1969
        return $newColumns;
1970
    }
1971
1972
    /**
1973
     * Build the WHERE clause(s)
1974
     *
1975
     * @param bool $purgeAliases
1976
     * @return string
1977
     */
1978
    protected function _getWhereString(bool $purgeAliases=false) : string
1979
    {
1980
        // If there are no WHERE clauses, return empty string
1981
        if ( empty($this->_where_conditions) )
1982
        {
1983
            return '';
1984
        }
1985
        $where_condition = '';
1986
        $last_condition = '';
1987
        foreach ( $this->_where_conditions as $condition )
1988
        {
1989
            if ( is_array($condition) )
1990
            {
1991
                if ( $where_condition && $last_condition != '(' && !preg_match('/\)\s+(OR|AND)\s+$/i', $where_condition))
1992
                {
1993
                    $where_condition .= $condition['OPERATOR'];
1994
                }
1995
                if ( $purgeAliases && ! empty($condition['STATEMENT']) && strpos($condition['STATEMENT'], '.') !== false && ! empty($this->_table_alias) )
1996
                {
1997
                    $condition['STATEMENT'] = preg_replace("/{$this->_table_alias}\./", '', $condition['STATEMENT']);
1998
                }
1999
                $where_condition .= $condition['STATEMENT'];
2000
                $this->_where_parameters = array_merge($this->_where_parameters, $condition['PARAMS']);
2001
            }
2002
            else
2003
            {
2004
                $where_condition .= $condition;
2005
            }
2006
            $last_condition = $condition;
2007
        }
2008
2009
        return " WHERE {$where_condition}" ;
2010
    }
2011
2012
    /**
2013
     * Return the HAVING clause
2014
     *
2015
     * @return string
2016
     */
2017
    protected function _getHavingString() : string
2018
    {
2019
        // If there are no WHERE clauses, return empty string
2020
        if ( empty($this->_having) )
2021
        {
2022
            return '';
2023
        }
2024
        $having_condition = '';
2025
        foreach ( $this->_having as $condition )
2026
        {
2027
            if ( $having_condition && ! preg_match('/\)\s+(OR|AND)\s+$/i', $having_condition) )
2028
            {
2029
                $having_condition .= $condition['OPERATOR'];
2030
            }
2031
            $having_condition .= $condition['STATEMENT'];
2032
        }
2033
2034
        return " HAVING {$having_condition}" ;
2035
    }
2036
2037
    /**
2038
     * Return the values to be bound for where
2039
     *
2040
     * @param bool $purgeAliases
2041
     * @return array
2042
     */
2043
    protected function _getWhereParameters(bool $purgeAliases=false) : array
2044
    {
2045
        unset($purgeAliases);
2046
2047
        return $this->_where_parameters;
2048
    }
2049
2050
    /**
2051
     * @param array $record
2052
     * @return stdClass
2053
     */
2054
    public function insertArr(array $record) : stdClass
2055
    {
2056
        return $this->insert((object)$record);
2057
    }
2058
2059
    /**
2060
     * Insert new rows
2061
     * $records can be a stdClass or an array of stdClass to add a bulk insert
2062
     * If a single row is inserted, it will return it's row instance
2063
     *
2064
     * @param stdClass $record
2065
     * @return stdClass
2066
     * @throws Exception
2067
     */
2068
    public function insert(stdClass $record) : stdClass
2069
    {
2070
        Assert((array)$record)->notEmpty("The data passed to insert does not contain any data");
2071
        Assert($record)->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
2072
2073
        $record = $this->beforeSave($record, self::SAVE_INSERT);
2074
        if ( ! empty($this->_errors) )
2075
        {
2076
            return $record;
2077
        }
2078
        list($sql, $insert_values) = $this->insertSqlQuery([$record]);
2079
        $this->execute((string)$sql, (array)$insert_values);
2080
        $rowCount = $this->rowCount();
2081
        if ( $rowCount === 1 )
2082
        {
2083
            $primaryKeyName                 = $this->getPrimaryKeyName();
2084
            $record->{$primaryKeyName}      = $this->getLastInsertId($primaryKeyName);
2085
        }
2086
        $record = $this->afterSave($record, self::SAVE_INSERT);
2087
        $this->destroy();
2088
2089
        return $record;
2090
    }
2091
2092
    /**
2093
     * @param string $name
2094
     * @return int
2095
     */
2096
    public function getLastInsertId(string $name='') : int
2097
    {
2098
        return (int)$this->getPdo()->lastInsertId($name ?: null);
2099
    }
2100
2101
    /**
2102
     * @param stdClass[] $records
2103
     * @return stdClass[]
2104
     */
2105
    public function insertSqlQuery(array $records) : array
2106
    {
2107
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
2108
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
2109
2110
        $insert_values      = [];
2111
        $question_marks     = [];
2112
        $properties         = [];
2113
        foreach ( $records as $record )
2114
        {
2115
            $properties         = !empty($properties) ? $properties : array_keys(get_object_vars($record));
2116
            $question_marks[]   = '('  . $this->_makePlaceholders(count($properties)) . ')';
2117
            $insert_values      = array_merge($insert_values, array_values((array)$record));
2118
        }
2119
        $properties         = implode(', ', $properties);
2120
        $question_marks     = implode(', ', $question_marks);
2121
        $sql                = "INSERT INTO {$this->_table_name} ({$properties}) VALUES {$question_marks}";
2122
2123
        return [$sql, $insert_values];
2124
    }
2125
2126
    /**
2127
     * @param       $data
2128
     * @param array $matchOn
2129
     * @param bool  $returnObj
2130
     * @return bool|int|stdClass
2131
     */
2132
    public function upsert($data, array $matchOn=[], $returnObj=false)
2133
    {
2134
        if ( ! is_array($data) )
2135
        {
2136
            return $this->upsertOne($data, $matchOn, $returnObj);
2137
        }
2138
        Assert($data)
2139
            ->notEmpty("The data passed to insert does not contain any data")
2140
            ->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
2141
        $num_success    = 0;
2142
        foreach ( $data as $row )
2143
        {
2144
            $clone = clone $this;
2145
            if ( $clone->upsertOne($row, $matchOn) )
2146
            {
2147
                $num_success++;
2148
            }
2149
            unset($clone->_handlers, $clone); // hhvm mem leak
2150
        }
2151
2152
        return $num_success;
2153
    }
2154
2155
    /**
2156
     * @param stdClass $object
2157
     * @param array    $matchOn
2158
     * @param bool     $returnObj
2159
     * @return bool|int|stdClass
2160
     */
2161
    public function upsertOne(stdClass $object, array $matchOn=[], $returnObj=false)
2162
    {
2163
        $primary_key    = $this->getPrimaryKeyName();
2164
        $matchOn       = empty($matchOn) && property_exists($object, $primary_key) ? [$primary_key] : $matchOn;
2165
        foreach ( $matchOn as $column )
2166
        {
2167
            Assert( ! property_exists($object, $column) && $column !== $primary_key)->false('The match on value for upserts is missing.');
2168
            if ( property_exists($object, $column) )
2169
            {
2170
                if ( is_null($object->{$column}) )
2171
                {
2172
                    $this->whereNull($column);
2173
                }
2174
                else
2175
                {
2176
                    $this->where($column, $object->{$column});
2177
                }
2178
            }
2179
        }
2180
        if ( count($this->_where_conditions) < 1 )
2181
        {
2182
            return $this->insert($object);
2183
        }
2184
        if ( ( $id = (int)$this->fetchField($primary_key) ) )
2185
        {
2186
            if ( property_exists($object, $primary_key) && empty($object->{$primary_key}) )
2187
            {
2188
                $object->$primary_key = $id;
2189
            }
2190
            $rows_affected = $this->reset()->wherePk($id)->update($object);
2191
            if ( $rows_affected === false )
2192
            {
2193
                return false;
2194
            }
2195
2196
            return $returnObj ? $this->reset()->fetchOne($id) : $id;
2197
        }
2198
2199
        return $this->insert($object);
2200
    }
2201
2202
    /**
2203
     * @param array      $data
2204
     * @param array      $matchOn
2205
     * @param bool|false $returnObj
2206
     * @return bool|int|stdClass
2207
     */
2208
    public function upsertArr(array $data, array $matchOn=[], bool $returnObj=false)
2209
    {
2210
        return $this->upsert((object)$data, $matchOn, $returnObj);
2211
    }
2212
2213
    /**
2214
     * Update entries
2215
     * Use the query builder to create the where clause
2216
     *
2217
     * @param stdClass $record
2218
     * @param bool     $updateAll
2219
     * @return int
2220
     * @throws Exception
2221
     */
2222
    public function update(stdClass $record, $updateAll=false) : int
2223
    {
2224
        Assert($record)
2225
            ->notEmpty("The data passed to update does not contain any data")
2226
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2227
2228
        if ( empty($this->_where_conditions) && ! $updateAll )
2229
        {
2230
            throw new Exception("You cannot update an entire table without calling update with updateAll=true", 500);
2231
        }
2232
        $record = $this->beforeSave($record, self::SAVE_UPDATE);
2233
        if ( ! empty($this->_errors) )
2234
        {
2235
            return 0;
2236
        }
2237
        list($sql, $values) = $this->updateSqlQuery($record);
2238
        $this->execute($sql, $values);
2239
        $this->afterSave($record, self::SAVE_UPDATE);
2240
        $rowCount = $this->rowCount();
2241
        $this->destroy();
2242
2243
        return $rowCount;
2244
    }
2245
2246
    /**
2247
     * @param array      $record
2248
     * @param bool|false $updateAll
2249
     * @return int
2250
     * @throws Exception
2251
     */
2252
    public function updateArr(array $record, $updateAll=false) : int
2253
    {
2254
        return $this->update((object)$record, $updateAll);
2255
    }
2256
2257
2258
    /**
2259
     * @param string  $field
2260
     * @param mixed   $value
2261
     * @param int     $id
2262
     * @param bool|false $updateAll
2263
     * @return int
2264
     * @throws Exception
2265
     */
2266
    public function updateField(string $field, $value, int $id=0, bool $updateAll=false) : int
2267
    {
2268
        if ( $id && $id > 0 )
2269
        {
2270
            $this->wherePk($id);
2271
        }
2272
        $columns    = $this->getColumns();
2273
        if ( $columns )
0 ignored issues
show
Bug Best Practice introduced by
The expression $columns 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...
2274
        {
2275
            Assert($field)->inArray($columns, "The field {$field} does not exist on this table {$this->_table_name}");
2276
        }
2277
2278
        return $this->update((object)[$field => $value], $updateAll);
2279
    }
2280
2281
    /**
2282
     * @param stdClass $record
2283
     * @return bool|int
2284
     * @throws Exception
2285
     */
2286
    public function updateChanged(stdClass $record) : int
2287
    {
2288
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2289
        {
2290
            if ( is_null($value) )
2291
            {
2292
                $this->whereNotNull($field);
2293
                continue;
2294
            }
2295
            $this->whereCoercedNot($field, $value);
2296
        }
2297
2298
        return $this->update($record);
2299
    }
2300
2301
    /**
2302
     * @param string    $expression
2303
     * @param array     $params
2304
     * @return FluentPdoModel|$this
2305
     */
2306
    public function updateByExpression(string $expression, array $params) : FluentPdoModel
2307
    {
2308
        $this->_update_raw[] = [$expression, $params];
2309
2310
        return $this;
2311
    }
2312
2313
    /**
2314
     * @param array $data
2315
     * @return int
2316
     * @throws Exception
2317
     */
2318
    public function rawUpdate(array $data=[]) : int
2319
    {
2320
        list($sql, $values) = $this->updateSql($data);
2321
        $this->execute($sql, $values);
2322
        $rowCount           = $this->rowCount();
2323
        $this->destroy();
2324
2325
        return $rowCount;
2326
    }
2327
2328
    /**
2329
     * @param stdClass $record
2330
     * @return array
2331
     */
2332
    public function updateSqlQuery(stdClass $record) : array
2333
    {
2334
        Assert($record)
2335
            ->notEmpty("The data passed to update does not contain any data")
2336
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2337
2338
        // Make sure we remove the primary key
2339
2340
        return $this->updateSql((array)$record);
2341
    }
2342
2343
    /**
2344
     * @param $record
2345
     * @return array
2346
     */
2347
    protected function updateSql(array $record) : array
2348
    {
2349
        unset($record[$this->getPrimaryKeyName()]);
2350
        Assert($this->_limit)->eq(0, 'You cannot limit updates');
2351
        // Sqlite needs a null primary key
2352
        //$record[$this->getPrimaryKeyName()] = null;
2353
        $fieldList  = [];
2354
        $fieldAlias = $this->_add_update_alias && ! empty($this->_table_alias) ? "{$this->_table_alias}." : '';
2355
        foreach ( $record as $key => $value )
2356
        {
2357
            if ( is_numeric($key) )
2358
            {
2359
                $fieldList[] = $value;
2360
                unset($record[$key]);
2361
                continue;
2362
            }
2363
            $fieldList[] = "{$fieldAlias}{$key} = ?";
2364
        }
2365
        $rawParams  = [];
2366
        foreach ( $this->_update_raw as $rawUpdate )
2367
        {
2368
            $fieldList[]   = $rawUpdate[0];
2369
            $rawParams      = array_merge($rawParams, $rawUpdate[1]);
2370
        }
2371
        $fieldList     = implode(', ', $fieldList);
2372
        $whereStr      = $this->_getWhereString();
2373
        $joins          = ! empty($this->_join_sources) ? (' ').implode(' ',$this->_join_sources) : '';
2374
        $alias          = ! empty($this->_table_alias) ? " AS {$this->_table_alias}" : '';
2375
        $sql            = "UPDATE {$this->_table_name}{$alias}{$joins} SET {$fieldList}{$whereStr}";
2376
        $values         = array_merge(array_values($record), $rawParams, $this->_getWhereParameters());
2377
2378
        return [$sql, $values];
2379
    }
2380
2381
    /**
2382
     * @param bool $deleteAll
2383
     * @param bool $force
2384
     * @return int
2385
     * @throws Exception
2386
     */
2387
    public function delete(bool $deleteAll=false, bool $force=false) : int
2388
    {
2389
        if ( ! $force && $this->_soft_deletes )
2390
        {
2391
            return $this->updateDeleted();
2392
        }
2393
2394
        list($sql, $params) = $this->deleteSqlQuery();
2395
        if ( empty($this->_where_conditions) && ! $deleteAll )
2396
        {
2397
            throw new Exception("You cannot update an entire table without calling update with deleteAll=true");
2398
        }
2399
        $this->execute($sql, $params);
2400
2401
        return $this->rowCount();
2402
    }
2403
2404
    /**
2405
     * @return bool
2406
     */
2407
    public function isSoftDelete() : bool
2408
    {
2409
        return $this->_soft_deletes;
2410
    }
2411
2412
    /**
2413
     * @param bool|false $force
2414
     * @return FluentPdoModel|$this
2415
     * @throws Exception
2416
     */
2417
    public function truncate(bool $force=false) : FluentPdoModel
2418
    {
2419
        if ( $force )
2420
        {
2421
            $this->execute('SET FOREIGN_KEY_CHECKS = 0');
2422
        }
2423
        $this->execute("TRUNCATE TABLE {$this->_table_name}");
2424
        if ( $force )
2425
        {
2426
            $this->execute('SET FOREIGN_KEY_CHECKS = 1');
2427
        }
2428
2429
        return $this;
2430
    }
2431
2432
    /**
2433
     * @return array
2434
     */
2435
    public function deleteSqlQuery() : array
2436
    {
2437
        $query  = "DELETE FROM {$this->_table_name}";
2438
        if ( !empty($this->_where_conditions) )
2439
        {
2440
            $query .= $this->_getWhereString(true);
2441
2442
            return [$query, $this->_getWhereParameters()];
2443
        }
2444
2445
        return [$query, []];
2446
    }
2447
2448
2449
    /**
2450
     * Return the aggregate count of column
2451
     *
2452
     * @param string $column
2453
     * @param int $cacheTtl
2454
     * @return float
2455
     */
2456
    public function count(string $column='*', int $cacheTtl=self::CACHE_NO) : float
2457
    {
2458
        $this->explicitSelectMode();
2459
2460
        if ( empty($this->_group_by) )
2461
        {
2462
            return $this->fetchFloat("COUNT({$column}) AS cnt", 0, $cacheTtl);
2463
        }
2464
        $this->select("COUNT({$column}) AS cnt");
2465
        $sql        = $this->getSelectQuery();
2466
        $params     = $this->_getWhereParameters();
2467
        $sql        = "SELECT COUNT(*) AS cnt FROM ({$sql}) t";
2468
        $object     = $this->query($sql, $params)->fetchOne(0, $cacheTtl);
2469
        if ( ! $object || empty($object->cnt) )
2470
        {
2471
            return 0.0;
2472
        }
2473
2474
        return (float)$object->cnt;
2475
    }
2476
2477
2478
    /**
2479
     * Return the aggregate max count of column
2480
     *
2481
     * @param string $column
2482
     * @param int $cacheTtl
2483
     * @return int|float|string|null
2484
     */
2485
    public function max(string $column, int $cacheTtl=self::CACHE_NO)
2486
    {
2487
        return $this
2488
            ->explicitSelectMode()
2489
            ->fetchField("MAX({$column}) AS max", 0, $cacheTtl);
2490
    }
2491
2492
2493
    /**
2494
     * Return the aggregate min count of column
2495
     *
2496
     * @param string $column
2497
     * @param int $cacheTtl
2498
     * @return int|float|string|null
2499
     */
2500
    public function min(string $column, int $cacheTtl=self::CACHE_NO)
2501
    {
2502
        return $this
2503
            ->explicitSelectMode()
2504
            ->fetchField("MIN({$column}) AS min", 0, $cacheTtl);
2505
    }
2506
2507
    /**
2508
     * Return the aggregate sum count of column
2509
     *
2510
     * @param string $column
2511
     * @param int $cacheTtl
2512
     * @return int|float|string|null
2513
     */
2514
    public function sum(string $column, int $cacheTtl=self::CACHE_NO)
2515
    {
2516
        return $this
2517
            ->explicitSelectMode()
2518
            ->fetchField("SUM({$column}) AS sum", 0, $cacheTtl);
2519
    }
2520
2521
    /**
2522
     * Return the aggregate average count of column
2523
     *
2524
     * @param string $column
2525
     * @param int $cacheTtl
2526
     * @return int|float|string|null
2527
     */
2528
    public function avg(string $column, int $cacheTtl=self::CACHE_NO)
2529
    {
2530
        return $this
2531
            ->explicitSelectMode()
2532
            ->fetchField("AVG({$column}) AS avg", 0, $cacheTtl);
2533
    }
2534
2535
    /*******************************************************************************/
2536
// Utilities methods
2537
2538
    /**
2539
     * Reset fields
2540
     *
2541
     * @return FluentPdoModel|$this
2542
     */
2543
    public function reset() : FluentPdoModel
2544
    {
2545
        $this->_where_parameters        = [];
2546
        $this->_select_fields           = [];
2547
        $this->_join_sources            = [];
2548
        $this->_join_aliases            = [];
2549
        $this->_where_conditions        = [];
2550
        $this->_limit                   = 0;
2551
        $this->_offset                  = 0;
2552
        $this->_order_by                = [];
2553
        $this->_group_by                = [];
2554
        $this->_and_or_operator         = self::OPERATOR_AND;
2555
        $this->_having                  = [];
2556
        $this->_wrap_open               = false;
2557
        $this->_last_wrap_position      = 0;
2558
        $this->_pdo_stmt                = null;
2559
        $this->_distinct                = false;
2560
        $this->_requested_fields        = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type null of property $_requested_fields.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2561
        $this->_filter_meta             = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type null of property $_filter_meta.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2562
        $this->_cache_ttl               = -1;
2563
        $this->_timer                   = [];
2564
        $this->_built_query             = '';
2565
        $this->_paging_meta             = [];
2566
        $this->_raw_sql                 = null;
2567
        $this->_explicit_select_mode    = false;
2568
2569
        return $this;
2570
    }
2571
2572
2573
    /**
2574
     * @return FluentPdoModel|$this
2575
     */
2576
    public function removeUnauthorisedFields() : FluentPdoModel
2577
    {
2578
        return $this;
2579
    }
2580
2581
    /**
2582
     * @return Closure[]
2583
     */
2584
    protected function _getFieldHandlers() : array
2585
    {
2586
        $columns = $this->getColumns(true);
2587
        if ( empty($columns) )
2588
        {
2589
            return [];
2590
        }
2591
2592
        return [
2593
            'id' => function(string $field, $value, string $type='', stdClass $record=null) {
2594
2595
                unset($record);
2596
                $value = $this->_fixType($field, $value);
2597
                if ( $type === self::SAVE_INSERT )
2598
                {
2599
                    Validate($value)->fieldName($field)->nullOr()->id('ID must be a valid integer id, (%s) submitted.');
2600
                    return $value;
2601
                }
2602
                Validate($value)->fieldName($field)->id('ID must be a valid integer id, (%s) submitted.');
2603
                return $value;
2604
            },
2605
            self::CREATOR_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2606
2607
                unset($type, $record);
2608
                $value = $this->_fixType($field, $value);
2609
                // Created user id is set to current user if record is an insert or deleted if not (unless override is true)
2610
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2611
                Validate($value)->fieldName($field)->id('Created By must be a valid integer id, (%s) submitted.');
2612
                return $value;
2613
            },
2614
            self::CREATED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2615
2616
                unset($type, $record);
2617
                $value = $this->_fixType($field, $value);
2618
                // Created ts is set to now if record is an insert or deleted if not (unless override is true)
2619
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2620
                Validate($value)->fieldName($field)->date('Created must be a valid timestamp, (%s) submitted.');
2621
                return $value;
2622
            },
2623
            self::MODIFIER_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2624
2625
                unset($type, $record);
2626
                $value = $this->_fixType($field, $value);
2627
                // Modified user id is set to current user (unless override is true)
2628
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2629
                Validate($value)->fieldName($field)->id('Modified By must be a valid integer id, (%s) submitted.');
2630
                return $value;
2631
            },
2632
            self::MODIFIED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2633
2634
                unset($type, $record);
2635
                $value = $this->_fixType($field, $value);
2636
                // Modified timestamps are set to now (unless override is true)
2637
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2638
                Validate($value)->fieldName($field)->date('Modified must be a valid timestamp, (%s) submitted.');
2639
                return $value;
2640
            },
2641
            self::DELETER_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2642
2643
                if ( $type === self::SAVE_INSERT )
2644
                {
2645
                    return null;
2646
                }
2647
                if ( empty($record->deleted_ts) )
2648
                {
2649
                    return null;
2650
                }
2651
                unset($type, $record);
2652
                $value = $this->_fixType($field, $value);
2653
2654
                // Modified user id is set to current user (unless override is true)
2655
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2656
                Validate($value)->fieldName($field)->nullOr()->id('Deleter must be a valid integer id, (%s) submitted.');
2657
                return $value;
2658
            },
2659
            self::DELETED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2660
2661
                if ( $type === self::SAVE_INSERT )
2662
                {
2663
                    return null;
2664
                }
2665
                unset($type, $record);
2666
                $value = $this->_fixType($field, $value);
2667
                if ( $value )
2668
                {
2669
                    $value = static::dateTime($this->_allow_meta_override ? $value : null);
2670
                    Validate($value)->fieldName($field)->date('Deleted Timestamp must be a valid timestamp, (%s) submitted.');
2671
                }
2672
2673
                return $value;
2674
            },
2675
        ];
2676
    }
2677
2678
    /**
2679
     * @return bool
2680
     */
2681
    public function begin() : bool
2682
    {
2683
        $pdo        = $this->getPdo();
2684
        $oldDepth   = $pdo->getTransactionDepth();
2685
        $res        = $pdo->beginTransaction();
2686
        $newDepth   = $pdo->getTransactionDepth();
2687
        $this->getLogger()->debug("Calling db begin transaction", [
2688
            'old_depth'     => $oldDepth,
2689
            'new_depth'     => $newDepth,
2690
            'trans_started' => $newDepth === 1 ? true : false,
2691
        ]);
2692
2693
        return $res;
2694
    }
2695
2696
    /**
2697
     * @return bool
2698
     */
2699
    public function commit() : bool
2700
    {
2701
        $pdo        = $this->getPdo();
2702
        $oldDepth   = $pdo->getTransactionDepth();
2703
        $res        = $pdo->commit();
2704
        $newDepth   = $pdo->getTransactionDepth();
2705
        $this->getLogger()->debug("Calling db commit transaction", [
2706
            'old_depth'     => $oldDepth,
2707
            'new_depth'     => $newDepth,
2708
            'trans_ended'   => $newDepth === 0 ? true : false,
2709
        ]);
2710
        if ( ! $res )
2711
        {
2712
            return false;
2713
        }
2714
2715
        return $res === 0 ? true : $res;
2716
    }
2717
2718
    /**
2719
     * @return bool
2720
     */
2721
    public function rollback() : bool
2722
    {
2723
        $pdo        = $this->getPdo();
2724
        $oldDepth   = $pdo->getTransactionDepth();
2725
        $res        = $pdo->rollback();
2726
        $newDepth   = $pdo->getTransactionDepth();
2727
        $this->getLogger()->debug("Calling db rollback transaction", [
2728
            'old_depth'     => $oldDepth,
2729
            'new_depth'     => $newDepth,
2730
            'trans_ended'   => $newDepth === 0 ? true : false,
2731
        ]);
2732
2733
        return $res;
2734
    }
2735
2736
    /**
2737
     * @param stdClass $record
2738
     * @param  string  $type
2739
     * @return stdClass
2740
     */
2741
    public function applyGlobalModifiers(stdClass $record, string $type) : stdClass
2742
    {
2743
        unset($type);
2744
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2745
        {
2746
            if ( is_string($record->{$field}) )
2747
            {
2748
                $record->{$field} = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", $value);
2749
            }
2750
        }
2751
2752
        return $record;
2753
    }
2754
2755
    /**
2756
     * @param stdClass $record
2757
     * @param  string $type
2758
     * @return stdClass
2759
     */
2760
    public function removeUnneededFields(stdClass $record, string $type) : stdClass
2761
    {
2762
        $creator_id     = self::CREATOR_ID_FIELD;
2763
        $created_ts     = self::CREATED_TS_FIELD;
2764
2765
        // remove un-needed fields
2766
        $columns = $this->getColumns(true);
2767
        if ( empty($columns) )
2768
        {
2769
            return $record;
2770
        }
2771
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2772
        {
2773
            if ( ! in_array($name, $columns) || in_array($name, $this->_virtual_fields) )
2774
            {
2775
                unset($record->{$name});
2776
            }
2777
        }
2778
        if ( property_exists($record, $created_ts) && $type !== 'INSERT' && ! $this->_allow_meta_override )
2779
        {
2780
            unset($record->{$created_ts});
2781
        }
2782
        if ( property_exists($record, $creator_id) && $type !== 'INSERT' && ! $this->_allow_meta_override )
2783
        {
2784
            unset($record->{$creator_id});
2785
        }
2786
2787
        return $record;
2788
    }
2789
2790
2791
    /**
2792
     * @param array $ids
2793
     * @param array $values
2794
     * @param int   $batch
2795
     * @return bool
2796
     */
2797
    public function setById(array $ids, array $values, int $batch=1000) : bool
2798
    {
2799
        $ids        = array_unique($ids);
2800
        if ( empty($ids) )
2801
        {
2802
            return true;
2803
        }
2804
        if ( count($ids) <= $batch )
2805
        {
2806
            return (bool)$this->whereIn('id', $ids)->updateArr($values);
2807
        }
2808
        while ( ! empty($ids) )
2809
        {
2810
            $thisBatch  = array_slice($ids, 0, $batch);
2811
            $ids        = array_diff($ids, $thisBatch);
2812
            $this->reset()->whereIn('id', $thisBatch)->updateArr($values);
2813
        }
2814
2815
        return true;
2816
    }
2817
2818
2819
    /**
2820
     * @param string $displayColumnValue
2821
     * @return int
2822
     */
2823
    public function resolveId(string $displayColumnValue) : int
2824
    {
2825
        $displayColumn  = $this->getDisplayColumn();
2826
        $className      = get_class($this);
2827
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
2828
2829
        return $this
2830
            ->reset()
2831
            ->where($displayColumn, $displayColumnValue)
2832
            ->fetchInt('id', 0, self::ONE_HOUR);
2833
    }
2834
2835
    /**
2836
     * @param int   $resourceId
2837
     * @param array $query
2838
     * @param array $extraFields
2839
     * @param int $cacheTtl
2840
     * @return array
2841
     */
2842
    public function fetchApiResource(int $resourceId, array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO) : array
2843
    {
2844
        Assert($resourceId)->id();
2845
2846
        $query['_limit']    = 1;
2847
        $pagingMetaData        = $this->wherePk($resourceId)->_prepareApiResource($query, $extraFields);
2848
        if ( $pagingMetaData['total'] === 0 )
2849
        {
2850
            return [[], $pagingMetaData];
2851
        }
2852
2853
        return [$this->fetchOne($resourceId, $cacheTtl), $pagingMetaData];
2854
    }
2855
2856
    /**
2857
     * @param array     $query
2858
     * @param array     $extraFields
2859
     * @param int       $cacheTtl
2860
     * @param string    $permEntity
2861
     * @return array
2862
     */
2863
    public function fetchApiResources(array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO, string $permEntity='') : array
2864
    {
2865
        $pagingMetaData    = $this->_prepareApiResource($query, $extraFields);
2866
        if ( $pagingMetaData['total'] === 0 )
2867
        {
2868
            return [[], $pagingMetaData];
2869
        }
2870
        $results = $this->fetch('', $cacheTtl);
2871
        if ( ! $permEntity )
2872
        {
2873
            return [$results, $pagingMetaData];
2874
        }
2875
        foreach ( $results as $record )
2876
        {
2877
            if ( ! empty($record->id) )
2878
            {
2879
                $pagingMetaData['perms'][(int)$record->id] = $this->getMaskByResourceAndId($permEntity, $record->id);
2880
            }
2881
        }
2882
2883
        return [$results, $pagingMetaData];
2884
    }
2885
2886
2887
    /**
2888
     * @return array
2889
     */
2890
    public function getSearchableAssociations() : array
2891
    {
2892
        $belongsTo = ! empty($this->_associations['belongsTo']) ? $this->_associations['belongsTo'] : [];
2893
        unset($belongsTo['CreatedBy'], $belongsTo['ModifiedBy']);
2894
2895
        return $belongsTo;
2896
    }
2897
2898
    /**
2899
     * @param array $fields
2900
     */
2901
    public function removeUnrequestedFields(array $fields)
2902
    {
2903
        foreach ( $this->_select_fields as $idx => $field )
2904
        {
2905
            $field = trim(static::after(' AS ', $field, true));
2906
            if ( ! in_array($field, $fields) )
2907
            {
2908
                unset($this->_select_fields[$idx]);
2909
            }
2910
        }
2911
    }
2912
2913
    /**
2914
     * @param array $removeFields
2915
     */
2916
    public function removeFields(array $removeFields=[])
2917
    {
2918
        $searches = [];
2919
        foreach ( $removeFields as $removeField )
2920
        {
2921
            $removeField    = str_replace("{$this->_table_alias}.", '', $removeField);
2922
            $searches[]     = "{$this->_table_alias}.{$removeField}";
2923
            $searches[]     = $removeField;
2924
        }
2925
        foreach ( $this->_select_fields as $idx => $selected )
2926
        {
2927
            $selected = stripos($selected, ' AS ') !== false ? preg_split('/ as /i', $selected) : [$selected];
2928
            foreach ( $selected as $haystack )
2929
            {
2930
                foreach ( $searches as $search )
2931
                {
2932
                    if ( trim($haystack) === trim($search) )
2933
                    {
2934
                        unset($this->_select_fields[$idx]);
2935
                        continue;
2936
                    }
2937
                }
2938
            }
2939
        }
2940
    }
2941
2942
    /**
2943
     * @return FluentPdoModel|$this
2944
     */
2945
    public function defaultFilters() : FluentPdoModel
2946
    {
2947
        return $this;
2948
    }
2949
2950
    /**
2951
     * @param bool $allow
2952
     *
2953
     * @return FluentPdoModel|$this
2954
     */
2955
    public function allowMetaColumnOverride(bool $allow=false) : FluentPdoModel
2956
    {
2957
        $this->_allow_meta_override = $allow;
2958
2959
        return $this;
2960
    }
2961
2962
    /**
2963
     * @param bool $skip
2964
     *
2965
     * @return FluentPdoModel|$this
2966
     */
2967
    public function skipMetaUpdates(bool $skip=true) : FluentPdoModel
2968
    {
2969
        $this->_skip_meta_updates = $skip;
2970
2971
        return $this;
2972
    }
2973
2974
    /**
2975
     * @param bool $add
2976
     *
2977
     * @return FluentPdoModel|$this
2978
     */
2979
    public function addUpdateAlias(bool $add=true) : FluentPdoModel
2980
    {
2981
        $this->_add_update_alias = $add;
2982
2983
        return $this;
2984
    }
2985
2986
    /**
2987
     * @param stdClass $record
2988
     * @return stdClass
2989
     */
2990
    public function onFetch(stdClass $record) : stdClass
2991
    {
2992
        $record     = $this->_trimAndLowerCaseKeys($record);
2993
        if ( $this->_filter_on_fetch )
2994
        {
2995
            $record     = $this->cleanseRecord($record);
2996
        }
2997
2998
        $record     =  $this->fixTypesToSentinel($record);
2999
3000
        return $this->fixTimestamps($record);
3001
    }
3002
3003
    /**
3004
     * @param $value
3005
     * @return string
3006
     */
3007
    public function gzEncodeData(string $value) : string
3008
    {
3009
        if ( $this->_hasGzipPrefix($value) )
3010
        {
3011
            return $value;
3012
        }
3013
3014
        return static::GZIP_PREFIX . base64_encode(gzencode($value, 9));
3015
    }
3016
3017
    /**
3018
     * @param $value
3019
     * @return mixed|string
3020
     */
3021
    public function gzDecodeData(string $value) : string
3022
    {
3023
        if ( ! $this->_hasGzipPrefix($value) )
3024
        {
3025
            return $value;
3026
        }
3027
        $value = substr_replace($value, '', 0, strlen(static::GZIP_PREFIX));
3028
3029
        return gzdecode(base64_decode($value));
3030
    }
3031
3032
    /**
3033
     * @param $value
3034
     * @return bool
3035
     */
3036
    protected function _hasGzipPrefix(string $value) : bool
3037
    {
3038
        return substr($value, 0, strlen(static::GZIP_PREFIX)) === static::GZIP_PREFIX ? true : false;
3039
    }
3040
3041
    /**
3042
     * @param stdClass $record
3043
     * @return stdClass
3044
     */
3045
    public function fixTimestamps(stdClass $record) : stdClass
3046
    {
3047
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3048
        {
3049
            if ( preg_match('/_ts$/', $field) )
3050
            {
3051
                $record->{$field} = empty($value) ? $value : static::atom($value);
3052
            }
3053
        }
3054
3055
        return $record;
3056
    }
3057
3058
    /**
3059
     * @param int $max
3060
     * @return FluentPdoModel|$this
3061
     */
3062
    public function setMaxRecords(int $max) : FluentPdoModel
3063
    {
3064
        Assert($max)->int();
3065
        $this->_default_max = $max;
3066
3067
        return $this;
3068
    }
3069
3070
3071
    /**
3072
     * @param stdClass $record
3073
     * @param string   $type
3074
     * @return stdClass
3075
     */
3076
    public function afterSave(stdClass $record, string $type) : stdClass
3077
    {
3078
        unset($type);
3079
        $this->clearCacheByTable();
3080
        foreach ( $record as $column => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3081
        {
3082
            if ( !empty($record->{$column}) )
3083
            {
3084
                if ( preg_match('/_ts$/', $column) )
3085
                {
3086
                    $record->{$column} = static::atom($value);
3087
                }
3088
                if ( preg_match('/_am$/', $column) )
3089
                {
3090
                    $record->{$column} = number_format($value, 2, '.', '');
3091
                }
3092
            }
3093
        }
3094
3095
        return $record;
3096
    }
3097
3098
    /**
3099
     * @param stdClass $record
3100
     * @param string $type
3101
     * @return stdClass
3102
     */
3103
    public function addDefaultFields(stdClass $record, string $type) : stdClass
3104
    {
3105
        $columns            = $this->getColumns(true);
3106
        if ( empty($columns) )
3107
        {
3108
            return $record;
3109
        }
3110
        $defaults           = [
3111
            self::SAVE_UPDATE   => [
3112
                self::MODIFIER_ID_FIELD        => null,
3113
                self::MODIFIED_TS_FIELD        => null,
3114
            ],
3115
            self::SAVE_INSERT   => [
3116
                self::CREATOR_ID_FIELD         => null,
3117
                self::CREATED_TS_FIELD         => null,
3118
                self::MODIFIER_ID_FIELD        => null,
3119
                self::MODIFIED_TS_FIELD        => null,
3120
            ]
3121
        ];
3122
        if ( $this->_skip_meta_updates )
3123
        {
3124
            $defaults[self::SAVE_UPDATE] = [];
3125
        }
3126
        $columns            = array_flip($this->getColumns());
3127
        $defaults           = array_intersect_key($defaults[$type], $columns);
3128
        foreach ( $defaults as $column => $def )
3129
        {
3130
            $record->{$column} = $record->{$column} ?? $def;
3131
        }
3132
        unset($record->active);
3133
3134
        return $record;
3135
    }
3136
3137
3138
    /**
3139
     * @return bool
3140
     */
3141
    public function createTable() : bool
3142
    {
3143
        return true;
3144
    }
3145
3146
    /**
3147
     * @param bool|false $force
3148
     * @return FluentPdoModel|$this
3149
     * @throws Exception
3150
     */
3151
    public function dropTable(bool $force=false) : FluentPdoModel
0 ignored issues
show
Unused Code introduced by
The parameter $force 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...
3152
    {
3153
        return $this;
3154
    }
3155
3156
    protected function _compileHandlers()
3157
    {
3158
        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...
3159
        {
3160
            return;
3161
        }
3162
        $parentHandlers      = self::_getFieldHandlers();
3163
        $this->_handlers    = array_merge($parentHandlers, $this->_getFieldHandlers());
3164
    }
3165
3166
    /**
3167
     * @param string $viewName
3168
     * @param int $cacheTtl
3169
     * @return array
3170
     */
3171
    public function getViewColumns($viewName, $cacheTtl=self::CACHE_NO)
3172
    {
3173
        return $this->_getColumnsByTableFromDb($viewName, $cacheTtl);
3174
    }
3175
3176
    /**
3177
     * @param int $id
3178
     * @return string
3179
     */
3180
    public function getDisplayNameById(int $id) : string
3181
    {
3182
        $displayColumn  = $this->getDisplayColumn();
3183
        $className      = get_class($this);
3184
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
3185
3186
        return $this
3187
            ->reset()
3188
            ->fetchStr($displayColumn, $id, self::ONE_HOUR);
3189
    }
3190
3191
    /**
3192
     * @param int $id
3193
     * @param string $displayColumnValue
3194
     * @return bool
3195
     */
3196
    public function validIdDisplayNameCombo(int $id, $displayColumnValue) : bool
3197
    {
3198
        return $displayColumnValue === $this->getDisplayNameById($id);
3199
    }
3200
3201
    /**
3202
     * @param array $toPopulate
3203
     * @return stdClass
3204
     */
3205
    protected function getEmptyObject(array $toPopulate=[]) : stdClass
3206
    {
3207
        $toPopulate[]   = 'id';
3208
3209
        return (object)array_flip($toPopulate);
3210
    }
3211
3212
    /**
3213
     * @param array $toPopulate
3214
     * @return stdClass
3215
     */
3216
    protected static function emptyObject(array $toPopulate=[]) : stdClass
3217
    {
3218
        $toPopulate[]   = 'id';
3219
3220
        return (object)array_flip($toPopulate);
3221
    }
3222
3223
    /**
3224
     * @param int $id
3225
     * @return bool
3226
     */
3227
    public static function isId(int $id) : bool
3228
    {
3229
        return $id > 0;
3230
    }
3231
3232
    /**
3233
     * @param int $cacheTtl
3234
     * @return int
3235
     */
3236
    public function activeCount(int $cacheTtl=self::CACHE_NO) : int
3237
    {
3238
        return (int)$this->whereActive()->count('*', $cacheTtl);
3239
    }
3240
3241
    /**
3242
     * @param string        $tableAlias
3243
     * @param string   $columnName
3244
     * @return FluentPdoModel|$this
3245
     */
3246
    public function whereActive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3247
    {
3248
        return $this->whereStatus(static::ACTIVE, $tableAlias, $columnName);
3249
    }
3250
3251
    /**
3252
     * @param string        $tableAlias
3253
     * @param string        $columnName
3254
     * @return FluentPdoModel|$this
3255
     */
3256
    public function whereInactive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3257
    {
3258
        return $this->whereStatus(static::INACTIVE, $tableAlias, $columnName);
3259
    }
3260
3261
    /**
3262
     * @param string        $tableAlias
3263
     * @param string        $columnName
3264
     * @return FluentPdoModel|$this
3265
     */
3266
    public function whereArchived(string $tableAlias='', string $columnName='status') : FluentPdoModel
3267
    {
3268
        return $this->whereStatus(static::ARCHIVED, $tableAlias, $columnName);
3269
    }
3270
3271
    /**
3272
     * @param int $status
3273
     * @param string $tableAlias
3274
     * @param string $columnName
3275
     * @return FluentPdoModel|$this
3276
     */
3277
    public function whereStatus(int $status, string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3278
    {
3279
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3280
3281
        $tableAlias = empty($tableAlias) ? $this->getTableAlias() : $tableAlias;
3282
        $field      = empty($tableAlias) ? $columnName : "{$tableAlias}.{$columnName}";
3283
3284
        return $this->where($field, $status);
3285
    }
3286
3287
    /**
3288
     * @param int $id
3289
     * @return int
3290
     */
3291
    public function updateActive(int $id=0) : int
3292
    {
3293
        Assert($id)->unsignedInt();
3294
        if ( $id )
3295
        {
3296
            $this->wherePk($id);
3297
        }
3298
3299
        return $this->updateStatus(static::ACTIVE);
3300
    }
3301
3302
    /**
3303
     * @param int $id
3304
     * @return int
3305
     */
3306
    public function updateInactive(int $id=0) : int
3307
    {
3308
        Assert($id)->unsignedInt();
3309
        if ( $id )
3310
        {
3311
            $this->wherePk($id);
3312
        }
3313
        return $this->updateStatus(static::INACTIVE);
3314
    }
3315
3316
    /**
3317
     * @param string $field
3318
     * @param int  $id
3319
     * @return int
3320
     */
3321
    public function updateNow(string $field, int $id=0) : int
3322
    {
3323
        Assert($field)->notEmpty();
3324
3325
        return $this->updateField($field, date('Y-m-d H:i:s'), $id);
3326
    }
3327
3328
    /**
3329
     * @param string $field
3330
     * @param int  $id
3331
     * @return int
3332
     */
3333
    public function updateToday($field, int $id=0) : int
3334
    {
3335
        Assert($field)->notEmpty();
3336
3337
        return $this->updateField($field, date('Y-m-d'), $id);
3338
    }
3339
3340
    /**
3341
     * @param int $id
3342
     * @return int
3343
     */
3344
    public function updateDeleted(int $id=0) : int
3345
    {
3346
        Assert($id)->unsignedInt();
3347
        if ( $id )
3348
        {
3349
            $this->wherePk($id);
3350
        }
3351
3352
        return $this->update((object)[
3353
            self::DELETER_ID_FIELD  => $this->getUserId(),
3354
            self::DELETED_TS_FIELD  => static::dateTime(),
3355
        ]);
3356
    }
3357
3358
    /**
3359
     * @param int $id
3360
     * @return int
3361
     */
3362
    public function updateArchived(int $id=0) : int
3363
    {
3364
        Assert($id)->unsignedInt();
3365
        if ( $id )
3366
        {
3367
            $this->wherePk($id);
3368
        }
3369
3370
        return $this->updateStatus(static::ARCHIVED);
3371
    }
3372
3373
    /**
3374
     * @param int $status
3375
     * @return int
3376
     * @throws \Exception
3377
     */
3378
    public function updateStatus(int $status)
3379
    {
3380
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3381
3382
        return $this->updateField('status', $status);
3383
    }
3384
3385
    /**
3386
     * Return a YYYY-MM-DD HH:II:SS date format
3387
     *
3388
     * @param string $datetime - An english textual datetime description
3389
     *          now, yesterday, 3 days ago, +1 week
3390
     *          http://php.net/manual/en/function.strtotime.php
3391
     * @return string YYYY-MM-DD HH:II:SS
3392
     */
3393
    public static function NOW(string $datetime='now') : string
3394
    {
3395
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
3396
    }
3397
3398
    /**
3399
     * Return a string containing the given number of question marks,
3400
     * separated by commas. Eg '?, ?, ?'
3401
     *
3402
     * @param int - total of placeholder to insert
3403
     * @return string
3404
     */
3405
    protected function _makePlaceholders(int $numberOfPlaceholders=1) : string
3406
    {
3407
        return implode(', ', array_fill(0, $numberOfPlaceholders, '?'));
3408
    }
3409
3410
    /**
3411
     * Format the table{Primary|Foreign}KeyName
3412
     *
3413
     * @param  string $pattern
3414
     * @param  string $tableName
3415
     * @return string
3416
     */
3417
    protected function _formatKeyName(string $pattern, string $tableName) : string
3418
    {
3419
        return sprintf($pattern, $tableName);
3420
    }
3421
3422
    /**
3423
     * @param array $query
3424
     * @param array $extraFields
3425
     * @return array
3426
     * @throws \Exception
3427
     */
3428
    protected function _prepareApiResource(array $query=[], array $extraFields=[]) : array
3429
    {
3430
        $this->defaultFilters()->filter($query)->paginate($query);
3431
        $pagingMetaData    = $this->getPagingMeta();
3432
        if ( $pagingMetaData['total'] === 0 )
3433
        {
3434
            return $pagingMetaData;
3435
        }
3436
        $this->withBelongsTo($pagingMetaData['fields']);
3437
        if ( ! empty($extraFields) )
3438
        {
3439
            $this->select($extraFields, '', false);
3440
        }
3441
        $this->removeUnauthorisedFields();
0 ignored issues
show
Unused Code introduced by
The call to the method Terah\FluentPdoModel\Flu...oveUnauthorisedFields() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
3442
        if ( ! empty($pagingMetaData['fields']) )
3443
        {
3444
            $this->removeUnrequestedFields($pagingMetaData['fields']);
3445
        }
3446
3447
        return $pagingMetaData;
3448
    }
3449
3450
    /**
3451
     * @param string $query
3452
     * @param array $parameters
3453
     *
3454
     * @return array
3455
     */
3456
    protected function _logQuery(string $query, array $parameters) : array
3457
    {
3458
        $query                  = $this->buildQuery($query, $parameters);
3459
        if ( ! $this->_log_queries )
3460
        {
3461
            return ['', ''];
3462
        }
3463
        $ident                  = substr(str_shuffle(md5($query)), 0, 10);
3464
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
3465
        $this->_timer['start']  = microtime(true);
3466
3467
        return [$query, $ident];
3468
    }
3469
3470
    /**
3471
     * @param string $ident
3472
     * @param string $builtQuery
3473
     */
3474
    protected function _logSlowQueries(string $ident, string $builtQuery)
3475
    {
3476
        if ( ! $this->_log_queries )
3477
        {
3478
            return ;
3479
        }
3480
        $this->_timer['end']    = microtime(true);
3481
        $seconds_taken          = round($this->_timer['end'] - $this->_timer['start'], 3);
3482
        if ( $seconds_taken > $this->_slow_query_secs )
3483
        {
3484
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$seconds_taken} seconds:\n{$builtQuery}");
3485
        }
3486
    }
3487
3488
    /**
3489
     * @return float
3490
     */
3491
    public function getTimeTaken() : float
3492
    {
3493
        $secondsTaken = $this->_timer['end'] - $this->_timer['start'];
3494
3495
        return (float)$secondsTaken;
3496
    }
3497
3498
    /**
3499
     * @param $secs
3500
     * @return FluentPdoModel|$this
3501
     */
3502
    public function slowQuerySeconds(int $secs) : FluentPdoModel
3503
    {
3504
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
3505
        $this->_slow_query_secs = $secs;
3506
3507
        return $this;
3508
    }
3509
3510
3511
    /**
3512
     * @param       $field
3513
     * @param array $values
3514
     * @param string  $placeholderPrefix
3515
     *
3516
     * @return array
3517
     */
3518
    public function getNamedWhereIn(string $field, array $values, string $placeholderPrefix='') : array
3519
    {
3520
        Assert($field)->string()->notEmpty();
3521
        Assert($values)->isArray();
3522
3523
        if ( empty($values) )
3524
        {
3525
            return ['', []];
3526
        }
3527
        $placeholderPrefix      = $placeholderPrefix ?: strtolower(str_replace('.', '__', $field));
3528
        $params                 = [];
3529
        $placeholders           = [];
3530
        $count                  = 1;
3531
        foreach ( $values as $val )
3532
        {
3533
            $name                   = "{$placeholderPrefix}_{$count}";
3534
            $params[$name]          = $val;
3535
            $placeholders[]         = ":{$name}";
3536
            $count++;
3537
        }
3538
        $placeholders           = implode(',', $placeholders);
3539
3540
        return ["AND {$field} IN ({$placeholders})\n", $params];
3541
    }
3542
3543
    /**
3544
     * @param string $field
3545
     * @param string $delimiter
3546
     *
3547
     * @return array
3548
     */
3549
    protected function _getColumnAliasParts(string $field, string $delimiter=':') : array
3550
    {
3551
        $parts      = explode($delimiter, $field);
3552
        if ( count($parts) === 2 )
3553
        {
3554
            return $parts;
3555
        }
3556
        $parts = explode('.', $field);
3557
        if ( count($parts) === 2 )
3558
        {
3559
            return $parts;
3560
        }
3561
3562
        return ['', $field];
3563
    }
3564
3565
    /**
3566
     * @param string $column
3567
     * @param string $term
3568
     * @return FluentPdoModel|$this
3569
     */
3570
    protected function _addWhereClause(string $column, string $term) : FluentPdoModel
3571
    {
3572
        /*
3573
3574
         whereLike          i.e ?name=whereLike(%terry%)
3575
         whereNotLike       i.e ?name=whereNotLike(%terry%)
3576
         whereLt            i.e ?age=whereLt(18)
3577
         whereLte           i.e ?age=whereLte(18)
3578
         whereGt            i.e ?event_dt=whereGt(2014-10-10)
3579
         whereGte           i.e ?event_dt=whereGte(2014-10-10)
3580
         whereBetween       i.e ?event_dt=whereBetween(2014-10-10,2014-10-15)
3581
         whereNotBetween    i.e ?event_dt=whereNotBetween(2014-10-10,2014-10-15)
3582
3583
         */
3584
        list ($func, $matches) = $this->parseWhereClause($term);
3585
        switch ($func)
3586
        {
3587
            case 'whereLike':
3588
3589
                return $this->whereLike($column, $matches[0]);
3590
3591
            case 'whereNotLike':
3592
3593
                return $this->whereNotLike($column, $matches[0]);
3594
3595
            case 'whereLt':
3596
3597
                return $this->whereLt($column, $matches[0]);
3598
3599
            case 'whereLte':
3600
3601
                return $this->whereLte($column, $matches[0]);
3602
3603
            case 'whereGt':
3604
3605
                return $this->whereGt($column, $matches[0]);
3606
3607
            case 'whereGte':
3608
3609
                return $this->whereGte($column, $matches[0]);
3610
3611
            case 'whereBetween':
3612
3613
                return $this->whereBetween($column, $matches[0], $matches[1]);
3614
3615
            case 'whereNotBetween':
3616
3617
                return $this->whereNotBetween($column, $matches[0], $matches[1]);
3618
3619
        }
3620
3621
        return $this->where($column, $term);
3622
    }
3623
3624
    /**
3625
     * @param string $term
3626
     * @return array
3627
     */
3628
    public function parseWhereClause(string $term) : array
3629
    {
3630
        $modifiers = [
3631
            'whereLike'         => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3632
            'whereNotLike'      => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3633
            'whereLt'           => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
3634
            'whereLte'          => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
3635
            'whereGt'           => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
3636
            'whereGte'          => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
3637
            'whereBetween'      => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3638
            'whereNotBetween'  => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3639
        ];
3640
3641
        foreach ( $modifiers as $func => $regex )
3642
        {
3643
            if ( preg_match($regex, $term, $matches) )
3644
            {
3645
                array_shift($matches);
3646
3647
                return [$func, $matches];
3648
            }
3649
        }
3650
3651
        return ['', []];
3652
    }
3653
3654
    public function destroy()
3655
    {
3656
        if ( !is_null($this->_pdo_stmt) )
3657
        {
3658
            $this->_pdo_stmt->closeCursor();
3659
        }
3660
        $this->_pdo_stmt    = null;
3661
        $this->_handlers    = [];
3662
    }
3663
3664
    public function __destruct()
3665
    {
3666
        $this->destroy();
3667
    }
3668
3669
    /**
3670
     * Load a model
3671
     *
3672
     * @param string $modelName
3673
     * @param AbstractPdo $connection
3674
     * @return FluentPdoModel|$this
3675
     * @throws ModelNotFoundException
3676
     */
3677
    public static function loadModel(string $modelName, AbstractPdo $connection=null) : FluentPdoModel
3678
    {
3679
        $modelName = static::$_model_namespace . $modelName;
3680
        if ( ! class_exists($modelName) )
3681
        {
3682
            throw new ModelNotFoundException("Failed to find model class {$modelName}.");
3683
        }
3684
3685
        return new $modelName($connection);
3686
    }
3687
3688
    /**
3689
     * Load a model
3690
     *
3691
     * @param string      $tableName
3692
     * @param AbstractPdo $connection
3693
     * @return FluentPdoModel|$this
3694
     */
3695
    public static function loadTable(string $tableName, AbstractPdo $connection=null) : FluentPdoModel
3696
    {
3697
        $modelName     = Inflector::classify($tableName);
3698
        Assert($modelName)->notEmpty("Could not resolve model name from table name.");
3699
3700
        return static::loadModel($modelName, $connection);
3701
    }
3702
3703
    /**
3704
     * @param string   $columnName
3705
     * @param int $cacheTtl
3706
     * @param bool $flushCache
3707
     * @return bool
3708
     */
3709
    public function columnExists(string $columnName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3710
    {
3711
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3712
3713
        return array_key_exists($columnName, $columns);
3714
    }
3715
3716
    /**
3717
     * @param string   $foreignKeyName
3718
     * @param int $cacheTtl
3719
     * @param bool $flushCache
3720
     * @return bool
3721
     */
3722
    public function foreignKeyExists(string $foreignKeyName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3723
    {
3724
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3725
3726
        return array_key_exists($foreignKeyName, $columns);
3727
    }
3728
3729
    /**
3730
     * @param string   $indexName
3731
     * @param int $cacheTtl
3732
     * @param bool $flushCache
3733
     * @return bool
3734
     */
3735
    public function indexExists(string $indexName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3736
    {
3737
        Assert($indexName)->string()->notEmpty();
3738
3739
        $callback = function() use ($indexName) {
3740
3741
            $index = $this->execute("SHOW INDEX FROM {$this->_table_name} WHERE Key_name = ':indexName'", compact('indexName'));
3742
3743
            return $index ? true : false;
3744
        };
3745
        if  ( $cacheTtl === self::CACHE_NO )
3746
        {
3747
            return $callback();
3748
        }
3749
        $cacheKey   = '/column_schema/' . $this->_table_name . '/index/' . $indexName;
3750
        if ( $flushCache === true )
3751
        {
3752
            $this->clearCache($cacheKey);
3753
        }
3754
3755
        return (bool)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3756
    }
3757
3758
3759
3760
    /**
3761
     * @param int $cacheTtl
3762
     * @param bool $flushCache
3763
     * @return FluentPdoModel|$this
3764
     */
3765
    public function loadSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : FluentPdoModel
3766
    {
3767
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
3768
        $this->schema($schema);
3769
3770
        return $this;
3771
    }
3772
3773
    /**
3774
     * @param int $cacheTtl
3775
     * @param bool $flushCache
3776
     * @return Column[][]
3777
     */
3778
    public function getSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3779
    {
3780
        $table      = $this->getTableName();
3781
        Assert($table)->string()->notEmpty();
3782
        $schema     = [];
3783
        $columns    = $this->_getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
3784
        foreach ( $columns[$table] as $column => $meta )
3785
        {
3786
            /** Column $meta */
3787
            $schema[$column] = $meta->dataType;
3788
        }
3789
3790
        return $schema;
3791
    }
3792
3793
    /**
3794
     * @param int $cacheTtl
3795
     * @param bool $flushCache
3796
     * @return array
3797
     */
3798
    public function getForeignKeysFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3799
    {
3800
        $table          = $this->getTableName();
3801
        Assert($table)->string()->notEmpty();
3802
        $schema         = [];
3803
        $foreignKeys    = $this->_getForeignKeysByTableFromDb($table, $cacheTtl, $flushCache);
3804
        foreach ( $foreignKeys[$table] as $key => $meta )
3805
        {
3806
            $schema[$key] = $meta->dataType;
3807
        }
3808
3809
        return $schema;
3810
    }
3811
3812
    /**
3813
     * @param string $table
3814
     * @param int $cacheTtl
3815
     * @param bool $flushCache
3816
     * @return Column[][]
3817
     */
3818
    protected function _getColumnsByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3819
    {
3820
        Assert($table)->string()->notEmpty();
3821
3822
        $callback = function() use ($table) {
3823
3824
            return $this->_connection->getColumns(true, $table);
3825
        };
3826
        $cacheKey   = '/column_schema/' . $table;
3827
        if ( $flushCache === true )
3828
        {
3829
            $this->clearCache($cacheKey);
3830
        }
3831
3832
        return (array)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3833
    }
3834
3835
    /**
3836
     * @param string $table
3837
     * @param int $cacheTtl
3838
     * @param bool $flushCache
3839
     * @return Column[][]
3840
     */
3841
    protected function _getForeignKeysByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3842
    {
3843
        Assert($table)->string()->notEmpty();
3844
3845
        $callback = function() use ($table) {
3846
3847
            return $this->_connection->getForeignKeys($table);
3848
        };
3849
        $cacheKey   = '/foreign_keys_schema/' . $table;
3850
        if ( $flushCache === true )
3851
        {
3852
            $this->clearCache($cacheKey);
3853
        }
3854
3855
        return (array)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3856
    }
3857
3858
    /**
3859
     * @param string $table
3860
     * @return bool
3861
     */
3862
    public function clearSchemaCache(string $table) : bool
3863
    {
3864
        return $this->clearCache('/column_schema/' . $table);
3865
    }
3866
3867
    /**
3868
     * @param stdClass $record
3869
     * @return stdClass
3870
     */
3871
    public function cleanseRecord(stdClass $record) : stdClass
3872
    {
3873
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3874
        {
3875
            if ( is_string($record->{$field}) )
3876
            {
3877
                $record->$field = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", filter_var($record->$field, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES));
3878
                if ( $this->_log_filter_changes && $value !== $record->$field )
3879
                {
3880
                    $table = $this->_table_name ? $this->_table_name : '';
3881
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->$field]);
3882
                }
3883
            }
3884
        }
3885
3886
        return $record;
3887
    }
3888
3889
    /**
3890
     * @param stdClass $record
3891
     * @param string   $type
3892
     * @return stdClass
3893
     */
3894
    public function beforeSave(stdClass $record, string $type) : stdClass
3895
    {
3896
        $record = $this->addDefaultFields($record, $type);
3897
        $record = $this->applyGlobalModifiers($record, $type);
3898
        $record = $this->applyHandlers($record, $type);
3899
        $record = $this->removeUnneededFields($record, $type);
3900
3901
        return $record;
3902
    }
3903
3904
    /**
3905
     * @param array $data
3906
     * @param string $saveType
3907
     * @return array
3908
     */
3909
    public function cleanseWebData(array $data, string $saveType) : array
3910
    {
3911
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
3912
        $columns = $this->getColumns(false);
3913
        if ( empty($columns) )
3914
        {
3915
            return $data;
3916
        }
3917
        foreach ( $data as $field => $val )
3918
        {
3919
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
3920
        }
3921
3922
        return array_intersect_key($data, $columns);
3923
    }
3924
3925
    /**
3926
     * @return array
3927
     */
3928
    public function skeleton() : array
3929
    {
3930
        $skel       = [];
3931
        $columns    = $this->columns(false);
3932
        foreach ( $columns as $column => $type )
3933
        {
3934
            $skel[$column] = null;
3935
        }
3936
3937
        return $skel;
3938
    }
3939
3940
    /**
3941
     * @param bool $toString
3942
     * @return array
3943
     */
3944
    public function getErrors(bool $toString=false) : array
3945
    {
3946
        if ( $toString )
3947
        {
3948
            $errors = [];
3949
            foreach ( $this->_errors as $field => $error )
3950
            {
3951
                $errors[] = implode("\n", $error);
3952
            }
3953
3954
            return implode("\n", $errors);
3955
        }
3956
3957
        return $this->_errors;
3958
    }
3959
3960
    /**
3961
     * @param bool $throw
3962
     * @return FluentPdoModel|$this
3963
     */
3964
    public function validationExceptions(bool $throw=true) : FluentPdoModel
3965
    {
3966
        $this->_validation_exceptions = $throw;
3967
3968
        return $this;
3969
    }
3970
3971
    /**
3972
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
3973
     *
3974
     * @return FluentPdoModel|$this
3975
     * @throws Exception
3976
     */
3977
    public function paginate(array $query=[]) : FluentPdoModel
3978
    {
3979
        $_fields = $_order = $_limit = $_offset = null;
3980
        extract($query);
3981
        $this->_setLimit((int)$_limit, (int)$_offset);
3982
        $this->_setOrderBy((string)$_order);
3983
        $_fields    = is_array($_fields) ? $_fields : (string)$_fields;
3984
        $_fields    = empty($_fields) ? [] : $_fields;
3985
        $_fields    = is_string($_fields) ? explode('|', $_fields) : $_fields;
3986
        $_fields    = empty($_fields) ? [] : $_fields;
3987
        $this->_setFields(is_array($_fields) ? $_fields : explode('|', (string)$_fields));
3988
3989
        return $this;
3990
    }
3991
3992
    /**
3993
     * @param int $limit
3994
     * @param int $offset
3995
     * @return FluentPdoModel|$this
3996
     */
3997
    protected function _setLimit(int $limit=0, int $offset=0) : FluentPdoModel
3998
    {
3999
        $limit      = ! $limit || (int)$limit > (int)$this->_default_max ? (int)$this->_default_max : (int)$limit;
4000
        if ( ! is_numeric($limit) )
4001
        {
4002
            return $this;
4003
        }
4004
        $this->limit((int)$limit);
4005
        if ( $offset && is_numeric($offset) )
4006
        {
4007
            $this->offset((int)$offset);
4008
        }
4009
4010
        return $this;
4011
    }
4012
4013
    /**
4014
     * @param array $fields
4015
     * @return FluentPdoModel|$this
4016
     * @throws Exception
4017
     */
4018
    protected function _setFields(array $fields=[]) : FluentPdoModel
4019
    {
4020
        if ( ! $fields )
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields 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...
4021
        {
4022
            return $this;
4023
        }
4024
        $this->explicitSelectMode();
4025
        $columns    = $this->getColumns();
4026
4027
        foreach ( $fields as $idx => $field )
4028
        {
4029
            list($alias, $field) = $this->_getColumnAliasParts($field);
4030
            $field = $field === '_display_field' ? $this->_display_column : $field;
4031
            // Regular primary table field
4032
            if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
4033
            {
4034
                $this->select("{$this->_table_alias}.{$field}");
4035
                $this->_requested_fields[] = "{$this->_table_alias}.{$field}";
4036
                continue;
4037
            }
4038
            // Reference table field with alias
4039
            if ( ! empty($alias) )
4040
            {
4041
                Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
4042
                Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
4043
                list(, , $join_field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
4044
                $this->autoJoin($alias, static::LEFT_JOIN, false);
4045
                $this->select($join_field, $fieldAlias);
4046
                $this->_requested_fields[] = $fieldAlias;
4047
                continue;
4048
            }
4049
            // Reference table select field without alias
4050
            $belongsTo = array_key_exists('belongsTo', $this->_associations) ?  $this->_associations['belongsTo'] : [];
4051
            foreach ( $belongsTo as $joinAlias => $config )
4052
            {
4053
                list(, , $join_field, $fieldAlias) = $config;
4054
                if ( $field === $fieldAlias )
4055
                {
4056
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
4057
                    $this->select($join_field, $fieldAlias);
4058
                    $this->_requested_fields[] = $fieldAlias;
4059
                    continue;
4060
                }
4061
            }
4062
        }
4063
4064
        return $this;
4065
    }
4066
4067
    /**
4068
     * @param string $orderBy
4069
     * @return FluentPdoModel|$this|FluentPdoModel
4070
     */
4071
    protected function _setOrderBy(string $orderBy='') : FluentPdoModel
4072
    {
4073
        if ( ! $orderBy )
4074
        {
4075
            return $this;
4076
        }
4077
        $columns                    = $this->getColumns();
4078
        list($order, $direction)    = strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
4079
        list($alias, $field)        = $this->_getColumnAliasParts(trim($order), '.');
4080
        $field                      = explode(' ', $field);
4081
        $field                      = trim($field[0]);
4082
        $direction                  = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
4083
        $belongsTo                  = array_key_exists('belongsTo', $this->_associations) ? $this->_associations['belongsTo'] : [];
4084
        // Regular primary table order by
4085
        if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
4086
        {
4087
            return $this->orderBy("{$this->_table_alias}.{$field}", $direction);
4088
        }
4089
        // Reference table order by with alias
4090
        if ( ! empty($alias) )
4091
        {
4092
            Assert($belongsTo)->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
4093
            Assert($field)->eq($belongsTo[$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
4094
4095
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
4096
        }
4097
        // Reference table order by without alias
4098
        foreach ( $belongsTo as $joinAlias => $config )
4099
        {
4100
            if ( $field === $config[3] )
4101
            {
4102
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
4103
            }
4104
        }
4105
4106
        return $this;
4107
    }
4108
4109
    /**
4110
     * @return array
4111
     */
4112
    public function getPagingMeta()
4113
    {
4114
        if ( empty($this->_paging_meta) )
4115
        {
4116
            $this->setPagingMeta();
4117
        }
4118
4119
        return $this->_paging_meta;
4120
    }
4121
4122
    /**
4123
     * @return FluentPdoModel|$this
4124
     */
4125
    public function setPagingMeta() : FluentPdoModel
4126
    {
4127
        $model                  = clone $this;
4128
        $limit                  = intval($this->getLimit());
4129
        $offset                 = intval($this->getOffset());
4130
        $total                  = intval($model->withBelongsTo()->select('')->offset(0)->limit(0)->orderBy()->count());
4131
        unset($model->_handlers, $model); //hhmv mem leak
4132
        $order_bys              = ! is_array($this->_order_by) ? [] : $this->_order_by;
4133
        $this->_paging_meta     = [
4134
            'limit'                 => $limit,
4135
            'offset'                => $offset,
4136
            'page'                  => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
4137
            'pages'                 => $limit === 0 ? 1 : intval(ceil($total / $limit)),
4138
            'order'                 => $order_bys,
4139
            'total'                 => $total = $limit === 1 && $total > 1 ? 1 : $total,
4140
            'filters'               => $this->_filter_meta,
4141
            'fields'                => $this->_requested_fields,
4142
            'perms'                 => [],
4143
        ];
4144
4145
        return $this;
4146
    }
4147
4148
    /**
4149
     * Take a web request and format a query
4150
     *
4151
     * @param array $query
4152
     *
4153
     * @return FluentPdoModel|$this
4154
     * @throws Exception
4155
     */
4156
    public function filter(array $query=[]) : FluentPdoModel
4157
    {
4158
        $columns   = $this->getColumns(false);
4159
        $alias     = '';
4160
        foreach ( $query as $column => $value )
4161
        {
4162
            if ( in_array($column, $this->_pagination_attribs) )
4163
            {
4164
                continue;
4165
            }
4166
            $field = $this->_findFieldByQuery($column, $this->_display_column);
4167
            if ( is_null($field) )
4168
            {
4169
                continue;
4170
            }
4171
            $this->_filter_meta[$field]     = $value;
4172
            $where                          = ! is_array($value) && mb_stripos((string)$value, '|') !== false ? explode('|', $value) : $value;
4173
            if ( is_array($where) )
4174
            {
4175
                $this->whereIn($field, $where);
4176
            }
4177
            else
4178
            {
4179
                $this->_addWhereClause($field, (string)$where);
4180
            }
4181
        }
4182
        if ( empty($query['_search']) )
4183
        {
4184
            return $this;
4185
        }
4186
        $alias          = ! empty($alias) ? $alias : $this->_table_alias;
4187
        $string_cols    = array_filter($columns, function($type) {
4188
4189
            return in_array($type, ['varchar', 'text', 'enum']);
4190
        });
4191
        $terms          = explode('|', $query['_search']);
4192
        $where_likes    = [];
4193
        foreach ( $string_cols as $column => $type )
4194
        {
4195
            if ( in_array($column, $this->excluded_search_cols) )
4196
            {
4197
                continue;
4198
            }
4199
            foreach ( $terms as $term )
4200
            {
4201
                $where_likes["{$alias}.{$column}"] = "%{$term}%";
4202
            }
4203
        }
4204
        // Reference fields...
4205
        $belongsTo = $this->getSearchableAssociations();
4206
        foreach ( $belongsTo as $alias => $config )
4207
        {
4208
            foreach ( $terms as $term )
4209
            {
4210
                $where_likes[$config[2]] = "%{$term}%";
4211
            }
4212
        }
4213
        if ( empty($where_likes) )
4214
        {
4215
            return $this;
4216
        }
4217
        $this->where('1', '1')->wrap()->_and();
4218
        foreach ( $where_likes as $column => $term )
4219
        {
4220
            $this->_or()->whereLike($column, $term);
4221
        }
4222
        $this->wrap();
4223
4224
        return $this;
4225
    }
4226
4227
    /**
4228
     * @param string $column
4229
     * @param string $displayCol
4230
     * @return string|null
4231
     */
4232
    protected function _findFieldByQuery(string $column, string $displayCol)
4233
    {
4234
        list($alias, $field)    = $this->_getColumnAliasParts($column);
4235
        $field                  = $field === '_display_field' ? $displayCol : $field;
4236
        $columns                = $this->getColumns();
4237
        $tableAlias             = $this->getTableAlias();
4238
        if ( ! empty($alias) && $alias === $tableAlias )
4239
        {
4240
            // Alias is set but the field isn't correct
4241
            if ( ! in_array($field, $columns) )
4242
            {
4243
                return null;
4244
            }
4245
            return "{$alias}.{$field}";
4246
        }
4247
        // Alias isn't passed in but the field is ok
4248
        if ( empty($alias) && in_array($field, $columns) )
4249
        {
4250
            return "{$tableAlias}.{$field}";
4251
        }
4252
//        // Alias is passed but not this table in but there is a matching field on this table
4253
//        if ( empty($alias) ) //&& in_array($field, $columns) )
4254
//        {
4255
//            return null;
4256
//        }
4257
        // Now search the associations for the field
4258
        $associations = $this->getSearchableAssociations();
4259
        if ( ! empty($alias) )
4260
        {
4261
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
4262
            {
4263
                return "{$alias}.{$field}";
4264
            }
4265
4266
            return null;
4267
        }
4268
        foreach ( $associations as $assocAlias => $config )
4269
        {
4270
            list(, , $assocField, $fieldAlias) = $config;
4271
            if ( $fieldAlias === $field )
4272
            {
4273
                return $assocField;
4274
            }
4275
        }
4276
4277
        return null;
4278
    }
4279
4280
    /**
4281
     * @param $keysOnly
4282
     * @return array
4283
     */
4284
4285
    public function columns(bool $keysOnly=true) : array
4286
    {
4287
        return $keysOnly ? array_keys($this->_schema) : $this->_schema;
4288
    }
4289
4290
    /**
4291
     * @param string $field
4292
     * @param mixed $value
4293
     * @param array $pdoMetaData
4294
     * @return float|int
4295
     * @throws Exception
4296
     */
4297
    protected function _fixTypeToSentinel(string $field, $value, array $pdoMetaData=[])
4298
    {
4299
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4300
4301
        $fieldType      = strtolower($pdoMetaData['native_type'] ??''?: '');
4302
        if ( ! $fieldType )
4303
        {
4304
            if ( empty($this->_schema) )
4305
            {
4306
                return $value;
4307
            }
4308
            $columns    = $this->getColumns(false);
4309
            Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4310
4311
            $fieldType = $columns[$field] ?: null;
4312
        }
4313
4314
4315
        // Don't cast invalid values... only those that can be cast cleanly
4316
        switch ( $fieldType )
4317
        {
4318
            case 'varchar':
4319
            case 'var_string':
4320
            case 'string':
4321
            case 'text';
4322
            case 'date':
4323
            case 'datetime':
4324
            case 'timestamp':
4325
            case 'blob':
4326
4327
                return (string)$value;
4328
4329
            case 'int':
4330
            case 'integer':
4331
            case 'tinyint':
4332
            case 'tiny':
4333
            case 'long':
4334
            case 'longlong':
4335
4336
                return (int)$value;
4337
4338
            case 'decimal':
4339
            case 'float':
4340
            case 'double':
4341
            case 'newdecimal':
4342
4343
                return (float)$value;
4344
4345
            default:
4346
4347
                Assert(false)->true('Unknown type: ' . $fieldType);
4348
4349
                return $value;
4350
        }
4351
    }
4352
4353
    /**
4354
     * @param string $field
4355
     * @param mixed $value
4356
     * @param bool|false $permissive
4357
     * @return float|int|null|string
4358
     */
4359
    protected function _fixType(string $field, $value, bool $permissive=false)
4360
    {
4361
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4362
        if ( empty($this->_schema) || ( ! array_key_exists($field, $this->_schema) && $permissive ) )
4363
        {
4364
            return $value;
4365
        }
4366
        $columns    = $this->getColumns(false);
4367
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4368
4369
        $fieldType = ! empty($columns[$field]) ? $columns[$field] : null;
4370
4371
        if ( is_null($value) )
4372
        {
4373
            return null;
4374
        }
4375
        // return on null, '' but not 0
4376
        if ( ! is_numeric($value) && empty($value) )
4377
        {
4378
            return null;
4379
        }
4380
        // Don't cast invalid values... only those that can be cast cleanly
4381
        switch ( $fieldType )
4382
        {
4383
            case 'varchar':
4384
            case 'text';
4385
            case 'date':
4386
            case 'datetime':
4387
            case 'timestamp':
4388
4389
                // return on null, '' but not 0
4390
                return ! is_numeric($value) && empty($value) ? null : (string)$value;
4391
4392
            case 'int':
4393
4394
                if ( $field === 'id' || substr($field, -3) === '_id' )
4395
                {
4396
                    return $value ? (int)$value : null;
4397
                }
4398
4399
                return ! is_numeric($value) ? null : (int)$value;
4400
4401
            case 'decimal':
4402
4403
                return ! is_numeric($value) ? null : (float)$value;
4404
4405
            default:
4406
4407
                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...
4408
        }
4409
    }
4410
4411
    /**
4412
     * @param stdClass $record
4413
     * @param string $type
4414
     * @return stdClass
4415
     */
4416
    public function fixTypesToSentinel(stdClass $record, string $type='') : stdClass
4417
    {
4418
        foreach ( $this->row_meta_data as $column => $pdoMetaData )
4419
        {
4420
            if ( ! property_exists($record, $column) )
4421
            {
4422
                continue;
4423
            }
4424
            $record->{$column} = $this->_fixTypeToSentinel($column, $record->{$column}, $pdoMetaData);
4425
        }
4426
        // PDO might not be able to generate the meta data to sniff types
4427
        if ( ! $this->row_meta_data )
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->row_meta_data 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...
4428
        {
4429
            foreach ( $this->getColumns(false) as $column => $fieldType )
4430
            {
4431
                if ( ! property_exists($record, $column) )
4432
                {
4433
                    continue;
4434
                }
4435
                $record->{$column} = $this->_fixTypeToSentinel($column, $record->{$column});
4436
            }
4437
        }
4438
4439
        unset($type);
4440
4441
        return $record;
4442
    }
4443
4444
    /**
4445
     * @param stdClass $record
4446
     * @param string   $type
4447
     * @return stdClass
4448
     * @throws Exception
4449
     */
4450
    public function applyHandlers(stdClass $record, string $type='INSERT') : stdClass
4451
    {
4452
        $this->_compileHandlers();
4453
        $this->_errors                  = [];
4454
        // Disable per field exceptions so we can capture all errors for the record
4455
        $tmpExceptions                  = $this->_validation_exceptions;
4456
        $this->_validation_exceptions   = false;
4457
        foreach ( $this->_handlers as $field => $fn_validator )
4458
        {
4459
            if ( ! property_exists($record, $field) )
4460
            {
4461
                // If the operation is an update it can be a partial update
4462
                if ( $type === self::SAVE_UPDATE )
4463
                {
4464
                    continue;
4465
                }
4466
                $record->{$field}               = null;
4467
            }
4468
            $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...
4469
        }
4470
        $this->_validation_exceptions = $tmpExceptions;
4471
        if ( $this->_validation_exceptions && ! empty($this->_errors) )
4472
        {
4473
            throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4474
        }
4475
4476
        return $record;
4477
    }
4478
4479
4480
    /**
4481
     * @param stdClass $record
4482
     * @param array    $fields
4483
     * @param string   $type
4484
     * @return bool
4485
     */
4486
    protected function uniqueCheck(stdClass $record, array $fields, string $type) : bool
4487
    {
4488
        if ( $type === self::SAVE_UPDATE )
4489
        {
4490
            $this->whereNot($this->_primary_key, $record->{$this->_primary_key});
4491
        }
4492
        foreach ( $fields as $field )
4493
        {
4494
            $this->where($field, $record->{$field});
4495
        }
4496
4497
        return (int)$this->count() > 0;
4498
    }
4499
4500
    /**
4501
     * @param string $field
4502
     * @param mixed $value
4503
     * @param string $type
4504
     * @param stdClass $record
4505
     * @return null
4506
     * @throws Exception
4507
     */
4508
    protected function applyHandler(string $field, $value, string $type='', stdClass $record=null)
4509
    {
4510
        $this->_compileHandlers();
4511
        $fnHandler = ! empty($this->_handlers[$field]) ? $this->_handlers[$field] : null;
4512
        if ( is_callable($fnHandler) )
4513
        {
4514
            try
4515
            {
4516
                $value = $fnHandler($field, $value, $type, $record);
4517
            }
4518
            catch( Exception $e )
4519
            {
4520
                $this->_errors[$field][] = $e->getMessage();
4521
                if ( $this->_validation_exceptions && ! empty($this->_errors) )
4522
                {
4523
                    throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4524
                }
4525
4526
                return null;
4527
            }
4528
        }
4529
4530
        return $value;
4531
    }
4532
4533
    /**
4534
     * @param string $start
4535
     * @param string $end
4536
     * @param string $hayStack
4537
     * @return mixed
4538
     */
4539
    public static function between(string $start, string $end, string $hayStack) : string
4540
    {
4541
        return static::before($end, static::after($start, $hayStack));
4542
    }
4543
4544
    /**
4545
     * @param string     $needle
4546
     * @param string     $hayStack
4547
     * @param bool $returnOrigIfNeedleNotExists
4548
     * @return mixed
4549
     */
4550
    public static function before(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4551
    {
4552
        $result = mb_substr($hayStack, 0, mb_strpos($hayStack, $needle));
4553
        if ( !$result && $returnOrigIfNeedleNotExists )
4554
        {
4555
            return $hayStack;
4556
        }
4557
4558
        return $result;
4559
    }
4560
4561
    /**
4562
     * @param string     $needle
4563
     * @param string     $hayStack
4564
     * @param bool $returnOrigIfNeedleNotExists
4565
     * @return string
4566
     */
4567
    public static function after(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4568
    {
4569
        if ( ! is_bool(mb_strpos($hayStack, $needle)) )
4570
        {
4571
            return mb_substr($hayStack, mb_strpos($hayStack, $needle) + mb_strlen($needle));
4572
        }
4573
4574
        return $returnOrigIfNeedleNotExists ? $hayStack : '';
4575
    }
4576
4577
    /**
4578
     * @return int
4579
     */
4580
    public function getUserId()
4581
    {
4582
        return 0;
4583
    }
4584
4585
    /**
4586
     * @param string $entity
4587
     * @param int $id
4588
     * @return int
4589
     */
4590
    public function getMaskByResourceAndId(string $entity, int $id) : int
0 ignored issues
show
Unused Code introduced by
The parameter $entity 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...
4591
    {
4592
        return 31;
4593
    }
4594
4595
    /**
4596
     * @param string|int|null $time
4597
     * @return string
4598
     */
4599
    public static function date($time=null) : string
4600
    {
4601
        return date('Y-m-d', static::getTime($time));
4602
    }
4603
4604
    /**
4605
     * @param string|int|null $time
4606
     * @return string
4607
     */
4608
    public static function dateTime($time=null) : string
4609
    {
4610
        return date('Y-m-d H:i:s', static::getTime($time));
4611
    }
4612
4613
    /**
4614
     * @param string|int|null $time
4615
     * @return string
4616
     */
4617
    public static function atom($time=null) : string
4618
    {
4619
        return date('Y-m-d\TH:i:sP', static::getTime($time));
4620
    }
4621
4622
    /**
4623
     * @param string|int|null $time
4624
     * @return int
4625
     */
4626
    public static function getTime($time=null) : int
4627
    {
4628
        if ( ! $time )
4629
        {
4630
            return time();
4631
        }
4632
        if ( is_int($time) )
4633
        {
4634
            return $time;
4635
        }
4636
4637
        return strtotime($time);
4638
    }
4639
4640
    /**
4641
     * @param int $id
4642
     * @param int $cacheTtl
4643
     * @return string
4644
     */
4645
    public function getCodeById(int $id, int $cacheTtl=self::ONE_DAY) : string
4646
    {
4647
        Assert($id)->id();
4648
        $code   = $this->defaultFilters()->fetchStr($this->getDisplayColumn(), $id, $cacheTtl);
4649
        Assert($code)->notEmpty();
4650
4651
        return $code;
4652
    }
4653
4654
    /**
4655
     * @param array $authUserRoles
4656
     * @param int   $authUserId
4657
     * @return FluentPdoModel
4658
     */
4659
    public function applyRoleFilter(array $authUserRoles, int $authUserId) : FluentPdoModel
0 ignored issues
show
Unused Code introduced by
The parameter $authUserRoles 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 $authUserId 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...
4660
    {
4661
        return $this;
4662
    }
4663
4664
    /**
4665
     * @param int    $id
4666
     * @param string[] $authUserRoles
4667
     * @param int    $authUserId
4668
     * @return bool
4669
     */
4670
    public function canAccessIdWithRole(int $id, array $authUserRoles, int $authUserId) : bool
0 ignored issues
show
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 $authUserRoles 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 $authUserId 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...
4671
    {
4672
        return true;
4673
    }
4674
}
4675