TDBMService::findObjectFromSql()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 6
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
7
8
This program is free software; you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation; either version 2 of the License, or
11
(at your option) any later version.
12
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
GNU General Public License for more details.
17
18
You should have received a copy of the GNU General Public License
19
along with this program; if not, write to the Free Software
20
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21
*/
22
23
namespace TheCodingMachine\TDBM;
24
25
use Doctrine\Common\Cache\Cache;
26
use Doctrine\Common\Cache\ClearableCache;
27
use Doctrine\DBAL\Connection;
28
use Doctrine\DBAL\Exception as DBALException;
29
use Doctrine\DBAL\Platforms\AbstractPlatform;
30
use Doctrine\DBAL\Platforms\MySQLPlatform;
31
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
32
use Doctrine\DBAL\Schema\Table;
33
use Doctrine\DBAL\Types\Type;
34
use Mouf\Database\MagicQuery;
35
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
36
use TheCodingMachine\TDBM\QueryFactory\FindObjectsFromSqlQueryFactory;
37
use TheCodingMachine\TDBM\QueryFactory\FindObjectsQueryFactory;
38
use TheCodingMachine\TDBM\QueryFactory\FindObjectsFromRawSqlQueryFactory;
39
use TheCodingMachine\TDBM\Utils\Logs\LevelFilter;
40
use TheCodingMachine\TDBM\Utils\ManyToManyRelationshipPathDescriptor;
41
use TheCodingMachine\TDBM\Utils\NamingStrategyInterface;
42
use TheCodingMachine\TDBM\Utils\TDBMDaoGenerator;
43
use Psr\Log\LoggerInterface;
44
use Psr\Log\LogLevel;
45
use Psr\Log\NullLogger;
46
47
use function class_exists;
48
49
/**
50
 * The TDBMService class is the main TDBM class. It provides methods to retrieve TDBMObject instances
51
 * from the database.
52
 *
53
 * @author David Negrier
54
 * @ExtendedAction {"name":"Generate DAOs", "url":"tdbmadmin/", "default":false}
55
 */
56
class TDBMService
57
{
58
    public const MODE_CURSOR = 1;
59
    public const MODE_ARRAY = 2;
60
61
    /**
62
     * The database connection.
63
     *
64
     * @var Connection
65
     */
66
    private $connection;
67
68
    /**
69
     * @var SchemaAnalyzer
70
     */
71
    private $schemaAnalyzer;
72
73
    /**
74
     * @var MagicQuery
75
     */
76
    private $magicQuery;
77
78
    /**
79
     * @var TDBMSchemaAnalyzer
80
     */
81
    private $tdbmSchemaAnalyzer;
82
83
    /**
84
     * @var string
85
     */
86
    private $cachePrefix;
87
88
    /**
89
     * Cache of table of primary keys.
90
     * Primary keys are stored by tables, as an array of column.
91
     * For instance $primary_key['my_table'][0] will return the first column of the primary key of table 'my_table'.
92
     *
93
     * @var string[][]
94
     */
95
    private $primaryKeysColumns;
96
97
    /**
98
     * Service storing objects in memory.
99
     * Access is done by table name and then by primary key.
100
     * If the primary key is split on several columns, access is done by an array of columns, serialized.
101
     *
102
     * @var ObjectStorageInterface
103
     */
104
    private $objectStorage;
105
106
    /**
107
     * The fetch mode of the result sets returned by `getObjects`.
108
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY.
109
     *
110
     * In 'MODE_ARRAY' mode (default), the result is an array. Use this mode by default (unless the list returned is very big).
111
     * In 'MODE_CURSOR' mode, the result is a Generator which is an iterable collection that can be scanned only once (only one "foreach") on it,
112
     * and it cannot be accessed via key. Use this mode for large datasets processed by batch.
113
     * In 'MODE_COMPATIBLE_ARRAY' mode, the result is an old TDBMObjectArray (used up to TDBM 3.2).
114
     * You can access the array by key, or using foreach, several times.
115
     *
116
     * @var int
117
     */
118
    private $mode = self::MODE_ARRAY;
119
120
    /**
121
     * Table of new objects not yet inserted in database or objects modified that must be saved.
122
     *
123
     * @var \SplObjectStorage of DbRow objects
124
     */
125
    private $toSaveObjects;
126
127
    /**
128
     * A cache service to be used.
129
     *
130
     * @var Cache
131
     */
132
    private $cache;
133
134
    /**
135
     * Map associating a table name to a fully qualified Bean class name.
136
     *
137
     * @var array
138
     */
139
    private $tableToBeanMap = [];
140
141
    /**
142
     * @var \ReflectionClass[]
143
     */
144
    private $reflectionClassCache = array();
145
146
    /**
147
     * @var LoggerInterface
148
     */
149
    private $rootLogger;
150
151
    /**
152
     * @var LevelFilter|NullLogger
153
     */
154
    private $logger;
155
156
    /**
157
     * @var OrderByAnalyzer
158
     */
159
    private $orderByAnalyzer;
160
161
    /**
162
     * @var string
163
     */
164
    private $beanNamespace;
165
    /**
166
     * @var string
167
     */
168
    private $resultIteratorNamespace;
169
170
    /**
171
     * @var NamingStrategyInterface
172
     */
173
    private $namingStrategy;
174
    /**
175
     * @var ConfigurationInterface
176
     */
177
    private $configuration;
178
    /**
179
     * @var SchemaLockFileDumper
180
     */
181
    private $schemaLockFileDumper;
182
183
    /**
184
     * @param ConfigurationInterface $configuration The configuration object
185
     */
186
    public function __construct(ConfigurationInterface $configuration)
187
    {
188
        $this->objectStorage = new NativeWeakrefObjectStorage();
189
        $this->connection = $configuration->getConnection();
190
        $this->cache = $configuration->getCache();
191
        $this->schemaAnalyzer = $configuration->getSchemaAnalyzer();
192
        $lockFilePath = $configuration->getLockFilePath();
193
194
        $this->magicQuery = new MagicQuery($this->connection, $this->cache, $this->schemaAnalyzer);
195
196
        $this->schemaLockFileDumper = new SchemaLockFileDumper($this->connection, $this->cache, $lockFilePath);
197
        $this->tdbmSchemaAnalyzer = new TDBMSchemaAnalyzer($this->connection, $this->cache, $this->schemaAnalyzer, $this->schemaLockFileDumper);
198
        $this->cachePrefix = $this->tdbmSchemaAnalyzer->getCachePrefix();
199
200
        $this->toSaveObjects = new \SplObjectStorage();
201
        $logger = $configuration->getLogger();
202
        if ($logger === null) {
203
            $this->logger = new NullLogger();
204
            $this->rootLogger = new NullLogger();
205
        } else {
206
            $this->rootLogger = $logger;
207
            $this->setLogLevel(LogLevel::WARNING);
208
        }
209
        $this->orderByAnalyzer = new OrderByAnalyzer($this->cache, $this->cachePrefix);
210
        $this->beanNamespace = $configuration->getBeanNamespace();
211
        $this->resultIteratorNamespace = $configuration->getResultIteratorNamespace();
212
        $this->namingStrategy = $configuration->getNamingStrategy();
213
        $this->configuration = $configuration;
214
    }
215
216
    /**
217
     * Returns the object used to connect to the database.
218
     *
219
     * @return Connection
220
     */
221
    public function getConnection(): Connection
222
    {
223
        return $this->connection;
224
    }
225
226
    /**
227
     * Sets the default fetch mode of the result sets returned by `findObjects`.
228
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY.
229
     *
230
     * In 'MODE_ARRAY' mode (default), the result is a ResultIterator object that behaves like an array. Use this mode by default (unless the list returned is very big).
231
     * In 'MODE_CURSOR' mode, the result is a ResultIterator object. If you scan it many times (by calling several time a foreach loop), the query will be run
232
     * several times. In cursor mode, you cannot access the result set by key. Use this mode for large datasets processed by batch.
233
     *
234
     * @param int $mode
235
     *
236
     * @return self
237
     *
238
     * @throws TDBMException
239
     */
240
    public function setFetchMode(int $mode): self
241
    {
242
        if ($mode !== self::MODE_CURSOR && $mode !== self::MODE_ARRAY) {
243
            throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
244
        }
245
        $this->mode = $mode;
246
247
        return $this;
248
    }
249
250
    /**
251
     * Removes the given object from database.
252
     * This cannot be called on an object that is not attached to this TDBMService
253
     * (will throw a TDBMInvalidOperationException).
254
     *
255
     * @param AbstractTDBMObject $object the object to delete
256
     *
257
     * @throws DBALException
258
     * @throws TDBMInvalidOperationException
259
     */
260
    public function delete(AbstractTDBMObject $object): void
261
    {
262
        switch ($object->_getStatus()) {
263
            case TDBMObjectStateEnum::STATE_DELETED:
264
                // Nothing to do, object already deleted.
265
                return;
266
            case TDBMObjectStateEnum::STATE_DETACHED:
267
                throw new TDBMInvalidOperationException('Cannot delete a detached object');
268
            case TDBMObjectStateEnum::STATE_NEW:
269
                $this->deleteManyToManyRelationships($object);
270
                foreach ($object->_getDbRows() as $dbRow) {
271
                    $this->removeFromToSaveObjectList($dbRow);
272
                }
273
                break;
274
            case TDBMObjectStateEnum::STATE_DIRTY:
275
                foreach ($object->_getDbRows() as $dbRow) {
276
                    $this->removeFromToSaveObjectList($dbRow);
277
                }
278
                // And continue deleting...
279
                // no break
280
            case TDBMObjectStateEnum::STATE_NOT_LOADED:
281
            case TDBMObjectStateEnum::STATE_LOADED:
282
                $this->connection->beginTransaction();
283
                try {
284
                    $this->deleteManyToManyRelationships($object);
285
                    // Let's delete db rows, in reverse order.
286
                    foreach (array_reverse($object->_getDbRows()) as $dbRow) {
287
                        /* @var $dbRow DbRow */
288
                        $tableName = $dbRow->_getDbTableName();
289
                        $primaryKeys = $dbRow->_getPrimaryKeys();
290
                        $quotedPrimaryKeys = [];
291
                        foreach ($primaryKeys as $column => $value) {
292
                            $quotedPrimaryKeys[$this->connection->quoteIdentifier($column)] = $value;
293
                        }
294
                        $this->connection->delete($this->connection->quoteIdentifier($tableName), $quotedPrimaryKeys);
295
                        $this->objectStorage->remove($dbRow->_getDbTableName(), $this->getObjectHash($primaryKeys));
296
                    }
297
                    $this->connection->commit();
298
                } catch (DBALException $e) {
299
                    $this->connection->rollBack();
300
                    throw $e;
301
                }
302
                break;
303
                // @codeCoverageIgnoreStart
304
            default:
305
                throw new TDBMInvalidOperationException('Unexpected status for bean');
306
                // @codeCoverageIgnoreEnd
307
        }
308
309
        $object->_setStatus(TDBMObjectStateEnum::STATE_DELETED);
310
    }
311
312
    /**
313
     * Removes all many to many relationships for this object.
314
     *
315
     * @param AbstractTDBMObject $object
316
     */
317
    private function deleteManyToManyRelationships(AbstractTDBMObject $object): void
318
    {
319
        foreach ($object->_getDbRows() as $tableName => $dbRow) {
320
            foreach ($object->_getManyToManyRelationshipDescriptorKeys() as $pathKey) {
321
                $pathModel = $object->_getManyToManyRelationshipDescriptor($pathKey);
322
                $remoteBeans = $object->_getRelationshipsFromModel($pathModel);
323
                foreach ($remoteBeans as $remoteBean) {
324
                    $object->_removeRelationship($pathModel->getPivotName(), $remoteBean);
325
                }
326
            }
327
        }
328
        $this->persistManyToManyRelationships($object);
329
    }
330
331
    /**
332
     * This function removes the given object from the database. It will also remove all objects relied to the one given
333
     * by parameter before all.
334
     *
335
     * Notice: if the object has a multiple primary key, the function will not work.
336
     *
337
     * @param AbstractTDBMObject $objToDelete
338
     */
339
    public function deleteCascade(AbstractTDBMObject $objToDelete): void
340
    {
341
        $this->deleteAllConstraintWithThisObject($objToDelete);
342
        $this->delete($objToDelete);
343
    }
344
345
    /**
346
     * This function is used only in TDBMService (private function)
347
     * It will call deleteCascade function foreach object relied with a foreign key to the object given by parameter.
348
     *
349
     * @param AbstractTDBMObject $obj
350
     */
351
    private function deleteAllConstraintWithThisObject(AbstractTDBMObject $obj): void
352
    {
353
        $dbRows = $obj->_getDbRows();
354
        foreach ($dbRows as $dbRow) {
355
            $tableName = $dbRow->_getDbTableName();
356
            $pks = array_values($dbRow->_getPrimaryKeys());
357
            if (!empty($pks)) {
358
                $incomingFks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($tableName);
359
360
                foreach ($incomingFks as $incomingFk) {
361
                    $filter = array_combine($incomingFk->getUnquotedLocalColumns(), $pks);
362
363
                    $localTableName = $incomingFk->getLocalTableName();
364
365
                    $className = $this->beanNamespace . '\\' . $this->namingStrategy->getBeanClassName($localTableName);
366
                    assert(class_exists($className));
367
368
                    $resultIteratorClassName = $this->resultIteratorNamespace . '\\' . $this->namingStrategy->getResultIteratorClassName($localTableName);
369
                    assert(class_exists($resultIteratorClassName));
370
371
                    $results = $this->findObjects(
372
                        $localTableName,
373
                        $filter,
374
                        [],
375
                        null,
376
                        [],
377
                        null,
378
                        $className,
379
                        $resultIteratorClassName
380
                    );
381
382
                    foreach ($results as $bean) {
383
                        $this->deleteCascade($bean);
384
                    }
385
                }
386
            }
387
        }
388
    }
389
390
    /**
391
     * This function performs a save() of all the objects that have been modified.
392
     */
393
    public function completeSave(): void
394
    {
395
        foreach ($this->toSaveObjects as $dbRow) {
396
            $this->save($dbRow->getTDBMObject());
397
        }
398
    }
399
400
    /**
401
     * Takes in input a filter_bag (which can be about anything from a string to an array of TDBMObjects... see above from documentation),
402
     * and gives back a proper Filter object.
403
     *
404
     * @param mixed $filter_bag
405
     * @param AbstractPlatform $platform The platform used to quote identifiers
406
     * @param int $counter
407
     * @return mixed[] First item: filter string, second item: parameters, third item: the count
408
     *
409
     * @throws TDBMException
410
     */
411
    public function buildFilterFromFilterBag($filter_bag, AbstractPlatform $platform, int $counter = 1): array
412
    {
413
        if ($filter_bag === null || $filter_bag === []) {
414
            return ['', [], $counter];
415
        } elseif (is_string($filter_bag)) {
416
            return [$filter_bag, [], $counter];
417
        } elseif (is_array($filter_bag)) {
418
            $sqlParts = [];
419
            $parameters = [];
420
421
            foreach ($filter_bag as $column => $value) {
422
                if (is_int($column)) {
423
                    list($subSqlPart, $subParameters, $counter) = $this->buildFilterFromFilterBag($value, $platform, $counter);
424
                    $sqlParts[] = $subSqlPart;
425
                    $parameters += $subParameters;
426
                } else {
427
                    $paramName = 'tdbmparam'.$counter;
428
                    if (is_array($value)) {
429
                        $sqlParts[] = $platform->quoteIdentifier($column).' IN (:'.$paramName.')';
430
                    } else {
431
                        $sqlParts[] = $platform->quoteIdentifier($column).' = :'.$paramName;
432
                    }
433
                    $parameters[$paramName] = $value;
434
                    ++$counter;
435
                }
436
            }
437
438
            return ['(' . implode(') AND (', $sqlParts) . ')', $parameters, $counter];
439
        } elseif ($filter_bag instanceof ResultIterator) {
440
            $subQuery = $filter_bag->_getSubQuery();
441
            return [$subQuery, [], $counter];
442
        } elseif ($filter_bag instanceof AbstractTDBMObject) {
443
            $sqlParts = [];
444
            $parameters = [];
445
            $dbRows = $filter_bag->_getDbRows();
446
            $dbRow = reset($dbRows);
447
            if ($dbRow === false) {
448
                throw new \RuntimeException('Unexpected error: empty dbRow'); // @codeCoverageIgnore
449
            }
450
            $primaryKeys = $dbRow->_getPrimaryKeys();
451
452
            foreach ($primaryKeys as $column => $value) {
453
                $paramName = 'tdbmparam'.$counter;
454
                $sqlParts[] = $platform->quoteIdentifier($dbRow->_getDbTableName()).'.'.$platform->quoteIdentifier($column).' = :'.$paramName;
455
                $parameters[$paramName] = $value;
456
                ++$counter;
457
            }
458
459
            return [implode(' AND ', $sqlParts), $parameters, $counter];
460
        } elseif ($filter_bag instanceof \Iterator) {
461
            // TODO: we could instead check if is_iterable($filter_bag). That would remove useless code here.
462
            return $this->buildFilterFromFilterBag(iterator_to_array($filter_bag), $platform, $counter);
463
        } else {
464
            throw new TDBMException('Error in filter. An object has been passed that is neither a SQL string, nor an array, nor a bean, nor null.');
465
        }
466
    }
467
468
    /**
469
     * @param string $table
470
     *
471
     * @return string[]
472
     */
473
    public function getPrimaryKeyColumns(string $table): array
474
    {
475
        if (!isset($this->primaryKeysColumns[$table])) {
476
            $primaryKey = $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKey();
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

476
            $primaryKey = /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKey();
Loading history...
477
            if ($primaryKey === null) {
478
                // Security check: a table MUST have a primary key
479
                throw new TDBMException(sprintf('Table "%s" does not have any primary key', $table));
480
            }
481
482
            $this->primaryKeysColumns[$table] = $primaryKey->getUnquotedColumns();
483
484
            // TODO TDBM4: See if we need to improve error reporting if table name does not exist.
485
486
            /*$arr = array();
487
            foreach ($this->connection->getPrimaryKey($table) as $col) {
488
                $arr[] = $col->name;
489
            }
490
            // The primaryKeysColumns contains only the column's name, not the DB_Column object.
491
            $this->primaryKeysColumns[$table] = $arr;
492
            if (empty($this->primaryKeysColumns[$table]))
493
            {
494
                // Unable to find primary key.... this is an error
495
                // Let's try to be precise in error reporting. Let's try to find the table.
496
                $tables = $this->connection->checkTableExist($table);
497
                if ($tables === true)
498
                throw new TDBMException("Could not find table primary key for table '$table'. Please define a primary key for this table.");
499
                elseif ($tables !== null) {
500
                    if (count($tables)==1)
501
                    $str = "Could not find table '$table'. Maybe you meant this table: '".$tables[0]."'";
502
                    else
503
                    $str = "Could not find table '$table'. Maybe you meant one of those tables: '".implode("', '",$tables)."'";
504
                    throw new TDBMException($str);
505
                }
506
            }*/
507
        }
508
509
        return $this->primaryKeysColumns[$table];
510
    }
511
512
    /**
513
     * This is an internal function, you should not use it in your application.
514
     * This is used internally by TDBM to add an object to the object cache.
515
     *
516
     * @param DbRow $dbRow
517
     */
518
    public function _addToCache(DbRow $dbRow): void
519
    {
520
        $primaryKey = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
521
        $hash = $this->getObjectHash($primaryKey);
522
        $this->objectStorage->set($dbRow->_getDbTableName(), $hash, $dbRow);
523
    }
524
525
    /**
526
     * This is an internal function, you should not use it in your application.
527
     * This is used internally by TDBM to remove the object from the list of objects that have been
528
     * created/updated but not saved yet.
529
     *
530
     * @param DbRow $myObject
531
     */
532
    private function removeFromToSaveObjectList(DbRow $myObject): void
533
    {
534
        unset($this->toSaveObjects[$myObject]);
535
    }
536
537
    /**
538
     * This is an internal function, you should not use it in your application.
539
     * This is used internally by TDBM to add an object to the list of objects that have been
540
     * created/updated but not saved yet.
541
     *
542
     * @param DbRow $myObject
543
     */
544
    public function _addToToSaveObjectList(DbRow $myObject): void
545
    {
546
        $this->toSaveObjects[$myObject] = true;
547
    }
548
549
    /**
550
     * Generates all the daos and beans.
551
     */
552
    public function generateAllDaosAndBeans(bool $fromLock = false): void
553
    {
554
        // Purge cache before generating anything.
555
        if ($this->cache instanceof ClearableCache) {
556
            $this->cache->deleteAll();
557
        }
558
559
        $tdbmDaoGenerator = new TDBMDaoGenerator($this->configuration, $this->tdbmSchemaAnalyzer);
560
        $tdbmDaoGenerator->generateAllDaosAndBeans($fromLock);
561
    }
562
563
    /**
564
     * Returns the fully qualified class name of the bean associated with table $tableName.
565
     *
566
     *
567
     * @param string $tableName
568
     *
569
     * @return class-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
570
     */
571
    public function getBeanClassName(string $tableName): string
572
    {
573
        if (isset($this->tableToBeanMap[$tableName])) {
574
            return $this->tableToBeanMap[$tableName];
575
        }
576
577
        $key = $this->cachePrefix.'_tableToBean_'.$tableName;
578
        $cache = $this->cache->fetch($key);
579
        if ($cache) {
580
            return $cache;
581
        }
582
583
        $className = $this->beanNamespace.'\\'.$this->namingStrategy->getBeanClassName($tableName);
584
585
        if (!class_exists($className)) {
586
            throw new TDBMInvalidArgumentException(sprintf('Could not find class "%s". Does table "%s" exist? If yes, consider regenerating the DAOs and beans.', $className, $tableName));
587
        }
588
589
        $this->tableToBeanMap[$tableName] = $className;
590
        $this->cache->save($key, $className);
591
        return $className;
592
    }
593
594
    /**
595
     * Saves $object by INSERTing or UPDAT(E)ing it in the database.
596
     *
597
     * @param AbstractTDBMObject $object
598
     *
599
     * @throws TDBMException
600
     */
601
    public function save(AbstractTDBMObject $object): void
602
    {
603
        $this->connection->beginTransaction();
604
        try {
605
            $status = $object->_getStatus();
606
607
            // Let's attach this object if it is in detached state.
608
            if ($status === TDBMObjectStateEnum::STATE_DETACHED) {
609
                $object->_attach($this);
610
                $status = $object->_getStatus();
611
            }
612
613
            if ($status === TDBMObjectStateEnum::STATE_NEW) {
614
                $dbRows = $object->_getDbRows();
615
616
                $unindexedPrimaryKeys = array();
617
618
                foreach ($dbRows as $dbRow) {
619
                    if ($dbRow->_getStatus() == TDBMObjectStateEnum::STATE_SAVING) {
620
                        throw TDBMCyclicReferenceException::createCyclicReference($dbRow->_getDbTableName(), $object);
621
                    }
622
                    $dbRow->_setStatus(TDBMObjectStateEnum::STATE_SAVING);
623
                    $tableName = $dbRow->_getDbTableName();
624
625
                    $schema = $this->tdbmSchemaAnalyzer->getSchema();
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

625
                    $schema = /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema();
Loading history...
626
                    $tableDescriptor = $schema->getTable($tableName);
627
628
                    $primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
629
630
                    $references = $dbRow->_getReferences();
631
632
                    // Let's save all references in NEW or DETACHED state (we need their primary key)
633
                    foreach ($references as $fkName => $reference) {
634
                        if ($reference !== null) {
635
                            $refStatus = $reference->_getStatus();
636
                            if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
637
                                try {
638
                                    $this->save($reference);
639
                                } catch (TDBMCyclicReferenceException $e) {
640
                                    throw TDBMCyclicReferenceException::extendCyclicReference($e, $dbRow->_getDbTableName(), $object, $fkName);
641
                                }
642
                            }
643
                        }
644
                    }
645
646
                    if (empty($unindexedPrimaryKeys)) {
647
                        $primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
648
                    } else {
649
                        // First insert, the children must have the same primary key as the parent.
650
                        $primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
651
                        $dbRow->_setPrimaryKeys($primaryKeys);
652
                    }
653
654
                    $dbRowData = $dbRow->_getDbRow();
655
656
                    // Let's see if the columns for primary key have been set before inserting.
657
                    // We assume that if one of the value of the PK has been set, the PK is set.
658
                    $isPkSet = !empty($primaryKeys);
659
660
                    /*if (!$isPkSet) {
661
                        // if there is no autoincrement and no pk set, let's go in error.
662
                        $isAutoIncrement = true;
663
664
                        foreach ($primaryKeyColumns as $pkColumnName) {
665
                            $pkColumn = $tableDescriptor->getColumn($pkColumnName);
666
                            if (!$pkColumn->getAutoincrement()) {
667
                                $isAutoIncrement = false;
668
                            }
669
                        }
670
671
                        if (!$isAutoIncrement) {
672
                            $msg = "Error! You did not set the primary key(s) for the new object of type '$tableName'. The primary key is not set to 'autoincrement' so you must either set the primary key in the object or modify the DB model to create an primary key with auto-increment.";
673
                            throw new TDBMException($msg);
674
                        }
675
676
                    }*/
677
678
                    $types = [];
679
                    $escapedDbRowData = [];
680
681
                    foreach ($dbRowData as $columnName => $value) {
682
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
683
                        $types[] = $columnDescriptor->getType();
684
                        $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
685
                    }
686
687
                    $quotedTableName = $this->connection->quoteIdentifier($tableName);
688
                    $this->connection->insert($quotedTableName, $escapedDbRowData, $types);
689
690
                    if (!$isPkSet && count($primaryKeyColumns) === 1) {
691
                        /** @var int|false $id @see OCI8Connection::lastInsertId() */
692
                        $id = $this->connection->lastInsertId();
693
694
                        if ($id === false) {
695
                            // In Oracle (if we are in 11g), the lastInsertId will fail. We try again with the column.
696
                            $sequenceName = $this->connection->getDatabasePlatform()->getIdentitySequenceName(
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Platforms\...tIdentitySequenceName() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

696
                            $sequenceName = /** @scrutinizer ignore-deprecated */ $this->connection->getDatabasePlatform()->getIdentitySequenceName(
Loading history...
697
                                $quotedTableName,
698
                                $this->connection->quoteIdentifier($primaryKeyColumns[0])
699
                            );
700
                            $id = $this->connection->lastInsertId($sequenceName);
701
                        }
702
703
                        $pkColumn = $primaryKeyColumns[0];
704
                        // lastInsertId returns a string but the column type is usually a int. Let's convert it back to the correct type.
705
                        $id = $tableDescriptor->getColumn($pkColumn)->getType()->convertToPHPValue($id, $this->getConnection()->getDatabasePlatform());
706
                        $primaryKeys[$pkColumn] = $id;
707
                    }
708
709
                    // TODO: change this to some private magic accessor in future
710
                    $dbRow->_setPrimaryKeys($primaryKeys);
711
                    $unindexedPrimaryKeys = array_values($primaryKeys);
712
713
                    // Let's remove this object from the $new_objects static table.
714
                    $this->removeFromToSaveObjectList($dbRow);
715
716
                    // Let's add this object to the list of objects in cache.
717
                    $this->_addToCache($dbRow);
718
                }
719
720
                $object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
721
            } elseif ($status === TDBMObjectStateEnum::STATE_DIRTY) {
722
                $dbRows = $object->_getDbRows();
723
724
                foreach ($dbRows as $dbRow) {
725
                    if ($dbRow->_getStatus() !== TDBMObjectStateEnum::STATE_DIRTY) {
726
                        // Not all db_rows in a bean need to be dirty when the bean itself is dirty.
727
                        continue;
728
                    }
729
                    $references = $dbRow->_getReferences();
730
731
                    // Let's save all references in NEW state (we need their primary key)
732
                    foreach ($references as $fkName => $reference) {
733
                        if ($reference !== null && $reference->_getStatus() === TDBMObjectStateEnum::STATE_NEW) {
734
                            $this->save($reference);
735
                        }
736
                    }
737
738
                    $tableName = $dbRow->_getDbTableName();
739
                    $dbRowData = $dbRow->_getUpdatedDbRow();
740
741
                    $schema = $this->tdbmSchemaAnalyzer->getSchema();
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

741
                    $schema = /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema();
Loading history...
742
                    $tableDescriptor = $schema->getTable($tableName);
743
744
                    $primaryKeys = $dbRow->_getPrimaryKeys();
745
746
                    $types = [];
747
                    $escapedDbRowData = [];
748
                    $escapedPrimaryKeys = [];
749
750
                    foreach ($dbRowData as $columnName => $value) {
751
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
752
                        $types[] = $columnDescriptor->getType();
753
                        $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
754
                    }
755
                    foreach ($primaryKeys as $columnName => $value) {
756
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
757
                        $types[] = $columnDescriptor->getType();
758
                        $escapedPrimaryKeys[$this->connection->quoteIdentifier($columnName)] = $value;
759
                    }
760
761
                    $this->connection->update($this->connection->quoteIdentifier($tableName), $escapedDbRowData, $escapedPrimaryKeys, $types);
762
763
                    // Let's check if the primary key has been updated...
764
                    $needsUpdatePk = false;
765
                    foreach ($primaryKeys as $column => $value) {
766
                        if (isset($dbRowData[$column]) && $dbRowData[$column] != $value) {
767
                            $needsUpdatePk = true;
768
                            break;
769
                        }
770
                    }
771
                    if ($needsUpdatePk) {
772
                        $this->objectStorage->remove($tableName, $this->getObjectHash($primaryKeys));
773
                        $newPrimaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
774
                        $dbRow->_setPrimaryKeys($newPrimaryKeys);
775
                        $this->objectStorage->set($tableName, $this->getObjectHash($primaryKeys), $dbRow);
776
                    }
777
778
                    // Let's remove this object from the list of objects to save.
779
                    $this->removeFromToSaveObjectList($dbRow);
780
                }
781
782
                $object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
783
            } elseif ($status === TDBMObjectStateEnum::STATE_DELETED) {
784
                throw new TDBMInvalidOperationException('This object has been deleted. It cannot be saved.');
785
            }
786
787
            // Finally, let's save all the many to many relationships to this bean.
788
            $this->persistManyToManyRelationships($object);
789
            $this->connection->commit();
790
        } catch (\Throwable $t) {
791
            $this->connection->rollBack();
792
            throw $t;
793
        }
794
    }
795
796
    private function persistManyToManyRelationships(AbstractTDBMObject $object): void
797
    {
798
        foreach ($object->_getCachedRelationships() as $pivotTableName => $storage) {
799
            $tableDescriptor = $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

799
            $tableDescriptor = /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
Loading history...
800
            list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $object);
801
802
            $toRemoveFromStorage = [];
803
804
            foreach ($storage as $remoteBean) {
805
                /* @var $remoteBean AbstractTDBMObject */
806
                $statusArr = $storage[$remoteBean];
807
                $status = $statusArr['status'];
808
                $reverse = $statusArr['reverse'];
809
                if ($reverse) {
810
                    continue;
811
                }
812
813
                if ($status === 'new') {
814
                    $remoteBeanStatus = $remoteBean->_getStatus();
815
                    if ($remoteBeanStatus === TDBMObjectStateEnum::STATE_NEW || $remoteBeanStatus === TDBMObjectStateEnum::STATE_DETACHED) {
816
                        // Let's save remote bean if needed.
817
                        $this->save($remoteBean);
818
                    }
819
820
                    ['filters' => $filters, 'types' => $types] = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk, $tableDescriptor);
821
822
                    $this->connection->insert($this->connection->quoteIdentifier($pivotTableName), $filters, $types);
823
824
                    // Finally, let's mark relationships as saved.
825
                    $statusArr['status'] = 'loaded';
826
                    $storage[$remoteBean] = $statusArr;
827
                    $remoteStorage = $remoteBean->_getCachedRelationships()[$pivotTableName];
828
                    $remoteStatusArr = $remoteStorage[$object];
829
                    $remoteStatusArr['status'] = 'loaded';
830
                    $remoteStorage[$object] = $remoteStatusArr;
831
                } elseif ($status === 'delete') {
832
                    ['filters' => $filters, 'types' => $types] = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk, $tableDescriptor);
833
834
                    $this->connection->delete($this->connection->quoteIdentifier($pivotTableName), $filters, $types);
835
836
                    // Finally, let's remove relationships completely from bean.
837
                    $toRemoveFromStorage[] = $remoteBean;
838
839
                    $remoteBean->_getCachedRelationships()[$pivotTableName]->detach($object);
840
                }
841
            }
842
843
            // Note: due to https://bugs.php.net/bug.php?id=65629, we cannot delete an element inside a foreach loop on a SplStorageObject.
844
            // Therefore, we cache elements in the $toRemoveFromStorage to remove them at a later stage.
845
            foreach ($toRemoveFromStorage as $remoteBean) {
846
                $storage->detach($remoteBean);
847
            }
848
        }
849
    }
850
851
    /**
852
     * @return mixed[] An array with 2 keys: "filters" and "types"
853
     */
854
    private function getPivotFilters(AbstractTDBMObject $localBean, AbstractTDBMObject $remoteBean, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk, Table $tableDescriptor): array
855
    {
856
        $localBeanPk = $this->getPrimaryKeyValues($localBean);
857
        $remoteBeanPk = $this->getPrimaryKeyValues($remoteBean);
858
        $localColumns = $localFk->getUnquotedLocalColumns();
859
        $remoteColumns = $remoteFk->getUnquotedLocalColumns();
860
861
        $localFilters = array_combine($localColumns, $localBeanPk);
862
        $remoteFilters = array_combine($remoteColumns, $remoteBeanPk);
863
864
        $filters = array_merge($localFilters, $remoteFilters);
865
866
        $types = [];
867
        $escapedFilters = [];
868
869
        foreach ($filters as $columnName => $value) {
870
            $columnDescriptor = $tableDescriptor->getColumn((string) $columnName);
871
            $types[] = $columnDescriptor->getType();
872
            $escapedFilters[$this->connection->quoteIdentifier((string) $columnName)] = $value;
873
        }
874
        return ['filters' => $escapedFilters, 'types' => $types];
875
    }
876
877
    /**
878
     * Returns the "values" of the primary key.
879
     * This returns the primary key from the $primaryKey attribute, not the one stored in the columns.
880
     *
881
     * @param AbstractTDBMObject $bean
882
     *
883
     * @return mixed[] numerically indexed array of values
884
     */
885
    private function getPrimaryKeyValues(AbstractTDBMObject $bean): array
886
    {
887
        $dbRows = $bean->_getDbRows();
888
        $dbRow = reset($dbRows);
889
        if ($dbRow === false) {
890
            throw new \RuntimeException('Unexpected error: empty dbRow'); // @codeCoverageIgnore
891
        }
892
893
        return array_values($dbRow->_getPrimaryKeys());
894
    }
895
896
    /**
897
     * Returns a unique hash used to store the object based on its primary key.
898
     * If the array contains only one value, then the value is returned.
899
     * Otherwise, a hash representing the array is returned.
900
     *
901
     * @param mixed[] $primaryKeys An array of columns => values forming the primary key
902
     *
903
     * @return string|int
904
     */
905
    public function getObjectHash(array $primaryKeys)
906
    {
907
        if (count($primaryKeys) === 1) {
908
            return reset($primaryKeys);
909
        } else {
910
            ksort($primaryKeys);
911
912
            $pkJson = json_encode($primaryKeys);
913
            if ($pkJson === false) {
914
                throw new TDBMException('Unexepected error: unable to encode primary keys'); // @codeCoverageIgnore
915
            }
916
            return md5($pkJson);
917
        }
918
    }
919
920
    /**
921
     * Returns an array of primary keys from the object.
922
     * The primary keys are extracted from the object columns and not from the primary keys stored in the
923
     * $primaryKeys variable of the object.
924
     *
925
     * @param DbRow $dbRow
926
     *
927
     * @return mixed[] Returns an array of column => value
928
     */
929
    public function getPrimaryKeysForObjectFromDbRow(DbRow $dbRow): array
930
    {
931
        $table = $dbRow->_getDbTableName();
932
933
        $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
934
        $values = array();
935
        $dbRowValues = $dbRow->_getDbRow();
936
        foreach ($primaryKeyColumns as $column) {
937
            if (isset($dbRowValues[$column])) {
938
                $values[$column] = $dbRowValues[$column];
939
            }
940
        }
941
942
        return $values;
943
    }
944
945
    /**
946
     * Returns an array of primary keys for the given row.
947
     * The primary keys are extracted from the object columns.
948
     *
949
     * @param string $table
950
     * @param mixed[] $columns
951
     *
952
     * @return mixed[] Returns an array of column => value
953
     */
954
    public function _getPrimaryKeysFromObjectData(string $table, array $columns): array
955
    {
956
        $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
957
        $values = [];
958
        foreach ($primaryKeyColumns as $column) {
959
            if (isset($columns[$column])) {
960
                $values[$column] = $columns[$column];
961
            }
962
        }
963
964
        return $values;
965
    }
966
967
    /**
968
     * Attaches $object to this TDBMService.
969
     * The $object must be in DETACHED state and will pass in NEW state.
970
     *
971
     * @param AbstractTDBMObject $object
972
     *
973
     * @throws TDBMInvalidOperationException
974
     */
975
    public function attach(AbstractTDBMObject $object): void
976
    {
977
        $object->_attach($this);
978
    }
979
980
    /**
981
     * Returns an associative array (column => value) for the primary keys from the table name and an
982
     * indexed array of primary key values.
983
     *
984
     * @param string $tableName
985
     * @param mixed[] $indexedPrimaryKeys
986
     * @return mixed[]
987
     */
988
    public function _getPrimaryKeysFromIndexedPrimaryKeys(string $tableName, array $indexedPrimaryKeys): array
989
    {
990
        $primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
991
992
        if (count($primaryKeyColumns) !== count($indexedPrimaryKeys)) {
993
            throw new TDBMException(sprintf('Wrong number of columns passed for primary key. Expected %s columns for table "%s",
994
			got %s instead.', count($primaryKeyColumns), $tableName, count($indexedPrimaryKeys)));
995
        }
996
997
        return array_combine($primaryKeyColumns, $indexedPrimaryKeys);
998
    }
999
1000
    /**
1001
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
1002
     * Tables must be in a single line of inheritance. The method will find missing tables.
1003
     *
1004
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1005
     * we must be able to find all other tables.
1006
     *
1007
     * @param string[] $tables
1008
     *
1009
     * @return string[]
1010
     */
1011
    public function _getLinkBetweenInheritedTables(array $tables): array
1012
    {
1013
        sort($tables);
1014
1015
        return $this->fromCache(
1016
            $this->cachePrefix.'_linkbetweeninheritedtables_'.implode('__split__', $tables),
1017
            function () use ($tables) {
1018
                return $this->_getLinkBetweenInheritedTablesWithoutCache($tables);
1019
            }
1020
        );
1021
    }
1022
1023
    /**
1024
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
1025
     * Tables must be in a single line of inheritance. The method will find missing tables.
1026
     *
1027
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1028
     * we must be able to find all other tables.
1029
     *
1030
     * @param string[] $tables
1031
     *
1032
     * @return string[]
1033
     */
1034
    private function _getLinkBetweenInheritedTablesWithoutCache(array $tables): array
1035
    {
1036
        $schemaAnalyzer = $this->schemaAnalyzer;
1037
1038
        foreach ($tables as $currentTable) {
1039
            $allParents = [$currentTable];
1040
            while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1041
                $currentTable = $currentFk->getForeignTableName();
1042
                $allParents[] = $currentTable;
1043
            }
1044
1045
            // Now, does the $allParents contain all the tables we want?
1046
            $notFoundTables = array_diff($tables, $allParents);
1047
            if (empty($notFoundTables)) {
1048
                // We have a winner!
1049
                return $allParents;
1050
            }
1051
        }
1052
1053
        throw TDBMInheritanceException::create($tables);
1054
    }
1055
1056
    /**
1057
     * Returns the list of tables related to this table (via a parent or child inheritance relationship).
1058
     *
1059
     * @param string $table
1060
     *
1061
     * @return string[]
1062
     */
1063
    public function _getRelatedTablesByInheritance(string $table): array
1064
    {
1065
        return $this->fromCache($this->cachePrefix.'_relatedtables_'.$table, function () use ($table) {
1066
            return $this->_getRelatedTablesByInheritanceWithoutCache($table);
1067
        });
1068
    }
1069
1070
    /**
1071
     * Returns the list of tables related to this table (via a parent or child inheritance relationship).
1072
     *
1073
     * @param string $table
1074
     *
1075
     * @return string[]
1076
     */
1077
    private function _getRelatedTablesByInheritanceWithoutCache(string $table): array
1078
    {
1079
        $schemaAnalyzer = $this->schemaAnalyzer;
1080
1081
        // Let's scan the parent tables
1082
        $currentTable = $table;
1083
1084
        $parentTables = [];
1085
1086
        // Get parent relationship
1087
        while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1088
            $currentTable = $currentFk->getForeignTableName();
1089
            $parentTables[] = $currentTable;
1090
        }
1091
1092
        // Let's recurse in children
1093
        $childrenTables = $this->exploreChildrenTablesRelationships($schemaAnalyzer, $table);
1094
1095
        return array_merge(array_reverse($parentTables), $childrenTables);
1096
    }
1097
1098
    /**
1099
     * Explore all the children and descendant of $table and returns ForeignKeyConstraints on those.
1100
     *
1101
     * @return string[]
1102
     */
1103
    private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyzer, string $table): array
1104
    {
1105
        $tables = [$table];
1106
        $keys = $schemaAnalyzer->getChildrenRelationships($table);
1107
1108
        foreach ($keys as $key) {
1109
            $tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, $key->getLocalTableName()));
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Schema\For...nt::getLocalTableName() has been deprecated: Use the table that contains the foreign key as part of its {@see Table::$_fkConstraints} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1109
            $tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, /** @scrutinizer ignore-deprecated */ $key->getLocalTableName()));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1110
        }
1111
1112
        return $tables;
1113
    }
1114
1115
    /**
1116
     * Returns a `ResultIterator` object representing filtered records of "$mainTable" .
1117
     *
1118
     * The findObjects method should be the most used query method in TDBM if you want to query the database for objects.
1119
     * (Note: if you want to query the database for an object by its primary key, use the findObjectByPk method).
1120
     *
1121
     * The findObjects method takes in parameter:
1122
     * 	- mainTable: the kind of bean you want to retrieve. In TDBM, a bean matches a database row, so the
1123
     * 			`$mainTable` parameter should be the name of an existing table in database.
1124
     *  - filter: The filter is a filter bag. It is what you use to filter your request (the WHERE part in SQL).
1125
     *          It can be a string (SQL Where clause), or even a bean or an associative array (key = column to filter, value = value to find)
1126
     *  - parameters: The parameters used in the filter. If you pass a SQL string as a filter, be sure to avoid
1127
     *          concatenating parameters in the string (this leads to SQL injection and also to poor caching performance).
1128
     *          Instead, please consider passing parameters (see documentation for more details).
1129
     *  - additionalTablesFetch: An array of SQL tables names. The beans related to those tables will be fetched along
1130
     *          the main table. This is useful to avoid hitting the database with numerous subqueries.
1131
     *  - mode: The fetch mode of the result. See `setFetchMode()` method for more details.
1132
     *
1133
     * The `findObjects` method will return a `ResultIterator`. A `ResultIterator` is an object that behaves as an array
1134
     * (in ARRAY mode) at least. It can be iterated using a `foreach` loop.
1135
     *
1136
     * Finally, if filter_bag is null, the whole table is returned.
1137
     *
1138
     * @param string                       $mainTable             The name of the table queried
1139
     * @param string|array|null            $filter                The SQL filters to apply to the query (the WHERE part). Columns from tables different from $mainTable must be prefixed by the table name (in the form: table.column)
1140
     * @param mixed[]                      $parameters
1141
     * @param string|UncheckedOrderBy|null $orderString           The ORDER BY part of the query. Columns from tables different from $mainTable must be prefixed by the table name (in the form: table.column)
1142
     * @param string[]                     $additionalTablesFetch
1143
     * @param int|null                     $mode
1144
     * @param class-string|null            $className             Optional: The name of the class to instantiate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
1145
     * @param class-string                 $resultIteratorClass   The name of the resultIterator class to return
1146
     *
1147
     * @return ResultIterator An object representing an array of results
1148
     *
1149
     * @throws TDBMException
1150
     */
1151
    public function findObjects(string $mainTable, $filter, array $parameters, $orderString, array $additionalTablesFetch, ?int $mode, ?string $className, string $resultIteratorClass): ResultIterator
1152
    {
1153
        if (!is_a($resultIteratorClass, ResultIterator::class, true)) {
1154
            throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.');
1155
        }
1156
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1157
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1158
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1159
        }
1160
1161
        $mode = $mode ?: $this->mode;
1162
1163
        // We quote in MySQL because MagicJoin requires MySQL style quotes
1164
        $mysqlPlatform = new MySqlPlatform();
1165
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter, $mysqlPlatform);
1166
1167
        $parameters = array_merge($parameters, $additionalParameters);
1168
1169
        $queryFactory = new FindObjectsQueryFactory($mainTable, $additionalTablesFetch, $filterString, $orderString, $this, $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer, $this->cache);
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1169
        $queryFactory = new FindObjectsQueryFactory($mainTable, $additionalTablesFetch, $filterString, $orderString, $this, /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer, $this->cache);
Loading history...
1170
1171
        return $resultIteratorClass::createResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1172
    }
1173
1174
    /**
1175
     * @param string                       $mainTable   The name of the table queried
1176
     * @param string                       $from        The from sql statement
1177
     * @param string|array|null            $filter      The SQL filters to apply to the query (the WHERE part). All columns must be prefixed by the table name (in the form: table.column)
1178
     * @param mixed[]                      $parameters
1179
     * @param string|UncheckedOrderBy|null $orderString The ORDER BY part of the query. All columns must be prefixed by the table name (in the form: table.column)
1180
     * @param int                          $mode
1181
     * @param class-string|null            $className   Optional: The name of the class to instantiate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
1182
     * @param class-string                 $resultIteratorClass   The name of the resultIterator class to return
1183
     *
1184
     * @return ResultIterator An object representing an array of results
1185
     *
1186
     * @throws TDBMException
1187
     */
1188
    public function findObjectsFromSql(string $mainTable, string $from, $filter, array $parameters, $orderString, ?int $mode, ?string $className, string $resultIteratorClass): ResultIterator
1189
    {
1190
        if (!is_a($resultIteratorClass, ResultIterator::class, true)) {
1191
            throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.');
1192
        }
1193
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1194
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1195
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1196
        }
1197
1198
        $mode = $mode ?: $this->mode;
1199
1200
        // We quote in MySQL because MagicJoin requires MySQL style quotes
1201
        $mysqlPlatform = new MySqlPlatform();
1202
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter, $mysqlPlatform);
1203
1204
        $parameters = array_merge($parameters, $additionalParameters);
1205
1206
        $queryFactory = new FindObjectsFromSqlQueryFactory($mainTable, $from, $filterString, $orderString, $this, $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer, $this->schemaAnalyzer, $this->cache, $this->cachePrefix);
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1206
        $queryFactory = new FindObjectsFromSqlQueryFactory($mainTable, $from, $filterString, $orderString, $this, /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer, $this->schemaAnalyzer, $this->cache, $this->cachePrefix);
Loading history...
1207
1208
        return $resultIteratorClass::createResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1209
    }
1210
1211
    /**
1212
     * @param string $table
1213
     * @param mixed[] $primaryKeys
1214
     * @param string[] $additionalTablesFetch
1215
     * @param bool $lazy Whether to perform lazy loading on this object or not
1216
     * @phpstan-param  class-string $className
1217
     *
1218
     * @return AbstractTDBMObject
1219
     *
1220
     * @throws TDBMException
1221
     */
1222
    public function findObjectByPk(string $table, array $primaryKeys, array $additionalTablesFetch, bool $lazy, string $className, string $resultIteratorClass): AbstractTDBMObject
1223
    {
1224
        assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'));
1225
        assert(is_a($className, AbstractTDBMObject::class, true), new TDBMInvalidArgumentException('$className should be a `'. AbstractTDBMObject::class. '`. `' . $className . '` provided.'));
1226
        $primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys);
1227
        $hash = $this->getObjectHash($primaryKeys);
1228
1229
        $dbRow = $this->objectStorage->get($table, $hash);
1230
        if ($dbRow !== null) {
1231
            $bean = $dbRow->getTDBMObject();
1232
            if (!is_a($bean, $className)) {
1233
                throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'");
1234
            }
1235
1236
            return $bean;
1237
        }
1238
1239
        // Are we performing lazy fetching?
1240
        if ($lazy === true) {
1241
            // Can we perform lazy fetching?
1242
            $tables = $this->_getRelatedTablesByInheritance($table);
1243
            // Only allowed if no inheritance.
1244
            if (count($tables) === 1) {
1245
                // Let's construct the bean
1246
                if (!isset($this->reflectionClassCache[$className])) {
1247
                    $this->reflectionClassCache[$className] = new \ReflectionClass($className);
1248
                }
1249
                // Let's bypass the constructor when creating the bean!
1250
                /** @var AbstractTDBMObject $bean */
1251
                $bean = $this->reflectionClassCache[$className]->newInstanceWithoutConstructor();
1252
                $bean->_constructLazy($table, $primaryKeys, $this);
1253
1254
                return $bean;
1255
            }
1256
        }
1257
1258
        // Did not find the object in cache? Let's query it!
1259
        try {
1260
            return $this->findObjectOrFail($table, $primaryKeys, [], $additionalTablesFetch, $className, $resultIteratorClass);
1261
        } catch (NoBeanFoundException $exception) {
1262
            throw NoBeanFoundException::missPrimaryKeyRecord($table, $primaryKeys, $this->getBeanClassName($table), $exception);
1263
        }
1264
    }
1265
1266
    /**
1267
     * Returns a unique bean (or null) according to the filters passed in parameter.
1268
     *
1269
     * @param string            $mainTable             The name of the table queried
1270
     * @param string|array|null $filter                The SQL filters to apply to the query (the WHERE part). All columns must be prefixed by the table name (in the form: table.column)
1271
     * @param mixed[]           $parameters
1272
     * @param string[]          $additionalTablesFetch
1273
     * @param class-string      $className             The name of the class to instantiate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
1274
     * @param class-string      $resultIteratorClass
1275
     *
1276
     * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters
1277
     *
1278
     * @throws TDBMException
1279
     */
1280
    public function findObject(string $mainTable, $filter, array $parameters, array $additionalTablesFetch, string $className, string $resultIteratorClass): ?AbstractTDBMObject
1281
    {
1282
        assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'));
1283
        $objects = $this->findObjects($mainTable, $filter, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className, $resultIteratorClass);
1284
        return $this->getAtMostOneObjectOrFail($objects, $mainTable, $filter, $parameters);
1285
    }
1286
1287
    /**
1288
     * @param string|array|null $filter
1289
     * @param mixed[]           $parameters
1290
     */
1291
    private function getAtMostOneObjectOrFail(ResultIterator $objects, string $mainTable, $filter, array $parameters): ?AbstractTDBMObject
1292
    {
1293
        $page = $objects->take(0, 2);
1294
1295
1296
        $pageArr = $page->toArray();
1297
        // Optimisation: the $page->count() query can trigger an additional SQL query in platforms other than MySQL.
1298
        // We try to avoid calling at by fetching all 2 columns instead.
1299
        $count = count($pageArr);
1300
1301
        if ($count > 1) {
1302
            $additionalErrorInfos = '';
1303
            if (is_string($filter) && !empty($parameters)) {
1304
                $additionalErrorInfos = ' for filter "' . $filter.'"';
1305
                foreach ($parameters as $fieldName => $parameter) {
1306
                    if (is_array($parameter)) {
1307
                        $value = '(' . implode(',', $parameter) . ')';
1308
                    } else {
1309
                        $value = $parameter;
1310
                    }
1311
                    $additionalErrorInfos = str_replace(':' . $fieldName, var_export($value, true), $additionalErrorInfos);
1312
                }
1313
            }
1314
            $additionalErrorInfos .= '.';
1315
            throw new DuplicateRowException("Error while querying an object in table '$mainTable': More than 1 row have been returned, but we should have received at most one" . $additionalErrorInfos);
1316
        } elseif ($count === 0) {
1317
            return null;
1318
        }
1319
1320
        return $pageArr[0];
1321
    }
1322
1323
    /**
1324
     * Returns a unique bean (or null) according to the filters passed in parameter.
1325
     *
1326
     * @param string            $mainTable  The name of the table queried
1327
     * @param string            $from       The from sql statement
1328
     * @param string|array|null $filter     The SQL filters to apply to the query (the WHERE part). All columns must be prefixed by the table name (in the form: table.column)
1329
     * @param mixed[]           $parameters
1330
     * @param class-string|null $className  Optional: The name of the class to instantiate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
1331
     * @param class-string      $resultIteratorClass
1332
     *
1333
     * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters
1334
     *
1335
     * @throws TDBMException
1336
     */
1337
    public function findObjectFromSql(string $mainTable, string $from, $filter, array $parameters, ?string $className, string $resultIteratorClass): ?AbstractTDBMObject
1338
    {
1339
        assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'));
1340
        $objects = $this->findObjectsFromSql($mainTable, $from, $filter, $parameters, null, self::MODE_ARRAY, $className, $resultIteratorClass);
1341
        return $this->getAtMostOneObjectOrFail($objects, $mainTable, $filter, $parameters);
1342
    }
1343
1344
    /**
1345
     * @param string $mainTable
1346
     * @param string $sql
1347
     * @param mixed[] $parameters
1348
     * @param int|null $mode
1349
     * @param class-string|null $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
1350
     * @param string $sqlCount
1351
     * @param class-string $resultIteratorClass The name of the resultIterator class to return
1352
     *
1353
     * @return ResultIterator
1354
     *
1355
     * @throws TDBMException
1356
     */
1357
    public function findObjectsFromRawSql(string $mainTable, string $sql, array $parameters, ?int $mode, ?string $className, ?string $sqlCount, string $resultIteratorClass): ResultIterator
1358
    {
1359
        if (!is_a($resultIteratorClass, ResultIterator::class, true)) {
1360
            throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.');
1361
        }
1362
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1363
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1364
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1365
        }
1366
1367
        $mode = $mode ?: $this->mode;
1368
1369
        $queryFactory = new FindObjectsFromRawSqlQueryFactory($this, $this->tdbmSchemaAnalyzer->getSchema(), $mainTable, $sql, $sqlCount);
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1369
        $queryFactory = new FindObjectsFromRawSqlQueryFactory($this, /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema(), $mainTable, $sql, $sqlCount);
Loading history...
1370
1371
        return $resultIteratorClass::createResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1372
    }
1373
1374
    /**
1375
     * Returns a unique bean according to the filters passed in parameter.
1376
     * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1377
     *
1378
     * @param string            $mainTable             The name of the table queried
1379
     * @param string|array|null $filter                The SQL filters to apply to the query (the WHERE part). All columns must be prefixed by the table name (in the form: table.column)
1380
     * @param mixed[]           $parameters
1381
     * @param string[]          $additionalTablesFetch
1382
     * @param class-string      $className             The name of the class to instantiate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
1383
     * @param class-string      $resultIteratorClass
1384
     *
1385
     * @return AbstractTDBMObject The object we want
1386
     *
1387
     * @throws TDBMException
1388
     */
1389
    public function findObjectOrFail(string $mainTable, $filter, array $parameters, array $additionalTablesFetch, string $className, string $resultIteratorClass): AbstractTDBMObject
1390
    {
1391
        assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'));
1392
        $bean = $this->findObject($mainTable, $filter, $parameters, $additionalTablesFetch, $className, $resultIteratorClass);
1393
        if ($bean === null) {
1394
            throw NoBeanFoundException::missFilterRecord($mainTable);
1395
        }
1396
1397
        return $bean;
1398
    }
1399
1400
    /**
1401
     * @param array<string, array> $beanData An array of data: array<table, array<column, value>>
1402
     *
1403
     * @return mixed[] an array with first item = class name, second item = table name and third item = list of tables needed
1404
     *
1405
     * @throws TDBMInheritanceException
1406
     */
1407
    public function _getClassNameFromBeanData(array $beanData): array
1408
    {
1409
        if (count($beanData) === 1) {
1410
            $tableName = (string) array_keys($beanData)[0];
1411
            $allTables = [$tableName];
1412
        } else {
1413
            $tables = [];
1414
            foreach ($beanData as $table => $row) {
1415
                $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1416
                $pkSet = false;
1417
                foreach ($primaryKeyColumns as $columnName) {
1418
                    if ($row[$columnName] !== null) {
1419
                        $pkSet = true;
1420
                        break;
1421
                    }
1422
                }
1423
                if ($pkSet) {
1424
                    $tables[] = $table;
1425
                }
1426
            }
1427
1428
            // $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1429
            try {
1430
                $allTables = $this->_getLinkBetweenInheritedTables($tables);
1431
            } catch (TDBMInheritanceException $e) {
1432
                throw TDBMInheritanceException::extendException($e, $this, $beanData);
1433
            }
1434
            $tableName = $allTables[0];
1435
        }
1436
1437
        // Only one table in this bean. Life is sweat, let's look at its type:
1438
        try {
1439
            $className = $this->getBeanClassName($tableName);
1440
        } catch (TDBMInvalidArgumentException $e) {
1441
            $className = 'TheCodingMachine\\TDBM\\TDBMObject';
1442
        }
1443
1444
        return [$className, $tableName, $allTables];
1445
    }
1446
1447
    /**
1448
     * Returns an item from cache or computes it using $closure and puts it in cache.
1449
     *
1450
     * @param string   $key
1451
     * @param callable $closure
1452
     *
1453
     * @return mixed
1454
     */
1455
    private function fromCache(string $key, callable $closure)
1456
    {
1457
        $item = $this->cache->fetch($key);
1458
        if ($item === false) {
1459
            $item = $closure();
1460
            $result = $this->cache->save($key, $item);
1461
1462
            if ($result === false) {
1463
                throw new TDBMException('An error occured while storing an object in cache. Please check that: 1. your cache is not full, 2. if you are using APC in CLI mode, that you have the "apc.enable_cli=On" setting added to your php.ini file.');
1464
            }
1465
        }
1466
1467
        return $item;
1468
    }
1469
1470
    /**
1471
     * @return AbstractTDBMObject[]|ResultIterator
1472
     */
1473
    public function _getRelatedBeans(ManyToManyRelationshipPathDescriptor $pathDescriptor, AbstractTDBMObject $bean): ResultIterator
1474
    {
1475
        // Magic Query expect MySQL syntax for quotes
1476
        $platform = new MySqlPlatform();
1477
1478
        return $this->findObjectsFromSql(
1479
            $pathDescriptor->getTargetName(),
1480
            $pathDescriptor->getPivotFrom($platform),
1481
            $pathDescriptor->getPivotWhere($platform),
1482
            $pathDescriptor->getPivotParams($this->getPrimaryKeyValues($bean)),
1483
            null,
1484
            null,
1485
            null,
1486
            $pathDescriptor->getResultIteratorClass()
1487
        );
1488
    }
1489
1490
    /**
1491
     * @param string $pivotTableName
1492
     * @param AbstractTDBMObject $bean The LOCAL bean
1493
     *
1494
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean
1495
     *
1496
     * @throws TDBMException
1497
     */
1498
    private function getPivotTableForeignKeys(string $pivotTableName, AbstractTDBMObject $bean): array
1499
    {
1500
        $fks = array_values($this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1500
        $fks = array_values(/** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
Loading history...
1501
        $table1 = $fks[0]->getForeignTableName();
1502
        $table2 = $fks[1]->getForeignTableName();
1503
1504
        $beanTables = array_map(function (DbRow $dbRow) {
1505
            return $dbRow->_getDbTableName();
1506
        }, $bean->_getDbRows());
1507
1508
        if (in_array($table1, $beanTables)) {
1509
            return [$fks[0], $fks[1]];
1510
        } elseif (in_array($table2, $beanTables)) {
1511
            return [$fks[1], $fks[0]];
1512
        } else {
1513
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2} for pivot table {$pivotTableName}");
1514
        }
1515
    }
1516
1517
    /**
1518
     * Array of types for tables.
1519
     * Key: table name
1520
     * Value: array of types indexed by column.
1521
     *
1522
     * @var array[]
1523
     */
1524
    private $typesForTable = [];
1525
1526
    /**
1527
     * @internal
1528
     *
1529
     * @param string $tableName
1530
     *
1531
     * @return Type[]
1532
     */
1533
    public function _getColumnTypesForTable(string $tableName): array
1534
    {
1535
        if (!isset($this->typesForTable[$tableName])) {
1536
            $columns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getColumns();
0 ignored issues
show
Deprecated Code introduced by
The function TheCodingMachine\TDBM\TD...maAnalyzer::getSchema() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1536
            $columns = /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getColumns();
Loading history...
1537
            foreach ($columns as $column) {
1538
                $this->typesForTable[$tableName][$column->getName()] = $column->getType();
1539
            }
1540
        }
1541
1542
        return $this->typesForTable[$tableName];
1543
    }
1544
1545
    /**
1546
     * Sets the minimum log level.
1547
     * $level must be one of Psr\Log\LogLevel::xxx.
1548
     *
1549
     * Defaults to LogLevel::WARNING
1550
     *
1551
     * @param string $level
1552
     */
1553
    public function setLogLevel(string $level): void
1554
    {
1555
        $this->logger = new LevelFilter($this->rootLogger, $level);
1556
    }
1557
}
1558