Completed
Pull Request — 4.3 (#149)
by Dorian
13:16
created

TDBMService::attach()   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 1
1
<?php
2
3
/*
4
 Copyright (C) 2006-2016 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 Mouf\Database\TDBM;
22
23
use Doctrine\Common\Cache\Cache;
24
use Doctrine\Common\Cache\VoidCache;
25
use Doctrine\DBAL\Connection;
26
use Doctrine\DBAL\Schema\Column;
27
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
28
use Doctrine\DBAL\Schema\Schema;
29
use Doctrine\DBAL\Schema\Table;
30
use Doctrine\DBAL\Types\Type;
31
use Mouf\Database\MagicQuery;
32
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
33
use Mouf\Database\TDBM\QueryFactory\FindObjectsFromSqlQueryFactory;
34
use Mouf\Database\TDBM\QueryFactory\FindObjectsQueryFactory;
35
use Mouf\Database\TDBM\QueryFactory\FindObjectsFromRawSqlQueryFactory;
36
use Mouf\Database\TDBM\Utils\NamingStrategyInterface;
37
use Mouf\Database\TDBM\Utils\TDBMDaoGenerator;
38
use Phlib\Logger\LevelFilter;
39
use Psr\Log\LoggerInterface;
40
use Psr\Log\LogLevel;
41
use Psr\Log\NullLogger;
42
43
/**
44
 * The TDBMService class is the main TDBM class. It provides methods to retrieve TDBMObject instances
45
 * from the database.
46
 *
47
 * @author David Negrier
48
 * @ExtendedAction {"name":"Generate DAOs", "url":"tdbmadmin/", "default":false}
49
 */
50
class TDBMService
51
{
52
    const MODE_CURSOR = 1;
53
    const MODE_ARRAY = 2;
54
55
    /**
56
     * The database connection.
57
     *
58
     * @var Connection
59
     */
60
    private $connection;
61
62
    /**
63
     * @var SchemaAnalyzer
64
     */
65
    private $schemaAnalyzer;
66
67
    /**
68
     * @var MagicQuery
69
     */
70
    private $magicQuery;
71
72
    /**
73
     * @var TDBMSchemaAnalyzer
74
     */
75
    private $tdbmSchemaAnalyzer;
76
77
    /**
78
     * @var string
79
     */
80
    private $cachePrefix;
81
82
    /**
83
     * Cache of table of primary keys.
84
     * Primary keys are stored by tables, as an array of column.
85
     * For instance $primary_key['my_table'][0] will return the first column of the primary key of table 'my_table'.
86
     *
87
     * @var string[]
88
     */
89
    private $primaryKeysColumns;
90
91
    /**
92
     * Service storing objects in memory.
93
     * Access is done by table name and then by primary key.
94
     * If the primary key is split on several columns, access is done by an array of columns, serialized.
95
     *
96
     * @var StandardObjectStorage|WeakrefObjectStorage
97
     */
98
    private $objectStorage;
99
100
    /**
101
     * The fetch mode of the result sets returned by `getObjects`.
102
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY.
103
     *
104
     * In 'MODE_ARRAY' mode (default), the result is an array. Use this mode by default (unless the list returned is very big).
105
     * 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,
106
     * and it cannot be accessed via key. Use this mode for large datasets processed by batch.
107
     * In 'MODE_COMPATIBLE_ARRAY' mode, the result is an old TDBMObjectArray (used up to TDBM 3.2).
108
     * You can access the array by key, or using foreach, several times.
109
     *
110
     * @var int
111
     */
112
    private $mode = self::MODE_ARRAY;
113
114
    /**
115
     * Table of new objects not yet inserted in database or objects modified that must be saved.
116
     *
117
     * @var \SplObjectStorage of DbRow objects
118
     */
119
    private $toSaveObjects;
120
121
    /**
122
     * A cache service to be used.
123
     *
124
     * @var Cache|null
125
     */
126
    private $cache;
127
128
    /**
129
     * Map associating a table name to a fully qualified Bean class name.
130
     *
131
     * @var array
132
     */
133
    private $tableToBeanMap = [];
134
135
    /**
136
     * @var \ReflectionClass[]
137
     */
138
    private $reflectionClassCache = array();
139
140
    /**
141
     * @var LoggerInterface
142
     */
143
    private $rootLogger;
144
145
    /**
146
     * @var LevelFilter|NullLogger
147
     */
148
    private $logger;
149
150
    /**
151
     * @var OrderByAnalyzer
152
     */
153
    private $orderByAnalyzer;
154
155
    /**
156
     * @var string
157
     */
158
    private $beanNamespace;
159
160
    /**
161
     * @var NamingStrategyInterface
162
     */
163
    private $namingStrategy;
164
    /**
165
     * @var ConfigurationInterface
166
     */
167
    private $configuration;
168
169
    /**
170
     * @param ConfigurationInterface $configuration The configuration object
171
     */
172
    public function __construct(ConfigurationInterface $configuration)
173
    {
174
        if (extension_loaded('weakref')) {
175
            $this->objectStorage = new WeakrefObjectStorage();
176
        } else {
177
            $this->objectStorage = new StandardObjectStorage();
178
        }
179
        $this->connection = $configuration->getConnection();
180
        $this->cache = $configuration->getCache();
181
        $this->schemaAnalyzer = $configuration->getSchemaAnalyzer();
182
183
        $this->magicQuery = new MagicQuery($this->connection, $this->cache, $this->schemaAnalyzer);
184
185
        $this->tdbmSchemaAnalyzer = new TDBMSchemaAnalyzer($this->connection, $this->cache, $this->schemaAnalyzer);
186
        $this->cachePrefix = $this->tdbmSchemaAnalyzer->getCachePrefix();
187
188
        $this->toSaveObjects = new \SplObjectStorage();
189
        $logger = $configuration->getLogger();
190
        if ($logger === null) {
191
            $this->logger = new NullLogger();
192
            $this->rootLogger = new NullLogger();
193
        } else {
194
            $this->rootLogger = $logger;
195
            $this->setLogLevel(LogLevel::WARNING);
196
        }
197
        $this->orderByAnalyzer = new OrderByAnalyzer($this->cache, $this->cachePrefix);
198
        $this->beanNamespace = $configuration->getBeanNamespace();
199
        $this->namingStrategy = $configuration->getNamingStrategy();
200
        $this->configuration = $configuration;
201
    }
202
203
    /**
204
     * Returns the object used to connect to the database.
205
     *
206
     * @return Connection
207
     */
208
    public function getConnection(): Connection
209
    {
210
        return $this->connection;
211
    }
212
213
    /**
214
     * Sets the default fetch mode of the result sets returned by `findObjects`.
215
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY.
216
     *
217
     * 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).
218
     * 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
219
     * several times. In cursor mode, you cannot access the result set by key. Use this mode for large datasets processed by batch.
220
     *
221
     * @param int $mode
222
     *
223
     * @return $this
224
     *
225
     * @throws TDBMException
226
     */
227
    public function setFetchMode($mode)
228
    {
229
        if ($mode !== self::MODE_CURSOR && $mode !== self::MODE_ARRAY) {
230
            throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
231
        }
232
        $this->mode = $mode;
233
234
        return $this;
235
    }
236
237
    /**
238
     * Removes the given object from database.
239
     * This cannot be called on an object that is not attached to this TDBMService
240
     * (will throw a TDBMInvalidOperationException).
241
     *
242
     * @param AbstractTDBMObject $object the object to delete
243
     *
244
     * @throws TDBMException
245
     * @throws TDBMInvalidOperationException
246
     */
247
    public function delete(AbstractTDBMObject $object)
248
    {
249
        switch ($object->_getStatus()) {
250
            case TDBMObjectStateEnum::STATE_DELETED:
251
                // Nothing to do, object already deleted.
252
                return;
253
            case TDBMObjectStateEnum::STATE_DETACHED:
254
                throw new TDBMInvalidOperationException('Cannot delete a detached object');
255
            case TDBMObjectStateEnum::STATE_NEW:
256
                $this->deleteManyToManyRelationships($object);
257
                foreach ($object->_getDbRows() as $dbRow) {
258
                    $this->removeFromToSaveObjectList($dbRow);
259
                }
260
                break;
261
            case TDBMObjectStateEnum::STATE_DIRTY:
262
                foreach ($object->_getDbRows() as $dbRow) {
263
                    $this->removeFromToSaveObjectList($dbRow);
264
                }
265
                // 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...
266
            case TDBMObjectStateEnum::STATE_NOT_LOADED:
267
            case TDBMObjectStateEnum::STATE_LOADED:
268
                $this->deleteManyToManyRelationships($object);
269
                // Let's delete db rows, in reverse order.
270
                foreach (array_reverse($object->_getDbRows()) as $dbRow) {
271
                    $tableName = $dbRow->_getDbTableName();
272
                    $primaryKeys = $dbRow->_getPrimaryKeys();
273
                    $this->connection->delete($tableName, $primaryKeys);
274
                    $this->objectStorage->remove($dbRow->_getDbTableName(), $this->getObjectHash($primaryKeys));
275
                }
276
                break;
277
            // @codeCoverageIgnoreStart
278
            default:
279
                throw new TDBMInvalidOperationException('Unexpected status for bean');
280
            // @codeCoverageIgnoreEnd
281
        }
282
283
        $object->_setStatus(TDBMObjectStateEnum::STATE_DELETED);
284
    }
285
286
    /**
287
     * Removes all many to many relationships for this object.
288
     *
289
     * @param AbstractTDBMObject $object
290
     */
291
    private function deleteManyToManyRelationships(AbstractTDBMObject $object)
292
    {
293
        foreach ($object->_getDbRows() as $tableName => $dbRow) {
294
            $pivotTables = $this->tdbmSchemaAnalyzer->getPivotTableLinkedToTable($tableName);
295
            foreach ($pivotTables as $pivotTable) {
296
                $remoteBeans = $object->_getRelationships($pivotTable);
297
                foreach ($remoteBeans as $remoteBean) {
298
                    $object->_removeRelationship($pivotTable, $remoteBean);
299
                }
300
            }
301
        }
302
        $this->persistManyToManyRelationships($object);
303
    }
304
305
    /**
306
     * This function removes the given object from the database. It will also remove all objects relied to the one given
307
     * by parameter before all.
308
     *
309
     * Notice: if the object has a multiple primary key, the function will not work.
310
     *
311
     * @param AbstractTDBMObject $objToDelete
312
     */
313
    public function deleteCascade(AbstractTDBMObject $objToDelete)
314
    {
315
        $this->deleteAllConstraintWithThisObject($objToDelete);
316
        $this->delete($objToDelete);
317
    }
318
319
    /**
320
     * This function is used only in TDBMService (private function)
321
     * It will call deleteCascade function foreach object relied with a foreign key to the object given by parameter.
322
     *
323
     * @param AbstractTDBMObject $obj
324
     */
325
    private function deleteAllConstraintWithThisObject(AbstractTDBMObject $obj)
326
    {
327
        $dbRows = $obj->_getDbRows();
328
        foreach ($dbRows as $dbRow) {
329
            $tableName = $dbRow->_getDbTableName();
330
            $pks = array_values($dbRow->_getPrimaryKeys());
331
            if (!empty($pks)) {
332
                $incomingFks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($tableName);
333
334
                foreach ($incomingFks as $incomingFk) {
335
                    $filter = array_combine($incomingFk->getLocalColumns(), $pks);
336
337
                    $results = $this->findObjects($incomingFk->getLocalTableName(), $filter);
338
339
                    foreach ($results as $bean) {
340
                        $this->deleteCascade($bean);
341
                    }
342
                }
343
            }
344
        }
345
    }
346
347
    /**
348
     * This function performs a save() of all the objects that have been modified.
349
     */
350
    public function completeSave()
351
    {
352
        foreach ($this->toSaveObjects as $dbRow) {
353
            $this->save($dbRow->getTDBMObject());
354
        }
355
    }
356
357
    /**
358
     * Takes in input a filter_bag (which can be about anything from a string to an array of TDBMObjects... see above from documentation),
359
     * and gives back a proper Filter object.
360
     *
361
     * @param mixed $filter_bag
362
     * @param int   $counter
363
     *
364
     * @return array First item: filter string, second item: parameters
365
     *
366
     * @throws TDBMException
367
     */
368
    public function buildFilterFromFilterBag($filter_bag, $counter = 1)
369
    {
370
        if ($filter_bag === null) {
371
            return ['', []];
372
        } elseif (is_string($filter_bag)) {
373
            return [$filter_bag, []];
374
        } elseif (is_array($filter_bag)) {
375
            $sqlParts = [];
376
            $parameters = [];
377
            foreach ($filter_bag as $column => $value) {
378
                if (is_int($column)) {
379
                    list($subSqlPart, $subParameters) = $this->buildFilterFromFilterBag($value, $counter);
380
                    $sqlParts[] = $subSqlPart;
381
                    $parameters += $subParameters;
382
                } else {
383
                    $paramName = 'tdbmparam'.$counter;
384
                    if (is_array($value)) {
385
                        $sqlParts[] = $this->connection->quoteIdentifier($column).' IN :'.$paramName;
386
                    } else {
387
                        $sqlParts[] = $this->connection->quoteIdentifier($column).' = :'.$paramName;
388
                    }
389
                    $parameters[$paramName] = $value;
390
                    ++$counter;
391
                }
392
            }
393
394
            return [implode(' AND ', $sqlParts), $parameters];
395
        } elseif ($filter_bag instanceof AbstractTDBMObject) {
396
            $sqlParts = [];
397
            $parameters = [];
398
            $dbRows = $filter_bag->_getDbRows();
399
            $dbRow = reset($dbRows);
400
            $primaryKeys = $dbRow->_getPrimaryKeys();
401
402
            foreach ($primaryKeys as $column => $value) {
403
                $paramName = 'tdbmparam'.$counter;
404
                $sqlParts[] = $this->connection->quoteIdentifier($dbRow->_getDbTableName()).'.'.$this->connection->quoteIdentifier($column).' = :'.$paramName;
405
                $parameters[$paramName] = $value;
406
                ++$counter;
407
            }
408
409
            return [implode(' AND ', $sqlParts), $parameters];
410
        } elseif ($filter_bag instanceof \Iterator) {
411
            return $this->buildFilterFromFilterBag(iterator_to_array($filter_bag), $counter);
412
        } else {
413
            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.');
414
        }
415
    }
416
417
    /**
418
     * @param string $table
419
     *
420
     * @return string[]
421
     */
422
    public function getPrimaryKeyColumns($table)
423
    {
424
        if (!isset($this->primaryKeysColumns[$table])) {
425
            $this->primaryKeysColumns[$table] = $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKeyColumns();
426
427
            // TODO TDBM4: See if we need to improve error reporting if table name does not exist.
428
429
            /*$arr = array();
430
            foreach ($this->connection->getPrimaryKey($table) as $col) {
431
                $arr[] = $col->name;
432
            }
433
            // The primaryKeysColumns contains only the column's name, not the DB_Column object.
434
            $this->primaryKeysColumns[$table] = $arr;
435
            if (empty($this->primaryKeysColumns[$table]))
436
            {
437
                // Unable to find primary key.... this is an error
438
                // Let's try to be precise in error reporting. Let's try to find the table.
439
                $tables = $this->connection->checkTableExist($table);
440
                if ($tables === true)
441
                throw new TDBMException("Could not find table primary key for table '$table'. Please define a primary key for this table.");
442
                elseif ($tables !== null) {
443
                    if (count($tables)==1)
444
                    $str = "Could not find table '$table'. Maybe you meant this table: '".$tables[0]."'";
445
                    else
446
                    $str = "Could not find table '$table'. Maybe you meant one of those tables: '".implode("', '",$tables)."'";
447
                    throw new TDBMException($str);
448
                }
449
            }*/
450
        }
451
452
        return $this->primaryKeysColumns[$table];
453
    }
454
455
    /**
456
     * This is an internal function, you should not use it in your application.
457
     * This is used internally by TDBM to add an object to the object cache.
458
     *
459
     * @param DbRow $dbRow
460
     */
461
    public function _addToCache(DbRow $dbRow)
462
    {
463
        $primaryKey = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
464
        $hash = $this->getObjectHash($primaryKey);
465
        $this->objectStorage->set($dbRow->_getDbTableName(), $hash, $dbRow);
466
    }
467
468
    /**
469
     * This is an internal function, you should not use it in your application.
470
     * This is used internally by TDBM to remove the object from the list of objects that have been
471
     * created/updated but not saved yet.
472
     *
473
     * @param DbRow $myObject
474
     */
475
    private function removeFromToSaveObjectList(DbRow $myObject)
476
    {
477
        unset($this->toSaveObjects[$myObject]);
478
    }
479
480
    /**
481
     * This is an internal function, you should not use it in your application.
482
     * This is used internally by TDBM to add an object to the list of objects that have been
483
     * created/updated but not saved yet.
484
     *
485
     * @param DbRow $myObject
486
     */
487
    public function _addToToSaveObjectList(DbRow $myObject)
488
    {
489
        $this->toSaveObjects[$myObject] = true;
490
    }
491
492
    /**
493
     * Generates all the daos and beans.
494
     *
495
     * @return \string[] the list of tables (key) and bean name (value)
496
     */
497
    public function generateAllDaosAndBeans()
498
    {
499
        // Purge cache before generating anything.
500
        $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...
501
502
        $tdbmDaoGenerator = new TDBMDaoGenerator($this->configuration, $this->tdbmSchemaAnalyzer);
503
        $tdbmDaoGenerator->generateAllDaosAndBeans();
504
    }
505
506
    /**
507
     * Returns the fully qualified class name of the bean associated with table $tableName.
508
     *
509
     *
510
     * @param string $tableName
511
     *
512
     * @return string
513
     */
514
    public function getBeanClassName(string $tableName) : string
515
    {
516
        if (isset($this->tableToBeanMap[$tableName])) {
517
            return $this->tableToBeanMap[$tableName];
518
        } else {
519
            $className = $this->beanNamespace.'\\'.$this->namingStrategy->getBeanClassName($tableName);
520
521
            if (!class_exists($className)) {
522
                throw new TDBMInvalidArgumentException(sprintf('Could not find class "%s". Does table "%s" exist? If yes, consider regenerating the DAOs and beans.', $className, $tableName));
523
            }
524
525
            $this->tableToBeanMap[$tableName] = $className;
526
            return $className;
527
        }
528
    }
529
530
    /**
531
     * Saves $object by INSERTing or UPDAT(E)ing it in the database.
532
     *
533
     * @param AbstractTDBMObject $object
534
     *
535
     * @throws TDBMException
536
     */
537
    public function save(AbstractTDBMObject $object)
538
    {
539
        $status = $object->_getStatus();
540
541
        if ($status === null) {
542
            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)));
543
        }
544
545
        // Let's attach this object if it is in detached state.
546
        if ($status === TDBMObjectStateEnum::STATE_DETACHED) {
547
            $object->_attach($this);
548
            $status = $object->_getStatus();
549
        }
550
551
        if ($status === TDBMObjectStateEnum::STATE_NEW) {
552
            $dbRows = $object->_getDbRows();
553
554
            $unindexedPrimaryKeys = array();
555
556
            foreach ($dbRows as $dbRow) {
557
                if ($dbRow->_getStatus() == TDBMObjectStateEnum::STATE_SAVING) {
558
                    throw TDBMCyclicReferenceException::createCyclicReference($dbRow->_getDbTableName(), $object);
559
                }
560
                $dbRow->_setStatus(TDBMObjectStateEnum::STATE_SAVING);
561
                $tableName = $dbRow->_getDbTableName();
562
563
                $schema = $this->tdbmSchemaAnalyzer->getSchema();
564
                $tableDescriptor = $schema->getTable($tableName);
565
566
                $primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
567
568
                $references = $dbRow->_getReferences();
569
570
                // Let's save all references in NEW or DETACHED state (we need their primary key)
571
                foreach ($references as $fkName => $reference) {
572
                    if ($reference !== null) {
573
                        $refStatus = $reference->_getStatus();
574
                        if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
575
                            try {
576
                                $this->save($reference);
577
                            } catch (TDBMCyclicReferenceException $e) {
578
                                throw TDBMCyclicReferenceException::extendCyclicReference($e, $dbRow->_getDbTableName(), $object, $fkName);
579
                            }
580
                        }
581
                    }
582
                }
583
584
                if (empty($unindexedPrimaryKeys)) {
585
                    $primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
586
                } else {
587
                    // First insert, the children must have the same primary key as the parent.
588
                    $primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
589
                    $dbRow->_setPrimaryKeys($primaryKeys);
590
                }
591
592
                $dbRowData = $dbRow->_getDbRow();
593
594
                // Let's see if the columns for primary key have been set before inserting.
595
                // We assume that if one of the value of the PK has been set, the PK is set.
596
                $isPkSet = !empty($primaryKeys);
597
598
                /*if (!$isPkSet) {
599
                    // if there is no autoincrement and no pk set, let's go in error.
600
                    $isAutoIncrement = true;
601
602
                    foreach ($primaryKeyColumns as $pkColumnName) {
603
                        $pkColumn = $tableDescriptor->getColumn($pkColumnName);
604
                        if (!$pkColumn->getAutoincrement()) {
605
                            $isAutoIncrement = false;
606
                        }
607
                    }
608
609
                    if (!$isAutoIncrement) {
610
                        $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.";
611
                        throw new TDBMException($msg);
612
                    }
613
614
                }*/
615
616
                $types = [];
617
                $escapedDbRowData = [];
618
619 View Code Duplication
                foreach ($dbRowData as $columnName => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
620
                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
621
                    $types[] = $columnDescriptor->getType();
622
                    $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
623
                }
624
625
                $this->connection->insert($tableName, $escapedDbRowData, $types);
626
627
                if (!$isPkSet && count($primaryKeyColumns) == 1) {
628
                    $id = $this->connection->lastInsertId();
629
                    $pkColumn = $primaryKeyColumns[0];
630
                    // lastInsertId returns a string but the column type is usually a int. Let's convert it back to the correct type.
631
                    $id = $tableDescriptor->getColumn($pkColumn)->getType()->convertToPHPValue($id, $this->getConnection()->getDatabasePlatform());
632
                    $primaryKeys[$pkColumn] = $id;
633
                }
634
635
                // TODO: change this to some private magic accessor in future
636
                $dbRow->_setPrimaryKeys($primaryKeys);
637
                $unindexedPrimaryKeys = array_values($primaryKeys);
638
639
                /*
640
                 * When attached, on "save", we check if the column updated is part of a primary key
641
                 * If this is part of a primary key, we call the _update_id method that updates the id in the list of known objects.
642
                 * This method should first verify that the id is not already used (and is not auto-incremented)
643
                 *
644
                 * In the object, the key is stored in an array of  (column => value), that can be directly used to update the record.
645
                 *
646
                 *
647
                 */
648
649
                /*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...
650
                    $this->db_connection->exec($sql);
651
                } catch (TDBMException $e) {
652
                    $this->db_onerror = true;
653
654
                    // Strange..... if we do not have the line below, bad inserts are not catched.
655
                    // It seems that destructors are called before the registered shutdown function (PHP >=5.0.5)
656
                    //if ($this->tdbmService->isProgramExiting())
657
                    //	trigger_error("program exiting");
658
                    trigger_error($e->getMessage(), E_USER_ERROR);
659
660
                    if (!$this->tdbmService->isProgramExiting())
661
                        throw $e;
662
                    else
663
                    {
664
                        trigger_error($e->getMessage(), E_USER_ERROR);
665
                    }
666
                }*/
667
668
                // Let's remove this object from the $new_objects static table.
669
                $this->removeFromToSaveObjectList($dbRow);
670
671
                // TODO: change this behaviour to something more sensible performance-wise
672
                // Maybe a setting to trigger this globally?
673
                //$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...
674
                //$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...
675
                //$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...
676
677
                // Let's add this object to the list of objects in cache.
678
                $this->_addToCache($dbRow);
679
            }
680
681
            $object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
682
        } elseif ($status === TDBMObjectStateEnum::STATE_DIRTY) {
683
            $dbRows = $object->_getDbRows();
684
685
            foreach ($dbRows as $dbRow) {
686
                $references = $dbRow->_getReferences();
687
688
                // Let's save all references in NEW state (we need their primary key)
689
                foreach ($references as $fkName => $reference) {
690
                    if ($reference !== null && $reference->_getStatus() === TDBMObjectStateEnum::STATE_NEW) {
691
                        $this->save($reference);
692
                    }
693
                }
694
695
                // Let's first get the primary keys
696
                $tableName = $dbRow->_getDbTableName();
697
                $dbRowData = $dbRow->_getDbRow();
698
699
                $schema = $this->tdbmSchemaAnalyzer->getSchema();
700
                $tableDescriptor = $schema->getTable($tableName);
701
702
                $primaryKeys = $dbRow->_getPrimaryKeys();
703
704
                $types = [];
705
                $escapedDbRowData = [];
706
                $escapedPrimaryKeys = [];
707
708 View Code Duplication
                foreach ($dbRowData as $columnName => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
709
                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
710
                    $types[] = $columnDescriptor->getType();
711
                    $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
712
                }
713 View Code Duplication
                foreach ($primaryKeys as $columnName => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
714
                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
715
                    $types[] = $columnDescriptor->getType();
716
                    $escapedPrimaryKeys[$this->connection->quoteIdentifier($columnName)] = $value;
717
                }
718
719
                $this->connection->update($tableName, $escapedDbRowData, $escapedPrimaryKeys, $types);
720
721
                // Let's check if the primary key has been updated...
722
                $needsUpdatePk = false;
723
                foreach ($primaryKeys as $column => $value) {
724
                    if (!isset($dbRowData[$column]) || $dbRowData[$column] != $value) {
725
                        $needsUpdatePk = true;
726
                        break;
727
                    }
728
                }
729
                if ($needsUpdatePk) {
730
                    $this->objectStorage->remove($tableName, $this->getObjectHash($primaryKeys));
731
                    $newPrimaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
732
                    $dbRow->_setPrimaryKeys($newPrimaryKeys);
733
                    $this->objectStorage->set($tableName, $this->getObjectHash($primaryKeys), $dbRow);
734
                }
735
736
                // Let's remove this object from the list of objects to save.
737
                $this->removeFromToSaveObjectList($dbRow);
738
            }
739
740
            $object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
741
        } elseif ($status === TDBMObjectStateEnum::STATE_DELETED) {
742
            throw new TDBMInvalidOperationException('This object has been deleted. It cannot be saved.');
743
        }
744
745
        // Finally, let's save all the many to many relationships to this bean.
746
        $this->persistManyToManyRelationships($object);
747
    }
748
749
    private function persistManyToManyRelationships(AbstractTDBMObject $object)
750
    {
751
        foreach ($object->_getCachedRelationships() as $pivotTableName => $storage) {
752
            $tableDescriptor = $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
753
            list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $object);
754
755
            $toRemoveFromStorage = [];
756
757
            foreach ($storage as $remoteBean) {
758
                /* @var $remoteBean AbstractTDBMObject */
759
                $statusArr = $storage[$remoteBean];
760
                $status = $statusArr['status'];
761
                $reverse = $statusArr['reverse'];
762
                if ($reverse) {
763
                    continue;
764
                }
765
766
                if ($status === 'new') {
767
                    $remoteBeanStatus = $remoteBean->_getStatus();
768
                    if ($remoteBeanStatus === TDBMObjectStateEnum::STATE_NEW || $remoteBeanStatus === TDBMObjectStateEnum::STATE_DETACHED) {
769
                        // Let's save remote bean if needed.
770
                        $this->save($remoteBean);
771
                    }
772
773
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
774
775
                    $types = [];
776
                    $escapedFilters = [];
777
778 View Code Duplication
                    foreach ($filters as $columnName => $value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
779
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
780
                        $types[] = $columnDescriptor->getType();
781
                        $escapedFilters[$this->connection->quoteIdentifier($columnName)] = $value;
782
                    }
783
784
                    $this->connection->insert($pivotTableName, $escapedFilters, $types);
785
786
                    // Finally, let's mark relationships as saved.
787
                    $statusArr['status'] = 'loaded';
788
                    $storage[$remoteBean] = $statusArr;
789
                    $remoteStorage = $remoteBean->_getCachedRelationships()[$pivotTableName];
790
                    $remoteStatusArr = $remoteStorage[$object];
791
                    $remoteStatusArr['status'] = 'loaded';
792
                    $remoteStorage[$object] = $remoteStatusArr;
793
                } elseif ($status === 'delete') {
794
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
795
796
                    $types = [];
797
798
                    foreach ($filters as $columnName => $value) {
799
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
800
                        $types[] = $columnDescriptor->getType();
801
                    }
802
803
                    $this->connection->delete($pivotTableName, $filters, $types);
804
805
                    // Finally, let's remove relationships completely from bean.
806
                    $toRemoveFromStorage[] = $remoteBean;
807
808
                    $remoteBean->_getCachedRelationships()[$pivotTableName]->detach($object);
809
                }
810
            }
811
812
            // Note: due to https://bugs.php.net/bug.php?id=65629, we cannot delete an element inside a foreach loop on a SplStorageObject.
813
            // Therefore, we cache elements in the $toRemoveFromStorage to remove them at a later stage.
814
            foreach ($toRemoveFromStorage as $remoteBean) {
815
                $storage->detach($remoteBean);
816
            }
817
        }
818
    }
819
820
    private function getPivotFilters(AbstractTDBMObject $localBean, AbstractTDBMObject $remoteBean, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk)
821
    {
822
        $localBeanPk = $this->getPrimaryKeyValues($localBean);
823
        $remoteBeanPk = $this->getPrimaryKeyValues($remoteBean);
824
        $localColumns = $localFk->getLocalColumns();
825
        $remoteColumns = $remoteFk->getLocalColumns();
826
827
        $localFilters = array_combine($localColumns, $localBeanPk);
828
        $remoteFilters = array_combine($remoteColumns, $remoteBeanPk);
829
830
        return array_merge($localFilters, $remoteFilters);
831
    }
832
833
    /**
834
     * Returns the "values" of the primary key.
835
     * This returns the primary key from the $primaryKey attribute, not the one stored in the columns.
836
     *
837
     * @param AbstractTDBMObject $bean
838
     *
839
     * @return array numerically indexed array of values
840
     */
841
    private function getPrimaryKeyValues(AbstractTDBMObject $bean)
842
    {
843
        $dbRows = $bean->_getDbRows();
844
        $dbRow = reset($dbRows);
845
846
        return array_values($dbRow->_getPrimaryKeys());
847
    }
848
849
    /**
850
     * Returns a unique hash used to store the object based on its primary key.
851
     * If the array contains only one value, then the value is returned.
852
     * Otherwise, a hash representing the array is returned.
853
     *
854
     * @param array $primaryKeys An array of columns => values forming the primary key
855
     *
856
     * @return string
857
     */
858
    public function getObjectHash(array $primaryKeys)
859
    {
860
        if (count($primaryKeys) === 1) {
861
            return reset($primaryKeys);
862
        } else {
863
            ksort($primaryKeys);
864
865
            return md5(json_encode($primaryKeys));
866
        }
867
    }
868
869
    /**
870
     * Returns an array of primary keys from the object.
871
     * The primary keys are extracted from the object columns and not from the primary keys stored in the
872
     * $primaryKeys variable of the object.
873
     *
874
     * @param DbRow $dbRow
875
     *
876
     * @return array Returns an array of column => value
877
     */
878
    public function getPrimaryKeysForObjectFromDbRow(DbRow $dbRow)
879
    {
880
        $table = $dbRow->_getDbTableName();
881
        $dbRowData = $dbRow->_getDbRow();
882
883
        return $this->_getPrimaryKeysFromObjectData($table, $dbRowData);
884
    }
885
886
    /**
887
     * Returns an array of primary keys for the given row.
888
     * The primary keys are extracted from the object columns.
889
     *
890
     * @param $table
891
     * @param array $columns
892
     *
893
     * @return array
894
     */
895
    public function _getPrimaryKeysFromObjectData($table, array $columns)
896
    {
897
        $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
898
        $values = array();
899
        foreach ($primaryKeyColumns as $column) {
0 ignored issues
show
Bug introduced by
The expression $primaryKeyColumns of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
900
            if (isset($columns[$column])) {
901
                $values[$column] = $columns[$column];
902
            }
903
        }
904
905
        return $values;
906
    }
907
908
    /**
909
     * Attaches $object to this TDBMService.
910
     * The $object must be in DETACHED state and will pass in NEW state.
911
     *
912
     * @param AbstractTDBMObject $object
913
     *
914
     * @throws TDBMInvalidOperationException
915
     */
916
    public function attach(AbstractTDBMObject $object)
917
    {
918
        $object->_attach($this);
919
    }
920
921
    /**
922
     * Returns an associative array (column => value) for the primary keys from the table name and an
923
     * indexed array of primary key values.
924
     *
925
     * @param string $tableName
926
     * @param array  $indexedPrimaryKeys
927
     */
928
    public function _getPrimaryKeysFromIndexedPrimaryKeys($tableName, array $indexedPrimaryKeys)
929
    {
930
        $primaryKeyColumns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getPrimaryKeyColumns();
931
932
        if (count($primaryKeyColumns) !== count($indexedPrimaryKeys)) {
933
            throw new TDBMException(sprintf('Wrong number of columns passed for primary key. Expected %s columns for table "%s",
934
			got %s instead.', count($primaryKeyColumns), $tableName, count($indexedPrimaryKeys)));
935
        }
936
937
        return array_combine($primaryKeyColumns, $indexedPrimaryKeys);
938
    }
939
940
    /**
941
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
942
     * Tables must be in a single line of inheritance. The method will find missing tables.
943
     *
944
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
945
     * we must be able to find all other tables.
946
     *
947
     * @param string[] $tables
948
     *
949
     * @return string[]
950
     */
951
    public function _getLinkBetweenInheritedTables(array $tables)
952
    {
953
        sort($tables);
954
955
        return $this->fromCache($this->cachePrefix.'_linkbetweeninheritedtables_'.implode('__split__', $tables),
956
            function () use ($tables) {
957
                return $this->_getLinkBetweenInheritedTablesWithoutCache($tables);
958
            });
959
    }
960
961
    /**
962
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
963
     * Tables must be in a single line of inheritance. The method will find missing tables.
964
     *
965
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
966
     * we must be able to find all other tables.
967
     *
968
     * @param string[] $tables
969
     *
970
     * @return string[]
971
     */
972
    private function _getLinkBetweenInheritedTablesWithoutCache(array $tables)
973
    {
974
        $schemaAnalyzer = $this->schemaAnalyzer;
975
976
        foreach ($tables as $currentTable) {
977
            $allParents = [$currentTable];
978
            while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
979
                $currentTable = $currentFk->getForeignTableName();
980
                $allParents[] = $currentTable;
981
            }
982
983
            // Now, does the $allParents contain all the tables we want?
984
            $notFoundTables = array_diff($tables, $allParents);
985
            if (empty($notFoundTables)) {
986
                // We have a winner!
987
                return $allParents;
988
            }
989
        }
990
991
        throw TDBMInheritanceException::create($tables);
992
    }
993
994
    /**
995
     * Returns the list of tables related to this table (via a parent or child inheritance relationship).
996
     *
997
     * @param string $table
998
     *
999
     * @return string[]
1000
     */
1001
    public function _getRelatedTablesByInheritance($table)
1002
    {
1003
        return $this->fromCache($this->cachePrefix.'_relatedtables_'.$table, function () use ($table) {
1004
            return $this->_getRelatedTablesByInheritanceWithoutCache($table);
1005
        });
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
    private function _getRelatedTablesByInheritanceWithoutCache($table)
1016
    {
1017
        $schemaAnalyzer = $this->schemaAnalyzer;
1018
1019
        // Let's scan the parent tables
1020
        $currentTable = $table;
1021
1022
        $parentTables = [];
1023
1024
        // Get parent relationship
1025
        while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1026
            $currentTable = $currentFk->getForeignTableName();
1027
            $parentTables[] = $currentTable;
1028
        }
1029
1030
        // Let's recurse in children
1031
        $childrenTables = $this->exploreChildrenTablesRelationships($schemaAnalyzer, $table);
1032
1033
        return array_merge(array_reverse($parentTables), $childrenTables);
1034
    }
1035
1036
    /**
1037
     * Explore all the children and descendant of $table and returns ForeignKeyConstraints on those.
1038
     *
1039
     * @param string $table
1040
     *
1041
     * @return string[]
1042
     */
1043
    private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyzer, $table)
1044
    {
1045
        $tables = [$table];
1046
        $keys = $schemaAnalyzer->getChildrenRelationships($table);
1047
1048
        foreach ($keys as $key) {
1049
            $tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, $key->getLocalTableName()));
1050
        }
1051
1052
        return $tables;
1053
    }
1054
1055
    /**
1056
     * Casts a foreign key into SQL, assuming table name is used with no alias.
1057
     * The returned value does contain only one table. For instance:.
1058
     *
1059
     * " LEFT JOIN table2 ON table1.id = table2.table1_id"
1060
     *
1061
     * @param ForeignKeyConstraint $fk
1062
     * @param bool                 $leftTableIsLocal
1063
     *
1064
     * @return string
1065
     */
1066
    /*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...
1067
        $onClauses = [];
1068
        $foreignTableName = $this->connection->quoteIdentifier($fk->getForeignTableName());
1069
        $foreignColumns = $fk->getForeignColumns();
1070
        $localTableName = $this->connection->quoteIdentifier($fk->getLocalTableName());
1071
        $localColumns = $fk->getLocalColumns();
1072
        $columnCount = count($localTableName);
1073
1074
        for ($i = 0; $i < $columnCount; $i++) {
1075
            $onClauses[] = sprintf("%s.%s = %s.%s",
1076
                $localTableName,
1077
                $this->connection->quoteIdentifier($localColumns[$i]),
1078
                $foreignColumns,
1079
                $this->connection->quoteIdentifier($foreignColumns[$i])
1080
                );
1081
        }
1082
1083
        $onClause = implode(' AND ', $onClauses);
1084
1085
        if ($leftTableIsLocal) {
1086
            return sprintf(" LEFT JOIN %s ON (%s)", $foreignTableName, $onClause);
1087
        } else {
1088
            return sprintf(" LEFT JOIN %s ON (%s)", $localTableName, $onClause);
1089
        }
1090
    }*/
1091
1092
    /**
1093
     * Returns a `ResultIterator` object representing filtered records of "$mainTable" .
1094
     *
1095
     * The findObjects method should be the most used query method in TDBM if you want to query the database for objects.
1096
     * (Note: if you want to query the database for an object by its primary key, use the findObjectByPk method).
1097
     *
1098
     * The findObjects method takes in parameter:
1099
     * 	- mainTable: the kind of bean you want to retrieve. In TDBM, a bean matches a database row, so the
1100
     * 			`$mainTable` parameter should be the name of an existing table in database.
1101
     *  - filter: The filter is a filter bag. It is what you use to filter your request (the WHERE part in SQL).
1102
     *          It can be a string (SQL Where clause), or even a bean or an associative array (key = column to filter, value = value to find)
1103
     *  - parameters: The parameters used in the filter. If you pass a SQL string as a filter, be sure to avoid
1104
     *          concatenating parameters in the string (this leads to SQL injection and also to poor caching performance).
1105
     *          Instead, please consider passing parameters (see documentation for more details).
1106
     *  - additionalTablesFetch: An array of SQL tables names. The beans related to those tables will be fetched along
1107
     *          the main table. This is useful to avoid hitting the database with numerous subqueries.
1108
     *  - mode: The fetch mode of the result. See `setFetchMode()` method for more details.
1109
     *
1110
     * The `findObjects` method will return a `ResultIterator`. A `ResultIterator` is an object that behaves as an array
1111
     * (in ARRAY mode) at least. It can be iterated using a `foreach` loop.
1112
     *
1113
     * Finally, if filter_bag is null, the whole table is returned.
1114
     *
1115
     * @param string                       $mainTable             The name of the table queried
1116
     * @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)
1117
     * @param array                        $parameters
1118
     * @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)
1119
     * @param array                        $additionalTablesFetch
1120
     * @param int                          $mode
1121
     * @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
1122
     *
1123
     * @return ResultIterator An object representing an array of results
1124
     *
1125
     * @throws TDBMException
1126
     */
1127
    public function findObjects(string $mainTable, $filter = null, array $parameters = array(), $orderString = null, array $additionalTablesFetch = array(), $mode = null, string $className = null)
1128
    {
1129
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1130
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1131
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1132
        }
1133
1134
        $mode = $mode ?: $this->mode;
1135
1136
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter);
1137
1138
        $parameters = array_merge($parameters, $additionalParameters);
1139
1140
        $queryFactory = new FindObjectsQueryFactory($mainTable, $additionalTablesFetch, $filterString, $orderString, $this, $this->tdbmSchemaAnalyzer->getSchema(), $this->orderByAnalyzer);
1141
1142
        return new ResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1143
    }
1144
1145
    /**
1146
     * @param string                       $mainTable   The name of the table queried
1147
     * @param string                       $from        The from sql statement
1148
     * @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)
1149
     * @param array                        $parameters
1150
     * @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)
1151
     * @param int                          $mode
1152
     * @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
1153
     *
1154
     * @return ResultIterator An object representing an array of results
1155
     *
1156
     * @throws TDBMException
1157
     */
1158
    public function findObjectsFromSql(string $mainTable, string $from, $filter = null, array $parameters = array(), $orderString = null, $mode = null, string $className = null)
1159
    {
1160
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1161
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1162
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1163
        }
1164
1165
        $mode = $mode ?: $this->mode;
1166
1167
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter);
1168
1169
        $parameters = array_merge($parameters, $additionalParameters);
1170
1171
        $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...
1172
1173
        return new ResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1174
    }
1175
1176
    /**
1177
     * @param string $mainTable
1178
     * @param string $sql
1179
     * @param array $parameters
1180
     * @param $mode
1181
     * @param string|null $className
1182
     * @param string $sqlCount
1183
     *
1184
     * @return ResultIterator
1185
     *
1186
     * @throws TDBMException
1187
     */
1188
    public function findObjectsFromRawSql(string $mainTable, string $sql, array $parameters = array(), $mode, string $className = null, string $sqlCount = null)
1189
    {
1190
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1191
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1192
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1193
        }
1194
1195
        $mode = $mode ?: $this->mode;
1196
1197
        $queryFactory = new FindObjectsFromRawSqlQueryFactory($this, $this->tdbmSchemaAnalyzer->getSchema(), $mainTable, $sql, $sqlCount);
1198
1199
        return new ResultIterator($queryFactory, $parameters, $this->objectStorage, $className, $this, $this->magicQuery, $mode, $this->logger);
1200
    }
1201
1202
    /**
1203
     * @param $table
1204
     * @param array  $primaryKeys
1205
     * @param array  $additionalTablesFetch
1206
     * @param bool   $lazy                  Whether to perform lazy loading on this object or not
1207
     * @param string $className
1208
     *
1209
     * @return AbstractTDBMObject
1210
     *
1211
     * @throws TDBMException
1212
     */
1213
    public function findObjectByPk(string $table, array $primaryKeys, array $additionalTablesFetch = array(), bool $lazy = false, string $className = null)
1214
    {
1215
        $primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys);
1216
        $hash = $this->getObjectHash($primaryKeys);
1217
1218
        if ($this->objectStorage->has($table, $hash)) {
1219
            $dbRow = $this->objectStorage->get($table, $hash);
1220
            $bean = $dbRow->getTDBMObject();
1221
            if ($className !== null && !is_a($bean, $className)) {
1222
                throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'");
1223
            }
1224
1225
            return $bean;
1226
        }
1227
1228
        // Are we performing lazy fetching?
1229
        if ($lazy === true) {
1230
            // Can we perform lazy fetching?
1231
            $tables = $this->_getRelatedTablesByInheritance($table);
1232
            // Only allowed if no inheritance.
1233
            if (count($tables) === 1) {
1234
                if ($className === null) {
1235
                    try {
1236
                        $className = $this->getBeanClassName($table);
1237
                    } catch (TDBMInvalidArgumentException $e) {
1238
                        $className = TDBMObject::class;
1239
                    }
1240
                }
1241
1242
                // Let's construct the bean
1243
                if (!isset($this->reflectionClassCache[$className])) {
1244
                    $this->reflectionClassCache[$className] = new \ReflectionClass($className);
1245
                }
1246
                // Let's bypass the constructor when creating the bean!
1247
                $bean = $this->reflectionClassCache[$className]->newInstanceWithoutConstructor();
1248
                /* @var $bean AbstractTDBMObject */
1249
                $bean->_constructLazy($table, $primaryKeys, $this);
1250
1251
                return $bean;
1252
            }
1253
        }
1254
1255
        // Did not find the object in cache? Let's query it!
1256
        return $this->findObjectOrFail($table, $primaryKeys, [], $additionalTablesFetch, $className);
1257
    }
1258
1259
    /**
1260
     * Returns a unique bean (or null) according to the filters passed in parameter.
1261
     *
1262
     * @param string            $mainTable             The name of the table queried
1263
     * @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)
1264
     * @param array             $parameters
1265
     * @param array             $additionalTablesFetch
1266
     * @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
1267
     *
1268
     * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters
1269
     *
1270
     * @throws TDBMException
1271
     */
1272 View Code Duplication
    public function findObject(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1273
    {
1274
        $objects = $this->findObjects($mainTable, $filter, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className);
1275
        $page = $objects->take(0, 2);
1276
        $count = $page->count();
1277
        if ($count > 1) {
1278
            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.");
1279
        } elseif ($count === 0) {
1280
            return;
1281
        }
1282
1283
        return $page[0];
1284
    }
1285
1286
    /**
1287
     * Returns a unique bean (or null) according to the filters passed in parameter.
1288
     *
1289
     * @param string            $mainTable  The name of the table queried
1290
     * @param string            $from       The from sql statement
1291
     * @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)
1292
     * @param array             $parameters
1293
     * @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
1294
     *
1295
     * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters
1296
     *
1297
     * @throws TDBMException
1298
     */
1299 View Code Duplication
    public function findObjectFromSql($mainTable, $from, $filter = null, array $parameters = array(), $className = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1300
    {
1301
        $objects = $this->findObjectsFromSql($mainTable, $from, $filter, $parameters, null, self::MODE_ARRAY, $className);
1302
        $page = $objects->take(0, 2);
1303
        $count = $page->count();
1304
        if ($count > 1) {
1305
            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.");
1306
        } elseif ($count === 0) {
1307
            return;
1308
        }
1309
1310
        return $page[0];
1311
    }
1312
1313
    /**
1314
     * Returns a unique bean according to the filters passed in parameter.
1315
     * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1316
     *
1317
     * @param string            $mainTable             The name of the table queried
1318
     * @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)
1319
     * @param array             $parameters
1320
     * @param array             $additionalTablesFetch
1321
     * @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
1322
     *
1323
     * @return AbstractTDBMObject The object we want
1324
     *
1325
     * @throws TDBMException
1326
     */
1327
    public function findObjectOrFail(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null)
1328
    {
1329
        $bean = $this->findObject($mainTable, $filter, $parameters, $additionalTablesFetch, $className);
1330
        if ($bean === null) {
1331
            throw new NoBeanFoundException("No result found for query on table '".$mainTable."'");
1332
        }
1333
1334
        return $bean;
1335
    }
1336
1337
    /**
1338
     * @param array $beanData An array of data: array<table, array<column, value>>
1339
     *
1340
     * @return array an array with first item = class name, second item = table name and third item = list of tables needed
1341
     *
1342
     * @throws TDBMInheritanceException
1343
     */
1344
    public function _getClassNameFromBeanData(array $beanData)
1345
    {
1346
        if (count($beanData) === 1) {
1347
            $tableName = array_keys($beanData)[0];
1348
            $allTables = [$tableName];
1349
        } else {
1350
            $tables = [];
1351
            foreach ($beanData as $table => $row) {
1352
                $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1353
                $pkSet = false;
1354
                foreach ($primaryKeyColumns as $columnName) {
0 ignored issues
show
Bug introduced by
The expression $primaryKeyColumns of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1355
                    if ($row[$columnName] !== null) {
1356
                        $pkSet = true;
1357
                        break;
1358
                    }
1359
                }
1360
                if ($pkSet) {
1361
                    $tables[] = $table;
1362
                }
1363
            }
1364
1365
            // $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1366
            try {
1367
                $allTables = $this->_getLinkBetweenInheritedTables($tables);
1368
            } catch (TDBMInheritanceException $e) {
1369
                throw TDBMInheritanceException::extendException($e, $this, $beanData);
1370
            }
1371
            $tableName = $allTables[0];
1372
        }
1373
1374
        // Only one table in this bean. Life is sweat, let's look at its type:
1375
        try {
1376
            $className = $this->getBeanClassName($tableName);
1377
        } catch (TDBMInvalidArgumentException $e) {
1378
            $className = 'Mouf\\Database\\TDBM\\TDBMObject';
1379
        }
1380
1381
        return [$className, $tableName, $allTables];
1382
    }
1383
1384
    /**
1385
     * Returns an item from cache or computes it using $closure and puts it in cache.
1386
     *
1387
     * @param string   $key
1388
     * @param callable $closure
1389
     *
1390
     * @return mixed
1391
     */
1392 View Code Duplication
    private function fromCache(string $key, callable $closure)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1393
    {
1394
        $item = $this->cache->fetch($key);
1395
        if ($item === false) {
1396
            $item = $closure();
1397
            $this->cache->save($key, $item);
1398
        }
1399
1400
        return $item;
1401
    }
1402
1403
    /**
1404
     * Returns the foreign key object.
1405
     *
1406
     * @param string $table
1407
     * @param string $fkName
1408
     *
1409
     * @return ForeignKeyConstraint
1410
     */
1411
    public function _getForeignKeyByName(string $table, string $fkName)
1412
    {
1413
        return $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getForeignKey($fkName);
1414
    }
1415
1416
    /**
1417
     * @param $pivotTableName
1418
     * @param AbstractTDBMObject $bean
1419
     *
1420
     * @return AbstractTDBMObject[]
1421
     */
1422
    public function _getRelatedBeans(string $pivotTableName, AbstractTDBMObject $bean)
1423
    {
1424
        list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $bean);
1425
        /* @var $localFk ForeignKeyConstraint */
1426
        /* @var $remoteFk ForeignKeyConstraint */
1427
        $remoteTable = $remoteFk->getForeignTableName();
1428
1429
        $primaryKeys = $this->getPrimaryKeyValues($bean);
1430
        $columnNames = array_map(function ($name) use ($pivotTableName) {
1431
            return $pivotTableName.'.'.$name;
1432
        }, $localFk->getLocalColumns());
1433
1434
        $filter = array_combine($columnNames, $primaryKeys);
1435
1436
        return $this->findObjects($remoteTable, $filter);
1437
    }
1438
1439
    /**
1440
     * @param $pivotTableName
1441
     * @param AbstractTDBMObject $bean The LOCAL bean
1442
     *
1443
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean
1444
     *
1445
     * @throws TDBMException
1446
     */
1447
    private function getPivotTableForeignKeys(string $pivotTableName, AbstractTDBMObject $bean)
1448
    {
1449
        $fks = array_values($this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
1450
        $table1 = $fks[0]->getForeignTableName();
1451
        $table2 = $fks[1]->getForeignTableName();
1452
1453
        $beanTables = array_map(function (DbRow $dbRow) {
1454
            return $dbRow->_getDbTableName();
1455
        }, $bean->_getDbRows());
1456
1457
        if (in_array($table1, $beanTables)) {
1458
            return [$fks[0], $fks[1]];
1459
        } elseif (in_array($table2, $beanTables)) {
1460
            return [$fks[1], $fks[0]];
1461
        } else {
1462
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2} for pivot table {$pivotTableName}");
1463
        }
1464
    }
1465
1466
    /**
1467
     * Returns a list of pivot tables linked to $bean.
1468
     *
1469
     * @param AbstractTDBMObject $bean
1470
     *
1471
     * @return string[]
1472
     */
1473
    public function _getPivotTablesLinkedToBean(AbstractTDBMObject $bean)
1474
    {
1475
        $junctionTables = [];
1476
        $allJunctionTables = $this->schemaAnalyzer->detectJunctionTables(true);
1477
        foreach ($bean->_getDbRows() as $dbRow) {
1478
            foreach ($allJunctionTables as $table) {
1479
                // There are exactly 2 FKs since this is a pivot table.
1480
                $fks = array_values($table->getForeignKeys());
1481
1482
                if ($fks[0]->getForeignTableName() === $dbRow->_getDbTableName() || $fks[1]->getForeignTableName() === $dbRow->_getDbTableName()) {
1483
                    $junctionTables[] = $table->getName();
1484
                }
1485
            }
1486
        }
1487
1488
        return $junctionTables;
1489
    }
1490
1491
    /**
1492
     * Array of types for tables.
1493
     * Key: table name
1494
     * Value: array of types indexed by column.
1495
     *
1496
     * @var array[]
1497
     */
1498
    private $typesForTable = [];
1499
1500
    /**
1501
     * @internal
1502
     *
1503
     * @param string $tableName
1504
     *
1505
     * @return Type[]
1506
     */
1507
    public function _getColumnTypesForTable(string $tableName)
1508
    {
1509
        if (!isset($typesForTable[$tableName])) {
0 ignored issues
show
Bug introduced by
The variable $typesForTable seems only to be defined at a later point. As such the call to isset() seems to always evaluate to false.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
1510
            $columns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getColumns();
1511
            $typesForTable[$tableName] = array_map(function (Column $column) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
$typesForTable was never initialized. Although not strictly required by PHP, it is generally a good practice to add $typesForTable = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1512
                return $column->getType();
1513
            }, $columns);
1514
        }
1515
1516
        return $typesForTable[$tableName];
1517
    }
1518
1519
    /**
1520
     * Sets the minimum log level.
1521
     * $level must be one of Psr\Log\LogLevel::xxx.
1522
     *
1523
     * Defaults to LogLevel::WARNING
1524
     *
1525
     * @param string $level
1526
     */
1527
    public function setLogLevel(string $level)
1528
    {
1529
        $this->logger = new LevelFilter($this->rootLogger, $level);
1530
    }
1531
}
1532