Passed
Push — oracle_tests ( 58c1c0 )
by David
03:12
created

TDBMService::_getForeignKeyByName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
3
/*
4
 Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
5
6
This program is free software; you can redistribute it and/or modify
7
it under the terms of the GNU General Public License as published by
8
the Free Software Foundation; either version 2 of the License, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
GNU General Public License for more details.
15
16
You should have received a copy of the GNU General Public License
17
along with this program; if not, write to the Free Software
18
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
*/
20
21
namespace TheCodingMachine\TDBM;
22
23
use Doctrine\Common\Cache\Cache;
24
use Doctrine\Common\Cache\VoidCache;
25
use Doctrine\DBAL\Connection;
26
use Doctrine\DBAL\Platforms\AbstractPlatform;
27
use Doctrine\DBAL\Platforms\MySqlPlatform;
28
use Doctrine\DBAL\Platforms\OraclePlatform;
29
use Doctrine\DBAL\Schema\Column;
30
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
31
use Doctrine\DBAL\Schema\Schema;
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\NamingStrategyInterface;
40
use TheCodingMachine\TDBM\Utils\TDBMDaoGenerator;
41
use Phlib\Logger\LevelFilter;
42
use Psr\Log\LoggerInterface;
43
use Psr\Log\LogLevel;
44
use Psr\Log\NullLogger;
45
46
/**
47
 * The TDBMService class is the main TDBM class. It provides methods to retrieve TDBMObject instances
48
 * from the database.
49
 *
50
 * @author David Negrier
51
 * @ExtendedAction {"name":"Generate DAOs", "url":"tdbmadmin/", "default":false}
52
 */
53
class TDBMService
54
{
55
    const MODE_CURSOR = 1;
56
    const MODE_ARRAY = 2;
57
58
    /**
59
     * The database connection.
60
     *
61
     * @var Connection
62
     */
63
    private $connection;
64
65
    /**
66
     * @var SchemaAnalyzer
67
     */
68
    private $schemaAnalyzer;
69
70
    /**
71
     * @var MagicQuery
72
     */
73
    private $magicQuery;
74
75
    /**
76
     * @var TDBMSchemaAnalyzer
77
     */
78
    private $tdbmSchemaAnalyzer;
79
80
    /**
81
     * @var string
82
     */
83
    private $cachePrefix;
84
85
    /**
86
     * Cache of table of primary keys.
87
     * Primary keys are stored by tables, as an array of column.
88
     * For instance $primary_key['my_table'][0] will return the first column of the primary key of table 'my_table'.
89
     *
90
     * @var string[]
91
     */
92
    private $primaryKeysColumns;
93
94
    /**
95
     * Service storing objects in memory.
96
     * Access is done by table name and then by primary key.
97
     * If the primary key is split on several columns, access is done by an array of columns, serialized.
98
     *
99
     * @var StandardObjectStorage|WeakrefObjectStorage
100
     */
101
    private $objectStorage;
102
103
    /**
104
     * The fetch mode of the result sets returned by `getObjects`.
105
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY.
106
     *
107
     * In 'MODE_ARRAY' mode (default), the result is an array. Use this mode by default (unless the list returned is very big).
108
     * 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,
109
     * and it cannot be accessed via key. Use this mode for large datasets processed by batch.
110
     * In 'MODE_COMPATIBLE_ARRAY' mode, the result is an old TDBMObjectArray (used up to TDBM 3.2).
111
     * You can access the array by key, or using foreach, several times.
112
     *
113
     * @var int
114
     */
115
    private $mode = self::MODE_ARRAY;
116
117
    /**
118
     * Table of new objects not yet inserted in database or objects modified that must be saved.
119
     *
120
     * @var \SplObjectStorage of DbRow objects
121
     */
122
    private $toSaveObjects;
123
124
    /**
125
     * A cache service to be used.
126
     *
127
     * @var Cache|null
128
     */
129
    private $cache;
130
131
    /**
132
     * Map associating a table name to a fully qualified Bean class name.
133
     *
134
     * @var array
135
     */
136
    private $tableToBeanMap = [];
137
138
    /**
139
     * @var \ReflectionClass[]
140
     */
141
    private $reflectionClassCache = array();
142
143
    /**
144
     * @var LoggerInterface
145
     */
146
    private $rootLogger;
147
148
    /**
149
     * @var LevelFilter|NullLogger
150
     */
151
    private $logger;
152
153
    /**
154
     * @var OrderByAnalyzer
155
     */
156
    private $orderByAnalyzer;
157
158
    /**
159
     * @var string
160
     */
161
    private $beanNamespace;
162
163
    /**
164
     * @var NamingStrategyInterface
165
     */
166
    private $namingStrategy;
167
    /**
168
     * @var ConfigurationInterface
169
     */
170
    private $configuration;
171
172
    /**
173
     * @param ConfigurationInterface $configuration The configuration object
174
     */
175
    public function __construct(ConfigurationInterface $configuration)
176
    {
177
        if (extension_loaded('weakref')) {
178
            $this->objectStorage = new WeakrefObjectStorage();
179
        } else {
180
            $this->objectStorage = new StandardObjectStorage();
181
        }
182
        $this->connection = $configuration->getConnection();
183
        $this->cache = $configuration->getCache();
184
        $this->schemaAnalyzer = $configuration->getSchemaAnalyzer();
185
186
        $this->magicQuery = new MagicQuery($this->connection, $this->cache, $this->schemaAnalyzer);
187
188
        $this->tdbmSchemaAnalyzer = new TDBMSchemaAnalyzer($this->connection, $this->cache, $this->schemaAnalyzer);
189
        $this->cachePrefix = $this->tdbmSchemaAnalyzer->getCachePrefix();
190
191
        $this->toSaveObjects = new \SplObjectStorage();
192
        $logger = $configuration->getLogger();
193
        if ($logger === null) {
194
            $this->logger = new NullLogger();
195
            $this->rootLogger = new NullLogger();
196
        } else {
197
            $this->rootLogger = $logger;
198
            $this->setLogLevel(LogLevel::WARNING);
199
        }
200
        $this->orderByAnalyzer = new OrderByAnalyzer($this->cache, $this->cachePrefix);
201
        $this->beanNamespace = $configuration->getBeanNamespace();
202
        $this->namingStrategy = $configuration->getNamingStrategy();
203
        $this->configuration = $configuration;
204
    }
205
206
    /**
207
     * Returns the object used to connect to the database.
208
     *
209
     * @return Connection
210
     */
211
    public function getConnection(): Connection
212
    {
213
        return $this->connection;
214
    }
215
216
    /**
217
     * Sets the default fetch mode of the result sets returned by `findObjects`.
218
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY.
219
     *
220
     * 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).
221
     * 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
222
     * several times. In cursor mode, you cannot access the result set by key. Use this mode for large datasets processed by batch.
223
     *
224
     * @param int $mode
225
     *
226
     * @return $this
227
     *
228
     * @throws TDBMException
229
     */
230
    public function setFetchMode($mode)
231
    {
232
        if ($mode !== self::MODE_CURSOR && $mode !== self::MODE_ARRAY) {
233
            throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
234
        }
235
        $this->mode = $mode;
236
237
        return $this;
238
    }
239
240
    /**
241
     * Removes the given object from database.
242
     * This cannot be called on an object that is not attached to this TDBMService
243
     * (will throw a TDBMInvalidOperationException).
244
     *
245
     * @param AbstractTDBMObject $object the object to delete
246
     *
247
     * @throws TDBMException
248
     * @throws TDBMInvalidOperationException
249
     */
250
    public function delete(AbstractTDBMObject $object)
251
    {
252
        switch ($object->_getStatus()) {
253
            case TDBMObjectStateEnum::STATE_DELETED:
254
                // Nothing to do, object already deleted.
255
                return;
256
            case TDBMObjectStateEnum::STATE_DETACHED:
257
                throw new TDBMInvalidOperationException('Cannot delete a detached object');
258
            case TDBMObjectStateEnum::STATE_NEW:
259
                $this->deleteManyToManyRelationships($object);
260
                foreach ($object->_getDbRows() as $dbRow) {
261
                    $this->removeFromToSaveObjectList($dbRow);
262
                }
263
                break;
264
            case TDBMObjectStateEnum::STATE_DIRTY:
265
                foreach ($object->_getDbRows() as $dbRow) {
266
                    $this->removeFromToSaveObjectList($dbRow);
267
                }
268
                // And continue deleting...
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
269
            case TDBMObjectStateEnum::STATE_NOT_LOADED:
270
            case TDBMObjectStateEnum::STATE_LOADED:
271
                $this->deleteManyToManyRelationships($object);
272
                // Let's delete db rows, in reverse order.
273
                foreach (array_reverse($object->_getDbRows()) as $dbRow) {
274
                    /* @var $dbRow DbRow */
275
                    $tableName = $dbRow->_getDbTableName();
276
                    $primaryKeys = $dbRow->_getPrimaryKeys();
277
                    $quotedPrimaryKeys = [];
278
                    foreach ($primaryKeys as $column => $value) {
279
                        $quotedPrimaryKeys[$this->connection->quoteIdentifier($column)] = $value;
280
                    }
281
                    $this->connection->delete($this->connection->quoteIdentifier($tableName), $quotedPrimaryKeys);
282
                    $this->objectStorage->remove($dbRow->_getDbTableName(), $this->getObjectHash($primaryKeys));
283
                }
284
                break;
285
            // @codeCoverageIgnoreStart
286
            default:
287
                throw new TDBMInvalidOperationException('Unexpected status for bean');
288
            // @codeCoverageIgnoreEnd
289
        }
290
291
        $object->_setStatus(TDBMObjectStateEnum::STATE_DELETED);
292
    }
293
294
    /**
295
     * Removes all many to many relationships for this object.
296
     *
297
     * @param AbstractTDBMObject $object
298
     */
299
    private function deleteManyToManyRelationships(AbstractTDBMObject $object)
300
    {
301
        foreach ($object->_getDbRows() as $tableName => $dbRow) {
302
            $pivotTables = $this->tdbmSchemaAnalyzer->getPivotTableLinkedToTable($tableName);
303
            foreach ($pivotTables as $pivotTable) {
304
                $remoteBeans = $object->_getRelationships($pivotTable);
305
                foreach ($remoteBeans as $remoteBean) {
306
                    $object->_removeRelationship($pivotTable, $remoteBean);
307
                }
308
            }
309
        }
310
        $this->persistManyToManyRelationships($object);
311
    }
312
313
    /**
314
     * This function removes the given object from the database. It will also remove all objects relied to the one given
315
     * by parameter before all.
316
     *
317
     * Notice: if the object has a multiple primary key, the function will not work.
318
     *
319
     * @param AbstractTDBMObject $objToDelete
320
     */
321
    public function deleteCascade(AbstractTDBMObject $objToDelete)
322
    {
323
        $this->deleteAllConstraintWithThisObject($objToDelete);
324
        $this->delete($objToDelete);
325
    }
326
327
    /**
328
     * This function is used only in TDBMService (private function)
329
     * It will call deleteCascade function foreach object relied with a foreign key to the object given by parameter.
330
     *
331
     * @param AbstractTDBMObject $obj
332
     */
333
    private function deleteAllConstraintWithThisObject(AbstractTDBMObject $obj)
334
    {
335
        $dbRows = $obj->_getDbRows();
336
        foreach ($dbRows as $dbRow) {
337
            $tableName = $dbRow->_getDbTableName();
338
            $pks = array_values($dbRow->_getPrimaryKeys());
339
            if (!empty($pks)) {
340
                $incomingFks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($tableName);
341
342
                foreach ($incomingFks as $incomingFk) {
343
                    $filter = array_combine($incomingFk->getUnquotedLocalColumns(), $pks);
344
345
                    $results = $this->findObjects($incomingFk->getLocalTableName(), $filter);
346
347
                    foreach ($results as $bean) {
348
                        $this->deleteCascade($bean);
349
                    }
350
                }
351
            }
352
        }
353
    }
354
355
    /**
356
     * This function performs a save() of all the objects that have been modified.
357
     */
358
    public function completeSave()
359
    {
360
        foreach ($this->toSaveObjects as $dbRow) {
361
            $this->save($dbRow->getTDBMObject());
362
        }
363
    }
364
365
    /**
366
     * Takes in input a filter_bag (which can be about anything from a string to an array of TDBMObjects... see above from documentation),
367
     * and gives back a proper Filter object.
368
     *
369
     * @param mixed $filter_bag
370
     * @param AbstractPlatform $platform The platform used to quote identifiers
371
     * @param int $counter
372
     * @return array First item: filter string, second item: parameters
373
     *
374
     * @throws TDBMException
375
     */
376
    public function buildFilterFromFilterBag($filter_bag, AbstractPlatform $platform, $counter = 1)
377
    {
378
        if ($filter_bag === null) {
379
            return ['', []];
380
        } elseif (is_string($filter_bag)) {
381
            return [$filter_bag, []];
382
        } elseif (is_array($filter_bag)) {
383
            $sqlParts = [];
384
            $parameters = [];
385
386
            foreach ($filter_bag as $column => $value) {
387
                if (is_int($column)) {
388
                    list($subSqlPart, $subParameters) = $this->buildFilterFromFilterBag($value, $platform, $counter);
389
                    $sqlParts[] = $subSqlPart;
390
                    $parameters += $subParameters;
391
                } else {
392
                    $paramName = 'tdbmparam'.$counter;
393
                    if (is_array($value)) {
394
                        $sqlParts[] = $platform->quoteIdentifier($column).' IN :'.$paramName;
395
                    } else {
396
                        $sqlParts[] = $platform->quoteIdentifier($column).' = :'.$paramName;
397
                    }
398
                    $parameters[$paramName] = $value;
399
                    ++$counter;
400
                }
401
            }
402
403
            return [implode(' AND ', $sqlParts), $parameters];
404
        } elseif ($filter_bag instanceof AbstractTDBMObject) {
405
            $sqlParts = [];
406
            $parameters = [];
407
            $dbRows = $filter_bag->_getDbRows();
408
            $dbRow = reset($dbRows);
409
            $primaryKeys = $dbRow->_getPrimaryKeys();
410
411
            foreach ($primaryKeys as $column => $value) {
412
                $paramName = 'tdbmparam'.$counter;
413
                $sqlParts[] = $platform->quoteIdentifier($dbRow->_getDbTableName()).'.'.$platform->quoteIdentifier($column).' = :'.$paramName;
414
                $parameters[$paramName] = $value;
415
                ++$counter;
416
            }
417
418
            return [implode(' AND ', $sqlParts), $parameters];
419
        } elseif ($filter_bag instanceof \Iterator) {
420
            return $this->buildFilterFromFilterBag(iterator_to_array($filter_bag), $platform, $counter);
421
        } else {
422
            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.');
423
        }
424
    }
425
426
    /**
427
     * @param string $table
428
     *
429
     * @return string[]
430
     */
431
    public function getPrimaryKeyColumns($table)
432
    {
433
        if (!isset($this->primaryKeysColumns[$table])) {
434
            $this->primaryKeysColumns[$table] = $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKey()->getUnquotedColumns();
435
436
            // TODO TDBM4: See if we need to improve error reporting if table name does not exist.
437
438
            /*$arr = array();
439
            foreach ($this->connection->getPrimaryKey($table) as $col) {
440
                $arr[] = $col->name;
441
            }
442
            // The primaryKeysColumns contains only the column's name, not the DB_Column object.
443
            $this->primaryKeysColumns[$table] = $arr;
444
            if (empty($this->primaryKeysColumns[$table]))
445
            {
446
                // Unable to find primary key.... this is an error
447
                // Let's try to be precise in error reporting. Let's try to find the table.
448
                $tables = $this->connection->checkTableExist($table);
449
                if ($tables === true)
450
                throw new TDBMException("Could not find table primary key for table '$table'. Please define a primary key for this table.");
451
                elseif ($tables !== null) {
452
                    if (count($tables)==1)
453
                    $str = "Could not find table '$table'. Maybe you meant this table: '".$tables[0]."'";
454
                    else
455
                    $str = "Could not find table '$table'. Maybe you meant one of those tables: '".implode("', '",$tables)."'";
456
                    throw new TDBMException($str);
457
                }
458
            }*/
459
        }
460
461
        return $this->primaryKeysColumns[$table];
462
    }
463
464
    /**
465
     * This is an internal function, you should not use it in your application.
466
     * This is used internally by TDBM to add an object to the object cache.
467
     *
468
     * @param DbRow $dbRow
469
     */
470
    public function _addToCache(DbRow $dbRow)
471
    {
472
        $primaryKey = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
473
        $hash = $this->getObjectHash($primaryKey);
474
        $this->objectStorage->set($dbRow->_getDbTableName(), $hash, $dbRow);
475
    }
476
477
    /**
478
     * This is an internal function, you should not use it in your application.
479
     * This is used internally by TDBM to remove the object from the list of objects that have been
480
     * created/updated but not saved yet.
481
     *
482
     * @param DbRow $myObject
483
     */
484
    private function removeFromToSaveObjectList(DbRow $myObject)
485
    {
486
        unset($this->toSaveObjects[$myObject]);
487
    }
488
489
    /**
490
     * This is an internal function, you should not use it in your application.
491
     * This is used internally by TDBM to add an object to the list of objects that have been
492
     * created/updated but not saved yet.
493
     *
494
     * @param DbRow $myObject
495
     */
496
    public function _addToToSaveObjectList(DbRow $myObject)
497
    {
498
        $this->toSaveObjects[$myObject] = true;
499
    }
500
501
    /**
502
     * Generates all the daos and beans.
503
     *
504
     * @return \string[] the list of tables (key) and bean name (value)
505
     */
506
    public function generateAllDaosAndBeans()
507
    {
508
        // Purge cache before generating anything.
509
        $this->cache->deleteAll();
0 ignored issues
show
Bug introduced by
The method deleteAll() does not exist on Doctrine\Common\Cache\Cache. Did you maybe mean delete()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
510
511
        $tdbmDaoGenerator = new TDBMDaoGenerator($this->configuration, $this->tdbmSchemaAnalyzer);
512
        $tdbmDaoGenerator->generateAllDaosAndBeans();
513
    }
514
515
    /**
516
     * Returns the fully qualified class name of the bean associated with table $tableName.
517
     *
518
     *
519
     * @param string $tableName
520
     *
521
     * @return string
522
     */
523
    public function getBeanClassName(string $tableName) : string
524
    {
525
        if (isset($this->tableToBeanMap[$tableName])) {
526
            return $this->tableToBeanMap[$tableName];
527
        } else {
528
            $className = $this->beanNamespace.'\\'.$this->namingStrategy->getBeanClassName($tableName);
529
530
            if (!class_exists($className)) {
531
                throw new TDBMInvalidArgumentException(sprintf('Could not find class "%s". Does table "%s" exist? If yes, consider regenerating the DAOs and beans.', $className, $tableName));
532
            }
533
534
            $this->tableToBeanMap[$tableName] = $className;
535
            return $className;
536
        }
537
    }
538
539
    /**
540
     * Saves $object by INSERTing or UPDAT(E)ing it in the database.
541
     *
542
     * @param AbstractTDBMObject $object
543
     *
544
     * @throws TDBMException
545
     */
546
    public function save(AbstractTDBMObject $object)
547
    {
548
        $status = $object->_getStatus();
549
550
        if ($status === null) {
551
            throw new TDBMException(sprintf('Your bean for class %s has no status. It is likely that you overloaded the __construct method and forgot to call parent::__construct.', get_class($object)));
552
        }
553
554
        // Let's attach this object if it is in detached state.
555
        if ($status === TDBMObjectStateEnum::STATE_DETACHED) {
556
            $object->_attach($this);
557
            $status = $object->_getStatus();
558
        }
559
560
        if ($status === TDBMObjectStateEnum::STATE_NEW) {
561
            $dbRows = $object->_getDbRows();
562
563
            $unindexedPrimaryKeys = array();
564
565
            foreach ($dbRows as $dbRow) {
566
                if ($dbRow->_getStatus() == TDBMObjectStateEnum::STATE_SAVING) {
567
                    throw TDBMCyclicReferenceException::createCyclicReference($dbRow->_getDbTableName(), $object);
568
                }
569
                $dbRow->_setStatus(TDBMObjectStateEnum::STATE_SAVING);
570
                $tableName = $dbRow->_getDbTableName();
571
572
                $schema = $this->tdbmSchemaAnalyzer->getSchema();
573
                $tableDescriptor = $schema->getTable($tableName);
574
575
                $primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
576
577
                $references = $dbRow->_getReferences();
578
579
                // Let's save all references in NEW or DETACHED state (we need their primary key)
580
                foreach ($references as $fkName => $reference) {
581
                    if ($reference !== null) {
582
                        $refStatus = $reference->_getStatus();
583
                        if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
584
                            try {
585
                                $this->save($reference);
586
                            } catch (TDBMCyclicReferenceException $e) {
587
                                throw TDBMCyclicReferenceException::extendCyclicReference($e, $dbRow->_getDbTableName(), $object, $fkName);
588
                            }
589
                        }
590
                    }
591
                }
592
593
                if (empty($unindexedPrimaryKeys)) {
594
                    $primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
595
                } else {
596
                    // First insert, the children must have the same primary key as the parent.
597
                    $primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
598
                    $dbRow->_setPrimaryKeys($primaryKeys);
599
                }
600
601
                $dbRowData = $dbRow->_getDbRow();
602
603
                // Let's see if the columns for primary key have been set before inserting.
604
                // We assume that if one of the value of the PK has been set, the PK is set.
605
                $isPkSet = !empty($primaryKeys);
606
607
                /*if (!$isPkSet) {
608
                    // if there is no autoincrement and no pk set, let's go in error.
609
                    $isAutoIncrement = true;
610
611
                    foreach ($primaryKeyColumns as $pkColumnName) {
612
                        $pkColumn = $tableDescriptor->getColumn($pkColumnName);
613
                        if (!$pkColumn->getAutoincrement()) {
614
                            $isAutoIncrement = false;
615
                        }
616
                    }
617
618
                    if (!$isAutoIncrement) {
619
                        $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.";
620
                        throw new TDBMException($msg);
621
                    }
622
623
                }*/
624
625
                $types = [];
626
                $escapedDbRowData = [];
627
628
                foreach ($dbRowData as $columnName => $value) {
629
                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
630
                    $types[] = $columnDescriptor->getType();
631
                    $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
632
                }
633
634
                $quotedTableName = $this->connection->quoteIdentifier($tableName);
635
                $this->connection->insert($quotedTableName, $escapedDbRowData, $types);
636
637
                if (!$isPkSet && count($primaryKeyColumns) === 1) {
638
                    $id = $this->connection->lastInsertId();
639
640
                    if ($id === false) {
641
                        // In Oracle (if we are in 11g), the lastInsertId will fail. We try again with the column.
642
                        $sequenceName = $this->connection->getDatabasePlatform()->getIdentitySequenceName(
643
                            $quotedTableName,
644
                            $this->connection->quoteIdentifier($primaryKeyColumns[0])
645
                        );
646
                        $id = $this->connection->lastInsertId($sequenceName);
647
                    }
648
649
                    $pkColumn = $primaryKeyColumns[0];
650
                    // lastInsertId returns a string but the column type is usually a int. Let's convert it back to the correct type.
651
                    $id = $tableDescriptor->getColumn($pkColumn)->getType()->convertToPHPValue($id, $this->getConnection()->getDatabasePlatform());
652
                    $primaryKeys[$pkColumn] = $id;
653
                }
654
655
                // TODO: change this to some private magic accessor in future
656
                $dbRow->_setPrimaryKeys($primaryKeys);
657
                $unindexedPrimaryKeys = array_values($primaryKeys);
658
659
                /*
660
                 * When attached, on "save", we check if the column updated is part of a primary key
661
                 * If this is part of a primary key, we call the _update_id method that updates the id in the list of known objects.
662
                 * This method should first verify that the id is not already used (and is not auto-incremented)
663
                 *
664
                 * In the object, the key is stored in an array of  (column => value), that can be directly used to update the record.
665
                 *
666
                 *
667
                 */
668
669
                /*try {
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
670
                    $this->db_connection->exec($sql);
671
                } catch (TDBMException $e) {
672
                    $this->db_onerror = true;
673
674
                    // Strange..... if we do not have the line below, bad inserts are not catched.
675
                    // It seems that destructors are called before the registered shutdown function (PHP >=5.0.5)
676
                    //if ($this->tdbmService->isProgramExiting())
677
                    //	trigger_error("program exiting");
678
                    trigger_error($e->getMessage(), E_USER_ERROR);
679
680
                    if (!$this->tdbmService->isProgramExiting())
681
                        throw $e;
682
                    else
683
                    {
684
                        trigger_error($e->getMessage(), E_USER_ERROR);
685
                    }
686
                }*/
687
688
                // Let's remove this object from the $new_objects static table.
689
                $this->removeFromToSaveObjectList($dbRow);
690
691
                // TODO: change this behaviour to something more sensible performance-wise
692
                // Maybe a setting to trigger this globally?
693
                //$this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
694
                //$this->db_modified_state = false;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
695
                //$dbRow = array();
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
696
697
                // Let's add this object to the list of objects in cache.
698
                $this->_addToCache($dbRow);
699
            }
700
701
            $object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
702
        } elseif ($status === TDBMObjectStateEnum::STATE_DIRTY) {
703
            $dbRows = $object->_getDbRows();
704
705
            foreach ($dbRows as $dbRow) {
706
                $references = $dbRow->_getReferences();
707
708
                // Let's save all references in NEW state (we need their primary key)
709
                foreach ($references as $fkName => $reference) {
710
                    if ($reference !== null && $reference->_getStatus() === TDBMObjectStateEnum::STATE_NEW) {
711
                        $this->save($reference);
712
                    }
713
                }
714
715
                // Let's first get the primary keys
716
                $tableName = $dbRow->_getDbTableName();
717
                $dbRowData = $dbRow->_getDbRow();
718
719
                $schema = $this->tdbmSchemaAnalyzer->getSchema();
720
                $tableDescriptor = $schema->getTable($tableName);
721
722
                $primaryKeys = $dbRow->_getPrimaryKeys();
723
724
                $types = [];
725
                $escapedDbRowData = [];
726
                $escapedPrimaryKeys = [];
727
728
                foreach ($dbRowData as $columnName => $value) {
729
                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
730
                    $types[] = $columnDescriptor->getType();
731
                    $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
732
                }
733
                foreach ($primaryKeys as $columnName => $value) {
734
                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
735
                    $types[] = $columnDescriptor->getType();
736
                    $escapedPrimaryKeys[$this->connection->quoteIdentifier($columnName)] = $value;
737
                }
738
739
                $this->connection->update($this->connection->quoteIdentifier($tableName), $escapedDbRowData, $escapedPrimaryKeys, $types);
740
741
                // Let's check if the primary key has been updated...
742
                $needsUpdatePk = false;
743
                foreach ($primaryKeys as $column => $value) {
744
                    if (!isset($dbRowData[$column]) || $dbRowData[$column] != $value) {
745
                        $needsUpdatePk = true;
746
                        break;
747
                    }
748
                }
749
                if ($needsUpdatePk) {
750
                    $this->objectStorage->remove($tableName, $this->getObjectHash($primaryKeys));
751
                    $newPrimaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
752
                    $dbRow->_setPrimaryKeys($newPrimaryKeys);
753
                    $this->objectStorage->set($tableName, $this->getObjectHash($primaryKeys), $dbRow);
754
                }
755
756
                // Let's remove this object from the list of objects to save.
757
                $this->removeFromToSaveObjectList($dbRow);
758
            }
759
760
            $object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
761
        } elseif ($status === TDBMObjectStateEnum::STATE_DELETED) {
762
            throw new TDBMInvalidOperationException('This object has been deleted. It cannot be saved.');
763
        }
764
765
        // Finally, let's save all the many to many relationships to this bean.
766
        $this->persistManyToManyRelationships($object);
767
    }
768
769
    private function persistManyToManyRelationships(AbstractTDBMObject $object)
770
    {
771
        foreach ($object->_getCachedRelationships() as $pivotTableName => $storage) {
772
            $tableDescriptor = $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
773
            list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $object);
774
775
            $toRemoveFromStorage = [];
776
777
            foreach ($storage as $remoteBean) {
778
                /* @var $remoteBean AbstractTDBMObject */
779
                $statusArr = $storage[$remoteBean];
780
                $status = $statusArr['status'];
781
                $reverse = $statusArr['reverse'];
782
                if ($reverse) {
783
                    continue;
784
                }
785
786
                if ($status === 'new') {
787
                    $remoteBeanStatus = $remoteBean->_getStatus();
788
                    if ($remoteBeanStatus === TDBMObjectStateEnum::STATE_NEW || $remoteBeanStatus === TDBMObjectStateEnum::STATE_DETACHED) {
789
                        // Let's save remote bean if needed.
790
                        $this->save($remoteBean);
791
                    }
792
793
                    ['filters' => $filters, 'types' => $types] = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk, $tableDescriptor);
0 ignored issues
show
Bug introduced by
The variable $filters does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $types does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1081
        $onClauses = [];
1082
        $foreignTableName = $this->connection->quoteIdentifier($fk->getForeignTableName());
1083
        $foreignColumns = $fk->getUnquotedForeignColumns();
1084
        $localTableName = $this->connection->quoteIdentifier($fk->getLocalTableName());
1085
        $localColumns = $fk->getUnquotedLocalColumns();
1086
        $columnCount = count($localTableName);
1087
1088
        for ($i = 0; $i < $columnCount; $i++) {
1089
            $onClauses[] = sprintf("%s.%s = %s.%s",
1090
                $localTableName,
1091
                $this->connection->quoteIdentifier($localColumns[$i]),
1092
                $foreignColumns,
1093
                $this->connection->quoteIdentifier($foreignColumns[$i])
1094
                );
1095
        }
1096
1097
        $onClause = implode(' AND ', $onClauses);
1098
1099
        if ($leftTableIsLocal) {
1100
            return sprintf(" LEFT JOIN %s ON (%s)", $foreignTableName, $onClause);
1101
        } else {
1102
            return sprintf(" LEFT JOIN %s ON (%s)", $localTableName, $onClause);
1103
        }
1104
    }*/
1105
1106
    /**
1107
     * Returns a `ResultIterator` object representing filtered records of "$mainTable" .
1108
     *
1109
     * The findObjects method should be the most used query method in TDBM if you want to query the database for objects.
1110
     * (Note: if you want to query the database for an object by its primary key, use the findObjectByPk method).
1111
     *
1112
     * The findObjects method takes in parameter:
1113
     * 	- mainTable: the kind of bean you want to retrieve. In TDBM, a bean matches a database row, so the
1114
     * 			`$mainTable` parameter should be the name of an existing table in database.
1115
     *  - filter: The filter is a filter bag. It is what you use to filter your request (the WHERE part in SQL).
1116
     *          It can be a string (SQL Where clause), or even a bean or an associative array (key = column to filter, value = value to find)
1117
     *  - parameters: The parameters used in the filter. If you pass a SQL string as a filter, be sure to avoid
1118
     *          concatenating parameters in the string (this leads to SQL injection and also to poor caching performance).
1119
     *          Instead, please consider passing parameters (see documentation for more details).
1120
     *  - additionalTablesFetch: An array of SQL tables names. The beans related to those tables will be fetched along
1121
     *          the main table. This is useful to avoid hitting the database with numerous subqueries.
1122
     *  - mode: The fetch mode of the result. See `setFetchMode()` method for more details.
1123
     *
1124
     * The `findObjects` method will return a `ResultIterator`. A `ResultIterator` is an object that behaves as an array
1125
     * (in ARRAY mode) at least. It can be iterated using a `foreach` loop.
1126
     *
1127
     * Finally, if filter_bag is null, the whole table is returned.
1128
     *
1129
     * @param string                       $mainTable             The name of the table queried
1130
     * @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)
1131
     * @param array                        $parameters
1132
     * @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)
1133
     * @param array                        $additionalTablesFetch
1134
     * @param int                          $mode
1135
     * @param string                       $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
1136
     *
1137
     * @return ResultIterator An object representing an array of results
1138
     *
1139
     * @throws TDBMException
1140
     */
1141
    public function findObjects(string $mainTable, $filter = null, array $parameters = array(), $orderString = null, array $additionalTablesFetch = array(), $mode = null, string $className = null)
1142
    {
1143
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1144
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1145
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1146
        }
1147
1148
        $mode = $mode ?: $this->mode;
1149
1150
        // We quote in MySQL because MagicJoin requires MySQL style quotes
1151
        $mysqlPlatform = new MySqlPlatform();
1152
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter, $mysqlPlatform);
1153
1154
        $parameters = array_merge($parameters, $additionalParameters);
1155
1156
        $queryFactory = new FindObjectsQueryFactory($mainTable, $additionalTablesFetch, $filterString, $orderString, $this, $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer);
1157
1158
        return new ResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1159
    }
1160
1161
    /**
1162
     * @param string                       $mainTable   The name of the table queried
1163
     * @param string                       $from        The from sql statement
1164
     * @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)
1165
     * @param array                        $parameters
1166
     * @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)
1167
     * @param int                          $mode
1168
     * @param string                       $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
1169
     *
1170
     * @return ResultIterator An object representing an array of results
1171
     *
1172
     * @throws TDBMException
1173
     */
1174
    public function findObjectsFromSql(string $mainTable, string $from, $filter = null, array $parameters = array(), $orderString = null, $mode = null, string $className = null)
1175
    {
1176
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1177
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1178
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1179
        }
1180
1181
        $mode = $mode ?: $this->mode;
1182
1183
        // We quote in MySQL because MagicJoin requires MySQL style quotes
1184
        $mysqlPlatform = new MySqlPlatform();
1185
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter, $mysqlPlatform);
1186
1187
        $parameters = array_merge($parameters, $additionalParameters);
1188
1189
        $queryFactory = new FindObjectsFromSqlQueryFactory($mainTable, $from, $filterString, $orderString, $this, $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer, $this->schemaAnalyzer, $this->cache, $this->cachePrefix);
0 ignored issues
show
Bug introduced by
It seems like $this->cache can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1190
1191
        return new ResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1192
    }
1193
1194
    /**
1195
     * @param $table
1196
     * @param array  $primaryKeys
1197
     * @param array  $additionalTablesFetch
1198
     * @param bool   $lazy                  Whether to perform lazy loading on this object or not
1199
     * @param string $className
1200
     *
1201
     * @return AbstractTDBMObject
1202
     *
1203
     * @throws TDBMException
1204
     */
1205
    public function findObjectByPk(string $table, array $primaryKeys, array $additionalTablesFetch = array(), bool $lazy = false, string $className = null)
1206
    {
1207
        $primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys);
1208
        $hash = $this->getObjectHash($primaryKeys);
1209
1210
        if ($this->objectStorage->has($table, $hash)) {
1211
            $dbRow = $this->objectStorage->get($table, $hash);
1212
            $bean = $dbRow->getTDBMObject();
1213
            if ($className !== null && !is_a($bean, $className)) {
1214
                throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'");
1215
            }
1216
1217
            return $bean;
1218
        }
1219
1220
        // Are we performing lazy fetching?
1221
        if ($lazy === true) {
1222
            // Can we perform lazy fetching?
1223
            $tables = $this->_getRelatedTablesByInheritance($table);
1224
            // Only allowed if no inheritance.
1225
            if (count($tables) === 1) {
1226
                if ($className === null) {
1227
                    try {
1228
                        $className = $this->getBeanClassName($table);
1229
                    } catch (TDBMInvalidArgumentException $e) {
1230
                        $className = TDBMObject::class;
1231
                    }
1232
                }
1233
1234
                // Let's construct the bean
1235
                if (!isset($this->reflectionClassCache[$className])) {
1236
                    $this->reflectionClassCache[$className] = new \ReflectionClass($className);
1237
                }
1238
                // Let's bypass the constructor when creating the bean!
1239
                $bean = $this->reflectionClassCache[$className]->newInstanceWithoutConstructor();
1240
                /* @var $bean AbstractTDBMObject */
1241
                $bean->_constructLazy($table, $primaryKeys, $this);
1242
1243
                return $bean;
1244
            }
1245
        }
1246
1247
        // Did not find the object in cache? Let's query it!
1248
        return $this->findObjectOrFail($table, $primaryKeys, [], $additionalTablesFetch, $className);
1249
    }
1250
1251
    /**
1252
     * Returns a unique bean (or null) according to the filters passed in parameter.
1253
     *
1254
     * @param string            $mainTable             The name of the table queried
1255
     * @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)
1256
     * @param array             $parameters
1257
     * @param array             $additionalTablesFetch
1258
     * @param string            $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
1259
     *
1260
     * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters
1261
     *
1262
     * @throws TDBMException
1263
     */
1264
    public function findObject(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null)
1265
    {
1266
        $objects = $this->findObjects($mainTable, $filter, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className);
1267
        $page = $objects->take(0, 2);
1268
1269
1270
        $pageArr = $page->toArray();
1271
        // Optimisation: the $page->count() query can trigger an additional SQL query in platforms other than MySQL.
1272
        // We try to avoid calling at by fetching all 2 columns instead.
1273
        $count = count($pageArr);
1274
1275
        if ($count > 1) {
1276
            throw new DuplicateRowException("Error while querying an object for table '$mainTable': More than 1 row have been returned, but we should have received at most one.");
1277
        } elseif ($count === 0) {
1278
            return;
1279
        }
1280
1281
        return $pageArr[0];
1282
    }
1283
1284
    /**
1285
     * Returns a unique bean (or null) according to the filters passed in parameter.
1286
     *
1287
     * @param string            $mainTable  The name of the table queried
1288
     * @param string            $from       The from sql statement
1289
     * @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)
1290
     * @param array             $parameters
1291
     * @param string            $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
1292
     *
1293
     * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters
1294
     *
1295
     * @throws TDBMException
1296
     */
1297
    public function findObjectFromSql($mainTable, $from, $filter = null, array $parameters = array(), $className = null)
1298
    {
1299
        $objects = $this->findObjectsFromSql($mainTable, $from, $filter, $parameters, null, self::MODE_ARRAY, $className);
1300
        $page = $objects->take(0, 2);
1301
        $count = $page->count();
1302
        if ($count > 1) {
1303
            throw new DuplicateRowException("Error while querying an object for table '$mainTable': More than 1 row have been returned, but we should have received at most one.");
1304
        } elseif ($count === 0) {
1305
            return;
1306
        }
1307
1308
        return $page[0];
1309
    }
1310
1311
    /**
1312
     * @param string $mainTable
1313
     * @param string $sql
1314
     * @param array $parameters
1315
     * @param $mode
1316
     * @param string|null $className
1317
     * @param string $sqlCount
1318
     *
1319
     * @return ResultIterator
1320
     *
1321
     * @throws TDBMException
1322
     */
1323
    public function findObjectsFromRawSql(string $mainTable, string $sql, array $parameters = array(), $mode, string $className = null, string $sqlCount = null)
1324
    {
1325
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1326
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1327
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1328
        }
1329
1330
        $mode = $mode ?: $this->mode;
1331
1332
        $queryFactory = new FindObjectsFromRawSqlQueryFactory($this, $this->tdbmSchemaAnalyzer->getSchema(), $mainTable, $sql, $sqlCount);
1333
1334
        return new ResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1335
    }
1336
1337
    /**
1338
     * Returns a unique bean according to the filters passed in parameter.
1339
     * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1340
     *
1341
     * @param string            $mainTable             The name of the table queried
1342
     * @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)
1343
     * @param array             $parameters
1344
     * @param array             $additionalTablesFetch
1345
     * @param string            $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
1346
     *
1347
     * @return AbstractTDBMObject The object we want
1348
     *
1349
     * @throws TDBMException
1350
     */
1351
    public function findObjectOrFail(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null)
1352
    {
1353
        $bean = $this->findObject($mainTable, $filter, $parameters, $additionalTablesFetch, $className);
1354
        if ($bean === null) {
1355
            throw new NoBeanFoundException("No result found for query on table '".$mainTable."'");
1356
        }
1357
1358
        return $bean;
1359
    }
1360
1361
    /**
1362
     * @param array $beanData An array of data: array<table, array<column, value>>
1363
     *
1364
     * @return array an array with first item = class name, second item = table name and third item = list of tables needed
1365
     *
1366
     * @throws TDBMInheritanceException
1367
     */
1368
    public function _getClassNameFromBeanData(array $beanData)
1369
    {
1370
        if (count($beanData) === 1) {
1371
            $tableName = array_keys($beanData)[0];
1372
            $allTables = [$tableName];
1373
        } else {
1374
            $tables = [];
1375
            foreach ($beanData as $table => $row) {
1376
                $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1377
                $pkSet = false;
1378
                foreach ($primaryKeyColumns as $columnName) {
1379
                    if ($row[$columnName] !== null) {
1380
                        $pkSet = true;
1381
                        break;
1382
                    }
1383
                }
1384
                if ($pkSet) {
1385
                    $tables[] = $table;
1386
                }
1387
            }
1388
1389
            // $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1390
            try {
1391
                $allTables = $this->_getLinkBetweenInheritedTables($tables);
1392
            } catch (TDBMInheritanceException $e) {
1393
                throw TDBMInheritanceException::extendException($e, $this, $beanData);
1394
            }
1395
            $tableName = $allTables[0];
1396
        }
1397
1398
        // Only one table in this bean. Life is sweat, let's look at its type:
1399
        try {
1400
            $className = $this->getBeanClassName($tableName);
1401
        } catch (TDBMInvalidArgumentException $e) {
1402
            $className = 'TheCodingMachine\\TDBM\\TDBMObject';
1403
        }
1404
1405
        return [$className, $tableName, $allTables];
1406
    }
1407
1408
    /**
1409
     * Returns an item from cache or computes it using $closure and puts it in cache.
1410
     *
1411
     * @param string   $key
1412
     * @param callable $closure
1413
     *
1414
     * @return mixed
1415
     */
1416
    private function fromCache(string $key, callable $closure)
1417
    {
1418
        $item = $this->cache->fetch($key);
1419
        if ($item === false) {
1420
            $item = $closure();
1421
            $this->cache->save($key, $item);
1422
        }
1423
1424
        return $item;
1425
    }
1426
1427
    /**
1428
     * Returns the foreign key object.
1429
     *
1430
     * @param string $table
1431
     * @param string $fkName
1432
     *
1433
     * @return ForeignKeyConstraint
1434
     */
1435
    public function _getForeignKeyByName(string $table, string $fkName)
1436
    {
1437
        return $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getForeignKey($fkName);
1438
    }
1439
1440
    /**
1441
     * @param $pivotTableName
1442
     * @param AbstractTDBMObject $bean
1443
     *
1444
     * @return AbstractTDBMObject[]
1445
     */
1446
    public function _getRelatedBeans(string $pivotTableName, AbstractTDBMObject $bean)
1447
    {
1448
        list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $bean);
1449
        /* @var $localFk ForeignKeyConstraint */
1450
        /* @var $remoteFk ForeignKeyConstraint */
1451
        $remoteTable = $remoteFk->getForeignTableName();
1452
1453
        $primaryKeys = $this->getPrimaryKeyValues($bean);
1454
        $columnNames = array_map(function ($name) use ($pivotTableName) {
1455
            return $pivotTableName.'.'.$name;
1456
        }, $localFk->getUnquotedLocalColumns());
1457
1458
        $filter = array_combine($columnNames, $primaryKeys);
1459
1460
        return $this->findObjects($remoteTable, $filter);
1461
    }
1462
1463
    /**
1464
     * @param $pivotTableName
1465
     * @param AbstractTDBMObject $bean The LOCAL bean
1466
     *
1467
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean
1468
     *
1469
     * @throws TDBMException
1470
     */
1471
    private function getPivotTableForeignKeys(string $pivotTableName, AbstractTDBMObject $bean)
1472
    {
1473
        $fks = array_values($this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
1474
        $table1 = $fks[0]->getForeignTableName();
1475
        $table2 = $fks[1]->getForeignTableName();
1476
1477
        $beanTables = array_map(function (DbRow $dbRow) {
1478
            return $dbRow->_getDbTableName();
1479
        }, $bean->_getDbRows());
1480
1481
        if (in_array($table1, $beanTables)) {
1482
            return [$fks[0], $fks[1]];
1483
        } elseif (in_array($table2, $beanTables)) {
1484
            return [$fks[1], $fks[0]];
1485
        } else {
1486
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2} for pivot table {$pivotTableName}");
1487
        }
1488
    }
1489
1490
    /**
1491
     * Returns a list of pivot tables linked to $bean.
1492
     *
1493
     * @param AbstractTDBMObject $bean
1494
     *
1495
     * @return string[]
1496
     */
1497
    public function _getPivotTablesLinkedToBean(AbstractTDBMObject $bean)
1498
    {
1499
        $junctionTables = [];
1500
        $allJunctionTables = $this->schemaAnalyzer->detectJunctionTables(true);
1501
        foreach ($bean->_getDbRows() as $dbRow) {
1502
            foreach ($allJunctionTables as $table) {
1503
                // There are exactly 2 FKs since this is a pivot table.
1504
                $fks = array_values($table->getForeignKeys());
1505
1506
                if ($fks[0]->getForeignTableName() === $dbRow->_getDbTableName() || $fks[1]->getForeignTableName() === $dbRow->_getDbTableName()) {
1507
                    $junctionTables[] = $table->getName();
1508
                }
1509
            }
1510
        }
1511
1512
        return $junctionTables;
1513
    }
1514
1515
    /**
1516
     * Array of types for tables.
1517
     * Key: table name
1518
     * Value: array of types indexed by column.
1519
     *
1520
     * @var array[]
1521
     */
1522
    private $typesForTable = [];
1523
1524
    /**
1525
     * @internal
1526
     *
1527
     * @param string $tableName
1528
     *
1529
     * @return Type[]
1530
     */
1531
    public function _getColumnTypesForTable(string $tableName)
1532
    {
1533
        if (!isset($this->typesForTable[$tableName])) {
1534
            $columns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getColumns();
1535
            foreach ($columns as $column) {
1536
                $this->typesForTable[$tableName][$column->getName()] = $column->getType();
1537
            }
1538
        }
1539
1540
        return $this->typesForTable[$tableName];
1541
    }
1542
1543
    /**
1544
     * Sets the minimum log level.
1545
     * $level must be one of Psr\Log\LogLevel::xxx.
1546
     *
1547
     * Defaults to LogLevel::WARNING
1548
     *
1549
     * @param string $level
1550
     */
1551
    public function setLogLevel(string $level)
1552
    {
1553
        $this->logger = new LevelFilter($this->rootLogger, $level);
1554
    }
1555
}
1556