• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rotexsoft / leanorm / 21295498656

23 Jan 2026 05:41PM UTC coverage: 95.613% (-0.06%) from 95.677%
21295498656

push

github

rotexdegba
Minimum PHP 8.2 refactoring

1482 of 1550 relevant lines covered (95.61%)

171.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.37
/src/LeanOrm/Model.php
1
<?php
2
declare(strict_types=1);
3
namespace LeanOrm;
4

5
use Aura\SqlQuery\QueryFactory;
6
use Rotexsoft\SqlSchema\ColumnFactory;
7
use LeanOrm\Utils;
8
use Psr\Log\LoggerInterface;
9
use function sprintf;
10

11
/**
12
 * Supported PDO drivers: mysql, pgsql, sqlite and sqlsrv
13
 *
14
 * @author Rotimi Adegbamigbe
15
 * @copyright (c) 2024, Rotexsoft
16
 */
17
class Model extends \GDAO\Model implements \Stringable {
18
    
19
    //overriden parent's properties
20
    /**
21
     * Name of the collection class for this model. 
22
     * Must be a descendant of \GDAO\Model\Collection
23
     */
24
    protected ?string $collection_class_name = \LeanOrm\Model\Collection::class;
25

26
    /**
27
     * Name of the record class for this model. 
28
     * Must be a descendant of \GDAO\Model\Record
29
     */
30
    protected ?string $record_class_name = \LeanOrm\Model\Record::class;
31

32
    /////////////////////////////////////////////////////////////////////////////
33
    // Properties declared here are specific to \LeanOrm\Model and its kids //
34
    /////////////////////////////////////////////////////////////////////////////
35

36
    /**
37
     * Name of the pdo driver currently being used.
38
     * It must be one of the values returned by $this->getPDO()->getAvailableDrivers()
39
     */
40
    protected string $pdo_driver_name = '';
41

42
    public function getPdoDriverName(): string {
43
        
44
        return $this->pdo_driver_name;
1,308✔
45
    }
46

47
    /**
48
     *  An object for interacting with the db
49
     */
50
    protected \LeanOrm\DBConnector $db_connector;
51

52
    // Query Logging related properties
53
    protected bool $can_log_queries = false;
54

55
    public function canLogQueries(): bool { return $this->can_log_queries; }
56

57
    /** @psalm-suppress PossiblyUnusedMethod */
58
    public function enableQueryLogging(): static {
59

60
        $this->can_log_queries = true;
48✔
61
        return $this;
48✔
62
    }
63
    
64
    /** @psalm-suppress PossiblyUnusedMethod */
65
    public function disableQueryLogging(): static {
66

67
        $this->can_log_queries = false;
40✔
68
        return $this;
40✔
69
    }
70

71
    /**
72
     * @var array<string, array>
73
     */
74
    protected array $query_log = [];
75

76
    /**
77
     * @var array<string, array>
78
     */
79
    protected static array $all_instances_query_log = [];
80

81
    protected ?LoggerInterface $logger = null;
82

83
    /** @psalm-suppress PossiblyUnusedMethod */
84
    public function setLogger(?LoggerInterface $logger): static {
85
        
86
        $this->logger = $logger;
144✔
87
        return $this;
144✔
88
    }
89
    
90
    public function getLogger(): ?LoggerInterface { return $this->logger; }
91
    
92
    /**
93
     * {@inheritDoc}
94
     * 
95
     * @psalm-suppress MixedPropertyFetch
96
     */
97
    public function __construct(
98
        string $dsn = '', 
99
        string $username = '', 
100
        #[\SensitiveParameter] string $passwd = '', 
101
        array $pdo_driver_opts = [],
102
        string $primary_col_name='',
103
        string $table_name=''
104
    ) {
105
        $pri_col_not_set_exception_msg = '';
1,308✔
106

107
        try {
108

109
            parent::__construct($dsn, $username, $passwd, $pdo_driver_opts, $primary_col_name, $table_name);
1,308✔
110

111
        } catch (\GDAO\ModelPrimaryColNameNotSetDuringConstructionException $e) {
44✔
112

113
            //$this->primary_col (primary key colun has not yet been set)
114
            //hold this exception for later if necessary
115
            $pri_col_not_set_exception_msg = $e->getMessage();
36✔
116
        }
117
        
118
        DBConnector::configure($dsn, null, $dsn);//use $dsn as connection name in 3rd parameter
1,308✔
119
        DBConnector::configure('username', $username, $dsn);//use $dsn as connection name in 3rd parameter
1,308✔
120
        DBConnector::configure('password', $passwd, $dsn);//use $dsn as connection name in 3rd parameter
1,308✔
121

122
        if( $pdo_driver_opts !== [] ) {
1,308✔
123

124
            DBConnector::configure( 'driver_options', $pdo_driver_opts, $dsn);//use $dsn as connection name in 3rd parameter
8✔
125
        }
126

127
        $this->db_connector = DBConnector::create($dsn);//use $dsn as connection name
1,308✔
128
        
129
        /** @psalm-suppress MixedAssignment */
130
        $this->pdo_driver_name = $this->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME);
1,308✔
131
        $this->pdoServerVersionCheck();
1,308✔
132

133
        ////////////////////////////////////////////////////////
134
        //Get and Set Table Schema Meta Data if Not Already Set
135
        ////////////////////////////////////////////////////////
136
        if ( $this->table_cols === [] ) {
1,308✔
137

138
            /** @var array $dsn_n_tname_to_schema_def_map */
139
            static $dsn_n_tname_to_schema_def_map;
1,308✔
140

141
            if( !$dsn_n_tname_to_schema_def_map ) {
1,308✔
142

143
                $dsn_n_tname_to_schema_def_map = [];
4✔
144
            }
145

146
            if( array_key_exists($dsn.$this->getTableName(), $dsn_n_tname_to_schema_def_map) ) {
1,308✔
147

148
                // use cached schema definition for the dsn and table name combo
149
                /** @psalm-suppress MixedAssignment */
150
                $schema_definitions = $dsn_n_tname_to_schema_def_map[$dsn.$this->getTableName()];
1,304✔
151

152
            } else {
153

154
                // let's make sure that $this->getTableName() is an actual table / view in the db
155
                if( !$this->tableExistsInDB($this->getTableName()) ) {
44✔
156

157
                    $msg = "ERROR: Table name `{$this->getTableName()}` supplied to " 
8✔
158
                            . static::class . '::' . __FUNCTION__ . '(...)'
8✔
159
                            . ' does not exist as a table or view in the database';
8✔
160
                    throw new \LeanOrm\Exceptions\BadModelTableNameException($msg);
8✔
161
                }
162

163
                $this->table_cols = [];
36✔
164
                $schema_definitions = $this->fetchTableColsFromDB($this->getTableName());
36✔
165

166
                // cache schema definition for the current dsn and table combo
167
                $dsn_n_tname_to_schema_def_map[$dsn.$this->getTableName()] = $schema_definitions;
36✔
168

169
            } // if( array_key_exists($dsn.$this->getTableName(), $dsn_n_tname_to_schema_def_map) )
170

171
            if( 
172
                $primary_col_name !== ''
1,308✔
173
                && !$this->columnExistsInDbTable($this->getTableName(), $primary_col_name) 
1,308✔
174
            ) {
175
                $msg = "ERROR: The Primary Key column name `{$primary_col_name}` supplied to " 
8✔
176
                        . static::class . '::' . __FUNCTION__ . '(...)'
8✔
177
                        . " does not exist as an actual column in the supplied table `{$this->getTableName()}`.";
8✔
178
                throw new \LeanOrm\Exceptions\BadModelPrimaryColumnNameException($msg);
8✔
179
            }
180

181
            /** @psalm-suppress MixedAssignment */
182
            foreach( $schema_definitions as $colname => $metadata_obj ) {
1,308✔
183

184
                /** @psalm-suppress MixedArrayOffset */
185
                $this->table_cols[$colname] = [];
1,308✔
186
                /** @psalm-suppress MixedArrayOffset */
187
                $this->table_cols[$colname]['name'] = $metadata_obj->name;
1,308✔
188
                /** @psalm-suppress MixedArrayOffset */
189
                $this->table_cols[$colname]['type'] = $metadata_obj->type;
1,308✔
190
                /** @psalm-suppress MixedArrayOffset */
191
                $this->table_cols[$colname]['size'] = $metadata_obj->size;
1,308✔
192
                /** @psalm-suppress MixedArrayOffset */
193
                $this->table_cols[$colname]['scale'] = $metadata_obj->scale;
1,308✔
194
                /** @psalm-suppress MixedArrayOffset */
195
                $this->table_cols[$colname]['notnull'] = $metadata_obj->notnull;
1,308✔
196
                /** @psalm-suppress MixedArrayOffset */
197
                $this->table_cols[$colname]['default'] = $metadata_obj->default;
1,308✔
198
                /** @psalm-suppress MixedArrayOffset */
199
                $this->table_cols[$colname]['autoinc'] = $metadata_obj->autoinc;
1,308✔
200
                /** @psalm-suppress MixedArrayOffset */
201
                $this->table_cols[$colname]['primary'] = $metadata_obj->primary;
1,308✔
202

203
                if( $this->getPrimaryCol() === '' && $metadata_obj->primary ) {
1,308✔
204

205
                    //this is a primary column
206
                    /** @psalm-suppress MixedArgument */
207
                    $this->setPrimaryCol($metadata_obj->name);
20✔
208

209
                } // $this->getPrimaryCol() === '' && $metadata_obj->primary
210
            } // foreach( $schema_definitions as $colname => $metadata_obj )
211
            
212
        } else { // $this->table_cols !== []
213

214
            if($this->getPrimaryCol() === '') {
96✔
215

216
                /** @psalm-suppress MixedAssignment */
217
                foreach ($this->table_cols as $colname => $col_metadata) {
8✔
218

219
                    /** @psalm-suppress MixedArrayAccess */
220
                    if($col_metadata['primary']) {
8✔
221

222
                        /** @psalm-suppress MixedArgumentTypeCoercion */
223
                        $this->setPrimaryCol($colname);
8✔
224
                        break;
8✔
225
                    }
226
                }
227
            } // if($this->getPrimaryCol() === '')
228
            
229
        }// if ( $this->table_cols === [] )
230

231
        //if $this->getPrimaryCol() is still '' at this point, throw an exception.
232
        if( $this->getPrimaryCol() === '' ) {
1,308✔
233

234
            throw new \GDAO\ModelPrimaryColNameNotSetDuringConstructionException($pri_col_not_set_exception_msg);
8✔
235
        }
236
    }
237
    
238
    /**
239
     * Detect if an unsupported DB Engine version is being used
240
     */
241
    protected function pdoServerVersionCheck(): void {
242

243
        if(strtolower($this->getPdoDriverName()) === 'sqlite') {
1,308✔
244

245
            $pdo_obj = $this->getPDO();
1,308✔
246

247
            /** @psalm-suppress MixedAssignment */
248
            $sqlite_version_number = $pdo_obj->getAttribute(\PDO::ATTR_SERVER_VERSION);
1,308✔
249

250
            /** @psalm-suppress MixedArgument */
251
            if(version_compare($sqlite_version_number, '3.7.10', '<=')) {
1,308✔
252

253
                $source = static::class . '::' . __FUNCTION__ . '(...)';
×
254
                $msg = "ERROR ({$source}): Sqlite version `{$sqlite_version_number}`"
×
255
                        . " detected. This package requires Sqlite version `3.7.11`"
×
256
                        . " or greater. Use a newer version of sqlite or use another"
×
257
                        . " DB server supported by this package." . PHP_EOL . 'Goodbye!!';
×
258

259
                throw new \LeanOrm\Exceptions\UnsupportedPdoServerVersionException($msg);
×
260

261
            } // if( version_compare($sqlite_version_number, '3.7.10', '<=') )
262
        } // if( strtolower($this->getPdoDriverName()) === 'sqlite' )
263
    }
264
    
265
    protected function columnExistsInDbTable(string $table_name, string $column_name): bool {
266
        
267
        $schema_definitions = $this->fetchTableColsFromDB($table_name);
1,308✔
268
        
269
        return array_key_exists($column_name, $schema_definitions);
1,308✔
270
    }
271
    
272
    protected function tableExistsInDB(string $table_name): bool {
273
        
274
        $list_of_tables_and_views = $this->fetchTableListFromDB();
1,308✔
275
        
276
        return in_array($table_name, $list_of_tables_and_views, true);
1,308✔
277
    }
278
    
279
    /**
280
     * @psalm-suppress MoreSpecificReturnType
281
     * @psalm-suppress UnusedPsalmSuppress
282
     */
283
    protected function getSchemaQueryingObject(): \Rotexsoft\SqlSchema\AbstractSchema {
284
        
285
        // a column definition factory 
286
        $column_factory = new ColumnFactory();
1,308✔
287
        /** @psalm-suppress MixedAssignment */
288
        $pdo_driver_name = $this->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME);
1,308✔
289

290
        $schema_class_name = '\\Rotexsoft\\SqlSchema\\' . ucfirst((string) $pdo_driver_name) . 'Schema';
1,308✔
291

292
        // the schema discovery object
293
        /** @psalm-suppress LessSpecificReturnStatement */
294
        return new $schema_class_name($this->getPDO(), $column_factory);
1,308✔
295
    }
296
    
297
    /**
298
     * @return mixed[]|string[]
299
     */
300
    protected function fetchTableListFromDB(): array {
301
        
302
        if(strtolower((string) $this->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME)) === 'sqlite') {
1,308✔
303
            
304
            // Do this to return both tables and views
305
            // $this->getSchemaQueryingObject()->fetchTableList()
306
            // only returns table names but no views. That's why
307
            // we are doing this here
308
            return $this->db_connector->dbFetchCol("
1,308✔
309
                SELECT name FROM sqlite_master
310
                UNION ALL
311
                SELECT name FROM sqlite_temp_master
312
                ORDER BY name
313
            ");
1,308✔
314
        }
315
        
316
        $schema = $this->getSchemaQueryingObject();
×
317
        
318
        if(strtolower((string) $this->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME)) ===  'pgsql') {
×
319
            
320
            // Calculate schema name for postgresql
321
            /** @psalm-suppress MixedAssignment */
322
            $schema_name = $this->db_connector->dbFetchValue('SELECT CURRENT_SCHEMA');
×
323
            
324
            /** @psalm-suppress MixedArgument */
325
            return $schema->fetchTableList($schema_name);
×
326
        }
327
        
328
        return $schema->fetchTableList();
×
329
    }
330
    
331
    /**
332
     * @return \Rotexsoft\SqlSchema\Column[]
333
     */
334
    protected function fetchTableColsFromDB(string $table_name): array {
335
        
336
        // This works so far for mysql, postgres & sqlite.  
337
        // Will need to test what works for MS Sql Server
338
        return $this->getSchemaQueryingObject()->fetchTableCols($table_name);
1,308✔
339
    }
340
    
341
    /** @psalm-suppress MoreSpecificReturnType */
342
    public function getSelect(): \Aura\SqlQuery\Common\Select {
343

344
        $selectObj = (new QueryFactory($this->getPdoDriverName()))->newSelect();
296✔
345

346
        $selectObj->from($this->getTableName());
296✔
347

348
        /** @psalm-suppress LessSpecificReturnStatement */
349
        return $selectObj;
296✔
350
    }
351

352
    /**
353
     * {@inheritDoc}
354
     * 
355
     * @psalm-suppress LessSpecificReturnStatement
356
     */
357
    #[\Override]
358
    public function createNewCollection(\GDAO\Model\RecordInterface ...$list_of_records): \GDAO\Model\CollectionInterface {
359

360
        return ($this->collection_class_name === null || $this->collection_class_name === '')
164✔
361
                ? //default to creating new collection of type \LeanOrm\Model\Collection
164✔
362
                  new \LeanOrm\Model\Collection($this, ...$list_of_records)
8✔
363
                : new $this->collection_class_name($this, ...$list_of_records);
164✔
364
    }
365

366
    /**
367
     * {@inheritDoc}
368
     */
369
    #[\Override]
370
    public function createNewRecord(array $col_names_and_values = []): \GDAO\Model\RecordInterface {
371

372

373
        $result = ($this->record_class_name === null || $this->record_class_name === '')
408✔
374
                    ? //default to creating new record of type \LeanOrm\Model\Record
408✔
375
                      new \LeanOrm\Model\Record($col_names_and_values, $this)
8✔
376
                    : new $this->record_class_name($col_names_and_values, $this);
408✔
377
        
378
        /** @psalm-suppress LessSpecificReturnStatement */
379
        return $result;
408✔
380
    }
381

382
    /**
383
     * 
384
     * @param string $table_name name of the table to select from (will default to $this->getTableName() if empty)
385
     * @return \Aura\SqlQuery\Common\Select or any of its descendants
386
     */
387
    protected function createQueryObjectIfNullAndAddColsToQuery(
388
        ?\Aura\SqlQuery\Common\Select $select_obj=null, string $table_name=''
389
    ): \Aura\SqlQuery\Common\Select {
390
        
391
        $initiallyNull = !( $select_obj instanceof \Aura\SqlQuery\Common\Select );
296✔
392
        $select_obj ??= $this->getSelect();
296✔
393

394
        if( $table_name === '' ) {
296✔
395

396
            $table_name = $this->getTableName();
296✔
397
        }
398

399
        if($initiallyNull || !$select_obj->hasCols()) {
296✔
400

401
            // We either just created the select object in this method or
402
            // there are no cols to select specified yet. 
403
            // Let's select all cols.
404
            $select_obj->cols([' ' . $table_name . '.* ']);
288✔
405
        }
406

407
        return $select_obj;
296✔
408
    }
409

410
    /**
411
     * @return mixed[]
412
     * @psalm-suppress PossiblyUnusedMethod
413
     */
414
    public function getDefaultColVals(): array {
415

416
        $default_colvals = [];
8✔
417

418
        /** @psalm-suppress MixedAssignment */
419
        foreach($this->table_cols as $col_name => $col_metadata) {
8✔
420
            
421
            /** @psalm-suppress MixedArrayAccess */
422
            $default_colvals[$col_name] = $col_metadata['default'];
8✔
423
        }
424

425
        return $default_colvals;
8✔
426
    }
427
    
428
    public function loadRelationshipData(
429
        string $rel_name, 
430
        \GDAO\Model\RecordInterface|\GDAO\Model\CollectionInterface|array &$parent_data, 
431
        bool $wrap_each_row_in_a_record=false, 
432
        bool $wrap_records_in_collection=false
433
    ): static {
434

435
        /** @psalm-suppress MixedArrayAccess */
436
        if( 
437
            array_key_exists($rel_name, $this->relations) 
120✔
438
            && $this->relations[$rel_name]['relation_type'] === \GDAO\Model::RELATION_TYPE_HAS_MANY 
120✔
439
        ) {
440
            $this->loadHasMany($rel_name, $parent_data, $wrap_each_row_in_a_record, $wrap_records_in_collection);
120✔
441

442
        } else if (
443
            array_key_exists($rel_name, $this->relations) 
88✔
444
            && $this->relations[$rel_name]['relation_type'] === \GDAO\Model::RELATION_TYPE_HAS_MANY_THROUGH        
88✔
445
        ) {
446
            $this->loadHasManyThrough($rel_name, $parent_data, $wrap_each_row_in_a_record, $wrap_records_in_collection);
88✔
447

448
        } else if (
449
            array_key_exists($rel_name, $this->relations) 
88✔
450
            && $this->relations[$rel_name]['relation_type'] === \GDAO\Model::RELATION_TYPE_HAS_ONE
88✔
451
        ) {
452
            $this->loadHasOne($rel_name, $parent_data, $wrap_each_row_in_a_record);
88✔
453

454
        } else if (
455
            array_key_exists($rel_name, $this->relations) 
88✔
456
            && $this->relations[$rel_name]['relation_type'] === \GDAO\Model::RELATION_TYPE_BELONGS_TO
88✔
457
        ) {
458
            $this->loadBelongsTo($rel_name, $parent_data, $wrap_each_row_in_a_record);
88✔
459
        }
460

461
        return $this;
120✔
462
    }
463
    
464
    /**
465
     * @psalm-suppress PossiblyUnusedReturnValue
466
     */
467
    protected function validateRelatedModelClassName(string $model_class_name): bool {
468
        
469
        // DO NOT use static::class here, we always want self::class
470
        // Subclasses can override this method to redefine their own
471
        // Valid Related Model Class logic.
472
        $parent_model_class_name = self::class;
1,308✔
473
        
474
        if( !is_a($model_class_name, $parent_model_class_name, true) ) {
1,308✔
475
            
476
            //throw exception
477
            $msg = "ERROR: '{$model_class_name}' is not a subclass or instance of "
32✔
478
                 . "'{$parent_model_class_name}'. A model class name specified"
32✔
479
                 . " for fetching related data must be the name of a class that"
32✔
480
                 . " is a sub-class or instance of '{$parent_model_class_name}'"
32✔
481
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
32✔
482
                 . PHP_EOL;
32✔
483

484
            throw new \LeanOrm\Exceptions\BadModelClassNameForFetchingRelatedDataException($msg);
32✔
485
        }
486
        
487
        return true;
1,308✔
488
    }
489
    
490
    /**
491
     * @psalm-suppress PossiblyUnusedReturnValue
492
     */
493
    protected function validateRelatedCollectionClassName(string $collection_class_name): bool {
494

495
        $parent_collection_class_name = \GDAO\Model\CollectionInterface::class;
1,308✔
496

497
        if( !is_subclass_of($collection_class_name, $parent_collection_class_name, true) ) {
1,308✔
498

499
            //throw exception
500
            $msg = "ERROR: '{$collection_class_name}' is not a subclass of "
32✔
501
                 . "'{$parent_collection_class_name}'. A collection class name specified"
32✔
502
                 . " for fetching related data must be the name of a class that"
32✔
503
                 . " is a sub-class of '{$parent_collection_class_name}'"
32✔
504
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
32✔
505
                 . PHP_EOL;
32✔
506

507
            throw new \LeanOrm\Exceptions\BadCollectionClassNameForFetchingRelatedDataException($msg);
32✔
508
        }
509

510
        return true;
1,308✔
511
    }
512

513
    /**
514
     * @psalm-suppress PossiblyUnusedReturnValue
515
     */
516
    protected function validateRelatedRecordClassName(string $record_class_name): bool {
517
        
518
        $parent_record_class_name = \GDAO\Model\RecordInterface::class;
1,308✔
519

520
        if( !is_subclass_of($record_class_name, $parent_record_class_name, true)  ) {
1,308✔
521

522
            //throw exception
523
            $msg = "ERROR: '{$record_class_name}' is not a subclass of "
32✔
524
                 . "'{$parent_record_class_name}'. A record class name specified for"
32✔
525
                 . " fetching related data must be the name of a class that"
32✔
526
                 . " is a sub-class of '{$parent_record_class_name}'"
32✔
527
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
32✔
528
                 . PHP_EOL;
32✔
529

530
            throw new \LeanOrm\Exceptions\BadRecordClassNameForFetchingRelatedDataException($msg);
32✔
531
        }
532

533
        return true;
1,308✔
534
    }
535
    
536
    protected function loadHasMany( 
537
        string $rel_name, 
538
        \GDAO\Model\RecordInterface|\GDAO\Model\CollectionInterface|array &$parent_data, 
539
        bool $wrap_each_row_in_a_record=false, 
540
        bool $wrap_records_in_collection=false 
541
    ): void {
542
        /** @psalm-suppress MixedArrayAccess */
543
        if( 
544
            array_key_exists($rel_name, $this->relations) 
120✔
545
            && $this->relations[$rel_name]['relation_type']  === \GDAO\Model::RELATION_TYPE_HAS_MANY
120✔
546
        ) {
547
            /*
548
                -- BASIC SQL For Fetching the Related Data
549

550
                -- $parent_data is a collection or array of records    
551
                SELECT {$foreign_table_name}.*
552
                  FROM {$foreign_table_name}
553
                 WHERE {$foreign_table_name}.{$fkey_col_in_foreign_table} IN ( $fkey_col_in_my_table column values in $parent_data )
554

555
                -- OR
556

557
                -- $parent_data is a single record
558
                SELECT {$foreign_table_name}.*
559
                  FROM {$foreign_table_name}
560
                 WHERE {$foreign_table_name}.{$fkey_col_in_foreign_table} = {$parent_data->$fkey_col_in_my_table}
561
            */
562
            [
120✔
563
                $fkey_col_in_foreign_table, $fkey_col_in_my_table, 
120✔
564
                $foreign_model_obj, $related_data
120✔
565
            ] = $this->getBelongsToOrHasOneOrHasManyData($rel_name, $parent_data);
120✔
566

567
            if ( 
568
                $parent_data instanceof \GDAO\Model\CollectionInterface
120✔
569
                || is_array($parent_data)
120✔
570
            ) {
571
                ///////////////////////////////////////////////////////////
572
                // Stitch the related data to the approriate parent records
573
                ///////////////////////////////////////////////////////////
574

575
                $fkey_val_to_related_data_keys = [];
64✔
576

577
                // Generate a map of 
578
                //      foreign key value => [keys of related rows in $related_data]
579
                /** @psalm-suppress MixedAssignment */
580
                foreach ($related_data as $curr_key => $related_datum) {
64✔
581

582
                    /** @psalm-suppress MixedArrayOffset */
583
                    $curr_fkey_val = $related_datum[$fkey_col_in_foreign_table];
64✔
584

585
                    /** @psalm-suppress MixedArgument */
586
                    if(!array_key_exists($curr_fkey_val, $fkey_val_to_related_data_keys)) {
64✔
587

588
                        /** @psalm-suppress MixedArrayOffset */
589
                        $fkey_val_to_related_data_keys[$curr_fkey_val] = [];
64✔
590
                    }
591

592
                    // Add current key in $related_data to sub array of keys for the 
593
                    // foreign key value in the current related row $related_datum
594
                    /** @psalm-suppress MixedArrayOffset */
595
                    $fkey_val_to_related_data_keys[$curr_fkey_val][] = $curr_key;
64✔
596

597
                } // foreach ($related_data as $curr_key => $related_datum)
598

599
                // Now use $fkey_val_to_related_data_keys map to
600
                // look up related rows of data for each parent row of data
601
                /** @psalm-suppress MixedAssignment */
602
                foreach( $parent_data as $p_rec_key => $parent_row ) {
64✔
603

604
                    $matching_related_rows = [];
64✔
605

606
                    /** 
607
                     * @psalm-suppress MixedArgument 
608
                     * @psalm-suppress MixedArrayOffset 
609
                     */
610
                    if(array_key_exists($parent_row[$fkey_col_in_my_table], $fkey_val_to_related_data_keys)) {
64✔
611

612
                        foreach ($fkey_val_to_related_data_keys[$parent_row[$fkey_col_in_my_table]] as $related_data_key) {
64✔
613

614
                            $matching_related_rows[] = $related_data[$related_data_key];
64✔
615
                        }
616
                    }
617

618
                    /** @psalm-suppress MixedArgument */
619
                    $this->wrapRelatedDataInsideRecordsAndCollection(
64✔
620
                        $matching_related_rows, $foreign_model_obj, 
64✔
621
                        $wrap_each_row_in_a_record, $wrap_records_in_collection
64✔
622
                    );
64✔
623

624
                    //set the related data for the current parent row / record
625
                    if( $parent_row instanceof \GDAO\Model\RecordInterface ) {
64✔
626
                        /**
627
                         * @psalm-suppress MixedArrayTypeCoercion
628
                         * @psalm-suppress MixedArrayOffset
629
                         * @psalm-suppress MixedMethodCall
630
                         */
631
                        $parent_data[$p_rec_key]->setRelatedData($rel_name, $matching_related_rows);
48✔
632

633
                    } else {
634

635
                        //the current row must be an array
636
                        /**
637
                         * @psalm-suppress MixedArrayOffset
638
                         * @psalm-suppress MixedArrayAssignment
639
                         * @psalm-suppress InvalidArgument
640
                         */
641
                        $parent_data[$p_rec_key][$rel_name] = $matching_related_rows;
24✔
642
                    }
643
                } // foreach( $parent_data as $p_rec_key => $parent_record )
644

645
                ////////////////////////////////////////////////////////////////
646
                // End: Stitch the related data to the approriate parent records
647
                ////////////////////////////////////////////////////////////////
648

649
            } else if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
56✔
650

651
                /** @psalm-suppress MixedArgument */
652
                $this->wrapRelatedDataInsideRecordsAndCollection(
56✔
653
                    $related_data, $foreign_model_obj, 
56✔
654
                    $wrap_each_row_in_a_record, $wrap_records_in_collection
56✔
655
                );
56✔
656

657
                ///////////////////////////////////////////////
658
                //stitch the related data to the parent record
659
                ///////////////////////////////////////////////
660
                $parent_data->setRelatedData($rel_name, $related_data);
56✔
661

662
            } // else if ($parent_data instanceof \GDAO\Model\RecordInterface)
663
        } // if( array_key_exists($rel_name, $this->relations) )
664
    }
665
    
666
    protected function loadHasManyThrough( 
667
        string $rel_name, 
668
        \GDAO\Model\RecordInterface|\GDAO\Model\CollectionInterface|array &$parent_data, 
669
        bool $wrap_each_row_in_a_record=false, 
670
        bool $wrap_records_in_collection=false 
671
    ): void {
672
        /** @psalm-suppress MixedArrayAccess */
673
        if( 
674
            array_key_exists($rel_name, $this->relations) 
88✔
675
            && $this->relations[$rel_name]['relation_type']  === \GDAO\Model::RELATION_TYPE_HAS_MANY_THROUGH
88✔
676
        ) {
677
            /** @psalm-suppress MixedAssignment */
678
            $rel_info = $this->relations[$rel_name];
88✔
679

680
            /** 
681
             * @psalm-suppress MixedAssignment
682
             * @psalm-suppress MixedArgument
683
             */
684
            $foreign_table_name = Utils::arrayGet($rel_info, 'foreign_table');
88✔
685

686
            /** @psalm-suppress MixedAssignment */
687
            $fkey_col_in_foreign_table = 
88✔
688
                Utils::arrayGet($rel_info, 'col_in_foreign_table_linked_to_join_table');
88✔
689

690
            /** @psalm-suppress MixedAssignment */
691
            $foreign_models_class_name = 
88✔
692
                Utils::arrayGet($rel_info, 'foreign_models_class_name', \LeanOrm\Model::class);
88✔
693

694
            /** @psalm-suppress MixedAssignment */
695
            $pri_key_col_in_foreign_models_table = 
88✔
696
                Utils::arrayGet($rel_info, 'primary_key_col_in_foreign_table');
88✔
697

698
            /** @psalm-suppress MixedAssignment */
699
            $fkey_col_in_my_table = 
88✔
700
                    Utils::arrayGet($rel_info, 'col_in_my_table_linked_to_join_table');
88✔
701

702
            //join table params
703
            /** @psalm-suppress MixedAssignment */
704
            $join_table_name = Utils::arrayGet($rel_info, 'join_table');
88✔
705

706
            /** @psalm-suppress MixedAssignment */
707
            $col_in_join_table_linked_to_my_models_table = 
88✔
708
                Utils::arrayGet($rel_info, 'col_in_join_table_linked_to_my_table');
88✔
709

710
            /** @psalm-suppress MixedAssignment */
711
            $col_in_join_table_linked_to_foreign_models_table = 
88✔
712
                Utils::arrayGet($rel_info, 'col_in_join_table_linked_to_foreign_table');
88✔
713

714
            /** @psalm-suppress MixedAssignment */
715
            $sql_query_modifier = 
88✔
716
                Utils::arrayGet($rel_info, 'sql_query_modifier', null);
88✔
717

718
            /** @psalm-suppress MixedArgument */
719
            $foreign_model_obj = 
88✔
720
                $this->createRelatedModelObject(
88✔
721
                    $foreign_models_class_name,
88✔
722
                    $pri_key_col_in_foreign_models_table,
88✔
723
                    $foreign_table_name
88✔
724
                );
88✔
725

726
            /** @psalm-suppress MixedAssignment */
727
            $foreign_models_collection_class_name = 
88✔
728
                Utils::arrayGet($rel_info, 'foreign_models_collection_class_name', '');
88✔
729

730
            /** @psalm-suppress MixedAssignment */
731
            $foreign_models_record_class_name = 
88✔
732
                Utils::arrayGet($rel_info, 'foreign_models_record_class_name', '');
88✔
733

734
            if($foreign_models_collection_class_name !== '') {
88✔
735

736
                /** @psalm-suppress MixedArgument */
737
                $foreign_model_obj->setCollectionClassName($foreign_models_collection_class_name);
88✔
738
            }
739

740
            if($foreign_models_record_class_name !== '') {
88✔
741

742
                /** @psalm-suppress MixedArgument */
743
                $foreign_model_obj->setRecordClassName($foreign_models_record_class_name);
88✔
744
            }
745

746
            $query_obj = $foreign_model_obj->getSelect();
88✔
747

748
            $query_obj->cols( [" {$join_table_name}.{$col_in_join_table_linked_to_my_models_table} ", " {$foreign_table_name}.* "] );
88✔
749

750
            /** @psalm-suppress MixedArgument */
751
            $query_obj->innerJoin(
88✔
752
                            $join_table_name, 
88✔
753
                            " {$join_table_name}.{$col_in_join_table_linked_to_foreign_models_table} = {$foreign_table_name}.{$fkey_col_in_foreign_table} "
88✔
754
                        );
88✔
755

756
            if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
88✔
757

758
                $query_obj->where(
32✔
759
                    " {$join_table_name}.{$col_in_join_table_linked_to_my_models_table} = :leanorm_col_in_join_table_linked_to_my_models_table_val ",
32✔
760
                    ['leanorm_col_in_join_table_linked_to_my_models_table_val' => $parent_data->$fkey_col_in_my_table]
32✔
761
                );
32✔
762

763
            } else {
764

765
                //assume it's a collection or array
766
                /** @psalm-suppress MixedArgument */
767
                $col_vals = $this->getColValsFromArrayOrCollection(
56✔
768
                                $parent_data, $fkey_col_in_my_table
56✔
769
                            );
56✔
770

771
                if( $col_vals !== [] ) {
56✔
772

773
                    $this->addWhereInAndOrIsNullToQuery(
56✔
774
                        "{$join_table_name}.{$col_in_join_table_linked_to_my_models_table}", 
56✔
775
                        $col_vals, 
56✔
776
                        $query_obj
56✔
777
                    );
56✔
778
                }
779
            }
780

781
            if(\is_callable($sql_query_modifier)) {
88✔
782

783
                $sql_query_modifier = Utils::getClosureFromCallable($sql_query_modifier);
16✔
784
                // modify the query object before executing the query
785
                /** @psalm-suppress MixedAssignment */
786
                $query_obj = $sql_query_modifier($query_obj);
16✔
787
            }
788

789
            /** @psalm-suppress MixedAssignment */
790
            $params_2_bind_2_sql = $query_obj->getBindValues();
88✔
791

792
            /** @psalm-suppress MixedAssignment */
793
            $sql_2_get_related_data = $query_obj->__toString();
88✔
794

795
/*
796
-- SQL For Fetching the Related Data
797

798
-- $parent_data is a collection or array of records    
799
SELECT {$foreign_table_name}.*,
800
       {$join_table_name}.{$col_in_join_table_linked_to_my_models_table}
801
  FROM {$foreign_table_name}
802
  JOIN {$join_table_name} ON {$join_table_name}.{$col_in_join_table_linked_to_foreign_models_table} = {$foreign_table_name}.{$fkey_col_in_foreign_table}
803
 WHERE {$join_table_name}.{$col_in_join_table_linked_to_my_models_table} IN ( $fkey_col_in_my_table column values in $parent_data )
804

805
OR
806

807
-- $parent_data is a single record
808
SELECT {$foreign_table_name}.*,
809
       {$join_table_name}.{$col_in_join_table_linked_to_my_models_table}
810
  FROM {$foreign_table_name}
811
  JOIN {$join_table_name} ON {$join_table_name}.{$col_in_join_table_linked_to_foreign_models_table} = {$foreign_table_name}.{$fkey_col_in_foreign_table}
812
 WHERE {$join_table_name}.{$col_in_join_table_linked_to_my_models_table} = {$parent_data->$fkey_col_in_my_table}
813
*/
814
            /** @psalm-suppress MixedArgument */
815
            $this->logQuery($sql_2_get_related_data, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
88✔
816

817
            //GRAB DA RELATED DATA
818
            $related_data = 
88✔
819
                $this->db_connector
88✔
820
                     ->dbFetchAll($sql_2_get_related_data, $params_2_bind_2_sql);
88✔
821

822
            if ( 
823
                $parent_data instanceof \GDAO\Model\CollectionInterface
88✔
824
                || is_array($parent_data)
88✔
825
            ) {
826
                ///////////////////////////////////////////////////////////
827
                // Stitch the related data to the approriate parent records
828
                ///////////////////////////////////////////////////////////
829

830
                $fkey_val_to_related_data_keys = [];
56✔
831

832
                // Generate a map of 
833
                //      foreign key value => [keys of related rows in $related_data]
834
                /** @psalm-suppress MixedAssignment */
835
                foreach ($related_data as $curr_key => $related_datum) {
56✔
836

837
                    /** @psalm-suppress MixedArrayOffset */
838
                    $curr_fkey_val = $related_datum[$col_in_join_table_linked_to_my_models_table];
56✔
839

840
                    /** @psalm-suppress MixedArgument */
841
                    if(!array_key_exists($curr_fkey_val, $fkey_val_to_related_data_keys)) {
56✔
842

843
                        /** @psalm-suppress MixedArrayOffset */
844
                        $fkey_val_to_related_data_keys[$curr_fkey_val] = [];
56✔
845
                    }
846

847
                    // Add current key in $related_data to sub array of keys for the 
848
                    // foreign key value in the current related row $related_datum
849
                    /** @psalm-suppress MixedArrayOffset */
850
                    $fkey_val_to_related_data_keys[$curr_fkey_val][] = $curr_key;
56✔
851

852
                } // foreach ($related_data as $curr_key => $related_datum)
853

854
                // Now use $fkey_val_to_related_data_keys map to
855
                // look up related rows of data for each parent row of data
856
                /** @psalm-suppress MixedAssignment */
857
                foreach( $parent_data as $p_rec_key => $parent_row ) {
56✔
858

859
                    $matching_related_rows = [];
56✔
860

861
                    /** 
862
                     * @psalm-suppress MixedArrayOffset
863
                     * @psalm-suppress MixedArgument
864
                     */
865
                    if(array_key_exists($parent_row[$fkey_col_in_my_table], $fkey_val_to_related_data_keys)) {
56✔
866

867
                        foreach ($fkey_val_to_related_data_keys[$parent_row[$fkey_col_in_my_table]] as $related_data_key) {
56✔
868

869
                            $matching_related_rows[] = $related_data[$related_data_key];
56✔
870
                        }
871
                    }
872

873
                    $this->wrapRelatedDataInsideRecordsAndCollection(
56✔
874
                        $matching_related_rows, $foreign_model_obj, 
56✔
875
                        $wrap_each_row_in_a_record, $wrap_records_in_collection
56✔
876
                    );
56✔
877

878
                    //set the related data for the current parent row / record
879
                    if( $parent_row instanceof \GDAO\Model\RecordInterface ) {
56✔
880

881
                        /** 
882
                         * @psalm-suppress MixedArrayOffset
883
                         * @psalm-suppress MixedArrayTypeCoercion
884
                         */
885
                        $parent_data[$p_rec_key]->setRelatedData($rel_name, $matching_related_rows);
40✔
886

887
                    } else {
888

889
                        //the current row must be an array
890
                        /** 
891
                         * @psalm-suppress MixedArrayOffset
892
                         * @psalm-suppress MixedArrayAssignment
893
                         * @psalm-suppress InvalidArgument
894
                         */
895
                        $parent_data[$p_rec_key][$rel_name] = $matching_related_rows;
16✔
896
                    }
897

898
                } // foreach( $parent_data as $p_rec_key => $parent_record )
899

900
                ////////////////////////////////////////////////////////////////
901
                // End: Stitch the related data to the approriate parent records
902
                ////////////////////////////////////////////////////////////////
903

904
            } else if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
32✔
905

906
                $this->wrapRelatedDataInsideRecordsAndCollection(
32✔
907
                    $related_data, $foreign_model_obj, 
32✔
908
                    $wrap_each_row_in_a_record, $wrap_records_in_collection
32✔
909
                );
32✔
910

911
                //stitch the related data to the parent record
912
                $parent_data->setRelatedData($rel_name, $related_data);
32✔
913
            } // else if ( $parent_data instanceof \GDAO\Model\RecordInterface )
914
        } // if( array_key_exists($rel_name, $this->relations) )
915
    }
916
    
917
    protected function loadHasOne( 
918
        string $rel_name, 
919
        \GDAO\Model\RecordInterface|\GDAO\Model\CollectionInterface|array &$parent_data, 
920
        bool $wrap_row_in_a_record=false
921
    ): void {
922
        /**
923
         * @psalm-suppress MixedArrayAccess
924
         */
925
        if( 
926
            array_key_exists($rel_name, $this->relations) 
88✔
927
            && $this->relations[$rel_name]['relation_type']  === \GDAO\Model::RELATION_TYPE_HAS_ONE
88✔
928
        ) {
929
            [
88✔
930
                $fkey_col_in_foreign_table, $fkey_col_in_my_table, 
88✔
931
                $foreign_model_obj, $related_data
88✔
932
            ] = $this->getBelongsToOrHasOneOrHasManyData($rel_name, $parent_data);
88✔
933

934
/*
935
-- SQL For Fetching the Related Data
936

937
-- $parent_data is a collection or array of records    
938
SELECT {$foreign_table_name}.*
939
  FROM {$foreign_table_name}
940
 WHERE {$foreign_table_name}.{$fkey_col_in_foreign_table} IN ( $fkey_col_in_my_table column values in $parent_data )
941

942
OR
943

944
-- $parent_data is a single record
945
SELECT {$foreign_table_name}.*
946
  FROM {$foreign_table_name}
947
 WHERE {$foreign_table_name}.{$fkey_col_in_foreign_table} = {$parent_data->$fkey_col_in_my_table}
948
*/
949

950
            if ( 
951
                $parent_data instanceof \GDAO\Model\CollectionInterface
88✔
952
                || is_array($parent_data)
88✔
953
            ) {
954
                ///////////////////////////////////////////////////////////
955
                // Stitch the related data to the approriate parent records
956
                ///////////////////////////////////////////////////////////
957

958
                $fkey_val_to_related_data_keys = [];
56✔
959

960
                // Generate a map of 
961
                //      foreign key value => [keys of related rows in $related_data]
962
                /** @psalm-suppress MixedAssignment */
963
                foreach ($related_data as $curr_key => $related_datum) {
56✔
964

965
                    /** @psalm-suppress MixedArrayOffset */
966
                    $curr_fkey_val = $related_datum[$fkey_col_in_foreign_table];
56✔
967

968
                    /** @psalm-suppress MixedArgument */
969
                    if(!array_key_exists($curr_fkey_val, $fkey_val_to_related_data_keys)) {
56✔
970

971
                        /** @psalm-suppress MixedArrayOffset */
972
                        $fkey_val_to_related_data_keys[$curr_fkey_val] = [];
56✔
973
                    }
974

975
                    // Add current key in $related_data to sub array of keys for the 
976
                    // foreign key value in the current related row $related_datum
977
                    /** @psalm-suppress MixedArrayOffset */
978
                    $fkey_val_to_related_data_keys[$curr_fkey_val][] = $curr_key;
56✔
979

980
                } // foreach ($related_data as $curr_key => $related_datum)
981

982
                // Now use $fkey_val_to_related_data_keys map to
983
                // look up related rows of data for each parent row of data
984
                /** @psalm-suppress MixedAssignment */
985
                foreach( $parent_data as $p_rec_key => $parent_row ) {
56✔
986

987
                    $matching_related_rows = [];
56✔
988

989
                    /** 
990
                     * @psalm-suppress MixedArgument
991
                     * @psalm-suppress MixedArrayOffset
992
                     */
993
                    if(array_key_exists($parent_row[$fkey_col_in_my_table], $fkey_val_to_related_data_keys)) {
56✔
994

995
                        foreach ($fkey_val_to_related_data_keys[$parent_row[$fkey_col_in_my_table]] as $related_data_key) {
56✔
996

997
                            // There should really only be one matching related 
998
                            // record per parent record since this is a hasOne
999
                            // relationship
1000
                            $matching_related_rows[] = $related_data[$related_data_key];
56✔
1001
                        }
1002
                    }
1003

1004
                    /** @psalm-suppress MixedArgument */
1005
                    $this->wrapRelatedDataInsideRecordsAndCollection(
56✔
1006
                        $matching_related_rows, $foreign_model_obj, 
56✔
1007
                        $wrap_row_in_a_record, false
56✔
1008
                    );
56✔
1009

1010
                    //set the related data for the current parent row / record
1011
                    if( $parent_row instanceof \GDAO\Model\RecordInterface ) {
56✔
1012

1013
                        // There should really only be one matching related 
1014
                        // record per parent record since this is a hasOne
1015
                        // relationship. That's why we are doing 
1016
                        // $matching_related_rows[0]
1017
                        /** 
1018
                         * @psalm-suppress MixedArrayTypeCoercion
1019
                         * @psalm-suppress MixedArrayOffset
1020
                         * @psalm-suppress MixedMethodCall
1021
                         */
1022
                        $parent_data[$p_rec_key]->setRelatedData(
40✔
1023
                                $rel_name, 
40✔
1024
                                (\count($matching_related_rows) > 0) 
40✔
1025
                                    ? $matching_related_rows[0] : []
40✔
1026
                            );
40✔
1027

1028
                    } else {
1029

1030
                        // There should really only be one matching related 
1031
                        // record per parent record since this is a hasOne
1032
                        // relationship. That's why we are doing 
1033
                        // $matching_related_rows[0]
1034

1035
                        //the current row must be an array
1036
                        /**
1037
                         * @psalm-suppress MixedArrayOffset
1038
                         * @psalm-suppress MixedArrayAssignment
1039
                         * @psalm-suppress MixedArgument
1040
                         * @psalm-suppress PossiblyInvalidArgument
1041
                         */
1042
                        $parent_data[$p_rec_key][$rel_name] = 
16✔
1043
                                (\count($matching_related_rows) > 0) 
16✔
1044
                                    ? $matching_related_rows[0] : [];
16✔
1045
                    }
1046
                } // foreach( $parent_data as $p_rec_key => $parent_record )
1047

1048
                ////////////////////////////////////////////////////////////////
1049
                // End: Stitch the related data to the approriate parent records
1050
                ////////////////////////////////////////////////////////////////
1051

1052
            } else if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
32✔
1053

1054
                /** @psalm-suppress MixedArgument */
1055
                $this->wrapRelatedDataInsideRecordsAndCollection(
32✔
1056
                            $related_data, $foreign_model_obj, 
32✔
1057
                            $wrap_row_in_a_record, false
32✔
1058
                        );
32✔
1059

1060
                //stitch the related data to the parent record
1061
                /** @psalm-suppress MixedArgument */
1062
                $parent_data->setRelatedData(
32✔
1063
                    $rel_name, 
32✔
1064
                    (\count($related_data) > 0) ? \array_shift($related_data) : []
32✔
1065
                );
32✔
1066
            } // else if ($parent_data instanceof \GDAO\Model\RecordInterface)
1067
        } // if( array_key_exists($rel_name, $this->relations) )
1068
    }
1069
    
1070
    protected function loadBelongsTo(
1071
        string $rel_name, 
1072
        \GDAO\Model\RecordInterface|\GDAO\Model\CollectionInterface|array &$parent_data, 
1073
        bool $wrap_row_in_a_record=false
1074
    ): void {
1075

1076
        /** @psalm-suppress MixedArrayAccess */
1077
        if( 
1078
            array_key_exists($rel_name, $this->relations) 
88✔
1079
            && $this->relations[$rel_name]['relation_type']  === \GDAO\Model::RELATION_TYPE_BELONGS_TO
88✔
1080
        ) {
1081
            //quick hack
1082
            /** @psalm-suppress MixedArrayAssignment */
1083
            $this->relations[$rel_name]['relation_type'] = \GDAO\Model::RELATION_TYPE_HAS_ONE;
88✔
1084

1085
            //I really don't see the difference in the sql to fetch data for
1086
            //a has-one relationship and a belongs-to relationship. Hence, I
1087
            //have resorted to using the same code to satisfy both relationships
1088
            $this->loadHasOne($rel_name, $parent_data, $wrap_row_in_a_record);
88✔
1089

1090
            //undo quick hack
1091
            /** @psalm-suppress MixedArrayAssignment */
1092
            $this->relations[$rel_name]['relation_type'] = \GDAO\Model::RELATION_TYPE_BELONGS_TO;
88✔
1093
        }
1094
    }
1095
    
1096
    /**
1097
     * @return mixed[]
1098
     */
1099
    protected function getBelongsToOrHasOneOrHasManyData(
1100
        string $rel_name, 
1101
        \GDAO\Model\RecordInterface|\GDAO\Model\CollectionInterface|array &$parent_data
1102
    ): array {
1103
        /** 
1104
         * @psalm-suppress MixedAssignment
1105
         */
1106
        $rel_info = $this->relations[$rel_name];
120✔
1107

1108
        /** 
1109
         * @psalm-suppress MixedAssignment
1110
         * @psalm-suppress MixedArgument
1111
         */
1112
        $foreign_table_name = Utils::arrayGet($rel_info, 'foreign_table');
120✔
1113

1114
        /** @psalm-suppress MixedAssignment */
1115
        $fkey_col_in_foreign_table = 
120✔
1116
            Utils::arrayGet($rel_info, 'foreign_key_col_in_foreign_table');
120✔
1117
        
1118
        /** @psalm-suppress MixedAssignment */
1119
        $foreign_models_class_name = 
120✔
1120
            Utils::arrayGet($rel_info, 'foreign_models_class_name', \LeanOrm\Model::class);
120✔
1121

1122
        /** @psalm-suppress MixedAssignment */
1123
        $pri_key_col_in_foreign_models_table = 
120✔
1124
            Utils::arrayGet($rel_info, 'primary_key_col_in_foreign_table');
120✔
1125

1126
        /** @psalm-suppress MixedAssignment */
1127
        $fkey_col_in_my_table = 
120✔
1128
                Utils::arrayGet($rel_info, 'foreign_key_col_in_my_table');
120✔
1129

1130
        /** @psalm-suppress MixedAssignment */
1131
        $sql_query_modifier = 
120✔
1132
                Utils::arrayGet($rel_info, 'sql_query_modifier', null);
120✔
1133

1134
        /** @psalm-suppress MixedArgument */
1135
        $foreign_model_obj = $this->createRelatedModelObject(
120✔
1136
                                        $foreign_models_class_name,
120✔
1137
                                        $pri_key_col_in_foreign_models_table,
120✔
1138
                                        $foreign_table_name
120✔
1139
                                    );
120✔
1140
        
1141
        /** @psalm-suppress MixedAssignment */
1142
        $foreign_models_collection_class_name = 
120✔
1143
            Utils::arrayGet($rel_info, 'foreign_models_collection_class_name', '');
120✔
1144

1145
        /** @psalm-suppress MixedAssignment */
1146
        $foreign_models_record_class_name = 
120✔
1147
            Utils::arrayGet($rel_info, 'foreign_models_record_class_name', '');
120✔
1148

1149
        if($foreign_models_collection_class_name !== '') {
120✔
1150
            
1151
            /** @psalm-suppress MixedArgument */
1152
            $foreign_model_obj->setCollectionClassName($foreign_models_collection_class_name);
120✔
1153
        }
1154

1155
        if($foreign_models_record_class_name !== '') {
120✔
1156
            
1157
            /** @psalm-suppress MixedArgument */
1158
            $foreign_model_obj->setRecordClassName($foreign_models_record_class_name);
120✔
1159
        }
1160

1161
        $query_obj = $foreign_model_obj->getSelect();
120✔
1162

1163
        if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
120✔
1164

1165
            $query_obj->where(
56✔
1166
                " {$foreign_table_name}.{$fkey_col_in_foreign_table} = :leanorm_fkey_col_in_foreign_table_val ",
56✔
1167
                ['leanorm_fkey_col_in_foreign_table_val' => $parent_data->$fkey_col_in_my_table]
56✔
1168
            );
56✔
1169

1170
        } else {
1171
            //assume it's a collection or array
1172
            /** @psalm-suppress MixedArgument */
1173
            $col_vals = $this->getColValsFromArrayOrCollection(
64✔
1174
                            $parent_data, $fkey_col_in_my_table
64✔
1175
                        );
64✔
1176

1177
            if( $col_vals !== [] ) {
64✔
1178
                
1179
                $this->addWhereInAndOrIsNullToQuery(
64✔
1180
                    "{$foreign_table_name}.{$fkey_col_in_foreign_table}", 
64✔
1181
                    $col_vals, 
64✔
1182
                    $query_obj
64✔
1183
                );
64✔
1184
            }
1185
        }
1186

1187
        if(\is_callable($sql_query_modifier)) {
120✔
1188

1189
            $sql_query_modifier = Utils::getClosureFromCallable($sql_query_modifier);
32✔
1190
            
1191
            // modify the query object before executing the query
1192
            /** @psalm-suppress MixedAssignment */
1193
            $query_obj = $sql_query_modifier($query_obj);
32✔
1194
        }
1195

1196
        if($query_obj->hasCols() === false){
120✔
1197

1198
            $query_obj->cols(["{$foreign_table_name}.*"]);
120✔
1199
        }
1200
        
1201
        /** @psalm-suppress MixedAssignment */
1202
        $params_2_bind_2_sql = $query_obj->getBindValues();
120✔
1203
        
1204
        /** @psalm-suppress MixedAssignment */
1205
        $sql_2_get_related_data = $query_obj->__toString();
120✔
1206
        
1207
        /** @psalm-suppress MixedArgument */
1208
        $this->logQuery($sql_2_get_related_data, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
120✔
1209
        
1210
        return [
120✔
1211
            $fkey_col_in_foreign_table, $fkey_col_in_my_table, $foreign_model_obj,
120✔
1212
            $this->db_connector->dbFetchAll($sql_2_get_related_data, $params_2_bind_2_sql) // fetch the related data
120✔
1213
        ]; 
120✔
1214
    }
1215
    
1216
    /** @psalm-suppress MoreSpecificReturnType */
1217
    protected function createRelatedModelObject(
1218
        string $f_models_class_name, 
1219
        string $pri_key_col_in_f_models_table, 
1220
        string $f_table_name
1221
    ): Model {
1222
        //$foreign_models_class_name will never be empty it will default to \LeanOrm\Model
1223
        //$foreign_table_name will never be empty because it is needed for fetching the 
1224
        //related data
1225
        if( ($f_models_class_name === '') ) {
120✔
1226

1227
            $f_models_class_name = \LeanOrm\Model::class;
×
1228
        }
1229

1230
        try {
1231
            //try to create a model object for the related data
1232
            /** @psalm-suppress MixedMethodCall */
1233
            $related_model = new $f_models_class_name(
120✔
1234
                $this->dsn, 
120✔
1235
                $this->username, 
120✔
1236
                $this->passwd, 
120✔
1237
                $this->pdo_driver_opts,
120✔
1238
                $pri_key_col_in_f_models_table,
120✔
1239
                $f_table_name
120✔
1240
            );
120✔
1241
            
1242
        } catch (\GDAO\ModelPrimaryColNameNotSetDuringConstructionException) {
×
1243
            
1244
            $msg = "ERROR: Couldn't create foreign model of type '{$f_models_class_name}'."
×
1245
                 . "  No primary key supplied for the database table '{$f_table_name}'"
×
1246
                 . " associated with the foreign table class '{$f_models_class_name}'."
×
1247
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
×
1248
                 . PHP_EOL;
×
1249
            throw new \LeanOrm\Exceptions\RelatedModelNotCreatedException($msg);
×
1250
            
1251
        } catch (\GDAO\ModelTableNameNotSetDuringConstructionException) {
×
1252
            
1253
            $msg = "ERROR: Couldn't create foreign model of type '{$f_models_class_name}'."
×
1254
                 . "  No database table name supplied."
×
1255
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
×
1256
                 . PHP_EOL;
×
1257
            throw new \LeanOrm\Exceptions\RelatedModelNotCreatedException($msg);
×
1258
            
1259
        } catch (\LeanOrm\Exceptions\BadModelTableNameException) {
×
1260
            
1261
            $msg = "ERROR: Couldn't create foreign model of type '{$f_models_class_name}'."
×
1262
                 . " The supplied table name `{$f_table_name}` does not exist as a table or"
×
1263
                 . " view in the database."
×
1264
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
×
1265
                 . PHP_EOL;
×
1266
            throw new \LeanOrm\Exceptions\RelatedModelNotCreatedException($msg);
×
1267
            
1268
        } catch(\LeanOrm\Exceptions\BadModelPrimaryColumnNameException) {
×
1269
            
1270
            $msg = "ERROR: Couldn't create foreign model of type '{$f_models_class_name}'."
×
1271
                 . " The supplied primary key column `{$pri_key_col_in_f_models_table}` "
×
1272
                 . " does not exist in the supplied table named `{$f_table_name}`."
×
1273
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
×
1274
                 . PHP_EOL;
×
1275
            throw new \LeanOrm\Exceptions\RelatedModelNotCreatedException($msg);
×
1276
        }
1277
        
1278
        if($this->canLogQueries()) {
120✔
1279
            
1280
            // Transfer logger settings from this model
1281
            // to the newly created model
1282
            /** @psalm-suppress MixedMethodCall */
1283
            $related_model->enableQueryLogging();
8✔
1284
        }
1285
        
1286
        /** @psalm-suppress MixedMethodCall */
1287
        if( 
1288
            $this->getLogger() instanceof \Psr\Log\LoggerInterface
120✔
1289
            && $related_model->getLogger() === null
120✔
1290
        ) {
1291
            /** @psalm-suppress MixedMethodCall */
1292
            $related_model->setLogger($this->getLogger());
8✔
1293
        }
1294
        
1295
        /** @psalm-suppress LessSpecificReturnStatement */
1296
        return $related_model;
120✔
1297
    }
1298

1299
    /**
1300
     * @return mixed[]
1301
     */
1302
    protected function getColValsFromArrayOrCollection(
1303
        \GDAO\Model\CollectionInterface|array &$parent_data, 
1304
        string $fkey_col_in_my_table
1305
    ): array {
1306
        $col_vals = [];
64✔
1307

1308
        if ( is_array($parent_data) ) {
64✔
1309

1310
            /** @psalm-suppress MixedAssignment */
1311
            foreach($parent_data as $data) {
40✔
1312

1313
                /** 
1314
                 * @psalm-suppress MixedAssignment
1315
                 * @psalm-suppress MixedArrayAccess
1316
                 */
1317
                $col_vals[] = $data[$fkey_col_in_my_table];
40✔
1318
            }
1319

1320
        } elseif($parent_data instanceof \GDAO\Model\CollectionInterface) {
32✔
1321

1322
            $col_vals = $parent_data->getColVals($fkey_col_in_my_table);
32✔
1323
        }
1324

1325
        return $col_vals;
64✔
1326
    }
1327

1328
    /** @psalm-suppress ReferenceConstraintViolation */
1329
    protected function wrapRelatedDataInsideRecordsAndCollection(
1330
        array &$matching_related_records, Model $foreign_model_obj, 
1331
        bool $wrap_each_row_in_a_record, bool $wrap_records_in_collection
1332
    ): void {
1333
        
1334
        if( $wrap_each_row_in_a_record ) {
120✔
1335

1336
            //wrap into records of the appropriate class
1337
            /** @psalm-suppress MixedAssignment */
1338
            foreach ($matching_related_records as $key=>$rec_data) {
104✔
1339
                
1340
                // Mark as not new because this is a related row of data that 
1341
                // already exists in the db as opposed to a row of data that
1342
                // has never been saved to the db
1343
                /** @psalm-suppress MixedArgument */
1344
                $matching_related_records[$key] = 
104✔
1345
                    $foreign_model_obj->createNewRecord($rec_data)
104✔
1346
                                      ->markAsNotNew();
104✔
1347
            }
1348
        }
1349

1350
        if($wrap_records_in_collection) {
120✔
1351
            
1352
            /** @psalm-suppress MixedArgument */
1353
            $matching_related_records = $foreign_model_obj->createNewCollection(...$matching_related_records);
88✔
1354
        }
1355
    }
1356

1357
    /**
1358
     * 
1359
     * Fetches a collection by primary key value(s).
1360
     * 
1361
     *      # `$use_collections === true`: return a \LeanOrm\Model\Collection of 
1362
     *        \LeanOrm\Model\Record records each matching the values in $ids
1363
     * 
1364
     *      # `$use_collections === false`:
1365
     * 
1366
     *          - `$use_records === true`: return an array of \LeanOrm\Model\Record 
1367
     *            records each matching the values in $ids
1368
     * 
1369
     *          - `$use_records === false`: return an array of rows (each row being
1370
     *            an associative array) each matching the values in $ids
1371
     * 
1372
     * @param array $ids an array of scalar values of the primary key field of db rows to be fetched
1373
     * 
1374
     * @param string[] $relations_to_include names of relations to include
1375
     * 
1376
     * @param bool $use_records true if each matched db row should be wrapped in 
1377
     *                          an instance of \LeanOrm\Model\Record; false if 
1378
     *                          rows should be returned as associative php 
1379
     *                          arrays. If $use_collections === true, records
1380
     *                          will be returned inside a collection regardless
1381
     *                          of the value of $use_records
1382
     * 
1383
     * @param bool $use_collections true if each matched db row should be wrapped
1384
     *                              in an instance of \LeanOrm\Model\Record and 
1385
     *                              all the records wrapped in an instance of
1386
     *                              \LeanOrm\Model\Collection; false if all 
1387
     *                              matched db rows should be returned in a
1388
     *                              php array
1389
     * 
1390
     * @param bool $use_p_k_val_as_key true means the collection or array returned should be keyed on the primary key values
1391
     * 
1392
     */
1393
    public function fetch(
1394
        array $ids, 
1395
        ?\Aura\SqlQuery\Common\Select $select_obj=null, 
1396
        array $relations_to_include=[], 
1397
        bool $use_records=false, 
1398
        bool $use_collections=false, 
1399
        bool $use_p_k_val_as_key=false
1400
    ): \GDAO\Model\CollectionInterface|array {
1401
        
1402
        $select_obj ??= $this->createQueryObjectIfNullAndAddColsToQuery($select_obj);
8✔
1403
        
1404
        if( $ids !== [] ) {
8✔
1405
            
1406
            $_result = [];
8✔
1407
            $this->addWhereInAndOrIsNullToQuery($this->getPrimaryCol(), $ids, $select_obj);
8✔
1408

1409
            if( $use_collections ) {
8✔
1410

1411
                $_result = ($use_p_k_val_as_key) 
8✔
1412
                            ? $this->fetchRecordsIntoCollectionKeyedOnPkVal($select_obj, $relations_to_include) 
8✔
1413
                            : $this->fetchRecordsIntoCollection($select_obj, $relations_to_include);
8✔
1414

1415
            } else {
1416

1417
                if( $use_records ) {
8✔
1418

1419
                    $_result = ($use_p_k_val_as_key) 
8✔
1420
                                ? $this->fetchRecordsIntoArrayKeyedOnPkVal($select_obj, $relations_to_include) 
8✔
1421
                                : $this->fetchRecordsIntoArray($select_obj, $relations_to_include);
8✔
1422
                } else {
1423

1424
                    //default
1425
                    $_result = ($use_p_k_val_as_key) 
8✔
1426
                                ? $this->fetchRowsIntoArrayKeyedOnPkVal($select_obj, $relations_to_include) 
8✔
1427
                                : $this->fetchRowsIntoArray($select_obj, $relations_to_include);
8✔
1428
                } // if( $use_records ) else ...
1429
            } // if( $use_collections ) else ...
1430
            
1431
            /** @psalm-suppress TypeDoesNotContainType */
1432
            if(!($_result instanceof \GDAO\Model\CollectionInterface) && !is_array($_result)) {
8✔
1433
               
1434
                return $use_collections ? $this->createNewCollection() : [];
×
1435
            } 
1436
            
1437
            return $_result;
8✔
1438
            
1439
        } // if( $ids !== [] )
1440

1441
        // return empty collection or array
1442
        return $use_collections ? $this->createNewCollection() : [];
8✔
1443
    }
1444

1445
    /**
1446
     * {@inheritDoc}
1447
     */
1448
    #[\Override]
1449
    public function fetchRecordsIntoCollection(?object $query=null, array $relations_to_include=[]): \GDAO\Model\CollectionInterface {
1450

1451
        return $this->doFetchRecordsIntoCollection(
80✔
1452
                    ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null, 
80✔
1453
                    $relations_to_include
80✔
1454
                );
80✔
1455
    }
1456

1457
    public function fetchRecordsIntoCollectionKeyedOnPkVal(?\Aura\SqlQuery\Common\Select $select_obj=null, array $relations_to_include=[]): \GDAO\Model\CollectionInterface {
1458

1459
        return $this->doFetchRecordsIntoCollection($select_obj, $relations_to_include, true);
32✔
1460
    }
1461

1462
    /**
1463
     * @psalm-suppress InvalidReturnType
1464
     */
1465
    protected function doFetchRecordsIntoCollection(
1466
        ?\Aura\SqlQuery\Common\Select $select_obj=null, 
1467
        array $relations_to_include=[], 
1468
        bool $use_p_k_val_as_key=false
1469
    ): \GDAO\Model\CollectionInterface {
1470
        $results = $this->createNewCollection();
96✔
1471
        $data = $this->getArrayOfRecordObjects($select_obj, $use_p_k_val_as_key);
96✔
1472

1473
        if($data !== [] ) {
96✔
1474

1475
            if($use_p_k_val_as_key) {
96✔
1476
                
1477
                foreach ($data as $pkey => $current_record) {
32✔
1478
                    
1479
                    $results[$pkey] = $current_record;
32✔
1480
                }
1481
                
1482
            } else {
1483
               
1484
                $results = $this->createNewCollection(...$data);
80✔
1485
            }
1486
            
1487
            /** @psalm-suppress MixedAssignment */
1488
            foreach( $relations_to_include as $rel_name ) {
96✔
1489

1490
                /** @psalm-suppress MixedArgument */
1491
                $this->loadRelationshipData($rel_name, $results, true, true);
32✔
1492
            }
1493
        }
1494

1495
        /** @psalm-suppress InvalidReturnStatement */
1496
        return $results;
96✔
1497
    }
1498

1499
    /**
1500
     * {@inheritDoc}
1501
     */
1502
    #[\Override]
1503
    public function fetchRecordsIntoArray(?object $query=null, array $relations_to_include=[]): array {
1504
        
1505
        return $this->doFetchRecordsIntoArray(
24✔
1506
                    ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null, 
24✔
1507
                    $relations_to_include
24✔
1508
                );
24✔
1509
    }
1510

1511
    /**
1512
     * @return \GDAO\Model\RecordInterface[]
1513
     */
1514
    public function fetchRecordsIntoArrayKeyedOnPkVal(?\Aura\SqlQuery\Common\Select $select_obj=null, array $relations_to_include=[]): array {
1515
        
1516
        return $this->doFetchRecordsIntoArray($select_obj, $relations_to_include, true);
24✔
1517
    }
1518

1519
    /**
1520
     * @return \GDAO\Model\RecordInterface[]
1521
     * @psalm-suppress MixedReturnTypeCoercion
1522
     */
1523
    protected function doFetchRecordsIntoArray(
1524
        ?\Aura\SqlQuery\Common\Select $select_obj=null, 
1525
        array $relations_to_include=[], 
1526
        bool $use_p_k_val_as_key=false
1527
    ): array {
1528
        $results = $this->getArrayOfRecordObjects($select_obj, $use_p_k_val_as_key);
40✔
1529

1530
        if( $results !== [] ) {
40✔
1531

1532
            /** @psalm-suppress MixedAssignment */
1533
            foreach( $relations_to_include as $rel_name ) {
40✔
1534

1535
                /** @psalm-suppress MixedArgument */
1536
                $this->loadRelationshipData($rel_name, $results, true);
16✔
1537
            }
1538
        }
1539

1540
        return $results;
40✔
1541
    }
1542

1543
    /**
1544
     * @return \GDAO\Model\RecordInterface[]
1545
     * @psalm-suppress MixedReturnTypeCoercion
1546
     */
1547
    protected function getArrayOfRecordObjects(?\Aura\SqlQuery\Common\Select $select_obj=null, bool $use_p_k_val_as_key=false): array {
1548

1549
        $results = $this->getArrayOfDbRows($select_obj, $use_p_k_val_as_key);
128✔
1550

1551
        /** @psalm-suppress MixedAssignment */
1552
        foreach ($results as $key=>$value) {
128✔
1553

1554
            /** @psalm-suppress MixedArgument */
1555
            $results[$key] = $this->createNewRecord($value)->markAsNotNew();
128✔
1556
        }
1557
        
1558
        return $results;
128✔
1559
    }
1560

1561
    /**
1562
     * @return mixed[]
1563
     */
1564
    protected function getArrayOfDbRows(?\Aura\SqlQuery\Common\Select $select_obj=null, bool $use_p_k_val_as_key=false): array {
1565

1566
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery($select_obj);
168✔
1567
        $sql = $query_obj->__toString();
168✔
1568
        $params_2_bind_2_sql = $query_obj->getBindValues();
168✔
1569
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
168✔
1570
        
1571
        $results = $this->db_connector->dbFetchAll($sql, $params_2_bind_2_sql);
168✔
1572
        
1573
        if( $use_p_k_val_as_key && $results !== [] && $this->getPrimaryCol() !== '' ) {
168✔
1574

1575
            $results_keyed_by_pk = [];
56✔
1576

1577
            /** @psalm-suppress MixedAssignment */
1578
            foreach( $results as $result ) {
56✔
1579

1580
                /** @psalm-suppress MixedArgument */
1581
                if( !array_key_exists($this->getPrimaryCol(), $result) ) {
56✔
1582

1583
                    $msg = "ERROR: Can't key fetch results by Primary Key value."
×
1584
                         . PHP_EOL . " One or more result rows has no Primary Key field (`{$this->getPrimaryCol()}`)" 
×
1585
                         . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).'
×
1586
                         . PHP_EOL . 'Fetch Results:' . PHP_EOL . var_export($results, true) . PHP_EOL
×
1587
                         . PHP_EOL . "Row without Primary Key field (`{$this->getPrimaryCol()}`):" . PHP_EOL . var_export($result, true) . PHP_EOL;
×
1588

1589
                    throw new \LeanOrm\Exceptions\KeyingFetchResultsByPrimaryKeyFailedException($msg);
×
1590
                }
1591

1592
                // key on primary key value
1593
                /** @psalm-suppress MixedArrayOffset */
1594
                $results_keyed_by_pk[$result[$this->getPrimaryCol()]] = $result;
56✔
1595
            }
1596

1597
            $results = $results_keyed_by_pk;
56✔
1598
        }
1599

1600
        return $results;
168✔
1601
    }
1602

1603
    /**
1604
     * {@inheritDoc}
1605
     */
1606
    #[\Override]
1607
    public function fetchRowsIntoArray(?object $query=null, array $relations_to_include=[]): array {
1608

1609
        return $this->doFetchRowsIntoArray(
44✔
1610
                    ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null, 
44✔
1611
                    $relations_to_include
44✔
1612
                );
44✔
1613
    }
1614

1615
    /**
1616
     * @return array[]
1617
     */
1618
    public function fetchRowsIntoArrayKeyedOnPkVal(?\Aura\SqlQuery\Common\Select $select_obj=null, array $relations_to_include=[]): array {
1619

1620
        return $this->doFetchRowsIntoArray($select_obj, $relations_to_include, true);
16✔
1621
    }
1622

1623
    /**
1624
     * @return array[]
1625
     * @psalm-suppress MixedReturnTypeCoercion
1626
     */
1627
    protected function doFetchRowsIntoArray(
1628
        ?\Aura\SqlQuery\Common\Select $select_obj=null, 
1629
        array $relations_to_include=[], 
1630
        bool $use_p_k_val_as_key=false
1631
    ): array {
1632
        $results = $this->getArrayOfDbRows($select_obj, $use_p_k_val_as_key);
52✔
1633
        
1634
        if( $results !== [] ) {
52✔
1635
            
1636
            /** @psalm-suppress MixedAssignment */
1637
            foreach( $relations_to_include as $rel_name ) {
52✔
1638

1639
                /** @psalm-suppress MixedArgument */
1640
                $this->loadRelationshipData($rel_name, $results);
24✔
1641
            }
1642
        }
1643

1644
        return $results;
52✔
1645
    }
1646

1647
    #[\Override]
1648
    public function getPDO(): \PDO {
1649

1650
        //return pdo object associated with the current dsn
1651
        return DBConnector::getDb($this->dsn); 
1,308✔
1652
    }
1653

1654
    /**
1655
     * {@inheritDoc}
1656
     */
1657
    #[\Override]
1658
    public function deleteMatchingDbTableRows(array $cols_n_vals): int {
1659

1660
        $result = 0;
48✔
1661

1662
        if ( $cols_n_vals !== [] ) {
48✔
1663

1664
            //delete statement
1665
            $del_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newDelete();
48✔
1666
            $sel_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newSelect();
48✔
1667
            $del_qry_obj->from($this->getTableName());
48✔
1668
            $sel_qry_obj->from($this->getTableName());
48✔
1669
            $sel_qry_obj->cols([' count(*) ']);
48✔
1670
            $table_cols = $this->getTableColNames();
48✔
1671

1672
            /** @psalm-suppress MixedAssignment */
1673
            foreach ($cols_n_vals as $colname => $colval) {
48✔
1674

1675
                if(!in_array($colname, $table_cols)) {
48✔
1676

1677
                    // specified column is not a valid db table col, remove it
1678
                    unset($cols_n_vals[$colname]);
8✔
1679
                    continue;
8✔
1680
                }
1681

1682
                if (is_array($colval)) {
48✔
1683

1684
                    /** @psalm-suppress MixedAssignment */
1685
                    foreach($colval as $key=>$val) {
24✔
1686

1687
                        if(!$this->isAcceptableDeleteQueryValue($val)) {
24✔
1688

1689
                            $this->throwExceptionForInvalidDeleteQueryArg($val, $cols_n_vals);
8✔
1690
                        }
1691

1692
                        /** @psalm-suppress MixedAssignment */
1693
                        $colval[$key] = $this->stringifyIfStringable($val);
24✔
1694
                    }
1695

1696
                    $this->addWhereInAndOrIsNullToQuery(''.$colname, $colval, $del_qry_obj);
16✔
1697
                    $this->addWhereInAndOrIsNullToQuery(''.$colname, $colval, $sel_qry_obj);
16✔
1698

1699
                } else {
1700

1701
                    if(!$this->isAcceptableDeleteQueryValue($colval)) {
40✔
1702

1703
                        $this->throwExceptionForInvalidDeleteQueryArg($colval, $cols_n_vals);
8✔
1704
                    }
1705

1706
                    $del_qry_obj->where(" {$colname} = :{$colname} ", [$colname => $this->stringifyIfStringable($colval)]);
40✔
1707
                    $sel_qry_obj->where(" {$colname} = :{$colname} ", [$colname => $this->stringifyIfStringable($colval)]);
40✔
1708
                }
1709
            }
1710

1711
            // if at least one of the column names in the array is an actual 
1712
            // db table columns, then do delete
1713
            if($cols_n_vals !== []) {
32✔
1714

1715
                $dlt_qry = $del_qry_obj->__toString();
32✔
1716
                $dlt_qry_params = $del_qry_obj->getBindValues();
32✔
1717
                $this->logQuery($dlt_qry, $dlt_qry_params, __METHOD__, '' . __LINE__);
32✔
1718

1719
                $matching_rows_before_delete = (int) $this->fetchValue($sel_qry_obj);
32✔
1720

1721
                $this->db_connector->executeQuery($dlt_qry, $dlt_qry_params, true);
32✔
1722

1723
                $matching_rows_after_delete = (int) $this->fetchValue($sel_qry_obj);
32✔
1724

1725
                //number of deleted rows
1726
                $result = $matching_rows_before_delete - $matching_rows_after_delete;
32✔
1727
            } // if($cols_n_vals !== []) 
1728
        } // if ( $cols_n_vals !== [] )
1729

1730
        return $result;
32✔
1731
    }
1732
    
1733
    protected function throwExceptionForInvalidDeleteQueryArg(mixed $val, array $cols_n_vals): never {
1734

1735
        $msg = "ERROR: the value "
16✔
1736
             . PHP_EOL . var_export($val, true) . PHP_EOL
16✔
1737
             . " you are trying to use to bulid the where clause for deleting from the table `{$this->getTableName()}`"
16✔
1738
             . " is not acceptable ('".  gettype($val) . "'"
16✔
1739
             . " supplied). Boolean, NULL, numeric or string value expected."
16✔
1740
             . PHP_EOL
16✔
1741
             . "Data supplied to "
16✔
1742
             . static::class . '::' . __FUNCTION__ . '(...).' 
16✔
1743
             . " for buiding the where clause for the deletion:"
16✔
1744
             . PHP_EOL . var_export($cols_n_vals, true) . PHP_EOL
16✔
1745
             . PHP_EOL;
16✔
1746

1747
        throw new \LeanOrm\Exceptions\InvalidArgumentException($msg);
16✔
1748
    }
1749
    
1750
    /**
1751
     * {@inheritDoc}
1752
     */
1753
    #[\Override]
1754
    public function deleteSpecifiedRecord(\GDAO\Model\RecordInterface $record): ?bool {
1755

1756
        $succesfully_deleted = null;
40✔
1757

1758
        if( $record instanceof \LeanOrm\Model\ReadOnlyRecord ) {
40✔
1759

1760
            $msg = "ERROR: Can't delete ReadOnlyRecord from the database in " 
8✔
1761
                 . static::class . '::' . __FUNCTION__ . '(...).'
8✔
1762
                 . PHP_EOL .'Undeleted record' . var_export($record, true) . PHP_EOL;
8✔
1763
            throw new \LeanOrm\Exceptions\CantDeleteReadOnlyRecordFromDBException($msg);
8✔
1764
        }
1765
        
1766
        if( 
1767
            $record->getModel()->getTableName() !== $this->getTableName() 
32✔
1768
            || $record->getModel()::class !== static::class  
32✔
1769
        ) {
1770
            $msg = "ERROR: Can't delete a record (an instance of `%s` belonging to the Model class `%s`) belonging to the database table `%s` " 
16✔
1771
                . "using a Model instance of `%s` belonging to the database table `%s` in " 
16✔
1772
                 . static::class . '::' . __FUNCTION__ . '(...).'
16✔
1773
                 . PHP_EOL .'Undeleted record: ' . PHP_EOL . var_export($record, true) . PHP_EOL; 
16✔
1774
            throw new \LeanOrm\Exceptions\InvalidArgumentException(
16✔
1775
                sprintf(
16✔
1776
                    $msg, $record::class, $record->getModel()::class, 
16✔
1777
                    $record->getModel()->getTableName(),
16✔
1778
                    static::class, $this->getTableName()
16✔
1779
                )
16✔
1780
            );
16✔
1781
        }
1782

1783
        if ( $record->getData() !== [] ) { //test if the record object has data
16✔
1784

1785
            /** @psalm-suppress MixedAssignment */
1786
            $pri_key_val = $record->getPrimaryVal();
16✔
1787
            $cols_n_vals = [$record->getPrimaryCol() => $pri_key_val];
16✔
1788

1789
            $succesfully_deleted = 
16✔
1790
                $this->deleteMatchingDbTableRows($cols_n_vals);
16✔
1791

1792
            if ( $succesfully_deleted === 1 ) {
16✔
1793
                
1794
                $record->markAsNew();
16✔
1795
                
1796
                /** @psalm-suppress MixedAssignment */
1797
                foreach ($this->getRelationNames() as $relation_name) {
16✔
1798
                    
1799
                    // Remove all the related data since the primary key of the 
1800
                    // record may change or there may be ON DELETE CASACADE 
1801
                    // constraints that may have triggred those records being 
1802
                    // deleted from the db because of the deletion of this record
1803
                    /** @psalm-suppress MixedArrayOffset */
1804
                    unset($record[$relation_name]);
×
1805
                }
1806
                
1807
                /** @psalm-suppress MixedArrayAccess */
1808
                if( $this->table_cols[$record->getPrimaryCol()]['autoinc'] ) {
16✔
1809
                    
1810
                    // unset the primary key value for auto-incrementing
1811
                    // primary key cols. It is actually set to null via
1812
                    // Record::offsetUnset(..)
1813
                    unset($record[$this->getPrimaryCol()]); 
×
1814
                }
1815
                
1816
            } elseif($succesfully_deleted <= 0) {
16✔
1817
                
1818
                $succesfully_deleted = null;
16✔
1819
                
1820
            } elseif(
1821
                count($this->fetch([$pri_key_val], null, [], true, true)) >= 1 
×
1822
            ) {
1823
                
1824
                //we were still able to fetch the record from the db, so delete failed
1825
                $succesfully_deleted = false;
×
1826
            }
1827
        }
1828

1829
        return ( $succesfully_deleted >= 1 ) ? true : $succesfully_deleted;
16✔
1830
    }
1831

1832
    /**
1833
     * {@inheritDoc}
1834
     */
1835
    #[\Override]
1836
    public function fetchCol(?object $query=null): array {
1837

1838
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
48✔
1839
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
48✔
1840
        );
48✔
1841
        $sql = $query_obj->__toString();
48✔
1842
        $params_2_bind_2_sql = $query_obj->getBindValues();
48✔
1843
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
48✔
1844

1845
        return $this->db_connector->dbFetchCol($sql, $params_2_bind_2_sql);
48✔
1846
    }
1847

1848
    /**
1849
     * {@inheritDoc}
1850
     */
1851
    #[\Override]
1852
    public function fetchOneRecord(?object $query=null, array $relations_to_include=[]): ?\GDAO\Model\RecordInterface {
1853

1854
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
124✔
1855
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
124✔
1856
        );
124✔
1857
        $query_obj->limit(1);
124✔
1858

1859
        $sql = $query_obj->__toString();
124✔
1860
        $params_2_bind_2_sql = $query_obj->getBindValues();
124✔
1861
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
124✔
1862

1863
        /** @psalm-suppress MixedAssignment */
1864
        $result = $this->db_connector->dbFetchOne($sql, $params_2_bind_2_sql);
124✔
1865

1866
        if( $result !== false && is_array($result) && $result !== [] ) {
124✔
1867

1868
            $result = $this->createNewRecord($result)->markAsNotNew();
124✔
1869

1870
            foreach( $relations_to_include as $rel_name ) {
124✔
1871

1872
                $this->loadRelationshipData($rel_name, $result, true, true);
24✔
1873
            }
1874
        }
1875
        
1876
        if(!($result instanceof \GDAO\Model\RecordInterface)) {
124✔
1877
            
1878
            $result = null;
32✔
1879
        }
1880

1881
        return $result;
124✔
1882
    }
1883
    
1884
    /**
1885
     * Convenience method to fetch one record by the specified primary key value.
1886
     * @param string[] $relations_to_include names of relations to include
1887
     * @psalm-suppress PossiblyUnusedMethod
1888
     */
1889
    public function fetchOneByPkey(string|int $id, array $relations_to_include = []): ?\GDAO\Model\RecordInterface {
1890
        
1891
        $select = $this->getSelect();
8✔
1892
        $query_placeholder = "leanorm_{$this->getTableName()}_{$this->getPrimaryCol()}_val";
8✔
1893
        $select->where(
8✔
1894
            " {$this->getPrimaryCol()} = :{$query_placeholder} ", 
8✔
1895
            [ $query_placeholder => $id]
8✔
1896
        );
8✔
1897
        
1898
        return $this->fetchOneRecord($select, $relations_to_include);
8✔
1899
    }
1900

1901
    /**
1902
     * {@inheritDoc}
1903
     */
1904
    #[\Override]
1905
    public function fetchPairs(?object $query=null): array {
1906

1907
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
8✔
1908
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
8✔
1909
        );
8✔
1910
        $sql = $query_obj->__toString();
8✔
1911
        $params_2_bind_2_sql = $query_obj->getBindValues();
8✔
1912
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
8✔
1913

1914
        return $this->db_connector->dbFetchPairs($sql, $params_2_bind_2_sql);
8✔
1915
    }
1916

1917
    /**
1918
     * {@inheritDoc}
1919
     */
1920
    #[\Override]
1921
    public function fetchValue(?object $query=null): mixed {
1922

1923
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
56✔
1924
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
56✔
1925
        );
56✔
1926
        $query_obj->limit(1);
56✔
1927

1928
        $query_obj_4_num_matching_rows = clone $query_obj;
56✔
1929

1930
        $sql = $query_obj->__toString();
56✔
1931
        $params_2_bind_2_sql = $query_obj->getBindValues();
56✔
1932
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
56✔
1933

1934
        /** @psalm-suppress MixedAssignment */
1935
        $result = $this->db_connector->dbFetchValue($sql, $params_2_bind_2_sql);
56✔
1936

1937
        // need to issue a second query to get the number of matching rows
1938
        // clear the cols part of the query above while preserving all the
1939
        // other parts of the query
1940
        $query_obj_4_num_matching_rows->resetCols();
56✔
1941
        $query_obj_4_num_matching_rows->cols([' COUNT(*) AS num_rows']);
56✔
1942

1943
        $sql = $query_obj_4_num_matching_rows->__toString();
56✔
1944
        $params_2_bind_2_sql = $query_obj_4_num_matching_rows->getBindValues();
56✔
1945
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
56✔
1946

1947
        /** @psalm-suppress MixedAssignment */
1948
        $num_matching_rows = $this->db_connector->dbFetchOne($sql, $params_2_bind_2_sql);
56✔
1949

1950
        //return null if there wasn't any matching row
1951
        /** @psalm-suppress MixedArrayAccess */
1952
        return (((int)$num_matching_rows['num_rows']) > 0) ? $result : null;
56✔
1953
    }
1954
    
1955
    protected function addTimestampToData(array &$data, ?string $timestamp_col_name, array $table_cols): void {
1956
        
1957
        if(
1958
            ($timestamp_col_name !== null && $timestamp_col_name !== '' )
84✔
1959
            && in_array($timestamp_col_name, $table_cols)
84✔
1960
            && 
1961
            (
1962
                !array_key_exists($timestamp_col_name, $data)
84✔
1963
                || empty($data[$timestamp_col_name])
84✔
1964
            )
1965
        ) {
1966
            //set timestamp to now
1967
            $data[$timestamp_col_name] = date('Y-m-d H:i:s');
32✔
1968
        }
1969
    }
1970
    
1971
    protected function stringifyIfStringable(mixed $col_val, string $col_name='', array $table_cols=[]): mixed {
1972
        
1973
        if(
1974
            ( 
1975
                ($col_name === '' && $table_cols === []) 
124✔
1976
                || in_array($col_name, $table_cols) 
124✔
1977
            )
1978
            && is_object($col_val) && method_exists($col_val, '__toString')
124✔
1979
        ) {
1980
            return $col_val->__toString();
24✔
1981
        }
1982
        
1983
        return $col_val;
124✔
1984
    }
1985
        
1986
    protected function isAcceptableInsertValue(mixed $val): bool {
1987
        
1988
        return is_bool($val) || is_null($val) || is_numeric($val) || is_string($val)
124✔
1989
               || ( is_object($val) && method_exists($val, '__toString') );
124✔
1990
    }
1991
    
1992
    protected function isAcceptableUpdateValue(mixed $val): bool {
1993
        
1994
        return $this->isAcceptableInsertValue($val);
100✔
1995
    }
1996
    
1997
    protected function isAcceptableUpdateQueryValue(mixed $val): bool {
1998
        
1999
        return $this->isAcceptableUpdateValue($val);
92✔
2000
    }
2001
    
2002
    protected function isAcceptableDeleteQueryValue(mixed $val): bool {
2003
        
2004
        return $this->isAcceptableUpdateQueryValue($val);
48✔
2005
    }
2006

2007
    protected function processRowOfDataToInsert(
2008
        array &$data, array &$table_cols, bool &$has_autoinc_pk_col=false
2009
    ): void {
2010

2011
        $this->addTimestampToData($data, $this->created_timestamp_column_name, $table_cols);
52✔
2012
        $this->addTimestampToData($data, $this->updated_timestamp_column_name, $table_cols);
52✔
2013

2014
        // remove non-existent table columns from the data and also
2015
        // converts object values for objects with __toString() to 
2016
        // their string value
2017
        /** @psalm-suppress MixedAssignment */
2018
        foreach ($data as $key => $val) {
52✔
2019

2020
            /** @psalm-suppress MixedAssignment */
2021
            $data[$key] = $this->stringifyIfStringable($val, ''.$key, $table_cols);
52✔
2022

2023
            if ( !in_array($key, $table_cols) ) {
52✔
2024

2025
                unset($data[$key]);
24✔
2026
                // not in the table, so no need to check for autoinc
2027
                continue;
24✔
2028

2029
            } elseif( !$this->isAcceptableInsertValue($val) ) {
52✔
2030

2031
                $msg = "ERROR: the value "
16✔
2032
                     . PHP_EOL . var_export($val, true) . PHP_EOL
16✔
2033
                     . " you are trying to insert into `{$this->getTableName()}`."
16✔
2034
                     . "`{$key}` is not acceptable ('".  gettype($val) . "'"
16✔
2035
                     . " supplied). Boolean, NULL, numeric or string value expected."
16✔
2036
                     . PHP_EOL
16✔
2037
                     . "Data supplied to "
16✔
2038
                     . static::class . '::' . __FUNCTION__ . '(...).' 
16✔
2039
                     . " for insertion:"
16✔
2040
                     . PHP_EOL . var_export($data, true) . PHP_EOL
16✔
2041
                     . PHP_EOL;
16✔
2042

2043
                throw new \GDAO\ModelInvalidInsertValueSuppliedException($msg);
16✔
2044
            }
2045

2046
            // Code below was lifted from Solar_Sql_Model::insert()
2047
            // remove empty autoinc columns to soothe postgres, which won't
2048
            // take explicit NULLs in SERIAL cols.
2049
            /** @psalm-suppress MixedArrayAccess */
2050
            if ( $this->table_cols[$key]['autoinc'] && empty($val) ) {
52✔
2051

2052
                unset($data[$key]);
×
2053

2054
            } // if ( $this->table_cols[$key]['autoinc'] && empty($val) )
2055
        } // foreach ($data as $key => $val)
2056

2057
        /** @psalm-suppress MixedAssignment */
2058
        foreach($this->table_cols as $col_name=>$col_info) {
36✔
2059

2060
            /** @psalm-suppress MixedArrayAccess */
2061
            if ( $col_info['autoinc'] === true && $col_info['primary'] === true ) {
36✔
2062

2063
                if(array_key_exists($col_name, $data)) {
×
2064

2065
                    //no need to add primary key value to the insert 
2066
                    //statement since the column is auto incrementing
2067
                    unset($data[$col_name]);
×
2068

2069
                } // if(array_key_exists($col_name, $data_2_insert))
2070

2071
                $has_autoinc_pk_col = true;
×
2072

2073
            } // if ( $col_info['autoinc'] === true && $col_info['primary'] === true )
2074
        } // foreach($this->table_cols as $col_name=>$col_info)
2075
    }
2076
    
2077
    protected function updateInsertDataArrayWithTheNewlyInsertedRecordFromDB(
2078
        array &$data_2_insert, array $table_cols
2079
    ): void {
2080
        
2081
        if(
2082
            array_key_exists($this->getPrimaryCol(), $data_2_insert)
20✔
2083
            && !empty($data_2_insert[$this->getPrimaryCol()])
20✔
2084
        ) {
2085
            $record = $this->fetchOneRecord(
8✔
2086
                        $this->getSelect()
8✔
2087
                             ->where(
8✔
2088
                                " {$this->getPrimaryCol()} = :{$this->getPrimaryCol()} ",
8✔
2089
                                [ $this->getPrimaryCol() => $data_2_insert[$this->getPrimaryCol()]]
8✔
2090
                             )
8✔
2091
                     );
8✔
2092
            $data_2_insert = ($record instanceof \GDAO\Model\RecordInterface) ? $record->getData() :  $data_2_insert;
8✔
2093
            
2094
        } else {
2095

2096
            // we don't have the primary key.
2097
            // Do a select using all the fields.
2098
            // If only one record is returned, we have found
2099
            // the record we just inserted, else we return $data_2_insert as is 
2100

2101
            $select = $this->getSelect();
20✔
2102

2103
            /** @psalm-suppress MixedAssignment */
2104
            foreach ($data_2_insert as $col => $val) {
20✔
2105

2106
                /** @psalm-suppress MixedAssignment */
2107
                $processed_val = $this->stringifyIfStringable($val, ''.$col, $table_cols);
20✔
2108

2109
                if(is_string($processed_val) || is_numeric($processed_val)) {
20✔
2110

2111
                    $select->where(" {$col} = :{$col} ", [$col=>$val]);
20✔
2112

2113
                } elseif(is_null($processed_val) && $this->getPrimaryCol() !== $col) {
8✔
2114

2115
                    $select->where(" {$col} IS NULL ");
8✔
2116
                } // if(is_string($processed_val) || is_numeric($processed_val))
2117
            } // foreach ($data_2_insert as $col => $val)
2118

2119
            $matching_rows = $this->fetchRowsIntoArray($select);
20✔
2120

2121
            if(count($matching_rows) === 1) {
20✔
2122

2123
                /** @psalm-suppress MixedAssignment */
2124
                $data_2_insert = array_pop($matching_rows);
20✔
2125
            }
2126
        }
2127
    }
2128

2129
    /**
2130
     * {@inheritDoc}
2131
     */
2132
    #[\Override]
2133
    public function insert(array $data_2_insert = []): bool|array {
2134
        
2135
        $result = false;
28✔
2136

2137
        if ( $data_2_insert !== [] ) {
28✔
2138

2139
            $table_cols = $this->getTableColNames();
28✔
2140
            $has_autoinc_pkey_col=false;
28✔
2141

2142
            $this->processRowOfDataToInsert(
28✔
2143
                $data_2_insert, $table_cols, $has_autoinc_pkey_col
28✔
2144
            );
28✔
2145

2146
            // Do we still have anything left to save after removing items
2147
            // in the array that do not map to actual db table columns
2148
            /**
2149
             * @psalm-suppress RedundantCondition
2150
             * @psalm-suppress TypeDoesNotContainType
2151
             */
2152
            if( (is_countable($data_2_insert) ? count($data_2_insert) : 0) > 0 ) {
20✔
2153

2154
                //Insert statement
2155
                $insrt_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newInsert();
20✔
2156
                $insrt_qry_obj->into($this->getTableName())->cols($data_2_insert);
20✔
2157

2158
                $insrt_qry_sql = $insrt_qry_obj->__toString();
20✔
2159
                $insrt_qry_params = $insrt_qry_obj->getBindValues();
20✔
2160
                $this->logQuery($insrt_qry_sql, $insrt_qry_params, __METHOD__, '' . __LINE__);
20✔
2161

2162
                if( $this->db_connector->executeQuery($insrt_qry_sql, $insrt_qry_params) ) {
20✔
2163

2164
                    // insert was successful, we are now going to try to 
2165
                    // fetch the inserted record from the db to get and 
2166
                    // return the db representation of the data
2167
                    if($has_autoinc_pkey_col) {
20✔
2168

2169
                        /** @psalm-suppress MixedAssignment */
2170
                        $last_insert_sequence_name = 
×
2171
                            $insrt_qry_obj->getLastInsertIdName($this->getPrimaryCol());
×
2172

2173
                        $pk_val_4_new_record = 
×
2174
                            $this->getPDO()->lastInsertId(is_string($last_insert_sequence_name) ? $last_insert_sequence_name : null);
×
2175

2176
                        // Add retrieved primary key value 
2177
                        // or null (if primary key value is empty) 
2178
                        // to the data to be returned.
2179
                        $data_2_insert[$this->primary_col] = 
×
2180
                            empty($pk_val_4_new_record) ? null : $pk_val_4_new_record;
×
2181

2182
                        $this->updateInsertDataArrayWithTheNewlyInsertedRecordFromDB(
×
2183
                            $data_2_insert, $table_cols
×
2184
                        );
×
2185

2186
                    } else {
2187

2188
                        $this->updateInsertDataArrayWithTheNewlyInsertedRecordFromDB(
20✔
2189
                            $data_2_insert, $table_cols
20✔
2190
                        );
20✔
2191

2192
                    } // if($has_autoinc_pkey_col)
2193

2194
                    //insert was successful
2195
                    $result = $data_2_insert;
20✔
2196

2197
                } // if( $this->db_connector->executeQuery($insrt_qry_sql, $insrt_qry_params) )
2198
            } // if(count($data_2_insert) > 0 ) 
2199
        } // if ( $data_2_insert !== [] )
2200
        
2201
        return $result;
20✔
2202
    }
2203

2204
    /**
2205
     * {@inheritDoc}
2206
     */
2207
    #[\Override]
2208
    public function insertMany(array $rows_of_data_2_insert = []): bool {
2209

2210
        $result = false;
36✔
2211

2212
        if ($rows_of_data_2_insert !== []) {
36✔
2213

2214
            $table_cols = $this->getTableColNames();
36✔
2215

2216
            foreach (array_keys($rows_of_data_2_insert) as $key) {
36✔
2217

2218
                if( !is_array($rows_of_data_2_insert[$key]) ) {
36✔
2219

2220
                    $item_type = gettype($rows_of_data_2_insert[$key]);
8✔
2221

2222
                    $msg = "ERROR: " . static::class . '::' . __FUNCTION__ . '(...)' 
8✔
2223
                         . " expects you to supply an array of arrays."
8✔
2224
                         . " One of the items in the array supplied is not an array."
8✔
2225
                         . PHP_EOL . " Item below of type `{$item_type}` is not an array: "
8✔
2226
                         . PHP_EOL . var_export($rows_of_data_2_insert[$key], true) 
8✔
2227
                         . PHP_EOL . PHP_EOL . "Data supplied to "
8✔
2228
                         . static::class . '::' . __FUNCTION__ . '(...).' 
8✔
2229
                         . " for insertion into the db table `{$this->getTableName()}`:"
8✔
2230
                         . PHP_EOL . var_export($rows_of_data_2_insert, true) . PHP_EOL
8✔
2231
                         . PHP_EOL;
8✔
2232

2233
                    throw new \GDAO\ModelInvalidInsertValueSuppliedException($msg);
8✔
2234
                }
2235

2236
                $this->processRowOfDataToInsert($rows_of_data_2_insert[$key], $table_cols);
28✔
2237

2238
                /** 
2239
                 * @psalm-suppress TypeDoesNotContainType
2240
                 * @psalm-suppress RedundantCondition
2241
                 */
2242
                if((is_countable($rows_of_data_2_insert[$key]) ? count($rows_of_data_2_insert[$key]) : 0) === 0) {
20✔
2243

2244
                    // all the keys in the curent row of data aren't valid
2245
                    // db table columns, remove the row of data from the 
2246
                    // data to be inserted into the DB.
2247
                    unset($rows_of_data_2_insert[$key]);
8✔
2248

2249
                } // if(count($rows_of_data_2_insert[$key]) === 0)
2250

2251
            } // foreach ($rows_of_data_2_insert as $key=>$row_2_insert)
2252

2253
            // do we still have any data left to insert after all the filtration above?
2254
            if($rows_of_data_2_insert !== []) {
20✔
2255

2256
                //Insert statement
2257
                $insrt_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newInsert();
20✔
2258

2259
                //Batch all the data into one insert query.
2260
                $insrt_qry_obj->into($this->getTableName())->addRows($rows_of_data_2_insert);           
20✔
2261
                $insrt_qry_sql = $insrt_qry_obj->__toString();
20✔
2262
                $insrt_qry_params = $insrt_qry_obj->getBindValues();
20✔
2263

2264
                $this->logQuery($insrt_qry_sql, $insrt_qry_params, __METHOD__, '' . __LINE__);
20✔
2265
                $result = (bool) $this->db_connector->executeQuery($insrt_qry_sql, $insrt_qry_params);
20✔
2266

2267
            } // if(count($rows_of_data_2_insert) > 0)
2268
        } // if ($rows_of_data_2_insert !== [])
2269

2270
        return $result;
20✔
2271
    }
2272
    
2273
    protected function throwExceptionForInvalidUpdateQueryArg(mixed $val, array $cols_n_vals): never {
2274

2275
        $msg = "ERROR: the value "
16✔
2276
             . PHP_EOL . var_export($val, true) . PHP_EOL
16✔
2277
             . " you are trying to use to bulid the where clause for updating the table `{$this->getTableName()}`"
16✔
2278
             . " is not acceptable ('".  gettype($val) . "'"
16✔
2279
             . " supplied). Boolean, NULL, numeric or string value expected."
16✔
2280
             . PHP_EOL
16✔
2281
             . "Data supplied to "
16✔
2282
             . static::class . '::' . __FUNCTION__ . '(...).' 
16✔
2283
             . " for buiding the where clause for the update:"
16✔
2284
             . PHP_EOL . var_export($cols_n_vals, true) . PHP_EOL
16✔
2285
             . PHP_EOL;
16✔
2286

2287
        throw new \LeanOrm\Exceptions\InvalidArgumentException($msg);
16✔
2288
    }
2289
    
2290
    /**
2291
     * {@inheritDoc}
2292
     * @psalm-suppress RedundantCondition
2293
     */
2294
    #[\Override]
2295
    public function updateMatchingDbTableRows(
2296
        array $col_names_n_values_2_save = [],
2297
        array $col_names_n_values_2_match = []
2298
    ): static {
2299
        $num_initial_match_items = count($col_names_n_values_2_match);
52✔
2300

2301
        if ($col_names_n_values_2_save !== []) {
52✔
2302

2303
            $table_cols = $this->getTableColNames();
52✔
2304
            $pkey_col_name = $this->getPrimaryCol();
52✔
2305
            $this->addTimestampToData(
52✔
2306
                $col_names_n_values_2_save, $this->updated_timestamp_column_name, $table_cols
52✔
2307
            );
52✔
2308

2309
            if(array_key_exists($pkey_col_name, $col_names_n_values_2_save)) {
52✔
2310

2311
                //don't update the primary key
2312
                unset($col_names_n_values_2_save[$pkey_col_name]);
28✔
2313
            }
2314

2315
            // remove non-existent table columns from the data
2316
            // and check that existent table columns have values of  
2317
            // the right data type: ie. Boolean, NULL, Number or String.
2318
            // Convert objects with a __toString to their string value.
2319
            /** @psalm-suppress MixedAssignment */
2320
            foreach ($col_names_n_values_2_save as $key => $val) {
52✔
2321

2322
                /** @psalm-suppress MixedAssignment */
2323
                $col_names_n_values_2_save[$key] = 
52✔
2324
                    $this->stringifyIfStringable($val, ''.$key, $table_cols);
52✔
2325

2326
                if ( !in_array($key, $table_cols) ) {
52✔
2327

2328
                    unset($col_names_n_values_2_save[$key]);
8✔
2329

2330
                } else if( !$this->isAcceptableUpdateValue($val) ) {
52✔
2331

2332
                    $msg = "ERROR: the value "
8✔
2333
                         . PHP_EOL . var_export($val, true) . PHP_EOL
8✔
2334
                         . " you are trying to update `{$this->getTableName()}`.`{$key}`."
8✔
2335
                         . "{$key} with is not acceptable ('".  gettype($val) . "'"
8✔
2336
                         . " supplied). Boolean, NULL, numeric or string value expected."
8✔
2337
                         . PHP_EOL
8✔
2338
                         . "Data supplied to "
8✔
2339
                         . static::class . '::' . __FUNCTION__ . '(...).' 
8✔
2340
                         . " for update:"
8✔
2341
                         . PHP_EOL . var_export($col_names_n_values_2_save, true) . PHP_EOL
8✔
2342
                         . PHP_EOL;
8✔
2343

2344
                    throw new \GDAO\ModelInvalidUpdateValueSuppliedException($msg);
8✔
2345
                } // if ( !in_array($key, $table_cols) )
2346
            } // foreach ($col_names_n_vals_2_save as $key => $val)
2347

2348
            // After filtering out non-table columns, if we have any table
2349
            // columns data left, we can do the update
2350
            if($col_names_n_values_2_save !== []) {
44✔
2351

2352
                //update statement
2353
                $update_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newUpdate();
44✔
2354
                $update_qry_obj->table($this->getTableName());
44✔
2355
                $update_qry_obj->cols($col_names_n_values_2_save);
44✔
2356

2357
                /** @psalm-suppress MixedAssignment */
2358
                foreach ($col_names_n_values_2_match as $colname => $colval) {
44✔
2359

2360
                    if(!in_array($colname, $table_cols)) {
44✔
2361

2362
                        //non-existent table column
2363
                        unset($col_names_n_values_2_match[$colname]);
8✔
2364
                        continue;
8✔
2365
                    }
2366

2367
                    if (is_array($colval)) {
44✔
2368

2369
                        if($colval !== []) {
16✔
2370

2371
                            /** @psalm-suppress MixedAssignment */
2372
                            foreach ($colval as $key=>$val) {
16✔
2373

2374
                                if(!$this->isAcceptableUpdateQueryValue($val)) {
16✔
2375

2376
                                    $this->throwExceptionForInvalidUpdateQueryArg(
8✔
2377
                                            $val, $col_names_n_values_2_match
8✔
2378
                                        );
8✔
2379
                                }
2380

2381
                                /** @psalm-suppress MixedAssignment */
2382
                                $colval[$key] = $this->stringifyIfStringable($val);
16✔
2383
                            }
2384

2385
                            $this->addWhereInAndOrIsNullToQuery(''.$colname, $colval, $update_qry_obj);
8✔
2386

2387
                        } // if($colval !== []) 
2388

2389
                    } else {
2390

2391
                        if(!$this->isAcceptableUpdateQueryValue($colval)) {
44✔
2392

2393
                            $this->throwExceptionForInvalidUpdateQueryArg(
8✔
2394
                                    $colval, $col_names_n_values_2_match
8✔
2395
                                );
8✔
2396
                        }
2397

2398
                        if(is_null($colval)) {
44✔
2399

2400
                            $update_qry_obj->where(
8✔
2401
                                " {$colname} IS NULL "
8✔
2402
                            );
8✔
2403

2404
                        } else {
2405

2406
                            $update_qry_obj->where(
44✔
2407
                                " {$colname} = :{$colname}_for_where ",  // add the _for_where suffix to deconflict where bind value, 
44✔
2408
                                                                         // from the set bind value when a column in the where clause
2409
                                                                         // is also being set and the value we are setting it to is 
2410
                                                                         // different from the value we are using for the same column 
2411
                                                                         // in the where clause
2412
                                ["{$colname}_for_where" => $this->stringifyIfStringable($colval)] 
44✔
2413
                            );
44✔
2414
                        }
2415

2416
                    } // if (is_array($colval))
2417
                } // foreach ($col_names_n_vals_2_match as $colname => $colval)
2418

2419
                // If after filtering out non existing cols in $col_names_n_vals_2_match
2420
                // if there is still data left in $col_names_n_vals_2_match, then
2421
                // finish building the update query and do the update
2422
                if( 
2423
                    $col_names_n_values_2_match !== [] // there are valid db table cols in here
28✔
2424
                    || 
2425
                    (
2426
                        $num_initial_match_items === 0
28✔
2427
                        && $col_names_n_values_2_match === [] // empty match array passed, we are updating all rows
28✔
2428
                    )
2429
                ) {
2430
                    $updt_qry = $update_qry_obj->__toString();
28✔
2431
                    $updt_qry_params = $update_qry_obj->getBindValues();
28✔
2432
                    $this->logQuery($updt_qry, $updt_qry_params, __METHOD__, '' . __LINE__);
28✔
2433

2434
                    $this->db_connector->executeQuery($updt_qry, $updt_qry_params, true);
28✔
2435
                }
2436

2437
            } // if($col_names_n_vals_2_save !== [])
2438
        } // if ($col_names_n_vals_2_save !== [])
2439

2440
        return $this;
28✔
2441
    }
2442

2443
    /**
2444
     * {@inheritDoc}
2445
     * @psalm-suppress UnusedVariable
2446
     */
2447
    #[\Override]
2448
    public function updateSpecifiedRecord(\GDAO\Model\RecordInterface $record): static {
2449
        
2450
        if( $record instanceof \LeanOrm\Model\ReadOnlyRecord ) {
44✔
2451

2452
            $msg = "ERROR: Can't save a ReadOnlyRecord to the database in " 
8✔
2453
                 . static::class . '::' . __FUNCTION__ . '(...).'
8✔
2454
                 . PHP_EOL .'Unupdated record' . var_export($record, true) . PHP_EOL;
8✔
2455
            throw new \LeanOrm\Exceptions\CantSaveReadOnlyRecordException($msg);
8✔
2456
        }
2457
        
2458
        if( $record->getModel()->getTableName() !== $this->getTableName() ) {
36✔
2459
            
2460
            $msg = "ERROR: Can't update a record (an instance of `%s`) belonging to the database table `%s` " 
8✔
2461
                . "using a Model instance of `%s` belonging to the database table `%s` in " 
8✔
2462
                 . static::class . '::' . __FUNCTION__ . '(...).'
8✔
2463
                 . PHP_EOL .'Unupdated record: ' . PHP_EOL . var_export($record, true) . PHP_EOL; 
8✔
2464
            throw new \GDAO\ModelInvalidUpdateValueSuppliedException(
8✔
2465
                sprintf(
8✔
2466
                    $msg, $record::class, $record->getModel()->getTableName(),
8✔
2467
                    static::class, $this->getTableName()
8✔
2468
                )
8✔
2469
            );
8✔
2470
        }
2471

2472
        /** @psalm-suppress MixedAssignment */
2473
        $pri_key_val = $record->getPrimaryVal();
28✔
2474
        
2475
        /** @psalm-suppress MixedOperand */
2476
        if( 
2477
            count($record) > 0  // There is data in the record
28✔
2478
            && !$record->isNew() // This is not a new record that wasn't fetched from the DB
28✔
2479
            && !Utils::isEmptyString(''.$pri_key_val) // Record has a primary key value
28✔
2480
            && $record->isChanged() // The data in the record has changed from the state it was when initially fetched from DB
28✔
2481
        ) {
2482
            $cols_n_vals_2_match = [$record->getPrimaryCol()=>$pri_key_val];
28✔
2483

2484
            if($this->getUpdatedTimestampColumnName() !== null) {
28✔
2485

2486
                // Record has changed value(s) & must definitely be updated.
2487
                // Set the value of the $this->getUpdatedTimestampColumnName()
2488
                // field to an empty string, force updateMatchingDbTableRows
2489
                // to add a new updated timestamp value during the update.
2490
                $record->{$this->getUpdatedTimestampColumnName()} = '';
8✔
2491
            }
2492

2493
            $data_2_save = $record->getData();
28✔
2494
            $this->updateMatchingDbTableRows(
28✔
2495
                $data_2_save, 
28✔
2496
                $cols_n_vals_2_match
28✔
2497
            );
28✔
2498

2499
            // update the record with the new updated copy from the DB
2500
            // which will contain the new updated timestamp value.
2501
            $record = $this->fetchOneRecord(
28✔
2502
                        $this->getSelect()
28✔
2503
                             ->where(
28✔
2504
                                    " {$record->getPrimaryCol()} = :{$record->getPrimaryCol()} ", 
28✔
2505
                                    [$record->getPrimaryCol() => $record->getPrimaryVal()]
28✔
2506
                                )
28✔
2507
                    );
28✔
2508
        } // if( count($record) > 0 && !$record->isNew()........
2509

2510
        return $this;
28✔
2511
    }
2512

2513
    /**
2514
     * @psalm-suppress RedundantConditionGivenDocblockType
2515
     */
2516
    protected function addWhereInAndOrIsNullToQuery(
2517
        string $colname, array &$colvals, \Aura\SqlQuery\Common\WhereInterface $qry_obj
2518
    ): void {
2519
        
2520
        if($colvals !== []) { // make sure it's a non-empty array
88✔
2521
            
2522
            // if there are one or more null values in the array,
2523
            // we need to unset them and add an
2524
            //      OR $colname IS NULL 
2525
            // clause to the query
2526
            $unique_colvals = array_unique($colvals);
88✔
2527
            $keys_for_null_vals = array_keys($unique_colvals, null, true);
88✔
2528

2529
            foreach($keys_for_null_vals as $key_for_null_val) {
88✔
2530

2531
                // remove the null vals from $colval
2532
                unset($unique_colvals[$key_for_null_val]);
8✔
2533
            }
2534

2535
            if(
2536
                $keys_for_null_vals !== [] && $unique_colvals !== []
88✔
2537
            ) {
2538
                // Some values in the array are null and some are non-null
2539
                // Generate WHERE COL IN () OR COL IS NULL
2540
                $qry_obj->where(
8✔
2541
                    " {$colname} IN (:bar) ",
8✔
2542
                    [ 'bar' => $unique_colvals ]
8✔
2543
                )->orWhere(" {$colname} IS NULL ");
8✔
2544

2545
            } elseif (
2546
                $keys_for_null_vals !== []
88✔
2547
                && $unique_colvals === []
88✔
2548
            ) {
2549
                // All values in the array are null
2550
                // Only generate WHERE COL IS NULL
2551
                $qry_obj->where(" {$colname} IS NULL ");
8✔
2552

2553
            } else { // ($keys_for_null_vals === [] && $unique_colvals !== []) // no nulls found
2554
                
2555
                ////////////////////////////////////////////////////////////////
2556
                // NOTE: ($keys_for_null_vals === [] && $unique_colvals === [])  
2557
                // is impossible because we started with if($colvals !== [])
2558
                ////////////////////////////////////////////////////////////////
2559

2560
                // All values in the array are non-null
2561
                // Only generate WHERE COL IN ()
2562
                $qry_obj->where(       
88✔
2563
                    " {$colname} IN (:bar) ",
88✔
2564
                    [ 'bar' => $unique_colvals ]
88✔
2565
                );
88✔
2566
            }
2567
        }
2568
    }
2569
    
2570
    /**
2571
     * @return array{
2572
     *              database_server_info: mixed, 
2573
     *              driver_name: mixed, 
2574
     *              pdo_client_version: mixed, 
2575
     *              database_server_version: mixed, 
2576
     *              connection_status: mixed, 
2577
     *              connection_is_persistent: mixed
2578
     *          }
2579
     * 
2580
     * @psalm-suppress PossiblyUnusedMethod
2581
     */
2582
    public function getCurrentConnectionInfo(): array {
2583

2584
        $pdo_obj = $this->getPDO();
8✔
2585
        $attributes = [
8✔
2586
            'database_server_info' => 'SERVER_INFO',
8✔
2587
            'driver_name' => 'DRIVER_NAME',
8✔
2588
            'pdo_client_version' => 'CLIENT_VERSION',
8✔
2589
            'database_server_version' => 'SERVER_VERSION',
8✔
2590
            'connection_status' => 'CONNECTION_STATUS',
8✔
2591
            'connection_is_persistent' => 'PERSISTENT',
8✔
2592
        ];
8✔
2593

2594
        foreach ($attributes as $key => $value) {
8✔
2595
            
2596
            try {
2597
                /**
2598
                 * @psalm-suppress MixedAssignment
2599
                 * @psalm-suppress MixedArgument
2600
                 */
2601
                $attributes[ $key ] = $pdo_obj->getAttribute(constant(\PDO::class .'::ATTR_' . $value));
8✔
2602
                
2603
            } catch (\PDOException) {
8✔
2604
                
2605
                $attributes[ $key ] = 'Unsupported attribute for the current PDO driver';
8✔
2606
                continue;
8✔
2607
            }
2608

2609
            if( $value === 'PERSISTENT' ) {
8✔
2610

2611
                $attributes[ $key ] = var_export($attributes[ $key ], true);
8✔
2612
            }
2613
        }
2614

2615
        return $attributes;
8✔
2616
    }
2617

2618
    /**
2619
     * @return mixed[]
2620
     * @psalm-suppress PossiblyUnusedMethod
2621
     */
2622
    public function getQueryLog(): array {
2623

2624
        return $this->query_log;
8✔
2625
    }
2626

2627
    /**
2628
     * To get the log for all existing instances of this class & its subclasses,
2629
     * call this method with no args or with null.
2630
     * 
2631
     * To get the log for instances of a specific class (this class or a
2632
     * particular sub-class of this class), you must call this method with 
2633
     * an instance of the class whose log you want to get.
2634
     * 
2635
     * @return mixed[]
2636
     * @psalm-suppress PossiblyUnusedMethod
2637
     */
2638
    public static function getQueryLogForAllInstances(?\GDAO\Model $obj=null): array {
2639
        
2640
        $key = ($obj instanceof \GDAO\Model) ? static::createLoggingKey($obj) : '';
16✔
2641
        
2642
        return ($obj instanceof \GDAO\Model)
16✔
2643
                ?
16✔
2644
                (
16✔
2645
                    array_key_exists($key, static::$all_instances_query_log) 
8✔
2646
                    ? static::$all_instances_query_log[$key] : [] 
8✔
2647
                )
16✔
2648
                : static::$all_instances_query_log 
16✔
2649
                ;
16✔
2650
    }
2651
    
2652
    /**
2653
     * @psalm-suppress PossiblyUnusedMethod
2654
     */
2655
    public static function clearQueryLogForAllInstances(): void {
2656
        
2657
        static::$all_instances_query_log = [];
24✔
2658
    }
2659

2660
    protected static function createLoggingKey(\GDAO\Model $obj): string {
2661
        
2662
        return "{$obj->getDsn()}::" . $obj::class;
32✔
2663
    }
2664
    
2665
    protected function logQuery(string $sql, array $bind_params, string $calling_method='', string $calling_line=''): static {
2666

2667
        if( $this->can_log_queries ) {
296✔
2668

2669
            $key = static::createLoggingKey($this);
32✔
2670
            
2671
            if(!array_key_exists($key, static::$all_instances_query_log)) {
32✔
2672

2673
                static::$all_instances_query_log[$key] = [];
32✔
2674
            }
2675

2676
            $log_record = [
32✔
2677
                'sql' => $sql,
32✔
2678
                'bind_params' => $bind_params,
32✔
2679
                'date_executed' => date('Y-m-d H:i:s'),
32✔
2680
                'class_method' => $calling_method,
32✔
2681
                'line_of_execution' => $calling_line,
32✔
2682
            ];
32✔
2683
            
2684
            /** @psalm-suppress InvalidPropertyAssignmentValue */
2685
            $this->query_log[] = $log_record;
32✔
2686
            static::$all_instances_query_log[$key][] = $log_record;
32✔
2687

2688
            if($this->logger instanceof \Psr\Log\LoggerInterface) {
32✔
2689

2690
                $this->logger->info(
8✔
2691
                    PHP_EOL . PHP_EOL .
8✔
2692
                    'SQL:' . PHP_EOL . "{$sql}" . PHP_EOL . PHP_EOL . PHP_EOL .
8✔
2693
                    'BIND PARAMS:' . PHP_EOL . var_export($bind_params, true) .
8✔
2694
                    PHP_EOL . "Calling Method: `{$calling_method}`" . PHP_EOL .
8✔
2695
                    "Line of Execution: `{$calling_line}`" . PHP_EOL .
8✔
2696
                     PHP_EOL . PHP_EOL . PHP_EOL
8✔
2697
                );
8✔
2698
            }                    
2699
        }
2700

2701
        return $this;
296✔
2702
    }
2703

2704
    ///////////////////////////////////////
2705
    // Methods for defining relationships
2706
    ///////////////////////////////////////
2707
    
2708
    /**
2709
     * @psalm-suppress PossiblyUnusedMethod
2710
     */
2711
    public function hasOne(
2712
        string $relation_name,  // name of the relation, via which the related data
2713
                                // will be accessed as a property with the same name 
2714
                                // on record objects for this model class or array key 
2715
                                // for the related data when data is fetched into arrays 
2716
                                // via this model
2717
        
2718
        string $relationship_col_in_my_table,
2719
        
2720
        string $relationship_col_in_foreign_table,
2721
        
2722
        string $foreign_table_name='',   // If empty, the value set in the $table_name property
2723
                                         // of the model class specified in $foreign_models_class_name
2724
                                         // will be used if $foreign_models_class_name !== '' 
2725
                                         // and the value of the $table_name property is not ''
2726
        
2727
        string $primary_key_col_in_foreign_table='',   // If empty, the value set in the $primary_col property
2728
                                                       // of the model class specified in $foreign_models_class_name
2729
                                                       // will be used if $foreign_models_class_name !== '' 
2730
                                                       // and the value of the $primary_col property is not ''
2731
        
2732
        string $foreign_models_class_name = '', // If empty, defaults to \LeanOrm\Model::class.
2733
                                                // If empty, you must specify $foreign_table_name & $primary_key_col_in_foreign_table
2734
        
2735
        string $foreign_models_record_class_name = '', // If empty will default to \LeanOrm\Model\Record 
2736
                                                       // or the value of the $record_class_name property
2737
                                                       // in the class specfied in $foreign_models_class_name
2738
        
2739
        string $foreign_models_collection_class_name = '',  // If empty will default to \LeanOrm\Model\Collection
2740
                                                            // or the value of the $collection_class_name property
2741
                                                            // in the class specfied in $foreign_models_class_name
2742
        
2743
        ?callable $sql_query_modifier = null // optional callback to modify the query object used to fetch the related data
2744
    ): static {
2745
        $this->checkThatRelationNameIsNotAnActualColumnName($relation_name);
272✔
2746
        $this->setRelationshipDefinitionDefaultsIfNeeded (
264✔
2747
            $foreign_models_class_name,
264✔
2748
            $foreign_table_name,
264✔
2749
            $primary_key_col_in_foreign_table,
264✔
2750
            $foreign_models_record_class_name,
264✔
2751
            $foreign_models_collection_class_name
264✔
2752
        );
264✔
2753
        
2754
        if($foreign_models_collection_class_name !== '') {
224✔
2755
            
2756
            $this->validateRelatedCollectionClassName($foreign_models_collection_class_name);
224✔
2757
        }
2758
        
2759
        if($foreign_models_record_class_name !== '') {
216✔
2760
            
2761
            $this->validateRelatedRecordClassName($foreign_models_record_class_name);
216✔
2762
        }
2763
        
2764
        $this->validateTableName($foreign_table_name);
208✔
2765
        
2766
        $this->validateThatTableHasColumn($this->getTableName(), $relationship_col_in_my_table);
200✔
2767
        $this->validateThatTableHasColumn($foreign_table_name, $relationship_col_in_foreign_table);
192✔
2768
        $this->validateThatTableHasColumn($foreign_table_name, $primary_key_col_in_foreign_table);
184✔
2769
        
2770
        $this->relations[$relation_name] = [];
176✔
2771
        $this->relations[$relation_name]['relation_type'] = \GDAO\Model::RELATION_TYPE_HAS_ONE;
176✔
2772
        $this->relations[$relation_name]['foreign_key_col_in_my_table'] = $relationship_col_in_my_table;
176✔
2773
        $this->relations[$relation_name]['foreign_table'] = $foreign_table_name;
176✔
2774
        $this->relations[$relation_name]['foreign_key_col_in_foreign_table'] = $relationship_col_in_foreign_table;
176✔
2775
        $this->relations[$relation_name]['primary_key_col_in_foreign_table'] = $primary_key_col_in_foreign_table;
176✔
2776

2777
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
176✔
2778
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
176✔
2779
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
176✔
2780

2781
        $this->relations[$relation_name]['sql_query_modifier'] = $sql_query_modifier;
176✔
2782

2783
        return $this;
176✔
2784
    }
2785
    
2786
    /**
2787
     * @psalm-suppress PossiblyUnusedMethod
2788
     */
2789
    public function belongsTo(
2790
        string $relation_name,  // name of the relation, via which the related data
2791
                                // will be accessed as a property with the same name 
2792
                                // on record objects for this model class or array key 
2793
                                // for the related data when data is fetched into arrays 
2794
                                // via this model
2795
        
2796
        string $relationship_col_in_my_table,
2797
            
2798
        string $relationship_col_in_foreign_table,
2799
        
2800
        string $foreign_table_name = '', // If empty, the value set in the $table_name property
2801
                                         // of the model class specified in $foreign_models_class_name
2802
                                         // will be used if $foreign_models_class_name !== '' 
2803
                                         // and the value of the $table_name property is not ''
2804
        
2805
        string $primary_key_col_in_foreign_table = '', // If empty, the value set in the $primary_col property
2806
                                                       // of the model class specified in $foreign_models_class_name
2807
                                                       // will be used if $foreign_models_class_name !== '' 
2808
                                                       // and the value of the $primary_col property is not ''
2809
        
2810
        string $foreign_models_class_name = '', // If empty, defaults to \LeanOrm\Model::class.
2811
                                                // If empty, you must specify $foreign_table_name & $primary_key_col_in_foreign_table
2812
        
2813
        string $foreign_models_record_class_name = '', // If empty will default to \LeanOrm\Model\Record 
2814
                                                       // or the value of the $record_class_name property
2815
                                                       // in the class specfied in $foreign_models_class_name
2816
        
2817
        string $foreign_models_collection_class_name = '',  // If empty will default to \LeanOrm\Model\Collection
2818
                                                            // or the value of the $collection_class_name property
2819
                                                            // in the class specfied in $foreign_models_class_name
2820
        
2821
        ?callable $sql_query_modifier = null // optional callback to modify the query object used to fetch the related data
2822
    ): static {
2823
        $this->checkThatRelationNameIsNotAnActualColumnName($relation_name);
328✔
2824
        $this->setRelationshipDefinitionDefaultsIfNeeded (
320✔
2825
            $foreign_models_class_name,
320✔
2826
            $foreign_table_name,
320✔
2827
            $primary_key_col_in_foreign_table,
320✔
2828
            $foreign_models_record_class_name,
320✔
2829
            $foreign_models_collection_class_name
320✔
2830
        );
320✔
2831
        
2832
        if($foreign_models_collection_class_name !== '') {
280✔
2833
        
2834
            $this->validateRelatedCollectionClassName($foreign_models_collection_class_name);
280✔
2835
        }
2836
        
2837
        if($foreign_models_record_class_name !== '') {
272✔
2838
            
2839
            $this->validateRelatedRecordClassName($foreign_models_record_class_name);
272✔
2840
        }
2841
        
2842
        $this->validateTableName($foreign_table_name);
264✔
2843
        
2844
        $this->validateThatTableHasColumn($this->getTableName(), $relationship_col_in_my_table);
256✔
2845
        $this->validateThatTableHasColumn($foreign_table_name, $relationship_col_in_foreign_table);
248✔
2846
        $this->validateThatTableHasColumn($foreign_table_name, $primary_key_col_in_foreign_table);
240✔
2847
        
2848
        $this->relations[$relation_name] = [];
232✔
2849
        $this->relations[$relation_name]['relation_type'] = \GDAO\Model::RELATION_TYPE_BELONGS_TO;
232✔
2850
        $this->relations[$relation_name]['foreign_key_col_in_my_table'] = $relationship_col_in_my_table;
232✔
2851
        $this->relations[$relation_name]['foreign_table'] = $foreign_table_name;
232✔
2852
        $this->relations[$relation_name]['foreign_key_col_in_foreign_table'] = $relationship_col_in_foreign_table;
232✔
2853
        $this->relations[$relation_name]['primary_key_col_in_foreign_table'] = $primary_key_col_in_foreign_table;
232✔
2854

2855
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
232✔
2856
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
232✔
2857
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
232✔
2858

2859
        $this->relations[$relation_name]['sql_query_modifier'] = $sql_query_modifier;
232✔
2860

2861
        return $this;
232✔
2862
    }
2863
    
2864
    /**
2865
     * @psalm-suppress PossiblyUnusedMethod
2866
     * 
2867
     */
2868
    public function hasMany(
2869
        string $relation_name,  // name of the relation, via which the related data
2870
                                // will be accessed as a property with the same name 
2871
                                // on record objects for this model class or array key 
2872
                                // for the related data when data is fetched into arrays 
2873
                                // via this model
2874
        
2875
        string $relationship_col_in_my_table,
2876
        
2877
        string $relationship_col_in_foreign_table,
2878
        
2879
        string $foreign_table_name='',   // If empty, the value set in the $table_name property
2880
                                         // of the model class specified in $foreign_models_class_name
2881
                                         // will be used if $foreign_models_class_name !== '' 
2882
                                         // and the value of the $table_name property is not ''
2883
        
2884
        string $primary_key_col_in_foreign_table='',   // If empty, the value set in the $primary_col property
2885
                                                       // of the model class specified in $foreign_models_class_name
2886
                                                       // will be used if $foreign_models_class_name !== '' 
2887
                                                       // and the value of the $primary_col property is not ''
2888
        
2889
        string $foreign_models_class_name = '', // If empty, defaults to \LeanOrm\Model::class.
2890
                                                // If empty, you must specify $foreign_table_name & $primary_key_col_in_foreign_table
2891
        
2892
        string $foreign_models_record_class_name = '', // If empty will default to \LeanOrm\Model\Record 
2893
                                                       // or the value of the $record_class_name property
2894
                                                       // in the class specfied in $foreign_models_class_name
2895
        
2896
        string $foreign_models_collection_class_name = '',  // If empty will default to \LeanOrm\Model\Collection
2897
                                                            // or the value of the $collection_class_name property
2898
                                                            // in the class specfied in $foreign_models_class_name
2899
        
2900
        ?callable $sql_query_modifier = null // optional callback to modify the query object used to fetch the related data
2901
    ): static {
2902
        $this->checkThatRelationNameIsNotAnActualColumnName($relation_name);
1,308✔
2903
        $this->setRelationshipDefinitionDefaultsIfNeeded (
1,308✔
2904
            $foreign_models_class_name,
1,308✔
2905
            $foreign_table_name,
1,308✔
2906
            $primary_key_col_in_foreign_table,
1,308✔
2907
            $foreign_models_record_class_name,
1,308✔
2908
            $foreign_models_collection_class_name
1,308✔
2909
        );
1,308✔
2910
        
2911
        if($foreign_models_collection_class_name !== '') {
1,308✔
2912
            
2913
            $this->validateRelatedCollectionClassName($foreign_models_collection_class_name);
1,308✔
2914
        }
2915
            
2916
        if($foreign_models_record_class_name !== '') {
1,308✔
2917
            
2918
            $this->validateRelatedRecordClassName($foreign_models_record_class_name);
1,308✔
2919
        }
2920
            
2921
        
2922
        $this->validateTableName($foreign_table_name);
1,308✔
2923
        
2924
        $this->validateThatTableHasColumn($this->getTableName(), $relationship_col_in_my_table);
1,308✔
2925
        $this->validateThatTableHasColumn($foreign_table_name, $relationship_col_in_foreign_table);
1,308✔
2926
        $this->validateThatTableHasColumn($foreign_table_name, $primary_key_col_in_foreign_table);
1,308✔
2927
        
2928
        $this->relations[$relation_name] = [];
1,308✔
2929
        $this->relations[$relation_name]['relation_type'] = \GDAO\Model::RELATION_TYPE_HAS_MANY;
1,308✔
2930
        $this->relations[$relation_name]['foreign_key_col_in_my_table'] = $relationship_col_in_my_table;
1,308✔
2931
        $this->relations[$relation_name]['foreign_table'] = $foreign_table_name;
1,308✔
2932
        $this->relations[$relation_name]['foreign_key_col_in_foreign_table'] = $relationship_col_in_foreign_table;
1,308✔
2933
        $this->relations[$relation_name]['primary_key_col_in_foreign_table'] = $primary_key_col_in_foreign_table;
1,308✔
2934

2935
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
1,308✔
2936
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
1,308✔
2937
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
1,308✔
2938

2939
        $this->relations[$relation_name]['sql_query_modifier'] = $sql_query_modifier;
1,308✔
2940

2941
        return $this;
1,308✔
2942
    }
2943

2944
    /**
2945
     * @psalm-suppress PossiblyUnusedMethod
2946
     */
2947
    public function hasManyThrough(
2948
        string $relation_name,  // name of the relation, via which the related data
2949
                                // will be accessed as a property with the same name 
2950
                                // on record objects for this model class or array key 
2951
                                // for the related data when data is fetched into arrays 
2952
                                // via this model
2953
        
2954
        string $col_in_my_table_linked_to_join_table,
2955
        string $join_table,
2956
        string $col_in_join_table_linked_to_my_table,
2957
        string $col_in_join_table_linked_to_foreign_table,
2958
        string $col_in_foreign_table_linked_to_join_table,
2959
        
2960
        string $foreign_table_name = '', // If empty, the value set in the $table_name property
2961
                                         // of the model class specified in $foreign_models_class_name
2962
                                         // will be used if $foreign_models_class_name !== '' 
2963
                                         // and the value of the $table_name property is not ''
2964
            
2965
        string $primary_key_col_in_foreign_table = '', // If empty, the value set in the $primary_col property
2966
                                                       // of the model class specified in $foreign_models_class_name
2967
                                                       // will be used if $foreign_models_class_name !== '' 
2968
                                                       // and the value of the $primary_col property is not ''
2969
            
2970
        string $foreign_models_class_name = '', // If empty, defaults to \LeanOrm\Model::class.
2971
                                                // If empty, you must specify $foreign_table_name & $primary_key_col_in_foreign_table
2972
        
2973
        string $foreign_models_record_class_name = '', // If empty will default to \LeanOrm\Model\Record 
2974
                                                       // or the value of the $record_class_name property
2975
                                                       // in the class specfied in $foreign_models_class_name
2976
        
2977
        string $foreign_models_collection_class_name = '',  // If empty will default to \LeanOrm\Model\Collection
2978
                                                            // or the value of the $collection_class_name property
2979
                                                            // in the class specfied in $foreign_models_class_name
2980
        
2981
        ?callable $sql_query_modifier = null // optional callback to modify the query object used to fetch the related data
2982
    ): static {
2983
        $this->checkThatRelationNameIsNotAnActualColumnName($relation_name);
312✔
2984
        $this->setRelationshipDefinitionDefaultsIfNeeded (
304✔
2985
            $foreign_models_class_name,
304✔
2986
            $foreign_table_name,
304✔
2987
            $primary_key_col_in_foreign_table,
304✔
2988
            $foreign_models_record_class_name,
304✔
2989
            $foreign_models_collection_class_name
304✔
2990
        );
304✔
2991
        
2992
        if ($foreign_models_collection_class_name !== '') {
264✔
2993
            
2994
            $this->validateRelatedCollectionClassName($foreign_models_collection_class_name);
264✔
2995
        }
2996
        
2997
        if ($foreign_models_record_class_name !== '') {
256✔
2998
            
2999
            $this->validateRelatedRecordClassName($foreign_models_record_class_name);
256✔
3000
        }
3001
        
3002
        $this->validateTableName($foreign_table_name);
248✔
3003
        $this->validateTableName($join_table);
240✔
3004
        
3005
        $this->validateThatTableHasColumn($this->getTableName(), $col_in_my_table_linked_to_join_table);
232✔
3006
        $this->validateThatTableHasColumn($join_table, $col_in_join_table_linked_to_my_table);
224✔
3007
        $this->validateThatTableHasColumn($join_table, $col_in_join_table_linked_to_foreign_table);
216✔
3008
        $this->validateThatTableHasColumn($foreign_table_name, $col_in_foreign_table_linked_to_join_table);
208✔
3009
        $this->validateThatTableHasColumn($foreign_table_name, $primary_key_col_in_foreign_table);
200✔
3010
        
3011
        $this->relations[$relation_name] = [];
192✔
3012
        $this->relations[$relation_name]['relation_type'] = \GDAO\Model::RELATION_TYPE_HAS_MANY_THROUGH;
192✔
3013
        $this->relations[$relation_name]['col_in_my_table_linked_to_join_table'] = $col_in_my_table_linked_to_join_table;
192✔
3014
        $this->relations[$relation_name]['join_table'] = $join_table;
192✔
3015
        $this->relations[$relation_name]['col_in_join_table_linked_to_my_table'] = $col_in_join_table_linked_to_my_table;
192✔
3016
        $this->relations[$relation_name]['col_in_join_table_linked_to_foreign_table'] = $col_in_join_table_linked_to_foreign_table;
192✔
3017
        $this->relations[$relation_name]['foreign_table'] = $foreign_table_name;
192✔
3018
        $this->relations[$relation_name]['col_in_foreign_table_linked_to_join_table'] = $col_in_foreign_table_linked_to_join_table;
192✔
3019
        $this->relations[$relation_name]['primary_key_col_in_foreign_table'] = $primary_key_col_in_foreign_table;
192✔
3020

3021
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
192✔
3022
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
192✔
3023
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
192✔
3024

3025
        $this->relations[$relation_name]['sql_query_modifier'] = $sql_query_modifier;
192✔
3026

3027
        return $this;
192✔
3028
    }
3029
    
3030
    /**
3031
     * @psalm-suppress MixedAssignment
3032
     */
3033
    protected function setRelationshipDefinitionDefaultsIfNeeded (
3034
        string &$foreign_models_class_name,
3035
        string &$foreign_table_name,
3036
        string &$primary_key_col_in_foreign_table,
3037
        string &$foreign_models_record_class_name,
3038
        string &$foreign_models_collection_class_name,
3039
    ): void {
3040
        
3041
        if($foreign_models_class_name !== '' && $foreign_models_class_name !== \LeanOrm\Model::class) {
1,308✔
3042
           
3043
            $this->validateRelatedModelClassName($foreign_models_class_name);
1,308✔
3044
            
3045
            /**
3046
             * @psalm-suppress ArgumentTypeCoercion
3047
             */
3048
            $ref_class = new \ReflectionClass($foreign_models_class_name);
1,308✔
3049
            
3050
            if($foreign_table_name === '') {
1,308✔
3051
                
3052
                // Try to set it using the default value of the table_name property 
3053
                // in the specified foreign model class $foreign_models_class_name
3054
                $reflected_foreign_table_name = 
64✔
3055
                        $ref_class->getProperty('table_name')->getDefaultValue();
64✔
3056

3057
                if($reflected_foreign_table_name === '' || $reflected_foreign_table_name === null) {
64✔
3058
                    
3059
                    $msg = "ERROR: '\$foreign_table_name' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3060
                         . $foreign_models_class_name . "'"
32✔
3061
                         . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3062

3063
                    // we can't use Reflection to figure out this table name
3064
                    throw new \LeanOrm\Exceptions\MissingRelationParamException($msg);
32✔
3065
                }
3066
                
3067
                $foreign_table_name = $reflected_foreign_table_name;
32✔
3068
            }
3069
            
3070
            if($primary_key_col_in_foreign_table === '') {
1,308✔
3071

3072
                // Try to set it using the default value of the primary_col property 
3073
                // in the specified foreign model class $foreign_models_class_name
3074
                $reflected_foreign_primary_key_col = 
64✔
3075
                        $ref_class->getProperty('primary_col')->getDefaultValue();
64✔
3076

3077
                if($reflected_foreign_primary_key_col === '' || $reflected_foreign_primary_key_col === null) {
64✔
3078

3079
                    $msg = "ERROR: '\$primary_key_col_in_foreign_table' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3080
                         . $foreign_models_class_name . "'"
32✔
3081
                         . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3082

3083
                    // we can't use Reflection to figure out this primary key column name
3084
                    throw new \LeanOrm\Exceptions\MissingRelationParamException($msg);
32✔
3085
                }
3086

3087
                // set it to the reflected value
3088
                $primary_key_col_in_foreign_table = $reflected_foreign_primary_key_col;
32✔
3089
            }
3090
            
3091
            $reflected_record_class_name = $ref_class->getProperty('record_class_name')->getDefaultValue();
1,308✔
3092
            
3093
            if(
3094
                $foreign_models_record_class_name === ''
1,308✔
3095
                && $reflected_record_class_name !== ''
1,308✔
3096
                && $reflected_record_class_name !== null
1,308✔
3097
            ) {
3098
                $foreign_models_record_class_name = $reflected_record_class_name;
32✔
3099
            }
3100
            
3101
            $reflected_collection_class_name = $ref_class->getProperty('collection_class_name')->getDefaultValue();
1,308✔
3102
            
3103
            if(
3104
                $foreign_models_collection_class_name === ''
1,308✔
3105
                && $reflected_collection_class_name !== ''
1,308✔
3106
                && $reflected_collection_class_name !== null
1,308✔
3107
            ) {
3108
                $foreign_models_collection_class_name = $reflected_collection_class_name;
32✔
3109
            }
3110
            
3111
        } else {
3112
            
3113
            $foreign_models_class_name = \LeanOrm\Model::class;
248✔
3114
            
3115
            if($foreign_table_name === '') {
248✔
3116
                
3117
                $msg = "ERROR: '\$foreign_table_name' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3118
                     . \LeanOrm\Model::class . "'"
32✔
3119
                     . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3120
                
3121
                // we can't use Reflection to figure out this table name
3122
                // because \LeanOrm\Model->table_name has a default value of ''
3123
                throw new \LeanOrm\Exceptions\MissingRelationParamException($msg);
32✔
3124
            }
3125
            
3126
            // $foreign_table_name !== '' if we got this far
3127
            if($primary_key_col_in_foreign_table === '') {
216✔
3128

3129
                $msg = "ERROR: '\$primary_key_col_in_foreign_table' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3130
                     . \LeanOrm\Model::class . "'"
32✔
3131
                     . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3132

3133
                // We can't use Reflection to figure out this primary key col
3134
                // because \LeanOrm\Model->primary_col has a default value of ''
3135
                throw new \LeanOrm\Exceptions\MissingRelationParamException($msg);
32✔
3136

3137
            }
3138
        } // if($foreign_models_class_name !== '' && $foreign_models_class_name !== \LeanOrm\Model::class)
3139
        
3140
        if($foreign_models_record_class_name === '') {
1,308✔
3141
            
3142
            $foreign_models_record_class_name = \LeanOrm\Model\Record::class;
184✔
3143
        }
3144
        
3145
        if($foreign_models_collection_class_name === '') {
1,308✔
3146
            
3147
            $foreign_models_collection_class_name = \LeanOrm\Model\Collection::class;
184✔
3148
        }
3149
    }
3150
    
3151
    protected function checkThatRelationNameIsNotAnActualColumnName(string $relationName): void {
3152

3153
        $tableCols = $this->getTableColNames();
1,308✔
3154

3155

3156
        $tableColsLowerCase = array_map(strtolower(...), $tableCols);
1,308✔
3157

3158
        if( in_array(strtolower($relationName), $tableColsLowerCase) ) {
1,308✔
3159

3160
            //Error trying to add a relation whose name collides with an actual
3161
            //name of a column in the db table associated with this model.
3162
            $msg = sprintf("ERROR: You cannont add a relationship with the name '%s' ", $relationName)
32✔
3163
                 . " to the Model (".static::class."). The database table "
32✔
3164
                 . sprintf(" '%s' associated with the ", $this->getTableName())
32✔
3165
                 . " model (".static::class.") already contains"
32✔
3166
                 . " a column with the same name."
32✔
3167
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
32✔
3168
                 . PHP_EOL;
32✔
3169

3170
            throw new \GDAO\Model\RecordRelationWithSameNameAsAnExistingDBTableColumnNameException($msg);
32✔
3171
        } // if( in_array(strtolower($relationName), $tableColsLowerCase) ) 
3172
    }
3173
    
3174
    /**
3175
     * @psalm-suppress PossiblyUnusedReturnValue
3176
     */
3177
    protected function validateTableName(string $table_name): bool {
3178
        
3179
        if(!$this->tableExistsInDB($table_name)) {
1,308✔
3180
            
3181
            //throw exception
3182
            $msg = "ERROR: The specified table `{$table_name}` does not exist in the DB."
40✔
3183
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
40✔
3184
                 . PHP_EOL;
40✔
3185
            throw new \LeanOrm\Exceptions\BadModelTableNameException($msg);
40✔
3186
        } // if(!$this->tableExistsInDB($table_name))
3187
        
3188
        return true;
1,308✔
3189
    }
3190
    
3191
    /**
3192
     * @psalm-suppress PossiblyUnusedReturnValue
3193
     */
3194
    protected function validateThatTableHasColumn(string $table_name, string $column_name): bool {
3195
        
3196
        if(!$this->columnExistsInDbTable($table_name, $column_name)) {
1,308✔
3197

3198
            //throw exception
3199
            $msg = "ERROR: The specified table `{$table_name}` in the DB"
112✔
3200
                 . " does not contain the specified column `{$column_name}`."
112✔
3201
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
112✔
3202
                 . PHP_EOL;
112✔
3203
            throw new \LeanOrm\Exceptions\BadModelColumnNameException($msg);
112✔
3204
        } // if(!$this->columnExistsInDbTable($table_name, $column_name))
3205
        
3206
        return true;
1,308✔
3207
    }
3208
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc