Completed
Push — master ( 652c5f...ca7a5e )
by Terry
05:45
created

FluentPdoModel::getForeignKeysFromDb()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

This method has been deprecated.

Loading history...
2597
                    return $value;
2598
                }
2599
                Validate($value)->name($field)->id('ID must be a valid integer id, (%s) submitted.');
0 ignored issues
show
Deprecated Code introduced by
The method Terah\Assert\Assert::name() has been deprecated.

This method has been deprecated.

Loading history...
2600
                return $value;
2601
            },
2602
            self::CREATOR_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2603
2604
                unset($type, $record);
2605
                $value = $this->_fixType($field, $value);
2606
                // Created user id is set to current user if record is an insert or deleted if not (unless override is true)
2607
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2608
                Validate($value)->name($field)->id('Created By must be a valid integer id, (%s) submitted.');
0 ignored issues
show
Deprecated Code introduced by
The method Terah\Assert\Assert::name() has been deprecated.

This method has been deprecated.

Loading history...
2609
                return $value;
2610
            },
2611
            self::CREATED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2612
2613
                unset($type, $record);
2614
                $value = $this->_fixType($field, $value);
2615
                // Created ts is set to now if record is an insert or deleted if not (unless override is true)
2616
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2617
                Validate($value)->name($field)->date('Created must be a valid timestamp, (%s) submitted.');
0 ignored issues
show
Deprecated Code introduced by
The method Terah\Assert\Assert::name() has been deprecated.

This method has been deprecated.

Loading history...
2618
                return $value;
2619
            },
2620
            self::MODIFIER_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2621
2622
                unset($type, $record);
2623
                $value = $this->_fixType($field, $value);
2624
                // Modified user id is set to current user (unless override is true)
2625
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2626
                Validate($value)->name($field)->id('Modified By must be a valid integer id, (%s) submitted.');
0 ignored issues
show
Deprecated Code introduced by
The method Terah\Assert\Assert::name() has been deprecated.

This method has been deprecated.

Loading history...
2627
                return $value;
2628
            },
2629
            self::MODIFIED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2630
2631
                unset($type, $record);
2632
                $value = $this->_fixType($field, $value);
2633
                // Modified timestamps are set to now (unless override is true)
2634
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2635
                Validate($value)->name($field)->date('Modified must be a valid timestamp, (%s) submitted.');
0 ignored issues
show
Deprecated Code introduced by
The method Terah\Assert\Assert::name() has been deprecated.

This method has been deprecated.

Loading history...
2636
                return $value;
2637
            },
2638
            'status' => function(string $field, $value, string $type='', stdClass $record=null) {
2639
2640
                unset($type, $record);
2641
                $value = $this->_fixType($field, $value);
2642
                // Statuses are set to active if not set
2643
                $value = is_null($value) ? self::ACTIVE : $value;
2644
                Validate($value)->name($field)->nullOr()->status('Status must be a valid integer between -1 and 1, (%s) submitted.');
0 ignored issues
show
Deprecated Code introduced by
The method Terah\Assert\Assert::name() has been deprecated.

This method has been deprecated.

Loading history...
2645
                return $value;
2646
            },
2647
        ];
2648
    }
2649
2650
    /**
2651
     * @return bool
2652
     */
2653
    public function begin() : bool
2654
    {
2655
        $pdo        = $this->getPdo();
2656
        $oldDepth   = $pdo->getTransactionDepth();
2657
        $res        = $pdo->beginTransaction();
2658
        $newDepth   = $pdo->getTransactionDepth();
2659
        $this->getLogger()->debug("Calling db begin transaction", [
2660
            'old_depth'     => $oldDepth,
2661
            'new_depth'     => $newDepth,
2662
            'trans_started' => $newDepth === 1 ? true : false,
2663
        ]);
2664
2665
        return $res;
2666
    }
2667
2668
    /**
2669
     * @return bool
2670
     */
2671
    public function commit() : bool
2672
    {
2673
        $pdo        = $this->getPdo();
2674
        $oldDepth   = $pdo->getTransactionDepth();
2675
        $res        = $pdo->commit();
2676
        $newDepth   = $pdo->getTransactionDepth();
2677
        $this->getLogger()->debug("Calling db commit transaction", [
2678
            'old_depth'     => $oldDepth,
2679
            'new_depth'     => $newDepth,
2680
            'trans_ended'   => $newDepth === 0 ? true : false,
2681
        ]);
2682
        if ( ! $res )
2683
        {
2684
            return false;
2685
        }
2686
2687
        return $res === 0 ? true : $res;
2688
    }
2689
2690
    /**
2691
     * @return bool
2692
     */
2693
    public function rollback() : bool
2694
    {
2695
        $pdo        = $this->getPdo();
2696
        $oldDepth   = $pdo->getTransactionDepth();
2697
        $res        = $pdo->rollback();
2698
        $newDepth   = $pdo->getTransactionDepth();
2699
        $this->getLogger()->debug("Calling db rollback transaction", [
2700
            'old_depth'     => $oldDepth,
2701
            'new_depth'     => $newDepth,
2702
            'trans_ended'   => $newDepth === 0 ? true : false,
2703
        ]);
2704
2705
        return $res;
2706
    }
2707
2708
    /**
2709
     * @param stdClass $record
2710
     * @param  string  $type
2711
     * @return stdClass
2712
     */
2713
    public function applyGlobalModifiers(stdClass $record, string $type) : stdClass
2714
    {
2715
        unset($type);
2716
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2717
        {
2718
            if ( is_string($record->{$field}) )
2719
            {
2720
                $record->{$field} = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", $value);
2721
            }
2722
        }
2723
2724
        return $record;
2725
    }
2726
2727
    /**
2728
     * @param stdClass $record
2729
     * @param  string $type
2730
     * @return stdClass
2731
     */
2732
    public function removeUnneededFields(stdClass $record, string $type) : stdClass
2733
    {
2734
        $creator_id     = self::CREATOR_ID_FIELD;
2735
        $created_ts     = self::CREATED_TS_FIELD;
2736
2737
        // remove un-needed fields
2738
        $columns = $this->getColumns(true);
2739
        if ( empty($columns) )
2740
        {
2741
            return $record;
2742
        }
2743
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2744
        {
2745
            if ( ! in_array($name, $columns) || in_array($name, $this->_virtual_fields) )
2746
            {
2747
                unset($record->{$name});
2748
            }
2749
        }
2750
        if ( property_exists($record, $created_ts) && $type !== 'INSERT' && ! $this->_allow_meta_override )
2751
        {
2752
            unset($record->{$created_ts});
2753
        }
2754
        if ( property_exists($record, $creator_id) && $type !== 'INSERT' && ! $this->_allow_meta_override )
2755
        {
2756
            unset($record->{$creator_id});
2757
        }
2758
2759
        return $record;
2760
    }
2761
2762
2763
    /**
2764
     * @param array $ids
2765
     * @param array $values
2766
     * @param int   $batch
2767
     * @return bool
2768
     */
2769
    public function setById(array $ids, array $values, int $batch=1000) : bool
2770
    {
2771
        $ids        = array_unique($ids);
2772
        if ( empty($ids) )
2773
        {
2774
            return true;
2775
        }
2776
        if ( count($ids) <= $batch )
2777
        {
2778
            return (bool)$this->whereIn('id', $ids)->updateArr($values);
2779
        }
2780
        while ( ! empty($ids) )
2781
        {
2782
            $thisBatch  = array_slice($ids, 0, $batch);
2783
            $ids        = array_diff($ids, $thisBatch);
2784
            $this->reset()->whereIn('id', $thisBatch)->updateArr($values);
2785
        }
2786
2787
        return true;
2788
    }
2789
2790
2791
    /**
2792
     * @param string $displayColumnValue
2793
     * @return int
2794
     */
2795
    public function resolveId(string $displayColumnValue) : int
2796
    {
2797
        $displayColumn  = $this->getDisplayColumn();
2798
        $className      = get_class($this);
2799
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
2800
2801
        return $this
2802
            ->reset()
2803
            ->where($displayColumn, $displayColumnValue)
2804
            ->fetchInt('id', 0, self::ONE_HOUR);
2805
    }
2806
2807
    /**
2808
     * @param int   $resourceId
2809
     * @param array $query
2810
     * @param array $extraFields
2811
     * @param int $cacheTtl
2812
     * @return array
2813
     */
2814
    public function fetchApiResource(int $resourceId, array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO) : array
2815
    {
2816
        Assert($resourceId)->id();
2817
2818
        $query['_limit']    = 1;
2819
        $pagingMetaData        = $this->wherePk($resourceId)->_prepareApiResource($query, $extraFields);
2820
        if ( $pagingMetaData['total'] === 0 )
2821
        {
2822
            return [[], $pagingMetaData];
2823
        }
2824
2825
        return [$this->fetchOne($resourceId, $cacheTtl), $pagingMetaData];
2826
    }
2827
2828
    /**
2829
     * @param array     $query
2830
     * @param array     $extraFields
2831
     * @param int       $cacheTtl
2832
     * @param string    $permEntity
2833
     * @return array
2834
     */
2835
    public function fetchApiResources(array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO, string $permEntity='') : array
2836
    {
2837
        $pagingMetaData    = $this->_prepareApiResource($query, $extraFields);
2838
        if ( $pagingMetaData['total'] === 0 )
2839
        {
2840
            return [[], $pagingMetaData];
2841
        }
2842
        $results = $this->fetch('', $cacheTtl);
2843
        if ( ! $permEntity )
2844
        {
2845
            return [$results, $pagingMetaData];
2846
        }
2847
        foreach ( $results as $record )
2848
        {
2849
            if ( ! empty($record->id) )
2850
            {
2851
                $pagingMetaData['perms'][(int)$record->id] = $this->getMaskByResourceAndId($permEntity, $record->id);
2852
            }
2853
        }
2854
2855
        return [$results, $pagingMetaData];
2856
    }
2857
2858
2859
    /**
2860
     * @return array
2861
     */
2862
    public function getSearchableAssociations() : array
2863
    {
2864
        $belongsTo = ! empty($this->_associations['belongsTo']) ? $this->_associations['belongsTo'] : [];
2865
        unset($belongsTo['CreatedBy'], $belongsTo['ModifiedBy']);
2866
2867
        return $belongsTo;
2868
    }
2869
2870
    /**
2871
     * @param array $fields
2872
     */
2873
    public function removeUnrequestedFields(array $fields)
2874
    {
2875
        foreach ( $this->_select_fields as $idx => $field )
2876
        {
2877
            $field = trim(static::after(' AS ', $field, true));
2878
            if ( ! in_array($field, $fields) )
2879
            {
2880
                unset($this->_select_fields[$idx]);
2881
            }
2882
        }
2883
    }
2884
2885
    /**
2886
     * @param array $removeFields
2887
     */
2888
    public function removeFields(array $removeFields=[])
2889
    {
2890
        $searches = [];
2891
        foreach ( $removeFields as $removeField )
2892
        {
2893
            $removeField    = str_replace("{$this->_table_alias}.", '', $removeField);
2894
            $searches[]     = "{$this->_table_alias}.{$removeField}";
2895
            $searches[]     = $removeField;
2896
        }
2897
        foreach ( $this->_select_fields as $idx => $selected )
2898
        {
2899
            $selected = stripos($selected, ' AS ') !== false ? preg_split('/ as /i', $selected) : [$selected];
2900
            foreach ( $selected as $haystack )
2901
            {
2902
                foreach ( $searches as $search )
2903
                {
2904
                    if ( trim($haystack) === trim($search) )
2905
                    {
2906
                        unset($this->_select_fields[$idx]);
2907
                        continue;
2908
                    }
2909
                }
2910
            }
2911
        }
2912
    }
2913
2914
    /**
2915
     * @return FluentPdoModel|$this
2916
     */
2917
    public function defaultFilters() : FluentPdoModel
2918
    {
2919
        return $this;
2920
    }
2921
2922
    /**
2923
     * @param bool $allow
2924
     *
2925
     * @return FluentPdoModel|$this
2926
     */
2927
    public function allowMetaColumnOverride(bool $allow=false) : FluentPdoModel
2928
    {
2929
        $this->_allow_meta_override = $allow;
2930
2931
        return $this;
2932
    }
2933
2934
    /**
2935
     * @param bool $skip
2936
     *
2937
     * @return FluentPdoModel|$this
2938
     */
2939
    public function skipMetaUpdates(bool $skip=true) : FluentPdoModel
2940
    {
2941
        $this->_skip_meta_updates = $skip;
2942
2943
        return $this;
2944
    }
2945
2946
    /**
2947
     * @param bool $add
2948
     *
2949
     * @return FluentPdoModel|$this
2950
     */
2951
    public function addUpdateAlias(bool $add=true) : FluentPdoModel
2952
    {
2953
        $this->_add_update_alias = $add;
2954
2955
        return $this;
2956
    }
2957
2958
    /**
2959
     * @param stdClass $record
2960
     * @return stdClass
2961
     */
2962
    public function onFetch(stdClass $record) : stdClass
2963
    {
2964
        $record     = $this->_trimAndLowerCaseKeys($record);
2965
        if ( $this->_filter_on_fetch )
2966
        {
2967
            $record     = $this->cleanseRecord($record);
2968
        }
2969
2970
        $record     =  $this->fixTypesToSentinel($record);
2971
2972
        return $this->fixTimestamps($record);
2973
    }
2974
2975
    /**
2976
     * @param $value
2977
     * @return string
2978
     */
2979
    public function gzEncodeData(string $value) : string
2980
    {
2981
        if ( $this->_hasGzipPrefix($value) )
2982
        {
2983
            return $value;
2984
        }
2985
2986
        return static::GZIP_PREFIX . base64_encode(gzencode($value, 9));
2987
    }
2988
2989
    /**
2990
     * @param $value
2991
     * @return mixed|string
2992
     */
2993
    public function gzDecodeData(string $value) : string
2994
    {
2995
        if ( ! $this->_hasGzipPrefix($value) )
2996
        {
2997
            return $value;
2998
        }
2999
        $value = substr_replace($value, '', 0, strlen(static::GZIP_PREFIX));
3000
3001
        return gzdecode(base64_decode($value));
3002
    }
3003
3004
    /**
3005
     * @param $value
3006
     * @return bool
3007
     */
3008
    protected function _hasGzipPrefix(string $value) : bool
3009
    {
3010
        return substr($value, 0, strlen(static::GZIP_PREFIX)) === static::GZIP_PREFIX ? true : false;
3011
    }
3012
3013
    /**
3014
     * @param stdClass $record
3015
     * @return stdClass
3016
     */
3017
    public function fixTimestamps(stdClass $record) : stdClass
3018
    {
3019
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3020
        {
3021
            if ( preg_match('/_ts$/', $field) )
3022
            {
3023
                $record->{$field} = empty($value) ? $value : static::atom($value);
3024
            }
3025
        }
3026
3027
        return $record;
3028
    }
3029
3030
    /**
3031
     * @param int $max
3032
     * @return FluentPdoModel|$this
3033
     */
3034
    public function setMaxRecords(int $max) : FluentPdoModel
3035
    {
3036
        Assert($max)->int();
3037
        $this->_default_max = $max;
3038
3039
        return $this;
3040
    }
3041
3042
3043
    /**
3044
     * @param stdClass $record
3045
     * @param string   $type
3046
     * @return stdClass
3047
     */
3048
    public function afterSave(stdClass $record, string $type) : stdClass
3049
    {
3050
        unset($type);
3051
        $this->clearCacheByTable();
3052
        foreach ( $record as $column => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3053
        {
3054
            if ( !empty($record->{$column}) )
3055
            {
3056
                if ( preg_match('/_ts$/', $column) )
3057
                {
3058
                    $record->{$column} = static::atom($value);
3059
                }
3060
                if ( preg_match('/_am$/', $column) )
3061
                {
3062
                    $record->{$column} = number_format($value, 2, '.', '');
3063
                }
3064
            }
3065
        }
3066
3067
        return $record;
3068
    }
3069
3070
    /**
3071
     * @param stdClass $record
3072
     * @param string $type
3073
     * @return stdClass
3074
     */
3075
    public function addDefaultFields(stdClass $record, string $type) : stdClass
3076
    {
3077
        $columns            = $this->getColumns(true);
3078
        if ( empty($columns) )
3079
        {
3080
            return $record;
3081
        }
3082
        $defaults           = [
3083
            self::SAVE_UPDATE   => [
3084
                self::MODIFIER_ID_FIELD        => null,
3085
                self::MODIFIED_TS_FIELD        => null,
3086
            ],
3087
            self::SAVE_INSERT   => [
3088
                self::CREATOR_ID_FIELD         => null,
3089
                self::CREATED_TS_FIELD         => null,
3090
                self::MODIFIER_ID_FIELD        => null,
3091
                self::MODIFIED_TS_FIELD        => null,
3092
                self::STATUS_FIELD             => null,
3093
            ]
3094
        ];
3095
        if ( $this->_skip_meta_updates )
3096
        {
3097
            $defaults[self::SAVE_UPDATE] = [];
3098
        }
3099
        $columns            = array_flip($this->getColumns());
3100
        $defaults           = array_intersect_key($defaults[$type], $columns);
3101
        foreach ( $defaults as $column => $def )
3102
        {
3103
            $record->{$column} = $record->{$column} ?? $def;
3104
        }
3105
3106
        return $record;
3107
    }
3108
3109
3110
    /**
3111
     * @return bool
3112
     */
3113
    public function createTable() : bool
3114
    {
3115
        return true;
3116
    }
3117
3118
    /**
3119
     * @param bool|false $force
3120
     * @return FluentPdoModel|$this
3121
     * @throws Exception
3122
     */
3123
    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...
3124
    {
3125
        return $this;
3126
    }
3127
3128
    protected function _compileHandlers()
3129
    {
3130
        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...
3131
        {
3132
            return;
3133
        }
3134
        $parentHandlers      = self::_getFieldHandlers();
3135
        $this->_handlers    = array_merge($parentHandlers, $this->_getFieldHandlers());
3136
    }
3137
3138
    /**
3139
     * @param string $viewName
3140
     * @param int $cacheTtl
3141
     * @return array
3142
     */
3143
    public function getViewColumns($viewName, $cacheTtl=self::CACHE_NO)
3144
    {
3145
        return $this->_getColumnsByTableFromDb($viewName, $cacheTtl);
3146
    }
3147
3148
    /**
3149
     * @param int $id
3150
     * @return string
3151
     */
3152
    public function getDisplayNameById(int $id) : string
3153
    {
3154
        $displayColumn  = $this->getDisplayColumn();
3155
        $className      = get_class($this);
3156
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
3157
3158
        return $this
3159
            ->reset()
3160
            ->fetchStr($displayColumn, $id, self::ONE_HOUR);
3161
    }
3162
3163
    /**
3164
     * @param int $id
3165
     * @param string $displayColumnValue
3166
     * @return bool
3167
     */
3168
    public function validIdDisplayNameCombo(int $id, $displayColumnValue) : bool
3169
    {
3170
        return $displayColumnValue === $this->getDisplayNameById($id);
3171
    }
3172
3173
    /**
3174
     * @param array $toPopulate
3175
     * @return stdClass
3176
     */
3177
    protected function getEmptyObject(array $toPopulate=[]) : stdClass
3178
    {
3179
        $toPopulate[]   = 'id';
3180
3181
        return (object)array_flip($toPopulate);
3182
    }
3183
3184
    /**
3185
     * @param array $toPopulate
3186
     * @return stdClass
3187
     */
3188
    protected static function emptyObject(array $toPopulate=[]) : stdClass
3189
    {
3190
        $toPopulate[]   = 'id';
3191
3192
        return (object)array_flip($toPopulate);
3193
    }
3194
3195
    /**
3196
     * @param int $id
3197
     * @return bool
3198
     */
3199
    public static function isId(int $id) : bool
3200
    {
3201
        return $id > 0;
3202
    }
3203
3204
    /**
3205
     * @param int $cacheTtl
3206
     * @return int
3207
     */
3208
    public function activeCount(int $cacheTtl=self::CACHE_NO) : int
3209
    {
3210
        return (int)$this->whereActive()->count('*', $cacheTtl);
3211
    }
3212
3213
    /**
3214
     * @param string        $tableAlias
3215
     * @param string   $columnName
3216
     * @return FluentPdoModel|$this
3217
     */
3218
    public function whereActive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3219
    {
3220
        return $this->whereStatus(static::ACTIVE, $tableAlias, $columnName);
3221
    }
3222
3223
    /**
3224
     * @param string        $tableAlias
3225
     * @param string        $columnName
3226
     * @return FluentPdoModel|$this
3227
     */
3228
    public function whereInactive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3229
    {
3230
        return $this->whereStatus(static::INACTIVE, $tableAlias, $columnName);
3231
    }
3232
3233
    /**
3234
     * @param string        $tableAlias
3235
     * @param string        $columnName
3236
     * @return FluentPdoModel|$this
3237
     */
3238
    public function whereArchived(string $tableAlias='', string $columnName='status') : FluentPdoModel
3239
    {
3240
        return $this->whereStatus(static::ARCHIVED, $tableAlias, $columnName);
3241
    }
3242
3243
    /**
3244
     * @param int $status
3245
     * @param string $tableAlias
3246
     * @param string $columnName
3247
     * @return FluentPdoModel|$this
3248
     */
3249
    public function whereStatus(int $status, string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3250
    {
3251
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3252
3253
        $tableAlias = empty($tableAlias) ? $this->getTableAlias() : $tableAlias;
3254
        $field      = empty($tableAlias) ? $columnName : "{$tableAlias}.{$columnName}";
3255
3256
        return $this->where($field, $status);
3257
    }
3258
3259
    /**
3260
     * @param int $id
3261
     * @return int
3262
     */
3263
    public function updateActive(int $id=0) : int
3264
    {
3265
        Assert($id)->unsignedInt();
3266
        if ( $id )
3267
        {
3268
            $this->wherePk($id);
3269
        }
3270
3271
        return $this->updateStatus(static::ACTIVE);
3272
    }
3273
3274
    /**
3275
     * @param int $id
3276
     * @return int
3277
     */
3278
    public function updateInactive(int $id=0) : int
3279
    {
3280
        Assert($id)->unsignedInt();
3281
        if ( $id )
3282
        {
3283
            $this->wherePk($id);
3284
        }
3285
        return $this->updateStatus(static::INACTIVE);
3286
    }
3287
3288
    /**
3289
     * @param string $field
3290
     * @param int  $id
3291
     * @return int
3292
     */
3293
    public function updateNow(string $field, int $id=0) : int
3294
    {
3295
        Assert($field)->notEmpty();
3296
3297
        return $this->updateField($field, date('Y-m-d H:i:s'), $id);
3298
    }
3299
3300
    /**
3301
     * @param string $field
3302
     * @param int  $id
3303
     * @return int
3304
     */
3305
    public function updateToday($field, int $id=0) : int
3306
    {
3307
        Assert($field)->notEmpty();
3308
3309
        return $this->updateField($field, date('Y-m-d'), $id);
3310
    }
3311
3312
    /**
3313
     * @param int $id
3314
     * @return int
3315
     */
3316
    public function updateDeleted(int $id=0) : int
3317
    {
3318
        Assert($id)->unsignedInt();
3319
        if ( $id )
3320
        {
3321
            $this->wherePk($id);
3322
        }
3323
3324
        return $this->update((object)[
3325
            self::DELETER_ID_FIELD  => $this->getUserId(),
3326
            self::DELETED_TS_FIELD  => static::dateTime(),
3327
        ]);
3328
    }
3329
3330
    /**
3331
     * @param int $id
3332
     * @return int
3333
     */
3334
    public function updateArchived(int $id=0) : int
3335
    {
3336
        Assert($id)->unsignedInt();
3337
        if ( $id )
3338
        {
3339
            $this->wherePk($id);
3340
        }
3341
3342
        return $this->updateStatus(static::ARCHIVED);
3343
    }
3344
3345
    /**
3346
     * @param int $status
3347
     * @return int
3348
     * @throws \Exception
3349
     */
3350
    public function updateStatus(int $status)
3351
    {
3352
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3353
3354
        return $this->updateField('status', $status);
3355
    }
3356
3357
    /**
3358
     * Return a YYYY-MM-DD HH:II:SS date format
3359
     *
3360
     * @param string $datetime - An english textual datetime description
3361
     *          now, yesterday, 3 days ago, +1 week
3362
     *          http://php.net/manual/en/function.strtotime.php
3363
     * @return string YYYY-MM-DD HH:II:SS
3364
     */
3365
    public static function NOW(string $datetime='now') : string
3366
    {
3367
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
3368
    }
3369
3370
    /**
3371
     * Return a string containing the given number of question marks,
3372
     * separated by commas. Eg '?, ?, ?'
3373
     *
3374
     * @param int - total of placeholder to insert
3375
     * @return string
3376
     */
3377
    protected function _makePlaceholders(int $numberOfPlaceholders=1) : string
3378
    {
3379
        return implode(', ', array_fill(0, $numberOfPlaceholders, '?'));
3380
    }
3381
3382
    /**
3383
     * Format the table{Primary|Foreign}KeyName
3384
     *
3385
     * @param  string $pattern
3386
     * @param  string $tableName
3387
     * @return string
3388
     */
3389
    protected function _formatKeyName(string $pattern, string $tableName) : string
3390
    {
3391
        return sprintf($pattern, $tableName);
3392
    }
3393
3394
    /**
3395
     * @param array $query
3396
     * @param array $extraFields
3397
     * @return array
3398
     * @throws \Exception
3399
     */
3400
    protected function _prepareApiResource(array $query=[], array $extraFields=[]) : array
3401
    {
3402
        $this->defaultFilters()->filter($query)->paginate($query);
3403
        $pagingMetaData    = $this->getPagingMeta();
3404
        if ( $pagingMetaData['total'] === 0 )
3405
        {
3406
            return $pagingMetaData;
3407
        }
3408
        $this->withBelongsTo($pagingMetaData['fields']);
3409
        if ( ! empty($extraFields) )
3410
        {
3411
            $this->select($extraFields, '', false);
3412
        }
3413
        $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...
3414
        if ( ! empty($pagingMetaData['fields']) )
3415
        {
3416
            $this->removeUnrequestedFields($pagingMetaData['fields']);
3417
        }
3418
3419
        return $pagingMetaData;
3420
    }
3421
3422
    /**
3423
     * @param string $query
3424
     * @param array $parameters
3425
     *
3426
     * @return array
3427
     */
3428
    protected function _logQuery(string $query, array $parameters) : array
3429
    {
3430
        $query                  = $this->buildQuery($query, $parameters);
3431
        if ( ! $this->_log_queries )
3432
        {
3433
            return ['', ''];
3434
        }
3435
        $ident                  = substr(str_shuffle(md5($query)), 0, 10);
3436
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
3437
        $this->_timer['start']  = microtime(true);
3438
3439
        return [$query, $ident];
3440
    }
3441
3442
    /**
3443
     * @param string $ident
3444
     * @param string $builtQuery
3445
     */
3446
    protected function _logSlowQueries(string $ident, string $builtQuery)
3447
    {
3448
        if ( ! $this->_log_queries )
3449
        {
3450
            return ;
3451
        }
3452
        $this->_timer['end']    = microtime(true);
3453
        $seconds_taken          = round($this->_timer['end'] - $this->_timer['start'], 3);
3454
        if ( $seconds_taken > $this->_slow_query_secs )
3455
        {
3456
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$seconds_taken} seconds:\n{$builtQuery}");
3457
        }
3458
    }
3459
3460
    /**
3461
     * @return float
3462
     */
3463
    public function getTimeTaken() : float
3464
    {
3465
        $secondsTaken = $this->_timer['end'] - $this->_timer['start'];
3466
3467
        return (float)$secondsTaken;
3468
    }
3469
3470
    /**
3471
     * @param $secs
3472
     * @return FluentPdoModel|$this
3473
     */
3474
    public function slowQuerySeconds(int $secs) : FluentPdoModel
3475
    {
3476
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
3477
        $this->_slow_query_secs = $secs;
3478
3479
        return $this;
3480
    }
3481
3482
3483
    /**
3484
     * @param       $field
3485
     * @param array $values
3486
     * @param string  $placeholderPrefix
3487
     *
3488
     * @return array
3489
     */
3490
    public function getNamedWhereIn(string $field, array $values, string $placeholderPrefix='') : array
3491
    {
3492
        Assert($field)->string()->notEmpty();
3493
        Assert($values)->isArray();
3494
3495
        if ( empty($values) )
3496
        {
3497
            return ['', []];
3498
        }
3499
        $placeholderPrefix      = $placeholderPrefix ?: strtolower(str_replace('.', '__', $field));
3500
        $params                 = [];
3501
        $placeholders           = [];
3502
        $count                  = 1;
3503
        foreach ( $values as $val )
3504
        {
3505
            $name                   = "{$placeholderPrefix}_{$count}";
3506
            $params[$name]          = $val;
3507
            $placeholders[]         = ":{$name}";
3508
            $count++;
3509
        }
3510
        $placeholders           = implode(',', $placeholders);
3511
3512
        return ["AND {$field} IN ({$placeholders})\n", $params];
3513
    }
3514
3515
    /**
3516
     * @param string $field
3517
     * @param string $delimiter
3518
     *
3519
     * @return array
3520
     */
3521
    protected function _getColumnAliasParts(string $field, string $delimiter=':') : array
3522
    {
3523
        $parts      = explode($delimiter, $field);
3524
        if ( count($parts) === 2 )
3525
        {
3526
            return $parts;
3527
        }
3528
        $parts = explode('.', $field);
3529
        if ( count($parts) === 2 )
3530
        {
3531
            return $parts;
3532
        }
3533
3534
        return ['', $field];
3535
    }
3536
3537
    /**
3538
     * @param string $column
3539
     * @param string $term
3540
     * @return FluentPdoModel|$this
3541
     */
3542
    protected function _addWhereClause(string $column, string $term) : FluentPdoModel
3543
    {
3544
        $modifiers = [
3545
            'whereLike'         => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3546
            'whereNotLike'      => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3547
            'whereLt'           => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
3548
            'whereLte'          => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
3549
            'whereGt'           => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
3550
            'whereGte'          => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
3551
            'whereBetween'      => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3552
            'whereNotBetween'  => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3553
        ];
3554
        /*
3555
3556
         whereLike          i.e ?name=whereLike(%terry%)
3557
         whereNotLike       i.e ?name=whereNotLike(%terry%)
3558
         whereLt            i.e ?age=whereLt(18)
3559
         whereLte           i.e ?age=whereLte(18)
3560
         whereGt            i.e ?event_dt=whereGt(2014-10-10)
3561
         whereGte           i.e ?event_dt=whereGte(2014-10-10)
3562
         whereBetween       i.e ?event_dt=whereBetween(2014-10-10,2014-10-15)
3563
         whereNotBetween    i.e ?event_dt=whereNotBetween(2014-10-10,2014-10-15)
3564
3565
         */
3566
        foreach ( $modifiers as $func => $regex )
3567
        {
3568
            if ( preg_match($regex, $term, $matches) )
3569
            {
3570
                array_shift($matches);
3571
                switch ($func)
3572
                {
3573
                    case 'whereLike':
3574
3575
                        return $this->whereLike($column, $matches[0]);
3576
3577
                    case 'whereNotLike':
3578
3579
                        return $this->whereNotLike($column, $matches[0]);
3580
3581
                    case 'whereLt':
3582
3583
                        return $this->whereLt($column, $matches[0]);
3584
3585
                    case 'whereLte':
3586
3587
                        return $this->whereLte($column, $matches[0]);
3588
3589
                    case 'whereGt':
3590
3591
                        return $this->whereGt($column, $matches[0]);
3592
3593
                    case 'whereGte':
3594
3595
                        return $this->whereGte($column, $matches[0]);
3596
3597
                    case 'whereBetween':
3598
3599
                        return $this->whereBetween($column, $matches[0], $matches[1]);
3600
3601
                    case 'whereNotBetween':
3602
3603
                        return $this->whereNotBetween($column, $matches[0], $matches[1]);
3604
3605
                }
3606
            }
3607
        }
3608
3609
        return $this->where($column, $term);
3610
    }
3611
3612
    public function destroy()
3613
    {
3614
        if ( !is_null($this->_pdo_stmt) )
3615
        {
3616
            $this->_pdo_stmt->closeCursor();
3617
        }
3618
        $this->_pdo_stmt    = null;
3619
        $this->_handlers    = [];
3620
    }
3621
3622
    public function __destruct()
3623
    {
3624
        $this->destroy();
3625
    }
3626
3627
    /**
3628
     * Load a model
3629
     *
3630
     * @param string $modelName
3631
     * @param AbstractPdo $connection
3632
     * @return FluentPdoModel|$this
3633
     * @throws ModelNotFoundException
3634
     */
3635
    public static function loadModel(string $modelName, AbstractPdo $connection=null) : FluentPdoModel
3636
    {
3637
        $modelName = static::$_model_namespace . $modelName;
3638
        if ( ! class_exists($modelName) )
3639
        {
3640
            throw new ModelNotFoundException("Failed to find model class {$modelName}.");
3641
        }
3642
3643
        return new $modelName($connection);
3644
    }
3645
3646
    /**
3647
     * Load a model
3648
     *
3649
     * @param string      $tableName
3650
     * @param AbstractPdo $connection
3651
     * @return FluentPdoModel|$this
3652
     */
3653
    public static function loadTable(string $tableName, AbstractPdo $connection=null) : FluentPdoModel
3654
    {
3655
        $modelName     = Inflector::classify($tableName);
3656
        Assert($modelName)->notEmpty("Could not resolve model name from table name.");
3657
3658
        return static::loadModel($modelName, $connection);
3659
    }
3660
3661
    /**
3662
     * @param string   $columnName
3663
     * @param int $cacheTtl
3664
     * @param bool $flushCache
3665
     * @return bool
3666
     */
3667
    public function columnExists(string $columnName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3668
    {
3669
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3670
3671
        return array_key_exists($columnName, $columns);
3672
    }
3673
3674
    /**
3675
     * @param string   $foreignKeyName
3676
     * @param int $cacheTtl
3677
     * @param bool $flushCache
3678
     * @return bool
3679
     */
3680
    public function foreignKeyExists(string $foreignKeyName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3681
    {
3682
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3683
3684
        return array_key_exists($foreignKeyName, $columns);
3685
    }
3686
3687
    /**
3688
     * @param string   $indexName
3689
     * @param int $cacheTtl
3690
     * @param bool $flushCache
3691
     * @return bool
3692
     */
3693
    public function indexExists(string $indexName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3694
    {
3695
        Assert($indexName)->string()->notEmpty();
3696
3697
        $callback = function() use ($indexName) {
3698
3699
            $index = $this->execute("SHOW INDEX FROM {$this->_table_name} WHERE Key_name = ':indexName'", compact('indexName'));
3700
3701
            return $index ? true : false;
3702
        };
3703
        if  ( $cacheTtl === self::CACHE_NO )
3704
        {
3705
            return $callback();
3706
        }
3707
        $cacheKey   = '/column_schema/' . $this->_table_name . '/index/' . $indexName;
3708
        if ( $flushCache === true )
3709
        {
3710
            $this->clearCache($cacheKey);
3711
        }
3712
3713
        return (bool)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3714
    }
3715
3716
3717
3718
    /**
3719
     * @param int $cacheTtl
3720
     * @param bool $flushCache
3721
     * @return FluentPdoModel|$this
3722
     */
3723
    public function loadSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : FluentPdoModel
3724
    {
3725
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
3726
        $this->schema($schema);
3727
3728
        return $this;
3729
    }
3730
3731
    /**
3732
     * @param int $cacheTtl
3733
     * @param bool $flushCache
3734
     * @return array
3735
     */
3736
    public function getSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3737
    {
3738
        $table      = $this->getTableName();
3739
        Assert($table)->string()->notEmpty();
3740
        $schema     = [];
3741
        $columns    = $this->_getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
3742
        foreach ( $columns[$table] as $column => $meta )
3743
        {
3744
            $schema[$column] = $meta->dataType;
3745
        }
3746
3747
        return $schema;
3748
    }
3749
3750
    /**
3751
     * @param int $cacheTtl
3752
     * @param bool $flushCache
3753
     * @return array
3754
     */
3755
    public function getForeignKeysFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3756
    {
3757
        $table          = $this->getTableName();
3758
        Assert($table)->string()->notEmpty();
3759
        $schema         = [];
3760
        $foreignKeys    = $this->_getForeignKeysByTableFromDb($table, $cacheTtl, $flushCache);
3761
        foreach ( $foreignKeys[$table] as $key => $meta )
3762
        {
3763
            $schema[$key] = $meta->dataType;
3764
        }
3765
3766
        return $schema;
3767
    }
3768
3769
    /**
3770
     * @param string $table
3771
     * @param int $cacheTtl
3772
     * @param bool $flushCache
3773
     * @return Column[][]
3774
     */
3775
    protected function _getColumnsByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3776
    {
3777
        Assert($table)->string()->notEmpty();
3778
3779
        $callback = function() use ($table) {
3780
3781
            return $this->_connection->getColumns(true, $table);
3782
        };
3783
        $cacheKey   = '/column_schema/' . $table;
3784
        if ( $flushCache === true )
3785
        {
3786
            $this->clearCache($cacheKey);
3787
        }
3788
3789
        return (array)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3790
    }
3791
3792
    /**
3793
     * @param string $table
3794
     * @param int $cacheTtl
3795
     * @param bool $flushCache
3796
     * @return Column[][]
3797
     */
3798
    protected function _getForeignKeysByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3799
    {
3800
        Assert($table)->string()->notEmpty();
3801
3802
        $callback = function() use ($table) {
3803
3804
            return $this->_connection->getForeignKeys($table);
3805
        };
3806
        $cacheKey   = '/foreign_keys_schema/' . $table;
3807
        if ( $flushCache === true )
3808
        {
3809
            $this->clearCache($cacheKey);
3810
        }
3811
3812
        return (array)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3813
    }
3814
3815
    /**
3816
     * @param string $table
3817
     * @return bool
3818
     */
3819
    public function clearSchemaCache(string $table) : bool
3820
    {
3821
        return $this->clearCache('/column_schema/' . $table);
3822
    }
3823
3824
    /**
3825
     * @param stdClass $record
3826
     * @return stdClass
3827
     */
3828
    public function cleanseRecord(stdClass $record) : stdClass
3829
    {
3830
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3831
        {
3832
            if ( is_string($record->{$field}) )
3833
            {
3834
                $record->$field = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", filter_var($record->$field, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES));
3835
                if ( $this->_log_filter_changes && $value !== $record->$field )
3836
                {
3837
                    $table = $this->_table_name ? $this->_table_name : '';
3838
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->$field]);
3839
                }
3840
            }
3841
        }
3842
3843
        return $record;
3844
    }
3845
3846
    /**
3847
     * @param stdClass $record
3848
     * @param string   $type
3849
     * @return stdClass
3850
     */
3851
    public function beforeSave(stdClass $record, string $type) : stdClass
3852
    {
3853
        $record = $this->addDefaultFields($record, $type);
3854
        $record = $this->applyGlobalModifiers($record, $type);
3855
        $record = $this->applyHandlers($record, $type);
3856
        $record = $this->removeUnneededFields($record, $type);
3857
3858
        return $record;
3859
    }
3860
3861
    /**
3862
     * @param array $data
3863
     * @param string $saveType
3864
     * @return array
3865
     */
3866
    public function cleanseWebData(array $data, string $saveType) : array
3867
    {
3868
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
3869
        $columns = $this->getColumns(false);
3870
        if ( empty($columns) )
3871
        {
3872
            return $data;
3873
        }
3874
        foreach ( $data as $field => $val )
3875
        {
3876
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
3877
        }
3878
3879
        return array_intersect_key($data, $columns);
3880
    }
3881
3882
    /**
3883
     * @return array
3884
     */
3885
    public function skeleton() : array
3886
    {
3887
        $skel       = [];
3888
        $columns    = $this->columns(false);
3889
        foreach ( $columns as $column => $type )
3890
        {
3891
            $skel[$column] = null;
3892
        }
3893
3894
        return $skel;
3895
    }
3896
3897
    /**
3898
     * @param bool $toString
3899
     * @return array
3900
     */
3901
    public function getErrors(bool $toString=false) : array
3902
    {
3903
        if ( $toString )
3904
        {
3905
            $errors = [];
3906
            foreach ( $this->_errors as $field => $error )
3907
            {
3908
                $errors[] = implode("\n", $error);
3909
            }
3910
3911
            return implode("\n", $errors);
3912
        }
3913
3914
        return $this->_errors;
3915
    }
3916
3917
    /**
3918
     * @param bool $throw
3919
     * @return FluentPdoModel|$this
3920
     */
3921
    public function validationExceptions(bool $throw=true) : FluentPdoModel
3922
    {
3923
        $this->_validation_exceptions = $throw;
3924
3925
        return $this;
3926
    }
3927
3928
    /**
3929
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
3930
     *
3931
     * @return FluentPdoModel|$this
3932
     * @throws Exception
3933
     */
3934
    public function paginate(array $query=[]) : FluentPdoModel
3935
    {
3936
        $_fields = $_order = $_limit = $_offset = null;
3937
        extract($query);
3938
        $this->_setLimit((int)$_limit, (int)$_offset);
3939
        $this->_setOrderBy((string)$_order);
3940
        $_fields    = is_array($_fields) ? $_fields : (string)$_fields;
3941
        $_fields    = empty($_fields) ? [] : $_fields;
3942
        $_fields    = is_string($_fields) ? explode('|', $_fields) : $_fields;
3943
        $_fields    = empty($_fields) ? [] : $_fields;
3944
        $this->_setFields(is_array($_fields) ? $_fields : explode('|', (string)$_fields));
3945
3946
        return $this;
3947
    }
3948
3949
    /**
3950
     * @param int $limit
3951
     * @param int $offset
3952
     * @return FluentPdoModel|$this
3953
     */
3954
    protected function _setLimit(int $limit=0, int $offset=0) : FluentPdoModel
3955
    {
3956
        $limit      = ! $limit || (int)$limit > (int)$this->_default_max ? (int)$this->_default_max : (int)$limit;
3957
        if ( ! is_numeric($limit) )
3958
        {
3959
            return $this;
3960
        }
3961
        $this->limit((int)$limit);
3962
        if ( $offset && is_numeric($offset) )
3963
        {
3964
            $this->offset((int)$offset);
3965
        }
3966
3967
        return $this;
3968
    }
3969
3970
    /**
3971
     * @param array $fields
3972
     * @return FluentPdoModel|$this
3973
     * @throws Exception
3974
     */
3975
    protected function _setFields(array $fields=[]) : FluentPdoModel
3976
    {
3977
        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...
3978
        {
3979
            return $this;
3980
        }
3981
        $this->explicitSelectMode();
3982
        $columns    = $this->getColumns();
3983
3984
        foreach ( $fields as $idx => $field )
3985
        {
3986
            list($alias, $field) = $this->_getColumnAliasParts($field);
3987
            $field = $field === '_display_field' ? $this->_display_column : $field;
3988
            // Regular primary table field
3989
            if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
3990
            {
3991
                $this->select("{$this->_table_alias}.{$field}");
3992
                $this->_requested_fields[] = "{$this->_table_alias}.{$field}";
3993
                continue;
3994
            }
3995
            // Reference table field with alias
3996
            if ( ! empty($alias) )
3997
            {
3998
                Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
3999
                Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
4000
                list(, , $join_field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
4001
                $this->autoJoin($alias, static::LEFT_JOIN, false);
4002
                $this->select($join_field, $fieldAlias);
4003
                $this->_requested_fields[] = $fieldAlias;
4004
                continue;
4005
            }
4006
            // Reference table select field without alias
4007
            $belongsTo = array_key_exists('belongsTo', $this->_associations) ?  $this->_associations['belongsTo'] : [];
4008
            foreach ( $belongsTo as $joinAlias => $config )
4009
            {
4010
                list(, , $join_field, $fieldAlias) = $config;
4011
                if ( $field === $fieldAlias )
4012
                {
4013
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
4014
                    $this->select($join_field, $fieldAlias);
4015
                    $this->_requested_fields[] = $fieldAlias;
4016
                    continue;
4017
                }
4018
            }
4019
        }
4020
4021
        return $this;
4022
    }
4023
4024
    /**
4025
     * @param string $orderBy
4026
     * @return FluentPdoModel|$this|FluentPdoModel
4027
     */
4028
    protected function _setOrderBy(string $orderBy='') : FluentPdoModel
4029
    {
4030
        if ( ! $orderBy )
4031
        {
4032
            return $this;
4033
        }
4034
        $columns                    = $this->getColumns();
4035
        list($order, $direction)    = strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
4036
        list($alias, $field)        = $this->_getColumnAliasParts(trim($order), '.');
4037
        $field                      = explode(' ', $field);
4038
        $field                      = trim($field[0]);
4039
        $direction                  = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
4040
        $belongsTo                  = array_key_exists('belongsTo', $this->_associations) ? $this->_associations['belongsTo'] : [];
4041
        // Regular primary table order by
4042
        if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
4043
        {
4044
            return $this->orderBy("{$this->_table_alias}.{$field}", $direction);
4045
        }
4046
        // Reference table order by with alias
4047
        if ( ! empty($alias) )
4048
        {
4049
            Assert($belongsTo)->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
4050
            Assert($field)->eq($belongsTo[$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
4051
4052
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
4053
        }
4054
        // Reference table order by without alias
4055
        foreach ( $belongsTo as $joinAlias => $config )
4056
        {
4057
            if ( $field === $config[3] )
4058
            {
4059
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
4060
            }
4061
        }
4062
4063
        return $this;
4064
    }
4065
4066
    /**
4067
     * @return array
4068
     */
4069
    public function getPagingMeta()
4070
    {
4071
        if ( empty($this->_paging_meta) )
4072
        {
4073
            $this->setPagingMeta();
4074
        }
4075
4076
        return $this->_paging_meta;
4077
    }
4078
4079
    /**
4080
     * @return FluentPdoModel|$this
4081
     */
4082
    public function setPagingMeta() : FluentPdoModel
4083
    {
4084
        $model                  = clone $this;
4085
        $limit                  = intval($this->getLimit());
4086
        $offset                 = intval($this->getOffset());
4087
        $total                  = intval($model->withBelongsTo()->select('')->offset(0)->limit(0)->orderBy()->count());
4088
        unset($model->_handlers, $model); //hhmv mem leak
4089
        $order_bys              = ! is_array($this->_order_by) ? [] : $this->_order_by;
4090
        $this->_paging_meta     = [
4091
            'limit'                 => $limit,
4092
            'offset'                => $offset,
4093
            'page'                  => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
4094
            'pages'                 => $limit === 0 ? 1 : intval(ceil($total / $limit)),
4095
            'order'                 => $order_bys,
4096
            'total'                 => $total = $limit === 1 && $total > 1 ? 1 : $total,
4097
            'filters'               => $this->_filter_meta,
4098
            'fields'                => $this->_requested_fields,
4099
            'perms'                 => [],
4100
        ];
4101
4102
        return $this;
4103
    }
4104
4105
    /**
4106
     * Take a web request and format a query
4107
     *
4108
     * @param array $query
4109
     *
4110
     * @return FluentPdoModel|$this
4111
     * @throws Exception
4112
     */
4113
    public function filter(array $query=[]) : FluentPdoModel
4114
    {
4115
        $columns   = $this->getColumns(false);
4116
        $alias     = '';
4117
        foreach ( $query as $column => $value )
4118
        {
4119
            if ( in_array($column, $this->_pagination_attribs) )
4120
            {
4121
                continue;
4122
            }
4123
            $field = $this->_findFieldByQuery($column, $this->_display_column);
4124
            if ( is_null($field) )
4125
            {
4126
                continue;
4127
            }
4128
            $this->_filter_meta[$field]     = $value;
4129
            $where                          = ! is_array($value) && mb_stripos((string)$value, '|') !== false ? explode('|', $value) : $value;
4130
            if ( is_array($where) )
4131
            {
4132
                $this->whereIn($field, $where);
4133
            }
4134
            else
4135
            {
4136
                $this->_addWhereClause($field, (string)$where);
4137
            }
4138
        }
4139
        if ( empty($query['_search']) )
4140
        {
4141
            return $this;
4142
        }
4143
        $alias          = ! empty($alias) ? $alias : $this->_table_alias;
4144
        $string_cols    = array_filter($columns, function($type) {
4145
4146
            return in_array($type, ['varchar', 'text', 'enum']);
4147
        });
4148
        $terms          = explode('|', $query['_search']);
4149
        $where_likes    = [];
4150
        foreach ( $string_cols as $column => $type )
4151
        {
4152
            if ( in_array($column, $this->excluded_search_cols) )
4153
            {
4154
                continue;
4155
            }
4156
            foreach ( $terms as $term )
4157
            {
4158
                $where_likes["{$alias}.{$column}"] = "%{$term}%";
4159
            }
4160
        }
4161
        // Reference fields...
4162
        $belongsTo = $this->getSearchableAssociations();
4163
        foreach ( $belongsTo as $alias => $config )
4164
        {
4165
            foreach ( $terms as $term )
4166
            {
4167
                $where_likes[$config[2]] = "%{$term}%";
4168
            }
4169
        }
4170
        if ( empty($where_likes) )
4171
        {
4172
            return $this;
4173
        }
4174
        $this->where('1', '1')->wrap()->_and();
4175
        foreach ( $where_likes as $column => $term )
4176
        {
4177
            $this->_or()->whereLike($column, $term);
4178
        }
4179
        $this->wrap();
4180
4181
        return $this;
4182
    }
4183
4184
    /**
4185
     * @param string $column
4186
     * @param string $displayCol
4187
     * @return string|null
4188
     */
4189
    protected function _findFieldByQuery(string $column, string $displayCol)
4190
    {
4191
        list($alias, $field)    = $this->_getColumnAliasParts($column);
4192
        $field                  = $field === '_display_field' ? $displayCol : $field;
4193
        $columns                = $this->getColumns();
4194
        $tableAlias             = $this->getTableAlias();
4195
        if ( ! empty($alias) && $alias === $tableAlias )
4196
        {
4197
            // Alias is set but the field isn't correct
4198
            if ( ! in_array($field, $columns) )
4199
            {
4200
                return null;
4201
            }
4202
            return "{$alias}.{$field}";
4203
        }
4204
        // Alias isn't passed in but the field is ok
4205
        if ( empty($alias) && in_array($field, $columns) )
4206
        {
4207
            return "{$tableAlias}.{$field}";
4208
        }
4209
//        // Alias is passed but not this table in but there is a matching field on this table
4210
//        if ( empty($alias) ) //&& in_array($field, $columns) )
4211
//        {
4212
//            return null;
4213
//        }
4214
        // Now search the associations for the field
4215
        $associations = $this->getSearchableAssociations();
4216
        if ( ! empty($alias) )
4217
        {
4218
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
4219
            {
4220
                return "{$alias}.{$field}";
4221
            }
4222
4223
            return null;
4224
        }
4225
        foreach ( $associations as $assocAlias => $config )
4226
        {
4227
            list(, , $assocField, $fieldAlias) = $config;
4228
            if ( $fieldAlias === $field )
4229
            {
4230
                return $assocField;
4231
            }
4232
        }
4233
4234
        return null;
4235
    }
4236
4237
    /**
4238
     * @param $keysOnly
4239
     * @return array
4240
     */
4241
4242
    public function columns(bool $keysOnly=true) : array
4243
    {
4244
        return $keysOnly ? array_keys($this->_schema) : $this->_schema;
4245
    }
4246
4247
    /**
4248
     * @param string $field
4249
     * @param mixed $value
4250
     * @param array $pdoMetaData
4251
     * @return float|int
4252
     * @throws Exception
4253
     */
4254
    protected function _fixTypeToSentinel(string $field, $value, array $pdoMetaData=[])
4255
    {
4256
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4257
4258
        $fieldType      = strtolower($pdoMetaData['native_type'] ??''?: '');
4259
        if ( ! $fieldType )
4260
        {
4261
            if ( empty($this->_schema) )
4262
            {
4263
                return $value;
4264
            }
4265
            $columns    = $this->getColumns(false);
4266
            Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4267
4268
            $fieldType = $columns[$field] ?: null;
4269
        }
4270
4271
4272
        // Don't cast invalid values... only those that can be cast cleanly
4273
        switch ( $fieldType )
4274
        {
4275
            case 'varchar':
4276
            case 'var_string':
4277
            case 'string':
4278
            case 'text';
4279
            case 'date':
4280
            case 'datetime':
4281
            case 'timestamp':
4282
            case 'blob':
4283
4284
                return (string)$value;
4285
4286
            case 'int':
4287
            case 'integer':
4288
            case 'tinyint':
4289
            case 'tiny':
4290
            case 'long':
4291
            case 'longlong':
4292
4293
                return (int)$value;
4294
4295
            case 'decimal':
4296
            case 'newdecimal':
4297
4298
                return (float)$value;
4299
4300
            default:
4301
4302
                throw new Exception('Unknown type: ' . $fieldType);
4303
                return $value;
0 ignored issues
show
Unused Code introduced by
return $value; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
4304
        }
4305
    }
4306
4307
    /**
4308
     * @param string $field
4309
     * @param mixed $value
4310
     * @param bool|false $permissive
4311
     * @return float|int|null|string
4312
     */
4313
    protected function _fixType(string $field, $value, bool $permissive=false)
4314
    {
4315
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4316
        if ( empty($this->_schema) || ( ! array_key_exists($field, $this->_schema) && $permissive ) )
4317
        {
4318
            return $value;
4319
        }
4320
        $columns    = $this->getColumns(false);
4321
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4322
4323
        $fieldType = ! empty($columns[$field]) ? $columns[$field] : null;
4324
4325
        if ( is_null($value) )
4326
        {
4327
            return null;
4328
        }
4329
        // return on null, '' but not 0
4330
        if ( ! is_numeric($value) && empty($value) )
4331
        {
4332
            return null;
4333
        }
4334
        // Don't cast invalid values... only those that can be cast cleanly
4335
        switch ( $fieldType )
4336
        {
4337
            case 'varchar':
4338
            case 'text';
4339
            case 'date':
4340
            case 'datetime':
4341
            case 'timestamp':
4342
4343
                // return on null, '' but not 0
4344
                return ! is_numeric($value) && empty($value) ? null : (string)$value;
4345
4346
            case 'int':
4347
4348
                if ( $field === 'id' || substr($field, -3) === '_id' )
4349
                {
4350
                    return $value ? (int)$value : null;
4351
                }
4352
4353
                return ! is_numeric($value) ? null : (int)$value;
4354
4355
            case 'decimal':
4356
4357
                return ! is_numeric($value) ? null : (float)$value;
4358
4359
            default:
4360
4361
                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...
4362
        }
4363
    }
4364
4365
    /**
4366
     * @param stdClass $record
4367
     * @param string $type
4368
     * @return stdClass
4369
     */
4370
    public function fixTypesToSentinel(stdClass $record, string $type='') : stdClass
4371
    {
4372
        foreach ( $this->row_meta_data as $column => $pdoMetaData )
4373
        {
4374
            if ( ! property_exists($record, $column) )
4375
            {
4376
                continue;
4377
            }
4378
            $record->{$column} = $this->_fixTypeToSentinel($column, $record->{$column}, $pdoMetaData);
4379
        }
4380
        // PDO might not be able to generate the meta data to sniff types
4381
        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...
4382
        {
4383
            foreach ( $this->getColumns(false) as $column => $fieldType )
4384
            {
4385
                if ( ! property_exists($record, $column) )
4386
                {
4387
                    continue;
4388
                }
4389
                $record->{$column} = $this->_fixTypeToSentinel($column, $record->{$column});
4390
            }
4391
        }
4392
4393
        unset($type);
4394
4395
        return $record;
4396
    }
4397
4398
    /**
4399
     * @param stdClass $record
4400
     * @param string   $type
4401
     * @return stdClass
4402
     * @throws Exception
4403
     */
4404
    public function applyHandlers(stdClass $record, string $type='INSERT') : stdClass
4405
    {
4406
        $this->_compileHandlers();
4407
        $this->_errors                  = [];
4408
        // Disable per field exceptions so we can capture all errors for the record
4409
        $tmpExceptions                  = $this->_validation_exceptions;
4410
        $this->_validation_exceptions   = false;
4411
        foreach ( $this->_handlers as $field => $fn_validator )
4412
        {
4413
            if ( ! property_exists($record, $field) )
4414
            {
4415
                // If the operation is an update it can be a partial update
4416
                if ( $type === self::SAVE_UPDATE )
4417
                {
4418
                    continue;
4419
                }
4420
                $record->{$field}               = null;
4421
            }
4422
            $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...
4423
        }
4424
        $this->_validation_exceptions = $tmpExceptions;
4425
        if ( $this->_validation_exceptions && ! empty($this->_errors) )
4426
        {
4427
            throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4428
        }
4429
4430
        return $record;
4431
    }
4432
4433
4434
    /**
4435
     * @param stdClass $record
4436
     * @param array    $fields
4437
     * @param string   $type
4438
     * @return bool
4439
     */
4440
    protected function uniqueCheck(stdClass $record, array $fields, string $type) : bool
4441
    {
4442
        if ( $type === self::SAVE_UPDATE )
4443
        {
4444
            $this->whereNot($this->_primary_key, $record->{$this->_primary_key});
4445
        }
4446
        foreach ( $fields as $field )
4447
        {
4448
            $this->where($field, $record->{$field});
4449
        }
4450
4451
        return (int)$this->count() > 0;
4452
    }
4453
4454
    /**
4455
     * @param string $field
4456
     * @param mixed $value
4457
     * @param string $type
4458
     * @param stdClass $record
4459
     * @return null
4460
     * @throws Exception
4461
     */
4462
    protected function applyHandler(string $field, $value, string $type='', stdClass $record=null)
4463
    {
4464
        $this->_compileHandlers();
4465
        $fnHandler = ! empty($this->_handlers[$field]) ? $this->_handlers[$field] : null;
4466
        if ( is_callable($fnHandler) )
4467
        {
4468
            try
4469
            {
4470
                $value = $fnHandler($field, $value, $type, $record);
4471
            }
4472
            catch( Exception $e )
4473
            {
4474
                $this->_errors[$field][] = $e->getMessage();
4475
                if ( $this->_validation_exceptions && ! empty($this->_errors) )
4476
                {
4477
                    throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4478
                }
4479
4480
                return null;
4481
            }
4482
        }
4483
4484
        return $value;
4485
    }
4486
4487
    /**
4488
     * @param string $start
4489
     * @param string $end
4490
     * @param string $hayStack
4491
     * @return mixed
4492
     */
4493
    public static function between(string $start, string $end, string $hayStack) : string
4494
    {
4495
        return static::before($end, static::after($start, $hayStack));
4496
    }
4497
4498
    /**
4499
     * @param string     $needle
4500
     * @param string     $hayStack
4501
     * @param bool $returnOrigIfNeedleNotExists
4502
     * @return mixed
4503
     */
4504
    public static function before(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4505
    {
4506
        $result = mb_substr($hayStack, 0, mb_strpos($hayStack, $needle));
4507
        if ( !$result && $returnOrigIfNeedleNotExists )
4508
        {
4509
            return $hayStack;
4510
        }
4511
4512
        return $result;
4513
    }
4514
4515
    /**
4516
     * @param string     $needle
4517
     * @param string     $hayStack
4518
     * @param bool $returnOrigIfNeedleNotExists
4519
     * @return string
4520
     */
4521
    public static function after(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4522
    {
4523
        if ( ! is_bool(mb_strpos($hayStack, $needle)) )
4524
        {
4525
            return mb_substr($hayStack, mb_strpos($hayStack, $needle) + mb_strlen($needle));
4526
        }
4527
4528
        return $returnOrigIfNeedleNotExists ? $hayStack : '';
4529
    }
4530
4531
    /**
4532
     * @return int
4533
     */
4534
    public function getUserId()
4535
    {
4536
        return 0;
4537
    }
4538
4539
    /**
4540
     * @param string $entity
4541
     * @param int $id
4542
     * @return int
4543
     */
4544
    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...
4545
    {
4546
        return 31;
4547
    }
4548
4549
    /**
4550
     * @param string|int|null $time
4551
     * @return string
4552
     */
4553
    public static function date($time=null) : string
4554
    {
4555
        return date('Y-m-d', static::getTime($time));
4556
    }
4557
4558
    /**
4559
     * @param string|int|null $time
4560
     * @return string
4561
     */
4562
    public static function dateTime($time=null) : string
4563
    {
4564
        return date('Y-m-d H:i:s', static::getTime($time));
4565
    }
4566
4567
    /**
4568
     * @param string|int|null $time
4569
     * @return string
4570
     */
4571
    public static function atom($time=null) : string
4572
    {
4573
        return date('Y-m-d\TH:i:sP', static::getTime($time));
4574
    }
4575
4576
    /**
4577
     * @param string|int|null $time
4578
     * @return int
4579
     */
4580
    public static function getTime($time=null) : int
4581
    {
4582
        if ( ! $time )
4583
        {
4584
            return time();
4585
        }
4586
        if ( is_int($time) )
4587
        {
4588
            return $time;
4589
        }
4590
4591
        return strtotime($time);
4592
    }
4593
4594
    /**
4595
     * @param int $id
4596
     * @param int $cacheTtl
4597
     * @return string
4598
     */
4599
    public function getCodeById(int $id, int $cacheTtl=self::ONE_DAY) : string
4600
    {
4601
        Assert($id)->id();
4602
        $code   = $this->defaultFilters()->fetchStr($this->getDisplayColumn(), $id, $cacheTtl);
4603
        Assert($code)->notEmpty();
4604
4605
        return $code;
4606
    }
4607
4608
    /**
4609
     * @param array $authUserRoles
4610
     * @param int   $authUserId
4611
     * @return FluentPdoModel
4612
     */
4613
    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...
4614
    {
4615
        return $this;
4616
    }
4617
4618
    /**
4619
     * @param int    $id
4620
     * @param string[] $authUserRoles
4621
     * @param int    $authUserId
4622
     * @return bool
4623
     */
4624
    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...
4625
    {
4626
        return true;
4627
    }
4628
}
4629