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

rotexsoft / leanorm / 17182199377

26 Jun 2025 05:35AM UTC coverage: 95.974%. Remained the same
17182199377

push

github

rotimi
Tweaked github action to stop using older unsupported ubuntu versions & to also test against PHP 8.4

1478 of 1540 relevant lines covered (95.97%)

172.73 hits per line

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

94.45
/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
        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
     * @psalm-suppress MoreSpecificReturnType
357
     */
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
     * @psalm-suppress MoreSpecificReturnType
370
     */
371
    public function createNewRecord(array $col_names_and_values = []): \GDAO\Model\RecordInterface {
372

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

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

392
        if( $table_name === '' ) {
296✔
393

394
            $table_name = $this->getTableName();
296✔
395
        }
396

397
        if($initiallyNull || !$select_obj->hasCols()) {
296✔
398

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

405
        return $select_obj;
296✔
406
    }
407

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

414
        $default_colvals = [];
8✔
415

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

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

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

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

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

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

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

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

493
        $parent_collection_class_name = \GDAO\Model\CollectionInterface::class;
1,308✔
494

495
        if( !is_subclass_of($collection_class_name, $parent_collection_class_name, true) ) {
1,308✔
496

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

505
            throw new \LeanOrm\Exceptions\BadCollectionClassNameForFetchingRelatedDataException($msg);
32✔
506
        }
507

508
        return true;
1,308✔
509
    }
510

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

518
        if( !is_subclass_of($record_class_name, $parent_record_class_name, true)  ) {
1,308✔
519

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

528
            throw new \LeanOrm\Exceptions\BadRecordClassNameForFetchingRelatedDataException($msg);
32✔
529
        }
530

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

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

553
                -- OR
554

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

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

573
                $fkey_val_to_related_data_keys = [];
64✔
574

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

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

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

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

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

595
                } // foreach ($related_data as $curr_key => $related_datum)
596

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

602
                    $matching_related_rows = [];
64✔
603

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

610
                        foreach ($fkey_val_to_related_data_keys[$parent_row[$fkey_col_in_my_table]] as $related_data_key) {
64✔
611

612
                            $matching_related_rows[] = $related_data[$related_data_key];
64✔
613
                        }
614
                    }
615

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

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

631
                    } else {
632

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

643
                ////////////////////////////////////////////////////////////////
644
                // End: Stitch the related data to the approriate parent records
645
                ////////////////////////////////////////////////////////////////
646

647
            } else if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
56✔
648

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

732
            if($foreign_models_collection_class_name !== '') {
88✔
733

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

738
            if($foreign_models_record_class_name !== '') {
88✔
739

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

744
            $query_obj = $foreign_model_obj->getSelect();
88✔
745

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

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

754
            if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
88✔
755

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

761
            } else {
762

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

769
                if( $col_vals !== [] ) {
56✔
770

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

779
            if(\is_callable($sql_query_modifier)) {
88✔
780

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

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

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

793
/*
794
-- SQL For Fetching the Related Data
795

796
-- $parent_data is a collection or array of records    
797
SELECT {$foreign_table_name}.*,
798
       {$join_table_name}.{$col_in_join_table_linked_to_my_models_table}
799
  FROM {$foreign_table_name}
800
  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}
801
 WHERE {$join_table_name}.{$col_in_join_table_linked_to_my_models_table} IN ( $fkey_col_in_my_table column values in $parent_data )
802

803
OR
804

805
-- $parent_data is a single record
806
SELECT {$foreign_table_name}.*,
807
       {$join_table_name}.{$col_in_join_table_linked_to_my_models_table}
808
  FROM {$foreign_table_name}
809
  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}
810
 WHERE {$join_table_name}.{$col_in_join_table_linked_to_my_models_table} = {$parent_data->$fkey_col_in_my_table}
811
*/
812
            /** @psalm-suppress MixedArgument */
813
            $this->logQuery($sql_2_get_related_data, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
88✔
814

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

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

828
                $fkey_val_to_related_data_keys = [];
56✔
829

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

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

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

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

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

850
                } // foreach ($related_data as $curr_key => $related_datum)
851

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

857
                    $matching_related_rows = [];
56✔
858

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

865
                        foreach ($fkey_val_to_related_data_keys[$parent_row[$fkey_col_in_my_table]] as $related_data_key) {
56✔
866

867
                            $matching_related_rows[] = $related_data[$related_data_key];
56✔
868
                        }
869
                    }
870

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

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

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

885
                    } else {
886

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

896
                } // foreach( $parent_data as $p_rec_key => $parent_record )
897

898
                ////////////////////////////////////////////////////////////////
899
                // End: Stitch the related data to the approriate parent records
900
                ////////////////////////////////////////////////////////////////
901

902
            } else if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
32✔
903

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

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

932
/*
933
-- SQL For Fetching the Related Data
934

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

940
OR
941

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

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

956
                $fkey_val_to_related_data_keys = [];
56✔
957

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

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

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

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

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

978
                } // foreach ($related_data as $curr_key => $related_datum)
979

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

985
                    $matching_related_rows = [];
56✔
986

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

993
                        foreach ($fkey_val_to_related_data_keys[$parent_row[$fkey_col_in_my_table]] as $related_data_key) {
56✔
994

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

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

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

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

1026
                    } else {
1027

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

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

1046
                ////////////////////////////////////////////////////////////////
1047
                // End: Stitch the related data to the approriate parent records
1048
                ////////////////////////////////////////////////////////////////
1049

1050
            } else if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
32✔
1051

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1159
        $query_obj = $foreign_model_obj->getSelect();
120✔
1160

1161
        if ( $parent_data instanceof \GDAO\Model\RecordInterface ) {
120✔
1162

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

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

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

1185
        if(\is_callable($sql_query_modifier)) {
120✔
1186

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

1194
        if($query_obj->hasCols() === false){
120✔
1195

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

1225
            $f_models_class_name = \LeanOrm\Model::class;
×
1226
        }
1227

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

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

1306
        if ( is_array($parent_data) ) {
64✔
1307

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

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

1318
        } elseif($parent_data instanceof \GDAO\Model\CollectionInterface) {
32✔
1319

1320
            $col_vals = $parent_data->getColVals($fkey_col_in_my_table);
32✔
1321
        }
1322

1323
        return $col_vals;
64✔
1324
    }
1325

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

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

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

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

1407
            if( $use_collections ) {
8✔
1408

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

1413
            } else {
1414

1415
                if( $use_records ) {
8✔
1416

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

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

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

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

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

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

1456
        return $this->doFetchRecordsIntoCollection($select_obj, $relations_to_include, true);
32✔
1457
    }
1458

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

1470
        if($data !== [] ) {
96✔
1471

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

1487
                /** @psalm-suppress MixedArgument */
1488
                $this->loadRelationshipData($rel_name, $results, true, true);
32✔
1489
            }
1490
        }
1491

1492
        /** @psalm-suppress InvalidReturnStatement */
1493
        return $results;
96✔
1494
    }
1495

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

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

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

1526
        if( $results !== [] ) {
40✔
1527

1528
            /** @psalm-suppress MixedAssignment */
1529
            foreach( $relations_to_include as $rel_name ) {
40✔
1530

1531
                /** @psalm-suppress MixedArgument */
1532
                $this->loadRelationshipData($rel_name, $results, true);
16✔
1533
            }
1534
        }
1535

1536
        return $results;
40✔
1537
    }
1538

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

1545
        $results = $this->getArrayOfDbRows($select_obj, $use_p_k_val_as_key);
128✔
1546

1547
        /** @psalm-suppress MixedAssignment */
1548
        foreach ($results as $key=>$value) {
128✔
1549

1550
            /** @psalm-suppress MixedArgument */
1551
            $results[$key] = $this->createNewRecord($value)->markAsNotNew();
128✔
1552
        }
1553
        
1554
        return $results;
128✔
1555
    }
1556

1557
    /**
1558
     * @return mixed[]
1559
     */
1560
    protected function getArrayOfDbRows(?\Aura\SqlQuery\Common\Select $select_obj=null, bool $use_p_k_val_as_key=false): array {
1561

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

1571
            $results_keyed_by_pk = [];
56✔
1572

1573
            /** @psalm-suppress MixedAssignment */
1574
            foreach( $results as $result ) {
56✔
1575

1576
                /** @psalm-suppress MixedArgument */
1577
                if( !array_key_exists($this->getPrimaryCol(), $result) ) {
56✔
1578

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

1585
                    throw new \LeanOrm\Exceptions\KeyingFetchResultsByPrimaryKeyFailedException($msg);
×
1586
                }
1587

1588
                // key on primary key value
1589
                /** @psalm-suppress MixedArrayOffset */
1590
                $results_keyed_by_pk[$result[$this->getPrimaryCol()]] = $result;
56✔
1591
            }
1592

1593
            $results = $results_keyed_by_pk;
56✔
1594
        }
1595

1596
        return $results;
168✔
1597
    }
1598

1599
    /**
1600
     * {@inheritDoc}
1601
     */
1602
    public function fetchRowsIntoArray(?object $query=null, array $relations_to_include=[]): array {
1603

1604
        return $this->doFetchRowsIntoArray(
44✔
1605
                    ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null, 
44✔
1606
                    $relations_to_include
44✔
1607
                );
44✔
1608
    }
1609

1610
    /**
1611
     * @return array[]
1612
     */
1613
    public function fetchRowsIntoArrayKeyedOnPkVal(?\Aura\SqlQuery\Common\Select $select_obj=null, array $relations_to_include=[]): array {
1614

1615
        return $this->doFetchRowsIntoArray($select_obj, $relations_to_include, true);
16✔
1616
    }
1617

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

1634
                /** @psalm-suppress MixedArgument */
1635
                $this->loadRelationshipData($rel_name, $results);
24✔
1636
            }
1637
        }
1638

1639
        return $results;
52✔
1640
    }
1641

1642
    public function getPDO(): \PDO {
1643

1644
        //return pdo object associated with the current dsn
1645
        return DBConnector::getDb($this->dsn); 
1,308✔
1646
    }
1647

1648
    /**
1649
     * {@inheritDoc}
1650
     */
1651
    public function deleteMatchingDbTableRows(array $cols_n_vals): int {
1652

1653
        $result = 0;
48✔
1654

1655
        if ( $cols_n_vals !== [] ) {
48✔
1656

1657
            //delete statement
1658
            $del_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newDelete();
48✔
1659
            $sel_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newSelect();
48✔
1660
            $del_qry_obj->from($this->getTableName());
48✔
1661
            $sel_qry_obj->from($this->getTableName());
48✔
1662
            $sel_qry_obj->cols([' count(*) ']);
48✔
1663
            $table_cols = $this->getTableColNames();
48✔
1664

1665
            /** @psalm-suppress MixedAssignment */
1666
            foreach ($cols_n_vals as $colname => $colval) {
48✔
1667

1668
                if(!in_array($colname, $table_cols)) {
48✔
1669

1670
                    // specified column is not a valid db table col, remove it
1671
                    unset($cols_n_vals[$colname]);
8✔
1672
                    continue;
8✔
1673
                }
1674

1675
                if (is_array($colval)) {
48✔
1676

1677
                    /** @psalm-suppress MixedAssignment */
1678
                    foreach($colval as $key=>$val) {
24✔
1679

1680
                        if(!$this->isAcceptableDeleteQueryValue($val)) {
24✔
1681

1682
                            $this->throwExceptionForInvalidDeleteQueryArg($val, $cols_n_vals);
8✔
1683
                        }
1684

1685
                        /** @psalm-suppress MixedAssignment */
1686
                        $colval[$key] = $this->stringifyIfStringable($val);
24✔
1687
                    }
1688

1689
                    $this->addWhereInAndOrIsNullToQuery(''.$colname, $colval, $del_qry_obj);
16✔
1690
                    $this->addWhereInAndOrIsNullToQuery(''.$colname, $colval, $sel_qry_obj);
16✔
1691

1692
                } else {
1693

1694
                    if(!$this->isAcceptableDeleteQueryValue($colval)) {
40✔
1695

1696
                        $this->throwExceptionForInvalidDeleteQueryArg($colval, $cols_n_vals);
8✔
1697
                    }
1698

1699
                    $del_qry_obj->where(" {$colname} = :{$colname} ", [$colname => $this->stringifyIfStringable($colval)]);
40✔
1700
                    $sel_qry_obj->where(" {$colname} = :{$colname} ", [$colname => $this->stringifyIfStringable($colval)]);
40✔
1701
                }
1702
            }
1703

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

1708
                $dlt_qry = $del_qry_obj->__toString();
32✔
1709
                $dlt_qry_params = $del_qry_obj->getBindValues();
32✔
1710
                $this->logQuery($dlt_qry, $dlt_qry_params, __METHOD__, '' . __LINE__);
32✔
1711

1712
                $matching_rows_before_delete = (int) $this->fetchValue($sel_qry_obj);
32✔
1713

1714
                $this->db_connector->executeQuery($dlt_qry, $dlt_qry_params, true);
32✔
1715

1716
                $matching_rows_after_delete = (int) $this->fetchValue($sel_qry_obj);
32✔
1717

1718
                //number of deleted rows
1719
                $result = $matching_rows_before_delete - $matching_rows_after_delete;
32✔
1720
            } // if($cols_n_vals !== []) 
1721
        } // if ( $cols_n_vals !== [] )
1722

1723
        return $result;
32✔
1724
    }
1725
    
1726
    protected function throwExceptionForInvalidDeleteQueryArg(mixed $val, array $cols_n_vals): never {
1727

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

1740
        throw new \LeanOrm\Exceptions\InvalidArgumentException($msg);
16✔
1741
    }
1742
    
1743
    /**
1744
     * {@inheritDoc}
1745
     */
1746
    public function deleteSpecifiedRecord(\GDAO\Model\RecordInterface $record): ?bool {
1747

1748
        $succesfully_deleted = null;
40✔
1749

1750
        if( $record instanceof \LeanOrm\Model\ReadOnlyRecord ) {
40✔
1751

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

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

1777
            /** @psalm-suppress MixedAssignment */
1778
            $pri_key_val = $record->getPrimaryVal();
16✔
1779
            $cols_n_vals = [$record->getPrimaryCol() => $pri_key_val];
16✔
1780

1781
            $succesfully_deleted = 
16✔
1782
                $this->deleteMatchingDbTableRows($cols_n_vals);
16✔
1783

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

1821
        return ( $succesfully_deleted >= 1 ) ? true : $succesfully_deleted;
16✔
1822
    }
1823

1824
    /**
1825
     * {@inheritDoc}
1826
     */
1827
    public function fetchCol(?object $query=null): array {
1828

1829
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
48✔
1830
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
48✔
1831
        );
48✔
1832
        $sql = $query_obj->__toString();
48✔
1833
        $params_2_bind_2_sql = $query_obj->getBindValues();
48✔
1834
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
48✔
1835

1836
        return $this->db_connector->dbFetchCol($sql, $params_2_bind_2_sql);
48✔
1837
    }
1838

1839
    /**
1840
     * {@inheritDoc}
1841
     */
1842
    public function fetchOneRecord(?object $query=null, array $relations_to_include=[]): ?\GDAO\Model\RecordInterface {
1843

1844
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
124✔
1845
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
124✔
1846
        );
124✔
1847
        $query_obj->limit(1);
124✔
1848

1849
        $sql = $query_obj->__toString();
124✔
1850
        $params_2_bind_2_sql = $query_obj->getBindValues();
124✔
1851
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
124✔
1852

1853
        /** @psalm-suppress MixedAssignment */
1854
        $result = $this->db_connector->dbFetchOne($sql, $params_2_bind_2_sql);
124✔
1855

1856
        if( $result !== false && is_array($result) && $result !== [] ) {
124✔
1857

1858
            $result = $this->createNewRecord($result)->markAsNotNew();
124✔
1859

1860
            foreach( $relations_to_include as $rel_name ) {
124✔
1861

1862
                $this->loadRelationshipData($rel_name, $result, true, true);
24✔
1863
            }
1864
        }
1865
        
1866
        if(!($result instanceof \GDAO\Model\RecordInterface)) {
124✔
1867
            
1868
            $result = null;
32✔
1869
        }
1870

1871
        return $result;
124✔
1872
    }
1873
    
1874
    /**
1875
     * Convenience method to fetch one record by the specified primary key value.
1876
     * @param string[] $relations_to_include names of relations to include
1877
     * @psalm-suppress PossiblyUnusedMethod
1878
     */
1879
    public function fetchOneByPkey(string|int $id, array $relations_to_include = []): ?\GDAO\Model\RecordInterface {
1880
        
1881
        $select = $this->getSelect();
8✔
1882
        $query_placeholder = "leanorm_{$this->getTableName()}_{$this->getPrimaryCol()}_val";
8✔
1883
        $select->where(
8✔
1884
            " {$this->getPrimaryCol()} = :{$query_placeholder} ", 
8✔
1885
            [ $query_placeholder => $id]
8✔
1886
        );
8✔
1887
        
1888
        return $this->fetchOneRecord($select, $relations_to_include);
8✔
1889
    }
1890

1891
    /**
1892
     * {@inheritDoc}
1893
     */
1894
    public function fetchPairs(?object $query=null): array {
1895

1896
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
8✔
1897
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
8✔
1898
        );
8✔
1899
        $sql = $query_obj->__toString();
8✔
1900
        $params_2_bind_2_sql = $query_obj->getBindValues();
8✔
1901
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
8✔
1902

1903
        return $this->db_connector->dbFetchPairs($sql, $params_2_bind_2_sql);
8✔
1904
    }
1905

1906
    /**
1907
     * {@inheritDoc}
1908
     */
1909
    public function fetchValue(?object $query=null): mixed {
1910

1911
        $query_obj = $this->createQueryObjectIfNullAndAddColsToQuery(
56✔
1912
            ($query instanceof \Aura\SqlQuery\Common\Select) ? $query : null
56✔
1913
        );
56✔
1914
        $query_obj->limit(1);
56✔
1915

1916
        $query_obj_4_num_matching_rows = clone $query_obj;
56✔
1917

1918
        $sql = $query_obj->__toString();
56✔
1919
        $params_2_bind_2_sql = $query_obj->getBindValues();
56✔
1920
        $this->logQuery($sql, $params_2_bind_2_sql, __METHOD__, '' . __LINE__);
56✔
1921

1922
        /** @psalm-suppress MixedAssignment */
1923
        $result = $this->db_connector->dbFetchValue($sql, $params_2_bind_2_sql);
56✔
1924

1925
        // need to issue a second query to get the number of matching rows
1926
        // clear the cols part of the query above while preserving all the
1927
        // other parts of the query
1928
        $query_obj_4_num_matching_rows->resetCols();
56✔
1929
        $query_obj_4_num_matching_rows->cols([' COUNT(*) AS num_rows']);
56✔
1930

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

1935
        /** @psalm-suppress MixedAssignment */
1936
        $num_matching_rows = $this->db_connector->dbFetchOne($sql, $params_2_bind_2_sql);
56✔
1937

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

1995
    protected function processRowOfDataToInsert(
1996
        array &$data, array &$table_cols, bool &$has_autoinc_pk_col=false
1997
    ): void {
1998

1999
        $this->addTimestampToData($data, $this->created_timestamp_column_name, $table_cols);
52✔
2000
        $this->addTimestampToData($data, $this->updated_timestamp_column_name, $table_cols);
52✔
2001

2002
        // remove non-existent table columns from the data and also
2003
        // converts object values for objects with __toString() to 
2004
        // their string value
2005
        /** @psalm-suppress MixedAssignment */
2006
        foreach ($data as $key => $val) {
52✔
2007

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

2011
            if ( !in_array($key, $table_cols) ) {
52✔
2012

2013
                unset($data[$key]);
24✔
2014
                // not in the table, so no need to check for autoinc
2015
                continue;
24✔
2016

2017
            } elseif( !$this->isAcceptableInsertValue($val) ) {
52✔
2018

2019
                $msg = "ERROR: the value "
16✔
2020
                     . PHP_EOL . var_export($val, true) . PHP_EOL
16✔
2021
                     . " you are trying to insert into `{$this->getTableName()}`."
16✔
2022
                     . "`{$key}` is not acceptable ('".  gettype($val) . "'"
16✔
2023
                     . " supplied). Boolean, NULL, numeric or string value expected."
16✔
2024
                     . PHP_EOL
16✔
2025
                     . "Data supplied to "
16✔
2026
                     . static::class . '::' . __FUNCTION__ . '(...).' 
16✔
2027
                     . " for insertion:"
16✔
2028
                     . PHP_EOL . var_export($data, true) . PHP_EOL
16✔
2029
                     . PHP_EOL;
16✔
2030

2031
                throw new \GDAO\ModelInvalidInsertValueSuppliedException($msg);
16✔
2032
            }
2033

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

2040
                unset($data[$key]);
×
2041

2042
            } // if ( $this->table_cols[$key]['autoinc'] && empty($val) )
2043
        } // foreach ($data as $key => $val)
2044

2045
        /** @psalm-suppress MixedAssignment */
2046
        foreach($this->table_cols as $col_name=>$col_info) {
36✔
2047

2048
            /** @psalm-suppress MixedArrayAccess */
2049
            if ( $col_info['autoinc'] === true && $col_info['primary'] === true ) {
36✔
2050

2051
                if(array_key_exists($col_name, $data)) {
×
2052

2053
                    //no need to add primary key value to the insert 
2054
                    //statement since the column is auto incrementing
2055
                    unset($data[$col_name]);
×
2056

2057
                } // if(array_key_exists($col_name, $data_2_insert))
2058

2059
                $has_autoinc_pk_col = true;
×
2060

2061
            } // if ( $col_info['autoinc'] === true && $col_info['primary'] === true )
2062
        } // foreach($this->table_cols as $col_name=>$col_info)
2063
    }
2064
    
2065
    protected function updateInsertDataArrayWithTheNewlyInsertedRecordFromDB(
2066
        array &$data_2_insert, array $table_cols
2067
    ): void {
2068
        
2069
        if(
2070
            array_key_exists($this->getPrimaryCol(), $data_2_insert)
20✔
2071
            && !empty($data_2_insert[$this->getPrimaryCol()])
20✔
2072
        ) {
2073
            $record = $this->fetchOneRecord(
8✔
2074
                        $this->getSelect()
8✔
2075
                             ->where(
8✔
2076
                                " {$this->getPrimaryCol()} = :{$this->getPrimaryCol()} ",
8✔
2077
                                [ $this->getPrimaryCol() => $data_2_insert[$this->getPrimaryCol()]]
8✔
2078
                             )
8✔
2079
                     );
8✔
2080
            $data_2_insert = ($record instanceof \GDAO\Model\RecordInterface) ? $record->getData() :  $data_2_insert;
8✔
2081
            
2082
        } else {
2083

2084
            // we don't have the primary key.
2085
            // Do a select using all the fields.
2086
            // If only one record is returned, we have found
2087
            // the record we just inserted, else we return $data_2_insert as is 
2088

2089
            $select = $this->getSelect();
20✔
2090

2091
            /** @psalm-suppress MixedAssignment */
2092
            foreach ($data_2_insert as $col => $val) {
20✔
2093

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

2097
                if(is_string($processed_val) || is_numeric($processed_val)) {
20✔
2098

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

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

2103
                    $select->where(" {$col} IS NULL ");
8✔
2104
                } // if(is_string($processed_val) || is_numeric($processed_val))
2105
            } // foreach ($data_2_insert as $col => $val)
2106

2107
            $matching_rows = $this->fetchRowsIntoArray($select);
20✔
2108

2109
            if(count($matching_rows) === 1) {
20✔
2110

2111
                /** @psalm-suppress MixedAssignment */
2112
                $data_2_insert = array_pop($matching_rows);
20✔
2113
            }
2114
        }
2115
    }
2116

2117
    /**
2118
     * {@inheritDoc}
2119
     */
2120
    public function insert(array $data_2_insert = []): bool|array {
2121
        
2122
        $result = false;
28✔
2123

2124
        if ( $data_2_insert !== [] ) {
28✔
2125

2126
            $table_cols = $this->getTableColNames();
28✔
2127
            $has_autoinc_pkey_col=false;
28✔
2128

2129
            $this->processRowOfDataToInsert(
28✔
2130
                $data_2_insert, $table_cols, $has_autoinc_pkey_col
28✔
2131
            );
28✔
2132

2133
            // Do we still have anything left to save after removing items
2134
            // in the array that do not map to actual db table columns
2135
            /**
2136
             * @psalm-suppress RedundantCondition
2137
             * @psalm-suppress TypeDoesNotContainType
2138
             */
2139
            if( (is_countable($data_2_insert) ? count($data_2_insert) : 0) > 0 ) {
20✔
2140

2141
                //Insert statement
2142
                $insrt_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newInsert();
20✔
2143
                $insrt_qry_obj->into($this->getTableName())->cols($data_2_insert);
20✔
2144

2145
                $insrt_qry_sql = $insrt_qry_obj->__toString();
20✔
2146
                $insrt_qry_params = $insrt_qry_obj->getBindValues();
20✔
2147
                $this->logQuery($insrt_qry_sql, $insrt_qry_params, __METHOD__, '' . __LINE__);
20✔
2148

2149
                if( $this->db_connector->executeQuery($insrt_qry_sql, $insrt_qry_params) ) {
20✔
2150

2151
                    // insert was successful, we are now going to try to 
2152
                    // fetch the inserted record from the db to get and 
2153
                    // return the db representation of the data
2154
                    if($has_autoinc_pkey_col) {
20✔
2155

2156
                        /** @psalm-suppress MixedAssignment */
2157
                        $last_insert_sequence_name = 
×
2158
                            $insrt_qry_obj->getLastInsertIdName($this->getPrimaryCol());
×
2159

2160
                        $pk_val_4_new_record = 
×
2161
                            $this->getPDO()->lastInsertId(is_string($last_insert_sequence_name) ? $last_insert_sequence_name : null);
×
2162

2163
                        // Add retrieved primary key value 
2164
                        // or null (if primary key value is empty) 
2165
                        // to the data to be returned.
2166
                        $data_2_insert[$this->primary_col] = 
×
2167
                            empty($pk_val_4_new_record) ? null : $pk_val_4_new_record;
×
2168

2169
                        $this->updateInsertDataArrayWithTheNewlyInsertedRecordFromDB(
×
2170
                            $data_2_insert, $table_cols
×
2171
                        );
×
2172

2173
                    } else {
2174

2175
                        $this->updateInsertDataArrayWithTheNewlyInsertedRecordFromDB(
20✔
2176
                            $data_2_insert, $table_cols
20✔
2177
                        );
20✔
2178

2179
                    } // if($has_autoinc_pkey_col)
2180

2181
                    //insert was successful
2182
                    $result = $data_2_insert;
20✔
2183

2184
                } // if( $this->db_connector->executeQuery($insrt_qry_sql, $insrt_qry_params) )
2185
            } // if(count($data_2_insert) > 0 ) 
2186
        } // if ( $data_2_insert !== [] )
2187
        
2188
        return $result;
20✔
2189
    }
2190

2191
    /**
2192
     * {@inheritDoc}
2193
     */
2194
    public function insertMany(array $rows_of_data_2_insert = []): bool {
2195

2196
        $result = false;
36✔
2197

2198
        if ($rows_of_data_2_insert !== []) {
36✔
2199

2200
            $table_cols = $this->getTableColNames();
36✔
2201

2202
            foreach (array_keys($rows_of_data_2_insert) as $key) {
36✔
2203

2204
                if( !is_array($rows_of_data_2_insert[$key]) ) {
36✔
2205

2206
                    $item_type = gettype($rows_of_data_2_insert[$key]);
8✔
2207

2208
                    $msg = "ERROR: " . static::class . '::' . __FUNCTION__ . '(...)' 
8✔
2209
                         . " expects you to supply an array of arrays."
8✔
2210
                         . " One of the items in the array supplied is not an array."
8✔
2211
                         . PHP_EOL . " Item below of type `{$item_type}` is not an array: "
8✔
2212
                         . PHP_EOL . var_export($rows_of_data_2_insert[$key], true) 
8✔
2213
                         . PHP_EOL . PHP_EOL . "Data supplied to "
8✔
2214
                         . static::class . '::' . __FUNCTION__ . '(...).' 
8✔
2215
                         . " for insertion into the db table `{$this->getTableName()}`:"
8✔
2216
                         . PHP_EOL . var_export($rows_of_data_2_insert, true) . PHP_EOL
8✔
2217
                         . PHP_EOL;
8✔
2218

2219
                    throw new \GDAO\ModelInvalidInsertValueSuppliedException($msg);
8✔
2220
                }
2221

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

2224
                /** 
2225
                 * @psalm-suppress TypeDoesNotContainType
2226
                 * @psalm-suppress RedundantCondition
2227
                 */
2228
                if((is_countable($rows_of_data_2_insert[$key]) ? count($rows_of_data_2_insert[$key]) : 0) === 0) {
20✔
2229

2230
                    // all the keys in the curent row of data aren't valid
2231
                    // db table columns, remove the row of data from the 
2232
                    // data to be inserted into the DB.
2233
                    unset($rows_of_data_2_insert[$key]);
8✔
2234

2235
                } // if(count($rows_of_data_2_insert[$key]) === 0)
2236

2237
            } // foreach ($rows_of_data_2_insert as $key=>$row_2_insert)
2238

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

2242
                //Insert statement
2243
                $insrt_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newInsert();
20✔
2244

2245
                //Batch all the data into one insert query.
2246
                $insrt_qry_obj->into($this->getTableName())->addRows($rows_of_data_2_insert);           
20✔
2247
                $insrt_qry_sql = $insrt_qry_obj->__toString();
20✔
2248
                $insrt_qry_params = $insrt_qry_obj->getBindValues();
20✔
2249

2250
                $this->logQuery($insrt_qry_sql, $insrt_qry_params, __METHOD__, '' . __LINE__);
20✔
2251
                $result = (bool) $this->db_connector->executeQuery($insrt_qry_sql, $insrt_qry_params);
20✔
2252

2253
            } // if(count($rows_of_data_2_insert) > 0)
2254
        } // if ($rows_of_data_2_insert !== [])
2255

2256
        return $result;
20✔
2257
    }
2258
    
2259
    protected function throwExceptionForInvalidUpdateQueryArg(mixed $val, array $cols_n_vals): never {
2260

2261
        $msg = "ERROR: the value "
16✔
2262
             . PHP_EOL . var_export($val, true) . PHP_EOL
16✔
2263
             . " you are trying to use to bulid the where clause for updating the table `{$this->getTableName()}`"
16✔
2264
             . " is not acceptable ('".  gettype($val) . "'"
16✔
2265
             . " supplied). Boolean, NULL, numeric or string value expected."
16✔
2266
             . PHP_EOL
16✔
2267
             . "Data supplied to "
16✔
2268
             . static::class . '::' . __FUNCTION__ . '(...).' 
16✔
2269
             . " for buiding the where clause for the update:"
16✔
2270
             . PHP_EOL . var_export($cols_n_vals, true) . PHP_EOL
16✔
2271
             . PHP_EOL;
16✔
2272

2273
        throw new \LeanOrm\Exceptions\InvalidArgumentException($msg);
16✔
2274
    }
2275
    
2276
    /**
2277
     * {@inheritDoc}
2278
     * @psalm-suppress RedundantCondition
2279
     */
2280
    public function updateMatchingDbTableRows(
2281
        array $col_names_n_values_2_save = [],
2282
        array $col_names_n_values_2_match = []
2283
    ): static {
2284
        $num_initial_match_items = count($col_names_n_values_2_match);
52✔
2285

2286
        if ($col_names_n_values_2_save !== []) {
52✔
2287

2288
            $table_cols = $this->getTableColNames();
52✔
2289
            $pkey_col_name = $this->getPrimaryCol();
52✔
2290
            $this->addTimestampToData(
52✔
2291
                $col_names_n_values_2_save, $this->updated_timestamp_column_name, $table_cols
52✔
2292
            );
52✔
2293

2294
            if(array_key_exists($pkey_col_name, $col_names_n_values_2_save)) {
52✔
2295

2296
                //don't update the primary key
2297
                unset($col_names_n_values_2_save[$pkey_col_name]);
28✔
2298
            }
2299

2300
            // remove non-existent table columns from the data
2301
            // and check that existent table columns have values of  
2302
            // the right data type: ie. Boolean, NULL, Number or String.
2303
            // Convert objects with a __toString to their string value.
2304
            /** @psalm-suppress MixedAssignment */
2305
            foreach ($col_names_n_values_2_save as $key => $val) {
52✔
2306

2307
                /** @psalm-suppress MixedAssignment */
2308
                $col_names_n_values_2_save[$key] = 
52✔
2309
                    $this->stringifyIfStringable($val, ''.$key, $table_cols);
52✔
2310

2311
                if ( !in_array($key, $table_cols) ) {
52✔
2312

2313
                    unset($col_names_n_values_2_save[$key]);
8✔
2314

2315
                } else if( !$this->isAcceptableUpdateValue($val) ) {
52✔
2316

2317
                    $msg = "ERROR: the value "
8✔
2318
                         . PHP_EOL . var_export($val, true) . PHP_EOL
8✔
2319
                         . " you are trying to update `{$this->getTableName()}`.`{$key}`."
8✔
2320
                         . "{$key} with is not acceptable ('".  gettype($val) . "'"
8✔
2321
                         . " supplied). Boolean, NULL, numeric or string value expected."
8✔
2322
                         . PHP_EOL
8✔
2323
                         . "Data supplied to "
8✔
2324
                         . static::class . '::' . __FUNCTION__ . '(...).' 
8✔
2325
                         . " for update:"
8✔
2326
                         . PHP_EOL . var_export($col_names_n_values_2_save, true) . PHP_EOL
8✔
2327
                         . PHP_EOL;
8✔
2328

2329
                    throw new \GDAO\ModelInvalidUpdateValueSuppliedException($msg);
8✔
2330
                } // if ( !in_array($key, $table_cols) )
2331
            } // foreach ($col_names_n_vals_2_save as $key => $val)
2332

2333
            // After filtering out non-table columns, if we have any table
2334
            // columns data left, we can do the update
2335
            if($col_names_n_values_2_save !== []) {
44✔
2336

2337
                //update statement
2338
                $update_qry_obj = (new QueryFactory($this->getPdoDriverName()))->newUpdate();
44✔
2339
                $update_qry_obj->table($this->getTableName());
44✔
2340
                $update_qry_obj->cols($col_names_n_values_2_save);
44✔
2341

2342
                /** @psalm-suppress MixedAssignment */
2343
                foreach ($col_names_n_values_2_match as $colname => $colval) {
44✔
2344

2345
                    if(!in_array($colname, $table_cols)) {
44✔
2346

2347
                        //non-existent table column
2348
                        unset($col_names_n_values_2_match[$colname]);
8✔
2349
                        continue;
8✔
2350
                    }
2351

2352
                    if (is_array($colval)) {
44✔
2353

2354
                        if($colval !== []) {
16✔
2355

2356
                            /** @psalm-suppress MixedAssignment */
2357
                            foreach ($colval as $key=>$val) {
16✔
2358

2359
                                if(!$this->isAcceptableUpdateQueryValue($val)) {
16✔
2360

2361
                                    $this->throwExceptionForInvalidUpdateQueryArg(
8✔
2362
                                            $val, $col_names_n_values_2_match
8✔
2363
                                        );
8✔
2364
                                }
2365

2366
                                /** @psalm-suppress MixedAssignment */
2367
                                $colval[$key] = $this->stringifyIfStringable($val);
16✔
2368
                            }
2369

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

2372
                        } // if($colval !== []) 
2373

2374
                    } else {
2375

2376
                        if(!$this->isAcceptableUpdateQueryValue($colval)) {
44✔
2377

2378
                            $this->throwExceptionForInvalidUpdateQueryArg(
8✔
2379
                                    $colval, $col_names_n_values_2_match
8✔
2380
                                );
8✔
2381
                        }
2382

2383
                        if(is_null($colval)) {
44✔
2384

2385
                            $update_qry_obj->where(
8✔
2386
                                " {$colname} IS NULL "
8✔
2387
                            );
8✔
2388

2389
                        } else {
2390

2391
                            $update_qry_obj->where(
44✔
2392
                                " {$colname} = :{$colname}_for_where ",  // add the _for_where suffix to deconflict where bind value, 
44✔
2393
                                                                         // from the set bind value when a column in the where clause
2394
                                                                         // is also being set and the value we are setting it to is 
2395
                                                                         // different from the value we are using for the same column 
2396
                                                                         // in the where clause
2397
                                ["{$colname}_for_where" => $this->stringifyIfStringable($colval)] 
44✔
2398
                            );
44✔
2399
                        }
2400

2401
                    } // if (is_array($colval))
2402
                } // foreach ($col_names_n_vals_2_match as $colname => $colval)
2403

2404
                // If after filtering out non existing cols in $col_names_n_vals_2_match
2405
                // if there is still data left in $col_names_n_vals_2_match, then
2406
                // finish building the update query and do the update
2407
                if( 
2408
                    $col_names_n_values_2_match !== [] // there are valid db table cols in here
28✔
2409
                    || 
2410
                    (
2411
                        $num_initial_match_items === 0
28✔
2412
                        && $col_names_n_values_2_match === [] // empty match array passed, we are updating all rows
28✔
2413
                    )
2414
                ) {
2415
                    $updt_qry = $update_qry_obj->__toString();
28✔
2416
                    $updt_qry_params = $update_qry_obj->getBindValues();
28✔
2417
                    $this->logQuery($updt_qry, $updt_qry_params, __METHOD__, '' . __LINE__);
28✔
2418

2419
                    $this->db_connector->executeQuery($updt_qry, $updt_qry_params, true);
28✔
2420
                }
2421

2422
            } // if($col_names_n_vals_2_save !== [])
2423
        } // if ($col_names_n_vals_2_save !== [])
2424

2425
        return $this;
28✔
2426
    }
2427

2428
    /**
2429
     * {@inheritDoc}
2430
     * @psalm-suppress UnusedVariable
2431
     */
2432
    public function updateSpecifiedRecord(\GDAO\Model\RecordInterface $record): static {
2433
        
2434
        if( $record instanceof \LeanOrm\Model\ReadOnlyRecord ) {
44✔
2435

2436
            $msg = "ERROR: Can't save a ReadOnlyRecord to the database in " 
8✔
2437
                 . static::class . '::' . __FUNCTION__ . '(...).'
8✔
2438
                 . PHP_EOL .'Unupdated record' . var_export($record, true) . PHP_EOL;
8✔
2439
            throw new \LeanOrm\Exceptions\CantSaveReadOnlyRecordException($msg);
8✔
2440
        }
2441
        
2442
        if( $record->getModel()->getTableName() !== $this->getTableName() ) {
36✔
2443
            
2444
            $msg = "ERROR: Can't update a record (an instance of `%s`) belonging to the database table `%s` " 
8✔
2445
                . "using a Model instance of `%s` belonging to the database table `%s` in " 
8✔
2446
                 . static::class . '::' . __FUNCTION__ . '(...).'
8✔
2447
                 . PHP_EOL .'Unupdated record: ' . PHP_EOL . var_export($record, true) . PHP_EOL; 
8✔
2448
            throw new \GDAO\ModelInvalidUpdateValueSuppliedException(
8✔
2449
                sprintf(
8✔
2450
                    $msg, $record::class, $record->getModel()->getTableName(),
8✔
2451
                    static::class, $this->getTableName()
8✔
2452
                )
8✔
2453
            );
8✔
2454
        }
2455

2456
        /** @psalm-suppress MixedAssignment */
2457
        $pri_key_val = $record->getPrimaryVal();
28✔
2458
        
2459
        /** @psalm-suppress MixedOperand */
2460
        if( 
2461
            count($record) > 0  // There is data in the record
28✔
2462
            && !$record->isNew() // This is not a new record that wasn't fetched from the DB
28✔
2463
            && !Utils::isEmptyString(''.$pri_key_val) // Record has a primary key value
28✔
2464
            && $record->isChanged() // The data in the record has changed from the state it was when initially fetched from DB
28✔
2465
        ) {
2466
            $cols_n_vals_2_match = [$record->getPrimaryCol()=>$pri_key_val];
28✔
2467

2468
            if($this->getUpdatedTimestampColumnName() !== null) {
28✔
2469

2470
                // Record has changed value(s) & must definitely be updated.
2471
                // Set the value of the $this->getUpdatedTimestampColumnName()
2472
                // field to an empty string, force updateMatchingDbTableRows
2473
                // to add a new updated timestamp value during the update.
2474
                $record->{$this->getUpdatedTimestampColumnName()} = '';
8✔
2475
            }
2476

2477
            $data_2_save = $record->getData();
28✔
2478
            $this->updateMatchingDbTableRows(
28✔
2479
                $data_2_save, 
28✔
2480
                $cols_n_vals_2_match
28✔
2481
            );
28✔
2482

2483
            // update the record with the new updated copy from the DB
2484
            // which will contain the new updated timestamp value.
2485
            $record = $this->fetchOneRecord(
28✔
2486
                        $this->getSelect()
28✔
2487
                             ->where(
28✔
2488
                                    " {$record->getPrimaryCol()} = :{$record->getPrimaryCol()} ", 
28✔
2489
                                    [$record->getPrimaryCol() => $record->getPrimaryVal()]
28✔
2490
                                )
28✔
2491
                    );
28✔
2492
        } // if( count($record) > 0 && !$record->isNew()........
2493

2494
        return $this;
28✔
2495
    }
2496

2497
    /**
2498
     * @psalm-suppress RedundantConditionGivenDocblockType
2499
     */
2500
    protected function addWhereInAndOrIsNullToQuery(
2501
        string $colname, array &$colvals, \Aura\SqlQuery\Common\WhereInterface $qry_obj
2502
    ): void {
2503
        
2504
        if($colvals !== []) { // make sure it's a non-empty array
88✔
2505
            
2506
            // if there are one or more null values in the array,
2507
            // we need to unset them and add an
2508
            //      OR $colname IS NULL 
2509
            // clause to the query
2510
            $unique_colvals = array_unique($colvals);
88✔
2511
            $keys_for_null_vals = array_keys($unique_colvals, null, true);
88✔
2512

2513
            foreach($keys_for_null_vals as $key_for_null_val) {
88✔
2514

2515
                // remove the null vals from $colval
2516
                unset($unique_colvals[$key_for_null_val]);
8✔
2517
            }
2518

2519
            if(
2520
                $keys_for_null_vals !== [] && $unique_colvals !== []
88✔
2521
            ) {
2522
                // Some values in the array are null and some are non-null
2523
                // Generate WHERE COL IN () OR COL IS NULL
2524
                $qry_obj->where(
8✔
2525
                    " {$colname} IN (:bar) ",
8✔
2526
                    [ 'bar' => $unique_colvals ]
8✔
2527
                )->orWhere(" {$colname} IS NULL ");
8✔
2528

2529
            } elseif (
2530
                $keys_for_null_vals !== []
88✔
2531
                && $unique_colvals === []
88✔
2532
            ) {
2533
                // All values in the array are null
2534
                // Only generate WHERE COL IS NULL
2535
                $qry_obj->where(" {$colname} IS NULL ");
8✔
2536

2537
            } else { // ($keys_for_null_vals === [] && $unique_colvals !== []) // no nulls found
2538
                
2539
                ////////////////////////////////////////////////////////////////
2540
                // NOTE: ($keys_for_null_vals === [] && $unique_colvals === [])  
2541
                // is impossible because we started with if($colvals !== [])
2542
                ////////////////////////////////////////////////////////////////
2543

2544
                // All values in the array are non-null
2545
                // Only generate WHERE COL IN ()
2546
                $qry_obj->where(       
88✔
2547
                    " {$colname} IN (:bar) ",
88✔
2548
                    [ 'bar' => $unique_colvals ]
88✔
2549
                );
88✔
2550
            }
2551
        }
2552
    }
2553
    
2554
    /**
2555
     * @return array{
2556
     *              database_server_info: mixed, 
2557
     *              driver_name: mixed, 
2558
     *              pdo_client_version: mixed, 
2559
     *              database_server_version: mixed, 
2560
     *              connection_status: mixed, 
2561
     *              connection_is_persistent: mixed
2562
     *          }
2563
     * 
2564
     * @psalm-suppress PossiblyUnusedMethod
2565
     */
2566
    public function getCurrentConnectionInfo(): array {
2567

2568
        $pdo_obj = $this->getPDO();
8✔
2569
        $attributes = [
8✔
2570
            'database_server_info' => 'SERVER_INFO',
8✔
2571
            'driver_name' => 'DRIVER_NAME',
8✔
2572
            'pdo_client_version' => 'CLIENT_VERSION',
8✔
2573
            'database_server_version' => 'SERVER_VERSION',
8✔
2574
            'connection_status' => 'CONNECTION_STATUS',
8✔
2575
            'connection_is_persistent' => 'PERSISTENT',
8✔
2576
        ];
8✔
2577

2578
        foreach ($attributes as $key => $value) {
8✔
2579
            
2580
            try {
2581
                /**
2582
                 * @psalm-suppress MixedAssignment
2583
                 * @psalm-suppress MixedArgument
2584
                 */
2585
                $attributes[ $key ] = $pdo_obj->getAttribute(constant(\PDO::class .'::ATTR_' . $value));
8✔
2586
                
2587
            } catch (\PDOException) {
8✔
2588
                
2589
                $attributes[ $key ] = 'Unsupported attribute for the current PDO driver';
8✔
2590
                continue;
8✔
2591
            }
2592

2593
            if( $value === 'PERSISTENT' ) {
8✔
2594

2595
                $attributes[ $key ] = var_export($attributes[ $key ], true);
8✔
2596
            }
2597
        }
2598

2599
        return $attributes;
8✔
2600
    }
2601

2602
    /**
2603
     * @return mixed[]
2604
     * @psalm-suppress PossiblyUnusedMethod
2605
     */
2606
    public function getQueryLog(): array {
2607

2608
        return $this->query_log;
8✔
2609
    }
2610

2611
    /**
2612
     * To get the log for all existing instances of this class & its subclasses,
2613
     * call this method with no args or with null.
2614
     * 
2615
     * To get the log for instances of a specific class (this class or a
2616
     * particular sub-class of this class), you must call this method with 
2617
     * an instance of the class whose log you want to get.
2618
     * 
2619
     * @return mixed[]
2620
     * @psalm-suppress PossiblyUnusedMethod
2621
     */
2622
    public static function getQueryLogForAllInstances(?\GDAO\Model $obj=null): array {
2623
        
2624
        $key = ($obj instanceof \GDAO\Model) ? static::createLoggingKey($obj) : '';
16✔
2625
        
2626
        return ($obj instanceof \GDAO\Model)
16✔
2627
                ?
16✔
2628
                (
16✔
2629
                    array_key_exists($key, static::$all_instances_query_log) 
8✔
2630
                    ? static::$all_instances_query_log[$key] : [] 
8✔
2631
                )
16✔
2632
                : static::$all_instances_query_log 
16✔
2633
                ;
16✔
2634
    }
2635
    
2636
    /**
2637
     * @psalm-suppress PossiblyUnusedMethod
2638
     */
2639
    public static function clearQueryLogForAllInstances(): void {
2640
        
2641
        static::$all_instances_query_log = [];
24✔
2642
    }
2643

2644
    protected static function createLoggingKey(\GDAO\Model $obj): string {
2645
        
2646
        return "{$obj->getDsn()}::" . $obj::class;
32✔
2647
    }
2648
    
2649
    protected function logQuery(string $sql, array $bind_params, string $calling_method='', string $calling_line=''): static {
2650

2651
        if( $this->can_log_queries ) {
296✔
2652

2653
            $key = static::createLoggingKey($this);
32✔
2654
            
2655
            if(!array_key_exists($key, static::$all_instances_query_log)) {
32✔
2656

2657
                static::$all_instances_query_log[$key] = [];
32✔
2658
            }
2659

2660
            $log_record = [
32✔
2661
                'sql' => $sql,
32✔
2662
                'bind_params' => $bind_params,
32✔
2663
                'date_executed' => date('Y-m-d H:i:s'),
32✔
2664
                'class_method' => $calling_method,
32✔
2665
                'line_of_execution' => $calling_line,
32✔
2666
            ];
32✔
2667
            
2668
            /** @psalm-suppress InvalidPropertyAssignmentValue */
2669
            $this->query_log[] = $log_record;
32✔
2670
            static::$all_instances_query_log[$key][] = $log_record;
32✔
2671

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

2674
                $this->logger->info(
8✔
2675
                    PHP_EOL . PHP_EOL .
8✔
2676
                    'SQL:' . PHP_EOL . "{$sql}" . PHP_EOL . PHP_EOL . PHP_EOL .
8✔
2677
                    'BIND PARAMS:' . PHP_EOL . var_export($bind_params, true) .
8✔
2678
                    PHP_EOL . "Calling Method: `{$calling_method}`" . PHP_EOL .
8✔
2679
                    "Line of Execution: `{$calling_line}`" . PHP_EOL .
8✔
2680
                     PHP_EOL . PHP_EOL . PHP_EOL
8✔
2681
                );
8✔
2682
            }                    
2683
        }
2684

2685
        return $this;
296✔
2686
    }
2687

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

2761
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
176✔
2762
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
176✔
2763
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
176✔
2764

2765
        $this->relations[$relation_name]['sql_query_modifier'] = $sql_query_modifier;
176✔
2766

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

2839
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
232✔
2840
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
232✔
2841
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
232✔
2842

2843
        $this->relations[$relation_name]['sql_query_modifier'] = $sql_query_modifier;
232✔
2844

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

2919
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
1,308✔
2920
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
1,308✔
2921
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
1,308✔
2922

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

2925
        return $this;
1,308✔
2926
    }
2927

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

3005
        $this->relations[$relation_name]['foreign_models_class_name'] = $foreign_models_class_name;
192✔
3006
        $this->relations[$relation_name]['foreign_models_record_class_name'] = $foreign_models_record_class_name;
192✔
3007
        $this->relations[$relation_name]['foreign_models_collection_class_name'] = $foreign_models_collection_class_name;
192✔
3008

3009
        $this->relations[$relation_name]['sql_query_modifier'] = $sql_query_modifier;
192✔
3010

3011
        return $this;
192✔
3012
    }
3013
    
3014
    /**
3015
     * @psalm-suppress MixedAssignment
3016
     */
3017
    protected function setRelationshipDefinitionDefaultsIfNeeded (
3018
        string &$foreign_models_class_name,
3019
        string &$foreign_table_name,
3020
        string &$primary_key_col_in_foreign_table,
3021
        string &$foreign_models_record_class_name,
3022
        string &$foreign_models_collection_class_name,
3023
    ): void {
3024
        
3025
        if($foreign_models_class_name !== '' && $foreign_models_class_name !== \LeanOrm\Model::class) {
1,308✔
3026
           
3027
            $this->validateRelatedModelClassName($foreign_models_class_name);
1,308✔
3028
            
3029
            /**
3030
             * @psalm-suppress ArgumentTypeCoercion
3031
             */
3032
            $ref_class = new \ReflectionClass($foreign_models_class_name);
1,308✔
3033
            
3034
            if($foreign_table_name === '') {
1,308✔
3035
                
3036
                // Try to set it using the default value of the table_name property 
3037
                // in the specified foreign model class $foreign_models_class_name
3038
                $reflected_foreign_table_name = 
64✔
3039
                        $ref_class->getProperty('table_name')->getDefaultValue();
64✔
3040

3041
                if($reflected_foreign_table_name === '' || $reflected_foreign_table_name === null) {
64✔
3042
                    
3043
                    $msg = "ERROR: '\$foreign_table_name' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3044
                         . $foreign_models_class_name . "'"
32✔
3045
                         . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3046

3047
                    // we can't use Reflection to figure out this table name
3048
                    throw new \LeanOrm\Exceptions\MissingRelationParamException($msg);
32✔
3049
                }
3050
                
3051
                $foreign_table_name = $reflected_foreign_table_name;
32✔
3052
            }
3053
            
3054
            if($primary_key_col_in_foreign_table === '') {
1,308✔
3055

3056
                // Try to set it using the default value of the primary_col property 
3057
                // in the specified foreign model class $foreign_models_class_name
3058
                $reflected_foreign_primary_key_col = 
64✔
3059
                        $ref_class->getProperty('primary_col')->getDefaultValue();
64✔
3060

3061
                if($reflected_foreign_primary_key_col === '' || $reflected_foreign_primary_key_col === null) {
64✔
3062

3063
                    $msg = "ERROR: '\$primary_key_col_in_foreign_table' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3064
                         . $foreign_models_class_name . "'"
32✔
3065
                         . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3066

3067
                    // we can't use Reflection to figure out this primary key column name
3068
                    throw new \LeanOrm\Exceptions\MissingRelationParamException($msg);
32✔
3069
                }
3070

3071
                // set it to the reflected value
3072
                $primary_key_col_in_foreign_table = $reflected_foreign_primary_key_col;
32✔
3073
            }
3074
            
3075
            $reflected_record_class_name = $ref_class->getProperty('record_class_name')->getDefaultValue();
1,308✔
3076
            
3077
            if(
3078
                $foreign_models_record_class_name === ''
1,308✔
3079
                && $reflected_record_class_name !== ''
1,308✔
3080
                && $reflected_record_class_name !== null
1,308✔
3081
            ) {
3082
                $foreign_models_record_class_name = $reflected_record_class_name;
32✔
3083
            }
3084
            
3085
            $reflected_collection_class_name = $ref_class->getProperty('collection_class_name')->getDefaultValue();
1,308✔
3086
            
3087
            if(
3088
                $foreign_models_collection_class_name === ''
1,308✔
3089
                && $reflected_collection_class_name !== ''
1,308✔
3090
                && $reflected_collection_class_name !== null
1,308✔
3091
            ) {
3092
                $foreign_models_collection_class_name = $reflected_collection_class_name;
351✔
3093
            }
3094
            
3095
        } else {
3096
            
3097
            $foreign_models_class_name = \LeanOrm\Model::class;
248✔
3098
            
3099
            if($foreign_table_name === '') {
248✔
3100
                
3101
                $msg = "ERROR: '\$foreign_table_name' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3102
                     . \LeanOrm\Model::class . "'"
32✔
3103
                     . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3104
                
3105
                // we can't use Reflection to figure out this table name
3106
                // because \LeanOrm\Model->table_name has a default value of ''
3107
                throw new \LeanOrm\Exceptions\MissingRelationParamException($msg);
32✔
3108
            }
3109
            
3110
            // $foreign_table_name !== '' if we got this far
3111
            if($primary_key_col_in_foreign_table === '') {
216✔
3112

3113
                $msg = "ERROR: '\$primary_key_col_in_foreign_table' cannot be empty when  '\$foreign_models_class_name' has a value of '"
32✔
3114
                     . \LeanOrm\Model::class . "'"
32✔
3115
                     . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' . PHP_EOL;
32✔
3116

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

3121
            }
3122
        } // if($foreign_models_class_name !== '' && $foreign_models_class_name !== \LeanOrm\Model::class)
3123
        
3124
        if($foreign_models_record_class_name === '') {
1,308✔
3125
            
3126
            $foreign_models_record_class_name = \LeanOrm\Model\Record::class;
184✔
3127
        }
3128
        
3129
        if($foreign_models_collection_class_name === '') {
1,308✔
3130
            
3131
            $foreign_models_collection_class_name = \LeanOrm\Model\Collection::class;
184✔
3132
        }
3133
    }
3134
    
3135
    protected function checkThatRelationNameIsNotAnActualColumnName(string $relationName): void {
3136

3137
        $tableCols = $this->getTableColNames();
1,308✔
3138

3139
        /** @psalm-suppress MixedArgument */
3140
        $tableColsLowerCase = array_map('strtolower', $tableCols);
1,308✔
3141

3142
        if( in_array(strtolower($relationName), $tableColsLowerCase) ) {
1,308✔
3143

3144
            //Error trying to add a relation whose name collides with an actual
3145
            //name of a column in the db table associated with this model.
3146
            $msg = sprintf("ERROR: You cannont add a relationship with the name '%s' ", $relationName)
32✔
3147
                 . " to the Model (".static::class."). The database table "
32✔
3148
                 . sprintf(" '%s' associated with the ", $this->getTableName())
32✔
3149
                 . " model (".static::class.") already contains"
32✔
3150
                 . " a column with the same name."
32✔
3151
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
32✔
3152
                 . PHP_EOL;
32✔
3153

3154
            throw new \GDAO\Model\RecordRelationWithSameNameAsAnExistingDBTableColumnNameException($msg);
32✔
3155
        } // if( in_array(strtolower($relationName), $tableColsLowerCase) ) 
3156
    }
3157
    
3158
    /**
3159
     * @psalm-suppress PossiblyUnusedReturnValue
3160
     */
3161
    protected function validateTableName(string $table_name): bool {
3162
        
3163
        if(!$this->tableExistsInDB($table_name)) {
1,308✔
3164
            
3165
            //throw exception
3166
            $msg = "ERROR: The specified table `{$table_name}` does not exist in the DB."
40✔
3167
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
40✔
3168
                 . PHP_EOL;
40✔
3169
            throw new \LeanOrm\Exceptions\BadModelTableNameException($msg);
40✔
3170
        } // if(!$this->tableExistsInDB($table_name))
3171
        
3172
        return true;
1,308✔
3173
    }
3174
    
3175
    /**
3176
     * @psalm-suppress PossiblyUnusedReturnValue
3177
     */
3178
    protected function validateThatTableHasColumn(string $table_name, string $column_name): bool {
3179
        
3180
        if(!$this->columnExistsInDbTable($table_name, $column_name)) {
1,308✔
3181

3182
            //throw exception
3183
            $msg = "ERROR: The specified table `{$table_name}` in the DB"
112✔
3184
                 . " does not contain the specified column `{$column_name}`."
112✔
3185
                 . PHP_EOL . static::class . '::' . __FUNCTION__ . '(...).' 
112✔
3186
                 . PHP_EOL;
112✔
3187
            throw new \LeanOrm\Exceptions\BadModelColumnNameException($msg);
112✔
3188
        } // if(!$this->columnExistsInDbTable($table_name, $column_name))
3189
        
3190
        return true;
1,308✔
3191
    }
3192
}
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