Completed
Push — master ( 9d7b5e...35b3f4 )
by Terry
08:42
created

FluentPdoModel::addDefaultFields()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2316
        {
2317
            Assert($field)->inArray($columns, "The field {$field} does not exist on this table {$this->tableName}");
2318
        }
2319
2320
        return $this->update((object)[$field => $value], $updateAll);
2321
    }
2322
2323
    /**
2324
     * @param stdClass $record
2325
     * @return bool|int
2326
     * @throws Exception
2327
     */
2328
    public function updateChanged(stdClass $record) : int
2329
    {
2330
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2331
        {
2332
            if ( is_null($value) )
2333
            {
2334
                $this->whereNotNull($field);
2335
                continue;
2336
            }
2337
            $this->whereCoercedNot($field, $value);
2338
        }
2339
2340
        return $this->update($record);
2341
    }
2342
2343
    /**
2344
     * @param string    $expression
2345
     * @param array     $params
2346
     * @return FluentPdoModel|$this
2347
     */
2348
    public function updateByExpression(string $expression, array $params) : FluentPdoModel
2349
    {
2350
        $this->updateRaw[]      = [$expression, $params];
2351
2352
        return $this;
2353
    }
2354
2355
    /**
2356
     * @param array $data
2357
     * @return int
2358
     * @throws Exception
2359
     */
2360
    public function rawUpdate(array $data=[]) : int
2361
    {
2362
        list($sql, $values)     = $this->updateSql($data);
2363
        $this->execute($sql, $values);
2364
        $rowCount               = $this->rowCount();
2365
        $this->destroy();
2366
2367
        return $rowCount;
2368
    }
2369
2370
    /**
2371
     * @param stdClass $record
2372
     * @return array
2373
     */
2374
    public function updateSqlQuery(stdClass $record) : array
2375
    {
2376
        Assert($record)
2377
            ->notEmpty("The data passed to update does not contain any data")
2378
            ->isInstanceOf('stdClass', "The data to be updated must be an object or an array of objects");
2379
        // Make sure we remove the primary key
2380
2381
        return $this->updateSql((array)$record);
2382
    }
2383
2384
    /**
2385
     * @param $record
2386
     * @return array
2387
     */
2388
    protected function updateSql(array $record) : array
2389
    {
2390
        unset($record[$this->getPrimaryKeyName()]);
2391
        Assert($this->limit)->eq(0, 'You cannot limit updates');
2392
2393
        $fieldList              = [];
2394
        $fieldAlias             = $this->addUpdateAlias && ! empty($this->tableAlias) ? "{$this->tableAlias}." : '';
2395
        foreach ( $record as $key => $value )
2396
        {
2397
            if ( is_numeric($key) )
2398
            {
2399
                $fieldList[]            = $value;
2400
                unset($record[$key]);
2401
2402
                continue;
2403
            }
2404
            $fieldList[]            = "{$fieldAlias}{$key} = ?";
2405
        }
2406
        $rawParams              = [];
2407
        foreach ( $this->updateRaw as $rawUpdate )
2408
        {
2409
            $fieldList[]            = $rawUpdate[0];
2410
            $rawParams              = array_merge($rawParams, $rawUpdate[1]);
2411
        }
2412
        $fieldList              = implode(', ', $fieldList);
2413
        $whereStr               = $this->getWhereString();
2414
        $joins                  = ! empty($this->joinSources) ? (' ').implode(' ',$this->joinSources) : '';
2415
        $alias                  = ! empty($this->tableAlias) ? " AS {$this->tableAlias}" : '';
2416
        $sql                    = "UPDATE {$this->tableName}{$alias}{$joins} SET {$fieldList}{$whereStr}";
2417
        $values                 = array_merge(array_values($record), $rawParams, $this->getWhereParameters());
2418
2419
        return [$sql, $values];
2420
    }
2421
2422
    /**
2423
     * @param bool $deleteAll
2424
     * @param bool $force
2425
     * @return int
2426
     * @throws Exception
2427
     */
2428
    public function delete(bool $deleteAll=false, bool $force=false) : int
2429
    {
2430
        if ( ! $force && $this->softDeletes )
2431
        {
2432
            return $this->updateArchived();
2433
        }
2434
2435
        list($sql, $params)     = $this->deleteSqlQuery();
2436
        if ( empty($this->whereConditions) && ! $deleteAll )
2437
        {
2438
            throw new Exception("You cannot update an entire table without calling update with deleteAll=true");
2439
        }
2440
        $this->execute($sql, $params);
2441
2442
        return $this->rowCount();
2443
    }
2444
2445
    /**
2446
     * @return bool
2447
     */
2448
    public function isSoftDelete() : bool
2449
    {
2450
        return $this->softDeletes;
2451
    }
2452
2453
    /**
2454
     * @param bool|false $force
2455
     * @return FluentPdoModel|$this
2456
     * @throws Exception
2457
     */
2458
    public function truncate(bool $force=false) : FluentPdoModel
2459
    {
2460
        if ( $force )
2461
        {
2462
            $this->execute('SET FOREIGN_KEY_CHECKS = 0');
2463
        }
2464
        $this->execute("TRUNCATE TABLE {$this->tableName}");
2465
        if ( $force )
2466
        {
2467
            $this->execute('SET FOREIGN_KEY_CHECKS = 1');
2468
        }
2469
2470
        return $this;
2471
    }
2472
2473
    /**
2474
     * @return array
2475
     */
2476
    public function deleteSqlQuery() : array
2477
    {
2478
        $query                  = "DELETE FROM {$this->tableName}";
2479
        if ( ! empty($this->whereConditions) )
2480
        {
2481
            $query                  .= $this->getWhereString(true);
2482
2483
            return [$query, $this->getWhereParameters()];
2484
        }
2485
2486
        return [$query, []];
2487
    }
2488
2489
2490
    /**
2491
     * Return the aggregate count of column
2492
     *
2493
     * @param string $column
2494
     * @param int $cacheTtl
2495
     * @return float
2496
     */
2497
    public function count(string $column='*', int $cacheTtl=self::CACHE_NO) : float
2498
    {
2499
        $this->explicitSelectMode();
2500
2501
        if ( empty($this->groupBy) )
2502
        {
2503
            return $this->fetchFloat("COUNT({$column}) AS cnt", 0, $cacheTtl);
2504
        }
2505
        $this->select("COUNT({$column}) AS cnt");
2506
        $sql                    = $this->getSelectQuery();
2507
        $params                 = $this->getWhereParameters();
2508
        $sql                    = "SELECT COUNT(*) AS cnt FROM ({$sql}) t";
2509
        $object                 = $this->query($sql, $params)->fetchOne(0, $cacheTtl);
2510
        if ( ! $object || empty($object->cnt) )
2511
        {
2512
            return 0.0;
2513
        }
2514
2515
        return (float)$object->cnt;
2516
    }
2517
2518
2519
    /**
2520
     * Return the aggregate max count of column
2521
     *
2522
     * @param string $column
2523
     * @param int $cacheTtl
2524
     * @return int|float|string|null
2525
     */
2526
    public function max(string $column, int $cacheTtl=self::CACHE_NO)
2527
    {
2528
        return $this
2529
            ->explicitSelectMode()
2530
            ->fetchField("MAX({$column}) AS max", 0, $cacheTtl);
2531
    }
2532
2533
2534
    /**
2535
     * Return the aggregate min count of column
2536
     *
2537
     * @param string $column
2538
     * @param int $cacheTtl
2539
     * @return int|float|string|null
2540
     */
2541
    public function min(string $column, int $cacheTtl=self::CACHE_NO)
2542
    {
2543
        return $this
2544
            ->explicitSelectMode()
2545
            ->fetchField("MIN({$column}) AS min", 0, $cacheTtl);
2546
    }
2547
2548
    /**
2549
     * Return the aggregate sum count of column
2550
     *
2551
     * @param string $column
2552
     * @param int $cacheTtl
2553
     * @return int|float|string|null
2554
     */
2555
    public function sum(string $column, int $cacheTtl=self::CACHE_NO)
2556
    {
2557
        return $this
2558
            ->explicitSelectMode()
2559
            ->fetchField("SUM({$column}) AS sum", 0, $cacheTtl);
2560
    }
2561
2562
    /**
2563
     * Return the aggregate average count of column
2564
     *
2565
     * @param string $column
2566
     * @param int $cacheTtl
2567
     * @return int|float|string|null
2568
     */
2569
    public function avg(string $column, int $cacheTtl=self::CACHE_NO)
2570
    {
2571
        return $this
2572
            ->explicitSelectMode()
2573
            ->fetchField("AVG({$column}) AS avg", 0, $cacheTtl);
2574
    }
2575
2576
    /*******************************************************************************/
2577
// Utilities methods
2578
2579
    /**
2580
     * Reset fields
2581
     *
2582
     * @return FluentPdoModel|$this
2583
     */
2584
    public function reset() : FluentPdoModel
2585
    {
2586
        $this->whereParameters      = [];
2587
        $this->selectFields         = [];
2588
        $this->joinSources          = [];
2589
        $this->joinAliases          = [];
2590
        $this->whereConditions      = [];
2591
        $this->limit                = 0;
2592
        $this->offset               = 0;
2593
        $this->orderBy              = [];
2594
        $this->groupBy              = [];
2595
        $this->andOrOperator        = self::OPERATOR_AND;
2596
        $this->having               = [];
2597
        $this->wrapOpen             = false;
2598
        $this->lastWrapPosition     = 0;
2599
        $this->pdoStmt              = null;
2600
        $this->distinct             = false;
2601
        $this->requestedFields      = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type null of property $requestedFields.

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...
2602
        $this->filterMeta           = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type null of property $filterMeta.

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...
2603
        $this->cacheTtl             = -1;
2604
        $this->timer                = [];
2605
        $this->builtQuery           = '';
2606
        $this->pagingMeta           = [];
2607
        $this->rawSql               = null;
2608
        $this->explicitSelectMode   = false;
2609
2610
        return $this;
2611
    }
2612
2613
2614
    /**
2615
     * @return FluentPdoModel|$this
2616
     */
2617
    public function removeUnauthorisedFields() : FluentPdoModel
2618
    {
2619
        return $this;
2620
    }
2621
2622
    /**
2623
     * @return Closure[]
2624
     */
2625
    protected function getFieldHandlers() : array
2626
    {
2627
        $columns                = $this->getColumns(true);
2628
        if ( empty($columns) )
2629
        {
2630
            return [];
2631
        }
2632
2633
        return [
2634
            'id'                    => function(string $field, $value, string $type='', stdClass $record=null) {
2635
2636
                unset($record);
2637
                $value                  = $this->fixType($field, $value);
2638
                if ( $type === self::SAVE_INSERT )
2639
                {
2640
                    Validate($value)->fieldName($field)->nullOr()->id('ID must be a valid integer id, (%s) submitted.');
2641
2642
                    return $value;
2643
                }
2644
                Validate($value)->fieldName($field)->id('ID must be a valid integer id, (%s) submitted.');
2645
2646
                return $value;
2647
            },
2648
            self::CREATOR_ID_FIELD  => function(string $field, $value, string $type='', stdClass $record=null) {
2649
2650
                unset($type, $record);
2651
                $value                  = $this->fixType($field, $value);
2652
                // Created user id is set to current user if record is an insert or deleted if not (unless override is true)
2653
                $value                  = $this->allowMetaOverride ? $value : $this->getUserId();
2654
                Validate($value)->fieldName($field)->id('Created By must be a valid integer id, (%s) submitted.');
2655
2656
                return $value;
2657
            },
2658
            self::CREATED_TS_FIELD  => function(string $field, $value, string $type='', stdClass $record=null) {
2659
2660
                unset($type, $record);
2661
                $value                  = $this->fixType($field, $value);
2662
                // Created ts is set to now if record is an insert or deleted if not (unless override is true)
2663
                $value                  = static::dateTime($this->allowMetaOverride ? $value : null);
2664
                Validate($value)->fieldName($field)->date('Created must be a valid timestamp, (%s) submitted.');
2665
2666
                return $value;
2667
            },
2668
            self::MODIFIER_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2669
2670
                unset($type, $record);
2671
                $value                  = $this->fixType($field, $value);
2672
                // Modified user id is set to current user (unless override is true)
2673
                $value                  = $this->allowMetaOverride ? $value : $this->getUserId();
2674
                Validate($value)->fieldName($field)->id('Modified By must be a valid integer id, (%s) submitted.');
2675
2676
                return $value;
2677
            },
2678
            self::MODIFIED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2679
2680
                unset($type, $record);
2681
                $value                  = $this->fixType($field, $value);
2682
                // Modified timestamps are set to now (unless override is true)
2683
                $value                  = static::dateTime($this->allowMetaOverride ? $value : null);
2684
                Validate($value)->fieldName($field)->date('Modified must be a valid timestamp, (%s) submitted.');
2685
2686
                return $value;
2687
            },
2688
            self::DELETER_ID_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2689
2690
                if ( $type === self::SAVE_INSERT )
2691
                {
2692
                    return null;
2693
                }
2694
                if ( empty($record->deleted_ts) )
2695
                {
2696
                    return null;
2697
                }
2698
                unset($type, $record);
2699
                $value                  = $this->fixType($field, $value);
2700
2701
                // Modified user id is set to current user (unless override is true)
2702
                $value                  = $this->allowMetaOverride ? $value : $this->getUserId();
2703
                Validate($value)->fieldName($field)->nullOr()->id('Deleter must be a valid integer id, (%s) submitted.');
2704
2705
                return $value;
2706
            },
2707
            self::DELETED_TS_FIELD => function(string $field, $value, string $type='', stdClass $record=null) {
2708
2709
                if ( $type === self::SAVE_INSERT )
2710
                {
2711
                    return null;
2712
                }
2713
                unset($type, $record);
2714
                $value                  = $this->fixType($field, $value);
2715
                if ( $value )
2716
                {
2717
                    $value                  = static::dateTime($this->allowMetaOverride ? $value : null);
2718
                    Validate($value)->fieldName($field)->date('Deleted Timestamp must be a valid timestamp, (%s) submitted.');
2719
                }
2720
2721
                return $value;
2722
            },
2723
        ];
2724
    }
2725
2726
    /**
2727
     * @return bool
2728
     */
2729
    public function begin() : bool
2730
    {
2731
        $pdo                    = $this->getPdo();
2732
        $oldDepth               = $pdo->getTransactionDepth();
2733
        $res                    = $pdo->beginTransaction();
2734
        $newDepth               = $pdo->getTransactionDepth();
2735
        $this->getLogger()->debug("Calling db begin transaction", [
2736
            'old_depth'             => $oldDepth,
2737
            'new_depth'             => $newDepth,
2738
            'trans_started'         => $newDepth === 1 ? true : false,
2739
        ]);
2740
2741
        return $res;
2742
    }
2743
2744
    /**
2745
     * @return bool
2746
     */
2747
    public function commit() : bool
2748
    {
2749
        $pdo                    = $this->getPdo();
2750
        $oldDepth               = $pdo->getTransactionDepth();
2751
        $res                    = $pdo->commit();
2752
        $newDepth               = $pdo->getTransactionDepth();
2753
        $this->getLogger()->debug("Calling db commit transaction", [
2754
            'old_depth'             => $oldDepth,
2755
            'new_depth'             => $newDepth,
2756
            'trans_ended'           => $newDepth === 0 ? true : false,
2757
        ]);
2758
        if ( ! $res )
2759
        {
2760
            return false;
2761
        }
2762
2763
        return $res === 0 ? true : $res;
2764
    }
2765
2766
    /**
2767
     * @return bool
2768
     */
2769
    public function rollback() : bool
2770
    {
2771
        $pdo                    = $this->getPdo();
2772
        $oldDepth               = $pdo->getTransactionDepth();
2773
        $res                    = $pdo->rollback();
2774
        $newDepth               = $pdo->getTransactionDepth();
2775
        $this->getLogger()->debug("Calling db rollback transaction", [
2776
            'old_depth'             => $oldDepth,
2777
            'new_depth'             => $newDepth,
2778
            'trans_ended'           => $newDepth === 0 ? true : false,
2779
        ]);
2780
2781
        return $res;
2782
    }
2783
2784
    /**
2785
     * @param stdClass $record
2786
     * @param  string  $type
2787
     * @return stdClass
2788
     */
2789
    public function applyGlobalModifiers(stdClass $record, string $type) : stdClass
2790
    {
2791
        unset($type);
2792
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2793
        {
2794
            if ( is_string($record->{$field}) )
2795
            {
2796
                $record->{$field}       = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", $value);
2797
            }
2798
        }
2799
2800
        return $record;
2801
    }
2802
2803
    /**
2804
     * @param stdClass $record
2805
     * @param  string $type
2806
     * @return stdClass
2807
     */
2808
    public function removeUnneededFields(stdClass $record, string $type) : stdClass
2809
    {
2810
        $creatorId              = self::CREATOR_ID_FIELD;
2811
        $createdTs              = self::CREATED_TS_FIELD;
2812
2813
        // remove un-needed fields
2814
        $columns = $this->getColumns(true);
2815
        if ( empty($columns) )
2816
        {
2817
            return $record;
2818
        }
2819
        foreach ( $record as $name => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
2820
        {
2821
            if ( ! in_array($name, $columns) || in_array($name, $this->virtualFields) )
2822
            {
2823
                unset($record->{$name});
2824
            }
2825
        }
2826
        if ( property_exists($record, $createdTs) && $type !== 'INSERT' && ! $this->allowMetaOverride )
2827
        {
2828
            unset($record->{$createdTs});
2829
        }
2830
        if ( property_exists($record, $creatorId) && $type !== 'INSERT' && ! $this->allowMetaOverride )
2831
        {
2832
            unset($record->{$creatorId});
2833
        }
2834
2835
        return $record;
2836
    }
2837
2838
2839
    /**
2840
     * @param array $ids
2841
     * @param array $values
2842
     * @param int   $batch
2843
     * @return bool
2844
     */
2845
    public function setById(array $ids, array $values, int $batch=1000) : bool
2846
    {
2847
        $ids                    = array_unique($ids);
2848
        if ( empty($ids) )
2849
        {
2850
            return true;
2851
        }
2852
        if ( count($ids) <= $batch )
2853
        {
2854
            return (bool)$this->whereIn('id', $ids)->updateArr($values);
2855
        }
2856
        while ( ! empty($ids) )
2857
        {
2858
            $thisBatch              = array_slice($ids, 0, $batch);
2859
            $ids                    = array_diff($ids, $thisBatch);
2860
            $this->reset()->whereIn('id', $thisBatch)->updateArr($values);
2861
        }
2862
2863
        return true;
2864
    }
2865
2866
2867
    /**
2868
     * @param string $displayColumnValue
2869
     * @return int
2870
     */
2871
    public function resolveId(string $displayColumnValue) : int
2872
    {
2873
        $displayColumn          = $this->getDisplayColumn();
2874
        $className              = get_class($this);
2875
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
2876
2877
        return $this
2878
            ->reset()
2879
            ->where($displayColumn, $displayColumnValue)
2880
            ->fetchInt('id', 0, self::ONE_HOUR);
2881
    }
2882
2883
    /**
2884
     * @param int   $resourceId
2885
     * @param array $query
2886
     * @param array $extraFields
2887
     * @param int $cacheTtl
2888
     * @return array
2889
     */
2890
    public function fetchApiResource(int $resourceId, array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO) : array
2891
    {
2892
        Assert($resourceId)->id();
2893
2894
        $query['_limit']        = 1;
2895
        $pagingMetaData         = $this->wherePk($resourceId)->prepareApiResource($query, $extraFields);
2896
        if ( $pagingMetaData['total'] === 0 )
2897
        {
2898
            return [[], $pagingMetaData];
2899
        }
2900
2901
        return [$this->fetchOne($resourceId, $cacheTtl), $pagingMetaData];
2902
    }
2903
2904
    /**
2905
     * @param array     $query
2906
     * @param array     $extraFields
2907
     * @param int       $cacheTtl
2908
     * @param string    $permEntity
2909
     * @return array
2910
     */
2911
    public function fetchApiResources(array $query=[], array $extraFields=[], int $cacheTtl=self::CACHE_NO, string $permEntity='') : array
2912
    {
2913
        $pagingMetaData         = $this->prepareApiResource($query, $extraFields);
2914
        if ( $pagingMetaData['total'] === 0 )
2915
        {
2916
            return [[], $pagingMetaData];
2917
        }
2918
        $results                = $this->fetch('', $cacheTtl);
2919
        if ( ! $permEntity )
2920
        {
2921
            return [$results, $pagingMetaData];
2922
        }
2923
        foreach ( $results as $rec )
2924
        {
2925
            if ( ! empty($rec->id) )
2926
            {
2927
                $pagingMetaData['perms'][(int)$rec->id] = $this->getMaskByResourceAndId($permEntity, $rec->id);
2928
            }
2929
        }
2930
2931
        return [$results, $pagingMetaData];
2932
    }
2933
2934
2935
    /**
2936
     * @return array
2937
     */
2938
    public function getSearchableAssociations() : array
2939
    {
2940
        $belongsTo              = ! empty($this->associations['belongsTo']) ? $this->associations['belongsTo'] : [];
2941
        unset($belongsTo['CreatedBy'], $belongsTo['ModifiedBy']);
2942
2943
        return $belongsTo;
2944
    }
2945
2946
    /**
2947
     * @param array $fields
2948
     */
2949
    public function removeUnrequestedFields(array $fields)
2950
    {
2951
        foreach ( $this->selectFields as $idx => $field )
2952
        {
2953
            $field = trim(static::after(' AS ', $field, true));
2954
            if ( ! in_array($field, $fields) )
2955
            {
2956
                unset($this->selectFields[$idx]);
2957
            }
2958
        }
2959
    }
2960
2961
    /**
2962
     * @param array $removeFields
2963
     */
2964
    public function removeFields(array $removeFields=[])
2965
    {
2966
        $searches               = [];
2967
        foreach ( $removeFields as $removeField )
2968
        {
2969
            $removeField            = str_replace("{$this->tableAlias}.", '', $removeField);
2970
            $searches[]             = "{$this->tableAlias}.{$removeField}";
2971
            $searches[]             = $removeField;
2972
        }
2973
        foreach ( $this->selectFields as $idx => $selected )
2974
        {
2975
            $selected               = stripos($selected, ' AS ') !== false ? preg_split('/ as /i', $selected) : [$selected];
2976
            foreach ( $selected as $haystack )
2977
            {
2978
                foreach ( $searches as $search )
2979
                {
2980
                    if ( trim($haystack) === trim($search) )
2981
                    {
2982
                        unset($this->selectFields[$idx]);
2983
2984
                        continue;
2985
                    }
2986
                }
2987
            }
2988
        }
2989
    }
2990
2991
    /**
2992
     * @return FluentPdoModel|$this
2993
     */
2994
    public function defaultFilters() : FluentPdoModel
2995
    {
2996
        return $this;
2997
    }
2998
2999
    /**
3000
     * @param bool $allow
3001
     *
3002
     * @return FluentPdoModel|$this
3003
     */
3004
    public function allowMetaColumnOverride(bool $allow=false) : FluentPdoModel
3005
    {
3006
        $this->allowMetaOverride = $allow;
3007
3008
        return $this;
3009
    }
3010
3011
    /**
3012
     * @param bool $skip
3013
     *
3014
     * @return FluentPdoModel|$this
3015
     */
3016
    public function skipMetaUpdates(bool $skip=true) : FluentPdoModel
3017
    {
3018
        $this->skipMetaUpdates  = $skip;
3019
3020
        return $this;
3021
    }
3022
3023
    /**
3024
     * @param bool $add
3025
     *
3026
     * @return FluentPdoModel|$this
3027
     */
3028
    public function addUpdateAlias(bool $add=true) : FluentPdoModel
3029
    {
3030
        $this->addUpdateAlias   = $add;
3031
3032
        return $this;
3033
    }
3034
3035
    /**
3036
     * @param stdClass $record
3037
     * @return stdClass
3038
     */
3039
    public function onFetch(stdClass $record) : stdClass
3040
    {
3041
        $record                 = $this->trimAndLowerCaseKeys($record);
3042
        if ( $this->filterOnFetch )
3043
        {
3044
            $record                 = $this->cleanseRecord($record);
3045
        }
3046
3047
        $record                 =  $this->fixTypesToSentinel($record);
3048
3049
        return $this->fixTimestamps($record);
3050
    }
3051
3052
    /**
3053
     * @param $value
3054
     * @return string
3055
     */
3056
    public function gzEncodeData(string $value) : string
3057
    {
3058
        if ( $this->hasGzipPrefix($value) )
3059
        {
3060
            return $value;
3061
        }
3062
3063
        return static::GZIP_PREFIX . base64_encode(gzencode($value, 9));
3064
    }
3065
3066
    /**
3067
     * @param $value
3068
     * @return mixed|string
3069
     */
3070
    public function gzDecodeData(string $value) : string
3071
    {
3072
        if ( ! $this->hasGzipPrefix($value) )
3073
        {
3074
            return $value;
3075
        }
3076
        $value = substr_replace($value, '', 0, strlen(static::GZIP_PREFIX));
3077
3078
        return gzdecode(base64_decode($value));
3079
    }
3080
3081
    /**
3082
     * @param $value
3083
     * @return bool
3084
     */
3085
    protected function hasGzipPrefix(string $value) : bool
3086
    {
3087
        return substr($value, 0, strlen(static::GZIP_PREFIX)) === static::GZIP_PREFIX ? true : false;
3088
    }
3089
3090
    /**
3091
     * @param stdClass $record
3092
     * @return stdClass
3093
     */
3094
    public function fixTimestamps(stdClass $record) : stdClass
3095
    {
3096
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3097
        {
3098
            if ( preg_match('/_ts$/', $field) )
3099
            {
3100
                $record->{$field}       = empty($value) ? $value : static::atom($value);
3101
            }
3102
        }
3103
3104
        return $record;
3105
    }
3106
3107
    /**
3108
     * @param int $max
3109
     * @return FluentPdoModel|$this
3110
     */
3111
    public function setMaxRecords(int $max) : FluentPdoModel
3112
    {
3113
        Assert($max)->int();
3114
        $this->defaultMax       = $max;
3115
3116
        return $this;
3117
    }
3118
3119
3120
    /**
3121
     * @param stdClass $record
3122
     * @param string   $type
3123
     * @return stdClass
3124
     */
3125
    public function afterSave(stdClass $record, string $type) : stdClass
3126
    {
3127
        unset($type);
3128
        $this->clearCacheByTable();
3129
        foreach ( $record as $col => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3130
        {
3131
            if ( !empty($record->{$col}) )
3132
            {
3133
                if ( preg_match('/_ts$/', $col) )
3134
                {
3135
                    $record->{$col}         = static::atom($value);
3136
                }
3137
                if ( preg_match('/_am$/', $col) )
3138
                {
3139
                    $record->{$col}         = number_format($value, 2, '.', '');
3140
                }
3141
            }
3142
        }
3143
3144
        return $record;
3145
    }
3146
3147
    /**
3148
     * @param stdClass $record
3149
     * @param string $type
3150
     * @return stdClass
3151
     */
3152
    public function addDefaultFields(stdClass $record, string $type) : stdClass
3153
    {
3154
        $columns                = $this->getColumns(true);
3155
        if ( empty($columns) )
3156
        {
3157
            return $record;
3158
        }
3159
        $defaults               = [
3160
            self::SAVE_UPDATE       => [
3161
                self::MODIFIER_ID_FIELD => null,
3162
                self::MODIFIED_TS_FIELD => null,
3163
            ],
3164
            self::SAVE_INSERT       => [
3165
                self::CREATOR_ID_FIELD  => null,
3166
                self::CREATED_TS_FIELD  => null,
3167
                self::MODIFIER_ID_FIELD => null,
3168
                self::MODIFIED_TS_FIELD => null,
3169
            ]
3170
        ];
3171
        if ( $this->skipMetaUpdates )
3172
        {
3173
            $defaults[self::SAVE_UPDATE] = [];
3174
        }
3175
        $columns            = array_flip($this->getColumns());
3176
        $defaults           = array_intersect_key($defaults[$type], $columns);
3177
        foreach ( $defaults as $column => $def )
3178
        {
3179
            $record->{$column} = $record->{$column} ?? $def;
3180
        }
3181
        unset($record->active);
3182
3183
        return $record;
3184
    }
3185
3186
3187
    /**
3188
     * @return bool
3189
     */
3190
    public function createTable() : bool
3191
    {
3192
        return true;
3193
    }
3194
3195
    /**
3196
     * @param bool|false $force
3197
     * @return FluentPdoModel|$this
3198
     * @throws Exception
3199
     */
3200
    public function dropTable(bool $force=false) : FluentPdoModel
0 ignored issues
show
Unused Code introduced by
The parameter $force is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3201
    {
3202
        return $this;
3203
    }
3204
3205
    protected function compileHandlers()
3206
    {
3207
        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...
3208
        {
3209
            return;
3210
        }
3211
        $parentHandlers         = self::getFieldHandlers();
3212
        $this->handlers         = array_merge($parentHandlers, $this->getFieldHandlers());
3213
    }
3214
3215
    /**
3216
     * @param string $viewName
3217
     * @param int $cacheTtl
3218
     * @return array
3219
     */
3220
    public function getViewColumns($viewName, $cacheTtl=self::CACHE_NO)
3221
    {
3222
        return $this->getColumnsByTableFromDb($viewName, $cacheTtl);
3223
    }
3224
3225
    /**
3226
     * @param int $id
3227
     * @return string
3228
     */
3229
    public function getDisplayNameById(int $id) : string
3230
    {
3231
        $displayColumn  = $this->getDisplayColumn();
3232
        $className      = get_class($this);
3233
        Assert($displayColumn)->notEmpty("Could not determine the display column for model ({$className})");
3234
3235
        return $this
3236
            ->reset()
3237
            ->fetchStr($displayColumn, $id, self::ONE_HOUR);
3238
    }
3239
3240
    /**
3241
     * @param int $id
3242
     * @param string $displayColumnValue
3243
     * @return bool
3244
     */
3245
    public function validIdDisplayNameCombo(int $id, $displayColumnValue) : bool
3246
    {
3247
        return $displayColumnValue === $this->getDisplayNameById($id);
3248
    }
3249
3250
    /**
3251
     * @param array $toPopulate
3252
     * @return stdClass
3253
     */
3254
    protected function getEmptyObject(array $toPopulate=[]) : stdClass
3255
    {
3256
        $toPopulate[]   = 'id';
3257
3258
        return (object)array_flip($toPopulate);
3259
    }
3260
3261
    /**
3262
     * @param array $toPopulate
3263
     * @return stdClass
3264
     */
3265
    protected static function emptyObject(array $toPopulate=[]) : stdClass
3266
    {
3267
        $toPopulate[]   = 'id';
3268
3269
        return (object)array_flip($toPopulate);
3270
    }
3271
3272
    /**
3273
     * @param int $id
3274
     * @return bool
3275
     */
3276
    public static function isId(int $id) : bool
3277
    {
3278
        return $id > 0;
3279
    }
3280
3281
    /**
3282
     * @param int $cacheTtl
3283
     * @return int
3284
     */
3285
    public function activeCount(int $cacheTtl=self::CACHE_NO) : int
3286
    {
3287
        return (int)$this->whereActive()->count('*', $cacheTtl);
3288
    }
3289
3290
    /**
3291
     * @param string        $tableAlias
3292
     * @param string   $columnName
3293
     * @return FluentPdoModel|$this
3294
     */
3295
    public function whereActive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3296
    {
3297
        return $this->whereStatus(static::ACTIVE, $tableAlias, $columnName);
3298
    }
3299
3300
    /**
3301
     * @param string        $tableAlias
3302
     * @param string        $columnName
3303
     * @return FluentPdoModel|$this
3304
     */
3305
    public function whereInactive(string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3306
    {
3307
        return $this->whereStatus(static::INACTIVE, $tableAlias, $columnName);
3308
    }
3309
3310
    /**
3311
     * @param string        $tableAlias
3312
     * @param string        $columnName
3313
     * @return FluentPdoModel|$this
3314
     */
3315
    public function whereArchived(string $tableAlias='', string $columnName='status') : FluentPdoModel
3316
    {
3317
        return $this->whereStatus(static::ARCHIVED, $tableAlias, $columnName);
3318
    }
3319
3320
    /**
3321
     * @param int $status
3322
     * @param string $tableAlias
3323
     * @param string $columnName
3324
     * @return FluentPdoModel|$this
3325
     */
3326
    public function whereStatus(int $status, string $tableAlias='', string $columnName=self::STATUS_FIELD) : FluentPdoModel
3327
    {
3328
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3329
3330
        $tableAlias = empty($tableAlias) ? $this->getTableAlias() : $tableAlias;
3331
        $field      = empty($tableAlias) ? $columnName : "{$tableAlias}.{$columnName}";
3332
3333
        return $this->where($field, $status);
3334
    }
3335
3336
    /**
3337
     * @param int $id
3338
     * @return int
3339
     */
3340
    public function updateActive(int $id=0) : int
3341
    {
3342
        Assert($id)->unsignedInt();
3343
        if ( $id )
3344
        {
3345
            $this->wherePk($id);
3346
        }
3347
3348
        return $this->updateStatus(static::ACTIVE);
3349
    }
3350
3351
    /**
3352
     * @param int $id
3353
     * @return int
3354
     */
3355
    public function updateInactive(int $id=0) : int
3356
    {
3357
        Assert($id)->unsignedInt();
3358
        if ( $id )
3359
        {
3360
            $this->wherePk($id);
3361
        }
3362
        return $this->updateStatus(static::INACTIVE);
3363
    }
3364
3365
    /**
3366
     * @param string $field
3367
     * @param int  $id
3368
     * @return int
3369
     */
3370
    public function updateNow(string $field, int $id=0) : int
3371
    {
3372
        Assert($field)->notEmpty();
3373
3374
        return $this->updateField($field, date('Y-m-d H:i:s'), $id);
3375
    }
3376
3377
    /**
3378
     * @param string $field
3379
     * @param int  $id
3380
     * @return int
3381
     */
3382
    public function updateToday($field, int $id=0) : int
3383
    {
3384
        Assert($field)->notEmpty();
3385
3386
        return $this->updateField($field, date('Y-m-d'), $id);
3387
    }
3388
3389
    /**
3390
     * @param int $id
3391
     * @return int
3392
     */
3393
    public function updateDeleted(int $id=0) : int
3394
    {
3395
        Assert($id)->unsignedInt();
3396
        if ( $id )
3397
        {
3398
            $this->wherePk($id);
3399
        }
3400
3401
        return $this->update((object)[
3402
            self::DELETER_ID_FIELD  => $this->getUserId(),
3403
            self::DELETED_TS_FIELD  => static::dateTime(),
3404
        ]);
3405
    }
3406
3407
    /**
3408
     * @param int $id
3409
     * @return int
3410
     */
3411
    public function updateArchived(int $id=0) : int
3412
    {
3413
        Assert($id)->unsignedInt();
3414
        if ( $id )
3415
        {
3416
            $this->wherePk($id);
3417
        }
3418
3419
        return $this->updateStatus(static::ARCHIVED);
3420
    }
3421
3422
    /**
3423
     * @param int $status
3424
     * @return int
3425
     * @throws \Exception
3426
     */
3427
    public function updateStatus(int $status)
3428
    {
3429
        Assert($status)->inArray([static::ACTIVE, static::INACTIVE, static::ARCHIVED]);
3430
3431
        return $this->updateField('status', $status);
3432
    }
3433
3434
    /**
3435
     * Return a YYYY-MM-DD HH:II:SS date format
3436
     *
3437
     * @param string $datetime - An english textual datetime description
3438
     *          now, yesterday, 3 days ago, +1 week
3439
     *          http://php.net/manual/en/function.strtotime.php
3440
     * @return string YYYY-MM-DD HH:II:SS
3441
     */
3442
    public static function NOW(string $datetime='now') : string
3443
    {
3444
        return (new DateTime($datetime ?: 'now'))->format('Y-m-d H:i:s');
3445
    }
3446
3447
    /**
3448
     * Return a string containing the given number of question marks,
3449
     * separated by commas. Eg '?, ?, ?'
3450
     *
3451
     * @param int - total of placeholder to insert
3452
     * @return string
3453
     */
3454
    protected function makePlaceholders(int $numberOfPlaceholders=1) : string
3455
    {
3456
        return implode(', ', array_fill(0, $numberOfPlaceholders, '?'));
3457
    }
3458
3459
    /**
3460
     * Format the table{Primary|Foreign}KeyName
3461
     *
3462
     * @param  string $pattern
3463
     * @param  string $tableName
3464
     * @return string
3465
     */
3466
    protected function formatKeyName(string $pattern, string $tableName) : string
3467
    {
3468
        return sprintf($pattern, $tableName);
3469
    }
3470
3471
    /**
3472
     * @param array $query
3473
     * @param array $extraFields
3474
     * @return array
3475
     * @throws \Exception
3476
     */
3477
    protected function prepareApiResource(array $query=[], array $extraFields=[]) : array
3478
    {
3479
        $this->defaultFilters()->filter($query)->paginate($query);
3480
        $pagingMetaData         = $this->getPagingMeta();
3481
        if ( $pagingMetaData['total'] === 0 )
3482
        {
3483
            return $pagingMetaData;
3484
        }
3485
        $this->withBelongsTo($pagingMetaData['fields']);
3486
        if ( ! empty($extraFields) )
3487
        {
3488
            $this->select($extraFields, '', false);
3489
        }
3490
        $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...
3491
        if ( ! empty($pagingMetaData['fields']) )
3492
        {
3493
            $this->removeUnrequestedFields($pagingMetaData['fields']);
3494
        }
3495
3496
        return $pagingMetaData;
3497
    }
3498
3499
    /**
3500
     * @param string $query
3501
     * @param array $parameters
3502
     *
3503
     * @return array
3504
     */
3505
    protected function logQuery(string $query, array $parameters) : array
3506
    {
3507
        $query                  = $this->buildQuery($query, $parameters);
3508
        if ( ! $this->logQueries )
3509
        {
3510
            return ['', ''];
3511
        }
3512
        $ident                  = substr(str_shuffle(md5($query)), 0, 10);
3513
        $this->getLogger()->debug($ident . ': ' . PHP_EOL . $query);
3514
        $this->timer['start']   = microtime(true);
3515
3516
        return [$query, $ident];
3517
    }
3518
3519
    /**
3520
     * @param string $ident
3521
     * @param string $builtQuery
3522
     */
3523
    protected function logSlowQueries(string $ident, string $builtQuery)
3524
    {
3525
        if ( ! $this->logQueries )
3526
        {
3527
            return ;
3528
        }
3529
        $this->timer['end']     = microtime(true);
3530
        $secondsTaken           = round($this->timer['end'] - $this->timer['start'], 3);
3531
        if ( $secondsTaken > $this->slowQuerySecs )
3532
        {
3533
            $this->getLogger()->warning("SLOW QUERY - {$ident} - {$secondsTaken} seconds:\n{$builtQuery}");
3534
        }
3535
    }
3536
3537
    /**
3538
     * @return float
3539
     */
3540
    public function getTimeTaken() : float
3541
    {
3542
        $secondsTaken           = $this->timer['end'] - $this->timer['start'];
3543
3544
        return (float)$secondsTaken;
3545
    }
3546
3547
    /**
3548
     * @param $secs
3549
     * @return FluentPdoModel|$this
3550
     */
3551
    public function slowQuerySeconds(int $secs) : FluentPdoModel
3552
    {
3553
        Assert($secs)->notEmpty("Seconds cannot be empty.")->numeric("Seconds must be numeric.");
3554
        $this->slowQuerySecs    = $secs;
3555
3556
        return $this;
3557
    }
3558
3559
3560
    /**
3561
     * @param       $field
3562
     * @param array $values
3563
     * @param string  $placeholderPrefix
3564
     *
3565
     * @return array
3566
     */
3567
    public function getNamedWhereIn(string $field, array $values, string $placeholderPrefix='') : array
3568
    {
3569
        Assert($field)->string()->notEmpty();
3570
        Assert($values)->isArray();
3571
3572
        if ( empty($values) )
3573
        {
3574
            return ['', []];
3575
        }
3576
        $placeholderPrefix      = $placeholderPrefix ?: strtolower(str_replace('.', '__', $field));
3577
        $params                 = [];
3578
        $placeholders           = [];
3579
        $count                  = 1;
3580
        foreach ( $values as $val )
3581
        {
3582
            $name                   = "{$placeholderPrefix}_{$count}";
3583
            $params[$name]          = $val;
3584
            $placeholders[]         = ":{$name}";
3585
            $count++;
3586
        }
3587
        $placeholders           = implode(',', $placeholders);
3588
3589
        return ["AND {$field} IN ({$placeholders})\n", $params];
3590
    }
3591
3592
    /**
3593
     * @param string $field
3594
     * @param string $delimiter
3595
     *
3596
     * @return array
3597
     */
3598
    protected function getColumnAliasParts(string $field, string $delimiter=':') : array
3599
    {
3600
        $parts                  = explode($delimiter, $field);
3601
        if ( count($parts) === 2 )
3602
        {
3603
            return $parts;
3604
        }
3605
        $parts                  = explode('.', $field);
3606
        if ( count($parts) === 2 )
3607
        {
3608
            return $parts;
3609
        }
3610
3611
        return ['', $field];
3612
    }
3613
3614
    /**
3615
     * @param string $column
3616
     * @param string $term
3617
     * @return FluentPdoModel|$this
3618
     */
3619
    protected function addWhereClause(string $column, string $term) : FluentPdoModel
3620
    {
3621
        /*
3622
3623
         whereLike          i.e ?name=whereLike(%terry%)
3624
         whereNotLike       i.e ?name=whereNotLike(%terry%)
3625
         whereLt            i.e ?age=whereLt(18)
3626
         whereLte           i.e ?age=whereLte(18)
3627
         whereGt            i.e ?event_dt=whereGt(2014-10-10)
3628
         whereGte           i.e ?event_dt=whereGte(2014-10-10)
3629
         whereBetween       i.e ?event_dt=whereBetween(2014-10-10,2014-10-15)
3630
         whereNotBetween    i.e ?event_dt=whereNotBetween(2014-10-10,2014-10-15)
3631
3632
         */
3633
        list ($func, $matches)  = $this->parseWhereClause($term);
3634
        switch ($func)
3635
        {
3636
            case 'whereLike':
3637
3638
                return $this->whereLike($column, $matches[0]);
3639
3640
            case 'whereNotLike':
3641
3642
                return $this->whereNotLike($column, $matches[0]);
3643
3644
            case 'whereLt':
3645
3646
                return $this->whereLt($column, $matches[0]);
3647
3648
            case 'whereLte':
3649
3650
                return $this->whereLte($column, $matches[0]);
3651
3652
            case 'whereGt':
3653
3654
                return $this->whereGt($column, $matches[0]);
3655
3656
            case 'whereGte':
3657
3658
                return $this->whereGte($column, $matches[0]);
3659
3660
            case 'whereBetween':
3661
3662
                return $this->whereBetween($column, $matches[0], $matches[1]);
3663
3664
            case 'whereNotBetween':
3665
3666
                return $this->whereNotBetween($column, $matches[0], $matches[1]);
3667
        }
3668
3669
        return $this->where($column, $term);
3670
    }
3671
3672
    /**
3673
     * @param string $term
3674
     * @return array
3675
     */
3676
    public function parseWhereClause(string $term) : array
3677
    {
3678
        $modifiers              = [
3679
            'whereLike'             => '/^whereLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3680
            'whereNotLike'          => '/^whereNotLike\(([%]?[ a-z0-9:-]+[%]?)\)$/i',
3681
            'whereLt'               => '/^whereLt\(([ a-z0-9:-]+)\)$/i',
3682
            'whereLte'              => '/^whereLte\(([ a-z0-9:-]+)\)$/i',
3683
            'whereGt'               => '/^whereGt\(([ a-z0-9:-]+)\)$/i',
3684
            'whereGte'              => '/^whereGte\(([ a-z0-9:-]+)\)$/i',
3685
            'whereBetween'          => '/^whereBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3686
            'whereNotBetween'       => '/^whereNotBetween\(([ a-z0-9:-]+),([ a-z0-9:-]+)\)$/i',
3687
        ];
3688
3689
        foreach ( $modifiers as $func => $regex )
3690
        {
3691
            if ( preg_match($regex, $term, $matches) )
3692
            {
3693
                array_shift($matches);
3694
3695
                return [$func, $matches];
3696
            }
3697
        }
3698
3699
        return ['', []];
3700
    }
3701
3702
    public function destroy()
3703
    {
3704
        if ( !is_null($this->pdoStmt) )
3705
        {
3706
            $this->pdoStmt->closeCursor();
3707
        }
3708
        $this->pdoStmt          = null;
3709
        $this->handlers         = [];
3710
    }
3711
3712
    public function __destruct()
3713
    {
3714
        $this->destroy();
3715
    }
3716
3717
    /**
3718
     * Load a model
3719
     *
3720
     * @param string $modelName
3721
     * @param AbstractPdo $connection
3722
     * @return FluentPdoModel|$this
3723
     * @throws ModelNotFoundException
3724
     */
3725
    public static function loadModel(string $modelName, AbstractPdo $connection=null) : FluentPdoModel
3726
    {
3727
        $modelName              = static::$modelNamespace . $modelName;
3728
        if ( ! class_exists($modelName) )
3729
        {
3730
            throw new ModelNotFoundException("Failed to find model class {$modelName}.");
3731
        }
3732
3733
        return new $modelName($connection);
3734
    }
3735
3736
    /**
3737
     * Load a model
3738
     *
3739
     * @param string      $tableName
3740
     * @param AbstractPdo $connection
3741
     * @return FluentPdoModel|$this
3742
     */
3743
    public static function loadTable(string $tableName, AbstractPdo $connection=null) : FluentPdoModel
3744
    {
3745
        $modelName              = Inflector::classify($tableName);
3746
        Assert($modelName)->notEmpty("Could not resolve model name from table name.");
3747
3748
        return static::loadModel($modelName, $connection);
3749
    }
3750
3751
    /**
3752
     * @param string   $columnName
3753
     * @param int $cacheTtl
3754
     * @param bool $flushCache
3755
     * @return bool
3756
     */
3757
    public function columnExists(string $columnName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3758
    {
3759
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3760
3761
        return array_key_exists($columnName, $columns);
3762
    }
3763
3764
    /**
3765
     * @param string   $foreignKeyName
3766
     * @param int $cacheTtl
3767
     * @param bool $flushCache
3768
     * @return bool
3769
     */
3770
    public function foreignKeyExists(string $foreignKeyName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3771
    {
3772
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3773
3774
        return array_key_exists($foreignKeyName, $columns);
3775
    }
3776
3777
    /**
3778
     * @param string   $indexName
3779
     * @param int $cacheTtl
3780
     * @param bool $flushCache
3781
     * @return bool
3782
     */
3783
    public function indexExists(string $indexName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3784
    {
3785
        Assert($indexName)->string()->notEmpty();
3786
3787
        $callback = function() use ($indexName) {
3788
3789
            $index = $this->execute("SHOW INDEX FROM {$this->tableName} WHERE Key_name = ':indexName'", compact('indexName'));
3790
3791
            return $index ? true : false;
3792
        };
3793
        if  ( $cacheTtl === self::CACHE_NO )
3794
        {
3795
            return $callback();
3796
        }
3797
        $cacheKey   = '/column_schema/' . $this->tableName . '/index/' . $indexName;
3798
        if ( $flushCache === true )
3799
        {
3800
            $this->clearCache($cacheKey);
3801
        }
3802
3803
        return (bool)$this->cacheData($cacheKey, $callback, $cacheTtl);
3804
    }
3805
3806
3807
3808
    /**
3809
     * @param int $cacheTtl
3810
     * @param bool $flushCache
3811
     * @return FluentPdoModel|$this
3812
     */
3813
    public function loadSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : FluentPdoModel
3814
    {
3815
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
3816
        $this->schema($schema);
3817
3818
        return $this;
3819
    }
3820
3821
    /**
3822
     * @param int $cacheTtl
3823
     * @param bool $flushCache
3824
     * @return Column[][]
3825
     */
3826
    public function getSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3827
    {
3828
        $table                  = $this->getTableName();
3829
        Assert($table)->string()->notEmpty();
3830
        $schema                 = [];
3831
        $columns                = $this->getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
3832
        foreach ( $columns[$table] as $column => $meta )
3833
        {
3834
            /** Column $meta */
3835
            $schema[$column]        = $meta->dataType;
3836
        }
3837
3838
        return $schema;
3839
    }
3840
3841
    /**
3842
     * @param int $cacheTtl
3843
     * @param bool $flushCache
3844
     * @return array
3845
     */
3846
    public function getForeignKeysFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3847
    {
3848
        $table                  = $this->getTableName();
3849
        Assert($table)->string()->notEmpty();
3850
        $schema                 = [];
3851
        $foreignKeys            = $this->getForeignKeysByTableFromDb($table, $cacheTtl, $flushCache);
3852
        foreach ( $foreignKeys[$table] as $key => $meta )
3853
        {
3854
            $schema[$key]           = $meta->dataType;
3855
        }
3856
3857
        return $schema;
3858
    }
3859
3860
    /**
3861
     * @param string $table
3862
     * @param int $cacheTtl
3863
     * @param bool $flushCache
3864
     * @return Column[][]
3865
     */
3866
    protected function getColumnsByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3867
    {
3868
        Assert($table)->string()->notEmpty();
3869
3870
        $callback = function() use ($table) {
3871
3872
            return $this->connection->getColumns(true, $table);
3873
        };
3874
        $cacheKey   = '/column_schema/' . $table;
3875
        if ( $flushCache === true )
3876
        {
3877
            $this->clearCache($cacheKey);
3878
        }
3879
3880
        return (array)$this->cacheData($cacheKey, $callback, $cacheTtl);
3881
    }
3882
3883
    /**
3884
     * @param string $table
3885
     * @param int $cacheTtl
3886
     * @param bool $flushCache
3887
     * @return Column[][]
3888
     */
3889
    protected function getForeignKeysByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3890
    {
3891
        Assert($table)->string()->notEmpty();
3892
3893
        $callback = function() use ($table) {
3894
3895
            return $this->connection->getForeignKeys($table);
3896
        };
3897
        $cacheKey   = '/foreign_keys_schema/' . $table;
3898
        if ( $flushCache === true )
3899
        {
3900
            $this->clearCache($cacheKey);
3901
        }
3902
3903
        return (array)$this->cacheData($cacheKey, $callback, $cacheTtl);
3904
    }
3905
3906
    /**
3907
     * @param string $table
3908
     * @return bool
3909
     */
3910
    public function clearSchemaCache(string $table) : bool
3911
    {
3912
        return $this->clearCache('/column_schema/' . $table);
3913
    }
3914
3915
    /**
3916
     * @param stdClass $record
3917
     * @return stdClass
3918
     */
3919
    public function cleanseRecord(stdClass $record) : stdClass
3920
    {
3921
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3922
        {
3923
            if ( is_string($record->{$field}) )
3924
            {
3925
                $sanitised          = filter_var($record->{$field}, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
3926
                $record->{$field}   = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", $sanitised);
3927
                if ( $this->logFilterChanges && $value !== $record->{$field} )
3928
                {
3929
                    $table              = $this->tableName ?: '';
3930
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->{$field}]);
3931
                }
3932
            }
3933
        }
3934
3935
        return $record;
3936
    }
3937
3938
    /**
3939
     * @param stdClass $record
3940
     * @param string   $type
3941
     * @return stdClass
3942
     */
3943
    public function beforeSave(stdClass $record, string $type) : stdClass
3944
    {
3945
        $record                 = $this->addDefaultFields($record, $type);
3946
        $record                 = $this->applyGlobalModifiers($record, $type);
3947
        $record                 = $this->applyHandlers($record, $type);
3948
        $record                 = $this->removeUnneededFields($record, $type);
3949
3950
        return $record;
3951
    }
3952
3953
    /**
3954
     * @param array $data
3955
     * @param string $saveType
3956
     * @return array
3957
     */
3958
    public function cleanseWebData(array $data, string $saveType) : array
3959
    {
3960
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
3961
        $columns = $this->getColumns(false);
3962
        if ( empty($columns) )
3963
        {
3964
            return $data;
3965
        }
3966
        foreach ( $data as $field => $val )
3967
        {
3968
            $data[$field] = empty($val) && $val !== 0 ? null : $val;
3969
        }
3970
3971
        return array_intersect_key($data, $columns);
3972
    }
3973
3974
    /**
3975
     * @return array
3976
     */
3977
    public function skeleton() : array
3978
    {
3979
        $skel                   = [];
3980
        $columns                = $this->getColumns(false);
3981
        foreach ( $columns as $column => $type )
3982
        {
3983
            $skel[$column]          = null;
3984
        }
3985
3986
        return $skel;
3987
    }
3988
3989
    /**
3990
     * @param bool $toString
3991
     * @return array
3992
     */
3993
    public function getErrors(bool $toString=false) : array
3994
    {
3995
        if ( ! $toString )
3996
        {
3997
            return $this->errors;
3998
        }
3999
        $errors                 = [];
4000
        foreach ( $this->errors as $field => $error )
4001
        {
4002
            $errors[]               = implode("\n", $error);
4003
        }
4004
4005
        return implode("\n", $errors);
4006
    }
4007
4008
    /**
4009
     * @param bool $throw
4010
     * @return FluentPdoModel|$this
4011
     */
4012
    public function validationExceptions(bool $throw=true) : FluentPdoModel
4013
    {
4014
        $this->validationExceptions = $throw;
4015
4016
        return $this;
4017
    }
4018
4019
    /**
4020
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
4021
     *
4022
     * @return FluentPdoModel|$this
4023
     * @throws Exception
4024
     */
4025
    public function paginate(array $query=[]) : FluentPdoModel
4026
    {
4027
        $_fields                = null;
4028
        $_order                 = null;
4029
        $_limit                 = null;
4030
        $_offset                = null;
4031
        extract($query);
4032
        $this->setLimit((int)$_limit, (int)$_offset);
4033
        $this->setOrderBy((string)$_order);
4034
        $_fields                = is_array($_fields) ? $_fields : (string)$_fields;
4035
        $_fields                = empty($_fields) ? [] : $_fields;
4036
        $_fields                = is_string($_fields) ? explode('|', $_fields) : $_fields;
4037
        $_fields                = empty($_fields) ? [] : $_fields;
4038
        $this->setFields(is_array($_fields) ? $_fields : explode('|', (string)$_fields));
4039
4040
        return $this;
4041
    }
4042
4043
    /**
4044
     * @param int $limit
4045
     * @param int $offset
4046
     * @return FluentPdoModel|$this
4047
     */
4048
    protected function setLimit(int $limit=0, int $offset=0) : FluentPdoModel
4049
    {
4050
        $limit                  = ! $limit || (int)$limit > (int)$this->defaultMax ? (int)$this->defaultMax : (int)$limit;
4051
        if ( ! is_numeric($limit) )
4052
        {
4053
            return $this;
4054
        }
4055
        $this->limit((int)$limit);
4056
        if ( $offset && is_numeric($offset) )
4057
        {
4058
            $this->offset((int)$offset);
4059
        }
4060
4061
        return $this;
4062
    }
4063
4064
    /**
4065
     * @param array $fields
4066
     * @return FluentPdoModel|$this
4067
     * @throws Exception
4068
     */
4069
    protected function setFields(array $fields=[]) : FluentPdoModel
4070
    {
4071
        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...
4072
        {
4073
            return $this;
4074
        }
4075
        $this->explicitSelectMode();
4076
        $columns                = $this->getColumns();
4077
4078
        foreach ( $fields as $idx => $field )
4079
        {
4080
            list($alias, $field)    = $this->getColumnAliasParts($field);
4081
            $field = $field === '_display_field' ? $this->displayColumn : $field;
4082
            // Regular primary table field
4083
            if ( ( empty($alias) || $alias === $this->tableAlias ) && in_array($field, $columns) )
4084
            {
4085
                $this->select("{$this->tableAlias}.{$field}");
4086
                $this->requestedFields[] = "{$this->tableAlias}.{$field}";
4087
4088
                continue;
4089
            }
4090
            // Reference table field with alias
4091
            if ( ! empty($alias) )
4092
            {
4093
                Assert($this->associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
4094
                Assert($field)->eq($this->associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
4095
                list(, , $joinField, $fieldAlias) = $this->associations['belongsTo'][$alias];
4096
                $this->autoJoin($alias, static::LEFT_JOIN, false);
4097
                $this->select($joinField, $fieldAlias);
4098
                $this->requestedFields[] = $fieldAlias;
4099
4100
                continue;
4101
            }
4102
            // Reference table select field without alias
4103
            $belongsTo              = array_key_exists('belongsTo', $this->associations) ?  $this->associations['belongsTo'] : [];
4104
            foreach ( $belongsTo as $joinAlias => $config )
4105
            {
4106
                list(, , $joinField, $fieldAlias) = $config;
4107
                if ( $field === $fieldAlias )
4108
                {
4109
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
4110
                    $this->select($joinField, $fieldAlias);
4111
                    $this->requestedFields[] = $fieldAlias;
4112
4113
                    continue;
4114
                }
4115
            }
4116
        }
4117
4118
        return $this;
4119
    }
4120
4121
    /**
4122
     * @param string $orderBy
4123
     * @return FluentPdoModel|$this|FluentPdoModel
4124
     */
4125
    protected function setOrderBy(string $orderBy='') : FluentPdoModel
4126
    {
4127
        if ( ! $orderBy )
4128
        {
4129
            return $this;
4130
        }
4131
        $columns                = $this->getColumns();
4132
        list($order, $direction)= strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
4133
        list($alias, $field)    = $this->getColumnAliasParts(trim($order), '.');
4134
        $field                  = explode(' ', $field);
4135
        $field                  = trim($field[0]);
4136
        $direction              = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
4137
        $belongsTo              = array_key_exists('belongsTo', $this->associations) ? $this->associations['belongsTo'] : [];
4138
        // Regular primary table order by
4139
        if ( ( empty($alias) || $alias === $this->tableAlias ) && in_array($field, $columns) )
4140
        {
4141
            return $this->orderBy("{$this->tableAlias}.{$field}", $direction);
4142
        }
4143
        // Reference table order by with alias
4144
        if ( ! empty($alias) )
4145
        {
4146
            Assert($belongsTo)->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
4147
            Assert($field)->eq($belongsTo[$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
4148
4149
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
4150
        }
4151
        // Reference table order by without alias
4152
        foreach ( $belongsTo as $joinAlias => $config )
4153
        {
4154
            if ( $field === $config[3] )
4155
            {
4156
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
4157
            }
4158
        }
4159
4160
        return $this;
4161
    }
4162
4163
    /**
4164
     * @return array
4165
     */
4166
    public function getPagingMeta()
4167
    {
4168
        if ( empty($this->pagingMeta) )
4169
        {
4170
            $this->setPagingMeta();
4171
        }
4172
4173
        return $this->pagingMeta;
4174
    }
4175
4176
    /**
4177
     * @return FluentPdoModel|$this
4178
     */
4179
    public function setPagingMeta() : FluentPdoModel
4180
    {
4181
        $model                  = clone $this;
4182
        $limit                  = intval($this->getLimit());
4183
        $offset                 = intval($this->getOffset());
4184
        $total                  = intval($model->withBelongsTo()->select('')->offset(0)->limit(0)->orderBy()->count());
4185
        unset($model->handlers, $model); //hhmv mem leak
4186
        $orderBys               = ! is_array($this->orderBy) ? [] : $this->orderBy;
4187
        $this->pagingMeta       = [
4188
            'limit'                 => $limit,
4189
            'offset'                => $offset,
4190
            'page'                  => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
4191
            'pages'                 => $limit === 0 ? 1 : intval(ceil($total / $limit)),
4192
            'order'                 => $orderBys,
4193
            'total'                 => $total = $limit === 1 && $total > 1 ? 1 : $total,
4194
            'filters'               => $this->filterMeta,
4195
            'fields'                => $this->requestedFields,
4196
            'perms'                 => [],
4197
        ];
4198
4199
        return $this;
4200
    }
4201
4202
    /**
4203
     * Take a web request and format a query
4204
     *
4205
     * @param array $query
4206
     *
4207
     * @return FluentPdoModel|$this
4208
     * @throws Exception
4209
     */
4210
    public function filter(array $query=[]) : FluentPdoModel
4211
    {
4212
        $columns                = $this->getColumns(false);
4213
        $alias                  = '';
4214
        foreach ( $query as $column => $value )
4215
        {
4216
            if ( in_array($column, $this->paginationAttribs) )
4217
            {
4218
                continue;
4219
            }
4220
            $field              = $this->findFieldByQuery($column, $this->displayColumn);
4221
            if ( is_null($field) )
4222
            {
4223
                continue;
4224
            }
4225
            $this->filterMeta[$field]   = $value;
4226
            $where                      = ! is_array($value) && mb_stripos((string)$value, '|') !== false ? explode('|', $value) : $value;
4227
            if ( is_array($where) )
4228
            {
4229
                $this->whereIn($field, $where);
4230
            }
4231
            else
4232
            {
4233
                $this->addWhereClause($field, (string)$where);
4234
            }
4235
        }
4236
        if ( empty($query['_search']) )
4237
        {
4238
            return $this;
4239
        }
4240
        $alias                  = ! empty($alias) ? $alias : $this->tableAlias;
4241
        $stringCols             = array_filter($columns, function($type) {
4242
4243
            return in_array($type, ['varchar', 'text', 'enum']);
4244
        });
4245
        $terms                  = explode('|', $query['_search']);
4246
        $whereLikes             = [];
4247
        foreach ( $stringCols as $column => $type )
4248
        {
4249
            if ( in_array($column, $this->excludedSearchCols) )
4250
            {
4251
                continue;
4252
            }
4253
            foreach ( $terms as $term )
4254
            {
4255
                $whereLikes["{$alias}.{$column}"] = "%{$term}%";
4256
            }
4257
        }
4258
        // Reference fields...
4259
        $belongsTo = $this->getSearchableAssociations();
4260
        foreach ( $belongsTo as $alias => $config )
4261
        {
4262
            foreach ( $terms as $term )
4263
            {
4264
                $whereLikes[$config[2]] = "%{$term}%";
4265
            }
4266
        }
4267
        if ( empty($whereLikes) )
4268
        {
4269
            return $this;
4270
        }
4271
        $this->where('1', '1')->wrap()->_and();
4272
        foreach ( $whereLikes as $column => $term )
4273
        {
4274
            $this->_or()->whereLike($column, $term);
4275
        }
4276
        $this->wrap();
4277
4278
        return $this;
4279
    }
4280
4281
    /**
4282
     * @param string $column
4283
     * @param string $displayCol
4284
     * @return string|null
4285
     */
4286
    protected function findFieldByQuery(string $column, string $displayCol)
4287
    {
4288
        list($alias, $field)    = $this->getColumnAliasParts($column);
4289
        $field                  = $field === '_display_field' ? $displayCol : $field;
4290
        $columns                = $this->getColumns();
4291
        $tableAlias             = $this->getTableAlias();
4292
        if ( ! empty($alias) && $alias === $tableAlias )
4293
        {
4294
            // Alias is set but the field isn't correct
4295
            if ( ! in_array($field, $columns) )
4296
            {
4297
                return null;
4298
            }
4299
            return "{$alias}.{$field}";
4300
        }
4301
        // Alias isn't passed in but the field is ok
4302
        if ( empty($alias) && in_array($field, $columns) )
4303
        {
4304
            return "{$tableAlias}.{$field}";
4305
        }
4306
//        // Alias is passed but not this table in but there is a matching field on this table
4307
//        if ( empty($alias) ) //&& in_array($field, $columns) )
4308
//        {
4309
//            return null;
4310
//        }
4311
        // Now search the associations for the field
4312
        $associations = $this->getSearchableAssociations();
4313
        if ( ! empty($alias) )
4314
        {
4315
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
4316
            {
4317
                return "{$alias}.{$field}";
4318
            }
4319
4320
            return null;
4321
        }
4322
        foreach ( $associations as $assocAlias => $config )
4323
        {
4324
            list(, , $assocField, $fieldAlias) = $config;
4325
            if ( $fieldAlias === $field )
4326
            {
4327
                return $assocField;
4328
            }
4329
        }
4330
4331
        return null;
4332
    }
4333
4334
    /**
4335
     * @param string $field
4336
     * @param mixed $value
4337
     * @param array $pdoMetaData
4338
     * @return float|int
4339
     * @throws Exception
4340
     */
4341
    protected function fixTypeToSentinel(string $field, $value, array $pdoMetaData=[])
4342
    {
4343
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4344
4345
        $fieldType              = strtolower($pdoMetaData['native_type'] ??''?: '');
4346
        if ( ! $fieldType )
4347
        {
4348
            $schema                 = $this->getColumns();
4349
            if ( empty($schema) )
4350
            {
4351
                return $value;
4352
            }
4353
            $columns                = $this->getColumns(false);
4354
            Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4355
4356
            $fieldType              = $columns[$field] ?: null;
4357
        }
4358
4359
4360
        // Don't cast invalid values... only those that can be cast cleanly
4361
        switch ( $fieldType )
4362
        {
4363
            case 'varchar':
4364
            case 'var_string':
4365
            case 'string':
4366
            case 'text';
4367
            case 'date':
4368
            case 'datetime':
4369
            case 'timestamp':
4370
            case 'blob':
4371
4372
                return (string)$value;
4373
4374
            case 'int':
4375
            case 'integer':
4376
            case 'tinyint':
4377
            case 'tiny':
4378
            case 'long':
4379
            case 'longlong':
4380
4381
                return (int)$value;
4382
4383
            case 'decimal':
4384
            case 'float':
4385
            case 'double':
4386
            case 'newdecimal':
4387
4388
                return (float)$value;
4389
4390
            default:
4391
4392
                Assert(false)->true('Unknown type: ' . $fieldType);
4393
4394
                return $value;
4395
        }
4396
    }
4397
4398
    /**
4399
     * @param string $field
4400
     * @param mixed $value
4401
     * @param bool|false $permissive
4402
     * @return float|int|null|string
4403
     */
4404
    protected function fixType(string $field, $value, bool $permissive=false)
4405
    {
4406
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4407
        $schema                 = $this->getColumns();
4408
        if ( empty($schema) || ( ! array_key_exists($field, $schema) && $permissive ) )
4409
        {
4410
            return $value;
4411
        }
4412
        $columns                = $this->getColumns(false);
4413
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4414
4415
        $fieldType              = ! empty($columns[$field]) ? $columns[$field] : null;
4416
4417
        if ( is_null($value) )
4418
        {
4419
            return null;
4420
        }
4421
        // return on null, '' but not 0
4422
        if ( ! is_numeric($value) && empty($value) )
4423
        {
4424
            return null;
4425
        }
4426
        // Don't cast invalid values... only those that can be cast cleanly
4427
        switch ( $fieldType )
4428
        {
4429
            case 'varchar':
4430
            case 'text';
4431
            case 'date':
4432
            case 'datetime':
4433
            case 'timestamp':
4434
4435
                // return on null, '' but not 0
4436
                return ! is_numeric($value) && empty($value) ? null : (string)$value;
4437
4438
            case 'int':
4439
4440
                if ( $field === 'id' || substr($field, -3) === '_id' )
4441
                {
4442
                    return $value ? (int)$value : null;
4443
                }
4444
4445
                return ! is_numeric($value) ? null : (int)$value;
4446
4447
            case 'decimal':
4448
4449
                return ! is_numeric($value) ? null : (float)$value;
4450
4451
            default:
4452
4453
                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...
4454
        }
4455
    }
4456
4457
    /**
4458
     * @param stdClass $record
4459
     * @param string $type
4460
     * @return stdClass
4461
     */
4462
    public function fixTypesToSentinel(stdClass $record, string $type='') : stdClass
4463
    {
4464
        foreach ( $this->rowMetaData as $column => $pdoMetaData )
4465
        {
4466
            if ( ! property_exists($record, $column) )
4467
            {
4468
                continue;
4469
            }
4470
            $record->{$column}      = $this->fixTypeToSentinel($column, $record->{$column}, $pdoMetaData);
4471
        }
4472
        // PDO might not be able to generate the meta data to sniff types
4473
        if ( ! $this->rowMetaData )
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->rowMetaData 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...
4474
        {
4475
            foreach ( $this->getColumns(false) as $column => $fieldType )
4476
            {
4477
                if ( ! property_exists($record, $column) )
4478
                {
4479
                    continue;
4480
                }
4481
                $record->{$column}      = $this->fixTypeToSentinel($column, $record->{$column});
4482
            }
4483
        }
4484
4485
        unset($type);
4486
4487
        return $record;
4488
    }
4489
4490
    /**
4491
     * @param stdClass $record
4492
     * @param string   $type
4493
     * @return stdClass
4494
     * @throws Exception
4495
     */
4496
    public function applyHandlers(stdClass $record, string $type='INSERT') : stdClass
4497
    {
4498
        $this->compileHandlers();
4499
        $this->errors               = [];
4500
        // Disable per field exceptions so we can capture all errors for the record
4501
        $tmpExceptions              = $this->validationExceptions;
4502
        $this->validationExceptions = false;
4503
        foreach ( $this->handlers as $field => $fnValidator )
4504
        {
4505
            if ( ! property_exists($record, $field) )
4506
            {
4507
                // If the operation is an update it can be a partial update
4508
                if ( $type === self::SAVE_UPDATE )
4509
                {
4510
                    continue;
4511
                }
4512
                $record->{$field}       = null;
4513
            }
4514
            $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...
4515
        }
4516
        $this->validationExceptions = $tmpExceptions;
4517
        if ( $this->validationExceptions && ! empty($this->errors) )
4518
        {
4519
            throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4520
        }
4521
4522
        return $record;
4523
    }
4524
4525
4526
    /**
4527
     * @param stdClass $record
4528
     * @param array    $fields
4529
     * @param string   $type
4530
     * @return bool
4531
     */
4532
    protected function uniqueCheck(stdClass $record, array $fields, string $type) : bool
4533
    {
4534
        if ( $type === self::SAVE_UPDATE )
4535
        {
4536
            $this->whereNot($this->primaryKey, $record->{$this->primaryKey});
4537
        }
4538
        foreach ( $fields as $field )
4539
        {
4540
            $this->where($field, $record->{$field});
4541
        }
4542
4543
        return (int)$this->count() > 0;
4544
    }
4545
4546
    /**
4547
     * @param string $field
4548
     * @param mixed $value
4549
     * @param string $type
4550
     * @param stdClass $record
4551
     * @return null
4552
     * @throws Exception
4553
     */
4554
    protected function applyHandler(string $field, $value, string $type='', stdClass $record=null)
4555
    {
4556
        $this->compileHandlers();
4557
        $fnHandler              = ! empty($this->handlers[$field]) ? $this->handlers[$field] : null;
4558
        if ( is_callable($fnHandler) )
4559
        {
4560
            try
4561
            {
4562
                $value                  = $fnHandler($field, $value, $type, $record);
4563
            }
4564
            catch( Exception $e )
4565
            {
4566
                $this->errors[$field][]     = $e->getMessage();
4567
                if ( $this->validationExceptions && ! empty($this->errors) )
4568
                {
4569
                    throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4570
                }
4571
4572
                return null;
4573
            }
4574
        }
4575
4576
        return $value;
4577
    }
4578
4579
    /**
4580
     * @param string $start
4581
     * @param string $end
4582
     * @param string $hayStack
4583
     * @return mixed
4584
     */
4585
    public static function between(string $start, string $end, string $hayStack) : string
4586
    {
4587
        return static::before($end, static::after($start, $hayStack));
4588
    }
4589
4590
    /**
4591
     * @param string     $needle
4592
     * @param string     $hayStack
4593
     * @param bool $returnOrigIfNeedleNotExists
4594
     * @return mixed
4595
     */
4596
    public static function before(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4597
    {
4598
        $result = mb_substr($hayStack, 0, mb_strpos($hayStack, $needle));
4599
        if ( !$result && $returnOrigIfNeedleNotExists )
4600
        {
4601
            return $hayStack;
4602
        }
4603
4604
        return $result;
4605
    }
4606
4607
    /**
4608
     * @param string     $needle
4609
     * @param string     $hayStack
4610
     * @param bool $returnOrigIfNeedleNotExists
4611
     * @return string
4612
     */
4613
    public static function after(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4614
    {
4615
        if ( ! is_bool(mb_strpos($hayStack, $needle)) )
4616
        {
4617
            return mb_substr($hayStack, mb_strpos($hayStack, $needle) + mb_strlen($needle));
4618
        }
4619
4620
        return $returnOrigIfNeedleNotExists ? $hayStack : '';
4621
    }
4622
4623
    /**
4624
     * @return int
4625
     */
4626
    public function getUserId()
4627
    {
4628
        return 0;
4629
    }
4630
4631
    /**
4632
     * @param string $entity
4633
     * @param int $id
4634
     * @return int
4635
     */
4636
    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...
4637
    {
4638
        return 31;
4639
    }
4640
4641
    /**
4642
     * @param string|int|null $time
4643
     * @return string
4644
     */
4645
    public static function date($time=null) : string
4646
    {
4647
        return date('Y-m-d', static::getTime($time));
4648
    }
4649
4650
    /**
4651
     * @param string|int|null $time
4652
     * @return string
4653
     */
4654
    public static function dateTime($time=null) : string
4655
    {
4656
        return date('Y-m-d H:i:s', static::getTime($time));
4657
    }
4658
4659
    /**
4660
     * @param string|int|null $time
4661
     * @return string
4662
     */
4663
    public static function atom($time=null) : string
4664
    {
4665
        return date('Y-m-d\TH:i:sP', static::getTime($time));
4666
    }
4667
4668
    /**
4669
     * @param string|int|null $time
4670
     * @return int
4671
     */
4672
    public static function getTime($time=null) : int
4673
    {
4674
        if ( ! $time )
4675
        {
4676
            return time();
4677
        }
4678
        if ( is_int($time) )
4679
        {
4680
            return $time;
4681
        }
4682
4683
        return strtotime($time);
4684
    }
4685
4686
    /**
4687
     * @param int $id
4688
     * @param int $cacheTtl
4689
     * @return string
4690
     */
4691
    public function getCodeById(int $id, int $cacheTtl=self::ONE_DAY) : string
4692
    {
4693
        Assert($id)->id();
4694
        $code   = $this->defaultFilters()->fetchStr($this->getDisplayColumn(), $id, $cacheTtl);
4695
        Assert($code)->notEmpty();
4696
4697
        return $code;
4698
    }
4699
4700
    /**
4701
     * @param array $authUserRoles
4702
     * @param int   $authUserId
4703
     * @return FluentPdoModel
4704
     */
4705
    public function applyRoleFilter(array $authUserRoles, int $authUserId) : FluentPdoModel
0 ignored issues
show
Unused Code introduced by
The parameter $authUserRoles is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $authUserId is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
4706
    {
4707
        return $this;
4708
    }
4709
4710
    /**
4711
     * @param int    $id
4712
     * @param string[] $authUserRoles
4713
     * @param int    $authUserId
4714
     * @return bool
4715
     */
4716
    public function canAccessIdWithRole(int $id, array $authUserRoles, int $authUserId) : bool
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $authUserRoles is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $authUserId is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
4717
    {
4718
        return true;
4719
    }
4720
}
4721