TDBMService::save()   F
last analyzed

Complexity

Conditions 29
Paths 2853

Size

Total Lines 192
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 29
eloc 101
c 3
b 0
f 0
nc 2853
nop 1
dl 0
loc 192
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

630
                    $schema = /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema();
Loading history...
631
                    $tableDescriptor = $schema->getTable($tableName);
632
633
                    $primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
634
635
                    $references = $dbRow->_getReferences();
636
637
                    // Let's save all references in NEW or DETACHED state (we need their primary key)
638
                    foreach ($references as $fkName => $reference) {
639
                        if ($reference !== null) {
640
                            $refStatus = $reference->_getStatus();
641
                            if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
642
                                try {
643
                                    $this->save($reference);
644
                                } catch (TDBMCyclicReferenceException $e) {
645
                                    throw TDBMCyclicReferenceException::extendCyclicReference($e, $dbRow->_getDbTableName(), $object, $fkName);
646
                                }
647
                            }
648
                        }
649
                    }
650
651
                    if (empty($unindexedPrimaryKeys)) {
652
                        $primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
653
                    } else {
654
                        // First insert, the children must have the same primary key as the parent.
655
                        $primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
656
                        $dbRow->_setPrimaryKeys($primaryKeys);
657
                    }
658
659
                    $dbRowData = $dbRow->_getDbRow();
660
661
                    // Let's see if the columns for primary key have been set before inserting.
662
                    // We assume that if one of the value of the PK has been set, the PK is set.
663
                    $isPkSet = !empty($primaryKeys);
664
665
                    /*if (!$isPkSet) {
666
                        // if there is no autoincrement and no pk set, let's go in error.
667
                        $isAutoIncrement = true;
668
669
                        foreach ($primaryKeyColumns as $pkColumnName) {
670
                            $pkColumn = $tableDescriptor->getColumn($pkColumnName);
671
                            if (!$pkColumn->getAutoincrement()) {
672
                                $isAutoIncrement = false;
673
                            }
674
                        }
675
676
                        if (!$isAutoIncrement) {
677
                            $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.";
678
                            throw new TDBMException($msg);
679
                        }
680
681
                    }*/
682
683
                    $types = [];
684
                    $escapedDbRowData = [];
685
686
                    foreach ($dbRowData as $columnName => $value) {
687
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
688
                        $types[] = $columnDescriptor->getType();
689
                        $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
690
                    }
691
692
                    $quotedTableName = $this->connection->quoteIdentifier($tableName);
693
                    $this->connection->insert($quotedTableName, $escapedDbRowData, $types);
694
695
                    if (!$isPkSet && count($primaryKeyColumns) === 1) {
696
                        /** @var int|false $id @see OCI8Connection::lastInsertId() */
697
                        $id = $this->connection->lastInsertId();
698
699
                        if ($id === false) {
700
                            // In Oracle (if we are in 11g), the lastInsertId will fail. We try again with the column.
701
                            $sequenceName = $this->connection->getDatabasePlatform()->getIdentitySequenceName(
702
                                $quotedTableName,
703
                                $this->connection->quoteIdentifier($primaryKeyColumns[0])
704
                            );
705
                            $id = $this->connection->lastInsertId($sequenceName);
706
                        }
707
708
                        $pkColumn = $primaryKeyColumns[0];
709
                        // lastInsertId returns a string but the column type is usually a int. Let's convert it back to the correct type.
710
                        $id = $tableDescriptor->getColumn($pkColumn)->getType()->convertToPHPValue($id, $this->getConnection()->getDatabasePlatform());
711
                        $primaryKeys[$pkColumn] = $id;
712
                    }
713
714
                    // TODO: change this to some private magic accessor in future
715
                    $dbRow->_setPrimaryKeys($primaryKeys);
716
                    $unindexedPrimaryKeys = array_values($primaryKeys);
717
718
                    // Let's remove this object from the $new_objects static table.
719
                    $this->removeFromToSaveObjectList($dbRow);
720
721
                    // Let's add this object to the list of objects in cache.
722
                    $this->_addToCache($dbRow);
723
                }
724
725
                $object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
726
            } elseif ($status === TDBMObjectStateEnum::STATE_DIRTY) {
727
                $dbRows = $object->_getDbRows();
728
729
                foreach ($dbRows as $dbRow) {
730
                    if ($dbRow->_getStatus() !== TDBMObjectStateEnum::STATE_DIRTY) {
731
                        // Not all db_rows in a bean need to be dirty when the bean itself is dirty.
732
                        continue;
733
                    }
734
                    $references = $dbRow->_getReferences();
735
736
                    // Let's save all references in NEW state (we need their primary key)
737
                    foreach ($references as $fkName => $reference) {
738
                        if ($reference !== null && $reference->_getStatus() === TDBMObjectStateEnum::STATE_NEW) {
739
                            $this->save($reference);
740
                        }
741
                    }
742
743
                    $tableName = $dbRow->_getDbTableName();
744
                    $dbRowData = $dbRow->_getUpdatedDbRow();
745
746
                    $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

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

804
            $tableDescriptor = /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
Loading history...
805
            list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $object);
806
807
            $toRemoveFromStorage = [];
808
809
            foreach ($storage as $remoteBean) {
810
                /* @var $remoteBean AbstractTDBMObject */
811
                $statusArr = $storage[$remoteBean];
812
                $status = $statusArr['status'];
813
                $reverse = $statusArr['reverse'];
814
                if ($reverse) {
815
                    continue;
816
                }
817
818
                if ($status === 'new') {
819
                    $remoteBeanStatus = $remoteBean->_getStatus();
820
                    if ($remoteBeanStatus === TDBMObjectStateEnum::STATE_NEW || $remoteBeanStatus === TDBMObjectStateEnum::STATE_DETACHED) {
821
                        // Let's save remote bean if needed.
822
                        $this->save($remoteBean);
823
                    }
824
825
                    ['filters' => $filters, 'types' => $types] = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk, $tableDescriptor);
826
827
                    $this->connection->insert($this->connection->quoteIdentifier($pivotTableName), $filters, $types);
828
829
                    // Finally, let's mark relationships as saved.
830
                    $statusArr['status'] = 'loaded';
831
                    $storage[$remoteBean] = $statusArr;
832
                    $remoteStorage = $remoteBean->_getCachedRelationships()[$pivotTableName];
833
                    $remoteStatusArr = $remoteStorage[$object];
834
                    $remoteStatusArr['status'] = 'loaded';
835
                    $remoteStorage[$object] = $remoteStatusArr;
836
                } elseif ($status === 'delete') {
837
                    ['filters' => $filters, 'types' => $types] = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk, $tableDescriptor);
838
839
                    $this->connection->delete($this->connection->quoteIdentifier($pivotTableName), $filters, $types);
840
841
                    // Finally, let's remove relationships completely from bean.
842
                    $toRemoveFromStorage[] = $remoteBean;
843
844
                    $remoteBean->_getCachedRelationships()[$pivotTableName]->detach($object);
845
                }
846
            }
847
848
            // Note: due to https://bugs.php.net/bug.php?id=65629, we cannot delete an element inside a foreach loop on a SplStorageObject.
849
            // Therefore, we cache elements in the $toRemoveFromStorage to remove them at a later stage.
850
            foreach ($toRemoveFromStorage as $remoteBean) {
851
                $storage->detach($remoteBean);
852
            }
853
        }
854
    }
855
856
    /**
857
     * @return mixed[] An array with 2 keys: "filters" and "types"
858
     */
859
    private function getPivotFilters(AbstractTDBMObject $localBean, AbstractTDBMObject $remoteBean, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk, Table $tableDescriptor): array
860
    {
861
        $localBeanPk = $this->getPrimaryKeyValues($localBean);
862
        $remoteBeanPk = $this->getPrimaryKeyValues($remoteBean);
863
        $localColumns = $localFk->getUnquotedLocalColumns();
864
        $remoteColumns = $remoteFk->getUnquotedLocalColumns();
865
866
        $localFilters = SafeFunctions::arrayCombine($localColumns, $localBeanPk);
867
        $remoteFilters = SafeFunctions::arrayCombine($remoteColumns, $remoteBeanPk);
868
869
        $filters = array_merge($localFilters, $remoteFilters);
870
871
        $types = [];
872
        $escapedFilters = [];
873
874
        foreach ($filters as $columnName => $value) {
875
            $columnDescriptor = $tableDescriptor->getColumn((string) $columnName);
876
            $types[] = $columnDescriptor->getType();
877
            $escapedFilters[$this->connection->quoteIdentifier((string) $columnName)] = $value;
878
        }
879
        return ['filters' => $escapedFilters, 'types' => $types];
880
    }
881
882
    /**
883
     * Returns the "values" of the primary key.
884
     * This returns the primary key from the $primaryKey attribute, not the one stored in the columns.
885
     *
886
     * @param AbstractTDBMObject $bean
887
     *
888
     * @return mixed[] numerically indexed array of values
889
     */
890
    private function getPrimaryKeyValues(AbstractTDBMObject $bean): array
891
    {
892
        $dbRows = $bean->_getDbRows();
893
        $dbRow = reset($dbRows);
894
        if ($dbRow === false) {
895
            throw new \RuntimeException('Unexpected error: empty dbRow'); // @codeCoverageIgnore
896
        }
897
898
        return array_values($dbRow->_getPrimaryKeys());
899
    }
900
901
    /**
902
     * Returns a unique hash used to store the object based on its primary key.
903
     * If the array contains only one value, then the value is returned.
904
     * Otherwise, a hash representing the array is returned.
905
     *
906
     * @param mixed[] $primaryKeys An array of columns => values forming the primary key
907
     *
908
     * @return string|int
909
     */
910
    public function getObjectHash(array $primaryKeys)
911
    {
912
        if (count($primaryKeys) === 1) {
913
            return reset($primaryKeys);
914
        } else {
915
            ksort($primaryKeys);
916
917
            $pkJson = json_encode($primaryKeys);
918
            if ($pkJson === false) {
919
                throw new TDBMException('Unexepected error: unable to encode primary keys'); // @codeCoverageIgnore
920
            }
921
            return md5($pkJson);
922
        }
923
    }
924
925
    /**
926
     * Returns an array of primary keys from the object.
927
     * The primary keys are extracted from the object columns and not from the primary keys stored in the
928
     * $primaryKeys variable of the object.
929
     *
930
     * @param DbRow $dbRow
931
     *
932
     * @return mixed[] Returns an array of column => value
933
     */
934
    public function getPrimaryKeysForObjectFromDbRow(DbRow $dbRow): array
935
    {
936
        $table = $dbRow->_getDbTableName();
937
938
        $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
939
        $values = array();
940
        $dbRowValues = $dbRow->_getDbRow();
941
        foreach ($primaryKeyColumns as $column) {
942
            if (isset($dbRowValues[$column])) {
943
                $values[$column] = $dbRowValues[$column];
944
            }
945
        }
946
947
        return $values;
948
    }
949
950
    /**
951
     * Returns an array of primary keys for the given row.
952
     * The primary keys are extracted from the object columns.
953
     *
954
     * @param string $table
955
     * @param mixed[] $columns
956
     *
957
     * @return mixed[] Returns an array of column => value
958
     */
959
    public function _getPrimaryKeysFromObjectData(string $table, array $columns): array
960
    {
961
        $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
962
        $values = [];
963
        foreach ($primaryKeyColumns as $column) {
964
            if (isset($columns[$column])) {
965
                $values[$column] = $columns[$column];
966
            }
967
        }
968
969
        return $values;
970
    }
971
972
    /**
973
     * Attaches $object to this TDBMService.
974
     * The $object must be in DETACHED state and will pass in NEW state.
975
     *
976
     * @param AbstractTDBMObject $object
977
     *
978
     * @throws TDBMInvalidOperationException
979
     */
980
    public function attach(AbstractTDBMObject $object): void
981
    {
982
        $object->_attach($this);
983
    }
984
985
    /**
986
     * Returns an associative array (column => value) for the primary keys from the table name and an
987
     * indexed array of primary key values.
988
     *
989
     * @param string $tableName
990
     * @param mixed[] $indexedPrimaryKeys
991
     * @return mixed[]
992
     */
993
    public function _getPrimaryKeysFromIndexedPrimaryKeys(string $tableName, array $indexedPrimaryKeys): array
994
    {
995
        $primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
996
997
        if (count($primaryKeyColumns) !== count($indexedPrimaryKeys)) {
998
            throw new TDBMException(sprintf('Wrong number of columns passed for primary key. Expected %s columns for table "%s",
999
			got %s instead.', count($primaryKeyColumns), $tableName, count($indexedPrimaryKeys)));
1000
        }
1001
1002
        return SafeFunctions::arrayCombine($primaryKeyColumns, $indexedPrimaryKeys);
1003
    }
1004
1005
    /**
1006
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
1007
     * Tables must be in a single line of inheritance. The method will find missing tables.
1008
     *
1009
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1010
     * we must be able to find all other tables.
1011
     *
1012
     * @param string[] $tables
1013
     *
1014
     * @return string[]
1015
     */
1016
    public function _getLinkBetweenInheritedTables(array $tables): array
1017
    {
1018
        sort($tables);
1019
1020
        return $this->fromCache(
1021
            $this->cachePrefix.'_linkbetweeninheritedtables_'.implode('__split__', $tables),
1022
            function () use ($tables) {
1023
                return $this->_getLinkBetweenInheritedTablesWithoutCache($tables);
1024
            }
1025
        );
1026
    }
1027
1028
    /**
1029
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
1030
     * Tables must be in a single line of inheritance. The method will find missing tables.
1031
     *
1032
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1033
     * we must be able to find all other tables.
1034
     *
1035
     * @param string[] $tables
1036
     *
1037
     * @return string[]
1038
     */
1039
    private function _getLinkBetweenInheritedTablesWithoutCache(array $tables): array
1040
    {
1041
        $schemaAnalyzer = $this->schemaAnalyzer;
1042
1043
        foreach ($tables as $currentTable) {
1044
            $allParents = [$currentTable];
1045
            while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1046
                $currentTable = $currentFk->getForeignTableName();
1047
                $allParents[] = $currentTable;
1048
            }
1049
1050
            // Now, does the $allParents contain all the tables we want?
1051
            $notFoundTables = array_diff($tables, $allParents);
1052
            if (empty($notFoundTables)) {
1053
                // We have a winner!
1054
                return $allParents;
1055
            }
1056
        }
1057
1058
        throw TDBMInheritanceException::create($tables);
1059
    }
1060
1061
    /**
1062
     * Returns the list of tables related to this table (via a parent or child inheritance relationship).
1063
     *
1064
     * @param string $table
1065
     *
1066
     * @return string[]
1067
     */
1068
    public function _getRelatedTablesByInheritance(string $table): array
1069
    {
1070
        return $this->fromCache($this->cachePrefix.'_relatedtables_'.$table, function () use ($table) {
1071
            return $this->_getRelatedTablesByInheritanceWithoutCache($table);
1072
        });
1073
    }
1074
1075
    /**
1076
     * Returns the list of tables related to this table (via a parent or child inheritance relationship).
1077
     *
1078
     * @param string $table
1079
     *
1080
     * @return string[]
1081
     */
1082
    private function _getRelatedTablesByInheritanceWithoutCache(string $table): array
1083
    {
1084
        $schemaAnalyzer = $this->schemaAnalyzer;
1085
1086
        // Let's scan the parent tables
1087
        $currentTable = $table;
1088
1089
        $parentTables = [];
1090
1091
        // Get parent relationship
1092
        while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1093
            $currentTable = $currentFk->getForeignTableName();
1094
            $parentTables[] = $currentTable;
1095
        }
1096
1097
        // Let's recurse in children
1098
        $childrenTables = $this->exploreChildrenTablesRelationships($schemaAnalyzer, $table);
1099
1100
        return array_merge(array_reverse($parentTables), $childrenTables);
1101
    }
1102
1103
    /**
1104
     * Explore all the children and descendant of $table and returns ForeignKeyConstraints on those.
1105
     *
1106
     * @return string[]
1107
     */
1108
    private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyzer, string $table): array
1109
    {
1110
        $tables = [$table];
1111
        $keys = $schemaAnalyzer->getChildrenRelationships($table);
1112
1113
        foreach ($keys as $key) {
1114
            $tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, $key->getLocalTableName()));
1115
        }
1116
1117
        return $tables;
1118
    }
1119
1120
    /**
1121
     * Returns a `ResultIterator` object representing filtered records of "$mainTable" .
1122
     *
1123
     * The findObjects method should be the most used query method in TDBM if you want to query the database for objects.
1124
     * (Note: if you want to query the database for an object by its primary key, use the findObjectByPk method).
1125
     *
1126
     * The findObjects method takes in parameter:
1127
     * 	- mainTable: the kind of bean you want to retrieve. In TDBM, a bean matches a database row, so the
1128
     * 			`$mainTable` parameter should be the name of an existing table in database.
1129
     *  - filter: The filter is a filter bag. It is what you use to filter your request (the WHERE part in SQL).
1130
     *          It can be a string (SQL Where clause), or even a bean or an associative array (key = column to filter, value = value to find)
1131
     *  - parameters: The parameters used in the filter. If you pass a SQL string as a filter, be sure to avoid
1132
     *          concatenating parameters in the string (this leads to SQL injection and also to poor caching performance).
1133
     *          Instead, please consider passing parameters (see documentation for more details).
1134
     *  - additionalTablesFetch: An array of SQL tables names. The beans related to those tables will be fetched along
1135
     *          the main table. This is useful to avoid hitting the database with numerous subqueries.
1136
     *  - mode: The fetch mode of the result. See `setFetchMode()` method for more details.
1137
     *
1138
     * The `findObjects` method will return a `ResultIterator`. A `ResultIterator` is an object that behaves as an array
1139
     * (in ARRAY mode) at least. It can be iterated using a `foreach` loop.
1140
     *
1141
     * Finally, if filter_bag is null, the whole table is returned.
1142
     *
1143
     * @param string                       $mainTable             The name of the table queried
1144
     * @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)
1145
     * @param mixed[]                      $parameters
1146
     * @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)
1147
     * @param string[]                     $additionalTablesFetch
1148
     * @param int|null                     $mode
1149
     * @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...
1150
     * @param class-string                 $resultIteratorClass   The name of the resultIterator class to return
1151
     *
1152
     * @return ResultIterator An object representing an array of results
1153
     *
1154
     * @throws TDBMException
1155
     */
1156
    public function findObjects(string $mainTable, $filter, array $parameters, $orderString, array $additionalTablesFetch, ?int $mode, ?string $className, string $resultIteratorClass): ResultIterator
1157
    {
1158
        if (!is_a($resultIteratorClass, ResultIterator::class, true)) {
1159
            throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.');
1160
        }
1161
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1162
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1163
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1164
        }
1165
1166
        $mode = $mode ?: $this->mode;
1167
1168
        // We quote in MySQL because MagicJoin requires MySQL style quotes
1169
        $mysqlPlatform = new MySqlPlatform();
1170
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter, $mysqlPlatform);
1171
1172
        $parameters = array_merge($parameters, $additionalParameters);
1173
1174
        $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

1174
        $queryFactory = new FindObjectsQueryFactory($mainTable, $additionalTablesFetch, $filterString, $orderString, $this, /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer, $this->cache);
Loading history...
1175
1176
        return $resultIteratorClass::createResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1177
    }
1178
1179
    /**
1180
     * @param string                       $mainTable   The name of the table queried
1181
     * @param string                       $from        The from sql statement
1182
     * @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)
1183
     * @param mixed[]                      $parameters
1184
     * @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)
1185
     * @param int                          $mode
1186
     * @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...
1187
     * @param class-string                 $resultIteratorClass   The name of the resultIterator class to return
1188
     *
1189
     * @return ResultIterator An object representing an array of results
1190
     *
1191
     * @throws TDBMException
1192
     */
1193
    public function findObjectsFromSql(string $mainTable, string $from, $filter, array $parameters, $orderString, ?int $mode, ?string $className, string $resultIteratorClass): ResultIterator
1194
    {
1195
        if (!is_a($resultIteratorClass, ResultIterator::class, true)) {
1196
            throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.');
1197
        }
1198
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1199
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1200
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1201
        }
1202
1203
        $mode = $mode ?: $this->mode;
1204
1205
        // We quote in MySQL because MagicJoin requires MySQL style quotes
1206
        $mysqlPlatform = new MySqlPlatform();
1207
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter, $mysqlPlatform);
1208
1209
        $parameters = array_merge($parameters, $additionalParameters);
1210
1211
        $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

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

1374
        $queryFactory = new FindObjectsFromRawSqlQueryFactory($this, /** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema(), $mainTable, $sql, $sqlCount);
Loading history...
1375
1376
        return $resultIteratorClass::createResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1377
    }
1378
1379
    /**
1380
     * Returns a unique bean according to the filters passed in parameter.
1381
     * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1382
     *
1383
     * @param string            $mainTable             The name of the table queried
1384
     * @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)
1385
     * @param mixed[]           $parameters
1386
     * @param string[]          $additionalTablesFetch
1387
     * @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...
1388
     * @param class-string      $resultIteratorClass
1389
     *
1390
     * @return AbstractTDBMObject The object we want
1391
     *
1392
     * @throws TDBMException
1393
     */
1394
    public function findObjectOrFail(string $mainTable, $filter, array $parameters, array $additionalTablesFetch, string $className, string $resultIteratorClass): AbstractTDBMObject
1395
    {
1396
        assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'));
1397
        $bean = $this->findObject($mainTable, $filter, $parameters, $additionalTablesFetch, $className, $resultIteratorClass);
1398
        if ($bean === null) {
1399
            throw NoBeanFoundException::missFilterRecord($mainTable);
1400
        }
1401
1402
        return $bean;
1403
    }
1404
1405
    /**
1406
     * @param array<string, array> $beanData An array of data: array<table, array<column, value>>
1407
     *
1408
     * @return mixed[] an array with first item = class name, second item = table name and third item = list of tables needed
1409
     *
1410
     * @throws TDBMInheritanceException
1411
     */
1412
    public function _getClassNameFromBeanData(array $beanData): array
1413
    {
1414
        if (count($beanData) === 1) {
1415
            $tableName = (string) array_keys($beanData)[0];
1416
            $allTables = [$tableName];
1417
        } else {
1418
            $tables = [];
1419
            foreach ($beanData as $table => $row) {
1420
                $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1421
                $pkSet = false;
1422
                foreach ($primaryKeyColumns as $columnName) {
1423
                    if ($row[$columnName] !== null) {
1424
                        $pkSet = true;
1425
                        break;
1426
                    }
1427
                }
1428
                if ($pkSet) {
1429
                    $tables[] = $table;
1430
                }
1431
            }
1432
1433
            // $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1434
            try {
1435
                $allTables = $this->_getLinkBetweenInheritedTables($tables);
1436
            } catch (TDBMInheritanceException $e) {
1437
                throw TDBMInheritanceException::extendException($e, $this, $beanData);
1438
            }
1439
            $tableName = $allTables[0];
1440
        }
1441
1442
        // Only one table in this bean. Life is sweat, let's look at its type:
1443
        try {
1444
            $className = $this->getBeanClassName($tableName);
1445
        } catch (TDBMInvalidArgumentException $e) {
1446
            $className = 'TheCodingMachine\\TDBM\\TDBMObject';
1447
        }
1448
1449
        return [$className, $tableName, $allTables];
1450
    }
1451
1452
    /**
1453
     * Returns an item from cache or computes it using $closure and puts it in cache.
1454
     *
1455
     * @param string   $key
1456
     * @param callable $closure
1457
     *
1458
     * @return mixed
1459
     */
1460
    private function fromCache(string $key, callable $closure)
1461
    {
1462
        $item = $this->cache->fetch($key);
1463
        if ($item === false) {
1464
            $item = $closure();
1465
            $result = $this->cache->save($key, $item);
1466
1467
            if ($result === false) {
1468
                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.');
1469
            }
1470
        }
1471
1472
        return $item;
1473
    }
1474
1475
    /**
1476
     * @return AbstractTDBMObject[]|ResultIterator
1477
     */
1478
    public function _getRelatedBeans(ManyToManyRelationshipPathDescriptor $pathDescriptor, AbstractTDBMObject $bean): ResultIterator
1479
    {
1480
        // Magic Query expect MySQL syntax for quotes
1481
        $platform = new MySqlPlatform();
1482
1483
        return $this->findObjectsFromSql(
1484
            $pathDescriptor->getTargetName(),
1485
            $pathDescriptor->getPivotFrom($platform),
1486
            $pathDescriptor->getPivotWhere($platform),
1487
            $pathDescriptor->getPivotParams($this->getPrimaryKeyValues($bean)),
1488
            null,
1489
            null,
1490
            null,
1491
            $pathDescriptor->getResultIteratorClass()
1492
        );
1493
    }
1494
1495
    /**
1496
     * @param string $pivotTableName
1497
     * @param AbstractTDBMObject $bean The LOCAL bean
1498
     *
1499
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean
1500
     *
1501
     * @throws TDBMException
1502
     */
1503
    private function getPivotTableForeignKeys(string $pivotTableName, AbstractTDBMObject $bean): array
1504
    {
1505
        $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

1505
        $fks = array_values(/** @scrutinizer ignore-deprecated */ $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
Loading history...
1506
        $table1 = $fks[0]->getForeignTableName();
1507
        $table2 = $fks[1]->getForeignTableName();
1508
1509
        $beanTables = array_map(function (DbRow $dbRow) {
1510
            return $dbRow->_getDbTableName();
1511
        }, $bean->_getDbRows());
1512
1513
        if (in_array($table1, $beanTables)) {
1514
            return [$fks[0], $fks[1]];
1515
        } elseif (in_array($table2, $beanTables)) {
1516
            return [$fks[1], $fks[0]];
1517
        } else {
1518
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2} for pivot table {$pivotTableName}");
1519
        }
1520
    }
1521
1522
    /**
1523
     * Array of types for tables.
1524
     * Key: table name
1525
     * Value: array of types indexed by column.
1526
     *
1527
     * @var array[]
1528
     */
1529
    private $typesForTable = [];
1530
1531
    /**
1532
     * @internal
1533
     *
1534
     * @param string $tableName
1535
     *
1536
     * @return Type[]
1537
     */
1538
    public function _getColumnTypesForTable(string $tableName): array
1539
    {
1540
        if (!isset($this->typesForTable[$tableName])) {
1541
            $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

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