Completed
Push — master ( e780a4...01431d )
by Terry
06:28
created

FluentPdoModel::delete()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 3
nop 2
dl 0
loc 16
rs 8.8571
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 ONE_DAY                   = 86400;
41
    const ONE_WEEK                  = 60480060;
42
    const ONE_HOUR                  = 3600;
43
    const TEN_MINS                  = 600;
44
    const OPERATOR_AND              = ' AND ';
45
    const OPERATOR_OR               = ' OR ';
46
    const ORDERBY_ASC               = 'ASC';
47
    const ORDERBY_DESC              = 'DESC';
48
    const SAVE_INSERT               = 'INSERT';
49
    const SAVE_UPDATE               = 'UPDATE';
50
    const LEFT_JOIN                 = 'LEFT';
51
    const INNER_JOIN                = 'INNER';
52
    const CACHE_NO                  = -1;
53
    const CACHE_DEFAULT             = 0;
54
55
    /** @var AbstractPdo $_connection */
56
    protected $_connection          = null;
57
58
    /** @var string */
59
    protected $_primary_key         = 'id';
60
61
    /** @var array */
62
    protected $_where_parameters    = [];
63
64
    /** @var array */
65
    protected $_select_fields       = [];
66
67
    /** @var array */
68
    protected $_join_sources        = [];
69
70
    /** @var array */
71
    protected $_join_aliases        = [];
72
73
    /** @var array $_associations */
74
    protected $_associations        = [
75
        'belongsTo' => [],
76
    ];
77
78
    /** @var array */
79
    protected $_where_conditions    = [];
80
81
    protected $_raw_sql             = '';
82
83
    /** @var int */
84
    protected $_limit               = 0;
85
86
    /** @var int */
87
    protected $_offset              = 0;
88
89
    /** @var array */
90
    protected $_order_by            = [];
91
92
    /** @var array */
93
    protected $_group_by            = [];
94
95
    /** @var string */
96
    protected $_and_or_operator     = self::OPERATOR_AND;
97
98
    /** @var array */
99
    protected $_having              = [];
100
101
    /** @var bool */
102
    protected $_wrap_open           = false;
103
104
    /** @var int */
105
    protected $_last_wrap_position  = 0;
106
107
    /** @var PDOStatement $_pdo_stmt */
108
    protected $_pdo_stmt            = null;
109
110
    /** @var bool */
111
    protected $_distinct            = false;
112
113
    /** @var null */
114
    protected $_requested_fields    = [];
115
116
    /** @var null */
117
    protected $_filter_meta         = [];
118
119
    /** @var bool */
120
    protected $_log_queries         = false;
121
122
    /** @var array */
123
    protected $_timer               = [];
124
125
    /** @var int */
126
    protected $_slow_query_secs     = 5;
127
128
    /** @var array */
129
    protected $_pagination_attribs  = [
130
        '_limit',
131
        '_offset',
132
        '_order',
133
        '_fields',
134
        '_search'
135
    ];
136
137
    /** @var string $_table_name */
138
    protected $_table_name          = '';
139
140
    /** @var string $_table_alias */
141
    protected $_table_alias         = '';
142
143
    /** @var string $_display_column */
144
    protected $_display_column      = '';
145
146
    /** @var string $_connection_name */
147
    protected $_connection_name     = '';
148
149
    /** @var array $_schema */
150
    protected $_schema              = [];
151
152
    /** @var array $_virtual_fields */
153
    protected $_virtual_fields      = [];
154
155
    /** @var array $_errors */
156
    protected $_errors              = [];
157
158
    /**
159
     * @var int - true  = connection default x days
160
     *          - false = no cache
161
     *          - int   = a specific amount
162
     */
163
    protected $_cache_ttl               = self::CACHE_NO;
164
165
    /** @var string */
166
    protected $_tmp_table_prefix        = 'tmp_';
167
168
    /** @var null|string */
169
    protected $_built_query             = '';
170
171
    /** @var array  */
172
    protected $_handlers                = [];
173
174
    /** @var bool User want to directly specify the fields */
175
    protected $_explicit_select_mode    = false;
176
177
    /** @var string[] */
178
    protected $_update_raw              = [];
179
180
    protected $_max_callback_failures   = -1;
181
182
    protected $_num_callback_failures   = 0;
183
184
    protected $_filter_on_fetch         = false;
185
186
    protected $_log_filter_changes      = true;
187
188
    protected $_include_count           = false;
189
190
    /** @var string */
191
    static protected $_model_namespace  = '';
192
193
    /** @var bool */
194
    protected $_validation_exceptions   = true;
195
196
    /** @var array */
197
    protected $_paging_meta             = [];
198
199
    /** @var bool */
200
    protected $_soft_deletes            = true;
201
202
203
    /** @var bool  */
204
    protected $_allow_meta_override     = false;
205
206
    /** @var int  */
207
    protected $_default_max             = 250;
208
209
    /** @var array  */
210
    protected $removeUnauthorisedFields = [];
211
212
    protected $_can_generic_update      = true;
213
    protected $_can_generic_add         = true;
214
    protected $_can_generic_delete      = true;
215
216
    /** @var array  */
217
    protected $globalRemoveUnauthorisedFields = [
218
        '/global_table_meta#view' => [
219
            'created_by_id',
220
            'created_by',
221
            'created_ts',
222
            'modified_by_id',
223
            'modified_by',
224
            'modified_ts',
225
            'status',
226
        ],
227
    ];
228
229
230
    /**
231
     * @param AbstractPdo|null $connection
232
     */
233
    public function __construct(AbstractPdo $connection=null)
234
    {
235
        $connection         = $connection ?: ConnectionPool::get($this->_connection_name);
236
        $this->_connection  = $connection;
237
        $this->_log_queries = $connection->logQueries();
238
        $this->init();
239
    }
240
241
    public function init()
242
    {}
243
244
    /**
245
     * @return AbstractPdo
246
     * @throws Exception
247
     */
248
    public function getPdo() : AbstractPdo
249
    {
250
        return $this->_connection;
251
    }
252
253
    /**
254
     * @return LoggerInterface
255
     */
256
    public function getLogger() : LoggerInterface
257
    {
258
        return $this->_connection->getLogger();
259
    }
260
261
    /**
262
     * @return CacheInterface
263
     */
264
    public function getCache() : CacheInterface
265
    {
266
        return $this->_connection->getCache();
267
    }
268
269
    /**
270
     * Define the working table and create a new instance
271
     *
272
     * @param string $tableName - Table name
273
     * @param string $alias     - The table alias name
274
     * @param string $displayColumn
275
     * @param string $primaryKeyName
276
     *
277
     * @return FluentPdoModel
278
     */
279
    public function table(string $tableName, string $alias='', string $displayColumn='', string $primaryKeyName='id') : FluentPdoModel
280
    {
281
        return $this->reset()
282
            ->tableName($tableName)
283
            ->tableAlias($alias)
284
            ->displayColumn($displayColumn)
285
            ->primaryKeyName($primaryKeyName);
286
    }
287
288
    /**
289
     * @param string $primaryKeyName
290
     * @return FluentPdoModel
291
     */
292
    public function primaryKeyName(string $primaryKeyName) : FluentPdoModel
293
    {
294
        $this->_primary_key = $primaryKeyName;
295
296
        return $this;
297
    }
298
299
    /**
300
     * @param string $tableName
301
     *
302
     * @return FluentPdoModel
303
     */
304
    public function tableName(string $tableName) : FluentPdoModel
305
    {
306
        $this->_table_name  = $tableName;
307
308
        return $this;
309
    }
310
311
    /**
312
     * @param $explicitSelect
313
     *
314
     * @return FluentPdoModel
315
     */
316
    public function explicitSelectMode(bool $explicitSelect=true) : FluentPdoModel
317
    {
318
        $this->_explicit_select_mode  = (bool)$explicitSelect;
319
320
        return $this;
321
    }
322
323
    /**
324
     * @param bool $filterOnFetch
325
     *
326
     * @return FluentPdoModel
327
     */
328
    public function filterOnFetch(bool $filterOnFetch=true) : FluentPdoModel
329
    {
330
        $this->_filter_on_fetch  = (bool)$filterOnFetch;
331
332
        return $this;
333
    }
334
335
    /**
336
     * @param bool $logFilterChanges
337
     *
338
     * @return FluentPdoModel
339
     */
340
    public function logFilterChanges(bool $logFilterChanges=true) : FluentPdoModel
341
    {
342
        $this->_log_filter_changes  = (bool)$logFilterChanges;
343
344
        return $this;
345
    }
346
347
    /**
348
     * Return the name of the table
349
     *
350
     * @return string
351
     */
352
    public function getTableName() : string
353
    {
354
        return $this->_table_name;
355
    }
356
357
    /**
358
     * @return string
359
     */
360
    public function getDisplayColumn() : string
361
    {
362
        return $this->_display_column;
363
    }
364
365
    /**
366
     * Set the display column
367
     *
368
     * @param string $column
369
     *
370
     * @return FluentPdoModel
371
     */
372
    public function displayColumn(string $column) : FluentPdoModel
373
    {
374
        $this->_display_column = $column;
375
376
        return $this;
377
    }
378
    /**
379
     * Set the table alias
380
     *
381
     * @param string $alias
382
     *
383
     * @return FluentPdoModel
384
     */
385
    public function tableAlias(string $alias) : FluentPdoModel
386
    {
387
        $this->_table_alias = $alias;
388
389
        return $this;
390
    }
391
392
    /**
393
     * @param int $cacheTtl
394
     * @return FluentPdoModel
395
     * @throws Exception
396
     */
397
    protected function _cacheTtl(int $cacheTtl) : FluentPdoModel
398
    {
399
        Assert($cacheTtl)->int('Cache ttl must be either -1 for no cache, 0 for default ttl or an integer for a custom ttl');
400
        if ( $cacheTtl !== self::CACHE_NO && ! is_null($this->_pdo_stmt) )
401
        {
402
            throw new Exception("You cannot cache pre-executed queries");
403
        }
404
        $this->_cache_ttl = $cacheTtl;
405
406
        return $this;
407
    }
408
409
    /**
410
     * @return string
411
     */
412
    public function getTableAlias() : string
413
    {
414
        return $this->_table_alias;
415
    }
416
417
    /**
418
     * @param array $associations
419
     *
420
     * @return FluentPdoModel
421
     */
422
    public function associations(array $associations) : FluentPdoModel
423
    {
424
        $this->_associations = $associations;
425
426
        return $this;
427
    }
428
429
    /**
430
     * @param string $alias
431
     * @param array $definition
432
     * @return FluentPdoModel
433
     */
434
    public function setBelongsTo(string $alias, array $definition) : FluentPdoModel
435
    {
436
        Assert($alias)->notEmpty();
437
        Assert($definition)->isArray()->count(4);
438
439
        $this->_associations['belongsTo'][$alias] = $definition;
440
441
        return $this;
442
    }
443
444
    /**
445
     * @param $alias
446
     * @param $displayField
447
     * @return FluentPdoModel
448
     * @throws \Terah\Assert\AssertionFailedException
449
     */
450
    public function setBelongsToDisplayField(string $alias, string $displayField) : FluentPdoModel
451
    {
452
        Assert($alias)->notEmpty();
453
        Assert($this->_associations['belongsTo'])->keyExists($alias);
454
        Assert($displayField)->notEmpty();
455
456
        $this->_associations['belongsTo'][$alias][2] = $displayField;
457
458
        return $this;
459
    }
460
461
    /**
462
     * @param PDOStatement $stmt
463
     *
464
     * @param PDOStatement $stmt
465
     * @param Closure $fnCallback
466
     * @return bool|stdClass
467
     */
468
    public function fetchRow(PDOStatement $stmt, Closure $fnCallback=null)
469
    {
470
        if ( ! ( $record = $stmt->fetch(PDO::FETCH_OBJ) ) )
471
        {
472
            return false;
473
        }
474
        $record = $this->onFetch($record);
475
        if ( empty($fnCallback) )
476
        {
477
            return $record;
478
        }
479
        $record = $fnCallback($record);
480
        if ( is_null($record) )
481
        {
482
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
483
        }
484
        unset($fnCallback);
485
486
        return $record;
487
    }
488
489
    /**
490
     * @param array $schema
491
     *
492
     * @return FluentPdoModel
493
     */
494
    public function schema(array $schema) : FluentPdoModel
495
    {
496
        $this->_schema = $schema;
497
498
        return $this;
499
    }
500
501
    /**
502
     * @param string|array $field
503
     * @param $type
504
     * @return FluentPdoModel
505
     */
506
    public function addSchema($field, string $type) : FluentPdoModel
507
    {
508
        if ( is_array($field) )
509
        {
510
            foreach ( $field as $field_name => $type_def )
511
            {
512
                $this->addSchema($field_name, $type_def);
513
            }
514
515
            return $this;
516
        }
517
        Assert($field)->string()->notEmpty();
518
        $this->_schema[$field] = $type;
519
520
        return $this;
521
    }
522
523
    /**
524
     * @param $keysOnly
525
     * @return array
526
     */
527
    public function getColumns(bool $keysOnly=true) : array
528
    {
529
        return $keysOnly ? array_keys($this->_schema) : $this->_schema;
530
    }
531
532
    /**
533
     * Get the primary key name
534
     *
535
     * @return string
536
     */
537
    public function getPrimaryKeyName() : string
538
    {
539
        return $this->_formatKeyName($this->_primary_key, $this->_table_name);
540
    }
541
542
    /**
543
     * @param string $query
544
     * @param array $parameters
545
     *
546
     * @return bool
547
     * @throws Exception
548
     */
549
    public function execute(string $query, array $parameters=[]) : bool
550
    {
551
        list($this->_built_query, $ident)  = $this->_logQuery($query, $parameters);
552
        try
553
        {
554
            $this->_pdo_stmt        = $this->getPdo()->prepare($query);
555
            $result                 = $this->_pdo_stmt->execute($parameters);
556
            if ( false === $result )
557
            {
558
                $this->_pdo_stmt        = null;
559
                throw new PDOException("The query failed to execute.");
560
            }
561
        }
562
        catch( Exception $e )
563
        {
564
            $built_query        = $this->_built_query ? $this->_built_query : $this->buildQuery($query, $parameters);
565
            $this->getLogger()->error("FAILED: \n\n{$built_query}\n WITH ERROR:\n" . $e->getMessage());
566
            $this->_pdo_stmt    = null;
567
568
            throw $e;
569
        }
570
        $this->_logSlowQueries($ident, $this->_built_query);
571
572
        return $result;
573
    }
574
575
    /**
576
     * @param string $query
577
     * @param array $params
578
     * @return FluentPdoModel
579
     */
580
    public function query(string $query, array $params=[]) : FluentPdoModel
581
    {
582
        $this->_raw_sql             = $query;
583
        $this->_where_parameters    = $params;
584
585
        return $this;
586
    }
587
588
    /**
589
     * @param string $sql
590
     * @param array $params
591
     *
592
     * @return string
593
     */
594
    public function buildQuery(string $sql, array $params=[]) : string
595
    {
596
        $indexed = $params == array_values($params);
597
        if ( $indexed )
598
        {
599
            foreach ( $params as $key => $val )
600
            {
601
                $val    = is_string($val) ? "'{$val}'" : $val;
602
                $val    = is_null($val) ? 'NULL' : $val;
603
                $sql    = preg_replace('/\?/', $val, $sql, 1);
604
            }
605
606
            return $sql;
607
        }
608
609
        uksort($params, function ($a, $b) {
610
            return strlen($b) - strlen($a);
611
        });
612
        foreach ( $params as $key => $val )
613
        {
614
            $val    = is_string($val) ? "'{$val}'" : $val;
615
            $val    = is_null($val) ? 'NULL' : $val;
616
            $sql    = str_replace(":$key", $val, $sql);
617
            //$sql    = str_replace("$key", $val, $sql);
618
        }
619
620
        return $sql;
621
    }
622
623
    /**
624
     * @param stdClass $record
625
     *
626
     * @return stdClass
627
     */
628
    protected function _trimAndLowerCaseKeys(stdClass $record) : stdClass
629
    {
630
        $fnTrimStrings = function($value) {
631
            return is_string($value) ? trim($value) : $value;
632
        };
633
        $record = array_map($fnTrimStrings, array_change_key_case((array)$record, CASE_LOWER));
634
        unset($fnTrimStrings);
635
636
        return (object)$record;
637
    }
638
639
    /**
640
     * Return the number of affected row by the last statement
641
     *
642
     * @return int
643
     */
644
    public function rowCount() : int
645
    {
646
        $stmt = $this->fetchStmt();
647
648
        return $stmt ? $stmt->rowCount() : 0;
649
    }
650
651
    /**
652
     * @return PDOStatement|null
653
     * @throws PDOException
654
     */
655
    public function fetchStmt()
656
    {
657
        if ( null === $this->_pdo_stmt )
658
        {
659
            $this->execute($this->getSelectQuery(), $this->_getWhereParameters());
660
        }
661
662
        return $this->_pdo_stmt;
663
    }
664
665
    /**
666
     * @return array
667
     */
668
    public function fetchSqlQuery() : array
669
    {
670
        $clone      = clone $this;
671
        $query      = $clone->getSelectQuery();
672
        $params     = $clone->_getWhereParameters();
673
        $result     = [$query, $params];
674
        unset($clone->_handlers, $clone, $query, $params);
675
676
        return $result;
677
    }
678
679
    /**
680
     * @param string $tableName
681
     * @param bool  $dropIfExists
682
     * @param array $indexes
683
     * @return boolean
684
     * @throws Exception
685
     */
686
    public function fetchIntoMemoryTable(string $tableName, bool $dropIfExists=true, array $indexes=[]) : bool
687
    {
688
        $tableName      = preg_replace('/[^A-Za-z0-9_]+/', '', $tableName);
689
        $tableName      = $this->_tmp_table_prefix . preg_replace('/^' . $this->_tmp_table_prefix . '/', '', $tableName);
690
        if ( $dropIfExists )
691
        {
692
            $this->execute("DROP TABLE IF EXISTS {$tableName}");
693
        }
694
        $indexSql = [];
695
        foreach ( $indexes as $name => $column )
696
        {
697
            $indexSql[] = "INDEX {$name} ({$column})";
698
        }
699
        $indexSql       = implode(", ", $indexSql);
700
        $indexSql       = empty($indexSql) ? '' : "({$indexSql})";
701
        list($querySql, $params) = $this->fetchSqlQuery();
702
        $sql = <<<SQL
703
        CREATE TEMPORARY TABLE {$tableName} {$indexSql} ENGINE=MEMORY {$querySql}
704
SQL;
705
706
        return $this->execute($sql, $params);
707
    }
708
709
    /**
710
     * @param string $keyedOn
711
     * @param int $cacheTtl
712
     * @return stdClass[]
713
     */
714
    public function fetch(string $keyedOn='', int $cacheTtl=self::CACHE_NO) : array
715
    {
716
        $this->_cacheTtl($cacheTtl);
717
        $fnCallback = function() use ($keyedOn) {
718
719
            $stmt   = $this->fetchStmt();
720
            $rows   = [];
721
            while ( $record = $this->fetchRow($stmt) )
0 ignored issues
show
Bug introduced by
It seems like $stmt defined by $this->fetchStmt() on line 719 can be null; however, Terah\FluentPdoModel\FluentPdoModel::fetchRow() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
722
            {
723
                if ( $record === false ) continue; // For scrutinizer...
724
                if ( $keyedOn && property_exists($record, $keyedOn) )
725
                {
726
                    $rows[$record->$keyedOn] = $record;
727
                    continue;
728
                }
729
                $rows[] = $record;
730
            }
731
            $this->reset();
732
733
            return $rows;
734
        };
735
        if ( $this->_cache_ttl === self::CACHE_NO )
736
        {
737
            return $fnCallback();
738
        }
739
        $table              = $this->getTableName();
740
        $id                 = $this->_parseWhereForPrimaryLookup();
741
        $id                 = $id ? "/{$id}" : '';
742
        list($sql, $params) = $this->fetchSqlQuery();
743
        $sql                = $this->buildQuery($sql, $params);
744
        $cacheKey           = "/{$table}{$id}/" . md5(json_encode([
745
            'sql'       => $sql,
746
            'keyed_on'  => $keyedOn,
747
        ]));
748
        $data = $this->_cacheData($cacheKey, $fnCallback, $this->_cache_ttl);
749
750
        return is_array($data) ? $data : [];
751
    }
752
753
    /**
754
     * @return string
755
     */
756
    protected function _parseWhereForPrimaryLookup() : string
757
    {
758
        if ( ! ( $alias = $this->getTableAlias() ) )
759
        {
760
            return '';
761
        }
762
        foreach ( $this->_where_conditions as $idx => $conds )
763
        {
764
            if ( ! empty($conds['STATEMENT']) && $conds['STATEMENT'] === "{$alias}.id = ?" )
765
            {
766
                return ! empty($conds['PARAMS'][0]) ? (string)$conds['PARAMS'][0] : '';
767
            }
768
        }
769
770
        return '';
771
    }
772
773
    /**
774
     * @param string $cacheKey
775
     * @param Closure $func
776
     * @param int $cacheTtl - 0 for default ttl, -1 for no cache or int for custom ttl
777
     * @return mixed
778
     */
779
    protected function _cacheData(string $cacheKey, Closure $func, int $cacheTtl=self::CACHE_DEFAULT)
780
    {
781
        if ( $cacheTtl === self::CACHE_NO )
782
        {
783
            /** @noinspection PhpVoidFunctionResultUsedInspection */
784
            return $func->__invoke();
785
        }
786
        $data = $this->getCache()->get($cacheKey);
787
        if ( $data && is_object($data) && property_exists($data, 'results') )
788
        {
789
            $this->getLogger()->debug("Cache hit on {$cacheKey}");
790
            return $data->results;
791
        }
792
        $this->getLogger()->debug("Cache miss on {$cacheKey}");
793
        /** @noinspection PhpVoidFunctionResultUsedInspection */
794
        $data = (object)[
795
            // Watch out... invoke most likely calls reset
796
            // which clears the model params like _cache_ttl
797
            'results' => $func->__invoke(),
798
        ];
799
        try
800
        {
801
            // The cache engine expects null for the default cache value
802
            $cacheTtl = $cacheTtl === self::CACHE_DEFAULT ? null : $cacheTtl;
803
            /** @noinspection PhpMethodParametersCountMismatchInspection */
804
            if ( ! $this->getCache()->set($cacheKey, $data, $cacheTtl) )
805
            {
806
                throw new \Exception("Could not save data to cache");
807
            }
808
            return $data->results;
809
        }
810
        catch (\Exception $e)
811
        {
812
            $this->getLogger()->error($e->getMessage(), $e->getTrace());
813
814
            return $data->results;
815
        }
816
    }
817
818
    /**
819
     * @param string $cacheKey
820
     * @return bool
821
     */
822
    public function clearCache(string $cacheKey) : bool
823
    {
824
        return $this->getCache()->delete($cacheKey);
825
    }
826
827
    /**
828
     * @param string $table
829
     * @return bool
830
     */
831
    public function clearCacheByTable(string $table='') : bool
832
    {
833
        $table  = $table ?: $this->getTableName();
834
        if ( ! $table )
835
        {
836
            return true;
837
        }
838
839
        return $this->clearCache("/{$table}/");
840
    }
841
842
    /**
843
     * @param Closure $fnCallback
844
     * @return int
845
     */
846
    public function fetchCallback(Closure $fnCallback) : int
847
    {
848
        $successCnt    = 0;
849
        $stmt           = $this->fetchStmt();
850
        while ( $this->_tallySuccessCount($stmt, $fnCallback, $successCnt) )
0 ignored issues
show
Bug introduced by
It seems like $stmt defined by $this->fetchStmt() on line 849 can be null; however, Terah\FluentPdoModel\Flu...l::_tallySuccessCount() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
851
        {}
852
853
        return $successCnt;
854
    }
855
856
    /**
857
     * @param Closure $fnCallback
858
     * @param string  $keyedOn
859
     * @return array
860
     */
861
    public function fetchObjectsByCallback(Closure $fnCallback, string $keyedOn='') : array
862
    {
863
        $stmt       = $this->fetchStmt();
864
        $rows       = [];
865
        while ( $record = $this->fetchRow($stmt, $fnCallback) )
0 ignored issues
show
Bug introduced by
It seems like $stmt defined by $this->fetchStmt() on line 863 can be null; however, Terah\FluentPdoModel\FluentPdoModel::fetchRow() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
866
        {
867
            if ( $keyedOn && property_exists($record, $keyedOn) )
868
            {
869
                $rows[$record->$keyedOn] = $record;
870
                continue;
871
            }
872
            $rows[] = $record;
873
        }
874
        $this->reset();
875
876
        return $rows;
877
    }
878
879
    /**
880
     * @param $numFailures
881
     * @return FluentPdoModel
882
     */
883
    public function maxCallbackFailures(int $numFailures) : FluentPdoModel
884
    {
885
        Assert($numFailures)->int();
886
        $this->_max_callback_failures = $numFailures;
887
888
        return $this;
889
    }
890
891
    /**
892
     * @param PDOStatement $stmt
893
     * @param Closure $fnCallback
894
     * @param int $successCnt
895
     * @return bool|null|stdClass
896
     */
897
    protected function _tallySuccessCount(PDOStatement $stmt, Closure $fnCallback, int &$successCnt)
898
    {
899
        $record = $this->fetchRow($stmt);
900
        if ( $record === false )
901
        {
902
            return false;
903
        }
904
        $record = $fnCallback($record);
905
        // Callback return null then we want to exit the fetch loop
906
        if ( is_null($record) )
907
        {
908
            $this->getLogger()->warning("The callback is not returning any data which might be causing early termination of the result iteration");
909
            return null;
910
        }
911
        // The not record then don't bump the tally
912
        if ( ! $record )
913
        {
914
            $this->_num_callback_failures++;
915
            if ( ! is_null($this->_max_callback_failures) && $this->_num_callback_failures >= $this->_max_callback_failures )
916
            {
917
                $this->getLogger()->error("The callback has failed {$this->_max_callback_failures} times... aborting...");
918
                $successCnt = null;
919
                return null;
920
            }
921
922
            return true;
923
        }
924
        $successCnt++;
925
926
        return $record;
927
    }
928
929
    /**
930
     * @return bool
931
     */
932
    public function canGenericUpdate() : bool
933
    {
934
        return $this->_can_generic_update;
935
    }
936
937
    /**
938
     * @return bool
939
     */
940
    public function canGenericAdd() : bool
941
    {
942
        return $this->_can_generic_add;
943
    }
944
945
    /**
946
     * @return bool
947
     */
948
    public function canGenericDelete() : bool
949
    {
950
        return $this->_can_generic_delete;
951
    }
952
953
    /**
954
     * @param string $keyedOn
955
     * @param string $valueField
956
     * @param int $cacheTtl
957
     * @return mixed
958
     */
959
    public function fetchList(string $keyedOn='', string $valueField='', int $cacheTtl=self::CACHE_NO) : array
960
    {
961
        $keyedOn            = $keyedOn ?: $this->getPrimaryKeyName();
962
        $valueField         = $valueField ?: $this->getDisplayColumn();
963
        $keyedOnAlias       = strtolower(str_replace('.', '_', $keyedOn));
964
        $valueFieldAlias    = strtolower(str_replace('.', '_', $valueField));
965
        if ( preg_match('/ as /i', $keyedOn) )
966
        {
967
            list($keyedOn, $keyedOnAlias)   = preg_split('/ as /i', $keyedOn);
968
            $keyedOn                        = trim($keyedOn);
969
            $keyedOnAlias                   = trim($keyedOnAlias);
970
        }
971
        if ( preg_match('/ as /i', $valueField) )
972
        {
973
            list($valueField, $valueFieldAlias) = preg_split('/ as /i', $valueField);
974
            $valueField                         = trim($valueField);
975
            $valueFieldAlias                    = trim($valueFieldAlias);
976
        }
977
978
        $this->_cacheTtl($cacheTtl);
979
        $fnCallback         = function() use ($keyedOn, $keyedOnAlias, $valueField, $valueFieldAlias) {
980
981
            $rows = [];
982
            $stmt = $this->select(null)
983
                ->select($keyedOn, $keyedOnAlias)
984
                ->select($valueField, $valueFieldAlias)
985
                ->fetchStmt();
986
            while ( $record = $this->fetchRow($stmt) )
0 ignored issues
show
Bug introduced by
It seems like $stmt defined by $this->select(null)->sel...ieldAlias)->fetchStmt() on line 982 can be null; however, Terah\FluentPdoModel\FluentPdoModel::fetchRow() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
987
            {
988
                $rows[$record->{$keyedOnAlias}] = $record->{$valueFieldAlias};
989
            }
990
991
            return $rows;
992
        };
993
        if ( $this->_cache_ttl === self::CACHE_NO )
994
        {
995
            $result = $fnCallback();
996
            unset($cacheKey, $fnCallback);
997
998
            return $result;
999
        }
1000
        $table              = $this->getTableName();
1001
        $cacheKey           = md5(json_encode([
1002
            'sql'               => $this->fetchSqlQuery(),
1003
            'keyed_on'          => $keyedOn,
1004
            'keyed_on_alias'    => $keyedOnAlias,
1005
            'value_field'       => $valueField,
1006
            'value_fieldAlias'  => $valueFieldAlias,
1007
        ]));
1008
1009
        return $this->_cacheData("/{$table}/list/{$cacheKey}", $fnCallback, $this->_cache_ttl);
1010
    }
1011
1012
    /**
1013
     * @param string $column
1014
     * @param int $cacheTtl
1015
     * @param bool|true $unique
1016
     * @return array
1017
     */
1018
    public function fetchColumn(string $column, int $cacheTtl=self::CACHE_NO, bool $unique=true) : array
1019
    {
1020
        $list = $this->select($column)->fetch('', $cacheTtl);
1021
        foreach ( $list as $idx => $obj )
1022
        {
1023
            $list[$idx] = $obj->{$column};
1024
        }
1025
        return $unique ? array_unique($list) : $list;
1026
    }
1027
1028
    /**
1029
     * @param null|string $field
1030
     * @param null|int $itemId
1031
     * @param int $cacheTtl
1032
     * @return mixed|null
1033
     */
1034
    public function fetchField(string $field='', int $itemId=0, int $cacheTtl=self::CACHE_NO)
1035
    {
1036
        $field      = $field ?: $this->getPrimaryKeyName();
1037
        $object     = $this->select(null)->select($field)->fetchOne($itemId, $cacheTtl);
1038
        if ( ! $object )
1039
        {
1040
            return null;
1041
        }
1042
        // Handle aliases
1043
        if ( preg_match('/ as /i', $field) )
1044
        {
1045
            list($expression, $alias) = preg_split('/ as /i', $field);
1046
            unset($expression);
1047
            $field = trim($alias);
1048
        }
1049
        if ( strpos($field, '.') !== false )
1050
        {
1051
            list($tableAlias, $field) = explode('.', $field);
1052
            unset($tableAlias);
1053
        }
1054
1055
        return property_exists($object, $field) ? $object->{$field} : null;
1056
    }
1057
1058
    /**
1059
     * @param string $field
1060
     * @param int $itemId
1061
     * @param int $cacheTtl
1062
     * @return string
1063
     */
1064
    public function fetchStr(string $field='', $itemId=0, int $cacheTtl=self::CACHE_NO) : string
1065
    {
1066
        return (string)$this->fetchField($field, $itemId, $cacheTtl);
1067
    }
1068
1069
    /**
1070
     * @param int $cacheTtl
1071
     * @return int
1072
     */
1073
    public function fetchId(int $cacheTtl=self::CACHE_NO) : int
1074
    {
1075
        return $this->fetchInt($this->getPrimaryKeyName(), 0, $cacheTtl);
1076
    }
1077
1078
    /**
1079
     * @param string $field
1080
     * @param int $itemId
1081
     * @param int $cacheTtl
1082
     * @return int
1083
     */
1084
    public function fetchInt(string $field='', $itemId=0, int $cacheTtl=self::CACHE_NO) : int
1085
    {
1086
        return (int)$this->fetchField($field, $itemId, $cacheTtl);
1087
    }
1088
1089
    /**
1090
     * @param string $field
1091
     * @param int $itemId
1092
     * @param int $cacheTtl
1093
     * @return float
1094
     */
1095
    public function fetchFloat(string $field='', $itemId=0, int $cacheTtl=self::CACHE_NO) : float
1096
    {
1097
        return (float)$this->fetchField($field, $itemId, $cacheTtl);
1098
    }
1099
1100
    /**
1101
     * @param string $field
1102
     * @param int $itemId
1103
     * @param int $cacheTtl
1104
     * @return bool
1105
     */
1106
    public function fetchBool(string $field='', $itemId=0, int $cacheTtl=self::CACHE_NO) : bool
1107
    {
1108
        return (bool)$this->fetchField($field, $itemId, $cacheTtl);
1109
    }
1110
1111
    /**
1112
     * @param int|null $id
1113
     * @param int $cacheTtl
1114
     * @return stdClass|bool
1115
     */
1116
    public function fetchOne(int $id=0, int $cacheTtl=self::CACHE_NO)
1117
    {
1118
        if ( $id > 0 )
1119
        {
1120
            $this->wherePk($id, true);
1121
        }
1122
        $this->limit(1);
1123
        $fetchAll   = $this->fetch('', $cacheTtl);
1124
1125
        return $fetchAll ? array_shift($fetchAll) : false;
1126
    }
1127
1128
    /**
1129
     * @param int|null $id
1130
     * @param int $cacheTtl
1131
     * @return boolean
1132
     */
1133
    public function fetchExists(int $id=0, int $cacheTtl=self::CACHE_NO) : bool
1134
    {
1135
        if ( $id > 0 )
1136
        {
1137
            $this->wherePk($id, true);
1138
        }
1139
1140
        return $this->count('*', $cacheTtl) !== 0;
1141
    }
1142
1143
    /*------------------------------------------------------------------------------
1144
                                    Fluent Query Builder
1145
    *-----------------------------------------------------------------------------*/
1146
1147
    /**
1148
     * Create the select clause
1149
     *
1150
     * @param  mixed    $columns  - the column to select. Can be string or array of fields
1151
     * @param  string   $alias - an alias to the column
1152
     * @param boolean $explicitSelect
1153
     * @return FluentPdoModel
1154
     */
1155
    public function select($columns='*', string $alias='', bool $explicitSelect=true) : FluentPdoModel
1156
    {
1157
        if ( $explicitSelect )
1158
        {
1159
            $this->explicitSelectMode();
1160
        }
1161
        if ( $alias && ! is_array($columns) & $columns !== $alias )
1162
        {
1163
            $columns .= " AS {$alias} ";
1164
        }
1165
        if ( $columns === '*' && !empty($this->_schema) )
1166
        {
1167
            $columns = $this->getColumns();
1168
        }
1169
        // Reset the select list
1170
        if ( is_null($columns) )
1171
        {
1172
            $this->_select_fields = [];
1173
1174
            return $this;
1175
        }
1176
        $columns = is_array($columns) ? $columns : [$columns];
1177
1178
//        if ( empty($this->_select_fields) && $addAllIfEmpty )
1179
//        {
1180
//            $this->select('*');
1181
//        }
1182
        if ( $this->_table_alias )
1183
        {
1184
            $schema = $this->columns();
1185
            foreach ( $columns as $idx => $col )
1186
            {
1187
                if ( in_array($col, $schema) )
1188
                {
1189
                    $columns[$idx] = "{$this->_table_alias}.{$col}";
1190
                }
1191
            }
1192
        }
1193
        $this->_select_fields = array_merge($this->_select_fields, $columns);
1194
1195
        return $this;
1196
    }
1197
1198
    /**
1199
     * @param string $select
1200
     * @return FluentPdoModel
1201
     */
1202
    public function selectRaw(string $select) : FluentPdoModel
1203
    {
1204
        $this->_select_fields[] = $select;
1205
1206
        return $this;
1207
    }
1208
1209
    /**
1210
     * @param bool $logQueries
1211
     *
1212
     * @return FluentPdoModel
1213
     */
1214
    public function logQueries(bool $logQueries=true) : FluentPdoModel
1215
    {
1216
        $this->_log_queries = $logQueries;
1217
1218
        return $this;
1219
    }
1220
1221
    /**
1222
     * @param bool $includeCnt
1223
     *
1224
     * @return FluentPdoModel
1225
     */
1226
    public function includeCount(bool $includeCnt=true) : FluentPdoModel
1227
    {
1228
        $this->_include_count = $includeCnt;
1229
1230
        return $this;
1231
    }
1232
1233
    /**
1234
     * @param bool $distinct
1235
     *
1236
     * @return FluentPdoModel
1237
     */
1238
    public function distinct(bool $distinct=true) : FluentPdoModel
1239
    {
1240
        $this->_distinct = $distinct;
1241
1242
        return $this;
1243
    }
1244
1245
    /**
1246
     * @param array $fields
1247
     * @return FluentPdoModel
1248
     */
1249
    public function withBelongsTo(array $fields=[]) : FluentPdoModel
1250
    {
1251
        if ( ! empty($this->_associations['belongsTo']) )
1252
        {
1253
            foreach ( $this->_associations['belongsTo'] as $alias => $config )
1254
            {
1255
                $addFieldsForJoins = empty($fields) || in_array($config[3], $fields);
1256
                $this->autoJoin($alias, self::LEFT_JOIN, $addFieldsForJoins);
1257
            }
1258
        }
1259
1260
        return $this;
1261
    }
1262
1263
    /**
1264
     * @param string $alias
1265
     * @param string $type
1266
     * @param bool   $addSelectField
1267
     * @return FluentPdoModel
1268
     */
1269
    public function autoJoin(string $alias, string $type=self::LEFT_JOIN, bool $addSelectField=true) : FluentPdoModel
1270
    {
1271
        Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid join... the alias does not exists");
1272
        list($table, $join_col, $field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
1273
        $condition = "{$alias}.id = {$this->_table_alias}.{$join_col}";
1274
        if ( in_array($alias, $this->_join_aliases) )
1275
        {
1276
            return $this;
1277
        }
1278
        $this->join($table, $condition, $alias, $type);
1279
        if ( $addSelectField )
1280
        {
1281
            $this->select($field, $fieldAlias, false);
1282
        }
1283
1284
        return $this;
1285
    }
1286
1287
    /**
1288
     * Add where condition, more calls appends with AND
1289
     *
1290
     * @param string $condition possibly containing ? or :name
1291
     * @param mixed $parameters accepted by PDOStatement::execute or a scalar value
1292
     * @param mixed ...
1293
     * @return FluentPdoModel
1294
     */
1295
    public function where($condition, $parameters=[]) : FluentPdoModel
1296
    {
1297
        // By default the and_or_operator and wrap operator is AND,
1298
        if ( $this->_wrap_open || ! $this->_and_or_operator )
1299
        {
1300
            $this->_and();
1301
        }
1302
1303
        // where(array("column1" => 1, "column2 > ?" => 2))
1304
        if ( is_array($condition) )
1305
        {
1306
            foreach ($condition as $key => $val)
1307
            {
1308
                $this->where($key, $val);
1309
            }
1310
1311
            return $this;
1312
        }
1313
1314
        $args = func_num_args();
1315
        if ( $args != 2 || strpbrk((string)$condition, '?:') )
1316
        { // where('column < ? OR column > ?', array(1, 2))
1317
            if ( $args != 2 || !is_array($parameters) )
1318
            { // where('column < ? OR column > ?', 1, 2)
1319
                $parameters = func_get_args();
1320
                array_shift($parameters);
1321
            }
1322
        }
1323
        else if ( ! is_array($parameters) )
1324
        {//where(column,value) => column=value
1325
            $condition .= ' = ?';
1326
            $parameters = [$parameters];
1327
        }
1328
        else if ( is_array($parameters) )
1329
        { // where('column', array(1, 2)) => column IN (?,?)
1330
            $placeholders = $this->_makePlaceholders(count($parameters));
1331
            $condition = "({$condition} IN ({$placeholders}))";
1332
        }
1333
1334
        $this->_where_conditions[] = [
1335
            'STATEMENT'   => $condition,
1336
            'PARAMS'      => $parameters,
1337
            'OPERATOR'    => $this->_and_or_operator
1338
        ];
1339
        // Reset the where operator to AND. To use OR, you must call _or()
1340
        $this->_and();
1341
1342
        return $this;
1343
    }
1344
1345
    /**
1346
     * Create an AND operator in the where clause
1347
     *
1348
     * @return FluentPdoModel
1349
     */
1350
    public function _and() : FluentPdoModel
1351
    {
1352
        if ( $this->_wrap_open )
1353
        {
1354
            $this->_where_conditions[] = self::OPERATOR_AND;
1355
            $this->_last_wrap_position = count($this->_where_conditions);
1356
            $this->_wrap_open = false;
1357
1358
            return $this;
1359
        }
1360
        $this->_and_or_operator = self::OPERATOR_AND;
1361
1362
        return $this;
1363
    }
1364
1365
1366
    /**
1367
     * Create an OR operator in the where clause
1368
     *
1369
     * @return FluentPdoModel
1370
     */
1371
    public function _or() : FluentPdoModel
1372
    {
1373
        if ( $this->_wrap_open )
1374
        {
1375
            $this->_where_conditions[]  = self::OPERATOR_OR;
1376
            $this->_last_wrap_position  = count($this->_where_conditions);
1377
            $this->_wrap_open           = false;
1378
1379
            return $this;
1380
        }
1381
        $this->_and_or_operator     = self::OPERATOR_OR;
1382
1383
        return $this;
1384
    }
1385
1386
    /**
1387
     * To group multiple where clauses together.
1388
     *
1389
     * @return FluentPdoModel
1390
     */
1391
    public function wrap() : FluentPdoModel
1392
    {
1393
        $this->_wrap_open           = true;
1394
        $spliced                    = array_splice($this->_where_conditions, $this->_last_wrap_position, count($this->_where_conditions), '(');
1395
        $this->_where_conditions    = array_merge($this->_where_conditions, $spliced);
1396
        array_push($this->_where_conditions,')');
1397
        $this->_last_wrap_position = count($this->_where_conditions);
1398
1399
        return $this;
1400
    }
1401
1402
    /**
1403
     * Where Primary key
1404
     *
1405
     * @param int  $id
1406
     * @param bool $addAlias
1407
     *
1408
     * @return FluentPdoModel
1409
     */
1410
    public function wherePk(int $id, bool $addAlias=true) : FluentPdoModel
1411
    {
1412
        $alias = $addAlias && ! empty($this->_table_alias) ? "{$this->_table_alias}." : '';
1413
1414
        return $this->where($alias . $this->getPrimaryKeyName(), $id);
1415
    }
1416
1417
    /**
1418
     * WHERE $columnName != $value
1419
     *
1420
     * @param  string   $columnName
1421
     * @param  mixed    $value
1422
     * @return FluentPdoModel
1423
     */
1424
    public function whereNot(string $columnName, $value) : FluentPdoModel
1425
    {
1426
        return $this->where("$columnName != ?", $value);
1427
    }
1428
    /**
1429
     * WHERE $columnName != $value
1430
     *
1431
     * @param  string   $columnName
1432
     * @param  mixed    $value
1433
     * @return FluentPdoModel
1434
     */
1435
    public function whereCoercedNot(string $columnName, $value) : FluentPdoModel
1436
    {
1437
        return $this->where("IFNULL({$columnName}, '') != ?", $value);
1438
    }
1439
1440
    /**
1441
     * WHERE $columnName LIKE $value
1442
     *
1443
     * @param  string   $columnName
1444
     * @param  mixed    $value
1445
     * @return FluentPdoModel
1446
     */
1447
    public function whereLike(string $columnName, $value) : FluentPdoModel
1448
    {
1449
        return $this->where("$columnName LIKE ?", $value);
1450
    }
1451
1452
    /**
1453
     * @param string $columnName
1454
     * @param mixed $value1
1455
     * @param mixed $value2
1456
     * @return FluentPdoModel
1457
     */
1458
    public function whereBetween(string $columnName, $value1, $value2) : FluentPdoModel
1459
    {
1460
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1461
    }
1462
1463
    /**
1464
     * @param string $columnName
1465
     * @param mixed $value1
1466
     * @param mixed $value2
1467
     * @return FluentPdoModel
1468
     */
1469
    public function whereNotBetween(string $columnName, $value1, $value2) : FluentPdoModel
1470
    {
1471
        return $this->where("$columnName BETWEEN ? AND ?", [$value1, $value2]);
1472
    }
1473
1474
    /**
1475
     * @param string $columnName
1476
     * @param string $regex
1477
     * @return FluentPdoModel
1478
     */
1479
    public function whereRegex(string $columnName, string $regex) : FluentPdoModel
1480
    {
1481
        return $this->where("$columnName REGEXP ?", $regex);
1482
    }
1483
1484
    /**
1485
     * @param string $columnName
1486
     * @param string $regex
1487
     * @return FluentPdoModel
1488
     */
1489
    public function whereNotRegex(string $columnName, string $regex) : FluentPdoModel
1490
    {
1491
        return $this->where("$columnName NOT REGEXP ?", $regex);
1492
    }
1493
1494
    /**
1495
     * WHERE $columnName NOT LIKE $value
1496
     *
1497
     * @param  string   $columnName
1498
     * @param  string   $value
1499
     * @return FluentPdoModel
1500
     */
1501
    public function whereNotLike(string $columnName, string $value) : FluentPdoModel
1502
    {
1503
        return $this->where("$columnName NOT LIKE ?", $value);
1504
    }
1505
1506
    /**
1507
     * WHERE $columnName > $value
1508
     *
1509
     * @param  string   $columnName
1510
     * @param  mixed    $value
1511
     * @return FluentPdoModel
1512
     */
1513
    public function whereGt(string $columnName, $value) : FluentPdoModel
1514
    {
1515
        return $this->where("$columnName > ?", $value);
1516
    }
1517
1518
    /**
1519
     * WHERE $columnName >= $value
1520
     *
1521
     * @param  string   $columnName
1522
     * @param  mixed    $value
1523
     * @return FluentPdoModel
1524
     */
1525
    public function whereGte(string $columnName, $value) : FluentPdoModel
1526
    {
1527
        return $this->where("$columnName >= ?", $value);
1528
    }
1529
1530
    /**
1531
     * WHERE $columnName < $value
1532
     *
1533
     * @param  string   $columnName
1534
     * @param  mixed    $value
1535
     * @return FluentPdoModel
1536
     */
1537
    public function whereLt(string $columnName, $value) : FluentPdoModel
1538
    {
1539
        return $this->where("$columnName < ?", $value);
1540
    }
1541
1542
    /**
1543
     * WHERE $columnName <= $value
1544
     *
1545
     * @param  string   $columnName
1546
     * @param  mixed    $value
1547
     * @return FluentPdoModel
1548
     */
1549
    public function whereLte(string $columnName, $value) : FluentPdoModel
1550
    {
1551
        return $this->where("$columnName <= ?", $value);
1552
    }
1553
1554
    /**
1555
     * WHERE $columnName IN (?,?,?,...)
1556
     *
1557
     * @param  string   $columnName
1558
     * @param  array    $values
1559
     * @return FluentPdoModel
1560
     */
1561
    public function whereIn(string $columnName, array $values) : FluentPdoModel
1562
    {
1563
        return $this->where($columnName, array_values($values));
1564
    }
1565
1566
    /**
1567
     * WHERE $columnName NOT IN (?,?,?,...)
1568
     *
1569
     * @param  string   $columnName
1570
     * @param  array    $values
1571
     * @return FluentPdoModel
1572
     */
1573
    public function whereNotIn(string $columnName, array $values) : FluentPdoModel
1574
    {
1575
        $placeholders = $this->_makePlaceholders(count($values));
1576
1577
        return $this->where("({$columnName} NOT IN ({$placeholders}))", $values);
1578
    }
1579
1580
    /**
1581
     * WHERE $columnName IS NULL
1582
     *
1583
     * @param  string   $columnName
1584
     * @return FluentPdoModel
1585
     */
1586
    public function whereNull(string $columnName) : FluentPdoModel
1587
    {
1588
        return $this->where("({$columnName} IS NULL)");
1589
    }
1590
1591
    /**
1592
     * WHERE $columnName IS NOT NULL
1593
     *
1594
     * @param  string   $columnName
1595
     * @return FluentPdoModel
1596
     */
1597
    public function whereNotNull(string $columnName) : FluentPdoModel
1598
    {
1599
        return $this->where("({$columnName} IS NOT NULL)");
1600
    }
1601
1602
    /**
1603
     * @param string $statement
1604
     * @param string $operator
1605
     * @return FluentPdoModel
1606
     */
1607
    public function having(string $statement, string $operator=self::OPERATOR_AND) : FluentPdoModel
1608
    {
1609
        $this->_having[] = [
1610
            'STATEMENT'   => $statement,
1611
            'OPERATOR'    => $operator
1612
        ];
1613
1614
        return $this;
1615
    }
1616
1617
    /**
1618
     * ORDER BY $columnName (ASC | DESC)
1619
     *
1620
     * @param  string   $columnName - The name of the column or an expression
1621
     * @param  string   $ordering   (DESC | ASC)
1622
     * @return FluentPdoModel
1623
     */
1624
    public function orderBy(string $columnName='', string $ordering='DESC') : FluentPdoModel
1625
    {
1626
        $ordering       = strtoupper($ordering);
1627
        Assert($ordering)->inArray(['DESC', 'ASC']);
1628
        if ( ! $columnName )
1629
        {
1630
            $this->_order_by = [];
1631
1632
            return $this;
1633
        }
1634
        $this->_order_by[] = trim("{$columnName} {$ordering}");
1635
1636
        return $this;
1637
    }
1638
1639
    /**
1640
     * GROUP BY $columnName
1641
     *
1642
     * @param  string   $columnName
1643
     * @return FluentPdoModel
1644
     */
1645
    public function groupBy(string $columnName) : FluentPdoModel
1646
    {
1647
        $columnName = is_array($columnName) ? $columnName : [$columnName];
1648
        foreach ( $columnName as $col )
1649
        {
1650
            $this->_group_by[] = $col;
1651
        }
1652
1653
        return $this;
1654
    }
1655
1656
1657
    /**
1658
     * LIMIT $limit
1659
     *
1660
     * @param  int      $limit
1661
     * @param  int|null $offset
1662
     * @return FluentPdoModel
1663
     */
1664
    public function limit(int $limit, int $offset=0) : FluentPdoModel
1665
    {
1666
        $this->_limit =  $limit;
1667
        if ( $offset )
1668
        {
1669
            $this->offset($offset);
1670
        }
1671
        return $this;
1672
    }
1673
1674
    /**
1675
     * Return the limit
1676
     *
1677
     * @return integer
1678
     */
1679
    public function getLimit() : int
1680
    {
1681
        return $this->_limit;
1682
    }
1683
1684
    /**
1685
     * OFFSET $offset
1686
     *
1687
     * @param  int      $offset
1688
     * @return FluentPdoModel
1689
     */
1690
    public function offset(int $offset) : FluentPdoModel
1691
    {
1692
        $this->_offset = (int)$offset;
1693
1694
        return $this;
1695
    }
1696
1697
    /**
1698
     * Return the offset
1699
     *
1700
     * @return integer
1701
     */
1702
    public function getOffset() : int
1703
    {
1704
        return $this->_offset;
1705
    }
1706
1707
    /**
1708
     * Build a join
1709
     *
1710
     * @param  string    $table         - The table name
1711
     * @param  string   $constraint    -> id = profile.user_id
1712
     * @param  string   $tableAlias   - The alias of the table name
1713
     * @param  string   $joinOperator - LEFT | INNER | etc...
1714
     * @return FluentPdoModel
1715
     */
1716
    public function join(string $table, string $constraint='', string $tableAlias='', string $joinOperator='') : FluentPdoModel
1717
    {
1718
        if ( ! $constraint )
1719
        {
1720
            return $this->autoJoin($table, $joinOperator);
1721
        }
1722
        $join                   = [$joinOperator ? "{$joinOperator} " : ''];
1723
        $join[]                 = "JOIN {$table} ";
1724
        $tableAlias             = $tableAlias ?: Inflector::classify($table);
1725
        $join[]                 = $tableAlias ? "AS {$tableAlias} " : '';
1726
        $join[]                 = "ON {$constraint}";
1727
        $this->_join_sources[]  = implode('', $join);
1728
        if ( $tableAlias )
1729
        {
1730
            $this->_join_aliases[]  = $tableAlias;
1731
        }
1732
1733
        return $this;
1734
    }
1735
1736
    /**
1737
     * Create a left join
1738
     *
1739
     * @param  string   $table
1740
     * @param  string   $constraint
1741
     * @param  string   $tableAlias
1742
     * @return FluentPdoModel
1743
     */
1744
    public function leftJoin(string $table, string $constraint, string $tableAlias='') : FluentPdoModel
1745
    {
1746
        return $this->join($table, $constraint, $tableAlias, self::LEFT_JOIN);
1747
    }
1748
1749
1750
    /**
1751
     * Return the build select query
1752
     *
1753
     * @return string
1754
     */
1755
    public function getSelectQuery() : string
1756
    {
1757
        if ( $this->_raw_sql )
1758
        {
1759
            return $this->_raw_sql;
1760
        }
1761
        if ( empty($this->_select_fields) || ! $this->_explicit_select_mode )
1762
        {
1763
            $this->select('*', '', false);
1764
        }
1765
        foreach ( $this->_select_fields as $idx => $cols )
1766
        {
1767
            if ( strpos(trim(strtolower($cols)), 'distinct ') === 0 )
1768
            {
1769
                $this->_distinct = true;
1770
                $this->_select_fields[$idx] = str_ireplace('distinct ', '', $cols);
1771
            }
1772
        }
1773
        if ( $this->_include_count )
1774
        {
1775
            $this->select('COUNT(*) as __cnt');
1776
        }
1777
        $query  = 'SELECT ';
1778
        $query .= $this->_distinct ? 'DISTINCT ' : '';
1779
        $query .= implode(', ', $this->_prepareColumns($this->_select_fields));
1780
        $query .= " FROM {$this->_table_name}" . ( $this->_table_alias ? " {$this->_table_alias}" : '' );
1781
        if ( count($this->_join_sources ) )
1782
        {
1783
            $query .= (' ').implode(' ',$this->_join_sources);
1784
        }
1785
        $query .= $this->_getWhereString(); // WHERE
1786
        if ( count($this->_group_by) )
1787
        {
1788
            $query .= ' GROUP BY ' . implode(', ', array_unique($this->_group_by));
1789
        }
1790
        if ( count($this->_order_by ) )
1791
        {
1792
            $query .= ' ORDER BY ' . implode(', ', array_unique($this->_order_by));
1793
        }
1794
        $query .= $this->_getHavingString(); // HAVING
1795
1796
        return $this->_connection->setLimit($query, $this->_limit, $this->_offset);
1797
    }
1798
1799
    /**
1800
     * Prepare columns to include the table alias name
1801
     * @param array $columns
1802
     * @return array
1803
     */
1804
    protected function _prepareColumns(array $columns) : array
1805
    {
1806
        if ( ! $this->_table_alias )
1807
        {
1808
            return $columns;
1809
        }
1810
        $newColumns = [];
1811
        foreach ($columns as $column)
1812
        {
1813
            if ( strpos($column, ',') && ! preg_match('/^[a-zA-Z_]{2,200}\(.{1,500}\)/', trim($column)) )
1814
            {
1815
                $newColumns = array_merge($this->_prepareColumns(explode(',', $column)), $newColumns);
1816
            }
1817
            elseif ( preg_match('/^(AVG|SUM|MAX|MIN|COUNT|CONCAT)/', $column) )
1818
            {
1819
                $newColumns[] = trim($column);
1820
            }
1821
            elseif (strpos($column, '.') === false && strpos(strtoupper($column), 'NULL') === false)
1822
            {
1823
                $column         = trim($column);
1824
                $newColumns[]   = preg_match('/^[0-9]/', $column) ? trim($column) : "{$this->_table_alias}.{$column}";
1825
            }
1826
            else
1827
            {
1828
                $newColumns[] = trim($column);
1829
            }
1830
        }
1831
1832
        return $newColumns;
1833
    }
1834
1835
    /**
1836
     * Build the WHERE clause(s)
1837
     *
1838
     * @param bool $purgeAliases
1839
     * @return string
1840
     */
1841
    protected function _getWhereString(bool $purgeAliases=false) : string
1842
    {
1843
        // If there are no WHERE clauses, return empty string
1844
        if ( empty($this->_where_conditions) )
1845
        {
1846
            return '';
1847
        }
1848
        $where_condition = '';
1849
        $last_condition = '';
1850
        foreach ( $this->_where_conditions as $condition )
1851
        {
1852
            if ( is_array($condition) )
1853
            {
1854
                if ( $where_condition && $last_condition != '(' && !preg_match('/\)\s+(OR|AND)\s+$/i', $where_condition))
1855
                {
1856
                    $where_condition .= $condition['OPERATOR'];
1857
                }
1858
                if ( $purgeAliases && ! empty($condition['STATEMENT']) && strpos($condition['STATEMENT'], '.') !== false && ! empty($this->_table_alias) )
1859
                {
1860
                    $condition['STATEMENT'] = preg_replace("/{$this->_table_alias}\./", '', $condition['STATEMENT']);
1861
                }
1862
                $where_condition .= $condition['STATEMENT'];
1863
                $this->_where_parameters = array_merge($this->_where_parameters, $condition['PARAMS']);
1864
            }
1865
            else
1866
            {
1867
                $where_condition .= $condition;
1868
            }
1869
            $last_condition = $condition;
1870
        }
1871
1872
        return " WHERE {$where_condition}" ;
1873
    }
1874
1875
    /**
1876
     * Return the HAVING clause
1877
     *
1878
     * @return string
1879
     */
1880
    protected function _getHavingString() : string
1881
    {
1882
        // If there are no WHERE clauses, return empty string
1883
        if ( empty($this->_having) )
1884
        {
1885
            return '';
1886
        }
1887
        $having_condition = '';
1888
        foreach ( $this->_having as $condition )
1889
        {
1890
            if ( $having_condition && ! preg_match('/\)\s+(OR|AND)\s+$/i', $having_condition) )
1891
            {
1892
                $having_condition .= $condition['OPERATOR'];
1893
            }
1894
            $having_condition .= $condition['STATEMENT'];
1895
        }
1896
1897
        return " HAVING {$having_condition}" ;
1898
    }
1899
1900
    /**
1901
     * Return the values to be bound for where
1902
     *
1903
     * @param bool $purgeAliases
1904
     * @return array
1905
     */
1906
    protected function _getWhereParameters(bool $purgeAliases=false) : array
1907
    {
1908
        unset($purgeAliases);
1909
1910
        return $this->_where_parameters;
1911
    }
1912
1913
    /**
1914
     * @param array $record
1915
     * @return stdClass
1916
     */
1917
    public function insertArr(array $record) : stdClass
1918
    {
1919
        return $this->insert((object)$record);
1920
    }
1921
1922
    /**
1923
     * Insert new rows
1924
     * $records can be a stdClass or an array of stdClass to add a bulk insert
1925
     * If a single row is inserted, it will return it's row instance
1926
     *
1927
     * @param stdClass $record
1928
     * @return stdClass
1929
     * @throws Exception
1930
     */
1931
    public function insert(stdClass $record) : stdClass
1932
    {
1933
        Assert((array)$record)->notEmpty("The data passed to insert does not contain any data");
1934
        Assert($record)->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1935
1936
        $record = $this->beforeSave($record, self::SAVE_INSERT);
1937
        if ( ! empty($this->_errors) )
1938
        {
1939
            return $record;
1940
        }
1941
        list($sql, $insert_values) = $this->insertSqlQuery([$record]);
1942
        $this->execute((string)$sql, (array)$insert_values);
1943
        $rowCount = $this->rowCount();
1944
        if ( $rowCount === 1 )
1945
        {
1946
            $primaryKeyName                 = $this->getPrimaryKeyName();
1947
            $record->{$primaryKeyName}      = $this->getLastInsertId($primaryKeyName);
1948
        }
1949
        $record = $this->afterSave($record, self::SAVE_INSERT);
1950
        $this->destroy();
1951
1952
        return $record;
1953
    }
1954
1955
    /**
1956
     * @param string $name
1957
     * @return int
1958
     */
1959
    public function getLastInsertId(string $name='') : int
1960
    {
1961
        return (int)$this->getPdo()->lastInsertId($name ?: null);
1962
    }
1963
1964
    /**
1965
     * @param stdClass[] $records
1966
     * @return stdClass[]
1967
     */
1968
    public function insertSqlQuery(array $records) : array
1969
    {
1970
        Assert($records)->notEmpty("The data passed to insert does not contain any data");
1971
        Assert($records)->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
1972
1973
        $insert_values      = [];
1974
        $question_marks     = [];
1975
        $properties         = [];
1976
        foreach ( $records as $record )
1977
        {
1978
            $properties         = !empty($properties) ? $properties : array_keys(get_object_vars($record));
1979
            $question_marks[]   = '('  . $this->_makePlaceholders(count($properties)) . ')';
1980
            $insert_values      = array_merge($insert_values, array_values((array)$record));
1981
        }
1982
        $properties         = implode(', ', $properties);
1983
        $question_marks     = implode(', ', $question_marks);
1984
        $sql                = "INSERT INTO {$this->_table_name} ({$properties}) VALUES {$question_marks}";
1985
1986
        return [$sql, $insert_values];
1987
    }
1988
1989
    /**
1990
     * @param       $data
1991
     * @param array $matchOn
1992
     * @param bool  $returnObj
1993
     * @return bool|int|stdClass
1994
     */
1995
    public function upsert($data, array $matchOn=[], $returnObj=false)
1996
    {
1997
        if ( ! is_array($data) )
1998
        {
1999
            return $this->upsertOne($data, $matchOn, $returnObj);
2000
        }
2001
        Assert($data)
2002
            ->notEmpty("The data passed to insert does not contain any data")
2003
            ->all()->isInstanceOf('stdClass', "The data to be inserted must be an object or an array of objects");
2004
        $num_success    = 0;
2005
        foreach ( $data as $row )
2006
        {
2007
            $clone = clone $this;
2008
            if ( $clone->upsertOne($row, $matchOn) )
2009
            {
2010
                $num_success++;
2011
            }
2012
            unset($clone->_handlers, $clone); // hhvm mem leak
2013
        }
2014
2015
        return $num_success;
2016
    }
2017
2018
    /**
2019
     * @param stdClass $object
2020
     * @param array    $matchOn
2021
     * @param bool     $returnObj
2022
     * @return bool|int|stdClass
2023
     */
2024
    public function upsertOne(stdClass $object, array $matchOn=[], $returnObj=false)
2025
    {
2026
        $primary_key    = $this->getPrimaryKeyName();
2027
        $matchOn       = empty($matchOn) && property_exists($object, $primary_key) ? [$primary_key] : $matchOn;
2028
        foreach ( $matchOn as $column )
2029
        {
2030
            Assert( ! property_exists($object, $column) && $column !== $primary_key)->false('The match on value for upserts is missing.');
2031
            if ( property_exists($object, $column) )
2032
            {
2033
                if ( is_null($object->$column) )
2034
                {
2035
                    $this->whereNull($column);
2036
                }
2037
                else
2038
                {
2039
                    $this->where($column, $object->$column);
2040
                }
2041
            }
2042
        }
2043
        if ( count($this->_where_conditions) < 1 )
2044
        {
2045
            return $this->insert($object);
2046
        }
2047
        if ( ( $id = (int)$this->fetchField($primary_key) ) )
2048
        {
2049
            if ( property_exists($object, $primary_key) && is_null($object->$primary_key) )
2050
            {
2051
                $object->$primary_key = $id;
2052
            }
2053
            $rows_affected = $this->reset()->wherePk($id)->update($object);
2054
            if ( $rows_affected === false )
2055
            {
2056
                return false;
2057
            }
2058
2059
            return $returnObj ? $this->reset()->fetchOne($id) : $id;
2060
        }
2061
2062
        return $this->insert($object);
2063
    }
2064
2065
    /**
2066
     * @param array      $data
2067
     * @param array      $matchOn
2068
     * @param bool|false $returnObj
2069
     * @return bool|int|stdClass
2070
     */
2071
    public function upsertArr(array $data, array $matchOn=[], bool $returnObj=false)
2072
    {
2073
        return $this->upsert((object)$data, $matchOn, $returnObj);
2074
    }
2075
2076
    /**
2077
     * Update entries
2078
     * Use the query builder to create the where clause
2079
     *
2080
     * @param stdClass $record
2081
     * @param bool     $updateAll
2082
     * @return int
2083
     * @throws Exception
2084
     */
2085
    public function update(stdClass $record, $updateAll=false) : int
2086
    {
2087
        Assert($record)
2088
            ->notEmpty("The data passed to update does not contain any data")
2089
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2090
2091
        if ( empty($this->_where_conditions) && ! $updateAll )
2092
        {
2093
            throw new Exception("You cannot update an entire table without calling update with updateAll=true", 500);
2094
        }
2095
        $record = $this->beforeSave($record, self::SAVE_UPDATE);
2096
        if ( ! empty($this->_errors) )
2097
        {
2098
            return 0;
2099
        }
2100
        list($sql, $values) = $this->updateSqlQuery($record);
2101
        $this->execute($sql, $values);
2102
        $this->afterSave($record, self::SAVE_UPDATE);
2103
        $rowCount = $this->rowCount();
2104
        $this->destroy();
2105
2106
        return $rowCount;
2107
    }
2108
2109
    /**
2110
     * @param array      $record
2111
     * @param bool|false $updateAll
2112
     * @return int
2113
     * @throws Exception
2114
     */
2115
    public function updateArr(array $record, $updateAll=false) : int
2116
    {
2117
        return $this->update((object)$record, $updateAll);
2118
    }
2119
2120
2121
    /**
2122
     * @param string  $field
2123
     * @param mixed   $value
2124
     * @param int     $id
2125
     * @param bool|false $updateAll
2126
     * @return int
2127
     * @throws Exception
2128
     */
2129
    public function updateField(string $field, $value, int $id=0, bool $updateAll=false) : int
2130
    {
2131
        if ( $id && $id > 0 )
2132
        {
2133
            $this->wherePk($id);
2134
        }
2135
2136
        return $this->update((object)[$field => $value], $updateAll);
2137
    }
2138
2139
    /**
2140
     * @param stdClass $record
2141
     * @return bool|int
2142
     * @throws Exception
2143
     */
2144
    public function updateChanged(stdClass $record) : int
2145
    {
2146
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2147
        {
2148
            if ( is_null($value) )
2149
            {
2150
                $this->whereNotNull($field);
2151
                continue;
2152
            }
2153
            $this->whereCoercedNot($field, $value);
2154
        }
2155
2156
        return $this->update($record);
2157
    }
2158
2159
    /**
2160
     * @param string    $expression
2161
     * @param array     $params
2162
     * @return FluentPdoModel
2163
     */
2164
    public function updateByExpression(string $expression, array $params) : FluentPdoModel
2165
    {
2166
        $this->_update_raw[] = [$expression, $params];
2167
2168
        return $this;
2169
    }
2170
2171
    /**
2172
     * @param array $data
2173
     * @return int
2174
     * @throws Exception
2175
     */
2176
    public function rawUpdate(array $data=[]) : int
2177
    {
2178
        list($sql, $values) = $this->updateSql($data);
2179
        $this->execute($sql, $values);
2180
        $rowCount           = $this->rowCount();
2181
        $this->destroy();
2182
2183
        return $rowCount;
2184
    }
2185
2186
    /**
2187
     * @param stdClass $record
2188
     * @return array
2189
     */
2190
    public function updateSqlQuery(stdClass $record) : array
2191
    {
2192
        Assert($record)
2193
            ->notEmpty("The data passed to update does not contain any data")
2194
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2195
2196
        // Make sure we remove the primary key
2197
2198
        return $this->updateSql((array)$record);
2199
    }
2200
2201
    /**
2202
     * @param $record
2203
     * @return array
2204
     */
2205
    protected function updateSql(array $record) : array
2206
    {
2207
        unset($record[$this->getPrimaryKeyName()]);
2208
        // Sqlite needs a null primary key
2209
        //$record[$this->getPrimaryKeyName()] = null;
2210
        $field_list = [];
2211
        foreach ( $record as $key => $value )
2212
        {
2213
            if ( is_numeric($key) )
2214
            {
2215
                $field_list[] = $value;
2216
                unset($record[$key]);
2217
                continue;
2218
            }
2219
            $field_list[] = "{$key} = ?";
2220
        }
2221
        $rawParams  = [];
2222
        foreach ( $this->_update_raw as $rawUpdate )
2223
        {
2224
            $field_list[]   = $rawUpdate[0];
2225
            $rawParams      = array_merge($rawParams, $rawUpdate[1]);
2226
        }
2227
        $field_list     = implode(', ', $field_list);
2228
        $where_str      = $this->_getWhereString();
2229
        $joins          = ! empty($this->_join_sources) ? (' ').implode(' ',$this->_join_sources) : '';
2230
        $alias          = ! empty($this->_table_alias) ? " AS {$this->_table_alias}" : '';
2231
        $sql            = "UPDATE {$this->_table_name}{$alias}{$joins} SET {$field_list}{$where_str}";
2232
        $values         = array_merge(array_values($record), $rawParams, $this->_getWhereParameters());
2233
2234
        return [$sql, $values];
2235
    }
2236
2237
    /**
2238
     * @param bool $deleteAll
2239
     * @param bool $force
2240
     * @return int
2241
     * @throws Exception
2242
     */
2243
    public function delete(bool $deleteAll=false, bool $force=false) : int
2244
    {
2245
        if ( ! $force && $this->_soft_deletes )
2246
        {
2247
            return $this->updateArchived();
2248
        }
2249
2250
        list($sql, $params) = $this->deleteSqlQuery();
2251
        if ( empty($this->_where_conditions) && ! $deleteAll )
2252
        {
2253
            throw new Exception("You cannot update an entire table without calling update with deleteAll=true");
2254
        }
2255
        $this->execute($sql, $params);
2256
2257
        return $this->rowCount();
2258
    }
2259
2260
    /**
2261
     * @param bool|false $force
2262
     * @return FluentPdoModel
2263
     * @throws Exception
2264
     */
2265
    public function truncate(bool $force=false) : FluentPdoModel
2266
    {
2267
        if ( $force )
2268
        {
2269
            $this->execute('SET FOREIGN_KEY_CHECKS = 0');
2270
        }
2271
        $this->execute("TRUNCATE TABLE {$this->_table_name}");
2272
        if ( $force )
2273
        {
2274
            $this->execute('SET FOREIGN_KEY_CHECKS = 1');
2275
        }
2276
2277
        return $this;
2278
    }
2279
2280
    /**
2281
     * @return array
2282
     */
2283
    public function deleteSqlQuery() : array
2284
    {
2285
        $query  = "DELETE FROM {$this->_table_name}";
2286
        if ( !empty($this->_where_conditions) )
2287
        {
2288
            $query .= $this->_getWhereString(true);
2289
2290
            return [$query, $this->_getWhereParameters()];
2291
        }
2292
2293
        return [$query, []];
2294
    }
2295
2296
2297
    /**
2298
     * Return the aggregate count of column
2299
     *
2300
     * @param string $column
2301
     * @param int $cacheTtl
2302
     * @return float
2303
     */
2304
    public function count(string $column='*', int $cacheTtl=self::CACHE_NO) : float
2305
    {
2306
        return $this
2307
            ->explicitSelectMode()
2308
            ->fetchFloat("COUNT({$column}) AS cnt", 0, $cacheTtl);
2309
    }
2310
2311
2312
    /**
2313
     * Return the aggregate max count of column
2314
     *
2315
     * @param string $column
2316
     * @param int $cacheTtl
2317
     * @return int|float|string|null
2318
     */
2319
    public function max(string $column, int $cacheTtl=self::CACHE_NO)
2320
    {
2321
        return $this
2322
            ->explicitSelectMode()
2323
            ->fetchField("MAX({$column}) AS max", 0, $cacheTtl);
2324
    }
2325
2326
2327
    /**
2328
     * Return the aggregate min count of column
2329
     *
2330
     * @param string $column
2331
     * @param int $cacheTtl
2332
     * @return int|float|string|null
2333
     */
2334
    public function min(string $column, int $cacheTtl=self::CACHE_NO)
2335
    {
2336
        return $this
2337
            ->explicitSelectMode()
2338
            ->fetchField("MIN({$column}) AS min", 0, $cacheTtl);
2339
    }
2340
2341
    /**
2342
     * Return the aggregate sum count of column
2343
     *
2344
     * @param string $column
2345
     * @param int $cacheTtl
2346
     * @return int|float|string|null
2347
     */
2348
    public function sum(string $column, int $cacheTtl=self::CACHE_NO)
2349
    {
2350
        return $this
2351
            ->explicitSelectMode()
2352
            ->fetchField("SUM({$column}) AS sum", 0, $cacheTtl);
2353
    }
2354
2355
    /**
2356
     * Return the aggregate average count of column
2357
     *
2358
     * @param string $column
2359
     * @param int $cacheTtl
2360
     * @return int|float|string|null
2361
     */
2362
    public function avg(string $column, int $cacheTtl=self::CACHE_NO)
2363
    {
2364
        return $this
2365
            ->explicitSelectMode()
2366
            ->fetchField("AVG({$column}) AS avg", 0, $cacheTtl);
2367
    }
2368
2369
    /*******************************************************************************/
2370
// Utilities methods
2371
2372
    /**
2373
     * Reset fields
2374
     *
2375
     * @return FluentPdoModel
2376
     */
2377
    public function reset() : FluentPdoModel
2378
    {
2379
        $this->_where_parameters        = [];
2380
        $this->_select_fields           = [];
2381
        $this->_join_sources            = [];
2382
        $this->_join_aliases            = [];
2383
        $this->_where_conditions        = [];
2384
        $this->_limit                   = 0;
2385
        $this->_offset                  = 0;
2386
        $this->_order_by                = [];
2387
        $this->_group_by                = [];
2388
        $this->_and_or_operator         = self::OPERATOR_AND;
2389
        $this->_having                  = [];
2390
        $this->_wrap_open               = false;
2391
        $this->_last_wrap_position      = 0;
2392
        $this->_pdo_stmt                = null;
2393
        $this->_distinct                = false;
2394
        $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...
2395
        $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...
2396
        $this->_cache_ttl               = -1;
2397
        $this->_timer                   = [];
2398
        $this->_built_query             = '';
2399
        $this->_paging_meta             = [];
2400
        $this->_raw_sql                 = null;
2401
        $this->_explicit_select_mode    = false;
2402
2403
        return $this;
2404
    }
2405
2406
2407
    /**
2408
     * @return FluentPdoModel
2409
     */
2410
    public function removeUnauthorisedFields() : FluentPdoModel
2411
    {
2412
        return $this;
2413
    }
2414
2415
    /**
2416
     * @return Closure[]
2417
     */
2418
    protected function _getFieldHandlers() : array
2419
    {
2420
        $columns = $this->getColumns(true);
2421
        if ( empty($columns) )
2422
        {
2423
            return [];
2424
        }
2425
        return [
2426
            'id' => function(string $field, $value, string $type='', stdClass $record=null) {
2427
2428
                unset($record);
2429
                $value = $this->_fixType($field, $value);
2430
                if ( $type === self::SAVE_INSERT )
2431
                {
2432
                    Validate($value)->name($field)->nullOr()->id('ID must be a valid integer id, (%s) submitted.');
2433
                    return $value;
2434
                }
2435
                Validate($value)->name($field)->id('ID must be a valid integer id, (%s) submitted.');
2436
                return $value;
2437
            },
2438
            'created_by_id' => function(string $field, $value, string $type='', stdClass $record=null) {
2439
2440
                unset($type, $record);
2441
                $value = $this->_fixType($field, $value);
2442
                // Created user id is set to current user if record is an insert or deleted if not (unless override is true)
2443
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2444
                Validate($value)->name($field)->id('Created By must be a valid integer id, (%s) submitted.');
2445
                return $value;
2446
            },
2447
            'created_ts' => function(string $field, $value, string $type='', stdClass $record=null) {
2448
2449
                unset($type, $record);
2450
                $value = $this->_fixType($field, $value);
2451
                // Created ts is set to now if record is an insert or deleted if not (unless override is true)
2452
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2453
                Validate($value)->name($field)->date('Created must be a valid timestamp, (%s) submitted.');
2454
                return $value;
2455
            },
2456
            'modified_by_id' => function(string $field, $value, string $type='', stdClass $record=null) {
2457
2458
                unset($type, $record);
2459
                $value = $this->_fixType($field, $value);
2460
                // Modified user id is set to current user (unless override is true)
2461
                $value = $this->_allow_meta_override ? $value : $this->getUserId();
2462
                Validate($value)->name($field)->id('Modified By must be a valid integer id, (%s) submitted.');
2463
                return $value;
2464
            },
2465
            'modified_ts' => function(string $field, $value, string $type='', stdClass $record=null) {
2466
2467
                unset($type, $record);
2468
                $value = $this->_fixType($field, $value);
2469
                // Modified timestamps are set to now (unless override is true)
2470
                $value = static::dateTime($this->_allow_meta_override ? $value : null);
2471
                Validate($value)->name($field)->date('Modified must be a valid timestamp, (%s) submitted.');
2472
                return $value;
2473
            },
2474
            'status' => function(string $field, $value, string $type='', stdClass $record=null) {
2475
2476
                unset($type, $record);
2477
                $value = $this->_fixType($field, $value);
2478
                // Statuses are set to active if not set
2479
                $value = is_null($value) ? self::ACTIVE : $value;
2480
                Validate($value)->name($field)->nullOr()->status('Status must be a valid integer between -1 and 1, (%s) submitted.');
2481
                return $value;
2482
            },
2483
        ];
2484
    }
2485
2486
    /**
2487
     * @return bool
2488
     */
2489
    public function begin() : bool
2490
    {
2491
        $pdo        = $this->getPdo();
2492
        $oldDepth   = $pdo->getTransactionDepth();
2493
        $res        = $pdo->beginTransaction();
2494
        $newDepth   = $pdo->getTransactionDepth();
2495
        $this->getLogger()->debug("Calling db begin transaction", [
2496
            'old_depth'     => $oldDepth,
2497
            'new_depth'     => $newDepth,
2498
            'trans_started' => $newDepth === 1 ? true : false,
2499
        ]);
2500
2501
        return $res;
2502
    }
2503
2504
    /**
2505
     * @return bool
2506
     */
2507
    public function commit() : bool
2508
    {
2509
        $pdo        = $this->getPdo();
2510
        $oldDepth   = $pdo->getTransactionDepth();
2511
        $res        = $pdo->commit();
2512
        $newDepth   = $pdo->getTransactionDepth();
2513
        $this->getLogger()->debug("Calling db commit transaction", [
2514
            'old_depth'     => $oldDepth,
2515
            'new_depth'     => $newDepth,
2516
            'trans_ended'   => $newDepth === 0 ? true : false,
2517
        ]);
2518
        if ( ! $res )
2519
        {
2520
            return false;
2521
        }
2522
2523
        return $res === 0 ? true : $res;
2524
    }
2525
2526
    /**
2527
     * @return bool
2528
     */
2529
    public function rollback() : bool
2530
    {
2531
        $pdo        = $this->getPdo();
2532
        $oldDepth   = $pdo->getTransactionDepth();
2533
        $res        = $pdo->rollback();
2534
        $newDepth   = $pdo->getTransactionDepth();
2535
        $this->getLogger()->debug("Calling db rollback transaction", [
2536
            'old_depth'     => $oldDepth,
2537
            'new_depth'     => $newDepth,
2538
            'trans_ended'   => $newDepth === 0 ? true : false,
2539
        ]);
2540
2541
        return $res;
2542
    }
2543
2544
    /**
2545
     * @param stdClass $record
2546
     * @param  string  $type
2547
     * @return stdClass
2548
     */
2549
    public function applyGlobalModifiers(stdClass $record, string $type) : stdClass
2550
    {
2551
        unset($type);
2552
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2553
        {
2554
            if ( is_string($record->{$field}) )
2555
            {
2556
                $record->{$field} = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", $value);
2557
            }
2558
        }
2559
2560
        return $record;
2561
    }
2562
2563
    /**
2564
     * @param stdClass $record
2565
     * @param  string $type
2566
     * @return stdClass
2567
     */
2568
    public function removeUnneededFields(stdClass $record, string $type) : stdClass
2569
    {
2570
        // remove un-needed fields
2571
        $columns = $this->getColumns(true);
2572
        if ( empty($columns) )
2573
        {
2574
            return $record;
2575
        }
2576
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2577
        {
2578
            if ( ! in_array($name, $columns) || in_array($name, $this->_virtual_fields) )
2579
            {
2580
                unset($record->{$name});
2581
            }
2582
        }
2583
        if ( property_exists($record, 'created_ts') && $type !== 'INSERT' && ! $this->_allow_meta_override )
2584
        {
2585
            unset($record->created_ts);
2586
        }
2587
        if ( property_exists($record, 'created_by_id') && $type !== 'INSERT' && ! $this->_allow_meta_override )
2588
        {
2589
            unset($record->created_by_id);
2590
        }
2591
2592
        return $record;
2593
    }
2594
2595
2596
    /**
2597
     * @param array $ids
2598
     * @param array $values
2599
     * @param int   $batch
2600
     * @return bool
2601
     */
2602
    public function setById(array $ids, array $values, int $batch=1000) : bool
2603
    {
2604
        $ids        = array_unique($ids);
2605
        if ( count($ids) <= $batch )
2606
        {
2607
            return (bool)$this->whereIn('id', $ids)->updateArr($values);
2608
        }
2609
        while ( ! empty($ids) )
2610
        {
2611
            $thisBatch  = array_slice($ids, 0, $batch);
2612
            $ids        = array_diff($ids, $thisBatch);
2613
            $this->reset()->whereIn('id', $thisBatch)->updateArr($values);
2614
        }
2615
2616
        return true;
2617
    }
2618
2619
2620
    /**
2621
     * @param string $displayColumnValue
2622
     * @return int
2623
     */
2624
    public function resolveId(string $displayColumnValue) : int
2625
    {
2626
        $displayColumn  = $this->getDisplayColumn();
2627
        $className      = get_class($this);
2628
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
2629
2630
        return $this
2631
            ->reset()
2632
            ->where($displayColumn, $displayColumnValue)
2633
            ->fetchInt('id', null, self::ONE_HOUR);
2634
    }
2635
2636
    /**
2637
     * @param int   $resourceId
2638
     * @param array $query
2639
     * @param array $extraFields
2640
     * @param int $cacheTtl
2641
     * @return array
2642
     */
2643
    public function fetchApiResource(int $resourceId, array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO) : array
2644
    {
2645
        Assert($resourceId)->id();
2646
2647
        $query['_limit']    = 1;
2648
        $pagingMetaData        = $this->wherePk($resourceId)->_prepareApiResource($query, $extraFields);
2649
        if ( $pagingMetaData['total'] === 0 )
2650
        {
2651
            return [[], $pagingMetaData];
2652
        }
2653
2654
        return [$this->fetchOne($resourceId, $cacheTtl), $pagingMetaData];
2655
    }
2656
2657
    /**
2658
     * @param array     $query
2659
     * @param array     $extraFields
2660
     * @param int       $cacheTtl
2661
     * @param string    $permEntity
2662
     * @return array
2663
     */
2664
    public function fetchApiResources(array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO, string $permEntity='') : array
2665
    {
2666
        $pagingMetaData    = $this->_prepareApiResource($query, $extraFields);
2667
        if ( $pagingMetaData['total'] === 0 )
2668
        {
2669
            return [[], $pagingMetaData];
2670
        }
2671
        $results = $this->fetch('', $cacheTtl);
2672
        if ( ! $permEntity )
2673
        {
2674
            return [$results, $pagingMetaData];
2675
        }
2676
        foreach ( $results as $record )
2677
        {
2678
            if ( ! empty($record->id) )
2679
            {
2680
                $pagingMetaData['perms'][(int)$record->id] = $this->getMaskByResourceAndId($permEntity, $record->id);
2681
            }
2682
        }
2683
2684
        return [$results, $pagingMetaData];
2685
    }
2686
2687
2688
    /**
2689
     * @return array
2690
     */
2691
    public function getSearchableAssociations() : array
2692
    {
2693
        $belongsTo = ! empty($this->_associations['belongsTo']) ? $this->_associations['belongsTo'] : [];
2694
        unset($belongsTo['CreatedBy'], $belongsTo['ModifiedBy']);
2695
2696
        return $belongsTo;
2697
    }
2698
2699
    /**
2700
     * @param array $fields
2701
     */
2702
    public function removeUnrequestedFields(array $fields)
2703
    {
2704
        foreach ( $this->_select_fields as $idx => $field )
2705
        {
2706
            $field = trim(static::after(' AS ', $field, true));
2707
            if ( ! in_array($field, $fields) )
2708
            {
2709
                unset($this->_select_fields[$idx]);
2710
            }
2711
        }
2712
    }
2713
2714
    /**
2715
     * @param array $removeFields
2716
     */
2717
    public function removeFields(array $removeFields=[])
2718
    {
2719
        $searches = [];
2720
        foreach ( $removeFields as $removeField )
2721
        {
2722
            $removeField    = str_replace("{$this->_table_alias}.", '', $removeField);
2723
            $searches[]     = "{$this->_table_alias}.{$removeField}";
2724
            $searches[]     = $removeField;
2725
        }
2726
        foreach ( $this->_select_fields as $idx => $selected )
2727
        {
2728
            $selected = stripos($selected, ' AS ') !== false ? preg_split('/ as /i', $selected) : [$selected];
2729
            foreach ( $selected as $haystack )
2730
            {
2731
                foreach ( $searches as $search )
2732
                {
2733
                    if ( trim($haystack) === trim($search) )
2734
                    {
2735
                        unset($this->_select_fields[$idx]);
2736
                        continue;
2737
                    }
2738
                }
2739
            }
2740
        }
2741
    }
2742
2743
    /**
2744
     * @return FluentPdoModel
2745
     */
2746
    public function defaultFilters() : FluentPdoModel
2747
    {
2748
        return $this;
2749
    }
2750
2751
    /**
2752
     * @param bool $allow
2753
     *
2754
     * @return FluentPdoModel
2755
     */
2756
    public function allowMetaColumnOverride(bool $allow=false) : FluentPdoModel
2757
    {
2758
        $this->_allow_meta_override = $allow;
2759
2760
        return $this;
2761
    }
2762
2763
    /**
2764
     * @param stdClass $record
2765
     * @return stdClass
2766
     */
2767
    public function onFetch(stdClass $record) : stdClass
2768
    {
2769
        $record     = $this->_trimAndLowerCaseKeys($record);
2770
        if ( $this->_filter_on_fetch )
2771
        {
2772
            $record     = $this->cleanseRecord($record);
2773
        }
2774
2775
        $record     =  $this->fixTypes($record);
2776
2777
        return $this->fixTimestamps($record);
2778
    }
2779
2780
    /**
2781
     * @param $value
2782
     * @return string
2783
     */
2784
    public function gzEncodeData(string $value) : string
2785
    {
2786
        if ( $this->_hasGzipPrefix($value) )
2787
        {
2788
            return $value;
2789
        }
2790
2791
        return static::GZIP_PREFIX . base64_encode(gzencode($value, 9));
2792
    }
2793
2794
    /**
2795
     * @param $value
2796
     * @return mixed|string
2797
     */
2798
    public function gzDecodeData(string $value) : string
2799
    {
2800
        if ( ! $this->_hasGzipPrefix($value) )
2801
        {
2802
            return $value;
2803
        }
2804
        $value = substr_replace($value, '', 0, strlen(static::GZIP_PREFIX));
2805
2806
        return gzdecode(base64_decode($value));
2807
    }
2808
2809
    /**
2810
     * @param $value
2811
     * @return bool
2812
     */
2813
    protected function _hasGzipPrefix(string $value) : bool
2814
    {
2815
        return substr($value, 0, strlen(static::GZIP_PREFIX)) === static::GZIP_PREFIX ? true : false;
2816
    }
2817
2818
    /**
2819
     * @param stdClass $record
2820
     * @return stdClass
2821
     */
2822
    public function fixTimestamps(stdClass $record) : stdClass
2823
    {
2824
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2825
        {
2826
            if ( preg_match('/_ts$/', $field) )
2827
            {
2828
                $record->{$field} = is_null($value) ? null : static::atom($value);
2829
            }
2830
        }
2831
2832
        return $record;
2833
    }
2834
2835
    /**
2836
     * @param int $max
2837
     * @return FluentPdoModel
2838
     */
2839
    public function setMaxRecords(int $max) : FluentPdoModel
2840
    {
2841
        Assert($max)->int();
2842
        $this->_default_max = $max;
2843
2844
        return $this;
2845
    }
2846
2847
2848
    /**
2849
     * @param stdClass $record
2850
     * @param string   $type
2851
     * @return stdClass
2852
     */
2853
    public function afterSave(stdClass $record, string $type) : stdClass
2854
    {
2855
        unset($type);
2856
        $this->clearCacheByTable();
2857
        foreach ( $record as $column => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2858
        {
2859
            if ( !empty($record->{$column}) )
2860
            {
2861
                if ( preg_match('/_ts$/', $column) )
2862
                {
2863
                    $record->{$column} = static::atom($value);
2864
                }
2865
                if ( preg_match('/_am$/', $column) )
2866
                {
2867
                    $record->{$column} = number_format($value, 2, '.', '');
2868
                }
2869
            }
2870
        }
2871
2872
        return $record;
2873
    }
2874
2875
2876
    /**
2877
     * @param stdClass $record
2878
     * @param string $type
2879
     * @return stdClass
2880
     */
2881
    public function addDefaultFields(stdClass $record, string $type) : stdClass
2882
    {
2883
        $columns            = $this->getColumns(true);
2884
        if ( empty($columns) )
2885
        {
2886
            return $record;
2887
        }
2888
        $defaults           = [
2889
            self::SAVE_UPDATE   => [
2890
                'modified_by_id'    => null,
2891
                'modified_ts'       => null,
2892
            ],
2893
            self::SAVE_INSERT   => [
2894
                'created_by_id'     => null,
2895
                'created_ts'        => null,
2896
                'modified_by_id'    => null,
2897
                'modified_ts'       => null,
2898
                'status'            => null,
2899
            ]
2900
        ];
2901
        $columns            = array_flip($this->getColumns());
2902
        $defaults           = array_intersect_key($defaults[$type], $columns);
2903
        foreach ( $defaults as $column => $def )
2904
        {
2905
            $record->{$column} = $record->{$column} ?? $def;
2906
        }
2907
2908
        return $record;
2909
    }
2910
2911
2912
    /**
2913
     * @return bool
2914
     */
2915
    public function createTable() : bool
2916
    {
2917
        return true;
2918
    }
2919
2920
    /**
2921
     * @return bool
2922
     */
2923
    public function dropTable() : bool
2924
    {
2925
        return true;
2926
    }
2927
2928
    protected function _compileHandlers()
2929
    {
2930
        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...
2931
        {
2932
            return;
2933
        }
2934
        $parentHandlers      = self::_getFieldHandlers();
2935
        $this->_handlers    = array_merge($parentHandlers, $this->_getFieldHandlers());
2936
    }
2937
2938
    /**
2939
     * @param string $viewName
2940
     * @param int $cacheTtl
2941
     * @return array
2942
     */
2943
    public function getViewColumns($viewName, $cacheTtl=self::CACHE_NO)
2944
    {
2945
        return $this->_getColumnsByTableFromDb($viewName, $cacheTtl);
2946
    }
2947
2948
    /**
2949
     * @param int $id
2950
     * @return string
2951
     */
2952
    public function getDisplayNameById(int $id) : string
2953
    {
2954
        $displayColumn  = $this->getDisplayColumn();
2955
        $className      = get_class($this);
2956
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
2957
2958
        return $this
2959
            ->reset()
2960
            ->fetchStr($displayColumn, $id, self::ONE_HOUR);
2961
    }
2962
2963
    /**
2964
     * @param int $id
2965
     * @param string $displayColumnValue
2966
     * @return bool
2967
     */
2968
    public function validIdDisplayNameCombo(int $id, $displayColumnValue) : bool
2969
    {
2970
        return $displayColumnValue === $this->getDisplayNameById($id);
2971
    }
2972
2973
    /**
2974
     * @param array $toPopulate
2975
     * @return stdClass
2976
     */
2977
    protected function getEmptyObject(array $toPopulate=[]) : stdClass
2978
    {
2979
        $toPopulate[]   = 'id';
2980
2981
        return (object)array_flip($toPopulate);
2982
    }
2983
2984
    /**
2985
     * @param int $id
2986
     * @return bool
2987
     */
2988
    public static function isId(int $id) : bool
2989
    {
2990
        return $id > 0;
2991
    }
2992
2993
    /**
2994
     * @param int $cacheTtl
2995
     * @return int
2996
     */
2997
    public function activeCount(int $cacheTtl=self::CACHE_NO) : int
2998
    {
2999
        return (int)$this->whereActive()->count('*', $cacheTtl);
3000
    }
3001
3002
    /**
3003
     * @param string        $tableAlias
3004
     * @param string   $columnName
3005
     * @return FluentPdoModel
3006
     */
3007
    public function whereActive(string $tableAlias='', string $columnName='status') : FluentPdoModel
3008
    {
3009
        return $this->whereStatus(static::ACTIVE, $tableAlias, $columnName);
3010
    }
3011
3012
    /**
3013
     * @param string        $tableAlias
3014
     * @param string        $columnName
3015
     * @return FluentPdoModel
3016
     */
3017
    public function whereInactive(string $tableAlias='', string $columnName='status') : FluentPdoModel
3018
    {
3019
        return $this->whereStatus(static::INACTIVE, $tableAlias, $columnName);
3020
    }
3021
3022
    /**
3023
     * @param string        $tableAlias
3024
     * @param string        $columnName
3025
     * @return FluentPdoModel
3026
     */
3027
    public function whereArchived(string $tableAlias='', string $columnName='status') : FluentPdoModel
3028
    {
3029
        return $this->whereStatus(static::ARCHIVED, $tableAlias, $columnName);
3030
    }
3031
3032
    /**
3033
     * @param int $status
3034
     * @param string $tableAlias
3035
     * @param string $columnName
3036
     * @return FluentPdoModel
3037
     */
3038
    public function whereStatus(int $status, string $tableAlias='', string $columnName='status') : FluentPdoModel
3039
    {
3040
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3041
3042
        $tableAlias = empty($tableAlias) ? $this->getTableAlias() : $tableAlias;
3043
        $field      = empty($tableAlias) ? $columnName : "{$tableAlias}.{$columnName}";
3044
3045
        return $this->where($field, $status);
3046
    }
3047
3048
    /**
3049
     * @param int $id
3050
     * @return int
3051
     */
3052
    public function updateActive(int $id=0) : int
3053
    {
3054
        Assert($id)->unsignedInt();
3055
        if ( $id )
3056
        {
3057
            $this->wherePk($id);
3058
        }
3059
3060
        return $this->updateStatus(static::ACTIVE);
3061
    }
3062
3063
    /**
3064
     * @param int $id
3065
     * @return int
3066
     */
3067
    public function updateInactive(int $id=0) : int
3068
    {
3069
        Assert($id)->unsignedInt();
3070
        if ( $id )
3071
        {
3072
            $this->wherePk($id);
3073
        }
3074
        return $this->updateStatus(static::INACTIVE);
3075
    }
3076
3077
    /**
3078
     * @param string $field
3079
     * @param int  $id
3080
     * @return int
3081
     */
3082
    public function updateNow(string $field, int $id=0) : int
3083
    {
3084
        Assert($field)->notEmpty();
3085
3086
        return $this->updateField($field, date('Y-m-d H:i:s'), $id);
3087
    }
3088
    /**
3089
     * @param string $field
3090
     * @param int  $id
3091
     * @return int
3092
     */
3093
    public function updateToday($field, int $id=0) : int
3094
    {
3095
        Assert($field)->notEmpty();
3096
3097
        return $this->updateField($field, date('Y-m-d'), $id);
3098
    }
3099
3100
    /**
3101
     * @param int $id
3102
     * @return int
3103
     */
3104
    public function updateArchived(int $id=0) : int
3105
    {
3106
        Assert($id)->unsignedInt();
3107
        if ( $id )
3108
        {
3109
            $this->wherePk($id);
3110
        }
3111
3112
        return $this->updateStatus(static::ARCHIVED);
3113
    }
3114
3115
    /**
3116
     * @param int $status
3117
     * @return int
3118
     * @throws \Exception
3119
     */
3120
    public function updateStatus(int $status)
3121
    {
3122
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3123
3124
        return $this->updateField('status', $status);
3125
    }
3126
3127
    /**
3128
     * Return a YYYY-MM-DD HH:II:SS date format
3129
     *
3130
     * @param string $datetime - An english textual datetime description
3131
     *          now, yesterday, 3 days ago, +1 week
3132
     *          http://php.net/manual/en/function.strtotime.php
3133
     * @return string YYYY-MM-DD HH:II:SS
3134
     */
3135
    public static function NOW(string $datetime='now') : string
3136
    {
3137
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
3138
    }
3139
3140
    /**
3141
     * Return a string containing the given number of question marks,
3142
     * separated by commas. Eg '?, ?, ?'
3143
     *
3144
     * @param int - total of placeholder to insert
3145
     * @return string
3146
     */
3147
    protected function _makePlaceholders(int $numberOfPlaceholders=1) : string
3148
    {
3149
        return implode(', ', array_fill(0, $numberOfPlaceholders, '?'));
3150
    }
3151
3152
    /**
3153
     * Format the table{Primary|Foreign}KeyName
3154
     *
3155
     * @param  string $pattern
3156
     * @param  string $tableName
3157
     * @return string
3158
     */
3159
    protected function _formatKeyName(string $pattern, string $tableName) : string
3160
    {
3161
        return sprintf($pattern, $tableName);
3162
    }
3163
3164
3165
    /**
3166
     * @param array $query
3167
     * @param array $extraFields
3168
     * @return array
3169
     * @throws \Exception
3170
     */
3171
    protected function _prepareApiResource(array $query=[], array $extraFields=[]) : array
3172
    {
3173
        $this->defaultFilters()->filter($query)->paginate($query);
3174
        $pagingMetaData    = $this->getPagingMeta();
3175
        if ( $pagingMetaData['total'] === 0 )
3176
        {
3177
            return $pagingMetaData;
3178
        }
3179
        $this->withBelongsTo($pagingMetaData['fields']);
3180
        if ( ! empty($extraFields) )
3181
        {
3182
            $this->select($extraFields, '', false);
3183
        }
3184
        $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...
3185
        if ( ! empty($pagingMetaData['fields']) )
3186
        {
3187
            $this->removeUnrequestedFields($pagingMetaData['fields']);
3188
        }
3189
3190
        return $pagingMetaData;
3191
    }
3192
3193
    /**
3194
     * @param string $query
3195
     * @param array $parameters
3196
     *
3197
     * @return array
3198
     */
3199
    protected function _logQuery(string $query, array $parameters) : array
3200
    {
3201
        $query                  = $this->buildQuery($query, $parameters);
3202
        if ( ! $this->_log_queries )
3203
        {
3204
            return ['', ''];
3205
        }
3206
        $ident                  = substr(str_shuffle(md5($query)), 0, 10);
3207
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
3208
        $this->_timer['start']  = microtime(true);
3209
3210
        return [$query, $ident];
3211
    }
3212
3213
    /**
3214
     * @param string $ident
3215
     * @param string $builtQuery
3216
     */
3217
    protected function _logSlowQueries(string $ident, string $builtQuery)
3218
    {
3219
        if ( ! $this->_log_queries )
3220
        {
3221
            return ;
3222
        }
3223
        $this->_timer['end']    = microtime(true);
3224
        $seconds_taken          = round($this->_timer['end'] - $this->_timer['start'], 3);
3225
        if ( $seconds_taken > $this->_slow_query_secs )
3226
        {
3227
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$seconds_taken} seconds:\n{$builtQuery}");
3228
        }
3229
    }
3230
3231
    /**
3232
     * @return float
3233
     */
3234
    public function getTimeTaken() : float
3235
    {
3236
        $secondsTaken = $this->_timer['end'] - $this->_timer['start'];
3237
3238
        return (float)$secondsTaken;
3239
    }
3240
3241
    /**
3242
     * @param $secs
3243
     * @return FluentPdoModel
3244
     */
3245
    public function slowQuerySeconds(int $secs) : FluentPdoModel
3246
    {
3247
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
3248
        $this->_slow_query_secs = $secs;
3249
3250
        return $this;
3251
    }
3252
3253
3254
    /**
3255
     * @param       $field
3256
     * @param array $values
3257
     * @param string  $placeholderPrefix
3258
     *
3259
     * @return array
3260
     */
3261
    public function getNamedWhereIn(string $field, array $values, string $placeholderPrefix='') : array
3262
    {
3263
        Assert($field)->string()->notEmpty();
3264
        Assert($values)->isArray();
3265
3266
        if ( empty($values) )
3267
        {
3268
            return ['', []];
3269
        }
3270
        $placeholderPrefix      = $placeholderPrefix ?: strtolower(str_replace('.', '__', $field));
3271
        $params                 = [];
3272
        $placeholders           = [];
3273
        $count                  = 1;
3274
        foreach ( $values as $val )
3275
        {
3276
            $name                   = "{$placeholderPrefix}_{$count}";
3277
            $params[$name]          = $val;
3278
            $placeholders[]         = ":{$name}";
3279
            $count++;
3280
        }
3281
        $placeholders           = implode(',', $placeholders);
3282
3283
        return ["AND {$field} IN ({$placeholders})\n", $params];
3284
    }
3285
3286
    /**
3287
     * @param string $field
3288
     * @param string $delimiter
3289
     *
3290
     * @return array
3291
     */
3292
    protected function _getColumnAliasParts(string $field, string $delimiter=':') : array
3293
    {
3294
        $parts      = explode($delimiter, $field);
3295
        if ( count($parts) === 2 )
3296
        {
3297
            return $parts;
3298
        }
3299
3300
        return ['', $field];
3301
    }
3302
3303
    /**
3304
     * @param string $column
3305
     * @param string $term
3306
     * @return FluentPdoModel
3307
     */
3308
    protected function _addWhereClause(string $column, string $term) : FluentPdoModel
3309
    {
3310
        $modifiers = [
3311
            'whereLike'         => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3312
            'whereNotLike'      => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3313
            'whereLt'           => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
3314
            'whereLte'          => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
3315
            'whereGt'           => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
3316
            'whereGte'          => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
3317
            'whereBetween'      => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3318
            'whereNotBetween'  => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3319
        ];
3320
        foreach ( $modifiers as $func => $regex )
3321
        {
3322
            if ( preg_match($regex, $term, $matches) )
3323
            {
3324
                array_shift($matches);
3325
                switch ($func)
3326
                {
3327
                    case 'whereLike':
3328
3329
                        return $this->whereLike($column, $matches[0]);
3330
3331
                    case 'whereNotLike':
3332
3333
                        return $this->whereNotLike($column, $matches[0]);
3334
3335
                    case 'whereLt':
3336
3337
                        return $this->whereLt($column, $matches[0]);
3338
3339
                    case 'whereLte':
3340
3341
                        return $this->whereLte($column, $matches[0]);
3342
3343
                    case 'whereGt':
3344
3345
                        return $this->whereGt($column, $matches[0]);
3346
3347
                    case 'whereGte':
3348
3349
                        return $this->whereGte($column, $matches[0]);
3350
3351
                    case 'whereBetween':
3352
3353
                        return $this->whereBetween($column, $matches[0], $matches[1]);
3354
3355
                    case 'whereNotBetween':
3356
3357
                        return $this->whereNotBetween($column, $matches[0], $matches[1]);
3358
3359
                }
3360
            }
3361
        }
3362
3363
        return $this->where($column, $term);
3364
    }
3365
3366
    public function destroy()
3367
    {
3368
        if ( !is_null($this->_pdo_stmt) )
3369
        {
3370
            $this->_pdo_stmt->closeCursor();
3371
        }
3372
        $this->_pdo_stmt    = null;
3373
        $this->_handlers    = [];
3374
    }
3375
3376
    public function __destruct()
3377
    {
3378
        $this->destroy();
3379
    }
3380
3381
    /**
3382
     * Load a model
3383
     *
3384
     * @param string $modelName
3385
     * @param AbstractPdo $connection
3386
     * @return FluentPdoModel
3387
     * @throws ModelNotFoundException
3388
     */
3389
    public static function loadModel(string $modelName, AbstractPdo $connection=null) : FluentPdoModel
3390
    {
3391
        $modelName = static::$_model_namespace . $modelName;
3392
        if ( ! class_exists($modelName) )
3393
        {
3394
            throw new ModelNotFoundException("Failed to find model class {$modelName}.");
3395
        }
3396
3397
        return new $modelName($connection);
3398
    }
3399
3400
    /**
3401
     * Load a model
3402
     *
3403
     * @param string      $tableName
3404
     * @param AbstractPdo $connection
3405
     * @return FluentPdoModel
3406
     */
3407
    public static function loadTable(string $tableName, AbstractPdo $connection=null) : FluentPdoModel
3408
    {
3409
        $modelName     = Inflector::classify($tableName);
3410
        Assert($modelName)->notEmpty("Could not resolve model name from table name.");
3411
3412
        return static::loadModel($modelName, $connection);
3413
    }
3414
3415
    /**
3416
     * @param string   $columnName
3417
     * @param int $cacheTtl
3418
     * @param bool $flushCache
3419
     * @return bool
3420
     */
3421
    public function columnExists(string $columnName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false)
3422
    {
3423
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3424
        return array_key_exists($columnName, $columns);
3425
    }
3426
3427
    /**
3428
     * @param int $cacheTtl
3429
     * @param bool $flushCache
3430
     * @return FluentPdoModel
3431
     */
3432
    public function loadSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : FluentPdoModel
3433
    {
3434
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
3435
        $this->schema($schema);
3436
3437
        return $this;
3438
    }
3439
3440
    /**
3441
     * @param int $cacheTtl
3442
     * @param bool $flushCache
3443
     * @return array
3444
     */
3445
    public function getSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3446
    {
3447
        $table      = $this->getTableName();
3448
        Assert($table)->string()->notEmpty();
3449
        $schema     = [];
3450
        $columns    = $this->_getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
3451
        foreach ( $columns[$table] as $column => $meta )
3452
        {
3453
            $schema[$column] = $meta->data_type;
3454
        }
3455
        return $schema;
3456
    }
3457
3458
    /**
3459
     * @param string $table
3460
     * @param int $cacheTtl
3461
     * @param bool $flushCache
3462
     * @return array
3463
     */
3464
    protected function _getColumnsByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3465
    {
3466
        Assert($table)->string()->notEmpty();
3467
3468
        $callback = function() use ($table) {
3469
3470
            return $this->_connection->getColumns(true, $table);
3471
        };
3472
        $cacheKey   = '/column_schema/' . $table;
3473
        if ( $flushCache === true )
3474
        {
3475
            $this->clearCache($cacheKey);
3476
        }
3477
3478
        return (array)$this->_cacheData($cacheKey, $callback, $cacheTtl);
3479
    }
3480
3481
    /**
3482
     * @param string $table
3483
     * @return bool
3484
     */
3485
    public function clearSchemaCache(string $table) : bool
3486
    {
3487
        return $this->clearCache('/column_schema/' . $table);
3488
    }
3489
3490
    /**
3491
     * @param stdClass $record
3492
     * @return stdClass
3493
     */
3494
    public function cleanseRecord(stdClass $record) : stdClass
3495
    {
3496
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3497
        {
3498
            if ( is_string($record->{$field}) )
3499
            {
3500
                $record->$field = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", filter_var($record->$field, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES));
3501
                if ( $this->_log_filter_changes && $value !== $record->$field )
3502
                {
3503
                    $table = $this->_table_name ? $this->_table_name : '';
3504
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->$field]);
3505
                }
3506
            }
3507
        }
3508
3509
        return $record;
3510
    }
3511
3512
    /**
3513
     * @param stdClass $record
3514
     * @param string   $type
3515
     * @return stdClass
3516
     */
3517
    public function beforeSave(stdClass $record, string $type) : stdClass
3518
    {
3519
        $record = $this->addDefaultFields($record, $type);
3520
        $record = $this->applyGlobalModifiers($record, $type);
3521
        $record = $this->applyHandlers($record, $type);
3522
        $record = $this->removeUnneededFields($record, $type);
3523
3524
        return $record;
3525
    }
3526
3527
    /**
3528
     * @param array $data
3529
     * @param string $saveType
3530
     * @return array
3531
     */
3532
    public function cleanseWebData(array $data, string $saveType) : array
3533
    {
3534
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
3535
        $columns = $this->getColumns(false);
3536
        if ( empty($columns) )
3537
        {
3538
            return $data;
3539
        }
3540
        foreach ( $data as $field => $val )
3541
        {
3542
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
3543
        }
3544
3545
        return array_intersect_key($data, $columns);
3546
    }
3547
3548
    /**
3549
     * @return array
3550
     */
3551
    public function skeleton() : array
3552
    {
3553
        $skel       = [];
3554
        $columns    = $this->columns(false);
3555
        foreach ( $columns as $column => $type )
3556
        {
3557
            $skel[$column] = null;
3558
        }
3559
3560
        return $skel;
3561
    }
3562
3563
    /**
3564
     * @param bool $toString
3565
     * @return array
3566
     */
3567
    public function getErrors(bool $toString=false) : array
3568
    {
3569
        if ( $toString )
3570
        {
3571
            $errors = [];
3572
            foreach ( $this->_errors as $field => $error )
3573
            {
3574
                $errors[] = implode("\n", $error);
3575
            }
3576
3577
            return implode("\n", $errors);
3578
        }
3579
3580
        return $this->_errors;
3581
    }
3582
3583
    /**
3584
     * @param bool $throw
3585
     * @return FluentPdoModel
3586
     */
3587
    public function validationExceptions(bool $throw=true) : FluentPdoModel
3588
    {
3589
        $this->_validation_exceptions = $throw;
3590
3591
        return $this;
3592
    }
3593
3594
    /**
3595
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
3596
     *
3597
     * @return FluentPdoModel
3598
     * @throws Exception
3599
     */
3600
    public function paginate(array $query=[]) : FluentPdoModel
3601
    {
3602
        $_fields = $_order = $_limit = $_offset = null;
3603
        extract($query);
3604
        $this->_setLimit((int)$_limit, (int)$_offset);
3605
        $this->_setOrderBy((string)$_order);
3606
        $_fields    = is_array($_fields) ? $_fields : (string)$_fields;
3607
        $_fields    = empty($_fields) ? [] : $_fields;
3608
        $_fields    = is_string($_fields) ? explode('|', $_fields) : $_fields;
3609
        $_fields    = empty($_fields) ? [] : $_fields;
3610
        $this->_setFields(is_array($_fields) ? $_fields : explode('|', (string)$_fields));
3611
3612
        return $this;
3613
    }
3614
3615
    /**
3616
     * @param int $limit
3617
     * @param int $offset
3618
     * @return FluentPdoModel
3619
     */
3620
    protected function _setLimit(int $limit=0, int $offset=0) : FluentPdoModel
3621
    {
3622
        $limit      = ! $limit || (int)$limit > (int)$this->_default_max ? (int)$this->_default_max : (int)$limit;
3623
        if ( ! is_numeric($limit) )
3624
        {
3625
            return $this;
3626
        }
3627
        $this->limit((int)$limit);
3628
        if ( $offset && is_numeric($offset) )
3629
        {
3630
            $this->offset((int)$offset);
3631
        }
3632
3633
        return $this;
3634
    }
3635
3636
    /**
3637
     * @param array $fields
3638
     * @return FluentPdoModel
3639
     * @throws Exception
3640
     */
3641
    protected function _setFields(array $fields=[]) : FluentPdoModel
3642
    {
3643
        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...
3644
        {
3645
            return $this;
3646
        }
3647
        $this->explicitSelectMode();
3648
        $columns    = $this->getColumns();
3649
3650
        foreach ( $fields as $idx => $field )
3651
        {
3652
            list($alias, $field) = $this->_getColumnAliasParts($field);
3653
            $field = $field === '_display_field' ? $this->_display_column : $field;
3654
            // Regular primary table field
3655
            if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
3656
            {
3657
                $this->select("{$this->_table_alias}.{$field}");
3658
                $this->_requested_fields[] = "{$this->_table_alias}.{$field}";
3659
                continue;
3660
            }
3661
            // Reference table field with alias
3662
            if ( ! empty($alias) )
3663
            {
3664
                Assert($this->_associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
3665
                Assert($field)->eq($this->_associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
3666
                list(, , $join_field, $fieldAlias) = $this->_associations['belongsTo'][$alias];
3667
                $this->autoJoin($alias, static::LEFT_JOIN, false);
3668
                $this->select($join_field, $fieldAlias);
3669
                $this->_requested_fields[] = $fieldAlias;
3670
                continue;
3671
            }
3672
            // Reference table select field without alias
3673
            $belongsTo = array_key_exists('belongsTo', $this->_associations) ?  $this->_associations['belongsTo'] : [];
3674
            foreach ( $belongsTo as $joinAlias => $config )
3675
            {
3676
                list(, , $join_field, $fieldAlias) = $config;
3677
                if ( $field === $fieldAlias )
3678
                {
3679
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
3680
                    $this->select($join_field, $fieldAlias);
3681
                    $this->_requested_fields[] = $fieldAlias;
3682
                    continue;
3683
                }
3684
            }
3685
        }
3686
3687
        return $this;
3688
    }
3689
3690
    /**
3691
     * @param string $orderBy
3692
     * @return FluentPdoModel|FluentPdoModel
3693
     */
3694
    protected function _setOrderBy(string $orderBy='') : FluentPdoModel
3695
    {
3696
        if ( ! $orderBy )
3697
        {
3698
            return $this;
3699
        }
3700
        $columns                    = $this->getColumns();
3701
        list($order, $direction)    = strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
3702
        list($alias, $field)        = $this->_getColumnAliasParts(trim($order), '.');
3703
        $field                      = explode(' ', $field);
3704
        $field                      = trim($field[0]);
3705
        $direction                  = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
3706
        $belongsTo                  = array_key_exists('belongsTo', $this->_associations) ? $this->_associations['belongsTo'] : [];
3707
        // Regular primary table order by
3708
        if ( ( empty($alias) || $alias === $this->_table_alias ) && in_array($field, $columns) )
3709
        {
3710
            return $this->orderBy("{$this->_table_alias}.{$field}", $direction);
3711
        }
3712
        // Reference table order by with alias
3713
        if ( ! empty($alias) )
3714
        {
3715
            Assert($belongsTo)->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
3716
            Assert($field)->eq($belongsTo[$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
3717
3718
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
3719
        }
3720
        // Reference table order by without alias
3721
        foreach ( $belongsTo as $joinAlias => $config )
3722
        {
3723
            if ( $field === $config[3] )
3724
            {
3725
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
3726
            }
3727
        }
3728
3729
        return $this;
3730
    }
3731
3732
    /**
3733
     * @return array
3734
     */
3735
    public function getPagingMeta()
3736
    {
3737
        if ( empty($this->_paging_meta) )
3738
        {
3739
            $this->setPagingMeta();
3740
        }
3741
3742
        return $this->_paging_meta;
3743
    }
3744
3745
    /**
3746
     * @return FluentPdoModel
3747
     */
3748
    public function setPagingMeta() : FluentPdoModel
3749
    {
3750
        $model                  = clone $this;
3751
        $limit                  = intval($this->getLimit());
3752
        $offset                 = intval($this->getOffset());
3753
        $total                  = intval($model->withBelongsTo()->select('')->offset(0)->limit(0)->orderBy()->count());
3754
        unset($model->_handlers, $model); //hhmv mem leak
3755
        $order_bys              = ! is_array($this->_order_by) ? [] : $this->_order_by;
3756
        $this->_paging_meta     = [
3757
            'limit'                 => $limit,
3758
            'offset'                => $offset,
3759
            'page'                  => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
3760
            'pages'                 => $limit === 0 ? 1 : intval(ceil($total / $limit)),
3761
            'order'                 => $order_bys,
3762
            'total'                 => $total = $limit === 1 && $total > 1 ? 1 : $total,
3763
            'filters'               => $this->_filter_meta,
3764
            'fields'                => $this->_requested_fields,
3765
            'perms'                 => [],
3766
        ];
3767
3768
        return $this;
3769
    }
3770
3771
    /**
3772
     * Take a web request and format a query
3773
     *
3774
     * @param array $query
3775
     *
3776
     * @return FluentPdoModel
3777
     * @throws Exception
3778
     */
3779
    public function filter(array $query=[]) : FluentPdoModel
3780
    {
3781
        $columns   = $this->getColumns(false);
3782
        $alias     = '';
3783
        foreach ( $query as $column => $value )
3784
        {
3785
            if ( in_array($column, $this->_pagination_attribs) )
3786
            {
3787
                continue;
3788
            }
3789
            $field = $this->_findFieldByQuery($column, $this->_display_column);
3790
            if ( is_null($field) )
3791
            {
3792
                continue;
3793
            }
3794
            $this->_filter_meta[$field]     = $value;
3795
            $where                          = !is_array($value) && mb_stripos($value, '|') !== false ? explode('|', $value) : $value;
3796
            if ( is_array($where) )
3797
            {
3798
                $this->whereIn($field, $where);
3799
            }
3800
            else
3801
            {
3802
                $this->_addWhereClause($field, (string)$where);
3803
            }
3804
        }
3805
        if ( empty($query['_search']) )
3806
        {
3807
            return $this;
3808
        }
3809
        $alias          = !empty($alias) ? $alias : $this->_table_alias;
3810
        $string_cols    = array_filter($columns, function($type) {
3811
3812
            return in_array($type, ['varchar', 'text', 'enum']);
3813
        });
3814
        $terms          = explode('|', $query['_search']);
3815
        $where_likes    = [];
3816
        foreach ( $string_cols as $column => $type )
3817
        {
3818
            foreach ( $terms as $term )
3819
            {
3820
                $where_likes["{$alias}.{$column}"] = "%{$term}%";
3821
            }
3822
        }
3823
        // Reference fields...
3824
        $belongsTo = $this->getSearchableAssociations();
3825
        foreach ( $belongsTo as $alias => $config )
3826
        {
3827
            foreach ( $terms as $term )
3828
            {
3829
                $where_likes[$config[2]] = "%{$term}%";
3830
            }
3831
        }
3832
        if ( empty($where_likes) )
3833
        {
3834
            return $this;
3835
        }
3836
        $this->where([1=>1])->wrap()->_and();
3837
        foreach ( $where_likes as $column => $term )
3838
        {
3839
            $this->_or()->whereLike($column, $term);
3840
        }
3841
        $this->wrap();
3842
3843
        return $this;
3844
    }
3845
3846
    /**
3847
     * @param string $column
3848
     * @param string $displayCol
3849
     * @return string|null
3850
     */
3851
    protected function _findFieldByQuery(string $column, string $displayCol)
3852
    {
3853
        list($alias, $field)    = $this->_getColumnAliasParts($column);
3854
        $field                  = $field === '_display_field' ? $displayCol : $field;
3855
        $columns                = $this->getColumns();
3856
        $tableAlias             = $this->getTableAlias();
3857
        if ( ! empty($alias) && $alias === $tableAlias )
3858
        {
3859
            // Alias is set but the field isn't correct
3860
            if ( ! in_array($field, $columns) )
3861
            {
3862
                return null;
3863
            }
3864
            return "{$alias}.{$field}";
3865
        }
3866
        // Alias isn't passed in but the field is ok
3867
        if ( empty($alias) && in_array($field, $columns) )
3868
        {
3869
            return "{$tableAlias}.{$field}";
3870
        }
3871
        // Alias is passed but not this table in but there is a matching field on this table
3872
        if ( ! empty($alias) && in_array($field, $columns) )
3873
        {
3874
            return null;
3875
        }
3876
        // Now search the associations for the field
3877
        $associations = $this->getSearchableAssociations();
3878
        if ( ! empty($alias) )
3879
        {
3880
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
3881
            {
3882
                return "{$alias}.{$field}";
3883
            }
3884
3885
            return null;
3886
        }
3887
        foreach ( $associations as $assocAlias => $config )
3888
        {
3889
            list(, , $assocField, $fieldAlias) = $config;
3890
            if ( $fieldAlias === $field )
3891
            {
3892
                return $assocField;
3893
            }
3894
        }
3895
3896
        return null;
3897
    }
3898
3899
    /**
3900
     * @param $keysOnly
3901
     * @return array
3902
     */
3903
3904
    public function columns(bool $keysOnly=true) : array
3905
    {
3906
        return $keysOnly ? array_keys($this->_schema) : $this->_schema;
3907
    }
3908
3909
    /**
3910
     * @param string $field
3911
     * @param mixed $value
3912
     * @param bool|false $permissive
3913
     * @return float|int|null|string
3914
     */
3915
    protected function _fixType(string $field, $value, bool $permissive=false)
3916
    {
3917
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
3918
        if ( empty($this->_schema) || ( ! array_key_exists($field, $this->_schema) && $permissive ) )
3919
        {
3920
            return $value;
3921
        }
3922
        $columns    = $this->getColumns(false);
3923
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
3924
3925
        $field_type = ! empty($columns[$field]) ? $columns[$field] : null;
3926
3927
        if ( is_null($value) )
3928
        {
3929
            return null;
3930
        }
3931
        // return on null, '' but not 0
3932
        if ( ! is_numeric($value) && empty($value) )
3933
        {
3934
            return null;
3935
        }
3936
        // Don't cast invalid values... only those that can be cast cleanly
3937
        switch ($field_type)
3938
        {
3939
            case 'varchar':
3940
            case 'text';
3941
            case 'date':
3942
            case 'datetime':
3943
            case 'timestamp':
3944
                return (string)$value;
3945
            case 'int':
3946
            case 'tinyint':
3947
                return ! is_numeric($value) ? $value : (int)$value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return !is_numeric($valu... $value : (int) $value; (string|object|array|boolean|integer) is incompatible with the return type documented by Terah\FluentPdoModel\FluentPdoModel::_fixType of type double|integer|null|string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
3948
            case 'decimal':
3949
                return ! is_numeric($value) ? $value : (float)$value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return !is_numeric($valu...alue : (double) $value; (string|object|array|boolean|double) is incompatible with the return type documented by Terah\FluentPdoModel\FluentPdoModel::_fixType of type double|integer|null|string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
3950
            default:
3951
3952
                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...
3953
        }
3954
    }
3955
3956
    /**
3957
     * @param stdClass $record
3958
     * @param string $type
3959
     * @return stdClass
3960
     */
3961
    public function fixTypes(stdClass $record, string $type='') : stdClass
3962
    {
3963
        foreach ( $this->getColumns(false) as $column => $field_type )
3964
        {
3965
            if ( ! property_exists($record, $column) || is_null($record->$column) )
3966
            {
3967
                continue;
3968
            }
3969
            $record->$column = $this->_fixType($column, $record->$column);
3970
        }
3971
        unset($type);
3972
3973
        return $record;
3974
    }
3975
3976
    /**
3977
     * @param stdClass $record
3978
     * @param string   $type
3979
     * @return stdClass
3980
     * @throws Exception
3981
     */
3982
    public function applyHandlers(stdClass $record, string $type='INSERT') : stdClass
3983
    {
3984
        $this->_compileHandlers();
3985
        $this->_errors                  = [];
3986
        // Disable per field exceptions so we can capture all errors for the record
3987
        $tmpExceptions                  = $this->_validation_exceptions;
3988
        $this->_validation_exceptions   = false;
3989
        foreach ( $this->_handlers as $field => $fn_validator )
3990
        {
3991
            if ( ! property_exists($record, $field) )
3992
            {
3993
                // If the operation is an update it can be a partial update
3994
                if ( $type === self::SAVE_UPDATE )
3995
                {
3996
                    continue;
3997
                }
3998
                $record->{$field}               = null;
3999
            }
4000
            $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...
4001
        }
4002
        $this->_validation_exceptions = $tmpExceptions;
4003
        if ( $this->_validation_exceptions && ! empty($this->_errors) )
4004
        {
4005
            throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4006
        }
4007
4008
        return $record;
4009
    }
4010
4011
    /**
4012
     * @param string $field
4013
     * @param mixed $value
4014
     * @param string $type
4015
     * @param stdClass $record
4016
     * @return null
4017
     * @throws Exception
4018
     */
4019
    protected function applyHandler(string $field, $value, string $type='', stdClass $record=null)
4020
    {
4021
        $this->_compileHandlers();
4022
        $fnHandler = ! empty($this->_handlers[$field]) ? $this->_handlers[$field] : null;
4023
        if ( is_callable($fnHandler) )
4024
        {
4025
            try
4026
            {
4027
                $value = $fnHandler($field, $value, $type, $record);
4028
            }
4029
            catch( Exception $e )
4030
            {
4031
                $this->_errors[$field][] = $e->getMessage();
4032
                if ( $this->_validation_exceptions && ! empty($this->_errors) )
4033
                {
4034
                    throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4035
                }
4036
4037
                return null;
4038
            }
4039
        }
4040
4041
        return $value;
4042
    }
4043
4044
    /**
4045
     * @param string $start
4046
     * @param string $end
4047
     * @param string $hayStack
4048
     * @return mixed
4049
     */
4050
    public static function between(string $start, string $end, string $hayStack) : string
4051
    {
4052
        return static::before($end, static::after($start, $hayStack));
4053
    }
4054
4055
    /**
4056
     * @param string     $needle
4057
     * @param string     $hayStack
4058
     * @param bool $returnOrigIfNeedleNotExists
4059
     * @return mixed
4060
     */
4061
    public static function before(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4062
    {
4063
        $result = mb_substr($hayStack, 0, mb_strpos($hayStack, $needle));
4064
        if ( !$result && $returnOrigIfNeedleNotExists )
4065
        {
4066
            return $hayStack;
4067
        }
4068
4069
        return $result;
4070
    }
4071
4072
    /**
4073
     * @param string     $needle
4074
     * @param string     $hayStack
4075
     * @param bool $returnOrigIfNeedleNotExists
4076
     * @return string
4077
     */
4078
    public static function after(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4079
    {
4080
        if ( ! is_bool(mb_strpos($hayStack, $needle)) )
4081
        {
4082
            return mb_substr($hayStack, mb_strpos($hayStack, $needle) + mb_strlen($needle));
4083
        }
4084
4085
        return $returnOrigIfNeedleNotExists ? $hayStack : '';
4086
    }
4087
4088
    /**
4089
     * @return int
4090
     */
4091
    public function getUserId()
4092
    {
4093
        return 0;
4094
    }
4095
4096
    /**
4097
     * @param string $entity
4098
     * @param int $id
4099
     * @return int
4100
     */
4101
    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...
4102
    {
4103
        return 31;
4104
    }
4105
4106
    /**
4107
     * @param string|int|null $time
4108
     * @return string
4109
     */
4110
    public static function date($time=null) : string
4111
    {
4112
        return date('Y-m-d', static::getTime($time));
4113
    }
4114
4115
    /**
4116
     * @param string|int|null $time
4117
     * @return string
4118
     */
4119
    public static function dateTime($time=null) : string
4120
    {
4121
        return date('Y-m-d H:i:s', static::getTime($time));
4122
    }
4123
4124
    /**
4125
     * @param string|int|null $time
4126
     * @return string
4127
     */
4128
    public static function atom($time=null) : string
4129
    {
4130
        return date('Y-m-d\TH:i:sP', static::getTime($time));
4131
    }
4132
4133
    /**
4134
     * @param string|int|null $time
4135
     * @return int
4136
     */
4137
    public static function getTime($time=null) : int
4138
    {
4139
        if ( ! $time )
4140
        {
4141
            return time();
4142
        }
4143
        if ( is_int($time) )
4144
        {
4145
            return $time;
4146
        }
4147
4148
        return strtotime($time);
4149
    }
4150
4151
4152
    /**
4153
     * @param int $id
4154
     * @param int $cacheTtl
4155
     * @return string
4156
     */
4157
    public function getCodeById(int $id, int $cacheTtl=self::ONE_DAY) : string
4158
    {
4159
        Assert($id)->id();
4160
        $code   = $this->defaultFilters()->fetchStr($this->getDisplayColumn(), $id, $cacheTtl);
4161
        Assert($code)->notEmpty();
4162
4163
        return $code;
4164
    }
4165
}
4166