Completed
Pull Request — 3.4 (#46)
by David
12:52 queued 01:27
created

TDBMService::_getRelatedBeans()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 1
eloc 7
nc 1
nop 2
1
<?php
2
/*
3
 Copyright (C) 2006-2016 David Négrier - THE CODING MACHINE
4
5
This program is free software; you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation; either version 2 of the License, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
GNU General Public License for more details.
14
15
You should have received a copy of the GNU General Public License
16
along with this program; if not, write to the Free Software
17
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
*/
19
20
namespace Mouf\Database\TDBM;
21
22
use Doctrine\Common\Cache\Cache;
23
use Doctrine\Common\Cache\VoidCache;
24
use Doctrine\DBAL\Connection;
25
use Doctrine\DBAL\DBALException;
26
use Doctrine\DBAL\Schema\Column;
27
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
28
use Doctrine\DBAL\Schema\Schema;
29
use Doctrine\DBAL\Schema\Table;
30
use Mouf\Database\MagicQuery;
31
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
32
use Mouf\Database\TDBM\Filters\OrderBySQLString;
33
use Mouf\Database\TDBM\Utils\TDBMDaoGenerator;
34
use Mouf\Utils\Cache\CacheInterface;
35
use SQLParser\Node\ColRef;
36
37
/**
38
 * The TDBMService class is the main TDBM class. It provides methods to retrieve TDBMObject instances
39
 * from the database.
40
 *
41
 * @author David Negrier
42
 * @ExtendedAction {"name":"Generate DAOs", "url":"tdbmadmin/", "default":false}
43
 */
44
class TDBMService {
45
	
46
	const MODE_CURSOR = 1;
47
	const MODE_ARRAY = 2;
48
	
49
	/**
50
	 * The database connection.
51
	 *
52
	 * @var Connection
53
	 */
54
	private $connection;
55
56
	/**
57
	 * @var SchemaAnalyzer
58
	 */
59
	private $schemaAnalyzer;
60
61
	/**
62
	 * @var MagicQuery
63
	 */
64
	private $magicQuery;
65
66
	/**
67
	 * @var TDBMSchemaAnalyzer
68
	 */
69
	private $tdbmSchemaAnalyzer;
70
71
	/**
72
	 * @var string
73
	 */
74
	private $cachePrefix;
75
76
	/**
77
	 * The default autosave mode for the objects
78
	 * True to automatically save the object.
79
	 * If false, the user must explicitly call the save() method to save the object.
80
	 *
81
	 * @var boolean
82
	 */
83
	//private $autosave_default = false;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
84
85
	/**
86
	 * Cache of table of primary keys.
87
	 * Primary keys are stored by tables, as an array of column.
88
	 * For instance $primary_key['my_table'][0] will return the first column of the primary key of table 'my_table'.
89
	 *
90
	 * @var string[]
91
	 */
92
	private $primaryKeysColumns;
93
94
	/**
95
	 * Service storing objects in memory.
96
	 * Access is done by table name and then by primary key.
97
	 * If the primary key is split on several columns, access is done by an array of columns, serialized.
98
	 * 
99
	 * @var StandardObjectStorage|WeakrefObjectStorage
100
	 */
101
	private $objectStorage;
102
	
103
	/**
104
	 * The fetch mode of the result sets returned by `getObjects`.
105
	 * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY
106
	 *
107
	 * In 'MODE_ARRAY' mode (default), the result is an array. Use this mode by default (unless the list returned is very big).
108
	 * In 'MODE_CURSOR' mode, the result is a Generator which is an iterable collection that can be scanned only once (only one "foreach") on it,
109
	 * and it cannot be accessed via key. Use this mode for large datasets processed by batch.
110
	 * In 'MODE_COMPATIBLE_ARRAY' mode, the result is an old TDBMObjectArray (used up to TDBM 3.2). 
111
	 * You can access the array by key, or using foreach, several times.
112
	 *
113
	 * @var int
114
	 */
115
	private $mode = self::MODE_ARRAY;
116
117
	/**
118
	 * Table of new objects not yet inserted in database or objects modified that must be saved.
119
	 * @var \SplObjectStorage of DbRow objects
120
	 */
121
	private $toSaveObjects;
122
123
	/// The timestamp of the script startup. Useful to stop execution before time limit is reached and display useful error message.
124
	public static $script_start_up_time;
125
126
	/**
127
	 * The content of the cache variable.
128
	 *
129
	 * @var array<string, mixed>
130
	 */
131
	private $cache;
132
133
	private $cacheKey = "__TDBM_Cache__";
0 ignored issues
show
Unused Code introduced by
The property $cacheKey is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
134
135
	/**
136
	 * Map associating a table name to a fully qualified Bean class name
137
	 * @var array
138
	 */
139
	private $tableToBeanMap = [];
140
141
	/**
142
	 * @var \ReflectionClass[]
143
	 */
144
	private $reflectionClassCache;
0 ignored issues
show
Unused Code introduced by
The property $reflectionClassCache is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
145
146
	/**
147
	 * @param Connection $connection The DBAL DB connection to use
148
	 * @param Cache|null $cache A cache service to be used
149
	 * @param SchemaAnalyzer $schemaAnalyzer The schema analyzer that will be used to find shortest paths...
150
	 * 										 Will be automatically created if not passed.
151
	 */
152
	public function __construct(Connection $connection, Cache $cache = null, SchemaAnalyzer $schemaAnalyzer = null) {
153
		//register_shutdown_function(array($this,"completeSaveOnExit"));
0 ignored issues
show
Unused Code Comprehensibility introduced by
90% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
154
		if (extension_loaded('weakref')) {
155
			$this->objectStorage = new WeakrefObjectStorage();
156
		} else {
157
			$this->objectStorage = new StandardObjectStorage();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Mouf\Database\TDBM\StandardObjectStorage() of type object<Mouf\Database\TDBM\StandardObjectStorage> is incompatible with the declared type object<Mouf\Database\TDBM\WeakrefObjectStorage> of property $objectStorage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
158
		}
159
		$this->connection = $connection;
160
		if ($cache !== null) {
161
			$this->cache = $cache;
162
		} else {
163
			$this->cache = new VoidCache();
164
		}
165
		if ($schemaAnalyzer) {
166
			$this->schemaAnalyzer = $schemaAnalyzer;
167
		} else {
168
			$this->schemaAnalyzer = new SchemaAnalyzer($this->connection->getSchemaManager(), $this->cache, $this->getConnectionUniqueId());
169
		}
170
171
		$this->magicQuery = new MagicQuery($this->connection, $this->cache, $this->schemaAnalyzer);
172
173
		$this->tdbmSchemaAnalyzer = new TDBMSchemaAnalyzer($connection, $this->cache, $this->schemaAnalyzer);
174
		$this->cachePrefix = $this->tdbmSchemaAnalyzer->getCachePrefix();
175
176
		if (self::$script_start_up_time === null) {
177
			self::$script_start_up_time = microtime(true);
178
		}
179
		$this->toSaveObjects = new \SplObjectStorage();
180
	}
181
182
183
	/**
184
	 * Returns the object used to connect to the database.
185
	 *
186
	 * @return Connection
187
	 */
188
	public function getConnection() {
189
		return $this->connection;
190
	}
191
192
	/**
193
	 * Creates a unique cache key for the current connection.
194
	 * @return string
195
	 */
196
	private function getConnectionUniqueId() {
197
		return hash('md4', $this->connection->getHost()."-".$this->connection->getPort()."-".$this->connection->getDatabase()."-".$this->connection->getDriver()->getName());
198
	}
199
200
	/**
201
	 * Sets the default fetch mode of the result sets returned by `getObjects`.
202
	 * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY.
203
	 *
204
	 * 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).
205
	 * 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
206
	 * several times. In cursor mode, you cannot access the result set by key. Use this mode for large datasets processed by batch.
207
	 *
208
	 * @param int $mode
209
	 * @return $this
210
	 * @throws TDBMException
211
	 */
212
	public function setFetchMode($mode) {
213 View Code Duplication
		if ($mode !== self::MODE_CURSOR && $mode !== self::MODE_ARRAY) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
214
			throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
215
		}
216
		$this->mode = $mode;
217
		return $this;
218
	}
219
220
	/**
221
	 * Returns a TDBMObject associated from table "$table_name".
222
	 * If the $filters parameter is an int/string, the object returned will be the object whose primary key = $filters.
223
	 * $filters can also be a set of TDBM_Filters (see the getObjects method for more details).
224
	 *
225
	 * For instance, if there is a table 'users', with a primary key on column 'user_id' and a column 'user_name', then
226
	 * 			$user = $tdbmService->getObject('users',1);
227
	 * 			echo $user->name;
228
	 * will return the name of the user whose user_id is one.
229
	 *
230
	 * If a table has a primary key over several columns, you should pass to $id an array containing the the value of the various columns.
231
	 * For instance:
232
	 * 			$group = $tdbmService->getObject('groups',array(1,2));
233
	 *
234
	 * Note that TDBMObject performs caching for you. If you get twice the same object, the reference of the object you will get
235
	 * will be the same.
236
	 *
237
	 * For instance:
238
	 * 			$user1 = $tdbmService->getObject('users',1);
239
	 * 			$user2 = $tdbmService->getObject('users',1);
240
	 * 			$user1->name = 'John Doe';
241
	 * 			echo $user2->name;
242
	 * will return 'John Doe'.
243
	 *
244
	 * You can use filters instead of passing the primary key. For instance:
245
	 * 			$user = $tdbmService->getObject('users',new EqualFilter('users', 'login', 'jdoe'));
246
	 * This will return the user whose login is 'jdoe'.
247
	 * Please note that if 2 users have the jdoe login in database, the method will throw a TDBM_DuplicateRowException.
248
	 *
249
	 * Also, you can specify the return class for the object (provided the return class extends TDBMObject).
250
	 * For instance:
251
	 *  	$user = $tdbmService->getObject('users',1,'User');
252
	 * will return an object from the "User" class. The "User" class must extend the "TDBMObject" class.
253
	 * Please be sure not to override any method or any property unless you perfectly know what you are doing!
254
	 *
255
	 * @param string $table_name The name of the table we retrieve an object from.
256
	 * @param mixed $filters If the filter is a string/integer, it will be considered as the id of the object (the value of the primary key). Otherwise, it can be a filter bag (see the filterbag parameter of the getObjects method for more details about filter bags)
257
	 * @param string $className Optional: The name of the class to instanciate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned.
258
	 * @param boolean $lazy_loading If set to true, and if the primary key is passed in parameter of getObject, the object will not be queried in database. It will be queried when you first try to access a column. If at that time the object cannot be found in database, an exception will be thrown.
259
	 * @return TDBMObject
260
	 */
261
/*	public function getObject($table_name, $filters, $className = null, $lazy_loading = false) {
262
263
		if (is_array($filters) || $filters instanceof FilterInterface) {
264
			$isFilterBag = false;
265
			if (is_array($filters)) {
266
				// Is this a multiple primary key or a filter bag?
267
				// Let's have a look at the first item of the array to decide.
268
				foreach ($filters as $filter) {
269
					if (is_array($filter) || $filter instanceof FilterInterface) {
270
						$isFilterBag = true;
271
					}
272
					break;
273
				}
274
			} else {
275
				$isFilterBag = true;
276
			}
277
				
278
			if ($isFilterBag == true) {
279
				// If a filter bag was passer in parameter, let's perform a getObjects.
280
				$objects = $this->getObjects($table_name, $filters, null, null, null, $className);
281
				if (count($objects) == 0) {
282
					return null;
283
				} elseif (count($objects) > 1) {
284
					throw new DuplicateRowException("Error while querying an object for table '$table_name': ".count($objects)." rows have been returned, but we should have received at most one.");
285
				}
286
				// Return the first and only object.
287
				if ($objects instanceof \Generator) {
288
					return $objects->current();
289
				} else {
290
					return $objects[0];
291
				}
292
			}
293
		}
294
		$id = $filters;
295
		if ($this->connection == null) {
296
			throw new TDBMException("Error while calling TdbmService->getObject(): No connection has been established on the database!");
297
		}
298
		$table_name = $this->connection->toStandardcase($table_name);
299
300
		// If the ID is null, let's throw an exception
301
		if ($id === null) {
302
			throw new TDBMException("The ID you passed to TdbmService->getObject is null for the object of type '$table_name'. Objects primary keys cannot be null.");
303
		}
304
305
		// If the primary key is split over many columns, the IDs are passed in an array. Let's serialize this array to store it.
306
		if (is_array($id)) {
307
			$id = serialize($id);
308
		}
309
310
		if ($className === null) {
311
			if (isset($this->tableToBeanMap[$table_name])) {
312
				$className = $this->tableToBeanMap[$table_name];
313
			} else {
314
				$className = "Mouf\\Database\\TDBM\\TDBMObject";
315
			}
316
		}
317
318
		if ($this->objectStorage->has($table_name, $id)) {
319
			$obj = $this->objectStorage->get($table_name, $id);
320
			if (is_a($obj, $className)) {
321
				return $obj;
322
			} else {
323
				throw new TDBMException("Error! The object with ID '$id' for table '$table_name' has already been retrieved. The type for this object is '".get_class($obj)."'' which is not a subtype of '$className'");
324
			}
325
		}
326
327
		if ($className != "Mouf\\Database\\TDBM\\TDBMObject" && !is_subclass_of($className, "Mouf\\Database\\TDBM\\TDBMObject")) {
328
			if (!class_exists($className)) {
329
				throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." does not exist.");
330
			} else {
331
				throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." should extend TDBMObject.");
332
			}
333
		}
334
		$obj = new $className($this, $table_name, $id);
335
336
		if ($lazy_loading == false) {
337
			// If we are not doing lazy loading, let's load the object:
338
			$obj->_dbLoadIfNotLoaded();
339
		}
340
341
		$this->objectStorage->set($table_name, $id, $obj);
342
343
		return $obj;
344
	}*/
345
346
	/**
347
	 * Removes the given object from database.
348
	 * This cannot be called on an object that is not attached to this TDBMService
349
	 * (will throw a TDBMInvalidOperationException)
350
	 *
351
	 * @param AbstractTDBMObject $object the object to delete.
352
	 * @throws TDBMException
353
	 * @throws TDBMInvalidOperationException
354
	 */
355
	public function delete(AbstractTDBMObject $object) {
356
		switch ($object->_getStatus()) {
357
			case TDBMObjectStateEnum::STATE_DELETED:
358
				// Nothing to do, object already deleted.
359
				return;
360
			case TDBMObjectStateEnum::STATE_DETACHED:
361
				throw new TDBMInvalidOperationException('Cannot delete a detached object');
362
			case TDBMObjectStateEnum::STATE_NEW:
363
                $this->deleteManyToManyRelationships($object);
364
				foreach ($object->_getDbRows() as $dbRow) {
365
					$this->removeFromToSaveObjectList($dbRow);
366
				}
367
				break;
368
			case TDBMObjectStateEnum::STATE_DIRTY:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
369
				foreach ($object->_getDbRows() as $dbRow) {
370
					$this->removeFromToSaveObjectList($dbRow);
371
				}
372
			case TDBMObjectStateEnum::STATE_NOT_LOADED:
373
			case TDBMObjectStateEnum::STATE_LOADED:
374
                $this->deleteManyToManyRelationships($object);
375
				// Let's delete db rows, in reverse order.
376
				foreach (array_reverse($object->_getDbRows()) as $dbRow) {
377
					$tableName = $dbRow->_getDbTableName();
378
					$primaryKeys = $dbRow->_getPrimaryKeys();
379
					$this->connection->delete($tableName, $primaryKeys);
380
					$this->objectStorage->remove($dbRow->_getDbTableName(), $this->getObjectHash($primaryKeys));
381
				}
382
				break;
383
			// @codeCoverageIgnoreStart
384
			default:
385
				throw new TDBMInvalidOperationException('Unexpected status for bean');
386
			// @codeCoverageIgnoreEnd
387
		}
388
389
		$object->_setStatus(TDBMObjectStateEnum::STATE_DELETED);
390
	}
391
392
    /**
393
     * Removes all many to many relationships for this object.
394
     * @param AbstractTDBMObject $object
395
     */
396
    private function deleteManyToManyRelationships(AbstractTDBMObject $object) {
397
        foreach ($object->_getDbRows() as $tableName => $dbRow) {
398
            $pivotTables = $this->tdbmSchemaAnalyzer->getPivotTableLinkedToTable($tableName);
399
            foreach ($pivotTables as $pivotTable) {
400
                $remoteBeans = $object->_getRelationships($pivotTable);
401
                foreach ($remoteBeans as $remoteBean) {
402
                    $object->_removeRelationship($pivotTable, $remoteBean);
403
                }
404
            }
405
        }
406
        $this->persistManyToManyRelationships($object);
407
    }
408
409
410
    /**
411
     * This function removes the given object from the database. It will also remove all objects relied to the one given
412
     * by parameter before all.
413
     *
414
     * Notice: if the object has a multiple primary key, the function will not work.
415
     *
416
     * @param AbstractTDBMObject $objToDelete
417
     */
418
    public function deleteCascade(AbstractTDBMObject $objToDelete) {
419
        $this->deleteAllConstraintWithThisObject($objToDelete);
420
        $this->delete($objToDelete);
421
    }
422
423
    /**
424
     * This function is used only in TDBMService (private function)
425
     * It will call deleteCascade function foreach object relied with a foreign key to the object given by parameter
426
     *
427
     * @param AbstractTDBMObject $obj
428
     */
429
    private function deleteAllConstraintWithThisObject(AbstractTDBMObject $obj) {
430
		$dbRows = $obj->_getDbRows();
431
		foreach ($dbRows as $dbRow) {
432
			$tableName = $dbRow->_getDbTableName();
433
			$pks = array_values($dbRow->_getPrimaryKeys());
434
			if (!empty($pks)) {
435
				$incomingFks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($tableName);
436
437
				foreach ($incomingFks as $incomingFk) {
438
					$filter = array_combine($incomingFk->getLocalColumns(), $pks);
439
440
					$results = $this->findObjects($incomingFk->getLocalTableName(), $filter);
441
442
					foreach ($results as $bean) {
443
						$this->deleteCascade($bean);
444
					}
445
				}
446
			}
447
		}
448
    }
449
450
	/**
451
	 * This function performs a save() of all the objects that have been modified.
452
	 */
453
	public function completeSave() {
454
455
		foreach ($this->toSaveObjects as $dbRow)
456
		{
457
			$this->save($dbRow->getTDBMObject());
458
		}
459
460
	}
461
462
	/**
463
	 * Returns an array of objects of "table_name" kind filtered from the filter bag.
464
	 *
465
	 * The getObjects method should be the most used query method in TDBM if you want to query the database for objects.
466
	 * (Note: if you want to query the database for an object by its primary key, use the getObject method).
467
	 *
468
	 * The getObjects method takes in parameter:
469
	 * 	- table_name: the kinf of TDBMObject you want to retrieve. In TDBM, a TDBMObject matches a database row, so the
470
	 * 			$table_name parameter should be the name of an existing table in database.
471
	 *  - filter_bag: The filter bag is anything that you can use to filter your request. It can be a SQL Where clause,
472
	 * 			a series of TDBM_Filter objects, or even TDBMObjects or TDBMObjectArrays that you will use as filters.
473
	 *  - order_bag: The order bag is anything that will be used to order the data that is passed back to you.
474
	 * 			A SQL Order by clause can be used as an order bag for instance, or a OrderByColumn object
475
	 * 	- from (optionnal): The offset from which the query should be performed. For instance, if $from=5, the getObjects method
476
	 * 			will return objects from the 6th rows.
477
	 * 	- limit (optionnal): The maximum number of objects to return. Used together with $from, you can implement
478
	 * 			paging mechanisms.
479
	 *  - hint_path (optionnal): EXPERTS ONLY! The path the request should use if not the most obvious one. This parameter
480
	 * 			should be used only if you perfectly know what you are doing.
481
	 *
482
	 * The getObjects method will return a TDBMObjectArray. A TDBMObjectArray is an array of TDBMObjects that does behave as
483
	 * a single TDBMObject if the array has only one member. Refer to the documentation of TDBMObjectArray and TDBMObject
484
	 * to learn more.
485
	 *
486
	 * More about the filter bag:
487
	 * A filter is anything that can change the set of objects returned by getObjects.
488
	 * There are many kind of filters in TDBM:
489
	 * A filter can be:
490
	 * 	- A SQL WHERE clause:
491
	 * 		The clause is specified without the "WHERE" keyword. For instance:
492
	 * 			$filter = "users.first_name LIKE 'J%'";
493
	 *     	is a valid filter.
494
	 * 	   	The only difference with SQL is that when you specify a column name, it should always be fully qualified with
495
	 * 		the table name: "country_name='France'" is not valid, while "countries.country_name='France'" is valid (if
496
	 * 		"countries" is a table and "country_name" a column in that table, sure.
497
	 * 		For instance,
498
	 * 				$french_users = TDBMObject::getObjects("users", "countries.country_name='France'");
499
	 * 		will return all the users that are French (based on trhe assumption that TDBM can find a way to connect the users
500
	 * 		table to the country table using foreign keys, see the manual for that point).
501
	 * 	- A TDBMObject:
502
	 * 		An object can be used as a filter. For instance, we could get the France object and then find any users related to
503
	 * 		that object using:
504
	 * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
505
	 * 				$french_users = TDBMObject::getObjects("users", $france);
506
	 *  - A TDBMObjectArray can be used as a filter too.
507
	 * 		For instance:
508
	 * 				$french_groups = TDBMObject::getObjects("groups", $french_users);
509
	 * 		might return all the groups in which french users can be found.
510
	 *  - Finally, TDBM_xxxFilter instances can be used.
511
	 * 		TDBM provides the developer a set of TDBM_xxxFilters that can be used to model a SQL Where query.
512
	 * 		Using the appropriate filter object, you can model the operations =,<,<=,>,>=,IN,LIKE,AND,OR, IS NULL and NOT
513
	 * 		For instance:
514
	 * 				$french_users = TDBMObject::getObjects("users", new EqualFilter('countries','country_name','France');
515
	 * 		Refer to the documentation of the appropriate filters for more information.
516
	 *
517
	 * The nice thing about a filter bag is that it can be any filter, or any array of filters. In that case, filters are
518
	 * 'ANDed' together.
519
	 * So a request like this is valid:
520
	 * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
521
	 * 				$french_administrators = TDBMObject::getObjects("users", array($france,"role.role_name='Administrators'");
522
	 * This requests would return the users that are both French and administrators.
523
	 *
524
	 * Finally, if filter_bag is null, the whole table is returned.
525
	 *
526
	 * More about the order bag:
527
	 * The order bag contains anything that can be used to order the data that is passed back to you.
528
	 * The order bag can contain two kinds of objects:
529
	 * 	- A SQL ORDER BY clause:
530
	 * 		The clause is specified without the "ORDER BY" keyword. For instance:
531
	 * 			$orderby = "users.last_name ASC, users.first_name ASC";
532
	 *     	is a valid order bag.
533
	 * 		The only difference with SQL is that when you specify a column name, it should always be fully qualified with
534
	 * 		the table name: "country_name ASC" is not valid, while "countries.country_name ASC" is valid (if
535
	 * 		"countries" is a table and "country_name" a column in that table, sure.
536
	 * 		For instance,
537
	 * 				$french_users = TDBMObject::getObjects("users", null, "countries.country_name ASC");
538
	 * 		will return all the users sorted by country.
539
	 *  - A OrderByColumn object
540
	 * 		This object models a single column in a database.
541
	 *
542
	 * @param string $table_name The name of the table queried
543
	 * @param mixed $filter_bag The filter bag (see above for complete description)
544
	 * @param mixed $orderby_bag The order bag (see above for complete description)
545
	 * @param integer $from The offset
546
	 * @param integer $limit The maximum number of rows returned
547
	 * @param string $className Optional: The name of the class to instanciate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned.
548
	 * @param unknown_type $hint_path Hints to get the path for the query (expert parameter, you should leave it to null).
549
	 * @return TDBMObjectArray A TDBMObjectArray containing the resulting objects of the query.
550
	 */
551
/*	public function getObjects($table_name, $filter_bag=null, $orderby_bag=null, $from=null, $limit=null, $className=null, $hint_path=null) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
552
		if ($this->connection == null) {
553
			throw new TDBMException("Error while calling TDBMObject::getObject(): No connection has been established on the database!");
554
		}
555
		return $this->getObjectsByMode('getObjects', $table_name, $filter_bag, $orderby_bag, $from, $limit, $className, $hint_path);
556
	}*/
557
558
559
	/**
560
	 * Takes in input a filter_bag (which can be about anything from a string to an array of TDBMObjects... see above from documentation),
561
	 * and gives back a proper Filter object.
562
	 *
563
	 * @param mixed $filter_bag
564
	 * @return array First item: filter string, second item: parameters.
565
	 * @throws TDBMException
566
	 */
567
	public function buildFilterFromFilterBag($filter_bag) {
568
		$counter = 1;
569
		if ($filter_bag === null) {
570
			return ['', []];
571
		} elseif (is_string($filter_bag)) {
572
			return [$filter_bag, []];
573
		} elseif (is_array($filter_bag)) {
574
			$sqlParts = [];
575
			$parameters = [];
576
			foreach ($filter_bag as $column => $value) {
577
				$paramName = "tdbmparam".$counter;
578
				if (is_array($value)) {
579
					$sqlParts[] = $this->connection->quoteIdentifier($column)." IN :".$paramName;
580
				} else {
581
					$sqlParts[] = $this->connection->quoteIdentifier($column)." = :".$paramName;
582
				}
583
				$parameters[$paramName] = $value;
584
				$counter++;
585
			}
586
			return [implode(' AND ', $sqlParts), $parameters];
587
		} elseif ($filter_bag instanceof AbstractTDBMObject) {
588
			$dbRows = $filter_bag->_getDbRows();
589
			$dbRow = reset($dbRows);
590
			$primaryKeys = $dbRow->_getPrimaryKeys();
591
592
			foreach ($primaryKeys as $column => $value) {
593
				$paramName = "tdbmparam".$counter;
594
				$sqlParts[] = $this->connection->quoteIdentifier($dbRow->_getDbTableName()).'.'.$this->connection->quoteIdentifier($column)." = :".$paramName;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$sqlParts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sqlParts = array(); before regardless.

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

Loading history...
595
				$parameters[$paramName] = $value;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parameters was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parameters = array(); before regardless.

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

Loading history...
596
				$counter++;
597
			}
598
			return [implode(' AND ', $sqlParts), $parameters];
0 ignored issues
show
Bug introduced by
The variable $sqlParts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $parameters does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
599
		} elseif ($filter_bag instanceof \Iterator) {
600
			return $this->buildFilterFromFilterBag(iterator_to_array($filter_bag));
601
		} else {
602
			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.");
603
		}
604
605
//		// First filter_bag should be an array, if it is a singleton, let's put it in an array.
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
606
//		if ($filter_bag === null) {
607
//			$filter_bag = array();
608
//		} elseif (!is_array($filter_bag)) {
609
//			$filter_bag = array($filter_bag);
610
//		}
611
//		elseif (is_a($filter_bag, 'Mouf\\Database\\TDBM\\TDBMObjectArray')) {
612
//			$filter_bag = array($filter_bag);
613
//		}
614
//
615
//		// Second, let's take all the objects out of the filter bag, and let's make filters from them
616
//		$filter_bag2 = array();
617
//		foreach ($filter_bag as $thing) {
618
//			if (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\FilterInterface')) {
619
//				$filter_bag2[] = $thing;
620
//			} elseif (is_string($thing)) {
621
//				$filter_bag2[] = new SqlStringFilter($thing);
622
//			} elseif (is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && count($thing)>0) {
623
//				// Get table_name and column_name
624
//				$filter_table_name = $thing[0]->_getDbTableName();
625
//				$filter_column_names = $thing[0]->getPrimaryKey();
626
//
627
//				// If there is only one primary key, we can use the InFilter
628
//				if (count($filter_column_names)==1) {
629
//					$primary_keys_array = array();
630
//					$filter_column_name = $filter_column_names[0];
631
//					foreach ($thing as $TDBMObject) {
632
//						$primary_keys_array[] = $TDBMObject->$filter_column_name;
633
//					}
634
//					$filter_bag2[] = new InFilter($filter_table_name, $filter_column_name, $primary_keys_array);
635
//				}
636
//				// else, we must use a (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx)...
637
//				else
638
//				{
639
//					$filter_bag_and = array();
640
//					foreach ($thing as $TDBMObject) {
641
//						$filter_bag_temp_and=array();
642
//						foreach ($filter_column_names as $pk) {
643
//							$filter_bag_temp_and[] = new EqualFilter($TDBMObject->_getDbTableName(), $pk, $TDBMObject->$pk);
644
//						}
645
//						$filter_bag_and[] = new AndFilter($filter_bag_temp_and);
646
//					}
647
//					$filter_bag2[] = new OrFilter($filter_bag_and);
648
//				}
649
//
650
//
651
//			} elseif (!is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && $thing!==null) {
652
//				throw new TDBMException("Error in filter bag in getObjectsByFilter. An object has been passed that is neither a filter, nor a TDBMObject, nor a TDBMObjectArray, nor a string, nor null.");
653
//			}
654
//		}
655
//
656
//		// Third, let's take all the filters and let's apply a huge AND filter
657
//		$filter = new AndFilter($filter_bag2);
658
//
659
//		return $filter;
660
	}
661
662
	/**
663
	 * Takes in input an order_bag (which can be about anything from a string to an array of OrderByColumn objects... see above from documentation),
664
	 * and gives back an array of OrderByColumn / OrderBySQLString objects.
665
	 *
666
	 * @param unknown_type $orderby_bag
667
	 * @return array
668
	 */
669
	public function buildOrderArrayFromOrderBag($orderby_bag) {
670
		// Fourth, let's apply the same steps to the orderby_bag
671
		// 4-1 orderby_bag should be an array, if it is a singleton, let's put it in an array.
672
673
		if (!is_array($orderby_bag))
674
		$orderby_bag = array($orderby_bag);
675
676
		// 4-2, let's take all the objects out of the orderby bag, and let's make objects from them
677
		$orderby_bag2 = array();
678
		foreach ($orderby_bag as $thing) {
679
			if (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\OrderBySQLString')) {
680
				$orderby_bag2[] = $thing;
681
			} elseif (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\OrderByColumn')) {
682
				$orderby_bag2[] = $thing;
683
			} elseif (is_string($thing)) {
684
				$orderby_bag2[] = new OrderBySQLString($thing);
685
			} elseif ($thing !== null) {
686
				throw new TDBMException("Error in orderby bag in getObjectsByFilter. An object has been passed that is neither a OrderBySQLString, nor a OrderByColumn, nor a string, nor null.");
687
			}
688
		}
689
		return $orderby_bag2;
690
	}
691
692
	/**
693
	 * @param string $table
694
	 * @return string[]
695
	 */
696
	public function getPrimaryKeyColumns($table) {
697
		if (!isset($this->primaryKeysColumns[$table]))
698
		{
699
			$this->primaryKeysColumns[$table] = $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKeyColumns();
700
701
			// TODO TDBM4: See if we need to improve error reporting if table name does not exist.
702
703
			/*$arr = array();
704
			foreach ($this->connection->getPrimaryKey($table) as $col) {
705
				$arr[] = $col->name;
706
			}
707
			// The primaryKeysColumns contains only the column's name, not the DB_Column object.
708
			$this->primaryKeysColumns[$table] = $arr;
709
			if (empty($this->primaryKeysColumns[$table]))
710
			{
711
				// Unable to find primary key.... this is an error
712
				// Let's try to be precise in error reporting. Let's try to find the table.
713
				$tables = $this->connection->checkTableExist($table);
714
				if ($tables === true)
715
				throw new TDBMException("Could not find table primary key for table '$table'. Please define a primary key for this table.");
716
				elseif ($tables !== null) {
717
					if (count($tables)==1)
718
					$str = "Could not find table '$table'. Maybe you meant this table: '".$tables[0]."'";
719
					else
720
					$str = "Could not find table '$table'. Maybe you meant one of those tables: '".implode("', '",$tables)."'";
721
					throw new TDBMException($str);
722
				}
723
			}*/
724
		}
725
		return $this->primaryKeysColumns[$table];
726
	}
727
728
	/**
729
	 * This is an internal function, you should not use it in your application.
730
	 * This is used internally by TDBM to add an object to the object cache.
731
	 *
732
	 * @param DbRow $dbRow
733
	 */
734
	public function _addToCache(DbRow $dbRow) {
735
		$primaryKey = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
736
		$hash = $this->getObjectHash($primaryKey);
737
		$this->objectStorage->set($dbRow->_getDbTableName(), $hash, $dbRow);
738
	}
739
740
	/**
741
	 * This is an internal function, you should not use it in your application.
742
	 * This is used internally by TDBM to remove the object from the list of objects that have been
743
	 * created/updated but not saved yet.
744
	 *
745
	 * @param DbRow $myObject
746
	 */
747
	private function removeFromToSaveObjectList(DbRow $myObject) {
748
		unset($this->toSaveObjects[$myObject]);
749
	}
750
751
	/**
752
	 * This is an internal function, you should not use it in your application.
753
	 * This is used internally by TDBM to add an object to the list of objects that have been
754
	 * created/updated but not saved yet.
755
	 *
756
	 * @param AbstractTDBMObject $myObject
757
	 */
758
	public function _addToToSaveObjectList(DbRow $myObject) {
759
		$this->toSaveObjects[$myObject] = true;
760
	}
761
762
	/**
763
	 * Generates all the daos and beans.
764
	 *
765
	 * @param string $daoFactoryClassName The classe name of the DAO factory
766
	 * @param string $daonamespace The namespace for the DAOs, without trailing \
767
	 * @param string $beannamespace The Namespace for the beans, without trailing \
768
	 * @param bool $storeInUtc If the generated daos should store the date in UTC timezone instead of user's timezone.
769
	 * @return \string[] the list of tables
770
	 */
771
	public function generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc) {
772
		$tdbmDaoGenerator = new TDBMDaoGenerator($this->schemaAnalyzer, $this->tdbmSchemaAnalyzer->getSchema(), $this->tdbmSchemaAnalyzer);
773
		return $tdbmDaoGenerator->generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc);
774
	}
775
776
	/**
777
 	* @param array<string, string> $tableToBeanMap
778
 	*/
779
	public function setTableToBeanMap(array $tableToBeanMap) {
780
		$this->tableToBeanMap = $tableToBeanMap;
781
	}
782
783
	/**
784
	 * Saves $object by INSERTing or UPDAT(E)ing it in the database.
785
	 *
786
	 * @param AbstractTDBMObject $object
787
	 * @throws TDBMException
788
	 */
789
	public function save(AbstractTDBMObject $object) {
790
		$status = $object->_getStatus();
791
792
		// Let's attach this object if it is in detached state.
793
		if ($status === TDBMObjectStateEnum::STATE_DETACHED) {
794
			$object->_attach($this);
795
			$status = $object->_getStatus();
796
		}
797
798
		if ($status === TDBMObjectStateEnum::STATE_NEW) {
799
			$dbRows = $object->_getDbRows();
800
801
			$unindexedPrimaryKeys = array();
802
803
			foreach ($dbRows as $dbRow) {
804
805
				$tableName = $dbRow->_getDbTableName();
806
807
				$schema = $this->tdbmSchemaAnalyzer->getSchema();
808
				$tableDescriptor = $schema->getTable($tableName);
809
810
				$primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
811
812
				if (empty($unindexedPrimaryKeys)) {
813
					$primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
814
				} else {
815
					// First insert, the children must have the same primary key as the parent.
816
					$primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
817
					$dbRow->_setPrimaryKeys($primaryKeys);
818
				}
819
820
				$references = $dbRow->_getReferences();
821
822
				// Let's save all references in NEW or DETACHED state (we need their primary key)
823
				foreach ($references as $fkName => $reference) {
824
                    $refStatus = $reference->_getStatus();
825
					if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
826
						$this->save($reference);
827
					}
828
				}
829
830
				$dbRowData = $dbRow->_getDbRow();
831
832
				// Let's see if the columns for primary key have been set before inserting.
833
				// We assume that if one of the value of the PK has been set, the PK is set.
834
				$isPkSet = !empty($primaryKeys);
835
836
837
				/*if (!$isPkSet) {
838
                    // if there is no autoincrement and no pk set, let's go in error.
839
                    $isAutoIncrement = true;
840
841
                    foreach ($primaryKeyColumns as $pkColumnName) {
842
                        $pkColumn = $tableDescriptor->getColumn($pkColumnName);
843
                        if (!$pkColumn->getAutoincrement()) {
844
                            $isAutoIncrement = false;
845
                        }
846
                    }
847
848
                    if (!$isAutoIncrement) {
849
                        $msg = "Error! You did not set the primary key(s) for the new object of type '$tableName'. The primary key is not set to 'autoincrement' so you must either set the primary key in the object or modify the DB model to create an primary key with auto-increment.";
850
                        throw new TDBMException($msg);
851
                    }
852
853
                }*/
854
855
				$types = [];
856
857
				foreach ($dbRowData as $columnName => $value) {
858
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
859
					$types[] = $columnDescriptor->getType();
860
				}
861
862
				$this->connection->insert($tableName, $dbRowData, $types);
863
864
				if (!$isPkSet && count($primaryKeyColumns) == 1) {
865
					$id = $this->connection->lastInsertId();
866
					$primaryKeys[$primaryKeyColumns[0]] = $id;
867
				}
868
869
				// TODO: change this to some private magic accessor in future
870
				$dbRow->_setPrimaryKeys($primaryKeys);
871
				$unindexedPrimaryKeys = array_values($primaryKeys);
872
873
874
875
876
				/*
877
                 * When attached, on "save", we check if the column updated is part of a primary key
878
                 * If this is part of a primary key, we call the _update_id method that updates the id in the list of known objects.
879
                 * This method should first verify that the id is not already used (and is not auto-incremented)
880
                 *
881
                 * In the object, the key is stored in an array of  (column => value), that can be directly used to update the record.
882
                 *
883
                 *
884
                 */
885
886
887
				/*try {
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
888
                    $this->db_connection->exec($sql);
889
                } catch (TDBMException $e) {
890
                    $this->db_onerror = true;
891
892
                    // Strange..... if we do not have the line below, bad inserts are not catched.
893
                    // It seems that destructors are called before the registered shutdown function (PHP >=5.0.5)
894
                    //if ($this->tdbmService->isProgramExiting())
895
                    //	trigger_error("program exiting");
896
                    trigger_error($e->getMessage(), E_USER_ERROR);
897
898
                    if (!$this->tdbmService->isProgramExiting())
899
                        throw $e;
900
                    else
901
                    {
902
                        trigger_error($e->getMessage(), E_USER_ERROR);
903
                    }
904
                }*/
905
906
				// Let's remove this object from the $new_objects static table.
907
				$this->removeFromToSaveObjectList($dbRow);
908
909
				// TODO: change this behaviour to something more sensible performance-wise
910
				// Maybe a setting to trigger this globally?
911
				//$this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

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

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

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

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

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

Loading history...
914
915
				// Let's add this object to the list of objects in cache.
916
				$this->_addToCache($dbRow);
917
			}
918
919
920
921
922
			$object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
923
		} elseif ($status === TDBMObjectStateEnum::STATE_DIRTY) {
924
			$dbRows = $object->_getDbRows();
925
926
			foreach ($dbRows as $dbRow) {
927
				$references = $dbRow->_getReferences();
928
929
				// Let's save all references in NEW state (we need their primary key)
930
				foreach ($references as $fkName => $reference) {
931
					if ($reference->_getStatus() === TDBMObjectStateEnum::STATE_NEW) {
932
						$this->save($reference);
933
					}
934
				}
935
936
				// Let's first get the primary keys
937
				$tableName = $dbRow->_getDbTableName();
938
				$dbRowData = $dbRow->_getDbRow();
939
940
				$schema = $this->tdbmSchemaAnalyzer->getSchema();
941
				$tableDescriptor = $schema->getTable($tableName);
942
943
				$primaryKeys = $dbRow->_getPrimaryKeys();
944
945
				$types = [];
946
947
				foreach ($dbRowData as $columnName => $value) {
948
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
949
					$types[] = $columnDescriptor->getType();
950
				}
951
				foreach ($primaryKeys as $columnName => $value) {
952
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
953
					$types[] = $columnDescriptor->getType();
954
				}
955
956
				$this->connection->update($tableName, $dbRowData, $primaryKeys, $types);
957
958
				// Let's check if the primary key has been updated...
959
				$needsUpdatePk = false;
960
				foreach ($primaryKeys as $column => $value) {
961
					if (!isset($dbRowData[$column]) || $dbRowData[$column] != $value) {
962
						$needsUpdatePk = true;
963
						break;
964
					}
965
				}
966
				if ($needsUpdatePk) {
967
					$this->objectStorage->remove($tableName, $this->getObjectHash($primaryKeys));
968
					$newPrimaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
969
					$dbRow->_setPrimaryKeys($newPrimaryKeys);
970
					$this->objectStorage->set($tableName, $this->getObjectHash($primaryKeys), $dbRow);
971
				}
972
973
				// Let's remove this object from the list of objects to save.
974
				$this->removeFromToSaveObjectList($dbRow);
975
			}
976
977
			$object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
978
979
		} elseif ($status === TDBMObjectStateEnum::STATE_DELETED) {
980
			throw new TDBMInvalidOperationException("This object has been deleted. It cannot be saved.");
981
		}
982
983
        // Finally, let's save all the many to many relationships to this bean.
984
        $this->persistManyToManyRelationships($object);
985
	}
986
987
    private function persistManyToManyRelationships(AbstractTDBMObject $object) {
988
        foreach ($object->_getCachedRelationships() as $pivotTableName => $storage) {
989
            $tableDescriptor = $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
990
            list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $object);
991
992
            foreach ($storage as $remoteBean) {
993
                /* @var $remoteBean AbstractTDBMObject */
994
                $statusArr = $storage[$remoteBean];
995
                $status = $statusArr['status'];
996
                $reverse = $statusArr['reverse'];
997
                if ($reverse) {
998
                    continue;
999
                }
1000
1001
                if ($status === 'new') {
1002
                    $remoteBeanStatus = $remoteBean->_getStatus();
1003
                    if ($remoteBeanStatus === TDBMObjectStateEnum::STATE_NEW || $remoteBeanStatus === TDBMObjectStateEnum::STATE_DETACHED) {
1004
                        // Let's save remote bean if needed.
1005
                        $this->save($remoteBean);
1006
                    }
1007
1008
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
1009
1010
                    $types = [];
1011
1012
                    foreach ($filters as $columnName => $value) {
1013
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
1014
                        $types[] = $columnDescriptor->getType();
1015
                    }
1016
1017
                    $this->connection->insert($pivotTableName, $filters, $types);
1018
1019
                    // Finally, let's mark relationships as saved.
1020
                    $statusArr['status'] = 'loaded';
1021
                    $storage[$remoteBean] = $statusArr;
1022
                    $remoteStorage = $remoteBean->_getCachedRelationships()[$pivotTableName];
1023
                    $remoteStatusArr = $remoteStorage[$object];
1024
                    $remoteStatusArr['status'] = 'loaded';
1025
                    $remoteStorage[$object] = $remoteStatusArr;
1026
1027
                } elseif ($status === 'delete') {
1028
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
1029
1030
                    $types = [];
1031
1032
                    foreach ($filters as $columnName => $value) {
1033
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
1034
                        $types[] = $columnDescriptor->getType();
1035
                    }
1036
1037
                    $this->connection->delete($pivotTableName, $filters, $types);
1038
1039
                    // Finally, let's remove relationships completely from bean.
1040
                    $storage->detach($remoteBean);
1041
                    $remoteBean->_getCachedRelationships()[$pivotTableName]->detach($object);
1042
                }
1043
            }
1044
        }
1045
    }
1046
1047
    private function getPivotFilters(AbstractTDBMObject $localBean, AbstractTDBMObject $remoteBean, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk) {
1048
        $localBeanPk = $this->getPrimaryKeyValues($localBean);
1049
        $remoteBeanPk = $this->getPrimaryKeyValues($remoteBean);
1050
        $localColumns = $localFk->getLocalColumns();
1051
        $remoteColumns = $remoteFk->getLocalColumns();
1052
1053
        $localFilters = array_combine($localColumns, $localBeanPk);
1054
        $remoteFilters = array_combine($remoteColumns, $remoteBeanPk);
1055
1056
        return array_merge($localFilters, $remoteFilters);
1057
    }
1058
1059
    /**
1060
     * Returns the "values" of the primary key.
1061
     * This returns the primary key from the $primaryKey attribute, not the one stored in the columns.
1062
     *
1063
     * @param AbstractTDBMObject $bean
1064
     * @return array numerically indexed array of values.
1065
     */
1066
    private function getPrimaryKeyValues(AbstractTDBMObject $bean) {
1067
        $dbRows = $bean->_getDbRows();
1068
        $dbRow = reset($dbRows);
1069
        return array_values($dbRow->_getPrimaryKeys());
1070
    }
1071
1072
	/**
1073
	 * Returns a unique hash used to store the object based on its primary key.
1074
	 * If the array contains only one value, then the value is returned.
1075
	 * Otherwise, a hash representing the array is returned.
1076
	 *
1077
	 * @param array $primaryKeys An array of columns => values forming the primary key
1078
	 * @return string
1079
	 */
1080
	public function getObjectHash(array $primaryKeys) {
1081
		if (count($primaryKeys) === 1) {
1082
			return reset($primaryKeys);
1083
		} else {
1084
			ksort($primaryKeys);
1085
			return md5(json_encode($primaryKeys));
1086
		}
1087
	}
1088
1089
	/**
1090
	 * Returns an array of primary keys from the object.
1091
	 * The primary keys are extracted from the object columns and not from the primary keys stored in the
1092
	 * $primaryKeys variable of the object.
1093
	 *
1094
	 * @param DbRow $dbRow
1095
	 * @return array Returns an array of column => value
1096
	 */
1097
	public function getPrimaryKeysForObjectFromDbRow(DbRow $dbRow) {
1098
		$table = $dbRow->_getDbTableName();
1099
		$dbRowData = $dbRow->_getDbRow();
1100
		return $this->_getPrimaryKeysFromObjectData($table, $dbRowData);
1101
	}
1102
1103
	/**
1104
	 * Returns an array of primary keys for the given row.
1105
	 * The primary keys are extracted from the object columns.
1106
	 *
1107
	 * @param $table
1108
	 * @param array $columns
1109
	 * @return array
1110
	 */
1111
	public function _getPrimaryKeysFromObjectData($table, array $columns) {
1112
		$primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1113
		$values = array();
1114
		foreach ($primaryKeyColumns as $column) {
1115
			if (isset($columns[$column])) {
1116
				$values[$column] = $columns[$column];
1117
			}
1118
		}
1119
		return $values;
1120
	}
1121
1122
	/**
1123
	 * Attaches $object to this TDBMService.
1124
	 * The $object must be in DETACHED state and will pass in NEW state.
1125
	 *
1126
	 * @param AbstractTDBMObject $object
1127
	 * @throws TDBMInvalidOperationException
1128
	 */
1129
	public function attach(AbstractTDBMObject $object) {
1130
		$object->_attach($this);
1131
	}
1132
1133
	/**
1134
	 * Returns an associative array (column => value) for the primary keys from the table name and an
1135
	 * indexed array of primary key values.
1136
	 *
1137
	 * @param string $tableName
1138
	 * @param array $indexedPrimaryKeys
1139
	 */
1140
	public function _getPrimaryKeysFromIndexedPrimaryKeys($tableName, array $indexedPrimaryKeys) {
1141
		$primaryKeyColumns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getPrimaryKeyColumns();
1142
1143
		if (count($primaryKeyColumns) !== count($indexedPrimaryKeys)) {
1144
			throw new TDBMException(sprintf('Wrong number of columns passed for primary key. Expected %s columns for table "%s",
1145
			got %s instead.', count($primaryKeyColumns), $tableName, count($indexedPrimaryKeys)));
1146
		}
1147
1148
		return array_combine($primaryKeyColumns, $indexedPrimaryKeys);
1149
	}
1150
1151
	/**
1152
	 * Return the list of tables (from child to parent) joining the tables passed in parameter.
1153
	 * Tables must be in a single line of inheritance. The method will find missing tables.
1154
	 *
1155
	 * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1156
	 * we must be able to find all other tables.
1157
	 *
1158
	 * @param string[] $tables
1159
	 * @return string[]
1160
	 */
1161
	public function _getLinkBetweenInheritedTables(array $tables)
1162
	{
1163
		sort($tables);
1164
		return $this->fromCache($this->cachePrefix.'_linkbetweeninheritedtables_'.implode('__split__', $tables),
1165
			function() use ($tables) {
1166
				return $this->_getLinkBetweenInheritedTablesWithoutCache($tables);
1167
			});
1168
	}
1169
1170
	/**
1171
	 * Return the list of tables (from child to parent) joining the tables passed in parameter.
1172
	 * Tables must be in a single line of inheritance. The method will find missing tables.
1173
	 *
1174
	 * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1175
	 * we must be able to find all other tables.
1176
	 *
1177
	 * @param string[] $tables
1178
	 * @return string[]
1179
	 */
1180
	private function _getLinkBetweenInheritedTablesWithoutCache(array $tables) {
1181
		$schemaAnalyzer = $this->schemaAnalyzer;
1182
1183
		foreach ($tables as $currentTable) {
1184
			$allParents = [ $currentTable ];
1185
			$currentFk = null;
0 ignored issues
show
Unused Code introduced by
$currentFk is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1186
			while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1187
				$currentTable = $currentFk->getForeignTableName();
1188
				$allParents[] = $currentTable;
1189
			};
1190
1191
			// Now, does the $allParents contain all the tables we want?
1192
			$notFoundTables = array_diff($tables, $allParents);
1193
			if (empty($notFoundTables)) {
1194
				// We have a winner!
1195
				return $allParents;
1196
			}
1197
		}
1198
1199
		throw new TDBMException(sprintf("The tables (%s) cannot be linked by an inheritance relationship.", implode(', ', $tables)));
1200
	}
1201
1202
	/**
1203
	 * Returns the list of tables related to this table (via a parent or child inheritance relationship)
1204
	 * @param string $table
1205
	 * @return string[]
1206
	 */
1207
	public function _getRelatedTablesByInheritance($table)
1208
	{
1209
		return $this->fromCache($this->cachePrefix."_relatedtables_".$table, function() use ($table) {
1210
			return $this->_getRelatedTablesByInheritanceWithoutCache($table);
1211
		});
1212
	}
1213
1214
	/**
1215
	 * Returns the list of tables related to this table (via a parent or child inheritance relationship)
1216
	 * @param string $table
1217
	 * @return string[]
1218
	 */
1219
	private function _getRelatedTablesByInheritanceWithoutCache($table) {
1220
		$schemaAnalyzer = $this->schemaAnalyzer;
1221
1222
1223
		// Let's scan the parent tables
1224
		$currentTable = $table;
1225
1226
		$parentTables = [ ];
1227
1228
		// Get parent relationship
1229
		while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1230
			$currentTable = $currentFk->getForeignTableName();
1231
			$parentTables[] = $currentTable;
1232
		};
1233
1234
		// Let's recurse in children
1235
		$childrenTables = $this->exploreChildrenTablesRelationships($schemaAnalyzer, $table);
1236
1237
		return array_merge(array_reverse($parentTables), $childrenTables);
1238
	}
1239
1240
	/**
1241
	 * Explore all the children and descendant of $table and returns ForeignKeyConstraints on those.
1242
	 *
1243
	 * @param string $table
1244
	 * @return string[]
1245
	 */
1246
	private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyzer, $table) {
1247
		$tables = [$table];
1248
		$keys = $schemaAnalyzer->getChildrenRelationships($table);
1249
1250
		foreach ($keys as $key) {
1251
			$tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, $key->getLocalTableName()));
1252
		}
1253
1254
		return $tables;
1255
	}
1256
1257
	/**
1258
	 * Casts a foreign key into SQL, assuming table name is used with no alias.
1259
	 * The returned value does contain only one table. For instance:
1260
	 *
1261
	 * " LEFT JOIN table2 ON table1.id = table2.table1_id"
1262
	 *
1263
	 * @param ForeignKeyConstraint $fk
1264
	 * @param bool $leftTableIsLocal
1265
	 * @return string
1266
	 */
1267
	/*private function foreignKeyToSql(ForeignKeyConstraint $fk, $leftTableIsLocal) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
1268
		$onClauses = [];
1269
		$foreignTableName = $this->connection->quoteIdentifier($fk->getForeignTableName());
1270
		$foreignColumns = $fk->getForeignColumns();
1271
		$localTableName = $this->connection->quoteIdentifier($fk->getLocalTableName());
1272
		$localColumns = $fk->getLocalColumns();
1273
		$columnCount = count($localTableName);
1274
1275
		for ($i = 0; $i < $columnCount; $i++) {
1276
			$onClauses[] = sprintf("%s.%s = %s.%s",
1277
				$localTableName,
1278
				$this->connection->quoteIdentifier($localColumns[$i]),
1279
				$foreignColumns,
1280
				$this->connection->quoteIdentifier($foreignColumns[$i])
1281
				);
1282
		}
1283
1284
		$onClause = implode(' AND ', $onClauses);
1285
1286
		if ($leftTableIsLocal) {
1287
			return sprintf(" LEFT JOIN %s ON (%s)", $foreignTableName, $onClause);
1288
		} else {
1289
			return sprintf(" LEFT JOIN %s ON (%s)", $localTableName, $onClause);
1290
		}
1291
	}*/
1292
1293
	/**
1294
	 * Returns an identifier for the group of tables passed in parameter.
1295
	 *
1296
	 * @param string[] $relatedTables
1297
	 * @return string
1298
	 */
1299
	private function getTableGroupName(array $relatedTables) {
1300
		sort($relatedTables);
1301
		return implode('_``_', $relatedTables);
1302
	}
1303
1304
	/**
1305
	 *
1306
	 * @param string $mainTable The name of the table queried
1307
	 * @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)
1308
	 * @param array $parameters
1309
	 * @param string|null $orderString The ORDER BY part of the query. All columns must be prefixed by the table name (in the form: table.column)
1310
	 * @param array $additionalTablesFetch
1311
	 * @param string $mode
1312
	 * @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.
1313
	 * @return ResultIterator An object representing an array of results.
1314
	 * @throws TDBMException
1315
	 */
1316
	public function findObjects($mainTable, $filter=null, array $parameters = array(), $orderString=null, array $additionalTablesFetch = array(), $mode = null, $className=null) {
1317
		// $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1318
		if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1319
			throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1320
		}
1321
1322
		list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter);
1323
1324
		$parameters = array_merge($parameters, $additionalParameters);
1325
1326
		// From the table name and the additional tables we want to fetch, let's build a list of all tables
1327
		// that must be part of the select columns.
1328
1329
		$tableGroups = [];
1330
		$allFetchedTables = $this->_getRelatedTablesByInheritance($mainTable);
1331
		$tableGroupName = $this->getTableGroupName($allFetchedTables);
1332
		foreach ($allFetchedTables as $table) {
1333
			$tableGroups[$table] = $tableGroupName;
1334
		}
1335
1336
		foreach ($additionalTablesFetch as $additionalTable) {
1337
			$relatedTables = $this->_getRelatedTablesByInheritance($additionalTable);
1338
			$tableGroupName = $this->getTableGroupName($relatedTables);
1339
			foreach ($relatedTables as $table) {
1340
				$tableGroups[$table] = $tableGroupName;
1341
			}
1342
			$allFetchedTables = array_merge($allFetchedTables, $relatedTables);
1343
		}
1344
1345
		// Let's remove any duplicate
1346
		$allFetchedTables = array_flip(array_flip($allFetchedTables));
1347
1348
		$columnsList = [];
1349
		$columnDescList = [];
1350
		$schema = $this->tdbmSchemaAnalyzer->getSchema();
1351
1352
		// Now, let's build the column list
1353
		foreach ($allFetchedTables as $table) {
1354
			foreach ($schema->getTable($table)->getColumns() as $column) {
1355
				$columnName = $column->getName();
1356
				$columnDescList[] = [
1357
					'as' => $table.'____'.$columnName,
1358
					'table' => $table,
1359
					'column' => $columnName,
1360
					'type' => $column->getType(),
1361
					'tableGroup' => $tableGroups[$table]
1362
				];
1363
				$columnsList[] = $this->connection->quoteIdentifier($table).'.'.$this->connection->quoteIdentifier($columnName).' as '.
1364
					$this->connection->quoteIdentifier($table.'____'.$columnName);
1365
			}
1366
		}
1367
1368
		$sql = "SELECT DISTINCT ".implode(', ', $columnsList)." FROM MAGICJOIN(".$mainTable.")";
1369
		$countSql = "SELECT COUNT(1) FROM MAGICJOIN(".$mainTable.")";
1370
1371
		if (!empty($filterString)) {
1372
			$sql .= " WHERE ".$filterString;
1373
			$countSql .= " WHERE ".$filterString;
1374
		}
1375
1376
		if (!empty($orderString)) {
1377
			$sql .= " ORDER BY ".$orderString;
1378
			$countSql .= " ORDER BY ".$orderString;
1379
		}
1380
1381 View Code Duplication
		if ($mode !== null && $mode !== self::MODE_CURSOR && $mode !== self::MODE_ARRAY) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $mode (string) and self::MODE_CURSOR (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $mode (string) and self::MODE_ARRAY (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1382
			throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
1383
		}
1384
1385
		$mode = $mode?:$this->mode;
1386
1387
		return new ResultIterator($sql, $countSql, $parameters, $columnDescList, $this->objectStorage, $className, $this, $this->magicQuery, $mode);
1388
	}
1389
1390
	/**
1391
	 * @param $table
1392
	 * @param array $primaryKeys
1393
	 * @param array $additionalTablesFetch
1394
	 * @param bool $lazy Whether to perform lazy loading on this object or not.
1395
	 * @param string $className
1396
	 * @return AbstractTDBMObject
1397
	 * @throws TDBMException
1398
	 */
1399
	public function findObjectByPk($table, array $primaryKeys, array $additionalTablesFetch = array(), $lazy = false, $className=null) {
1400
		$primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys);
1401
		$hash = $this->getObjectHash($primaryKeys);
1402
1403
		if ($this->objectStorage->has($table, $hash)) {
1404
			$dbRow = $this->objectStorage->get($table, $hash);
1405
			$bean = $dbRow->getTDBMObject();
1406
			if ($className !== null && !is_a($bean, $className)) {
1407
				throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'");
1408
			}
1409
			return $bean;
1410
		}
1411
1412
		// Are we performing lazy fetching?
1413
		if ($lazy === true) {
1414
			// Can we perform lazy fetching?
1415
			$tables = $this->_getRelatedTablesByInheritance($table);
1416
			// Only allowed if no inheritance.
1417
			if (count($tables) === 1) {
1418
				if ($className === null) {
1419
					$className = isset($this->tableToBeanMap[$table])?$this->tableToBeanMap[$table]:"Mouf\\Database\\TDBM\\TDBMObject";
1420
				}
1421
1422
				// Let's construct the bean
1423
				if (!isset($reflectionClassCache[$className])) {
0 ignored issues
show
Bug introduced by
The variable $reflectionClassCache seems only to be defined at a later point. As such the call to isset() seems to always evaluate to false.

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

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

Loading history...
1424
					$reflectionClassCache[$className] = new \ReflectionClass($className);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$reflectionClassCache was never initialized. Although not strictly required by PHP, it is generally a good practice to add $reflectionClassCache = array(); before regardless.

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

Loading history...
1425
				}
1426
				// Let's bypass the constructor when creating the bean!
1427
				$bean = $reflectionClassCache[$className]->newInstanceWithoutConstructor();
1428
				/* @var $bean AbstractTDBMObject */
1429
				$bean->_constructLazy($table, $primaryKeys, $this);
1430
			}
1431
		}
1432
1433
		// Did not find the object in cache? Let's query it!
1434
		return $this->findObjectOrFail($table, $primaryKeys, [], $additionalTablesFetch, $className);
0 ignored issues
show
Documentation introduced by
$primaryKeys is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1435
	}
1436
1437
	/**
1438
	 * Returns a unique bean (or null) according to the filters passed in parameter.
1439
	 *
1440
	 * @param string $mainTable The name of the table queried
1441
	 * @param string|null $filterString 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)
1442
	 * @param array $parameters
1443
	 * @param array $additionalTablesFetch
1444
	 * @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.
1445
	 * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters.
1446
	 * @throws TDBMException
1447
	 */
1448
	public function findObject($mainTable, $filterString=null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null) {
1449
		$objects = $this->findObjects($mainTable, $filterString, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className);
1450
		$page = $objects->take(0, 2);
1451
		$count = $page->count();
1452
		if ($count > 1) {
1453
			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.");
1454
		} elseif ($count === 0) {
1455
			return null;
1456
		}
1457
		return $objects[0];
1458
	}
1459
1460
	/**
1461
	 * Returns a unique bean according to the filters passed in parameter.
1462
	 * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1463
	 *
1464
	 * @param string $mainTable The name of the table queried
1465
	 * @param string|null $filterString 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)
1466
	 * @param array $parameters
1467
	 * @param array $additionalTablesFetch
1468
	 * @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.
1469
	 * @return AbstractTDBMObject The object we want
1470
	 * @throws TDBMException
1471
	 */
1472
	public function findObjectOrFail($mainTable, $filterString=null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null) {
1473
		$bean = $this->findObject($mainTable, $filterString, $parameters, $additionalTablesFetch, $className);
1474
		if ($bean === null) {
1475
			throw new NoBeanFoundException("No result found for query on table '".$mainTable."'");
1476
		}
1477
		return $bean;
1478
	}
1479
1480
	/**
1481
	 * @param array $beanData An array of data: array<table, array<column, value>>
1482
	 * @return array an array with first item = class name and second item = table name
1483
	 */
1484
	public function _getClassNameFromBeanData(array $beanData) {
1485
		if (count($beanData) === 1) {
1486
			$tableName = array_keys($beanData)[0];
1487
		} else {
1488
			foreach ($beanData as $table => $row) {
1489
				$tables = [];
1490
				$primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1491
				$pkSet = false;
1492
				foreach ($primaryKeyColumns as $columnName) {
1493
					if ($row[$columnName] !== null) {
1494
						$pkSet = true;
1495
						break;
1496
					}
1497
				}
1498
				if ($pkSet) {
1499
					$tables[] = $table;
1500
				}
1501
			}
1502
1503
			// $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1504
			$allTables = $this->_getLinkBetweenInheritedTables($tables);
0 ignored issues
show
Bug introduced by
The variable $tables does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1505
			$tableName = $allTables[0];
1506
		}
1507
1508
		// Only one table in this bean. Life is sweat, let's look at its type:
1509
		if (isset($this->tableToBeanMap[$tableName])) {
1510
			return [$this->tableToBeanMap[$tableName], $tableName];
1511
		} else {
1512
			return ["Mouf\\Database\\TDBM\\TDBMObject", $tableName];
1513
		}
1514
	}
1515
1516
	/**
1517
	 * Returns an item from cache or computes it using $closure and puts it in cache.
1518
	 *
1519
	 * @param string   $key
1520
	 * @param callable $closure
1521
	 *
1522
	 * @return mixed
1523
	 */
1524
	private function fromCache($key, callable $closure)
1525
	{
1526
		$item = $this->cache->fetch($key);
1527
		if ($item === false) {
1528
			$item = $closure();
1529
			$this->cache->save($key, $item);
1530
		}
1531
1532
		return $item;
1533
	}
1534
1535
	/**
1536
	 * Returns the foreign key object.
1537
	 * @param string $table
1538
	 * @param string $fkName
1539
	 * @return ForeignKeyConstraint
1540
	 */
1541
	public function _getForeignKeyByName($table, $fkName) {
1542
		return $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getForeignKey($fkName);
1543
	}
1544
1545
	/**
1546
	 * @param $pivotTableName
1547
	 * @param AbstractTDBMObject $bean
1548
	 * @return AbstractTDBMObject[]
1549
	 */
1550
	public function _getRelatedBeans($pivotTableName, AbstractTDBMObject $bean) {
1551
1552
        list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $bean);
1553
        /* @var $localFk ForeignKeyConstraint */
1554
        /* @var $remoteFk ForeignKeyConstraint */
1555
        $remoteTable = $remoteFk->getForeignTableName();
1556
1557
1558
        $primaryKeys = $this->getPrimaryKeyValues($bean);
1559
        $columnNames = array_map(function($name) use ($pivotTableName) { return $pivotTableName.'.'.$name; }, $localFk->getLocalColumns());
1560
1561
        $filter = array_combine($columnNames, $primaryKeys);
1562
1563
        return $this->findObjects($remoteTable, $filter);
1564
	}
1565
1566
    /**
1567
     * @param $pivotTableName
1568
     * @param AbstractTDBMObject $bean The LOCAL bean
1569
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean.
1570
     * @throws TDBMException
1571
     */
1572
    private function getPivotTableForeignKeys($pivotTableName, AbstractTDBMObject $bean) {
1573
        $fks = array_values($this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
1574
        $table1 = $fks[0]->getForeignTableName();
1575
        $table2 = $fks[1]->getForeignTableName();
1576
1577
        $beanTables = array_map(function(DbRow $dbRow) { return $dbRow->_getDbTableName(); }, $bean->_getDbRows());
1578
1579
        if (in_array($table1, $beanTables)) {
1580
            return [$fks[0], $fks[1]];
1581
        } elseif (in_array($table2, $beanTables)) {
1582
            return [$fks[1], $fks[0]];
1583
        } else {
1584
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2} for pivot table {$pivotTableName}");
1585
        }
1586
    }
1587
1588
	/**
1589
	 * Returns a list of pivot tables linked to $bean.
1590
	 *
1591
	 * @access private
1592
	 * @param AbstractTDBMObject $bean
1593
	 * @return string[]
1594
	 */
1595
	public function _getPivotTablesLinkedToBean(AbstractTDBMObject $bean) {
1596
		$junctionTables = [];
1597
		$allJunctionTables = $this->schemaAnalyzer->detectJunctionTables();
1598
		foreach ($bean->_getDbRows() as $dbRow) {
1599
			foreach ($allJunctionTables as $table) {
1600
				// There are exactly 2 FKs since this is a pivot table.
1601
				$fks = array_values($table->getForeignKeys());
1602
1603
				if ($fks[0]->getForeignTableName() === $dbRow->_getDbTableName() || $fks[1]->getForeignTableName() === $dbRow->_getDbTableName()) {
1604
					$junctionTables[] = $table->getName();
1605
				}
1606
			}
1607
		}
1608
1609
		return $junctionTables;
1610
	}
1611
}
1612