Completed
Push — master ( baf761...81d404 )
by Terry
03:36
created

FluentPdoModel::indexExists()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 3
nop 3
dl 0
loc 22
rs 8.9197
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 string */
177
    protected $_tmp_table_prefix        = 'tmp_';
178
179
    /** @var null|string */
180
    protected $_built_query             = '';
181
182
    /** @var array  */
183
    protected $_handlers                = [];
184
185
    /** @var bool User want to directly specify the fields */
186
    protected $_explicit_select_mode    = false;
187
188
    /** @var string[] */
189
    protected $_update_raw              = [];
190
191
    protected $_max_callback_failures   = -1;
192
193
    protected $_num_callback_failures   = 0;
194
195
    protected $_filter_on_fetch         = false;
196
197
    protected $_log_filter_changes      = true;
198
199
    protected $_include_count           = false;
200
201
    /** @var string */
202
    static protected $_model_namespace  = '';
203
204
    /** @var bool */
205
    protected $_validation_exceptions   = true;
206
207
    /** @var array */
208
    protected $_paging_meta             = [];
209
210
    /** @var bool */
211
    protected $_soft_deletes            = true;
212
213
214
    /** @var bool  */
215
    protected $_allow_meta_override     = false;
216
217
    /** @var bool  */
218
    protected $_skip_meta_updates       = false;
219
220
    /** @var  bool */
221
    protected $_add_update_alias        = false;
222
223
    /** @var int  */
224
    protected $_default_max             = 250;
225
226
    /** @var array  */
227
    protected $removeUnauthorisedFields = [];
228
229
    protected $_can_generic_update      = true;
230
    protected $_can_generic_add         = true;
231
    protected $_can_generic_delete      = true;
232
233
    /** @var  array */
234
    protected $row_meta_data            = [];
235
236
    protected $excluded_search_cols     = [];
237
238
239
    /** @var array  */
240
    protected $globalRemoveUnauthorisedFields = [
241
        '/global_table_meta#view'               => [
242
            self::CREATOR_ID_FIELD,
243
            self::CREATOR_FIELD,
244
            self::CREATED_TS_FIELD,
245
            self::MODIFIER_ID_FIELD,
246
            self::MODIFIER_FIELD,
247
            self::MODIFIED_TS_FIELD,
248
            self::STATUS_FIELD,
249
        ],
250
    ];
251
252
253
    /**
254
     * @param AbstractPdo|null $connection
255
     */
256
    public function __construct(AbstractPdo $connection=null)
257
    {
258
        $connection         = $connection ?: ConnectionPool::get($this->_connection_name);
259
        $this->_connection  = $connection;
260
        $this->_log_queries = $connection->logQueries();
261
        $this->init();
262
    }
263
264
    public function init()
265
    {}
266
267
    /**
268
     * @return AbstractPdo
269
     * @throws Exception
270
     */
271
    public function getPdo() : AbstractPdo
272
    {
273
        return $this->_connection;
274
    }
275
276
    /**
277
     * @return LoggerInterface
278
     */
279
    public function getLogger() : LoggerInterface
280
    {
281
        return $this->_connection->getLogger();
282
    }
283
284
    /**
285
     * @return CacheInterface
286
     */
287
    public function getCache() : CacheInterface
288
    {
289
        return $this->_connection->getCache();
290
    }
291
292
    /**
293
     * Define the working table and create a new instance
294
     *
295
     * @param string $tableName - Table name
296
     * @param string $alias     - The table alias name
297
     * @param string $displayColumn
298
     * @param string $primaryKeyName
299
     *
300
     * @return FluentPdoModel|$this
301
     */
302
    public function table(string $tableName, string $alias='', string $displayColumn='', string $primaryKeyName='id') : FluentPdoModel
303
    {
304
        return $this->reset()
305
            ->tableName($tableName)
306
            ->tableAlias($alias)
307
            ->displayColumn($displayColumn)
308
            ->primaryKeyName($primaryKeyName);
309
    }
310
311
    /**
312
     * @param string $primaryKeyName
313
     * @return FluentPdoModel|$this
314
     */
315
    public function primaryKeyName(string $primaryKeyName) : FluentPdoModel
316
    {
317
        $this->_primary_key = $primaryKeyName;
318
319
        return $this;
320
    }
321
322
    /**
323
     * @param string $tableName
324
     *
325
     * @return FluentPdoModel|$this
326
     */
327
    public function tableName(string $tableName) : FluentPdoModel
328
    {
329
        $this->_table_name  = $tableName;
330
331
        return $this;
332
    }
333
334
    /**
335
     * @param $explicitSelect
336
     *
337
     * @return FluentPdoModel|$this
338
     */
339
    public function explicitSelectMode(bool $explicitSelect=true) : FluentPdoModel
340
    {
341
        $this->_explicit_select_mode  = (bool)$explicitSelect;
342
343
        return $this;
344
    }
345
346
    /**
347
     * @param bool $filterOnFetch
348
     *
349
     * @return FluentPdoModel|$this
350
     */
351
    public function filterOnFetch(bool $filterOnFetch=true) : FluentPdoModel
352
    {
353
        $this->_filter_on_fetch  = (bool)$filterOnFetch;
354
355
        return $this;
356
    }
357
358
    /**
359
     * @param bool $logFilterChanges
360
     *
361
     * @return FluentPdoModel|$this
362
     */
363
    public function logFilterChanges(bool $logFilterChanges=true) : FluentPdoModel
364
    {
365
        $this->_log_filter_changes  = (bool)$logFilterChanges;
366
367
        return $this;
368
    }
369
370
    /**
371
     * Return the name of the table
372
     *
373
     * @return string
374
     */
375
    public function getTableName() : string
376
    {
377
        return $this->_table_name;
378
    }
379
380
    /**
381
     * @return string
382
     */
383
    public function getDisplayColumn() : string
384
    {
385
        return $this->_display_column;
386
    }
387
388
    /**
389
     * Set the display column
390
     *
391
     * @param string $column
392
     *
393
     * @return FluentPdoModel|$this
394
     */
395
    public function displayColumn(string $column) : FluentPdoModel
396
    {
397
        $this->_display_column = $column;
398
399
        return $this;
400
    }
401
    /**
402
     * Set the table alias
403
     *
404
     * @param string $alias
405
     *
406
     * @return FluentPdoModel|$this
407
     */
408
    public function tableAlias(string $alias) : FluentPdoModel
409
    {
410
        $this->_table_alias = $alias;
411
412
        return $this;
413
    }
414
415
    /**
416
     * @param int $cacheTtl
417
     * @return FluentPdoModel|$this
418
     * @throws Exception
419
     */
420
    protected function _cacheTtl(int $cacheTtl) : FluentPdoModel
421
    {
422
        Assert($cacheTtl)->int('Cache ttl must be either -1 for no cache, 0 for default ttl or an integer for a custom ttl');
423
        if ( $cacheTtl !== self::CACHE_NO && ! is_null($this->_pdo_stmt) )
424
        {
425
            throw new Exception("You cannot cache pre-executed queries");
426
        }
427
        $this->_cache_ttl = $cacheTtl;
428
429
        return $this;
430
    }
431
432
    /**
433
     * @return string
434
     */
435
    public function getTableAlias() : string
436
    {
437
        return $this->_table_alias;
438
    }
439
440
    /**
441
     * @param array $associations
442
     *
443
     * @return FluentPdoModel|$this
444
     */
445
    public function associations(array $associations) : FluentPdoModel
446
    {
447
        $this->_associations = $associations;
448
449
        return $this;
450
    }
451
452
    /**
453
     * @param string $alias
454
     * @param array $definition
455
     * @return FluentPdoModel|$this
456
     */
457
    public function setBelongsTo(string $alias, array $definition) : FluentPdoModel
458
    {
459
        Assert($alias)->notEmpty();
460
        Assert($definition)->isArray()->count(4);
461
462
        $this->_associations['belongsTo'][$alias] = $definition;
463
464
        return $this;
465
    }
466
467
    /**
468
     * @param $alias
469
     * @param $displayField
470
     * @return FluentPdoModel|$this
471
     * @throws \Terah\Assert\AssertionFailedException
472
     */
473
    public function setBelongsToDisplayField(string $alias, string $displayField) : FluentPdoModel
474
    {
475
        Assert($alias)->notEmpty();
476
        Assert($this->_associations['belongsTo'])->keyExists($alias);
477
        Assert($displayField)->notEmpty();
478
479
        $this->_associations['belongsTo'][$alias][2] = $displayField;
480
481
        return $this;
482
    }
483
484
    /**
485
     * @param PDOStatement $stmt
486
     *
487
     * @param PDOStatement $stmt
488
     * @param Closure $fnCallback
489
     * @return bool|stdClass
490
     */
491
    public function fetchRow(PDOStatement $stmt, Closure $fnCallback=null)
492
    {
493
        if ( ! ( $record = $stmt->fetch(PDO::FETCH_OBJ) ) )
494
        {
495
            $this->row_meta_data    = [];
496
497
            return false;
498
        }
499
        $this->row_meta_data  = $this->row_meta_data  ?: $this->getColumnMeta($stmt, $record);
500
        $record = $this->onFetch($record);
501
        if ( empty($fnCallback) )
502
        {
503
            return $record;
504
        }
505
        $record             = $fnCallback($record);
506
        if ( is_null($record) )
507
        {
508
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
509
        }
510
        unset($fnCallback);
511
512
        return $record;
513
    }
514
515
    /**
516
     * @param PDOStatement $stmt
517
     * @param $record
518
     * @return array
519
     */
520
    protected function getColumnMeta(PDOStatement $stmt, $record) : array
521
    {
522
        $meta                   = [];
523
        if ( ! $this->_connection->supportsColumnMeta() )
524
        {
525
            return $meta;
526
        }
527
        foreach(range(0, $stmt->columnCount() - 1) as $index)
528
        {
529
            $data                   = $stmt->getColumnMeta($index);
530
            $meta[$data['name']]    = $data;
531
        }
532
        foreach ( $record as $field => $value )
533
        {
534
            Assert($meta)->keyExists($field);
535
        }
536
537
        return $meta;
538
    }
539
540
    /**
541
     * @param array $schema
542
     *
543
     * @return FluentPdoModel|$this
544
     */
545
    public function schema(array $schema) : FluentPdoModel
546
    {
547
        $this->_schema = $schema;
548
549
        return $this;
550
    }
551
552
    /**
553
     * @param string|array $field
554
     * @param $type
555
     * @return FluentPdoModel|$this
556
     */
557
    public function addSchema($field, string $type) : FluentPdoModel
558
    {
559
        if ( is_array($field) )
560
        {
561
            foreach ( $field as $field_name => $type_def )
562
            {
563
                $this->addSchema($field_name, $type_def);
564
            }
565
566
            return $this;
567
        }
568
        Assert($field)->string()->notEmpty();
569
        $this->_schema[$field] = $type;
570
571
        return $this;
572
    }
573
574
    /**
575
     * @param $keysOnly
576
     * @return array
577
     */
578
    public function getColumns(bool $keysOnly=true) : array
579
    {
580
        return $keysOnly ? array_keys($this->_schema) : $this->_schema;
581
    }
582
583
    /**
584
     * Get the primary key name
585
     *
586
     * @return string
587
     */
588
    public function getPrimaryKeyName() : string
589
    {
590
        return $this->_formatKeyName($this->_primary_key, $this->_table_name);
591
    }
592
593
    /**
594
     * @param string $query
595
     * @param array $parameters
596
     *
597
     * @return bool
598
     * @throws Exception
599
     */
600
    public function execute(string $query, array $parameters=[]) : bool
601
    {
602
        list($this->_built_query, $ident)  = $this->_logQuery($query, $parameters);
603
        try
604
        {
605
            $this->_pdo_stmt        = $this->getPdo()->prepare($query);
606
            $result                 = $this->_pdo_stmt->execute($parameters);
607
            if ( false === $result )
608
            {
609
                $this->_pdo_stmt        = null;
610
                throw new PDOException("The query failed to execute.");
611
            }
612
        }
613
        catch( Exception $e )
614
        {
615
            $built_query        = $this->_built_query ? $this->_built_query : $this->buildQuery($query, $parameters);
616
            $this->getLogger()->error("FAILED: \n\n{$built_query}\n WITH ERROR:\n" . $e->getMessage());
617
            $this->_pdo_stmt    = null;
618
619
            throw $e;
620
        }
621
        $this->_logSlowQueries($ident, $this->_built_query);
622
623
        return $result;
624
    }
625
626
    /**
627
     * @param string $query
628
     * @param array $params
629
     * @return FluentPdoModel|$this
630
     */
631
    public function query(string $query, array $params=[]) : FluentPdoModel
632
    {
633
        $this->_raw_sql             = $query;
634
        $this->_where_parameters    = $params;
635
636
        return $this;
637
    }
638
639
    /**
640
     * @param string $sql
641
     * @param array $params
642
     *
643
     * @return string
644
     */
645
    public function buildQuery(string $sql, array $params=[]) : string
646
    {
647
        $indexed = $params == array_values($params);
648
        if ( $indexed )
649
        {
650
            foreach ( $params as $key => $val )
651
            {
652
                $val    = is_string($val) ? "'{$val}'" : $val;
653
                $val    = is_null($val) ? 'NULL' : $val;
654
                $sql    = preg_replace('/\?/', $val, $sql, 1);
655
            }
656
657
            return $sql;
658
        }
659
660
        uksort($params, function ($a, $b) {
661
            return strlen($b) - strlen($a);
662
        });
663
        foreach ( $params as $key => $val )
664
        {
665
            $val    = is_string($val) ? "'{$val}'" : $val;
666
            $val    = is_null($val) ? 'NULL' : $val;
667
            $sql    = str_replace(":$key", $val, $sql);
668
            //$sql    = str_replace("$key", $val, $sql);
669
        }
670
671
        return $sql;
672
    }
673
674
    /**
675
     * @param stdClass $record
676
     *
677
     * @return stdClass
678
     */
679
    protected function _trimAndLowerCaseKeys(stdClass $record) : stdClass
680
    {
681
        $fnTrimStrings = function($value) {
682
            return is_string($value) ? trim($value) : $value;
683
        };
684
        $record = array_map($fnTrimStrings, array_change_key_case((array)$record, CASE_LOWER));
685
        unset($fnTrimStrings);
686
687
        return (object)$record;
688
    }
689
690
    /**
691
     * Return the number of affected row by the last statement
692
     *
693
     * @return int
694
     */
695
    public function rowCount() : int
696
    {
697
        $stmt = $this->fetchStmt();
698
699
        return $stmt ? $stmt->rowCount() : 0;
700
    }
701
702
    /**
703
     * @return PDOStatement
704
     * @throws PDOException
705
     */
706
    public function fetchStmt()
707
    {
708
        if ( null === $this->_pdo_stmt )
709
        {
710
            $this->execute($this->getSelectQuery(), $this->_getWhereParameters());
711
        }
712
713
        return $this->_pdo_stmt;
714
    }
715
716
    /**
717
     * @return array
718
     */
719
    public function fetchSqlQuery() : array
720
    {
721
        $clone      = clone $this;
722
        $query      = $clone->getSelectQuery();
723
        $params     = $clone->_getWhereParameters();
724
        $result     = [$query, $params];
725
        unset($clone->_handlers, $clone, $query, $params);
726
727
        return $result;
728
    }
729
730
    /**
731
     * @param string $tableName
732
     * @param bool  $dropIfExists
733
     * @param array $indexes
734
     * @return boolean
735
     * @throws Exception
736
     */
737
    public function fetchIntoMemoryTable(string $tableName, bool $dropIfExists=true, array $indexes=[]) : bool
738
    {
739
        $tableName      = preg_replace('/[^A-Za-z0-9_]+/', '', $tableName);
740
        $tableName      = $this->_tmp_table_prefix . preg_replace('/^' . $this->_tmp_table_prefix . '/', '', $tableName);
741
        if ( $dropIfExists )
742
        {
743
            $this->execute("DROP TABLE IF EXISTS {$tableName}");
744
        }
745
        $indexSql = [];
746
        foreach ( $indexes as $name => $column )
747
        {
748
            $indexSql[] = "INDEX {$name} ({$column})";
749
        }
750
        $indexSql       = implode(", ", $indexSql);
751
        $indexSql       = empty($indexSql) ? '' : "({$indexSql})";
752
        list($querySql, $params) = $this->fetchSqlQuery();
753
        $sql = <<<SQL
754
        CREATE TEMPORARY TABLE {$tableName} {$indexSql} ENGINE=MEMORY {$querySql}
755
SQL;
756
757
        return $this->execute($sql, $params);
758
    }
759
760
    /**
761
     * @param string $keyedOn
762
     * @param int $cacheTtl
763
     * @return stdClass[]
764
     */
765
    public function fetch(string $keyedOn='', int $cacheTtl=self::CACHE_NO) : array
766
    {
767
        $this->_cacheTtl($cacheTtl);
768
        $fnCallback         = function() use ($keyedOn) {
769
770
            $stmt               = $this->fetchStmt();
771
            $rows               = [];
772
            while ( $record = $this->fetchRow($stmt) )
773
            {
774
                if ( $record === false ) continue; // For scrutinizer...
775
                if ( $keyedOn && property_exists($record, $keyedOn) )
776
                {
777
                    $rows[$record->{$keyedOn}] = $record;
778
                    continue;
779
                }
780
                $rows[]         = $record;
781
            }
782
            $this->reset();
783
784
            return $rows;
785
        };
786
        if ( $this->_cache_ttl === self::CACHE_NO )
787
        {
788
            return $fnCallback();
789
        }
790
        $table              = $this->getTableName();
791
        $id                 = $this->_parseWhereForPrimaryLookup();
792
        $id                 = $id ? "/{$id}" : '';
793
        list($sql, $params) = $this->fetchSqlQuery();
794
        $sql                = $this->buildQuery($sql, $params);
795
        $cacheKey           = "/{$table}{$id}/" . md5(json_encode([
796
            'sql'       => $sql,
797
            'keyed_on'  => $keyedOn,
798
        ]));
799
        $data = $this->_cacheData($cacheKey, $fnCallback, $this->_cache_ttl);
800
801
        return is_array($data) ? $data : [];
802
    }
803
804
    /**
805
     * @return string
806
     */
807
    protected function _parseWhereForPrimaryLookup() : string
808
    {
809
        if ( ! ( $alias = $this->getTableAlias() ) )
810
        {
811
            return '';
812
        }
813
        foreach ( $this->_where_conditions as $idx => $conds )
814
        {
815
            if ( ! empty($conds['STATEMENT']) && $conds['STATEMENT'] === "{$alias}.id = ?" )
816
            {
817
                return ! empty($conds['PARAMS'][0]) ? (string)$conds['PARAMS'][0] : '';
818
            }
819
        }
820
821
        return '';
822
    }
823
824
    /**
825
     * @param string $cacheKey
826
     * @param Closure $func
827
     * @param int $cacheTtl - 0 for default ttl, -1 for no cache or int for custom ttl
828
     * @return mixed
829
     */
830
    protected function _cacheData(string $cacheKey, Closure $func, int $cacheTtl=self::CACHE_DEFAULT)
831
    {
832
        if ( $cacheTtl === self::CACHE_NO )
833
        {
834
            /** @noinspection PhpVoidFunctionResultUsedInspection */
835
            return $func->__invoke();
836
        }
837
        $data = $this->getCache()->get($cacheKey);
838
        if ( $data && is_object($data) && property_exists($data, 'results') )
839
        {
840
            $this->getLogger()->debug("Cache hit on {$cacheKey}");
841
            return $data->results;
842
        }
843
        $this->getLogger()->debug("Cache miss on {$cacheKey}");
844
        /** @noinspection PhpVoidFunctionResultUsedInspection */
845
        $data = (object)[
846
            // Watch out... invoke most likely calls reset
847
            // which clears the model params like _cache_ttl
848
            'results' => $func->__invoke(),
849
        ];
850
        try
851
        {
852
            // The cache engine expects null for the default cache value
853
            $cacheTtl = $cacheTtl === self::CACHE_DEFAULT ? 0 : $cacheTtl;
854
            /** @noinspection PhpMethodParametersCountMismatchInspection */
855
            if ( ! $this->getCache()->set($cacheKey, $data, $cacheTtl) )
856
            {
857
                throw new \Exception("Could not save data to cache");
858
            }
859
            return $data->results;
860
        }
861
        catch (\Exception $e)
862
        {
863
            $this->getLogger()->error($e->getMessage(), $e->getTrace());
864
865
            return $data->results;
866
        }
867
    }
868
869
    /**
870
     * @param string $cacheKey
871
     * @return bool
872
     */
873
    public function clearCache(string $cacheKey) : bool
874
    {
875
        return $this->getCache()->delete($cacheKey);
876
    }
877
878
    /**
879
     * @param string $table
880
     * @return bool
881
     */
882
    public function clearCacheByTable(string $table='') : bool
883
    {
884
        $table  = $table ?: $this->getTableName();
885
        if ( ! $table )
886
        {
887
            return true;
888
        }
889
890
        return $this->clearCache("/{$table}/");
891
    }
892
893
    /**
894
     * @param Closure $fnCallback
895
     * @return int
896
     */
897
    public function fetchCallback(Closure $fnCallback) : int
898
    {
899
        $successCnt    = 0;
900
        $stmt           = $this->fetchStmt();
901
        while ( $this->_tallySuccessCount($stmt, $fnCallback, $successCnt) )
902
        {}
903
904
        return $successCnt;
905
    }
906
907
    /**
908
     * @param Closure $fnCallback
909
     * @param string  $keyedOn
910
     * @return array
911
     */
912
    public function fetchObjectsByCallback(Closure $fnCallback, string $keyedOn='') : array
913
    {
914
        $stmt       = $this->fetchStmt();
915
        $rows       = [];
916
        while ( $record = $this->fetchRow($stmt, $fnCallback) )
917
        {
918
            if ( $keyedOn && property_exists($record, $keyedOn) )
919
            {
920
                $rows[$record->{$keyedOn}] = $record;
921
                continue;
922
            }
923
            $rows[] = $record;
924
        }
925
        $this->reset();
926
927
        return $rows;
928
    }
929
930
    /**
931
     * @param $numFailures
932
     * @return FluentPdoModel|$this
933
     */
934
    public function maxCallbackFailures(int $numFailures) : FluentPdoModel
935
    {
936
        Assert($numFailures)->int();
937
        $this->_max_callback_failures = $numFailures;
938
939
        return $this;
940
    }
941
942
    /**
943
     * @param PDOStatement $stmt
944
     * @param Closure $fnCallback
945
     * @param int $successCnt
946
     * @return bool|null|stdClass
947
     */
948
    protected function _tallySuccessCount(PDOStatement $stmt, Closure $fnCallback, int &$successCnt)
949
    {
950
        $record = $this->fetchRow($stmt);
951
        if ( $record === false )
952
        {
953
            return false;
954
        }
955
        $record = $fnCallback($record);
956
        // Callback return null then we want to exit the fetch loop
957
        if ( is_null($record) )
958
        {
959
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
960
            return null;
961
        }
962
        // The not record then don't bump the tally
963
        if ( ! $record )
964
        {
965
            $this->_num_callback_failures++;
966
            if ( $this->_max_callback_failures !== -1 && $this->_num_callback_failures >= $this->_max_callback_failures )
967
            {
968
                $this->getLogger()->error("The callback has failed {$this->_max_callback_failures} times... aborting...");
969
                $successCnt = null;
970
                return null;
971
            }
972
973
            return true;
974
        }
975
        $successCnt++;
976
977
        return $record;
978
    }
979
980
    /**
981
     * @return bool
982
     */
983
    public function canGenericUpdate() : bool
984
    {
985
        return $this->_can_generic_update;
986
    }
987
988
    /**
989
     * @return bool
990
     */
991
    public function canGenericAdd() : bool
992
    {
993
        return $this->_can_generic_add;
994
    }
995
996
    /**
997
     * @return bool
998
     */
999
    public function canGenericDelete() : bool
1000
    {
1001
        return $this->_can_generic_delete;
1002
    }
1003
1004
    /**
1005
     * @param string $keyedOn
1006
     * @param string $valueField
1007
     * @param int $cacheTtl
1008
     * @return mixed
1009
     */
1010
    public function fetchList(string $keyedOn='', string $valueField='', int $cacheTtl=self::CACHE_NO) : array
1011
    {
1012
        $keyedOn            = $keyedOn ?: $this->getPrimaryKeyName();
1013
        $valueField         = $valueField ?: $this->getDisplayColumn();
1014
        $keyedOnAlias       = strtolower(str_replace('.', '_', $keyedOn));
1015
        $valueFieldAlias    = strtolower(str_replace('.', '_', $valueField));
1016
        if ( preg_match('/ as /i', $keyedOn) )
1017
        {
1018
            list($keyedOn, $keyedOnAlias)   = preg_split('/ as /i', $keyedOn);
1019
            $keyedOn                        = trim($keyedOn);
1020
            $keyedOnAlias                   = trim($keyedOnAlias);
1021
        }
1022
        if ( preg_match('/ as /i', $valueField) )
1023
        {
1024
            list($valueField, $valueFieldAlias) = preg_split('/ as /i', $valueField);
1025
            $valueField                         = trim($valueField);
1026
            $valueFieldAlias                    = trim($valueFieldAlias);
1027
        }
1028
1029
        $this->_cacheTtl($cacheTtl);
1030
        $fnCallback         = function() use ($keyedOn, $keyedOnAlias, $valueField, $valueFieldAlias) {
1031
1032
            $rows = [];
1033
            $stmt = $this->select(null)
1034
                ->select($keyedOn, $keyedOnAlias)
1035
                ->select($valueField, $valueFieldAlias)
1036
                ->fetchStmt();
1037
            while ( $record = $this->fetchRow($stmt) )
1038
            {
1039
                $rows[$record->{$keyedOnAlias}] = $record->{$valueFieldAlias};
1040
            }
1041
1042
            return $rows;
1043
        };
1044
        if ( $this->_cache_ttl === self::CACHE_NO )
1045
        {
1046
            $result = $fnCallback();
1047
            unset($cacheKey, $fnCallback);
1048
1049
            return $result;
1050
        }
1051
        $table              = $this->getTableName();
1052
        $cacheData          = [
1053
            'sql'               => $this->fetchSqlQuery(),
1054
            'keyed_on'          => $keyedOn,
1055
            'keyed_on_alias'    => $keyedOnAlias,
1056
            'value_field'       => $valueField,
1057
            'value_fieldAlias'  => $valueFieldAlias,
1058
        ];
1059
        $cacheKey           = json_encode($cacheData);
1060
        if ( ! $cacheKey )
1061
        {
1062
            $this->getLogger()->warning('Could not generate cache key from data', $cacheData);
1063
            $result = $fnCallback();
1064
            unset($cacheKey, $fnCallback);
1065
1066
            return $result;
1067
        }
1068
        $cacheKey           = md5($cacheKey);
1069
1070
        return $this->_cacheData("/{$table}/list/{$cacheKey}", $fnCallback, $this->_cache_ttl);
1071
    }
1072
1073
    /**
1074
     * @param string $column
1075
     * @param int $cacheTtl
1076
     * @param bool|true $unique
1077
     * @return array
1078
     */
1079
    public function fetchColumn(string $column, int $cacheTtl=self::CACHE_NO, bool $unique=true) : array
1080
    {
1081
        $list = $this->select($column)->fetch('', $cacheTtl);
1082
        foreach ( $list as $idx => $obj )
1083
        {
1084
            $list[$idx] = $obj->{$column};
1085
        }
1086
        return $unique ? array_unique($list) : $list;
1087
    }
1088
1089
    /**
1090
     * @param string $field
1091
     * @param int $itemId
1092
     * @param int $cacheTtl
1093
     * @return mixed|null
1094
     */
1095
    public function fetchField(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO)
1096
    {
1097
        $field      = $field ?: $this->getPrimaryKeyName();
1098
        $object     = $this->select(null)->select($field)->fetchOne($itemId, $cacheTtl);
1099
        if ( ! $object )
1100
        {
1101
            return null;
1102
        }
1103
        // Handle aliases
1104
        if ( preg_match('/ as /i', $field) )
1105
        {
1106
            list($expression, $alias) = preg_split('/ as /i', $field);
1107
            unset($expression);
1108
            $field = trim($alias);
1109
        }
1110
        if ( strpos($field, '.') !== false )
1111
        {
1112
            list($tableAlias, $field) = explode('.', $field);
1113
            unset($tableAlias);
1114
        }
1115
1116
        return property_exists($object, $field) ? $object->{$field} : null;
1117
    }
1118
1119
    /**
1120
     * @param string $field
1121
     * @param int $itemId
1122
     * @param int $cacheTtl
1123
     * @return string
1124
     */
1125
    public function fetchStr(string $field='', $itemId=0, int $cacheTtl=self::CACHE_NO) : string
1126
    {
1127
        return (string)$this->fetchField($field, $itemId, $cacheTtl);
1128
    }
1129
1130
    /**
1131
     * @param int $cacheTtl
1132
     * @return int
1133
     */
1134
    public function fetchId(int $cacheTtl=self::CACHE_NO) : int
1135
    {
1136
        return $this->fetchInt($this->getPrimaryKeyName(), 0, $cacheTtl);
1137
    }
1138
1139
    /**
1140
     * @param string $field
1141
     * @param int $itemId
1142
     * @param int $cacheTtl
1143
     * @return int
1144
     */
1145
    public function fetchInt(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO) : int
1146
    {
1147
        return (int)$this->fetchField($field, $itemId, $cacheTtl);
1148
    }
1149
1150
    /**
1151
     * @param string $field
1152
     * @param int $itemId
1153
     * @param int $cacheTtl
1154
     * @return float
1155
     */
1156
    public function fetchFloat(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO) : float
1157
    {
1158
        return (float)$this->fetchField($field, $itemId, $cacheTtl);
1159
    }
1160
1161
    /**
1162
     * @param string $field
1163
     * @param int $itemId
1164
     * @param int $cacheTtl
1165
     * @return bool
1166
     */
1167
    public function fetchBool(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO) : bool
1168
    {
1169
        return (bool)$this->fetchField($field, $itemId, $cacheTtl);
1170
    }
1171
1172
    /**
1173
     * @param int|null $id
1174
     * @param int $cacheTtl
1175
     * @return stdClass|bool
1176
     */
1177
    public function fetchOne(int $id=0, int $cacheTtl=self::CACHE_NO)
1178
    {
1179
        if ( $id > 0 )
1180
        {
1181
            $this->wherePk($id, true);
1182
        }
1183
        $this->limit(1);
1184
        $fetchAll   = $this->fetch('', $cacheTtl);
1185
1186
        return $fetchAll ? array_shift($fetchAll) : false;
1187
    }
1188
1189
    /**
1190
     * @param int|null $id
1191
     * @param int $cacheTtl
1192
     * @return boolean
1193
     */
1194
    public function fetchExists(int $id=0, int $cacheTtl=self::CACHE_NO) : bool
1195
    {
1196
        if ( $id > 0 )
1197
        {
1198
            $this->wherePk($id, true);
1199
        }
1200
        $cnt = $this->count('*', $cacheTtl);
1201
1202
        return $cnt > 0;
1203
    }
1204
1205
    /*------------------------------------------------------------------------------
1206
                                    Fluent Query Builder
1207
    *-----------------------------------------------------------------------------*/
1208
1209
    /**
1210
     * Create the select clause
1211
     *
1212
     * @param  mixed    $columns  - the column to select. Can be string or array of fields
1213
     * @param  string   $alias - an alias to the column
1214
     * @param boolean $explicitSelect
1215
     * @return FluentPdoModel|$this
1216
     */
1217
    public function select($columns='*', string $alias='', bool $explicitSelect=true) : FluentPdoModel
1218
    {
1219
        if ( $explicitSelect )
1220
        {
1221
            $this->explicitSelectMode();
1222
        }
1223
        if ( $alias && ! is_array($columns) & $columns !== $alias )
1224
        {
1225
            $columns .= " AS {$alias} ";
1226
        }
1227
        if ( $columns === '*' && !empty($this->_schema) )
1228
        {
1229
            $columns = $this->getColumns();
1230
        }
1231
        // Reset the select list
1232
        if ( is_null($columns) || $columns === '' )
1233
        {
1234
            $this->_select_fields = [];
1235
1236
            return $this;
1237
        }
1238
        $columns = is_array($columns) ? $columns : [$columns];
1239
1240
//        if ( empty($this->_select_fields) && $addAllIfEmpty )
1241
//        {
1242
//            $this->select('*');
1243
//        }
1244
        if ( $this->_table_alias )
1245
        {
1246
            $schema = $this->columns();
1247
            foreach ( $columns as $idx => $col )
1248
            {
1249
                if ( in_array($col, $schema) )
1250
                {
1251
                    $columns[$idx] = "{$this->_table_alias}.{$col}";
1252
                }
1253
            }
1254
        }
1255
        $this->_select_fields = array_merge($this->_select_fields, $columns);
1256
1257
        return $this;
1258
    }
1259
1260
    /**
1261
     * @param string $select
1262
     * @return FluentPdoModel|$this
1263
     */
1264
    public function selectRaw(string $select) : FluentPdoModel
1265
    {
1266
        $this->_select_fields[] = $select;
1267
1268
        return $this;
1269
    }
1270
1271
    /**
1272
     * @param bool $logQueries
1273
     *
1274
     * @return FluentPdoModel|$this
1275
     */
1276
    public function logQueries(bool $logQueries=true) : FluentPdoModel
1277
    {
1278
        $this->_log_queries = $logQueries;
1279
1280
        return $this;
1281
    }
1282
1283
    /**
1284
     * @param bool $includeCnt
1285
     *
1286
     * @return FluentPdoModel|$this
1287
     */
1288
    public function includeCount(bool $includeCnt=true) : FluentPdoModel
1289
    {
1290
        $this->_include_count = $includeCnt;
1291
1292
        return $this;
1293
    }
1294
1295
    /**
1296
     * @param bool $distinct
1297
     *
1298
     * @return FluentPdoModel|$this
1299
     */
1300
    public function distinct(bool $distinct=true) : FluentPdoModel
1301
    {
1302
        $this->_distinct = $distinct;
1303
1304
        return $this;
1305
    }
1306
1307
    /**
1308
     * @param array $fields
1309
     * @return FluentPdoModel|$this
1310
     */
1311
    public function withBelongsTo(array $fields=[]) : FluentPdoModel
1312
    {
1313
        if ( ! empty($this->_associations['belongsTo']) )
1314
        {
1315
            foreach ( $this->_associations['belongsTo'] as $alias => $config )
1316
            {
1317
                $addFieldsForJoins = empty($fields) || in_array($config[3], $fields);
1318
                $this->autoJoin($alias, self::LEFT_JOIN, $addFieldsForJoins);
1319
            }
1320
        }
1321
1322
        return $this;
1323
    }
1324
1325
    /**
1326
     * @param string $alias
1327
     * @param bool   $addSelectField
1328
     * @return FluentPdoModel
1329
     */
1330
    public function autoInnerJoin(string $alias, bool $addSelectField=true) : FluentPdoModel
1331
    {
1332
        return $this->autoJoin($alias, self::INNER_JOIN, $addSelectField);
1333
    }
1334
1335
    /**
1336
     * @param string $alias
1337
     * @param string $type
1338
     * @param bool   $addSelectField
1339
     * @return FluentPdoModel|$this
1340
     */
1341
    public function autoJoin(string $alias, string $type=self::LEFT_JOIN, bool $addSelectField=true) : FluentPdoModel
1342
    {
1343
        Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid join... the alias does not exists");
1344
        list($table, $join_col, $field, $fieldAlias)    = $this->_associations['belongsTo'][$alias];
1345
        $tableJoinField                                 = "{$this->_table_alias}.{$join_col}";
1346
        // Extra join onto another second level table
1347
        if ( strpos($join_col, '.') !== false )
1348
        {
1349
            $tableJoinField =  $join_col;
1350
            if ( $addSelectField )
1351
            {
1352
                $this->select($join_col, '', false);
1353
            }
1354
        }
1355
1356
1357
        $condition = "{$alias}.id = {$tableJoinField}";
1358
        if ( in_array($alias, $this->_join_aliases) )
1359
        {
1360
            return $this;
1361
        }
1362
        $this->join($table, $condition, $alias, $type);
1363
        if ( $addSelectField )
1364
        {
1365
            $this->select($field, $fieldAlias, false);
1366
        }
1367
1368
        return $this;
1369
    }
1370
1371
    /**
1372
     * @param array $conditions
1373
     * @return FluentPdoModel
1374
     */
1375
    public function whereArr(array $conditions) : FluentPdoModel
1376
    {
1377
        foreach ($conditions as $key => $val)
1378
        {
1379
            $this->where($key, $val);
1380
        }
1381
1382
        return $this;
1383
    }
1384
    /**
1385
     * Add where condition, more calls appends with AND
1386
     *
1387
     * @param string $condition possibly containing ? or :name
1388
     * @param mixed $parameters accepted by PDOStatement::execute or a scalar value
1389
     * @param mixed ...
1390
     * @return FluentPdoModel|$this
1391
     */
1392
    public function where($condition, $parameters=[]) : FluentPdoModel
1393
    {
1394
        // By default the and_or_operator and wrap operator is AND,
1395
        if ( $this->_wrap_open || ! $this->_and_or_operator )
1396
        {
1397
            $this->_and();
1398
        }
1399
1400
        // where(array("column1" => 1, "column2 > ?" => 2))
1401
        if ( is_array($condition) )
1402
        {
1403
            foreach ($condition as $key => $val)
1404
            {
1405
                $this->where($key, $val);
1406
            }
1407
1408
            return $this;
1409
        }
1410
1411
        $args = func_num_args();
1412
        if ( $args != 2 || strpbrk((string)$condition, '?:') )
1413
        { // where('column < ? OR column > ?', array(1, 2))
1414
            if ( $args != 2 || !is_array($parameters) )
1415
            { // where('column < ? OR column > ?', 1, 2)
1416
                $parameters = func_get_args();
1417
                array_shift($parameters);
1418
            }
1419
        }
1420
        else if ( ! is_array($parameters) )
1421
        {//where(column,value) => column=value
1422
            $condition .= ' = ?';
1423
            $parameters = [$parameters];
1424
        }
1425
        else if ( is_array($parameters) )
1426
        { // where('column', array(1, 2)) => column IN (?,?)
1427
            $placeholders = $this->_makePlaceholders(count($parameters));
1428
            $condition = "({$condition} IN ({$placeholders}))";
1429
        }
1430
1431
        $this->_where_conditions[] = [
1432
            'STATEMENT'   => $condition,
1433
            'PARAMS'      => $parameters,
1434
            'OPERATOR'    => $this->_and_or_operator
1435
        ];
1436
        // Reset the where operator to AND. To use OR, you must call _or()
1437
        $this->_and();
1438
1439
        return $this;
1440
    }
1441
1442
    /**
1443
     * Create an AND operator in the where clause
1444
     *
1445
     * @return FluentPdoModel|$this
1446
     */
1447
    public function _and() : FluentPdoModel
1448
    {
1449
        if ( $this->_wrap_open )
1450
        {
1451
            $this->_where_conditions[] = self::OPERATOR_AND;
1452
            $this->_last_wrap_position = count($this->_where_conditions);
1453
            $this->_wrap_open = false;
1454
1455
            return $this;
1456
        }
1457
        $this->_and_or_operator = self::OPERATOR_AND;
1458
1459
        return $this;
1460
    }
1461
1462
1463
    /**
1464
     * Create an OR operator in the where clause
1465
     *
1466
     * @return FluentPdoModel|$this
1467
     */
1468
    public function _or() : FluentPdoModel
1469
    {
1470
        if ( $this->_wrap_open )
1471
        {
1472
            $this->_where_conditions[]  = self::OPERATOR_OR;
1473
            $this->_last_wrap_position  = count($this->_where_conditions);
1474
            $this->_wrap_open           = false;
1475
1476
            return $this;
1477
        }
1478
        $this->_and_or_operator     = self::OPERATOR_OR;
1479
1480
        return $this;
1481
    }
1482
1483
    /**
1484
     * To group multiple where clauses together.
1485
     *
1486
     * @return FluentPdoModel|$this
1487
     */
1488
    public function wrap() : FluentPdoModel
1489
    {
1490
        $this->_wrap_open           = true;
1491
        $spliced                    = array_splice($this->_where_conditions, $this->_last_wrap_position, count($this->_where_conditions), '(');
1492
        $this->_where_conditions    = array_merge($this->_where_conditions, $spliced);
1493
        array_push($this->_where_conditions,')');
1494
        $this->_last_wrap_position = count($this->_where_conditions);
1495
1496
        return $this;
1497
    }
1498
1499
    /**
1500
     * Where Primary key
1501
     *
1502
     * @param int  $id
1503
     * @param bool $addAlias
1504
     *
1505
     * @return FluentPdoModel|$this
1506
     */
1507
    public function wherePk(int $id, bool $addAlias=true) : FluentPdoModel
1508
    {
1509
        $alias = $addAlias && ! empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1510
1511
        return $this->where($alias . $this->getPrimaryKeyName(), $id);
1512
    }
1513
1514
    /**
1515
     * @param string $name
1516
     * @param bool   $addAlias
1517
     * @return FluentPdoModel
1518
     */
1519
    public function whereDisplayName(string $name, bool $addAlias=true) : FluentPdoModel
1520
    {
1521
        $alias = $addAlias && ! empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1522
1523
        return $this->where($alias . $this->getDisplayColumn(), $name);
1524
    }
1525
1526
    /**
1527
     * WHERE $columnName != $value
1528
     *
1529
     * @param  string   $columnName
1530
     * @param  mixed    $value
1531
     * @return FluentPdoModel|$this
1532
     */
1533
    public function whereNot(string $columnName, $value) : FluentPdoModel
1534
    {
1535
        return $this->where("$columnName != ?", $value);
1536
    }
1537
    /**
1538
     * WHERE $columnName != $value
1539
     *
1540
     * @param  string   $columnName
1541
     * @param  mixed    $value
1542
     * @return FluentPdoModel|$this
1543
     */
1544
    public function whereCoercedNot(string $columnName, $value) : FluentPdoModel
1545
    {
1546
        return $this->where("IFNULL({$columnName}, '') != ?", $value);
1547
    }
1548
1549
    /**
1550
     * WHERE $columnName LIKE $value
1551
     *
1552
     * @param  string   $columnName
1553
     * @param  mixed    $value
1554
     * @return FluentPdoModel|$this
1555
     */
1556
    public function whereLike(string $columnName, $value) : FluentPdoModel
1557
    {
1558
        return $this->where("$columnName LIKE ?", $value);
1559
    }
1560
1561
    /**
1562
     * @param string $columnName
1563
     * @param mixed $value1
1564
     * @param mixed $value2
1565
     * @return FluentPdoModel|$this
1566
     */
1567
    public function whereBetween(string $columnName, $value1, $value2) : FluentPdoModel
1568
    {
1569
        $value1 = is_string($value1) ? trim($value1) : $value1;
1570
        $value2 = is_string($value2) ? trim($value2) : $value2;
1571
1572
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1573
    }
1574
1575
    /**
1576
     * @param string $columnName
1577
     * @param mixed $value1
1578
     * @param mixed $value2
1579
     * @return FluentPdoModel|$this
1580
     */
1581
    public function whereNotBetween(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 NOT BETWEEN ? AND ?", [$value1, $value2]);
1587
    }
1588
1589
    /**
1590
     * @param string $columnName
1591
     * @param string $regex
1592
     * @return FluentPdoModel|$this
1593
     */
1594
    public function whereRegex(string $columnName, string $regex) : FluentPdoModel
1595
    {
1596
        return $this->where("$columnName REGEXP ?", $regex);
1597
    }
1598
1599
    /**
1600
     * @param string $columnName
1601
     * @param string $regex
1602
     * @return FluentPdoModel|$this
1603
     */
1604
    public function whereNotRegex(string $columnName, string $regex) : FluentPdoModel
1605
    {
1606
        return $this->where("$columnName NOT REGEXP ?", $regex);
1607
    }
1608
1609
    /**
1610
     * WHERE $columnName NOT LIKE $value
1611
     *
1612
     * @param  string   $columnName
1613
     * @param  string   $value
1614
     * @return FluentPdoModel|$this
1615
     */
1616
    public function whereNotLike(string $columnName, string $value) : FluentPdoModel
1617
    {
1618
        return $this->where("$columnName NOT LIKE ?", $value);
1619
    }
1620
1621
    /**
1622
     * WHERE $columnName > $value
1623
     *
1624
     * @param  string   $columnName
1625
     * @param  mixed    $value
1626
     * @return FluentPdoModel|$this
1627
     */
1628
    public function whereGt(string $columnName, $value) : FluentPdoModel
1629
    {
1630
        return $this->where("$columnName > ?", $value);
1631
    }
1632
1633
    /**
1634
     * WHERE $columnName >= $value
1635
     *
1636
     * @param  string   $columnName
1637
     * @param  mixed    $value
1638
     * @return FluentPdoModel|$this
1639
     */
1640
    public function whereGte(string $columnName, $value) : FluentPdoModel
1641
    {
1642
        return $this->where("$columnName >= ?", $value);
1643
    }
1644
1645
    /**
1646
     * WHERE $columnName < $value
1647
     *
1648
     * @param  string   $columnName
1649
     * @param  mixed    $value
1650
     * @return FluentPdoModel|$this
1651
     */
1652
    public function whereLt(string $columnName, $value) : FluentPdoModel
1653
    {
1654
        return $this->where("$columnName < ?", $value);
1655
    }
1656
1657
    /**
1658
     * WHERE $columnName <= $value
1659
     *
1660
     * @param  string   $columnName
1661
     * @param  mixed    $value
1662
     * @return FluentPdoModel|$this
1663
     */
1664
    public function whereLte(string $columnName, $value) : FluentPdoModel
1665
    {
1666
        return $this->where("$columnName <= ?", $value);
1667
    }
1668
1669
    /**
1670
     * WHERE $columnName IN (?,?,?,...)
1671
     *
1672
     * @param  string   $columnName
1673
     * @param  array    $values
1674
     * @return FluentPdoModel|$this
1675
     */
1676
    public function whereIn(string $columnName, array $values) : FluentPdoModel
1677
    {
1678
        return $this->where($columnName, array_values($values));
1679
    }
1680
1681
    /**
1682
     * WHERE $columnName NOT IN (?,?,?,...)
1683
     *
1684
     * @param  string   $columnName
1685
     * @param  array    $values
1686
     * @return FluentPdoModel|$this
1687
     */
1688
    public function whereNotIn(string $columnName, array $values) : FluentPdoModel
1689
    {
1690
        $placeholders = $this->_makePlaceholders(count($values));
1691
1692
        return $this->where("({$columnName} NOT IN ({$placeholders}))", $values);
1693
    }
1694
1695
    /**
1696
     * WHERE $columnName IS NULL
1697
     *
1698
     * @param  string   $columnName
1699
     * @return FluentPdoModel|$this
1700
     */
1701
    public function whereNull(string $columnName) : FluentPdoModel
1702
    {
1703
        return $this->where("({$columnName} IS NULL)");
1704
    }
1705
1706
    /**
1707
     * WHERE $columnName IS NOT NULL
1708
     *
1709
     * @param  string   $columnName
1710
     * @return FluentPdoModel|$this
1711
     */
1712
    public function whereNotNull(string $columnName) : FluentPdoModel
1713
    {
1714
        return $this->where("({$columnName} IS NOT NULL)");
1715
    }
1716
1717
    /**
1718
     * @param string $statement
1719
     * @param string $operator
1720
     * @return FluentPdoModel|$this
1721
     */
1722
    public function having(string $statement, string $operator=self::OPERATOR_AND) : FluentPdoModel
1723
    {
1724
        $this->_having[] = [
1725
            'STATEMENT'   => $statement,
1726
            'OPERATOR'    => $operator
1727
        ];
1728
1729
        return $this;
1730
    }
1731
1732
    /**
1733
     * ORDER BY $columnName (ASC | DESC)
1734
     *
1735
     * @param  string   $columnName - The name of the column or an expression
1736
     * @param  string   $ordering   (DESC | ASC)
1737
     * @return FluentPdoModel|$this
1738
     */
1739
    public function orderBy(string $columnName='', string $ordering='ASC') : FluentPdoModel
1740
    {
1741
        $ordering       = strtoupper($ordering);
1742
        Assert($ordering)->inArray(['DESC', 'ASC']);
1743
        if ( ! $columnName )
1744
        {
1745
            $this->_order_by = [];
1746
1747
            return $this;
1748
        }
1749
        $this->_order_by[] = trim("{$columnName} {$ordering}");
1750
1751
        return $this;
1752
    }
1753
1754
    /**
1755
     * GROUP BY $columnName
1756
     *
1757
     * @param  string   $columnName
1758
     * @return FluentPdoModel|$this
1759
     */
1760
    public function groupBy(string $columnName) : FluentPdoModel
1761
    {
1762
        $columnName = is_array($columnName) ? $columnName : [$columnName];
1763
        foreach ( $columnName as $col )
1764
        {
1765
            $this->_group_by[] = $col;
1766
        }
1767
1768
        return $this;
1769
    }
1770
1771
1772
    /**
1773
     * LIMIT $limit
1774
     *
1775
     * @param  int      $limit
1776
     * @param  int|null $offset
1777
     * @return FluentPdoModel|$this
1778
     */
1779
    public function limit(int $limit, int $offset=0) : FluentPdoModel
1780
    {
1781
        $this->_limit =  $limit;
1782
        if ( $offset )
1783
        {
1784
            $this->offset($offset);
1785
        }
1786
        return $this;
1787
    }
1788
1789
    /**
1790
     * Return the limit
1791
     *
1792
     * @return integer
1793
     */
1794
    public function getLimit() : int
1795
    {
1796
        return $this->_limit;
1797
    }
1798
1799
    /**
1800
     * OFFSET $offset
1801
     *
1802
     * @param  int      $offset
1803
     * @return FluentPdoModel|$this
1804
     */
1805
    public function offset(int $offset) : FluentPdoModel
1806
    {
1807
        $this->_offset = (int)$offset;
1808
1809
        return $this;
1810
    }
1811
1812
    /**
1813
     * Return the offset
1814
     *
1815
     * @return integer
1816
     */
1817
    public function getOffset() : int
1818
    {
1819
        return $this->_offset;
1820
    }
1821
1822
    /**
1823
     * Build a join
1824
     *
1825
     * @param  string    $table         - The table name
1826
     * @param  string   $constraint    -> id = profile.user_id
1827
     * @param  string   $tableAlias   - The alias of the table name
1828
     * @param  string   $joinOperator - LEFT | INNER | etc...
1829
     * @return FluentPdoModel|$this
1830
     */
1831
    public function join(string $table, string $constraint='', string $tableAlias='', string $joinOperator='') : FluentPdoModel
1832
    {
1833
        if ( ! $constraint )
1834
        {
1835
            return $this->autoJoin($table, $joinOperator);
1836
        }
1837
        $join                   = [$joinOperator ? "{$joinOperator} " : ''];
1838
        $join[]                 = "JOIN {$table} ";
1839
        $tableAlias             = $tableAlias ?: Inflector::classify($table);
1840
        $join[]                 = $tableAlias ? "AS {$tableAlias} " : '';
1841
        $join[]                 = "ON {$constraint}";
1842
        $this->_join_sources[]  = implode('', $join);
1843
        if ( $tableAlias )
1844
        {
1845
            $this->_join_aliases[]  = $tableAlias;
1846
        }
1847
1848
        return $this;
1849
    }
1850
1851
    /**
1852
     * Create a left join
1853
     *
1854
     * @param  string   $table
1855
     * @param  string   $constraint
1856
     * @param  string   $tableAlias
1857
     * @return FluentPdoModel|$this
1858
     */
1859
    public function leftJoin(string $table, string $constraint, string $tableAlias='') : FluentPdoModel
1860
    {
1861
        return $this->join($table, $constraint, $tableAlias, self::LEFT_JOIN);
1862
    }
1863
1864
1865
    /**
1866
     * Return the build select query
1867
     *
1868
     * @return string
1869
     */
1870
    public function getSelectQuery() : string
1871
    {
1872
        if ( $this->_raw_sql )
1873
        {
1874
            return $this->_raw_sql;
1875
        }
1876
        if ( empty($this->_select_fields) || ! $this->_explicit_select_mode )
1877
        {
1878
            $this->select('*', '', false);
1879
        }
1880
        foreach ( $this->_select_fields as $idx => $cols )
1881
        {
1882
            if ( strpos(trim(strtolower($cols)), 'distinct ') === 0 )
1883
            {
1884
                $this->_distinct = true;
1885
                $this->_select_fields[$idx] = str_ireplace('distinct ', '', $cols);
1886
            }
1887
        }
1888
        if ( $this->_include_count )
1889
        {
1890
            $this->select('COUNT(*) as __cnt');
1891
        }
1892
        $query  = 'SELECT ';
1893
        $query .= $this->_distinct ? 'DISTINCT ' : '';
1894
        $query .= implode(', ', $this->_prepareColumns($this->_select_fields));
1895
        $query .= " FROM {$this->_table_name}" . ( $this->_table_alias ? " {$this->_table_alias}" : '' );
1896
        if ( count($this->_join_sources ) )
1897
        {
1898
            $query .= (' ').implode(' ',$this->_join_sources);
1899
        }
1900
        $query .= $this->_getWhereString(); // WHERE
1901
        if ( count($this->_group_by) )
1902
        {
1903
            $query .= ' GROUP BY ' . implode(', ', array_unique($this->_group_by));
1904
        }
1905
        if ( count($this->_order_by ) )
1906
        {
1907
            $query .= ' ORDER BY ' . implode(', ', array_unique($this->_order_by));
1908
        }
1909
        $query .= $this->_getHavingString(); // HAVING
1910
1911
        return $this->_connection->setLimit($query, $this->_limit, $this->_offset);
1912
    }
1913
1914
    /**
1915
     * @param string $field
1916
     * @param string $column
1917
     * @return string
1918
     */
1919
    public function getFieldComment(string $field, string $column) : string
1920
    {
1921
        return $this->_connection->getFieldComment($field, $column);
1922
    }
1923
1924
    /**
1925
     * Prepare columns to include the table alias name
1926
     * @param array $columns
1927
     * @return array
1928
     */
1929
    protected function _prepareColumns(array $columns) : array
1930
    {
1931
        if ( ! $this->_table_alias )
1932
        {
1933
            return $columns;
1934
        }
1935
        $newColumns = [];
1936
        foreach ($columns as $column)
1937
        {
1938
            if ( strpos($column, ',') && ! preg_match('/^[a-zA-Z_]{2,200}\(.{1,500}\)/', trim($column)) )
1939
            {
1940
                $newColumns = array_merge($this->_prepareColumns(explode(',', $column)), $newColumns);
1941
            }
1942
            elseif ( preg_match('/^(AVG|SUM|MAX|MIN|COUNT|CONCAT)/', $column) )
1943
            {
1944
                $newColumns[] = trim($column);
1945
            }
1946
            elseif (strpos($column, '.') === false && strpos(strtoupper($column), 'NULL') === false)
1947
            {
1948
                $column         = trim($column);
1949
                $newColumns[]   = preg_match('/^[0-9]/', $column) ? trim($column) : "{$this->_table_alias}.{$column}";
1950
            }
1951
            else
1952
            {
1953
                $newColumns[] = trim($column);
1954
            }
1955
        }
1956
1957
        return $newColumns;
1958
    }
1959
1960
    /**
1961
     * Build the WHERE clause(s)
1962
     *
1963
     * @param bool $purgeAliases
1964
     * @return string
1965
     */
1966
    protected function _getWhereString(bool $purgeAliases=false) : string
1967
    {
1968
        // If there are no WHERE clauses, return empty string
1969
        if ( empty($this->_where_conditions) )
1970
        {
1971
            return '';
1972
        }
1973
        $where_condition = '';
1974
        $last_condition = '';
1975
        foreach ( $this->_where_conditions as $condition )
1976
        {
1977
            if ( is_array($condition) )
1978
            {
1979
                if ( $where_condition && $last_condition != '(' && !preg_match('/\)\s+(OR|AND)\s+$/i', $where_condition))
1980
                {
1981
                    $where_condition .= $condition['OPERATOR'];
1982
                }
1983
                if ( $purgeAliases && ! empty($condition['STATEMENT']) && strpos($condition['STATEMENT'], '.') !== false && ! empty($this->_table_alias) )
1984
                {
1985
                    $condition['STATEMENT'] = preg_replace("/{$this->_table_alias}\./", '', $condition['STATEMENT']);
1986
                }
1987
                $where_condition .= $condition['STATEMENT'];
1988
                $this->_where_parameters = array_merge($this->_where_parameters, $condition['PARAMS']);
1989
            }
1990
            else
1991
            {
1992
                $where_condition .= $condition;
1993
            }
1994
            $last_condition = $condition;
1995
        }
1996
1997
        return " WHERE {$where_condition}" ;
1998
    }
1999
2000
    /**
2001
     * Return the HAVING clause
2002
     *
2003
     * @return string
2004
     */
2005
    protected function _getHavingString() : string
2006
    {
2007
        // If there are no WHERE clauses, return empty string
2008
        if ( empty($this->_having) )
2009
        {
2010
            return '';
2011
        }
2012
        $having_condition = '';
2013
        foreach ( $this->_having as $condition )
2014
        {
2015
            if ( $having_condition && ! preg_match('/\)\s+(OR|AND)\s+$/i', $having_condition) )
2016
            {
2017
                $having_condition .= $condition['OPERATOR'];
2018
            }
2019
            $having_condition .= $condition['STATEMENT'];
2020
        }
2021
2022
        return " HAVING {$having_condition}" ;
2023
    }
2024
2025
    /**
2026
     * Return the values to be bound for where
2027
     *
2028
     * @param bool $purgeAliases
2029
     * @return array
2030
     */
2031
    protected function _getWhereParameters(bool $purgeAliases=false) : array
2032
    {
2033
        unset($purgeAliases);
2034
2035
        return $this->_where_parameters;
2036
    }
2037
2038
    /**
2039
     * @param array $record
2040
     * @return stdClass
2041
     */
2042
    public function insertArr(array $record) : stdClass
2043
    {
2044
        return $this->insert((object)$record);
2045
    }
2046
2047
    /**
2048
     * Insert new rows
2049
     * $records can be a stdClass or an array of stdClass to add a bulk insert
2050
     * If a single row is inserted, it will return it's row instance
2051
     *
2052
     * @param stdClass $record
2053
     * @return stdClass
2054
     * @throws Exception
2055
     */
2056
    public function insert(stdClass $record) : stdClass
2057
    {
2058
        Assert((array)$record)->notEmpty("The data passed to insert does not contain any data");
2059
        Assert($record)->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
2060
2061
        $record = $this->beforeSave($record, self::SAVE_INSERT);
2062
        if ( ! empty($this->_errors) )
2063
        {
2064
            return $record;
2065
        }
2066
        list($sql, $insert_values) = $this->insertSqlQuery([$record]);
2067
        $this->execute((string)$sql, (array)$insert_values);
2068
        $rowCount = $this->rowCount();
2069
        if ( $rowCount === 1 )
2070
        {
2071
            $primaryKeyName                 = $this->getPrimaryKeyName();
2072
            $record->{$primaryKeyName}      = $this->getLastInsertId($primaryKeyName);
2073
        }
2074
        $record = $this->afterSave($record, self::SAVE_INSERT);
2075
        $this->destroy();
2076
2077
        return $record;
2078
    }
2079
2080
    /**
2081
     * @param string $name
2082
     * @return int
2083
     */
2084
    public function getLastInsertId(string $name='') : int
2085
    {
2086
        return (int)$this->getPdo()->lastInsertId($name ?: null);
2087
    }
2088
2089
    /**
2090
     * @param stdClass[] $records
2091
     * @return stdClass[]
2092
     */
2093
    public function insertSqlQuery(array $records) : array
2094
    {
2095
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
2096
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
2097
2098
        $insert_values      = [];
2099
        $question_marks     = [];
2100
        $properties         = [];
2101
        foreach ( $records as $record )
2102
        {
2103
            $properties         = !empty($properties) ? $properties : array_keys(get_object_vars($record));
2104
            $question_marks[]   = '('  . $this->_makePlaceholders(count($properties)) . ')';
2105
            $insert_values      = array_merge($insert_values, array_values((array)$record));
2106
        }
2107
        $properties         = implode(', ', $properties);
2108
        $question_marks     = implode(', ', $question_marks);
2109
        $sql                = "INSERT INTO {$this->_table_name} ({$properties}) VALUES {$question_marks}";
2110
2111
        return [$sql, $insert_values];
2112
    }
2113
2114
    /**
2115
     * @param       $data
2116
     * @param array $matchOn
2117
     * @param bool  $returnObj
2118
     * @return bool|int|stdClass
2119
     */
2120
    public function upsert($data, array $matchOn=[], $returnObj=false)
2121
    {
2122
        if ( ! is_array($data) )
2123
        {
2124
            return $this->upsertOne($data, $matchOn, $returnObj);
2125
        }
2126
        Assert($data)
2127
            ->notEmpty("The data passed to insert does not contain any data")
2128
            ->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
2129
        $num_success    = 0;
2130
        foreach ( $data as $row )
2131
        {
2132
            $clone = clone $this;
2133
            if ( $clone->upsertOne($row, $matchOn) )
2134
            {
2135
                $num_success++;
2136
            }
2137
            unset($clone->_handlers, $clone); // hhvm mem leak
2138
        }
2139
2140
        return $num_success;
2141
    }
2142
2143
    /**
2144
     * @param stdClass $object
2145
     * @param array    $matchOn
2146
     * @param bool     $returnObj
2147
     * @return bool|int|stdClass
2148
     */
2149
    public function upsertOne(stdClass $object, array $matchOn=[], $returnObj=false)
2150
    {
2151
        $primary_key    = $this->getPrimaryKeyName();
2152
        $matchOn       = empty($matchOn) && property_exists($object, $primary_key) ? [$primary_key] : $matchOn;
2153
        foreach ( $matchOn as $column )
2154
        {
2155
            Assert( ! property_exists($object, $column) && $column !== $primary_key)->false('The match on value for upserts is missing.');
2156
            if ( property_exists($object, $column) )
2157
            {
2158
                if ( is_null($object->{$column}) )
2159
                {
2160
                    $this->whereNull($column);
2161
                }
2162
                else
2163
                {
2164
                    $this->where($column, $object->{$column});
2165
                }
2166
            }
2167
        }
2168
        if ( count($this->_where_conditions) < 1 )
2169
        {
2170
            return $this->insert($object);
2171
        }
2172
        if ( ( $id = (int)$this->fetchField($primary_key) ) )
2173
        {
2174
            if ( property_exists($object, $primary_key) && empty($object->{$primary_key}) )
2175
            {
2176
                $object->$primary_key = $id;
2177
            }
2178
            $rows_affected = $this->reset()->wherePk($id)->update($object);
2179
            if ( $rows_affected === false )
2180
            {
2181
                return false;
2182
            }
2183
2184
            return $returnObj ? $this->reset()->fetchOne($id) : $id;
2185
        }
2186
2187
        return $this->insert($object);
2188
    }
2189
2190
    /**
2191
     * @param array      $data
2192
     * @param array      $matchOn
2193
     * @param bool|false $returnObj
2194
     * @return bool|int|stdClass
2195
     */
2196
    public function upsertArr(array $data, array $matchOn=[], bool $returnObj=false)
2197
    {
2198
        return $this->upsert((object)$data, $matchOn, $returnObj);
2199
    }
2200
2201
    /**
2202
     * Update entries
2203
     * Use the query builder to create the where clause
2204
     *
2205
     * @param stdClass $record
2206
     * @param bool     $updateAll
2207
     * @return int
2208
     * @throws Exception
2209
     */
2210
    public function update(stdClass $record, $updateAll=false) : int
2211
    {
2212
        Assert($record)
2213
            ->notEmpty("The data passed to update does not contain any data")
2214
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2215
2216
        if ( empty($this->_where_conditions) && ! $updateAll )
2217
        {
2218
            throw new Exception("You cannot update an entire table without calling update with updateAll=true", 500);
2219
        }
2220
        $record = $this->beforeSave($record, self::SAVE_UPDATE);
2221
        if ( ! empty($this->_errors) )
2222
        {
2223
            return 0;
2224
        }
2225
        list($sql, $values) = $this->updateSqlQuery($record);
2226
        $this->execute($sql, $values);
2227
        $this->afterSave($record, self::SAVE_UPDATE);
2228
        $rowCount = $this->rowCount();
2229
        $this->destroy();
2230
2231
        return $rowCount;
2232
    }
2233
2234
    /**
2235
     * @param array      $record
2236
     * @param bool|false $updateAll
2237
     * @return int
2238
     * @throws Exception
2239
     */
2240
    public function updateArr(array $record, $updateAll=false) : int
2241
    {
2242
        return $this->update((object)$record, $updateAll);
2243
    }
2244
2245
2246
    /**
2247
     * @param string  $field
2248
     * @param mixed   $value
2249
     * @param int     $id
2250
     * @param bool|false $updateAll
2251
     * @return int
2252
     * @throws Exception
2253
     */
2254
    public function updateField(string $field, $value, int $id=0, bool $updateAll=false) : int
2255
    {
2256
        if ( $id && $id > 0 )
2257
        {
2258
            $this->wherePk($id);
2259
        }
2260
2261
        return $this->update((object)[$field => $value], $updateAll);
2262
    }
2263
2264
    /**
2265
     * @param stdClass $record
2266
     * @return bool|int
2267
     * @throws Exception
2268
     */
2269
    public function updateChanged(stdClass $record) : int
2270
    {
2271
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2272
        {
2273
            if ( is_null($value) )
2274
            {
2275
                $this->whereNotNull($field);
2276
                continue;
2277
            }
2278
            $this->whereCoercedNot($field, $value);
2279
        }
2280
2281
        return $this->update($record);
2282
    }
2283
2284
    /**
2285
     * @param string    $expression
2286
     * @param array     $params
2287
     * @return FluentPdoModel|$this
2288
     */
2289
    public function updateByExpression(string $expression, array $params) : FluentPdoModel
2290
    {
2291
        $this->_update_raw[] = [$expression, $params];
2292
2293
        return $this;
2294
    }
2295
2296
    /**
2297
     * @param array $data
2298
     * @return int
2299
     * @throws Exception
2300
     */
2301
    public function rawUpdate(array $data=[]) : int
2302
    {
2303
        list($sql, $values) = $this->updateSql($data);
2304
        $this->execute($sql, $values);
2305
        $rowCount           = $this->rowCount();
2306
        $this->destroy();
2307
2308
        return $rowCount;
2309
    }
2310
2311
    /**
2312
     * @param stdClass $record
2313
     * @return array
2314
     */
2315
    public function updateSqlQuery(stdClass $record) : array
2316
    {
2317
        Assert($record)
2318
            ->notEmpty("The data passed to update does not contain any data")
2319
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2320
2321
        // Make sure we remove the primary key
2322
2323
        return $this->updateSql((array)$record);
2324
    }
2325
2326
    /**
2327
     * @param $record
2328
     * @return array
2329
     */
2330
    protected function updateSql(array $record) : array
2331
    {
2332
        unset($record[$this->getPrimaryKeyName()]);
2333
        Assert($this->_limit)->eq(0, 'You cannot limit updates');
2334
        // Sqlite needs a null primary key
2335
        //$record[$this->getPrimaryKeyName()] = null;
2336
        $fieldList  = [];
2337
        $fieldAlias = $this->_add_update_alias && ! empty($this->_table_alias) ? "{$this->_table_alias}." : '';
2338
        foreach ( $record as $key => $value )
2339
        {
2340
            if ( is_numeric($key) )
2341
            {
2342
                $fieldList[] = $value;
2343
                unset($record[$key]);
2344
                continue;
2345
            }
2346
            $fieldList[] = "{$fieldAlias}{$key} = ?";
2347
        }
2348
        $rawParams  = [];
2349
        foreach ( $this->_update_raw as $rawUpdate )
2350
        {
2351
            $fieldList[]   = $rawUpdate[0];
2352
            $rawParams      = array_merge($rawParams, $rawUpdate[1]);
2353
        }
2354
        $fieldList     = implode(', ', $fieldList);
2355
        $whereStr      = $this->_getWhereString();
2356
        $joins          = ! empty($this->_join_sources) ? (' ').implode(' ',$this->_join_sources) : '';
2357
        $alias          = ! empty($this->_table_alias) ? " AS {$this->_table_alias}" : '';
2358
        $sql            = "UPDATE {$this->_table_name}{$alias}{$joins} SET {$fieldList}{$whereStr}";
2359
        $values         = array_merge(array_values($record), $rawParams, $this->_getWhereParameters());
2360
2361
        return [$sql, $values];
2362
    }
2363
2364
    /**
2365
     * @param bool $deleteAll
2366
     * @param bool $force
2367
     * @return int
2368
     * @throws Exception
2369
     */
2370
    public function delete(bool $deleteAll=false, bool $force=false) : int
2371
    {
2372
        if ( ! $force && $this->_soft_deletes )
2373
        {
2374
            return $this->updateArchived();
2375
        }
2376
2377
        list($sql, $params) = $this->deleteSqlQuery();
2378
        if ( empty($this->_where_conditions) && ! $deleteAll )
2379
        {
2380
            throw new Exception("You cannot update an entire table without calling update with deleteAll=true");
2381
        }
2382
        $this->execute($sql, $params);
2383
2384
        return $this->rowCount();
2385
    }
2386
2387
    /**
2388
     * @return bool
2389
     */
2390
    public function isSoftDelete() : bool
2391
    {
2392
        return $this->_soft_deletes;
2393
    }
2394
2395
    /**
2396
     * @param bool|false $force
2397
     * @return FluentPdoModel|$this
2398
     * @throws Exception
2399
     */
2400
    public function truncate(bool $force=false) : FluentPdoModel
2401
    {
2402
        if ( $force )
2403
        {
2404
            $this->execute('SET FOREIGN_KEY_CHECKS = 0');
2405
        }
2406
        $this->execute("TRUNCATE TABLE {$this->_table_name}");
2407
        if ( $force )
2408
        {
2409
            $this->execute('SET FOREIGN_KEY_CHECKS = 1');
2410
        }
2411
2412
        return $this;
2413
    }
2414
2415
    /**
2416
     * @return array
2417
     */
2418
    public function deleteSqlQuery() : array
2419
    {
2420
        $query  = "DELETE FROM {$this->_table_name}";
2421
        if ( !empty($this->_where_conditions) )
2422
        {
2423
            $query .= $this->_getWhereString(true);
2424
2425
            return [$query, $this->_getWhereParameters()];
2426
        }
2427
2428
        return [$query, []];
2429
    }
2430
2431
2432
    /**
2433
     * Return the aggregate count of column
2434
     *
2435
     * @param string $column
2436
     * @param int $cacheTtl
2437
     * @return float
2438
     */
2439
    public function count(string $column='*', int $cacheTtl=self::CACHE_NO) : float
2440
    {
2441
        $this->explicitSelectMode();
2442
2443
        if ( empty($this->_group_by) )
2444
        {
2445
            return $this->fetchFloat("COUNT({$column}) AS cnt", 0, $cacheTtl);
2446
        }
2447
        $this->select("COUNT({$column}) AS cnt");
2448
        $sql        = $this->getSelectQuery();
2449
        $params     = $this->_getWhereParameters();
2450
        $sql        = "SELECT COUNT(*) AS cnt FROM ({$sql}) t";
2451
        $object     = $this->query($sql, $params)->fetchOne(0, $cacheTtl);
2452
        if ( ! $object || empty($object->cnt) )
2453
        {
2454
            return 0.0;
2455
        }
2456
2457
        return (float)$object->cnt;
2458
    }
2459
2460
2461
    /**
2462
     * Return the aggregate max count of column
2463
     *
2464
     * @param string $column
2465
     * @param int $cacheTtl
2466
     * @return int|float|string|null
2467
     */
2468
    public function max(string $column, int $cacheTtl=self::CACHE_NO)
2469
    {
2470
        return $this
2471
            ->explicitSelectMode()
2472
            ->fetchField("MAX({$column}) AS max", 0, $cacheTtl);
2473
    }
2474
2475
2476
    /**
2477
     * Return the aggregate min count of column
2478
     *
2479
     * @param string $column
2480
     * @param int $cacheTtl
2481
     * @return int|float|string|null
2482
     */
2483
    public function min(string $column, int $cacheTtl=self::CACHE_NO)
2484
    {
2485
        return $this
2486
            ->explicitSelectMode()
2487
            ->fetchField("MIN({$column}) AS min", 0, $cacheTtl);
2488
    }
2489
2490
    /**
2491
     * Return the aggregate sum count of column
2492
     *
2493
     * @param string $column
2494
     * @param int $cacheTtl
2495
     * @return int|float|string|null
2496
     */
2497
    public function sum(string $column, int $cacheTtl=self::CACHE_NO)
2498
    {
2499
        return $this
2500
            ->explicitSelectMode()
2501
            ->fetchField("SUM({$column}) AS sum", 0, $cacheTtl);
2502
    }
2503
2504
    /**
2505
     * Return the aggregate average count of column
2506
     *
2507
     * @param string $column
2508
     * @param int $cacheTtl
2509
     * @return int|float|string|null
2510
     */
2511
    public function avg(string $column, int $cacheTtl=self::CACHE_NO)
2512
    {
2513
        return $this
2514
            ->explicitSelectMode()
2515
            ->fetchField("AVG({$column}) AS avg", 0, $cacheTtl);
2516
    }
2517
2518
    /*******************************************************************************/
2519
// Utilities methods
2520
2521
    /**
2522
     * Reset fields
2523
     *
2524
     * @return FluentPdoModel|$this
2525
     */
2526
    public function reset() : FluentPdoModel
2527
    {
2528
        $this->_where_parameters        = [];
2529
        $this->_select_fields           = [];
2530
        $this->_join_sources            = [];
2531
        $this->_join_aliases            = [];
2532
        $this->_where_conditions        = [];
2533
        $this->_limit                   = 0;
2534
        $this->_offset                  = 0;
2535
        $this->_order_by                = [];
2536
        $this->_group_by                = [];
2537
        $this->_and_or_operator         = self::OPERATOR_AND;
2538
        $this->_having                  = [];
2539
        $this->_wrap_open               = false;
2540
        $this->_last_wrap_position      = 0;
2541
        $this->_pdo_stmt                = null;
2542
        $this->_distinct                = false;
2543
        $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...
2544
        $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...
2545
        $this->_cache_ttl               = -1;
2546
        $this->_timer                   = [];
2547
        $this->_built_query             = '';
2548
        $this->_paging_meta             = [];
2549
        $this->_raw_sql                 = null;
2550
        $this->_explicit_select_mode    = false;
2551
2552
        return $this;
2553
    }
2554
2555
2556
    /**
2557
     * @return FluentPdoModel|$this
2558
     */
2559
    public function removeUnauthorisedFields() : FluentPdoModel
2560
    {
2561
        return $this;
2562
    }
2563
2564
    /**
2565
     * @return Closure[]
2566
     */
2567
    protected function _getFieldHandlers() : array
2568
    {
2569
        $columns = $this->getColumns(true);
2570
        if ( empty($columns) )
2571
        {
2572
            return [];
2573
        }
2574
2575
        return [
2576
            'id' => function(string $field, $value, string $type='', stdClass $record=null) {
2577
2578
                unset($record);
2579
                $value = $this->_fixType($field, $value);
2580
                if ( $type === self::SAVE_INSERT )
2581
                {
2582
                    Validate($value)->name($field)->nullOr()->id('ID must be a valid integer id, (%s) submitted.');
2583
                    return $value;
2584
                }
2585
                Validate($value)->name($field)->id('ID must be a valid integer id, (%s) submitted.');
2586
                return $value;
2587
            },
2588
            self::CREATOR_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2589
2590
                unset($type, $record);
2591
                $value = $this->_fixType($field, $value);
2592
                // Created user id is set to current user if record is an insert or deleted if not (unless override is true)
2593
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2594
                Validate($value)->name($field)->id('Created By must be a valid integer id, (%s) submitted.');
2595
                return $value;
2596
            },
2597
            self::CREATED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2598
2599
                unset($type, $record);
2600
                $value = $this->_fixType($field, $value);
2601
                // Created ts is set to now if record is an insert or deleted if not (unless override is true)
2602
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2603
                Validate($value)->name($field)->date('Created must be a valid timestamp, (%s) submitted.');
2604
                return $value;
2605
            },
2606
            self::MODIFIER_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2607
2608
                unset($type, $record);
2609
                $value = $this->_fixType($field, $value);
2610
                // Modified user id is set to current user (unless override is true)
2611
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2612
                Validate($value)->name($field)->id('Modified By must be a valid integer id, (%s) submitted.');
2613
                return $value;
2614
            },
2615
            self::MODIFIED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2616
2617
                unset($type, $record);
2618
                $value = $this->_fixType($field, $value);
2619
                // Modified timestamps are set to now (unless override is true)
2620
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2621
                Validate($value)->name($field)->date('Modified must be a valid timestamp, (%s) submitted.');
2622
                return $value;
2623
            },
2624
            'status' => function(string $field, $value, string $type='', stdClass $record=null) {
2625
2626
                unset($type, $record);
2627
                $value = $this->_fixType($field, $value);
2628
                // Statuses are set to active if not set
2629
                $value = is_null($value) ? self::ACTIVE : $value;
2630
                Validate($value)->name($field)->nullOr()->status('Status must be a valid integer between -1 and 1, (%s) submitted.');
2631
                return $value;
2632
            },
2633
        ];
2634
    }
2635
2636
    /**
2637
     * @return bool
2638
     */
2639
    public function begin() : bool
2640
    {
2641
        $pdo        = $this->getPdo();
2642
        $oldDepth   = $pdo->getTransactionDepth();
2643
        $res        = $pdo->beginTransaction();
2644
        $newDepth   = $pdo->getTransactionDepth();
2645
        $this->getLogger()->debug("Calling db begin transaction", [
2646
            'old_depth'     => $oldDepth,
2647
            'new_depth'     => $newDepth,
2648
            'trans_started' => $newDepth === 1 ? true : false,
2649
        ]);
2650
2651
        return $res;
2652
    }
2653
2654
    /**
2655
     * @return bool
2656
     */
2657
    public function commit() : bool
2658
    {
2659
        $pdo        = $this->getPdo();
2660
        $oldDepth   = $pdo->getTransactionDepth();
2661
        $res        = $pdo->commit();
2662
        $newDepth   = $pdo->getTransactionDepth();
2663
        $this->getLogger()->debug("Calling db commit transaction", [
2664
            'old_depth'     => $oldDepth,
2665
            'new_depth'     => $newDepth,
2666
            'trans_ended'   => $newDepth === 0 ? true : false,
2667
        ]);
2668
        if ( ! $res )
2669
        {
2670
            return false;
2671
        }
2672
2673
        return $res === 0 ? true : $res;
2674
    }
2675
2676
    /**
2677
     * @return bool
2678
     */
2679
    public function rollback() : bool
2680
    {
2681
        $pdo        = $this->getPdo();
2682
        $oldDepth   = $pdo->getTransactionDepth();
2683
        $res        = $pdo->rollback();
2684
        $newDepth   = $pdo->getTransactionDepth();
2685
        $this->getLogger()->debug("Calling db rollback transaction", [
2686
            'old_depth'     => $oldDepth,
2687
            'new_depth'     => $newDepth,
2688
            'trans_ended'   => $newDepth === 0 ? true : false,
2689
        ]);
2690
2691
        return $res;
2692
    }
2693
2694
    /**
2695
     * @param stdClass $record
2696
     * @param  string  $type
2697
     * @return stdClass
2698
     */
2699
    public function applyGlobalModifiers(stdClass $record, string $type) : stdClass
2700
    {
2701
        unset($type);
2702
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2703
        {
2704
            if ( is_string($record->{$field}) )
2705
            {
2706
                $record->{$field} = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", $value);
2707
            }
2708
        }
2709
2710
        return $record;
2711
    }
2712
2713
    /**
2714
     * @param stdClass $record
2715
     * @param  string $type
2716
     * @return stdClass
2717
     */
2718
    public function removeUnneededFields(stdClass $record, string $type) : stdClass
2719
    {
2720
        $creator_id     = self::CREATOR_ID_FIELD;
2721
        $created_ts     = self::CREATED_TS_FIELD;
2722
2723
        // remove un-needed fields
2724
        $columns = $this->getColumns(true);
2725
        if ( empty($columns) )
2726
        {
2727
            return $record;
2728
        }
2729
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2730
        {
2731
            if ( ! in_array($name, $columns) || in_array($name, $this->_virtual_fields) )
2732
            {
2733
                unset($record->{$name});
2734
            }
2735
        }
2736
        if ( property_exists($record, $created_ts) && $type !== 'INSERT' && ! $this->_allow_meta_override )
2737
        {
2738
            unset($record->{$created_ts});
2739
        }
2740
        if ( property_exists($record, $creator_id) && $type !== 'INSERT' && ! $this->_allow_meta_override )
2741
        {
2742
            unset($record->{$creator_id});
2743
        }
2744
2745
        return $record;
2746
    }
2747
2748
2749
    /**
2750
     * @param array $ids
2751
     * @param array $values
2752
     * @param int   $batch
2753
     * @return bool
2754
     */
2755
    public function setById(array $ids, array $values, int $batch=1000) : bool
2756
    {
2757
        $ids        = array_unique($ids);
2758
        if ( count($ids) <= $batch )
2759
        {
2760
            return (bool)$this->whereIn('id', $ids)->updateArr($values);
2761
        }
2762
        while ( ! empty($ids) )
2763
        {
2764
            $thisBatch  = array_slice($ids, 0, $batch);
2765
            $ids        = array_diff($ids, $thisBatch);
2766
            $this->reset()->whereIn('id', $thisBatch)->updateArr($values);
2767
        }
2768
2769
        return true;
2770
    }
2771
2772
2773
    /**
2774
     * @param string $displayColumnValue
2775
     * @return int
2776
     */
2777
    public function resolveId(string $displayColumnValue) : int
2778
    {
2779
        $displayColumn  = $this->getDisplayColumn();
2780
        $className      = get_class($this);
2781
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
2782
2783
        return $this
2784
            ->reset()
2785
            ->where($displayColumn, $displayColumnValue)
2786
            ->fetchInt('id', 0, self::ONE_HOUR);
2787
    }
2788
2789
    /**
2790
     * @param int   $resourceId
2791
     * @param array $query
2792
     * @param array $extraFields
2793
     * @param int $cacheTtl
2794
     * @return array
2795
     */
2796
    public function fetchApiResource(int $resourceId, array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO) : array
2797
    {
2798
        Assert($resourceId)->id();
2799
2800
        $query['_limit']    = 1;
2801
        $pagingMetaData        = $this->wherePk($resourceId)->_prepareApiResource($query, $extraFields);
2802
        if ( $pagingMetaData['total'] === 0 )
2803
        {
2804
            return [[], $pagingMetaData];
2805
        }
2806
2807
        return [$this->fetchOne($resourceId, $cacheTtl), $pagingMetaData];
2808
    }
2809
2810
    /**
2811
     * @param array     $query
2812
     * @param array     $extraFields
2813
     * @param int       $cacheTtl
2814
     * @param string    $permEntity
2815
     * @return array
2816
     */
2817
    public function fetchApiResources(array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO, string $permEntity='') : array
2818
    {
2819
        $pagingMetaData    = $this->_prepareApiResource($query, $extraFields);
2820
        if ( $pagingMetaData['total'] === 0 )
2821
        {
2822
            return [[], $pagingMetaData];
2823
        }
2824
        $results = $this->fetch('', $cacheTtl);
2825
        if ( ! $permEntity )
2826
        {
2827
            return [$results, $pagingMetaData];
2828
        }
2829
        foreach ( $results as $record )
2830
        {
2831
            if ( ! empty($record->id) )
2832
            {
2833
                $pagingMetaData['perms'][(int)$record->id] = $this->getMaskByResourceAndId($permEntity, $record->id);
2834
            }
2835
        }
2836
2837
        return [$results, $pagingMetaData];
2838
    }
2839
2840
2841
    /**
2842
     * @return array
2843
     */
2844
    public function getSearchableAssociations() : array
2845
    {
2846
        $belongsTo = ! empty($this->_associations['belongsTo']) ? $this->_associations['belongsTo'] : [];
2847
        unset($belongsTo['CreatedBy'], $belongsTo['ModifiedBy']);
2848
2849
        return $belongsTo;
2850
    }
2851
2852
    /**
2853
     * @param array $fields
2854
     */
2855
    public function removeUnrequestedFields(array $fields)
2856
    {
2857
        foreach ( $this->_select_fields as $idx => $field )
2858
        {
2859
            $field = trim(static::after(' AS ', $field, true));
2860
            if ( ! in_array($field, $fields) )
2861
            {
2862
                unset($this->_select_fields[$idx]);
2863
            }
2864
        }
2865
    }
2866
2867
    /**
2868
     * @param array $removeFields
2869
     */
2870
    public function removeFields(array $removeFields=[])
2871
    {
2872
        $searches = [];
2873
        foreach ( $removeFields as $removeField )
2874
        {
2875
            $removeField    = str_replace("{$this->_table_alias}.", '', $removeField);
2876
            $searches[]     = "{$this->_table_alias}.{$removeField}";
2877
            $searches[]     = $removeField;
2878
        }
2879
        foreach ( $this->_select_fields as $idx => $selected )
2880
        {
2881
            $selected = stripos($selected, ' AS ') !== false ? preg_split('/ as /i', $selected) : [$selected];
2882
            foreach ( $selected as $haystack )
2883
            {
2884
                foreach ( $searches as $search )
2885
                {
2886
                    if ( trim($haystack) === trim($search) )
2887
                    {
2888
                        unset($this->_select_fields[$idx]);
2889
                        continue;
2890
                    }
2891
                }
2892
            }
2893
        }
2894
    }
2895
2896
    /**
2897
     * @return FluentPdoModel|$this
2898
     */
2899
    public function defaultFilters() : FluentPdoModel
2900
    {
2901
        return $this;
2902
    }
2903
2904
    /**
2905
     * @param bool $allow
2906
     *
2907
     * @return FluentPdoModel|$this
2908
     */
2909
    public function allowMetaColumnOverride(bool $allow=false) : FluentPdoModel
2910
    {
2911
        $this->_allow_meta_override = $allow;
2912
2913
        return $this;
2914
    }
2915
2916
    /**
2917
     * @param bool $skip
2918
     *
2919
     * @return FluentPdoModel|$this
2920
     */
2921
    public function skipMetaUpdates(bool $skip=true) : FluentPdoModel
2922
    {
2923
        $this->_skip_meta_updates = $skip;
2924
2925
        return $this;
2926
    }
2927
2928
    /**
2929
     * @param bool $add
2930
     *
2931
     * @return FluentPdoModel|$this
2932
     */
2933
    public function addUpdateAlias(bool $add=true) : FluentPdoModel
2934
    {
2935
        $this->_add_update_alias = $add;
2936
2937
        return $this;
2938
    }
2939
2940
    /**
2941
     * @param stdClass $record
2942
     * @return stdClass
2943
     */
2944
    public function onFetch(stdClass $record) : stdClass
2945
    {
2946
        $record     = $this->_trimAndLowerCaseKeys($record);
2947
        if ( $this->_filter_on_fetch )
2948
        {
2949
            $record     = $this->cleanseRecord($record);
2950
        }
2951
2952
        $record     =  $this->fixTypesToSentinel($record);
2953
2954
        return $this->fixTimestamps($record);
2955
    }
2956
2957
    /**
2958
     * @param $value
2959
     * @return string
2960
     */
2961
    public function gzEncodeData(string $value) : string
2962
    {
2963
        if ( $this->_hasGzipPrefix($value) )
2964
        {
2965
            return $value;
2966
        }
2967
2968
        return static::GZIP_PREFIX . base64_encode(gzencode($value, 9));
2969
    }
2970
2971
    /**
2972
     * @param $value
2973
     * @return mixed|string
2974
     */
2975
    public function gzDecodeData(string $value) : string
2976
    {
2977
        if ( ! $this->_hasGzipPrefix($value) )
2978
        {
2979
            return $value;
2980
        }
2981
        $value = substr_replace($value, '', 0, strlen(static::GZIP_PREFIX));
2982
2983
        return gzdecode(base64_decode($value));
2984
    }
2985
2986
    /**
2987
     * @param $value
2988
     * @return bool
2989
     */
2990
    protected function _hasGzipPrefix(string $value) : bool
2991
    {
2992
        return substr($value, 0, strlen(static::GZIP_PREFIX)) === static::GZIP_PREFIX ? true : false;
2993
    }
2994
2995
    /**
2996
     * @param stdClass $record
2997
     * @return stdClass
2998
     */
2999
    public function fixTimestamps(stdClass $record) : stdClass
3000
    {
3001
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3002
        {
3003
            if ( preg_match('/_ts$/', $field) )
3004
            {
3005
                $record->{$field} = empty($value) ? $value : static::atom($value);
3006
            }
3007
        }
3008
3009
        return $record;
3010
    }
3011
3012
    /**
3013
     * @param int $max
3014
     * @return FluentPdoModel|$this
3015
     */
3016
    public function setMaxRecords(int $max) : FluentPdoModel
3017
    {
3018
        Assert($max)->int();
3019
        $this->_default_max = $max;
3020
3021
        return $this;
3022
    }
3023
3024
3025
    /**
3026
     * @param stdClass $record
3027
     * @param string   $type
3028
     * @return stdClass
3029
     */
3030
    public function afterSave(stdClass $record, string $type) : stdClass
3031
    {
3032
        unset($type);
3033
        $this->clearCacheByTable();
3034
        foreach ( $record as $column => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3035
        {
3036
            if ( !empty($record->{$column}) )
3037
            {
3038
                if ( preg_match('/_ts$/', $column) )
3039
                {
3040
                    $record->{$column} = static::atom($value);
3041
                }
3042
                if ( preg_match('/_am$/', $column) )
3043
                {
3044
                    $record->{$column} = number_format($value, 2, '.', '');
3045
                }
3046
            }
3047
        }
3048
3049
        return $record;
3050
    }
3051
3052
    /**
3053
     * @param stdClass $record
3054
     * @param string $type
3055
     * @return stdClass
3056
     */
3057
    public function addDefaultFields(stdClass $record, string $type) : stdClass
3058
    {
3059
        $columns            = $this->getColumns(true);
3060
        if ( empty($columns) )
3061
        {
3062
            return $record;
3063
        }
3064
        $defaults           = [
3065
            self::SAVE_UPDATE   => [
3066
                self::MODIFIER_ID_FIELD        => null,
3067
                self::MODIFIED_TS_FIELD        => null,
3068
            ],
3069
            self::SAVE_INSERT   => [
3070
                self::CREATOR_ID_FIELD         => null,
3071
                self::CREATED_TS_FIELD         => null,
3072
                self::MODIFIER_ID_FIELD        => null,
3073
                self::MODIFIED_TS_FIELD        => null,
3074
                self::STATUS_FIELD             => null,
3075
            ]
3076
        ];
3077
        if ( $this->_skip_meta_updates )
3078
        {
3079
            $defaults[self::SAVE_UPDATE] = [];
3080
        }
3081
        $columns            = array_flip($this->getColumns());
3082
        $defaults           = array_intersect_key($defaults[$type], $columns);
3083
        foreach ( $defaults as $column => $def )
3084
        {
3085
            $record->{$column} = $record->{$column} ?? $def;
3086
        }
3087
3088
        return $record;
3089
    }
3090
3091
3092
    /**
3093
     * @return bool
3094
     */
3095
    public function createTable() : bool
3096
    {
3097
        return true;
3098
    }
3099
3100
    /**
3101
     * @return bool
3102
     */
3103
    public function dropTable() : bool
3104
    {
3105
        return true;
3106
    }
3107
3108
    protected function _compileHandlers()
3109
    {
3110
        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...
3111
        {
3112
            return;
3113
        }
3114
        $parentHandlers      = self::_getFieldHandlers();
3115
        $this->_handlers    = array_merge($parentHandlers, $this->_getFieldHandlers());
3116
    }
3117
3118
    /**
3119
     * @param string $viewName
3120
     * @param int $cacheTtl
3121
     * @return array
3122
     */
3123
    public function getViewColumns($viewName, $cacheTtl=self::CACHE_NO)
3124
    {
3125
        return $this->_getColumnsByTableFromDb($viewName, $cacheTtl);
3126
    }
3127
3128
    /**
3129
     * @param int $id
3130
     * @return string
3131
     */
3132
    public function getDisplayNameById(int $id) : string
3133
    {
3134
        $displayColumn  = $this->getDisplayColumn();
3135
        $className      = get_class($this);
3136
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
3137
3138
        return $this
3139
            ->reset()
3140
            ->fetchStr($displayColumn, $id, self::ONE_HOUR);
3141
    }
3142
3143
    /**
3144
     * @param int $id
3145
     * @param string $displayColumnValue
3146
     * @return bool
3147
     */
3148
    public function validIdDisplayNameCombo(int $id, $displayColumnValue) : bool
3149
    {
3150
        return $displayColumnValue === $this->getDisplayNameById($id);
3151
    }
3152
3153
    /**
3154
     * @param array $toPopulate
3155
     * @return stdClass
3156
     */
3157
    protected function getEmptyObject(array $toPopulate=[]) : stdClass
3158
    {
3159
        $toPopulate[]   = 'id';
3160
3161
        return (object)array_flip($toPopulate);
3162
    }
3163
3164
    /**
3165
     * @param int $id
3166
     * @return bool
3167
     */
3168
    public static function isId(int $id) : bool
3169
    {
3170
        return $id > 0;
3171
    }
3172
3173
    /**
3174
     * @param int $cacheTtl
3175
     * @return int
3176
     */
3177
    public function activeCount(int $cacheTtl=self::CACHE_NO) : int
3178
    {
3179
        return (int)$this->whereActive()->count('*', $cacheTtl);
3180
    }
3181
3182
    /**
3183
     * @param string        $tableAlias
3184
     * @param string   $columnName
3185
     * @return FluentPdoModel|$this
3186
     */
3187
    public function whereActive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3188
    {
3189
        return $this->whereStatus(static::ACTIVE, $tableAlias, $columnName);
3190
    }
3191
3192
    /**
3193
     * @param string        $tableAlias
3194
     * @param string        $columnName
3195
     * @return FluentPdoModel|$this
3196
     */
3197
    public function whereInactive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3198
    {
3199
        return $this->whereStatus(static::INACTIVE, $tableAlias, $columnName);
3200
    }
3201
3202
    /**
3203
     * @param string        $tableAlias
3204
     * @param string        $columnName
3205
     * @return FluentPdoModel|$this
3206
     */
3207
    public function whereArchived(string $tableAlias='', string $columnName='status') : FluentPdoModel
3208
    {
3209
        return $this->whereStatus(static::ARCHIVED, $tableAlias, $columnName);
3210
    }
3211
3212
    /**
3213
     * @param int $status
3214
     * @param string $tableAlias
3215
     * @param string $columnName
3216
     * @return FluentPdoModel|$this
3217
     */
3218
    public function whereStatus(int $status, string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3219
    {
3220
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3221
3222
        $tableAlias = empty($tableAlias) ? $this->getTableAlias() : $tableAlias;
3223
        $field      = empty($tableAlias) ? $columnName : "{$tableAlias}.{$columnName}";
3224
3225
        return $this->where($field, $status);
3226
    }
3227
3228
    /**
3229
     * @param int $id
3230
     * @return int
3231
     */
3232
    public function updateActive(int $id=0) : int
3233
    {
3234
        Assert($id)->unsignedInt();
3235
        if ( $id )
3236
        {
3237
            $this->wherePk($id);
3238
        }
3239
3240
        return $this->updateStatus(static::ACTIVE);
3241
    }
3242
3243
    /**
3244
     * @param int $id
3245
     * @return int
3246
     */
3247
    public function updateInactive(int $id=0) : int
3248
    {
3249
        Assert($id)->unsignedInt();
3250
        if ( $id )
3251
        {
3252
            $this->wherePk($id);
3253
        }
3254
        return $this->updateStatus(static::INACTIVE);
3255
    }
3256
3257
    /**
3258
     * @param string $field
3259
     * @param int  $id
3260
     * @return int
3261
     */
3262
    public function updateNow(string $field, int $id=0) : int
3263
    {
3264
        Assert($field)->notEmpty();
3265
3266
        return $this->updateField($field, date('Y-m-d H:i:s'), $id);
3267
    }
3268
    /**
3269
     * @param string $field
3270
     * @param int  $id
3271
     * @return int
3272
     */
3273
    public function updateToday($field, int $id=0) : int
3274
    {
3275
        Assert($field)->notEmpty();
3276
3277
        return $this->updateField($field, date('Y-m-d'), $id);
3278
    }
3279
3280
    /**
3281
     * @param int $id
3282
     * @return int
3283
     */
3284
    public function updateDeleted(int $id=0) : int
3285
    {
3286
        Assert($id)->unsignedInt();
3287
        if ( $id )
3288
        {
3289
            $this->wherePk($id);
3290
        }
3291
3292
        return $this->update((object)[
3293
            self::DELETER_ID_FIELD  => $this->getUserId(),
3294
            self::DELETED_TS_FIELD  => static::dateTime(),
3295
        ]);
3296
    }
3297
3298
    /**
3299
     * @param int $id
3300
     * @return int
3301
     */
3302
    public function updateArchived(int $id=0) : int
3303
    {
3304
        Assert($id)->unsignedInt();
3305
        if ( $id )
3306
        {
3307
            $this->wherePk($id);
3308
        }
3309
3310
        return $this->updateStatus(static::ARCHIVED);
3311
    }
3312
3313
    /**
3314
     * @param int $status
3315
     * @return int
3316
     * @throws \Exception
3317
     */
3318
    public function updateStatus(int $status)
3319
    {
3320
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3321
3322
        return $this->updateField('status', $status);
3323
    }
3324
3325
    /**
3326
     * Return a YYYY-MM-DD HH:II:SS date format
3327
     *
3328
     * @param string $datetime - An english textual datetime description
3329
     *          now, yesterday, 3 days ago, +1 week
3330
     *          http://php.net/manual/en/function.strtotime.php
3331
     * @return string YYYY-MM-DD HH:II:SS
3332
     */
3333
    public static function NOW(string $datetime='now') : string
3334
    {
3335
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
3336
    }
3337
3338
    /**
3339
     * Return a string containing the given number of question marks,
3340
     * separated by commas. Eg '?, ?, ?'
3341
     *
3342
     * @param int - total of placeholder to insert
3343
     * @return string
3344
     */
3345
    protected function _makePlaceholders(int $numberOfPlaceholders=1) : string
3346
    {
3347
        return implode(', ', array_fill(0, $numberOfPlaceholders, '?'));
3348
    }
3349
3350
    /**
3351
     * Format the table{Primary|Foreign}KeyName
3352
     *
3353
     * @param  string $pattern
3354
     * @param  string $tableName
3355
     * @return string
3356
     */
3357
    protected function _formatKeyName(string $pattern, string $tableName) : string
3358
    {
3359
        return sprintf($pattern, $tableName);
3360
    }
3361
3362
3363
    /**
3364
     * @param array $query
3365
     * @param array $extraFields
3366
     * @return array
3367
     * @throws \Exception
3368
     */
3369
    protected function _prepareApiResource(array $query=[], array $extraFields=[]) : array
3370
    {
3371
        $this->defaultFilters()->filter($query)->paginate($query);
3372
        $pagingMetaData    = $this->getPagingMeta();
3373
        if ( $pagingMetaData['total'] === 0 )
3374
        {
3375
            return $pagingMetaData;
3376
        }
3377
        $this->withBelongsTo($pagingMetaData['fields']);
3378
        if ( ! empty($extraFields) )
3379
        {
3380
            $this->select($extraFields, '', false);
3381
        }
3382
        $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...
3383
        if ( ! empty($pagingMetaData['fields']) )
3384
        {
3385
            $this->removeUnrequestedFields($pagingMetaData['fields']);
3386
        }
3387
3388
        return $pagingMetaData;
3389
    }
3390
3391
    /**
3392
     * @param string $query
3393
     * @param array $parameters
3394
     *
3395
     * @return array
3396
     */
3397
    protected function _logQuery(string $query, array $parameters) : array
3398
    {
3399
        $query                  = $this->buildQuery($query, $parameters);
3400
        if ( ! $this->_log_queries )
3401
        {
3402
            return ['', ''];
3403
        }
3404
        $ident                  = substr(str_shuffle(md5($query)), 0, 10);
3405
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
3406
        $this->_timer['start']  = microtime(true);
3407
3408
        return [$query, $ident];
3409
    }
3410
3411
    /**
3412
     * @param string $ident
3413
     * @param string $builtQuery
3414
     */
3415
    protected function _logSlowQueries(string $ident, string $builtQuery)
3416
    {
3417
        if ( ! $this->_log_queries )
3418
        {
3419
            return ;
3420
        }
3421
        $this->_timer['end']    = microtime(true);
3422
        $seconds_taken          = round($this->_timer['end'] - $this->_timer['start'], 3);
3423
        if ( $seconds_taken > $this->_slow_query_secs )
3424
        {
3425
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$seconds_taken} seconds:\n{$builtQuery}");
3426
        }
3427
    }
3428
3429
    /**
3430
     * @return float
3431
     */
3432
    public function getTimeTaken() : float
3433
    {
3434
        $secondsTaken = $this->_timer['end'] - $this->_timer['start'];
3435
3436
        return (float)$secondsTaken;
3437
    }
3438
3439
    /**
3440
     * @param $secs
3441
     * @return FluentPdoModel|$this
3442
     */
3443
    public function slowQuerySeconds(int $secs) : FluentPdoModel
3444
    {
3445
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
3446
        $this->_slow_query_secs = $secs;
3447
3448
        return $this;
3449
    }
3450
3451
3452
    /**
3453
     * @param       $field
3454
     * @param array $values
3455
     * @param string  $placeholderPrefix
3456
     *
3457
     * @return array
3458
     */
3459
    public function getNamedWhereIn(string $field, array $values, string $placeholderPrefix='') : array
3460
    {
3461
        Assert($field)->string()->notEmpty();
3462
        Assert($values)->isArray();
3463
3464
        if ( empty($values) )
3465
        {
3466
            return ['', []];
3467
        }
3468
        $placeholderPrefix      = $placeholderPrefix ?: strtolower(str_replace('.', '__', $field));
3469
        $params                 = [];
3470
        $placeholders           = [];
3471
        $count                  = 1;
3472
        foreach ( $values as $val )
3473
        {
3474
            $name                   = "{$placeholderPrefix}_{$count}";
3475
            $params[$name]          = $val;
3476
            $placeholders[]         = ":{$name}";
3477
            $count++;
3478
        }
3479
        $placeholders           = implode(',', $placeholders);
3480
3481
        return ["AND {$field} IN ({$placeholders})\n", $params];
3482
    }
3483
3484
    /**
3485
     * @param string $field
3486
     * @param string $delimiter
3487
     *
3488
     * @return array
3489
     */
3490
    protected function _getColumnAliasParts(string $field, string $delimiter=':') : array
3491
    {
3492
        $parts      = explode($delimiter, $field);
3493
        if ( count($parts) === 2 )
3494
        {
3495
            return $parts;
3496
        }
3497
        $parts = explode('.', $field);
3498
        if ( count($parts) === 2 )
3499
        {
3500
            return $parts;
3501
        }
3502
3503
        return ['', $field];
3504
    }
3505
3506
    /**
3507
     * @param string $column
3508
     * @param string $term
3509
     * @return FluentPdoModel|$this
3510
     */
3511
    protected function _addWhereClause(string $column, string $term) : FluentPdoModel
3512
    {
3513
        $modifiers = [
3514
            'whereLike'         => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3515
            'whereNotLike'      => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3516
            'whereLt'           => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
3517
            'whereLte'          => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
3518
            'whereGt'           => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
3519
            'whereGte'          => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
3520
            'whereBetween'      => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3521
            'whereNotBetween'  => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3522
        ];
3523
        foreach ( $modifiers as $func => $regex )
3524
        {
3525
            if ( preg_match($regex, $term, $matches) )
3526
            {
3527
                array_shift($matches);
3528
                switch ($func)
3529
                {
3530
                    case 'whereLike':
3531
3532
                        return $this->whereLike($column, $matches[0]);
3533
3534
                    case 'whereNotLike':
3535
3536
                        return $this->whereNotLike($column, $matches[0]);
3537
3538
                    case 'whereLt':
3539
3540
                        return $this->whereLt($column, $matches[0]);
3541
3542
                    case 'whereLte':
3543
3544
                        return $this->whereLte($column, $matches[0]);
3545
3546
                    case 'whereGt':
3547
3548
                        return $this->whereGt($column, $matches[0]);
3549
3550
                    case 'whereGte':
3551
3552
                        return $this->whereGte($column, $matches[0]);
3553
3554
                    case 'whereBetween':
3555
3556
                        return $this->whereBetween($column, $matches[0], $matches[1]);
3557
3558
                    case 'whereNotBetween':
3559
3560
                        return $this->whereNotBetween($column, $matches[0], $matches[1]);
3561
3562
                }
3563
            }
3564
        }
3565
3566
        return $this->where($column, $term);
3567
    }
3568
3569
    public function destroy()
3570
    {
3571
        if ( !is_null($this->_pdo_stmt) )
3572
        {
3573
            $this->_pdo_stmt->closeCursor();
3574
        }
3575
        $this->_pdo_stmt    = null;
3576
        $this->_handlers    = [];
3577
    }
3578
3579
    public function __destruct()
3580
    {
3581
        $this->destroy();
3582
    }
3583
3584
    /**
3585
     * Load a model
3586
     *
3587
     * @param string $modelName
3588
     * @param AbstractPdo $connection
3589
     * @return FluentPdoModel|$this
3590
     * @throws ModelNotFoundException
3591
     */
3592
    public static function loadModel(string $modelName, AbstractPdo $connection=null) : FluentPdoModel
3593
    {
3594
        $modelName = static::$_model_namespace . $modelName;
3595
        if ( ! class_exists($modelName) )
3596
        {
3597
            throw new ModelNotFoundException("Failed to find model class {$modelName}.");
3598
        }
3599
3600
        return new $modelName($connection);
3601
    }
3602
3603
    /**
3604
     * Load a model
3605
     *
3606
     * @param string      $tableName
3607
     * @param AbstractPdo $connection
3608
     * @return FluentPdoModel|$this
3609
     */
3610
    public static function loadTable(string $tableName, AbstractPdo $connection=null) : FluentPdoModel
3611
    {
3612
        $modelName     = Inflector::classify($tableName);
3613
        Assert($modelName)->notEmpty("Could not resolve model name from table name.");
3614
3615
        return static::loadModel($modelName, $connection);
3616
    }
3617
3618
    /**
3619
     * @param string   $columnName
3620
     * @param int $cacheTtl
3621
     * @param bool $flushCache
3622
     * @return bool
3623
     */
3624
    public function columnExists(string $columnName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3625
    {
3626
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3627
3628
        return array_key_exists($columnName, $columns);
3629
    }
3630
3631
    /**
3632
     * @param string   $indexName
3633
     * @param int $cacheTtl
3634
     * @param bool $flushCache
3635
     * @return bool
3636
     */
3637
    public function indexExists(string $indexName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3638
    {
3639
        Assert($indexName)->string()->notEmpty();
3640
3641
        $callback = function() use ($indexName) {
3642
3643
            $index = $this->execute("SHOW INDEX FROM {$this->_table_name} WHERE Key_name = ':indexName'", compact('indexName'));
3644
3645
            return $index ? true : false;
3646
        };
3647
        if  ( $cacheTtl === self::CACHE_NO )
3648
        {
3649
            return $callback();
3650
        }
3651
        $cacheKey   = '/column_schema/' . $this->_table_name . '/index/' . $indexName;
3652
        if ( $flushCache === true )
3653
        {
3654
            $this->clearCache($cacheKey);
3655
        }
3656
3657
        return (bool)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3658
    }
3659
3660
3661
3662
    /**
3663
     * @param int $cacheTtl
3664
     * @param bool $flushCache
3665
     * @return FluentPdoModel|$this
3666
     */
3667
    public function loadSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : FluentPdoModel
3668
    {
3669
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
3670
        $this->schema($schema);
3671
3672
        return $this;
3673
    }
3674
3675
    /**
3676
     * @param int $cacheTtl
3677
     * @param bool $flushCache
3678
     * @return array
3679
     */
3680
    public function getSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3681
    {
3682
        $table      = $this->getTableName();
3683
        Assert($table)->string()->notEmpty();
3684
        $schema     = [];
3685
        $columns    = $this->_getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
3686
        foreach ( $columns[$table] as $column => $meta )
3687
        {
3688
            $schema[$column] = $meta->dataType;
3689
        }
3690
        return $schema;
3691
    }
3692
3693
    /**
3694
     * @param string $table
3695
     * @param int $cacheTtl
3696
     * @param bool $flushCache
3697
     * @return Column[][]
3698
     */
3699
    protected function _getColumnsByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3700
    {
3701
        Assert($table)->string()->notEmpty();
3702
3703
        $callback = function() use ($table) {
3704
3705
            return $this->_connection->getColumns(true, $table);
3706
        };
3707
        $cacheKey   = '/column_schema/' . $table;
3708
        if ( $flushCache === true )
3709
        {
3710
            $this->clearCache($cacheKey);
3711
        }
3712
3713
        return (array)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3714
    }
3715
3716
    /**
3717
     * @param string $table
3718
     * @return bool
3719
     */
3720
    public function clearSchemaCache(string $table) : bool
3721
    {
3722
        return $this->clearCache('/column_schema/' . $table);
3723
    }
3724
3725
    /**
3726
     * @param stdClass $record
3727
     * @return stdClass
3728
     */
3729
    public function cleanseRecord(stdClass $record) : stdClass
3730
    {
3731
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3732
        {
3733
            if ( is_string($record->{$field}) )
3734
            {
3735
                $record->$field = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", filter_var($record->$field, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES));
3736
                if ( $this->_log_filter_changes && $value !== $record->$field )
3737
                {
3738
                    $table = $this->_table_name ? $this->_table_name : '';
3739
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->$field]);
3740
                }
3741
            }
3742
        }
3743
3744
        return $record;
3745
    }
3746
3747
    /**
3748
     * @param stdClass $record
3749
     * @param string   $type
3750
     * @return stdClass
3751
     */
3752
    public function beforeSave(stdClass $record, string $type) : stdClass
3753
    {
3754
        $record = $this->addDefaultFields($record, $type);
3755
        $record = $this->applyGlobalModifiers($record, $type);
3756
        $record = $this->applyHandlers($record, $type);
3757
        $record = $this->removeUnneededFields($record, $type);
3758
3759
        return $record;
3760
    }
3761
3762
    /**
3763
     * @param array $data
3764
     * @param string $saveType
3765
     * @return array
3766
     */
3767
    public function cleanseWebData(array $data, string $saveType) : array
3768
    {
3769
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
3770
        $columns = $this->getColumns(false);
3771
        if ( empty($columns) )
3772
        {
3773
            return $data;
3774
        }
3775
        foreach ( $data as $field => $val )
3776
        {
3777
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
3778
        }
3779
3780
        return array_intersect_key($data, $columns);
3781
    }
3782
3783
    /**
3784
     * @return array
3785
     */
3786
    public function skeleton() : array
3787
    {
3788
        $skel       = [];
3789
        $columns    = $this->columns(false);
3790
        foreach ( $columns as $column => $type )
3791
        {
3792
            $skel[$column] = null;
3793
        }
3794
3795
        return $skel;
3796
    }
3797
3798
    /**
3799
     * @param bool $toString
3800
     * @return array
3801
     */
3802
    public function getErrors(bool $toString=false) : array
3803
    {
3804
        if ( $toString )
3805
        {
3806
            $errors = [];
3807
            foreach ( $this->_errors as $field => $error )
3808
            {
3809
                $errors[] = implode("\n", $error);
3810
            }
3811
3812
            return implode("\n", $errors);
3813
        }
3814
3815
        return $this->_errors;
3816
    }
3817
3818
    /**
3819
     * @param bool $throw
3820
     * @return FluentPdoModel|$this
3821
     */
3822
    public function validationExceptions(bool $throw=true) : FluentPdoModel
3823
    {
3824
        $this->_validation_exceptions = $throw;
3825
3826
        return $this;
3827
    }
3828
3829
    /**
3830
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
3831
     *
3832
     * @return FluentPdoModel|$this
3833
     * @throws Exception
3834
     */
3835
    public function paginate(array $query=[]) : FluentPdoModel
3836
    {
3837
        $_fields = $_order = $_limit = $_offset = null;
3838
        extract($query);
3839
        $this->_setLimit((int)$_limit, (int)$_offset);
3840
        $this->_setOrderBy((string)$_order);
3841
        $_fields    = is_array($_fields) ? $_fields : (string)$_fields;
3842
        $_fields    = empty($_fields) ? [] : $_fields;
3843
        $_fields    = is_string($_fields) ? explode('|', $_fields) : $_fields;
3844
        $_fields    = empty($_fields) ? [] : $_fields;
3845
        $this->_setFields(is_array($_fields) ? $_fields : explode('|', (string)$_fields));
3846
3847
        return $this;
3848
    }
3849
3850
    /**
3851
     * @param int $limit
3852
     * @param int $offset
3853
     * @return FluentPdoModel|$this
3854
     */
3855
    protected function _setLimit(int $limit=0, int $offset=0) : FluentPdoModel
3856
    {
3857
        $limit      = ! $limit || (int)$limit > (int)$this->_default_max ? (int)$this->_default_max : (int)$limit;
3858
        if ( ! is_numeric($limit) )
3859
        {
3860
            return $this;
3861
        }
3862
        $this->limit((int)$limit);
3863
        if ( $offset && is_numeric($offset) )
3864
        {
3865
            $this->offset((int)$offset);
3866
        }
3867
3868
        return $this;
3869
    }
3870
3871
    /**
3872
     * @param array $fields
3873
     * @return FluentPdoModel|$this
3874
     * @throws Exception
3875
     */
3876
    protected function _setFields(array $fields=[]) : FluentPdoModel
3877
    {
3878
        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...
3879
        {
3880
            return $this;
3881
        }
3882
        $this->explicitSelectMode();
3883
        $columns    = $this->getColumns();
3884
3885
        foreach ( $fields as $idx => $field )
3886
        {
3887
            list($alias, $field) = $this->_getColumnAliasParts($field);
3888
            $field = $field === '_display_field' ? $this->_display_column : $field;
3889
            // Regular primary table field
3890
            if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
3891
            {
3892
                $this->select("{$this->_table_alias}.{$field}");
3893
                $this->_requested_fields[] = "{$this->_table_alias}.{$field}";
3894
                continue;
3895
            }
3896
            // Reference table field with alias
3897
            if ( ! empty($alias) )
3898
            {
3899
                Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
3900
                Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
3901
                list(, , $join_field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
3902
                $this->autoJoin($alias, static::LEFT_JOIN, false);
3903
                $this->select($join_field, $fieldAlias);
3904
                $this->_requested_fields[] = $fieldAlias;
3905
                continue;
3906
            }
3907
            // Reference table select field without alias
3908
            $belongsTo = array_key_exists('belongsTo', $this->_associations) ?  $this->_associations['belongsTo'] : [];
3909
            foreach ( $belongsTo as $joinAlias => $config )
3910
            {
3911
                list(, , $join_field, $fieldAlias) = $config;
3912
                if ( $field === $fieldAlias )
3913
                {
3914
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
3915
                    $this->select($join_field, $fieldAlias);
3916
                    $this->_requested_fields[] = $fieldAlias;
3917
                    continue;
3918
                }
3919
            }
3920
        }
3921
3922
        return $this;
3923
    }
3924
3925
    /**
3926
     * @param string $orderBy
3927
     * @return FluentPdoModel|$this|FluentPdoModel
3928
     */
3929
    protected function _setOrderBy(string $orderBy='') : FluentPdoModel
3930
    {
3931
        if ( ! $orderBy )
3932
        {
3933
            return $this;
3934
        }
3935
        $columns                    = $this->getColumns();
3936
        list($order, $direction)    = strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
3937
        list($alias, $field)        = $this->_getColumnAliasParts(trim($order), '.');
3938
        $field                      = explode(' ', $field);
3939
        $field                      = trim($field[0]);
3940
        $direction                  = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
3941
        $belongsTo                  = array_key_exists('belongsTo', $this->_associations) ? $this->_associations['belongsTo'] : [];
3942
        // Regular primary table order by
3943
        if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
3944
        {
3945
            return $this->orderBy("{$this->_table_alias}.{$field}", $direction);
3946
        }
3947
        // Reference table order by with alias
3948
        if ( ! empty($alias) )
3949
        {
3950
            Assert($belongsTo)->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
3951
            Assert($field)->eq($belongsTo[$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
3952
3953
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
3954
        }
3955
        // Reference table order by without alias
3956
        foreach ( $belongsTo as $joinAlias => $config )
3957
        {
3958
            if ( $field === $config[3] )
3959
            {
3960
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
3961
            }
3962
        }
3963
3964
        return $this;
3965
    }
3966
3967
    /**
3968
     * @return array
3969
     */
3970
    public function getPagingMeta()
3971
    {
3972
        if ( empty($this->_paging_meta) )
3973
        {
3974
            $this->setPagingMeta();
3975
        }
3976
3977
        return $this->_paging_meta;
3978
    }
3979
3980
    /**
3981
     * @return FluentPdoModel|$this
3982
     */
3983
    public function setPagingMeta() : FluentPdoModel
3984
    {
3985
        $model                  = clone $this;
3986
        $limit                  = intval($this->getLimit());
3987
        $offset                 = intval($this->getOffset());
3988
        $total                  = intval($model->withBelongsTo()->select('')->offset(0)->limit(0)->orderBy()->count());
3989
        unset($model->_handlers, $model); //hhmv mem leak
3990
        $order_bys              = ! is_array($this->_order_by) ? [] : $this->_order_by;
3991
        $this->_paging_meta     = [
3992
            'limit'                 => $limit,
3993
            'offset'                => $offset,
3994
            'page'                  => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
3995
            'pages'                 => $limit === 0 ? 1 : intval(ceil($total / $limit)),
3996
            'order'                 => $order_bys,
3997
            'total'                 => $total = $limit === 1 && $total > 1 ? 1 : $total,
3998
            'filters'               => $this->_filter_meta,
3999
            'fields'                => $this->_requested_fields,
4000
            'perms'                 => [],
4001
        ];
4002
4003
        return $this;
4004
    }
4005
4006
    /**
4007
     * Take a web request and format a query
4008
     *
4009
     * @param array $query
4010
     *
4011
     * @return FluentPdoModel|$this
4012
     * @throws Exception
4013
     */
4014
    public function filter(array $query=[]) : FluentPdoModel
4015
    {
4016
        $columns   = $this->getColumns(false);
4017
        $alias     = '';
4018
        foreach ( $query as $column => $value )
4019
        {
4020
            if ( in_array($column, $this->_pagination_attribs) )
4021
            {
4022
                continue;
4023
            }
4024
            $field = $this->_findFieldByQuery($column, $this->_display_column);
4025
            if ( is_null($field) )
4026
            {
4027
                continue;
4028
            }
4029
            $this->_filter_meta[$field]     = $value;
4030
            $where                          = ! is_array($value) && mb_stripos((string)$value, '|') !== false ? explode('|', $value) : $value;
4031
            if ( is_array($where) )
4032
            {
4033
                $this->whereIn($field, $where);
4034
            }
4035
            else
4036
            {
4037
                $this->_addWhereClause($field, (string)$where);
4038
            }
4039
        }
4040
        if ( empty($query['_search']) )
4041
        {
4042
            return $this;
4043
        }
4044
        $alias          = ! empty($alias) ? $alias : $this->_table_alias;
4045
        $string_cols    = array_filter($columns, function($type) {
4046
4047
            return in_array($type, ['varchar', 'text', 'enum']);
4048
        });
4049
        $terms          = explode('|', $query['_search']);
4050
        $where_likes    = [];
4051
        foreach ( $string_cols as $column => $type )
4052
        {
4053
            if ( in_array($column, $this->excluded_search_cols) )
4054
            {
4055
                continue;
4056
            }
4057
            foreach ( $terms as $term )
4058
            {
4059
                $where_likes["{$alias}.{$column}"] = "%{$term}%";
4060
            }
4061
        }
4062
        // Reference fields...
4063
        $belongsTo = $this->getSearchableAssociations();
4064
        foreach ( $belongsTo as $alias => $config )
4065
        {
4066
            foreach ( $terms as $term )
4067
            {
4068
                $where_likes[$config[2]] = "%{$term}%";
4069
            }
4070
        }
4071
        if ( empty($where_likes) )
4072
        {
4073
            return $this;
4074
        }
4075
        $this->where('1', '1')->wrap()->_and();
4076
        foreach ( $where_likes as $column => $term )
4077
        {
4078
            $this->_or()->whereLike($column, $term);
4079
        }
4080
        $this->wrap();
4081
4082
        return $this;
4083
    }
4084
4085
    /**
4086
     * @param string $column
4087
     * @param string $displayCol
4088
     * @return string|null
4089
     */
4090
    protected function _findFieldByQuery(string $column, string $displayCol)
4091
    {
4092
        list($alias, $field)    = $this->_getColumnAliasParts($column);
4093
        $field                  = $field === '_display_field' ? $displayCol : $field;
4094
        $columns                = $this->getColumns();
4095
        $tableAlias             = $this->getTableAlias();
4096
        if ( ! empty($alias) && $alias === $tableAlias )
4097
        {
4098
            // Alias is set but the field isn't correct
4099
            if ( ! in_array($field, $columns) )
4100
            {
4101
                return null;
4102
            }
4103
            return "{$alias}.{$field}";
4104
        }
4105
        // Alias isn't passed in but the field is ok
4106
        if ( empty($alias) && in_array($field, $columns) )
4107
        {
4108
            return "{$tableAlias}.{$field}";
4109
        }
4110
//        // Alias is passed but not this table in but there is a matching field on this table
4111
//        if ( empty($alias) ) //&& in_array($field, $columns) )
4112
//        {
4113
//            return null;
4114
//        }
4115
        // Now search the associations for the field
4116
        $associations = $this->getSearchableAssociations();
4117
        if ( ! empty($alias) )
4118
        {
4119
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
4120
            {
4121
                return "{$alias}.{$field}";
4122
            }
4123
4124
            return null;
4125
        }
4126
        foreach ( $associations as $assocAlias => $config )
4127
        {
4128
            list(, , $assocField, $fieldAlias) = $config;
4129
            if ( $fieldAlias === $field )
4130
            {
4131
                return $assocField;
4132
            }
4133
        }
4134
4135
        return null;
4136
    }
4137
4138
    /**
4139
     * @param $keysOnly
4140
     * @return array
4141
     */
4142
4143
    public function columns(bool $keysOnly=true) : array
4144
    {
4145
        return $keysOnly ? array_keys($this->_schema) : $this->_schema;
4146
    }
4147
4148
    /**
4149
     * @param string $field
4150
     * @param mixed $value
4151
     * @param array $pdoMetaData
4152
     * @return float|int
4153
     * @throws Exception
4154
     */
4155
    protected function _fixTypeToSentinel(string $field, $value, array $pdoMetaData=[])
4156
    {
4157
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4158
4159
        $fieldType      = strtolower($pdoMetaData['native_type']) ?: null;
4160
        if ( ! $fieldType )
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldType of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
4161
        {
4162
            if ( empty($this->_schema) )
4163
            {
4164
                return $value;
4165
            }
4166
            $columns    = $this->getColumns(false);
4167
            Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4168
4169
            $fieldType = $columns[$field] ?: null;
4170
        }
4171
4172
4173
        // Don't cast invalid values... only those that can be cast cleanly
4174
        switch ( $fieldType )
4175
        {
4176
            case 'varchar':
4177
            case 'var_string':
4178
            case 'string':
4179
            case 'text';
4180
            case 'date':
4181
            case 'datetime':
4182
            case 'timestamp':
4183
            case 'blob':
4184
4185
                return (string)$value;
4186
4187
            case 'int':
4188
            case 'integer':
4189
            case 'tinyint':
4190
            case 'tiny':
4191
            case 'long':
4192
            case 'longlong':
4193
4194
                return (int)$value;
4195
4196
            case 'decimal':
4197
            case 'newdecimal':
4198
4199
                return (float)$value;
4200
4201
            default:
4202
4203
                throw new Exception('Unknown type: ' . $fieldType);
4204
                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...
4205
        }
4206
    }
4207
4208
    /**
4209
     * @param string $field
4210
     * @param mixed $value
4211
     * @param bool|false $permissive
4212
     * @return float|int|null|string
4213
     */
4214
    protected function _fixType(string $field, $value, bool $permissive=false)
4215
    {
4216
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4217
        if ( empty($this->_schema) || ( ! array_key_exists($field, $this->_schema) && $permissive ) )
4218
        {
4219
            return $value;
4220
        }
4221
        $columns    = $this->getColumns(false);
4222
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4223
4224
        $fieldType = ! empty($columns[$field]) ? $columns[$field] : null;
4225
4226
        if ( is_null($value) )
4227
        {
4228
            return null;
4229
        }
4230
        // return on null, '' but not 0
4231
        if ( ! is_numeric($value) && empty($value) )
4232
        {
4233
            return null;
4234
        }
4235
        // Don't cast invalid values... only those that can be cast cleanly
4236
        switch ( $fieldType )
4237
        {
4238
            case 'varchar':
4239
            case 'text';
4240
            case 'date':
4241
            case 'datetime':
4242
            case 'timestamp':
4243
4244
                // return on null, '' but not 0
4245
                return ! is_numeric($value) && empty($value) ? null : (string)$value;
4246
4247
            case 'int':
4248
4249
                if ( $field === 'id' || substr($field, -3) === '_id' )
4250
                {
4251
                    return $value ? (int)$value : null;
4252
                }
4253
4254
                return ! is_numeric($value) ? null : (int)$value;
4255
4256
            case 'decimal':
4257
4258
                return ! is_numeric($value) ? null : (float)$value;
4259
4260
            default:
4261
4262
                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...
4263
        }
4264
    }
4265
4266
    /**
4267
     * @param stdClass $record
4268
     * @param string $type
4269
     * @return stdClass
4270
     */
4271
    public function fixTypesToSentinel(stdClass $record, string $type='') : stdClass
4272
    {
4273
        foreach ( $this->row_meta_data as $column => $pdoMetaData )
4274
        {
4275
            if ( ! property_exists($record, $column) )
4276
            {
4277
                continue;
4278
            }
4279
            $record->{$column} = $this->_fixTypeToSentinel($column, $record->{$column}, $pdoMetaData);
4280
        }
4281
        // PDO might not be able to generate the meta data to sniff types
4282
        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...
4283
        {
4284
            foreach ( $this->getColumns(false) as $column => $fieldType )
4285
            {
4286
                if ( ! property_exists($record, $column) )
4287
                {
4288
                    continue;
4289
                }
4290
                $record->{$column} = $this->_fixTypeToSentinel($column, $record->{$column});
4291
            }
4292
        }
4293
4294
        unset($type);
4295
4296
        return $record;
4297
    }
4298
4299
    /**
4300
     * @param stdClass $record
4301
     * @param string   $type
4302
     * @return stdClass
4303
     * @throws Exception
4304
     */
4305
    public function applyHandlers(stdClass $record, string $type='INSERT') : stdClass
4306
    {
4307
        $this->_compileHandlers();
4308
        $this->_errors                  = [];
4309
        // Disable per field exceptions so we can capture all errors for the record
4310
        $tmpExceptions                  = $this->_validation_exceptions;
4311
        $this->_validation_exceptions   = false;
4312
        foreach ( $this->_handlers as $field => $fn_validator )
4313
        {
4314
            if ( ! property_exists($record, $field) )
4315
            {
4316
                // If the operation is an update it can be a partial update
4317
                if ( $type === self::SAVE_UPDATE )
4318
                {
4319
                    continue;
4320
                }
4321
                $record->{$field}               = null;
4322
            }
4323
            $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...
4324
        }
4325
        $this->_validation_exceptions = $tmpExceptions;
4326
        if ( $this->_validation_exceptions && ! empty($this->_errors) )
4327
        {
4328
            throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4329
        }
4330
4331
        return $record;
4332
    }
4333
4334
4335
    /**
4336
     * @param stdClass $record
4337
     * @param array    $fields
4338
     * @param string   $type
4339
     * @return bool
4340
     */
4341
    protected function uniqueCheck(stdClass $record, array $fields, string $type) : bool
4342
    {
4343
        if ( $type === self::SAVE_UPDATE )
4344
        {
4345
            $this->whereNot($this->_primary_key, $record->{$this->_primary_key});
4346
        }
4347
        foreach ( $fields as $field )
4348
        {
4349
            $this->where($field, $record->{$field});
4350
        }
4351
4352
        return (int)$this->count() > 0;
4353
    }
4354
4355
    /**
4356
     * @param string $field
4357
     * @param mixed $value
4358
     * @param string $type
4359
     * @param stdClass $record
4360
     * @return null
4361
     * @throws Exception
4362
     */
4363
    protected function applyHandler(string $field, $value, string $type='', stdClass $record=null)
4364
    {
4365
        $this->_compileHandlers();
4366
        $fnHandler = ! empty($this->_handlers[$field]) ? $this->_handlers[$field] : null;
4367
        if ( is_callable($fnHandler) )
4368
        {
4369
            try
4370
            {
4371
                $value = $fnHandler($field, $value, $type, $record);
4372
            }
4373
            catch( Exception $e )
4374
            {
4375
                $this->_errors[$field][] = $e->getMessage();
4376
                if ( $this->_validation_exceptions && ! empty($this->_errors) )
4377
                {
4378
                    throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4379
                }
4380
4381
                return null;
4382
            }
4383
        }
4384
4385
        return $value;
4386
    }
4387
4388
    /**
4389
     * @param string $start
4390
     * @param string $end
4391
     * @param string $hayStack
4392
     * @return mixed
4393
     */
4394
    public static function between(string $start, string $end, string $hayStack) : string
4395
    {
4396
        return static::before($end, static::after($start, $hayStack));
4397
    }
4398
4399
    /**
4400
     * @param string     $needle
4401
     * @param string     $hayStack
4402
     * @param bool $returnOrigIfNeedleNotExists
4403
     * @return mixed
4404
     */
4405
    public static function before(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4406
    {
4407
        $result = mb_substr($hayStack, 0, mb_strpos($hayStack, $needle));
4408
        if ( !$result && $returnOrigIfNeedleNotExists )
4409
        {
4410
            return $hayStack;
4411
        }
4412
4413
        return $result;
4414
    }
4415
4416
    /**
4417
     * @param string     $needle
4418
     * @param string     $hayStack
4419
     * @param bool $returnOrigIfNeedleNotExists
4420
     * @return string
4421
     */
4422
    public static function after(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4423
    {
4424
        if ( ! is_bool(mb_strpos($hayStack, $needle)) )
4425
        {
4426
            return mb_substr($hayStack, mb_strpos($hayStack, $needle) + mb_strlen($needle));
4427
        }
4428
4429
        return $returnOrigIfNeedleNotExists ? $hayStack : '';
4430
    }
4431
4432
    /**
4433
     * @return int
4434
     */
4435
    public function getUserId()
4436
    {
4437
        return 0;
4438
    }
4439
4440
    /**
4441
     * @param string $entity
4442
     * @param int $id
4443
     * @return int
4444
     */
4445
    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...
4446
    {
4447
        return 31;
4448
    }
4449
4450
    /**
4451
     * @param string|int|null $time
4452
     * @return string
4453
     */
4454
    public static function date($time=null) : string
4455
    {
4456
        return date('Y-m-d', static::getTime($time));
4457
    }
4458
4459
    /**
4460
     * @param string|int|null $time
4461
     * @return string
4462
     */
4463
    public static function dateTime($time=null) : string
4464
    {
4465
        return date('Y-m-d H:i:s', static::getTime($time));
4466
    }
4467
4468
    /**
4469
     * @param string|int|null $time
4470
     * @return string
4471
     */
4472
    public static function atom($time=null) : string
4473
    {
4474
        return date('Y-m-d\TH:i:sP', static::getTime($time));
4475
    }
4476
4477
    /**
4478
     * @param string|int|null $time
4479
     * @return int
4480
     */
4481
    public static function getTime($time=null) : int
4482
    {
4483
        if ( ! $time )
4484
        {
4485
            return time();
4486
        }
4487
        if ( is_int($time) )
4488
        {
4489
            return $time;
4490
        }
4491
4492
        return strtotime($time);
4493
    }
4494
4495
    /**
4496
     * @param int $id
4497
     * @param int $cacheTtl
4498
     * @return string
4499
     */
4500
    public function getCodeById(int $id, int $cacheTtl=self::ONE_DAY) : string
4501
    {
4502
        Assert($id)->id();
4503
        $code   = $this->defaultFilters()->fetchStr($this->getDisplayColumn(), $id, $cacheTtl);
4504
        Assert($code)->notEmpty();
4505
4506
        return $code;
4507
    }
4508
4509
    /**
4510
     * @param array $authUserRoles
4511
     * @param int   $authUserId
4512
     * @return FluentPdoModel
4513
     */
4514
    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...
4515
    {
4516
        return $this;
4517
    }
4518
4519
    /**
4520
     * @param int    $id
4521
     * @param string[] $authUserRoles
4522
     * @param int    $authUserId
4523
     * @return bool
4524
     */
4525
    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...
4526
    {
4527
        return true;
4528
    }
4529
}
4530