FluentPdoModel   D
last analyzed

Complexity

Total Complexity 685

Size/Duplication

Total Lines 4721
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
dl 0
loc 4721
rs 4.4102
c 0
b 0
f 0
wmc 685
lcom 2
cbo 9

235 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A init() 0 2 1
A getPdo() 0 4 1
A getLogger() 0 4 1
A getCache() 0 4 1
A table() 0 8 1
A primaryKeyName() 0 6 1
A tableName() 0 6 1
A explicitSelectMode() 0 6 1
A filterOnFetch() 0 6 1
A logFilterChanges() 0 6 1
A getTableName() 0 4 1
A getDisplayColumn() 0 4 1
A displayColumn() 0 6 1
A tableAlias() 0 6 1
A cacheTtl() 0 11 3
A getTableAlias() 0 4 1
A associations() 0 6 1
A setBelongsTo() 0 9 1
A setBelongsToDisplayField() 0 10 1
B fetchRow() 0 23 5
A getColumnMeta() 0 19 4
A schema() 0 6 1
A addSchema() 0 16 3
A getSchema() 0 11 2
A getColumns() 0 6 2
A getPrimaryKeyName() 0 4 1
C execute() 0 36 7
A query() 0 7 1
C buildQuery() 0 28 8
A trimAndLowerCaseKeys() 0 11 2
A rowCount() 0 6 2
A fetchStmt() 0 9 2
A fetchSqlQuery() 0 10 1
B fetchIntoMemoryTable() 0 22 4
C fetch() 0 38 8
B parseWhereForPrimaryLookup() 0 16 6
C cacheData() 0 38 8
A clearCache() 0 4 1
A clearCacheByTable() 0 10 3
A getFlushCacheTables() 0 4 2
A fetchCallback() 0 11 2
A fetchObjectsByCallback() 0 18 4
A maxCallbackFailures() 0 7 1
B tallySuccessCount() 0 33 6
A canGenericUpdate() 0 4 1
A canGenericCreate() 0 4 1
A canGenericDelete() 0 4 1
C fetchList() 0 62 8
A fetchColumn() 0 10 3
B fetchField() 0 21 6
A fetchStr() 0 4 1
A fetchId() 0 4 1
A fetchInt() 0 4 1
A fetchFloat() 0 4 1
A fetchBool() 0 4 1
A fetchOne() 0 12 3
A fetchExists() 0 10 2
C select() 0 43 12
A selectRaw() 0 6 1
A logQueries() 0 6 1
A includeCount() 0 6 1
A distinct() 0 6 1
A withBelongsTo() 0 14 4
A autoInnerJoin() 0 4 1
B autoJoin() 0 27 5
A whereArr() 0 9 2
C where() 0 49 11
A _and() 0 14 2
A _or() 0 14 2
A wrap() 0 10 1
A wherePk() 0 6 3
A whereDisplayName() 0 6 3
A whereNot() 0 4 1
A whereCoercedNot() 0 4 1
A whereLike() 0 4 1
A whereBetween() 0 7 3
A whereNotBetween() 0 7 3
A whereRegex() 0 4 1
A whereNotRegex() 0 4 1
A whereNotLike() 0 4 1
A whereGt() 0 4 1
A whereGte() 0 4 1
A whereLt() 0 4 1
A whereLte() 0 4 1
A whereIn() 0 4 1
A whereNotIn() 0 6 1
A whereNull() 0 4 1
A whereNotNull() 0 4 1
A having() 0 9 1
A orderBy() 0 14 2
A groupBy() 0 10 3
A limit() 0 9 2
A getLimit() 0 4 1
A offset() 0 6 1
A getOffset() 0 4 1
B join() 0 19 6
A leftJoin() 0 4 1
F getSelectQuery() 0 43 12
A getFieldComment() 0 4 1
D prepareColumns() 0 30 9
C getWhereString() 0 33 11
B getHavingString() 0 19 5
A getWhereParameters() 0 6 1
A insertArr() 0 4 1
A insert() 0 23 3
A getLastInsertId() 0 4 2
A insertSqlQuery() 0 20 3
B upsert() 0 22 4
D upsertOne() 0 40 13
A upsertArr() 0 4 1
B update() 0 23 4
A updateArr() 0 4 1
A updateField() 0 14 4
A updateChanged() 0 14 3
A updateByExpression() 0 6 1
A rawUpdate() 0 9 1
A updateSqlQuery() 0 9 1
C updateSql() 0 33 8
B delete() 0 21 6
A isSoftDelete() 0 4 1
A truncate() 0 14 3
A deleteSqlQuery() 0 12 2
A count() 0 20 4
A max() 0 6 1
A min() 0 6 1
A sum() 0 6 1
A avg() 0 6 1
B reset() 0 28 1
A removeUnauthorisedFields() 0 4 1
D getFieldHandlers() 0 100 13
A begin() 0 14 2
A commit() 0 18 4
A rollback() 0 14 2
A applyGlobalModifiers() 0 13 3
C removeUnneededFields() 0 31 11
A setById() 0 20 4
A resolveId() 0 11 1
A fetchApiResource() 0 13 2
B fetchApiResources() 0 22 5
A getSearchableAssociations() 0 7 2
A removeUnrequestedFields() 0 11 3
C removeFields() 0 26 7
A defaultFilters() 0 4 1
A allowMetaColumnOverride() 0 6 1
A skipMetaUpdates() 0 6 1
A addUpdateAlias() 0 6 1
A onFetch() 0 12 2
A gzEncodeData() 0 9 2
A gzDecodeData() 0 10 2
A hasGzipPrefix() 0 4 2
A fixTimestamps() 0 12 4
A setMaxRecords() 0 7 1
B afterSave() 0 21 5
B addDefaultFields() 0 33 4
A createTable() 0 4 1
A dropTable() 0 4 1
A compileHandlers() 0 9 2
A getViewColumns() 0 4 1
A getDisplayNameById() 0 10 1
A validIdDisplayNameCombo() 0 4 1
A getEmptyObject() 0 6 1
A emptyObject() 0 6 1
A isId() 0 4 1
A activeCount() 0 4 1
A whereActive() 0 4 1
A whereInactive() 0 4 1
A whereArchived() 0 4 1
A whereStatus() 0 9 3
A updateActive() 0 10 2
A updateInactive() 0 9 2
A updateNow() 0 6 1
A updateToday() 0 6 1
A updateDeleted() 0 13 2
A isDeleterTableType() 0 10 4
A updateArchived() 0 10 2
A updateStatus() 0 6 1
A NOW() 0 4 2
A makePlaceholders() 0 4 1
A formatKeyName() 0 4 1
A prepareApiResource() 0 21 4
A logQuery() 0 13 2
A logSlowQueries() 0 13 3
A getTimeTaken() 0 6 1
A slowQuerySeconds() 0 7 1
B getNamedWhereIn() 0 24 4
A getColumnAliasParts() 0 15 3
C addWhereClause() 0 52 9
B parseWhereClause() 0 25 3
A destroy() 0 9 2
A __destruct() 0 4 1
A loadModel() 0 10 2
A loadTable() 0 7 1
A columnExists() 0 6 1
A typeExists() 0 4 1
A foreignKeyExists() 0 6 1
B indexExists() 0 22 4
A loadSchemaFromDb() 0 7 1
A getSchemaFromDb() 0 14 2
A getForeignKeysFromDb() 0 13 2
A getColumnsByTableFromDb() 0 16 2
A getForeignKeysByTableFromDb() 0 16 2
A clearSchemaCache() 0 4 1
B cleanseRecord() 0 18 6
A beforeSave() 0 9 1
B cleanseWebData() 0 16 6
A skeleton() 0 11 2
A getErrors() 0 14 3
A validationExceptions() 0 6 1
B paginate() 0 17 6
B setLimit() 0 15 6
C setFields() 0 51 11
C setOrderBy() 0 37 11
A getPagingMeta() 0 9 2
B setPagingMeta() 0 22 6
F filter() 0 70 16
C findFieldByQuery() 0 47 12
C fixTypeToSentinel() 0 56 23
C fixType() 0 52 22
B fixTypesToSentinel() 0 27 6
B applyHandlers() 0 28 6
A uniqueCheck() 0 13 3
B applyHandler() 0 24 6
A between() 0 4 1
A before() 0 10 3
A after() 0 9 3
A getUserId() 0 4 1
A getMaskByResourceAndId() 0 4 1
A date() 0 4 1
A dateTime() 0 4 1
A atom() 0 4 1
A getTime() 0 13 3
A getCodeById() 0 8 1
A applyRoleFilter() 0 4 1
A canAccessIdWithRole() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like FluentPdoModel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FluentPdoModel, and based on these observations, apply Extract Interface, too.

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

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
3793
    }
3794
3795
    /**
3796
     * @param string   $foreignKeyName
3797
     * @param int $cacheTtl
3798
     * @param bool $flushCache
3799
     * @return bool
3800
     */
3801
    public function foreignKeyExists(string $foreignKeyName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3802
    {
3803
        $columns = $this->getSchemaFromDb($cacheTtl, $flushCache);
3804
3805
        return array_key_exists($foreignKeyName, $columns);
3806
    }
3807
3808
    /**
3809
     * @param string   $indexName
3810
     * @param int $cacheTtl
3811
     * @param bool $flushCache
3812
     * @return bool
3813
     */
3814
    public function indexExists(string $indexName, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : bool
3815
    {
3816
        Assert($indexName)->string()->notEmpty();
3817
3818
        $callback = function() use ($indexName) {
3819
3820
            $index = $this->execute("SHOW INDEX FROM {$this->tableName} WHERE Key_name = ':indexName'", compact('indexName'));
3821
3822
            return $index ? true : false;
3823
        };
3824
        if  ( $cacheTtl === self::CACHE_NO )
3825
        {
3826
            return $callback();
3827
        }
3828
        $cacheKey   = '/column_schema/' . $this->tableName . '/index/' . $indexName;
3829
        if ( $flushCache === true )
3830
        {
3831
            $this->clearCache($cacheKey);
3832
        }
3833
3834
        return (bool)$this->cacheData($cacheKey, $callback, $cacheTtl);
3835
    }
3836
3837
3838
3839
    /**
3840
     * @param int $cacheTtl
3841
     * @param bool $flushCache
3842
     * @return FluentPdoModel|$this
3843
     */
3844
    public function loadSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : FluentPdoModel
3845
    {
3846
        $schema = $this->getSchemaFromDb($cacheTtl, $flushCache);
3847
        $this->schema($schema);
3848
3849
        return $this;
3850
    }
3851
3852
    /**
3853
     * @param int $cacheTtl
3854
     * @param bool $flushCache
3855
     * @return Column[][]
3856
     */
3857
    public function getSchemaFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3858
    {
3859
        $table                  = $this->getTableName();
3860
        Assert($table)->string()->notEmpty();
3861
        $schema                 = [];
3862
        $columns                = $this->getColumnsByTableFromDb($table, $cacheTtl, $flushCache);
3863
        foreach ( $columns[$table] as $column => $meta )
3864
        {
3865
            /** Column $meta */
3866
            $schema[$column]        = $meta->dataType;
3867
        }
3868
3869
        return $schema;
3870
    }
3871
3872
    /**
3873
     * @param int $cacheTtl
3874
     * @param bool $flushCache
3875
     * @return array
3876
     */
3877
    public function getForeignKeysFromDb(int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3878
    {
3879
        $table                  = $this->getTableName();
3880
        Assert($table)->string()->notEmpty();
3881
        $schema                 = [];
3882
        $foreignKeys            = $this->getForeignKeysByTableFromDb($table, $cacheTtl, $flushCache);
3883
        foreach ( $foreignKeys[$table] as $key => $meta )
3884
        {
3885
            $schema[$key]           = $meta->dataType;
3886
        }
3887
3888
        return $schema;
3889
    }
3890
3891
    /**
3892
     * @param string $table
3893
     * @param int $cacheTtl
3894
     * @param bool $flushCache
3895
     * @return Column[][]
3896
     */
3897
    protected function getColumnsByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3898
    {
3899
        Assert($table)->string()->notEmpty();
3900
3901
        $callback = function() use ($table) {
3902
3903
            return $this->connection->getColumns(true, $table);
3904
        };
3905
        $cacheKey   = '/column_schema/' . $table;
3906
        if ( $flushCache === true )
3907
        {
3908
            $this->clearCache($cacheKey);
3909
        }
3910
3911
        return (array)$this->cacheData($cacheKey, $callback, $cacheTtl);
3912
    }
3913
3914
    /**
3915
     * @param string $table
3916
     * @param int $cacheTtl
3917
     * @param bool $flushCache
3918
     * @return Column[][]
3919
     */
3920
    protected function getForeignKeysByTableFromDb(string $table, int $cacheTtl=self::CACHE_NO, bool $flushCache=false) : array
3921
    {
3922
        Assert($table)->string()->notEmpty();
3923
3924
        $callback = function() use ($table) {
3925
3926
            return $this->connection->getForeignKeys($table);
3927
        };
3928
        $cacheKey   = '/foreign_keys_schema/' . $table;
3929
        if ( $flushCache === true )
3930
        {
3931
            $this->clearCache($cacheKey);
3932
        }
3933
3934
        return (array)$this->cacheData($cacheKey, $callback, $cacheTtl);
3935
    }
3936
3937
    /**
3938
     * @param string $table
3939
     * @return bool
3940
     */
3941
    public function clearSchemaCache(string $table) : bool
3942
    {
3943
        return $this->clearCache('/column_schema/' . $table);
3944
    }
3945
3946
    /**
3947
     * @param stdClass $record
3948
     * @return stdClass
3949
     */
3950
    public function cleanseRecord(stdClass $record) : stdClass
3951
    {
3952
        foreach ( $record as $field => $value )
0 ignored issues
show
Bug introduced by
The expression $record of type object<stdClass> is not traversable.
Loading history...
3953
        {
3954
            if ( is_string($record->{$field}) )
3955
            {
3956
                $sanitised          = filter_var($record->{$field}, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
3957
                $record->{$field}   = str_replace(["\r\n", "\\r\\n", "\\n"], "\n", $sanitised);
3958
                if ( $this->logFilterChanges && $value !== $record->{$field} )
3959
                {
3960
                    $table              = $this->tableName ?: '';
3961
                    $this->getLogger()->debug("Field {$table}.{$field} has been cleansed", ['old' => $value, 'new' => $record->{$field}]);
3962
                }
3963
            }
3964
        }
3965
3966
        return $record;
3967
    }
3968
3969
    /**
3970
     * @param stdClass $record
3971
     * @param string   $type
3972
     * @return stdClass
3973
     */
3974
    public function beforeSave(stdClass $record, string $type) : stdClass
3975
    {
3976
        $record                 = $this->addDefaultFields($record, $type);
3977
        $record                 = $this->applyGlobalModifiers($record, $type);
3978
        $record                 = $this->applyHandlers($record, $type);
3979
        $record                 = $this->removeUnneededFields($record, $type);
3980
3981
        return $record;
3982
    }
3983
3984
    /**
3985
     * @param array  $data
3986
     * @param string $saveType
3987
     * @param array  $preserve
3988
     * @return array
3989
     * @throws \Terah\Assert\AssertionFailedException
3990
     */
3991
    public function cleanseWebData(array $data, string $saveType, array $preserve=[]) : array
3992
    {
3993
        Assert($saveType)->inArray([self::SAVE_UPDATE, self::SAVE_INSERT]);
3994
        $columns                = $this->getColumns(false);
3995
        if ( empty($columns) )
3996
        {
3997
            return $data;
3998
        }
3999
        foreach ( $data as $field => $val )
4000
        {
4001
            $data[$field]           = empty($val) && $val !== 0 && ! is_array($val) ? null : $val;
4002
        }
4003
        $columns                = array_merge($columns, array_flip($preserve));
4004
4005
        return array_intersect_key($data, $columns);
4006
    }
4007
4008
    /**
4009
     * @return array
4010
     */
4011
    public function skeleton() : array
4012
    {
4013
        $skel                   = [];
4014
        $columns                = $this->getColumns(false);
4015
        foreach ( $columns as $column => $type )
4016
        {
4017
            $skel[$column]          = null;
4018
        }
4019
4020
        return $skel;
4021
    }
4022
4023
    /**
4024
     * @param bool $toString
4025
     * @return array
4026
     */
4027
    public function getErrors(bool $toString=false) : array
4028
    {
4029
        if ( ! $toString )
4030
        {
4031
            return $this->errors;
4032
        }
4033
        $errors                 = [];
4034
        foreach ( $this->errors as $field => $error )
4035
        {
4036
            $errors[]               = implode("\n", $error);
4037
        }
4038
4039
        return implode("\n", $errors);
4040
    }
4041
4042
    /**
4043
     * @param bool $throw
4044
     * @return FluentPdoModel|$this
4045
     */
4046
    public function validationExceptions(bool $throw=true) : FluentPdoModel
4047
    {
4048
        $this->validationExceptions = $throw;
4049
4050
        return $this;
4051
    }
4052
4053
    /**
4054
     * @param array $query array('_limit' => int, '_offset' => int, '_order' => string, '_fields' => string, _search)
4055
     *
4056
     * @return FluentPdoModel|$this
4057
     * @throws Exception
4058
     */
4059
    public function paginate(array $query=[]) : FluentPdoModel
4060
    {
4061
        $_fields                = null;
4062
        $_order                 = null;
4063
        $_limit                 = null;
4064
        $_offset                = null;
4065
        extract($query);
4066
        $this->setLimit((int)$_limit, (int)$_offset);
4067
        $this->setOrderBy((string)$_order);
4068
        $_fields                = is_array($_fields) ? $_fields : (string)$_fields;
4069
        $_fields                = empty($_fields) ? [] : $_fields;
4070
        $_fields                = is_string($_fields) ? explode('|', $_fields) : $_fields;
4071
        $_fields                = empty($_fields) ? [] : $_fields;
4072
        $this->setFields(is_array($_fields) ? $_fields : explode('|', (string)$_fields));
4073
4074
        return $this;
4075
    }
4076
4077
    /**
4078
     * @param int $limit
4079
     * @param int $offset
4080
     * @return FluentPdoModel|$this
4081
     */
4082
    protected function setLimit(int $limit=0, int $offset=0) : FluentPdoModel
4083
    {
4084
        $limit                  = ! $limit || (int)$limit > (int)$this->defaultMax ? (int)$this->defaultMax : (int)$limit;
4085
        if ( ! is_numeric($limit) )
4086
        {
4087
            return $this;
4088
        }
4089
        $this->limit((int)$limit);
4090
        if ( $offset && is_numeric($offset) )
4091
        {
4092
            $this->offset((int)$offset);
4093
        }
4094
4095
        return $this;
4096
    }
4097
4098
    /**
4099
     * @param array $fields
4100
     * @return FluentPdoModel|$this
4101
     * @throws Exception
4102
     */
4103
    protected function setFields(array $fields=[]) : FluentPdoModel
4104
    {
4105
        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...
4106
        {
4107
            return $this;
4108
        }
4109
        $this->explicitSelectMode();
4110
        $columns                = $this->getColumns();
4111
4112
        foreach ( $fields as $idx => $field )
4113
        {
4114
            list($alias, $field)    = $this->getColumnAliasParts($field);
4115
            $field = $field === '_display_field' ? $this->displayColumn : $field;
4116
            // Regular primary table field
4117
            if ( ( empty($alias) || $alias === $this->tableAlias ) && in_array($field, $columns) )
4118
            {
4119
                $this->select("{$this->tableAlias}.{$field}");
4120
                $this->requestedFields[] = "{$this->tableAlias}.{$field}";
4121
4122
                continue;
4123
            }
4124
            // Reference table field with alias
4125
            if ( ! empty($alias) )
4126
            {
4127
                Assert($this->associations['belongsTo'])->keyExists($alias, "Invalid table alias ({$alias}) specified for the field query");
4128
                Assert($field)->eq($this->associations['belongsTo'][$alias][3], "Invalid field ({$alias}.{$field}) specified for the field query");
4129
                list(, , $joinField, $fieldAlias) = $this->associations['belongsTo'][$alias];
4130
                $this->autoJoin($alias, static::LEFT_JOIN, false);
4131
                $this->select($joinField, $fieldAlias);
4132
                $this->requestedFields[] = $fieldAlias;
4133
4134
                continue;
4135
            }
4136
            // Reference table select field without alias
4137
            $belongsTo              = array_key_exists('belongsTo', $this->associations) ?  $this->associations['belongsTo'] : [];
4138
            foreach ( $belongsTo as $joinAlias => $config )
4139
            {
4140
                list(, , $joinField, $fieldAlias) = $config;
4141
                if ( $field === $fieldAlias )
4142
                {
4143
                    $this->autoJoin($joinAlias, static::LEFT_JOIN, false);
4144
                    $this->select($joinField, $fieldAlias);
4145
                    $this->requestedFields[] = $fieldAlias;
4146
4147
                    continue;
4148
                }
4149
            }
4150
        }
4151
4152
        return $this;
4153
    }
4154
4155
    /**
4156
     * @param string $orderBy
4157
     * @return FluentPdoModel|$this|FluentPdoModel
4158
     */
4159
    protected function setOrderBy(string $orderBy='') : FluentPdoModel
4160
    {
4161
        if ( ! $orderBy )
4162
        {
4163
            return $this;
4164
        }
4165
        $columns                = $this->getColumns();
4166
        list($order, $direction)= strpos($orderBy, ',') !== false ? explode(',', $orderBy) : [$orderBy, 'ASC'];
4167
        list($alias, $field)    = $this->getColumnAliasParts(trim($order), '.');
4168
        $field                  = explode(' ', $field);
4169
        $field                  = trim($field[0]);
4170
        $direction              = ! in_array(strtoupper(trim($direction)), ['ASC', 'DESC']) ? 'ASC' : strtoupper(trim($direction));
4171
        $belongsTo              = array_key_exists('belongsTo', $this->associations) ? $this->associations['belongsTo'] : [];
4172
        // Regular primary table order by
4173
        if ( ( empty($alias) || $alias === $this->tableAlias ) && in_array($field, $columns) )
4174
        {
4175
            return $this->orderBy("{$this->tableAlias}.{$field}", $direction);
4176
        }
4177
        // Reference table order by with alias
4178
        if ( ! empty($alias) )
4179
        {
4180
            Assert($belongsTo)->keyExists($alias, "Invalid table alias ({$alias}) specified for the order query");
4181
            Assert($field)->eq($belongsTo[$alias][3], "Invalid field ({$alias}.{$field}) specified for the order query");
4182
4183
            return $this->autoJoin($alias)->orderBy("{$alias}.{$field}", $direction);
4184
        }
4185
        // Reference table order by without alias
4186
        foreach ( $belongsTo as $joinAlias => $config )
4187
        {
4188
            if ( $field === $config[3] )
4189
            {
4190
                return $this->autoJoin($joinAlias)->orderBy($config[2], $direction);
4191
            }
4192
        }
4193
4194
        return $this;
4195
    }
4196
4197
    /**
4198
     * @return array
4199
     */
4200
    public function getPagingMeta()
4201
    {
4202
        if ( empty($this->pagingMeta) )
4203
        {
4204
            $this->setPagingMeta();
4205
        }
4206
4207
        return $this->pagingMeta;
4208
    }
4209
4210
    /**
4211
     * @return FluentPdoModel|$this
4212
     */
4213
    public function setPagingMeta() : FluentPdoModel
4214
    {
4215
        $model                  = clone $this;
4216
        $limit                  = intval($this->getLimit());
4217
        $offset                 = intval($this->getOffset());
4218
        $total                  = intval($model->withBelongsTo()->select('')->offset(0)->limit(0)->orderBy()->count());
4219
        unset($model->handlers, $model); //hhmv mem leak
4220
        $orderBys               = ! is_array($this->orderBy) ? [] : $this->orderBy;
4221
        $this->pagingMeta       = [
4222
            'limit'                 => $limit,
4223
            'offset'                => $offset,
4224
            'page'                  => $offset === 0 ? 1 : intval( $offset / $limit ) + 1,
4225
            'pages'                 => $limit === 0 ? 1 : intval(ceil($total / $limit)),
4226
            'order'                 => $orderBys,
4227
            'total'                 => $total = $limit === 1 && $total > 1 ? 1 : $total,
4228
            'filters'               => $this->filterMeta,
4229
            'fields'                => $this->requestedFields,
4230
            'perms'                 => [],
4231
        ];
4232
4233
        return $this;
4234
    }
4235
4236
    /**
4237
     * Take a web request and format a query
4238
     *
4239
     * @param array $query
4240
     *
4241
     * @return FluentPdoModel|$this
4242
     * @throws Exception
4243
     */
4244
    public function filter(array $query=[]) : FluentPdoModel
4245
    {
4246
        $columns                = $this->getColumns(false);
4247
        $alias                  = '';
4248
        foreach ( $query as $column => $value )
4249
        {
4250
            if ( in_array($column, $this->paginationAttribs) )
4251
            {
4252
                continue;
4253
            }
4254
            $field              = $this->findFieldByQuery($column, $this->displayColumn);
4255
            if ( is_null($field) )
4256
            {
4257
                continue;
4258
            }
4259
            $this->filterMeta[$field]   = $value;
4260
            $where                      = ! is_array($value) && mb_stripos((string)$value, '|') !== false ? explode('|', $value) : $value;
4261
            if ( is_array($where) )
4262
            {
4263
                $this->whereIn($field, $where);
4264
            }
4265
            else
4266
            {
4267
                $this->addWhereClause($field, (string)$where);
4268
            }
4269
        }
4270
        if ( empty($query['_search']) )
4271
        {
4272
            return $this;
4273
        }
4274
        $alias                  = ! empty($alias) ? $alias : $this->tableAlias;
4275
        $stringCols             = array_filter($columns, function($type) {
4276
4277
            return in_array($type, ['varchar', 'text', 'enum']);
4278
        });
4279
        $terms                  = explode('|', $query['_search']);
4280
        $whereLikes             = [];
4281
        foreach ( $stringCols as $column => $type )
4282
        {
4283
            if ( in_array($column, $this->excludedSearchCols) )
4284
            {
4285
                continue;
4286
            }
4287
            foreach ( $terms as $term )
4288
            {
4289
                $whereLikes["{$alias}.{$column}"] = "%{$term}%";
4290
            }
4291
        }
4292
        // Reference fields...
4293
        $belongsTo = $this->getSearchableAssociations();
4294
        foreach ( $belongsTo as $alias => $config )
4295
        {
4296
            foreach ( $terms as $term )
4297
            {
4298
                $whereLikes[$config[2]] = "%{$term}%";
4299
            }
4300
        }
4301
        if ( empty($whereLikes) )
4302
        {
4303
            return $this;
4304
        }
4305
        $this->where('1', '1')->wrap()->_and();
4306
        foreach ( $whereLikes as $column => $term )
4307
        {
4308
            $this->_or()->whereLike($column, $term);
4309
        }
4310
        $this->wrap();
4311
4312
        return $this;
4313
    }
4314
4315
    /**
4316
     * @param string $column
4317
     * @param string $displayCol
4318
     * @return string|null
4319
     */
4320
    protected function findFieldByQuery(string $column, string $displayCol)
4321
    {
4322
        list($alias, $field)    = $this->getColumnAliasParts($column);
4323
        $field                  = $field === '_display_field' ? $displayCol : $field;
4324
        $columns                = $this->getColumns();
4325
        $tableAlias             = $this->getTableAlias();
4326
        if ( ! empty($alias) && $alias === $tableAlias )
4327
        {
4328
            // Alias is set but the field isn't correct
4329
            if ( ! in_array($field, $columns) )
4330
            {
4331
                return null;
4332
            }
4333
            return "{$alias}.{$field}";
4334
        }
4335
        // Alias isn't passed in but the field is ok
4336
        if ( empty($alias) && in_array($field, $columns) )
4337
        {
4338
            return "{$tableAlias}.{$field}";
4339
        }
4340
//        // Alias is passed but not this table in but there is a matching field on this table
4341
//        if ( empty($alias) ) //&& in_array($field, $columns) )
4342
//        {
4343
//            return null;
4344
//        }
4345
        // Now search the associations for the field
4346
        $associations = $this->getSearchableAssociations();
4347
        if ( ! empty($alias) )
4348
        {
4349
            if ( array_key_exists($alias, $associations) && $associations[$alias][3] === $field )
4350
            {
4351
                return "{$alias}.{$field}";
4352
            }
4353
4354
            return null;
4355
        }
4356
        foreach ( $associations as $assocAlias => $config )
4357
        {
4358
            list(, , $assocField, $fieldAlias) = $config;
4359
            if ( $fieldAlias === $field )
4360
            {
4361
                return $assocField;
4362
            }
4363
        }
4364
4365
        return null;
4366
    }
4367
4368
    /**
4369
     * @param string $field
4370
     * @param mixed $value
4371
     * @param array $pdoMetaData
4372
     * @return float|int
4373
     * @throws Exception
4374
     */
4375
    protected function fixTypeToSentinel(string $field, $value, array $pdoMetaData=[])
4376
    {
4377
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4378
4379
        $fieldType              = strtolower($pdoMetaData['native_type'] ??''?: '');
4380
        if ( ! $fieldType )
4381
        {
4382
            $schema                 = $this->getColumns();
4383
            if ( empty($schema) )
4384
            {
4385
                return $value;
4386
            }
4387
            $columns                = $this->getColumns(false);
4388
            Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4389
4390
            $fieldType              = $columns[$field] ?: null;
4391
        }
4392
4393
4394
        // Don't cast invalid values... only those that can be cast cleanly
4395
        switch ( $fieldType )
4396
        {
4397
            case 'varchar':
4398
            case 'var_string':
4399
            case 'string':
4400
            case 'text';
4401
            case 'date':
4402
            case 'datetime':
4403
            case 'timestamp':
4404
            case 'blob':
4405
4406
                return (string)$value;
4407
4408
            case 'int':
4409
            case 'integer':
4410
            case 'tinyint':
4411
            case 'tiny':
4412
            case 'long':
4413
            case 'longlong':
4414
4415
                return (int)$value;
4416
4417
            case 'decimal':
4418
            case 'float':
4419
            case 'double':
4420
            case 'newdecimal':
4421
4422
                return (float)$value;
4423
4424
            default:
4425
4426
                Assert(false)->true('Unknown type: ' . $fieldType);
4427
4428
                return $value;
4429
        }
4430
    }
4431
4432
    /**
4433
     * @param string $field
4434
     * @param mixed $value
4435
     * @param bool|false $permissive
4436
     * @return float|int|null|string
4437
     */
4438
    protected function fixType(string $field, $value, bool $permissive=false)
4439
    {
4440
        Assert($value)->nullOr()->scalar("var is type of " . gettype($value));
4441
        $schema                 = $this->getColumns();
4442
        if ( empty($schema) || ( ! array_key_exists($field, $schema) && $permissive ) )
4443
        {
4444
            return $value;
4445
        }
4446
        $columns                = $this->getColumns(false);
4447
        Assert($columns)->keyExists($field, "The property {$field} does not exist.");
4448
4449
        $fieldType              = ! empty($columns[$field]) ? $columns[$field] : null;
4450
4451
        if ( is_null($value) )
4452
        {
4453
            return null;
4454
        }
4455
        // return on null, '' but not 0
4456
        if ( ! is_numeric($value) && empty($value) )
4457
        {
4458
            return null;
4459
        }
4460
        // Don't cast invalid values... only those that can be cast cleanly
4461
        switch ( $fieldType )
4462
        {
4463
            case 'varchar':
4464
            case 'text';
4465
            case 'date':
4466
            case 'datetime':
4467
            case 'timestamp':
4468
4469
                // return on null, '' but not 0
4470
                return ! is_numeric($value) && empty($value) ? null : (string)$value;
4471
4472
            case 'int':
4473
4474
                if ( $field === 'id' || substr($field, -3) === '_id' )
4475
                {
4476
                    return $value ? (int)$value : null;
4477
                }
4478
4479
                return ! is_numeric($value) ? null : (int)$value;
4480
4481
            case 'decimal':
4482
4483
                return ! is_numeric($value) ? null : (float)$value;
4484
4485
            default:
4486
4487
                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...
4488
        }
4489
    }
4490
4491
    /**
4492
     * @param stdClass $record
4493
     * @param string $type
4494
     * @return stdClass
4495
     */
4496
    public function fixTypesToSentinel(stdClass $record, string $type='') : stdClass
4497
    {
4498
        foreach ( $this->rowMetaData as $column => $pdoMetaData )
4499
        {
4500
            if ( ! property_exists($record, $column) )
4501
            {
4502
                continue;
4503
            }
4504
            $record->{$column}      = $this->fixTypeToSentinel($column, $record->{$column}, $pdoMetaData);
4505
        }
4506
        // PDO might not be able to generate the meta data to sniff types
4507
        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...
4508
        {
4509
            foreach ( $this->getColumns(false) as $column => $fieldType )
4510
            {
4511
                if ( ! property_exists($record, $column) )
4512
                {
4513
                    continue;
4514
                }
4515
                $record->{$column}      = $this->fixTypeToSentinel($column, $record->{$column});
4516
            }
4517
        }
4518
4519
        unset($type);
4520
4521
        return $record;
4522
    }
4523
4524
    /**
4525
     * @param stdClass $record
4526
     * @param string   $type
4527
     * @return stdClass
4528
     * @throws Exception
4529
     */
4530
    public function applyHandlers(stdClass $record, string $type='INSERT') : stdClass
4531
    {
4532
        $this->compileHandlers();
4533
        $this->errors               = [];
4534
        // Disable per field exceptions so we can capture all errors for the record
4535
        $tmpExceptions              = $this->validationExceptions;
4536
        $this->validationExceptions = false;
4537
        foreach ( $this->handlers as $field => $fnValidator )
4538
        {
4539
            if ( ! property_exists($record, $field) )
4540
            {
4541
                // If the operation is an update it can be a partial update
4542
                if ( $type === self::SAVE_UPDATE )
4543
                {
4544
                    continue;
4545
                }
4546
                $record->{$field}       = null;
4547
            }
4548
            $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...
4549
        }
4550
        $this->validationExceptions = $tmpExceptions;
4551
        if ( $this->validationExceptions && ! empty($this->errors) )
4552
        {
4553
            throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4554
        }
4555
4556
        return $record;
4557
    }
4558
4559
4560
    /**
4561
     * @param stdClass $record
4562
     * @param array    $fields
4563
     * @param string   $type
4564
     * @return bool
4565
     */
4566
    protected function uniqueCheck(stdClass $record, array $fields, string $type) : bool
4567
    {
4568
        if ( $type === self::SAVE_UPDATE )
4569
        {
4570
            $this->whereNot($this->primaryKey, $record->{$this->primaryKey});
4571
        }
4572
        foreach ( $fields as $field )
4573
        {
4574
            $this->where($field, $record->{$field});
4575
        }
4576
4577
        return (int)$this->count() > 0;
4578
    }
4579
4580
    /**
4581
     * @param string $field
4582
     * @param mixed $value
4583
     * @param string $type
4584
     * @param stdClass $record
4585
     * @return null
4586
     * @throws Exception
4587
     */
4588
    protected function applyHandler(string $field, $value, string $type='', stdClass $record=null)
4589
    {
4590
        $this->compileHandlers();
4591
        $fnHandler              = ! empty($this->handlers[$field]) ? $this->handlers[$field] : null;
4592
        if ( is_callable($fnHandler) )
4593
        {
4594
            try
4595
            {
4596
                $value                  = $fnHandler($field, $value, $type, $record);
4597
            }
4598
            catch( Exception $e )
4599
            {
4600
                $this->errors[$field][]     = $e->getMessage();
4601
                if ( $this->validationExceptions && ! empty($this->errors) )
4602
                {
4603
                    throw new ModelFailedValidationException("Validation of data failed", $this->getErrors(), 422);
4604
                }
4605
4606
                return null;
4607
            }
4608
        }
4609
4610
        return $value;
4611
    }
4612
4613
    /**
4614
     * @param string $start
4615
     * @param string $end
4616
     * @param string $hayStack
4617
     * @return mixed
4618
     */
4619
    public static function between(string $start, string $end, string $hayStack) : string
4620
    {
4621
        return static::before($end, static::after($start, $hayStack));
4622
    }
4623
4624
    /**
4625
     * @param string     $needle
4626
     * @param string     $hayStack
4627
     * @param bool $returnOrigIfNeedleNotExists
4628
     * @return mixed
4629
     */
4630
    public static function before(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4631
    {
4632
        $result = mb_substr($hayStack, 0, mb_strpos($hayStack, $needle));
4633
        if ( !$result && $returnOrigIfNeedleNotExists )
4634
        {
4635
            return $hayStack;
4636
        }
4637
4638
        return $result;
4639
    }
4640
4641
    /**
4642
     * @param string     $needle
4643
     * @param string     $hayStack
4644
     * @param bool $returnOrigIfNeedleNotExists
4645
     * @return string
4646
     */
4647
    public static function after(string $needle, string $hayStack, bool $returnOrigIfNeedleNotExists=false) : string
4648
    {
4649
        if ( ! is_bool(mb_strpos($hayStack, $needle)) )
4650
        {
4651
            return mb_substr($hayStack, mb_strpos($hayStack, $needle) + mb_strlen($needle));
4652
        }
4653
4654
        return $returnOrigIfNeedleNotExists ? $hayStack : '';
4655
    }
4656
4657
    /**
4658
     * @return int
4659
     */
4660
    public function getUserId()
4661
    {
4662
        return 0;
4663
    }
4664
4665
    /**
4666
     * @param string $entity
4667
     * @param int $id
4668
     * @return int
4669
     */
4670
    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...
4671
    {
4672
        return 31;
4673
    }
4674
4675
    /**
4676
     * @param string|int|null $time
4677
     * @return string
4678
     */
4679
    public static function date($time=null) : string
4680
    {
4681
        return date('Y-m-d', static::getTime($time));
4682
    }
4683
4684
    /**
4685
     * @param string|int|null $time
4686
     * @return string
4687
     */
4688
    public static function dateTime($time=null) : string
4689
    {
4690
        return date('Y-m-d H:i:s', static::getTime($time));
4691
    }
4692
4693
    /**
4694
     * @param string|int|null $time
4695
     * @return string
4696
     */
4697
    public static function atom($time=null) : string
4698
    {
4699
        return date('Y-m-d\TH:i:sP', static::getTime($time));
4700
    }
4701
4702
    /**
4703
     * @param string|int|null $time
4704
     * @return int
4705
     */
4706
    public static function getTime($time=null) : int
4707
    {
4708
        if ( ! $time )
4709
        {
4710
            return time();
4711
        }
4712
        if ( is_int($time) )
4713
        {
4714
            return $time;
4715
        }
4716
4717
        return strtotime($time);
4718
    }
4719
4720
    /**
4721
     * @param int $id
4722
     * @param int $cacheTtl
4723
     * @return string
4724
     */
4725
    public function getCodeById(int $id, int $cacheTtl=self::ONE_DAY) : string
4726
    {
4727
        Assert($id)->id();
4728
        $code   = $this->defaultFilters()->fetchStr($this->getDisplayColumn(), $id, $cacheTtl);
4729
        Assert($code)->notEmpty();
4730
4731
        return $code;
4732
    }
4733
4734
    /**
4735
     * @param array $authUserRoles
4736
     * @param int   $authUserId
4737
     * @return FluentPdoModel
4738
     */
4739
    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...
4740
    {
4741
        return $this;
4742
    }
4743
4744
    /**
4745
     * @param int    $id
4746
     * @param string[] $authUserRoles
4747
     * @param int    $authUserId
4748
     * @return bool
4749
     */
4750
    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...
4751
    {
4752
        return true;
4753
    }
4754
}
4755