Completed
Pull Request — 4.3 (#149)
by Dorian
13:16
created
src/Mouf/Database/TDBM/TDBMService.php 1 patch
Indentation   +1384 added lines, -1384 removed lines patch added patch discarded remove patch
@@ -49,384 +49,384 @@  discard block
 block discarded – undo
49 49
  */
50 50
 class TDBMService
51 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...
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();
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...
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 430
             foreach ($this->connection->getPrimaryKey($table) as $col) {
431 431
                 $arr[] = $col->name;
432 432
             }
@@ -447,155 +447,155 @@  discard block
 block discarded – undo
447 447
                     throw new TDBMException($str);
448 448
                 }
449 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();
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) {
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();
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 599
                     // if there is no autoincrement and no pk set, let's go in error.
600 600
                     $isAutoIncrement = true;
601 601
 
@@ -613,30 +613,30 @@  discard block
 block discarded – undo
613 613
 
614 614
                 }*/
615 615
 
616
-                $types = [];
617
-                $escapedDbRowData = [];
616
+				$types = [];
617
+				$escapedDbRowData = [];
618 618
 
619
-                foreach ($dbRowData as $columnName => $value) {
620
-                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
621
-                    $types[] = $columnDescriptor->getType();
622
-                    $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
623
-                }
619
+				foreach ($dbRowData as $columnName => $value) {
620
+					$columnDescriptor = $tableDescriptor->getColumn($columnName);
621
+					$types[] = $columnDescriptor->getType();
622
+					$escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
623
+				}
624 624
 
625
-                $this->connection->insert($tableName, $escapedDbRowData, $types);
625
+				$this->connection->insert($tableName, $escapedDbRowData, $types);
626 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
-                }
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 634
 
635
-                // TODO: change this to some private magic accessor in future
636
-                $dbRow->_setPrimaryKeys($primaryKeys);
637
-                $unindexedPrimaryKeys = array_values($primaryKeys);
635
+				// TODO: change this to some private magic accessor in future
636
+				$dbRow->_setPrimaryKeys($primaryKeys);
637
+				$unindexedPrimaryKeys = array_values($primaryKeys);
638 638
 
639
-                /*
639
+				/*
640 640
                  * When attached, on "save", we check if the column updated is part of a primary key
641 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 642
                  * This method should first verify that the id is not already used (and is not auto-incremented)
@@ -646,7 +646,7 @@  discard block
 block discarded – undo
646 646
                  *
647 647
                  */
648 648
 
649
-                /*try {
649
+				/*try {
650 650
                     $this->db_connection->exec($sql);
651 651
                 } catch (TDBMException $e) {
652 652
                     $this->db_onerror = true;
@@ -665,405 +665,405 @@  discard block
 block discarded – undo
665 665
                     }
666 666
                 }*/
667 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;
674
-                //$this->db_modified_state = false;
675
-                //$dbRow = array();
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
-                foreach ($dbRowData as $columnName => $value) {
709
-                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
710
-                    $types[] = $columnDescriptor->getType();
711
-                    $escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
712
-                }
713
-                foreach ($primaryKeys as $columnName => $value) {
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
-                    foreach ($filters as $columnName => $value) {
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) {
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",
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;
674
+				//$this->db_modified_state = false;
675
+				//$dbRow = array();
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
+				foreach ($dbRowData as $columnName => $value) {
709
+					$columnDescriptor = $tableDescriptor->getColumn($columnName);
710
+					$types[] = $columnDescriptor->getType();
711
+					$escapedDbRowData[$this->connection->quoteIdentifier($columnName)] = $value;
712
+				}
713
+				foreach ($primaryKeys as $columnName => $value) {
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
+					foreach ($filters as $columnName => $value) {
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) {
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 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) {
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) {
1067 1067
         $onClauses = [];
1068 1068
         $foreignTableName = $this->connection->quoteIdentifier($fk->getForeignTableName());
1069 1069
         $foreignColumns = $fk->getForeignColumns();
@@ -1089,443 +1089,443 @@  discard block
 block discarded – undo
1089 1089
         }
1090 1090
     }*/
1091 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);
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
-    public function findObject(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null)
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
-    public function findObjectFromSql($mainTable, $from, $filter = null, array $parameters = array(), $className = null)
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) {
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
-    private function fromCache(string $key, callable $closure)
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])) {
1510
-            $columns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getColumns();
1511
-            $typesForTable[$tableName] = array_map(function (Column $column) {
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
-    }
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);
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
+	public function findObject(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null)
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
+	public function findObjectFromSql($mainTable, $from, $filter = null, array $parameters = array(), $className = null)
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) {
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
+	private function fromCache(string $key, callable $closure)
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])) {
1510
+			$columns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getColumns();
1511
+			$typesForTable[$tableName] = array_map(function (Column $column) {
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 1531
 }
Please login to merge, or discard this patch.