Completed
Pull Request — 3.4 (#46)
by David
20:58
created

TDBMService::flatten_paths()   C

Complexity

Conditions 10
Paths 8

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 24
rs 5.2165
cc 10
eloc 17
nc 8
nop 1

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 Copyright (C) 2006-2015 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 Mouf\Database\MagicQuery;
30
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
31
use Mouf\Database\TDBM\Filters\OrderBySQLString;
32
use Mouf\Database\TDBM\Utils\TDBMDaoGenerator;
33
use Mouf\Utils\Cache\CacheInterface;
34
use SQLParser\Node\ColRef;
35
36
/**
37
 * The TDBMService class is the main TDBM class. It provides methods to retrieve TDBMObject instances
38
 * from the database.
39
 *
40
 * @author David Negrier
41
 * @ExtendedAction {"name":"Generate DAOs", "url":"tdbmadmin/", "default":false}
42
 */
43
class TDBMService {
44
	
45
	const MODE_CURSOR = 1;
46
	const MODE_ARRAY = 2;
47
	
48
	/**
49
	 * The database connection.
50
	 *
51
	 * @var Connection
52
	 */
53
	private $connection;
54
55
	/**
56
	 * The cache service to cache data.
57
	 *
58
	 * @var CacheInterface
59
	 */
60
	private $cacheService;
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
	 * The default autosave mode for the objects
84
	 * True to automatically save the object.
85
	 * If false, the user must explicitly call the save() method to save the object.
86
	 *
87
	 * @var boolean
88
	 */
89
	//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...
90
91
	/**
92
	 * Cache of table of primary keys.
93
	 * Primary keys are stored by tables, as an array of column.
94
	 * For instance $primary_key['my_table'][0] will return the first column of the primary key of table 'my_table'.
95
	 *
96
	 * @var string[]
97
	 */
98
	private $primaryKeysColumns;
99
100
	/**
101
	 * Whether we should track execution time or not.
102
	 * If true, if the execution time reaches 90% of the allowed execution time, the request will stop with an exception.
103
	 *
104
	 * @var bool
105
	 */
106
	private $trackExecutionTime = true;
107
108
	/**
109
	 * Service storing objects in memory.
110
	 * Access is done by table name and then by primary key.
111
	 * If the primary key is split on several columns, access is done by an array of columns, serialized.
112
	 * 
113
	 * @var StandardObjectStorage|WeakrefObjectStorage
114
	 */
115
	private $objectStorage;
116
	
117
	/**
118
	 * The fetch mode of the result sets returned by `getObjects`.
119
	 * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY
120
	 *
121
	 * In 'MODE_ARRAY' mode (default), the result is an array. Use this mode by default (unless the list returned is very big).
122
	 * 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,
123
	 * and it cannot be accessed via key. Use this mode for large datasets processed by batch.
124
	 * In 'MODE_COMPATIBLE_ARRAY' mode, the result is an old TDBMObjectArray (used up to TDBM 3.2). 
125
	 * You can access the array by key, or using foreach, several times.
126
	 *
127
	 * @var int
128
	 */
129
	private $mode = self::MODE_ARRAY;
130
131
	/**
132
	 * Table of new objects not yet inserted in database or objects modified that must be saved.
133
	 * @var DbRow[]
134
	 */
135
	private $toSaveObjects = array();
136
137
	/// The timestamp of the script startup. Useful to stop execution before time limit is reached and display useful error message.
138
	public static $script_start_up_time;
139
140
	/// True if the program is exiting (we are in the "exit" statement). False otherwise.
141
	private $is_program_exiting = false;
142
143
	/**
144
	 * The content of the cache variable.
145
	 *
146
	 * @var array<string, mixed>
147
	 */
148
	private $cache;
149
150
	private $cacheKey = "__TDBM_Cache__";
151
152
	/**
153
	 * Map associating a table name to a fully qualified Bean class name
154
	 * @var array
155
	 */
156
	private $tableToBeanMap = [];
157
158
	/**
159
	 * @var \ReflectionClass[]
160
	 */
161
	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...
162
163
	/**
164
	 * @param Connection $connection The DBAL DB connection to use
165
	 * @param Cache|null $cache A cache service to be used
166
	 * @param SchemaAnalyzer $schemaAnalyzer The schema analyzer that will be used to find shortest paths...
167
	 * 										 Will be automatically created if not passed.
168
	 */
169
	public function __construct(Connection $connection, Cache $cache = null, SchemaAnalyzer $schemaAnalyzer = null) {
170
		//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...
171
		if (extension_loaded('weakref')) {
172
			$this->objectStorage = new WeakrefObjectStorage();
173
		} else {
174
			$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...
175
		}
176
		$this->connection = $connection;
177
		if ($cache !== null) {
178
			$this->cache = $cache;
179
		} else {
180
			$this->cache = new VoidCache();
181
		}
182
		if ($schemaAnalyzer) {
183
			$this->schemaAnalyzer = $schemaAnalyzer;
184
		} else {
185
			$this->schemaAnalyzer = new SchemaAnalyzer($this->connection->getSchemaManager(), $this->cache, $this->getConnectionUniqueId());
186
		}
187
188
		$this->magicQuery = new MagicQuery($this->connection, $this->cache, $this->schemaAnalyzer);
189
190
		$this->tdbmSchemaAnalyzer = new TDBMSchemaAnalyzer($connection, $this->cache, $this->schemaAnalyzer);
191
		$this->cachePrefix = $this->tdbmSchemaAnalyzer->getCachePrefix();
192
193
		if (self::$script_start_up_time === null) {
194
			self::$script_start_up_time = microtime(true);
195
		}
196
197
	}
198
199
200
	/**
201
	 * Returns the object used to connect to the database.
202
	 *
203
	 * @return Connection
204
	 */
205
	public function getConnection() {
206
		return $this->connection;
207
	}
208
209
	/**
210
	 * Creates a unique cache key for the current connection.
211
	 * @return string
212
	 */
213
	private function getConnectionUniqueId() {
214
		return hash('md4', $this->connection->getHost()."-".$this->connection->getPort()."-".$this->connection->getDatabase()."-".$this->connection->getDriver()->getName());
215
	}
216
217
	/**
218
	 * Returns true if the objects will save automatically by default,
219
	 * false if an explicit call to save() is required.
220
	 *
221
	 * The behaviour can be overloaded by setAutoSaveMode on each object.
222
	 *
223
	 * @return boolean
224
	 */
225
	/*public function getDefaultAutoSaveMode() {
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% 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...
226
		return $this->autosave_default;
227
	}*/
228
229
	/**
230
	 * Sets the autosave mode:
231
	 * true if the object will save automatically,
232
	 * false if an explicit call to save() is required.
233
	 *
234
	 * @Compulsory
235
	 * @param boolean $autoSave
236
	 */
237
	/*public function setDefaultAutoSaveMode($autoSave = true) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% 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...
238
		$this->autosave_default = $autoSave;
239
	}*/
240
241
	/**
242
	 * Sets the fetch mode of the result sets returned by `getObjects`.
243
	 * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY
244
	 *
245
	 * 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).
246
	 * 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
247
	 * several times. In cursor mode, you cannot access the result set by key. Use this mode for large datasets processed by batch.
248
	 *
249
	 * @param int $mode
250
	 */
251
	public function setFetchMode($mode) {
252 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...
253
			throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
254
		}
255
		$this->mode = $mode;
256
		return $this;
257
	}
258
259
	/**
260
	 * Whether we should track execution time or not.
261
	 * If true, if the execution time reaches 90% of the allowed execution time, the request will stop with an exception.
262
	 *
263
	 * @param boolean $trackExecutionTime
264
	 */
265
	public function setTrackExecutionTime($trackExecutionTime = true) {
266
		$this->trackExecutionTime = $trackExecutionTime;
267
	}
268
269
270
	/**
271
	 * Loads the cache and stores it (to be reused in this instance).
272
	 * Note: the cache is not returned. It is stored in the $cache instance variable.
273
	 */
274
	/*private function loadCache() {
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% 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...
275
		// TODO: evaluate for 4.0
276
		if ($this->cache == null) {
277
			if ($this->cacheService == null) {
278
				throw new TDBMException("A cache service must be explicitly bound to the TDBM Service. Please configure your instance of TDBM Service.");
279
			}
280
			$this->cache = $this->cacheService->get($this->cacheKey);
281
		}
282
	}*/
283
284
	/**
285
	 * Saves the cache.
286
	 *
287
	 */
288
	private function saveCache() {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
289
		// TODO: evaluate for 4.0
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% 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...
290
		$this->cacheService->set($this->cacheKey, $this->cache);
291
	}
292
293
	/**
294
	 * Returns a TDBMObject associated from table "$table_name".
295
	 * If the $filters parameter is an int/string, the object returned will be the object whose primary key = $filters.
296
	 * $filters can also be a set of TDBM_Filters (see the getObjects method for more details).
297
	 *
298
	 * For instance, if there is a table 'users', with a primary key on column 'user_id' and a column 'user_name', then
299
	 * 			$user = $tdbmService->getObject('users',1);
300
	 * 			echo $user->name;
301
	 * will return the name of the user whose user_id is one.
302
	 *
303
	 * 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.
304
	 * For instance:
305
	 * 			$group = $tdbmService->getObject('groups',array(1,2));
306
	 *
307
	 * Note that TDBMObject performs caching for you. If you get twice the same object, the reference of the object you will get
308
	 * will be the same.
309
	 *
310
	 * For instance:
311
	 * 			$user1 = $tdbmService->getObject('users',1);
312
	 * 			$user2 = $tdbmService->getObject('users',1);
313
	 * 			$user1->name = 'John Doe';
314
	 * 			echo $user2->name;
315
	 * will return 'John Doe'.
316
	 *
317
	 * You can use filters instead of passing the primary key. For instance:
318
	 * 			$user = $tdbmService->getObject('users',new EqualFilter('users', 'login', 'jdoe'));
319
	 * This will return the user whose login is 'jdoe'.
320
	 * Please note that if 2 users have the jdoe login in database, the method will throw a TDBM_DuplicateRowException.
321
	 *
322
	 * Also, you can specify the return class for the object (provided the return class extends TDBMObject).
323
	 * For instance:
324
	 *  	$user = $tdbmService->getObject('users',1,'User');
325
	 * will return an object from the "User" class. The "User" class must extend the "TDBMObject" class.
326
	 * Please be sure not to override any method or any property unless you perfectly know what you are doing!
327
	 *
328
	 * @param string $table_name The name of the table we retrieve an object from.
329
	 * @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)
330
	 * @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.
331
	 * @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.
332
	 * @return TDBMObject
333
	 */
334
/*	public function getObject($table_name, $filters, $className = null, $lazy_loading = false) {
335
336
		if (is_array($filters) || $filters instanceof FilterInterface) {
337
			$isFilterBag = false;
338
			if (is_array($filters)) {
339
				// Is this a multiple primary key or a filter bag?
340
				// Let's have a look at the first item of the array to decide.
341
				foreach ($filters as $filter) {
342
					if (is_array($filter) || $filter instanceof FilterInterface) {
343
						$isFilterBag = true;
344
					}
345
					break;
346
				}
347
			} else {
348
				$isFilterBag = true;
349
			}
350
				
351
			if ($isFilterBag == true) {
352
				// If a filter bag was passer in parameter, let's perform a getObjects.
353
				$objects = $this->getObjects($table_name, $filters, null, null, null, $className);
354
				if (count($objects) == 0) {
355
					return null;
356
				} elseif (count($objects) > 1) {
357
					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.");
358
				}
359
				// Return the first and only object.
360
				if ($objects instanceof \Generator) {
361
					return $objects->current();
362
				} else {
363
					return $objects[0];
364
				}
365
			}
366
		}
367
		$id = $filters;
368
		if ($this->connection == null) {
369
			throw new TDBMException("Error while calling TdbmService->getObject(): No connection has been established on the database!");
370
		}
371
		$table_name = $this->connection->toStandardcase($table_name);
372
373
		// If the ID is null, let's throw an exception
374
		if ($id === null) {
375
			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.");
376
		}
377
378
		// If the primary key is split over many columns, the IDs are passed in an array. Let's serialize this array to store it.
379
		if (is_array($id)) {
380
			$id = serialize($id);
381
		}
382
383
		if ($className === null) {
384
			if (isset($this->tableToBeanMap[$table_name])) {
385
				$className = $this->tableToBeanMap[$table_name];
386
			} else {
387
				$className = "Mouf\\Database\\TDBM\\TDBMObject";
388
			}
389
		}
390
391
		if ($this->objectStorage->has($table_name, $id)) {
392
			$obj = $this->objectStorage->get($table_name, $id);
393
			if (is_a($obj, $className)) {
394
				return $obj;
395
			} else {
396
				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'");
397
			}
398
		}
399
400
		if ($className != "Mouf\\Database\\TDBM\\TDBMObject" && !is_subclass_of($className, "Mouf\\Database\\TDBM\\TDBMObject")) {
401
			if (!class_exists($className)) {
402
				throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." does not exist.");
403
			} else {
404
				throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." should extend TDBMObject.");
405
			}
406
		}
407
		$obj = new $className($this, $table_name, $id);
408
409
		if ($lazy_loading == false) {
410
			// If we are not doing lazy loading, let's load the object:
411
			$obj->_dbLoadIfNotLoaded();
412
		}
413
414
		$this->objectStorage->set($table_name, $id, $obj);
415
416
		return $obj;
417
	}*/
418
419
	/**
420
	 * Creates a new object that will be stored in table "table_name".
421
	 * If $auto_assign_id is true, the primary key of the object will be automatically be filled.
422
	 * Otherwise, the database system or the user will have to fill it itself (for exemple with
423
	 * AUTOINCREMENT in MySQL or with a sequence in POSTGRESQL).
424
	 * Please note that $auto_assign_id parameter is ignored if the primary key is autoincremented (MySQL only)
425
	 * Also, please note that $auto_assign_id does not work on tables that have primary keys on multiple
426
	 * columns.
427
	 *
428
	 * @param string $table_name
429
	 * @param boolean $auto_assign_id
430
	 * @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.
431
	 * @return TDBMObject
432
	 */
433
	public function getNewObject($table_name, $auto_assign_id=true, $className = null) {
434
		if ($this->connection == null) {
435
			throw new TDBMException("Error while calling TDBMObject::getNewObject(): No connection has been established on the database!");
436
		}
437
		$table_name = $this->connection->toStandardcase($table_name);
0 ignored issues
show
Bug introduced by
The method toStandardcase() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
438
439
		// Ok, let's verify that the table does exist:
440
		try {
441
			/*$data =*/ $this->connection->getTableInfo($table_name);
0 ignored issues
show
Bug introduced by
The method getTableInfo() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
442
		} catch (TDBMException $exception) {
443
			$probable_table_name = $this->connection->checkTableExist($table_name);
0 ignored issues
show
Bug introduced by
The method checkTableExist() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
444
			if ($probable_table_name == null)
445
			throw new TDBMException("Error while calling TDBMObject::getNewObject(): The table named '$table_name' does not exist.");
446
			else
447
			throw new TDBMException("Error while calling TDBMObject::getNewObject(): The table named '$table_name' does not exist. Maybe you meant the table '$probable_table_name'.");
448
		}
449
450
		if ($className === null) {
451
			if (isset($this->tableToBeanMap[$table_name])) {
452
				$className = $this->tableToBeanMap[$table_name];
453
			} else {
454
				$className = "Mouf\\Database\\TDBM\\TDBMObject";
455
			}
456
		}
457
458
		if (!is_string($className)) {
459
			throw new TDBMException("Error while calling TDBMObject::getNewObject(): The third parameter should be a string representing a class name to instantiate.");
460
		}
461
		if (!is_a($className, "Mouf\\Database\\TDBM\\TDBMObject", true)) {
462
			throw new TDBMException("Error while calling TDBMObject::getNewObject(): The class ".$className." should extend TDBMObject.");
463
		}
464
		$object = new $className($this, $table_name);
465
466
		if ($auto_assign_id && !$this->isPrimaryKeyAutoIncrement($table_name)) {
0 ignored issues
show
Bug introduced by
The method isPrimaryKeyAutoIncrement() does not seem to exist on object<Mouf\Database\TDBM\TDBMService>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
467
			$pk_table =  $this->getPrimaryKeyColumns($table_name);
468
			if (count($pk_table)==1)
469
			{
470
				$root_table = $this->connection->findRootSequenceTable($table_name);
0 ignored issues
show
Bug introduced by
The method findRootSequenceTable() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
471
				$id = $this->connection->nextId($root_table);
0 ignored issues
show
Bug introduced by
The method nextId() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
472
				// If $id == 1, it is likely that the sequence was just created.
473
				// However, there might be already some data in the database. We will check the biggest ID in the table.
474
				if ($id == 1) {
475
					$sql = "SELECT MAX(".$this->connection->escapeDBItem($pk_table[0]).") AS maxkey FROM ".$root_table;
0 ignored issues
show
Bug introduced by
The method escapeDBItem() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
476
					$res = $this->connection->getAll($sql);
0 ignored issues
show
Bug introduced by
The method getAll() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
477
					// NOTE: this will work only if the ID is an integer!
478
					$newid = $res[0]['maxkey'] + 1;
479
					if ($newid>$id) {
480
						$id = $newid;
481
					}
482
					$this->connection->setSequenceId($root_table, $id);
0 ignored issues
show
Bug introduced by
The method setSequenceId() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
483
				}
484
485
				$object->TDBMObject_id = $id;
486
487
				$object->db_row[$pk_table[0]] = $object->TDBMObject_id;
488
			}
489
		}
490
491
		$this->_addToToSaveObjectList($object);
492
493
		return $object;
494
	}
495
496
	/**
497
	 * Removes the given object from database.
498
	 * This cannot be called on an object that is not attached to this TDBMService
499
	 * (will throw a TDBMInvalidOperationException)
500
	 *
501
	 * @param AbstractTDBMObject $object the object to delete.
502
	 * @throws TDBMException
503
	 * @throws TDBMInvalidOperationException
504
	 */
505
	public function delete(AbstractTDBMObject $object) {
506
		switch ($object->_getStatus()) {
507
			case TDBMObjectStateEnum::STATE_DELETED:
508
				// Nothing to do, object already deleted.
509
				return;
510
			case TDBMObjectStateEnum::STATE_DETACHED:
511
				throw new TDBMInvalidOperationException('Cannot delete a detached object');
512
			case TDBMObjectStateEnum::STATE_NEW:
513
                $this->deleteManyToManyRelationships($object);
514
				foreach ($object->_getDbRows() as $dbRow) {
515
					$this->removeFromToSaveObjectList($dbRow);
516
				}
517
				break;
518
			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...
519
				foreach ($object->_getDbRows() as $dbRow) {
520
					$this->removeFromToSaveObjectList($dbRow);
521
				}
522
			case TDBMObjectStateEnum::STATE_NOT_LOADED:
523
			case TDBMObjectStateEnum::STATE_LOADED:
524
                $this->deleteManyToManyRelationships($object);
525
				// Let's delete db rows, in reverse order.
526
				foreach (array_reverse($object->_getDbRows()) as $dbRow) {
527
					$tableName = $dbRow->_getDbTableName();
528
					$primaryKeys = $dbRow->_getPrimaryKeys();
529
530
					$this->connection->delete($tableName, $primaryKeys);
531
532
					$this->objectStorage->remove($dbRow->_getDbTableName(), $this->getObjectHash($primaryKeys));
533
				}
534
				break;
535
			// @codeCoverageIgnoreStart
536
			default:
537
				throw new TDBMInvalidOperationException('Unexpected status for bean');
538
			// @codeCoverageIgnoreEnd
539
		}
540
541
		$object->_setStatus(TDBMObjectStateEnum::STATE_DELETED);
542
	}
543
544
    /**
545
     * Removes all many to many relationships for this object.
546
     * @param AbstractTDBMObject $object
547
     */
548
    private function deleteManyToManyRelationships(AbstractTDBMObject $object) {
549
        foreach ($object->_getDbRows() as $tableName => $dbRow) {
550
            $pivotTables = $this->tdbmSchemaAnalyzer->getPivotTableLinkedToTable($tableName);
551
            foreach ($pivotTables as $pivotTable) {
552
                $remoteBeans = $object->_getRelationships($pivotTable);
553
                foreach ($remoteBeans as $remoteBean) {
554
                    $object->_removeRelationship($pivotTable, $remoteBean);
555
                }
556
            }
557
        }
558
        $this->persistManyToManyRelationships($object);
559
    }
560
561
562
    /**
563
     * This function removes the given object from the database. It will also remove all objects relied to the one given
564
     * by parameter before all.
565
     *
566
     * Notice: if the object has a multiple primary key, the function will not work.
567
     *
568
     * @param AbstractTDBMObject $objToDelete
569
     */
570
    public function deleteCascade(AbstractTDBMObject $objToDelete) {
571
        $this->deleteAllConstraintWithThisObject($objToDelete);
0 ignored issues
show
Compatibility introduced by
$objToDelete of type object<Mouf\Database\TDBM\AbstractTDBMObject> is not a sub-type of object<Mouf\Database\TDBM\TDBMObject>. It seems like you assume a child class of the class Mouf\Database\TDBM\AbstractTDBMObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
572
        $this->delete($objToDelete);
573
    }
574
575
    /**
576
     * This function is used only in TDBMService (private function)
577
     * It will call deleteCascade function foreach object relied with a foreign key to the object given by parameter
578
     *
579
     * @param TDBMObject $obj
580
     * @return TDBMObjectArray
581
     */
582
    private function deleteAllConstraintWithThisObject(TDBMObject $obj) {
583
        $tableFrom = $this->connection->escapeDBItem($obj->_getDbTableName());
0 ignored issues
show
Bug introduced by
The method _getDbTableName() does not seem to exist on object<Mouf\Database\TDBM\TDBMObject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method escapeDBItem() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
584
        $constraints = $this->connection->getConstraintsFromTable($tableFrom);
0 ignored issues
show
Bug introduced by
The method getConstraintsFromTable() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
585
        foreach ($constraints as $constraint) {
586
            $tableTo = $this->connection->escapeDBItem($constraint["table1"]);
0 ignored issues
show
Bug introduced by
The method escapeDBItem() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
587
            $colFrom = $this->connection->escapeDBItem($constraint["col2"]);
0 ignored issues
show
Bug introduced by
The method escapeDBItem() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
588
            $colTo = $this->connection->escapeDBItem($constraint["col1"]);
0 ignored issues
show
Bug introduced by
The method escapeDBItem() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
589
            $idVarName = $this->connection->escapeDBItem($obj->getPrimaryKey()[0]);
0 ignored issues
show
Bug introduced by
The method getPrimaryKey() does not exist on Mouf\Database\TDBM\TDBMObject. Did you maybe mean getPrimaryKeyWhereStatement()?

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

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

Loading history...
Bug introduced by
The method escapeDBItem() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
590
            $idValue = $this->connection->quoteSmart($obj->TDBMObject_id);
0 ignored issues
show
Documentation introduced by
The property TDBMObject_id does not exist on object<Mouf\Database\TDBM\TDBMObject>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Bug introduced by
The method quoteSmart() does not seem to exist on object<Doctrine\DBAL\Connection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
591
            $sql = "SELECT DISTINCT ".$tableTo.".*"
592
                    ." FROM ".$tableFrom
593
                    ." LEFT JOIN ".$tableTo." ON ".$tableFrom.".".$colFrom." = ".$tableTo.".".$colTo
594
                    ." WHERE ".$tableFrom.".".$idVarName."=".$idValue;
595
            $result = $this->getObjectsFromSQL($constraint["table1"], $sql);
0 ignored issues
show
Bug introduced by
The method getObjectsFromSQL() does not seem to exist on object<Mouf\Database\TDBM\TDBMService>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
596
            foreach ($result as $tdbmObj) {
597
                $this->deleteCascade($tdbmObj);
598
            }
599
        }
600
    }
601
602
	/**
603
	 * This function performs a save() of all the objects that have been modified.
604
	 * This function is automatically called at the end of your script, so you don't have to call it yourself.
605
	 *
606
	 * Note: if you want to catch or display efficiently any error that might happen, you might want to call this
607
	 * method explicitly and to catch any TDBMException that it might throw like this:
608
	 *
609
	 * try {
610
	 * 		TDBMObject::completeSave();
611
	 * } catch (TDBMException e) {
612
	 * 		// Do something here.
613
	 * }
614
	 *
615
	 */
616
	public function completeSave() {
617
618
		foreach ($this->toSaveObjects as $object)
619
		{
620
			if (!$object->db_onerror && $object->db_autosave)
0 ignored issues
show
Bug introduced by
The property db_autosave does not seem to exist in Mouf\Database\TDBM\DbRow.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
621
			{
622
				$this->save($object);
0 ignored issues
show
Documentation introduced by
$object is of type object<Mouf\Database\TDBM\DbRow>, but the function expects a object<Mouf\Database\TDBM\AbstractTDBMObject>.

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...
623
			}
624
		}
625
626
	}
627
628
	/**
629
	 * This function performs a save() of all the objects that have been modified just before the program exits.
630
	 * It should never be called by the user, the program will call it directly.
631
	 *
632
	 */
633
	/*public function completeSaveOnExit() {
634
		$this->is_program_exiting = true;
635
		$this->completeSave();
636
637
		// Now, let's commit or rollback if needed.
638
		if ($this->connection != null && $this->connection->hasActiveTransaction()) {
639
			if ($this->commitOnQuit) {
640
				try  {
641
					$this->connection->commit();
642
				} catch (Exception $e) {
643
					echo $e->getMessage()."<br/>";
644
					echo $e->getTraceAsString();
645
				}
646
			} else {
647
				try  {
648
					$this->connection->rollback();
649
				} catch (Exception $e) {
650
					echo $e->getMessage()."<br/>";
651
					echo $e->getTraceAsString();
652
				}
653
			}
654
		}
655
	}*/
656
657
	/**
658
	 * Function used internally by TDBM.
659
	 * Returns true if the program is exiting.
660
	 *
661
	 * @return bool
662
	 */
663
	public function isProgramExiting() {
664
		return $this->is_program_exiting;
665
	}
666
667
	/**
668
	 * This function performs a save() of all the objects that have been modified, then it sets all the data to a not loaded state.
669
	 * Therefore, the database will be queried again next time we access the object. Meanwhile, if another process modifies the database,
670
	 * the changes will be retrieved when we access the object again.
671
	 *
672
	 */
673
	/*public function completeSaveAndFlush() {
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
674
		$this->completeSave();
675
676
		$this->objectStorage->apply(function(TDBMObject $object) {
677
			/* @var $object TDBMObject * /
678
			if (!$object->db_onerror && $object->_getStatus() == TDBMObjectStateEnum::STATE_LOADED)
679
			{
680
				$object->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED);
681
			}
682
		});
683
	}
684
*/
685
686
687
	/**
688
	 * Returns an array of objects of "table_name" kind filtered from the filter bag.
689
	 *
690
	 * The getObjects method should be the most used query method in TDBM if you want to query the database for objects.
691
	 * (Note: if you want to query the database for an object by its primary key, use the getObject method).
692
	 *
693
	 * The getObjects method takes in parameter:
694
	 * 	- table_name: the kinf of TDBMObject you want to retrieve. In TDBM, a TDBMObject matches a database row, so the
695
	 * 			$table_name parameter should be the name of an existing table in database.
696
	 *  - filter_bag: The filter bag is anything that you can use to filter your request. It can be a SQL Where clause,
697
	 * 			a series of TDBM_Filter objects, or even TDBMObjects or TDBMObjectArrays that you will use as filters.
698
	 *  - order_bag: The order bag is anything that will be used to order the data that is passed back to you.
699
	 * 			A SQL Order by clause can be used as an order bag for instance, or a OrderByColumn object
700
	 * 	- from (optionnal): The offset from which the query should be performed. For instance, if $from=5, the getObjects method
701
	 * 			will return objects from the 6th rows.
702
	 * 	- limit (optionnal): The maximum number of objects to return. Used together with $from, you can implement
703
	 * 			paging mechanisms.
704
	 *  - hint_path (optionnal): EXPERTS ONLY! The path the request should use if not the most obvious one. This parameter
705
	 * 			should be used only if you perfectly know what you are doing.
706
	 *
707
	 * The getObjects method will return a TDBMObjectArray. A TDBMObjectArray is an array of TDBMObjects that does behave as
708
	 * a single TDBMObject if the array has only one member. Refer to the documentation of TDBMObjectArray and TDBMObject
709
	 * to learn more.
710
	 *
711
	 * More about the filter bag:
712
	 * A filter is anything that can change the set of objects returned by getObjects.
713
	 * There are many kind of filters in TDBM:
714
	 * A filter can be:
715
	 * 	- A SQL WHERE clause:
716
	 * 		The clause is specified without the "WHERE" keyword. For instance:
717
	 * 			$filter = "users.first_name LIKE 'J%'";
718
	 *     	is a valid filter.
719
	 * 	   	The only difference with SQL is that when you specify a column name, it should always be fully qualified with
720
	 * 		the table name: "country_name='France'" is not valid, while "countries.country_name='France'" is valid (if
721
	 * 		"countries" is a table and "country_name" a column in that table, sure.
722
	 * 		For instance,
723
	 * 				$french_users = TDBMObject::getObjects("users", "countries.country_name='France'");
724
	 * 		will return all the users that are French (based on trhe assumption that TDBM can find a way to connect the users
725
	 * 		table to the country table using foreign keys, see the manual for that point).
726
	 * 	- A TDBMObject:
727
	 * 		An object can be used as a filter. For instance, we could get the France object and then find any users related to
728
	 * 		that object using:
729
	 * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
730
	 * 				$french_users = TDBMObject::getObjects("users", $france);
731
	 *  - A TDBMObjectArray can be used as a filter too.
732
	 * 		For instance:
733
	 * 				$french_groups = TDBMObject::getObjects("groups", $french_users);
734
	 * 		might return all the groups in which french users can be found.
735
	 *  - Finally, TDBM_xxxFilter instances can be used.
736
	 * 		TDBM provides the developer a set of TDBM_xxxFilters that can be used to model a SQL Where query.
737
	 * 		Using the appropriate filter object, you can model the operations =,<,<=,>,>=,IN,LIKE,AND,OR, IS NULL and NOT
738
	 * 		For instance:
739
	 * 				$french_users = TDBMObject::getObjects("users", new EqualFilter('countries','country_name','France');
740
	 * 		Refer to the documentation of the appropriate filters for more information.
741
	 *
742
	 * The nice thing about a filter bag is that it can be any filter, or any array of filters. In that case, filters are
743
	 * 'ANDed' together.
744
	 * So a request like this is valid:
745
	 * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
746
	 * 				$french_administrators = TDBMObject::getObjects("users", array($france,"role.role_name='Administrators'");
747
	 * This requests would return the users that are both French and administrators.
748
	 *
749
	 * Finally, if filter_bag is null, the whole table is returned.
750
	 *
751
	 * More about the order bag:
752
	 * The order bag contains anything that can be used to order the data that is passed back to you.
753
	 * The order bag can contain two kinds of objects:
754
	 * 	- A SQL ORDER BY clause:
755
	 * 		The clause is specified without the "ORDER BY" keyword. For instance:
756
	 * 			$orderby = "users.last_name ASC, users.first_name ASC";
757
	 *     	is a valid order bag.
758
	 * 		The only difference with SQL is that when you specify a column name, it should always be fully qualified with
759
	 * 		the table name: "country_name ASC" is not valid, while "countries.country_name ASC" is valid (if
760
	 * 		"countries" is a table and "country_name" a column in that table, sure.
761
	 * 		For instance,
762
	 * 				$french_users = TDBMObject::getObjects("users", null, "countries.country_name ASC");
763
	 * 		will return all the users sorted by country.
764
	 *  - A OrderByColumn object
765
	 * 		This object models a single column in a database.
766
	 *
767
	 * @param string $table_name The name of the table queried
768
	 * @param mixed $filter_bag The filter bag (see above for complete description)
769
	 * @param mixed $orderby_bag The order bag (see above for complete description)
770
	 * @param integer $from The offset
771
	 * @param integer $limit The maximum number of rows returned
772
	 * @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.
773
	 * @param unknown_type $hint_path Hints to get the path for the query (expert parameter, you should leave it to null).
774
	 * @return TDBMObjectArray A TDBMObjectArray containing the resulting objects of the query.
775
	 */
776
/*	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...
777
		if ($this->connection == null) {
778
			throw new TDBMException("Error while calling TDBMObject::getObject(): No connection has been established on the database!");
779
		}
780
		return $this->getObjectsByMode('getObjects', $table_name, $filter_bag, $orderby_bag, $from, $limit, $className, $hint_path);
781
	}*/
782
783
784
	/**
785
	 * Takes in input a filter_bag (which can be about anything from a string to an array of TDBMObjects... see above from documentation),
786
	 * and gives back a proper Filter object.
787
	 *
788
	 * @param mixed $filter_bag
789
	 * @return array First item: filter string, second item: parameters.
790
	 * @throws TDBMException
791
	 */
792
	public function buildFilterFromFilterBag($filter_bag) {
793
		$counter = 1;
794
		if ($filter_bag === null) {
795
			return ['', []];
796
		} elseif (is_string($filter_bag)) {
797
			return [$filter_bag, []];
798
		} elseif (is_array($filter_bag)) {
799
			$sqlParts = [];
800
			$parameters = [];
801
			foreach ($filter_bag as $column => $value) {
802
				$paramName = "tdbmparam".$counter;
803
				if (is_array($value)) {
804
					$sqlParts[] = $this->connection->quoteIdentifier($column)." IN :".$paramName;
805
				} else {
806
					$sqlParts[] = $this->connection->quoteIdentifier($column)." = :".$paramName;
807
				}
808
				$parameters[$paramName] = $value;
809
				$counter++;
810
			}
811
			return [implode(' AND ', $sqlParts), $parameters];
812
		} elseif ($filter_bag instanceof AbstractTDBMObject) {
813
			$dbRows = $filter_bag->_getDbRows();
814
			$dbRow = reset($dbRows);
815
			$primaryKeys = $dbRow->_getPrimaryKeys();
816
817
			foreach ($primaryKeys as $column => $value) {
818
				$paramName = "tdbmparam".$counter;
819
				$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...
820
				$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...
821
				$counter++;
822
			}
823
			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...
824
		} elseif ($filter_bag instanceof \Iterator) {
825
			return $this->buildFilterFromFilterBag(iterator_to_array($filter_bag));
826
		} else {
827
			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.");
828
		}
829
830
//		// 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...
831
//		if ($filter_bag === null) {
832
//			$filter_bag = array();
833
//		} elseif (!is_array($filter_bag)) {
834
//			$filter_bag = array($filter_bag);
835
//		}
836
//		elseif (is_a($filter_bag, 'Mouf\\Database\\TDBM\\TDBMObjectArray')) {
837
//			$filter_bag = array($filter_bag);
838
//		}
839
//
840
//		// Second, let's take all the objects out of the filter bag, and let's make filters from them
841
//		$filter_bag2 = array();
842
//		foreach ($filter_bag as $thing) {
843
//			if (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\FilterInterface')) {
844
//				$filter_bag2[] = $thing;
845
//			} elseif (is_string($thing)) {
846
//				$filter_bag2[] = new SqlStringFilter($thing);
847
//			} elseif (is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && count($thing)>0) {
848
//				// Get table_name and column_name
849
//				$filter_table_name = $thing[0]->_getDbTableName();
850
//				$filter_column_names = $thing[0]->getPrimaryKey();
851
//
852
//				// If there is only one primary key, we can use the InFilter
853
//				if (count($filter_column_names)==1) {
854
//					$primary_keys_array = array();
855
//					$filter_column_name = $filter_column_names[0];
856
//					foreach ($thing as $TDBMObject) {
857
//						$primary_keys_array[] = $TDBMObject->$filter_column_name;
858
//					}
859
//					$filter_bag2[] = new InFilter($filter_table_name, $filter_column_name, $primary_keys_array);
860
//				}
861
//				// else, we must use a (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx)...
862
//				else
863
//				{
864
//					$filter_bag_and = array();
865
//					foreach ($thing as $TDBMObject) {
866
//						$filter_bag_temp_and=array();
867
//						foreach ($filter_column_names as $pk) {
868
//							$filter_bag_temp_and[] = new EqualFilter($TDBMObject->_getDbTableName(), $pk, $TDBMObject->$pk);
869
//						}
870
//						$filter_bag_and[] = new AndFilter($filter_bag_temp_and);
871
//					}
872
//					$filter_bag2[] = new OrFilter($filter_bag_and);
873
//				}
874
//
875
//
876
//			} elseif (!is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && $thing!==null) {
877
//				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.");
878
//			}
879
//		}
880
//
881
//		// Third, let's take all the filters and let's apply a huge AND filter
882
//		$filter = new AndFilter($filter_bag2);
883
//
884
//		return $filter;
885
	}
886
887
	/**
888
	 * Takes in input an order_bag (which can be about anything from a string to an array of OrderByColumn objects... see above from documentation),
889
	 * and gives back an array of OrderByColumn / OrderBySQLString objects.
890
	 *
891
	 * @param unknown_type $orderby_bag
892
	 * @return array
893
	 */
894
	public function buildOrderArrayFromOrderBag($orderby_bag) {
895
		// Fourth, let's apply the same steps to the orderby_bag
896
		// 4-1 orderby_bag should be an array, if it is a singleton, let's put it in an array.
897
898
		if (!is_array($orderby_bag))
899
		$orderby_bag = array($orderby_bag);
900
901
		// 4-2, let's take all the objects out of the orderby bag, and let's make objects from them
902
		$orderby_bag2 = array();
903
		foreach ($orderby_bag as $thing) {
904
			if (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\OrderBySQLString')) {
905
				$orderby_bag2[] = $thing;
906
			} elseif (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\OrderByColumn')) {
907
				$orderby_bag2[] = $thing;
908
			} elseif (is_string($thing)) {
909
				$orderby_bag2[] = new OrderBySQLString($thing);
910
			} elseif ($thing !== null) {
911
				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.");
912
			}
913
		}
914
		return $orderby_bag2;
915
	}
916
917
	/**
918
	 * @param string $table
919
	 * @return string[]
920
	 */
921
	public function getPrimaryKeyColumns($table) {
922
		if (!isset($this->primaryKeysColumns[$table]))
923
		{
924
			$this->primaryKeysColumns[$table] = $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKeyColumns();
925
926
			// TODO TDBM4: See if we need to improve error reporting if table name does not exist.
927
928
			/*$arr = array();
929
			foreach ($this->connection->getPrimaryKey($table) as $col) {
930
				$arr[] = $col->name;
931
			}
932
			// The primaryKeysColumns contains only the column's name, not the DB_Column object.
933
			$this->primaryKeysColumns[$table] = $arr;
934
			if (empty($this->primaryKeysColumns[$table]))
935
			{
936
				// Unable to find primary key.... this is an error
937
				// Let's try to be precise in error reporting. Let's try to find the table.
938
				$tables = $this->connection->checkTableExist($table);
939
				if ($tables === true)
940
				throw new TDBMException("Could not find table primary key for table '$table'. Please define a primary key for this table.");
941
				elseif ($tables !== null) {
942
					if (count($tables)==1)
943
					$str = "Could not find table '$table'. Maybe you meant this table: '".$tables[0]."'";
944
					else
945
					$str = "Could not find table '$table'. Maybe you meant one of those tables: '".implode("', '",$tables)."'";
946
					throw new TDBMException($str);
947
				}
948
			}*/
949
		}
950
		return $this->primaryKeysColumns[$table];
951
	}
952
953
	/**
954
	 * This is an internal function, you should not use it in your application.
955
	 * This is used internally by TDBM to add an object to the object cache.
956
	 *
957
	 * @param DbRow $dbRow
958
	 */
959
	public function _addToCache(DbRow $dbRow) {
960
		$primaryKey = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
961
		$hash = $this->getObjectHash($primaryKey);
962
		$this->objectStorage->set($dbRow->_getDbTableName(), $hash, $dbRow);
963
	}
964
965
	/**
966
	 * This is an internal function, you should not use it in your application.
967
	 * This is used internally by TDBM to remove the object from the list of objects that have been
968
	 * created/updated but not saved yet.
969
	 *
970
	 * @param DbRow $myObject
971
	 */
972
	private function removeFromToSaveObjectList(DbRow $myObject) {
973
		// TODO: replace this by a SplObjectStorage!!! Much more efficient on search!!!!
974
		foreach ($this->toSaveObjects as $id=>$object) {
975
			if ($object == $myObject)
976
			{
977
				unset($this->toSaveObjects[$id]);
978
				break;
979
			}
980
		}
981
	}
982
983
	/**
984
	 * This is an internal function, you should not use it in your application.
985
	 * This is used internally by TDBM to add an object to the list of objects that have been
986
	 * created/updated but not saved yet.
987
	 *
988
	 * @param AbstractTDBMObject $myObject
989
	 */
990
	public function _addToToSaveObjectList(DbRow $myObject) {
991
		$this->toSaveObjects[] = $myObject;
992
	}
993
994
	/**
995
	 * Generates all the daos and beans.
996
	 *
997
	 * @param string $daoFactoryClassName The classe name of the DAO factory
998
	 * @param string $daonamespace The namespace for the DAOs, without trailing \
999
	 * @param string $beannamespace The Namespace for the beans, without trailing \
1000
	 * @param bool $storeInUtc If the generated daos should store the date in UTC timezone instead of user's timezone.
1001
	 * @return \string[] the list of tables
1002
	 */
1003
	public function generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc) {
1004
		$tdbmDaoGenerator = new TDBMDaoGenerator($this->schemaAnalyzer, $this->tdbmSchemaAnalyzer->getSchema(), $this->tdbmSchemaAnalyzer);
1005
		return $tdbmDaoGenerator->generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc);
1006
	}
1007
1008
	/**
1009
 	* @param array<string, string> $tableToBeanMap
1010
 	*/
1011
	public function setTableToBeanMap(array $tableToBeanMap) {
1012
		$this->tableToBeanMap = $tableToBeanMap;
1013
	}
1014
1015
	/**
1016
	 * Saves $object by INSERTing or UPDAT(E)ing it in the database.
1017
	 *
1018
	 * @param AbstractTDBMObject $object
1019
	 * @throws TDBMException
1020
	 * @throws \Exception
1021
	 */
1022
	public function save(AbstractTDBMObject $object) {
1023
		$status = $object->_getStatus();
1024
1025
		// Let's attach this object if it is in detached state.
1026
		if ($status === TDBMObjectStateEnum::STATE_DETACHED) {
1027
			$object->_attach($this);
1028
			$status = $object->_getStatus();
1029
		}
1030
1031
		if ($status === TDBMObjectStateEnum::STATE_NEW) {
1032
			$dbRows = $object->_getDbRows();
1033
1034
			$unindexedPrimaryKeys = array();
1035
1036
			foreach ($dbRows as $dbRow) {
1037
1038
				$tableName = $dbRow->_getDbTableName();
1039
1040
				$schema = $this->tdbmSchemaAnalyzer->getSchema();
1041
				$tableDescriptor = $schema->getTable($tableName);
1042
1043
				$primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
1044
1045
				if (empty($unindexedPrimaryKeys)) {
1046
					$primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
1047
				} else {
1048
					// First insert, the children must have the same primary key as the parent.
1049
					$primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
1050
					$dbRow->_setPrimaryKeys($primaryKeys);
1051
				}
1052
1053
				$references = $dbRow->_getReferences();
1054
1055
				// Let's save all references in NEW or DETACHED state (we need their primary key)
1056
				foreach ($references as $fkName => $reference) {
1057
                    $refStatus = $reference->_getStatus();
1058
					if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
1059
						$this->save($reference);
1060
					}
1061
				}
1062
1063
				$dbRowData = $dbRow->_getDbRow();
1064
1065
				// Let's see if the columns for primary key have been set before inserting.
1066
				// We assume that if one of the value of the PK has been set, the PK is set.
1067
				$isPkSet = !empty($primaryKeys);
1068
1069
1070
				/*if (!$isPkSet) {
1071
                    // if there is no autoincrement and no pk set, let's go in error.
1072
                    $isAutoIncrement = true;
1073
1074
                    foreach ($primaryKeyColumns as $pkColumnName) {
1075
                        $pkColumn = $tableDescriptor->getColumn($pkColumnName);
1076
                        if (!$pkColumn->getAutoincrement()) {
1077
                            $isAutoIncrement = false;
1078
                        }
1079
                    }
1080
1081
                    if (!$isAutoIncrement) {
1082
                        $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.";
1083
                        throw new TDBMException($msg);
1084
                    }
1085
1086
                }*/
1087
1088
				$types = [];
1089
1090
				foreach ($dbRowData as $columnName => $value) {
1091
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
1092
					$types[] = $columnDescriptor->getType();
1093
				}
1094
1095
				$this->connection->insert($tableName, $dbRowData, $types);
1096
1097
				if (!$isPkSet && count($primaryKeyColumns) == 1) {
1098
					$id = $this->connection->lastInsertId();
1099
					$primaryKeys[$primaryKeyColumns[0]] = $id;
1100
				}
1101
1102
				// TODO: change this to some private magic accessor in future
1103
				$dbRow->_setPrimaryKeys($primaryKeys);
1104
				$unindexedPrimaryKeys = array_values($primaryKeys);
1105
1106
1107
1108
1109
				/*
1110
                 * When attached, on "save", we check if the column updated is part of a primary key
1111
                 * If this is part of a primary key, we call the _update_id method that updates the id in the list of known objects.
1112
                 * This method should first verify that the id is not already used (and is not auto-incremented)
1113
                 *
1114
                 * In the object, the key is stored in an array of  (column => value), that can be directly used to update the record.
1115
                 *
1116
                 *
1117
                 */
1118
1119
1120
				/*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...
1121
                    $this->db_connection->exec($sql);
1122
                } catch (TDBMException $e) {
1123
                    $this->db_onerror = true;
1124
1125
                    // Strange..... if we do not have the line below, bad inserts are not catched.
1126
                    // It seems that destructors are called before the registered shutdown function (PHP >=5.0.5)
1127
                    //if ($this->tdbmService->isProgramExiting())
1128
                    //	trigger_error("program exiting");
1129
                    trigger_error($e->getMessage(), E_USER_ERROR);
1130
1131
                    if (!$this->tdbmService->isProgramExiting())
1132
                        throw $e;
1133
                    else
1134
                    {
1135
                        trigger_error($e->getMessage(), E_USER_ERROR);
1136
                    }
1137
                }*/
1138
1139
				// Let's remove this object from the $new_objects static table.
1140
				$this->removeFromToSaveObjectList($dbRow);
1141
1142
				// TODO: change this behaviour to something more sensible performance-wise
1143
				// Maybe a setting to trigger this globally?
1144
				//$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...
1145
				//$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...
1146
				//$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...
1147
1148
				// Let's add this object to the list of objects in cache.
1149
				$this->_addToCache($dbRow);
1150
			}
1151
1152
1153
1154
1155
			$object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
1156
		} elseif ($status === TDBMObjectStateEnum::STATE_DIRTY) {
1157
			$dbRows = $object->_getDbRows();
1158
1159
			foreach ($dbRows as $dbRow) {
1160
				$references = $dbRow->_getReferences();
1161
1162
				// Let's save all references in NEW state (we need their primary key)
1163
				foreach ($references as $fkName => $reference) {
1164
					if ($reference->_getStatus() === TDBMObjectStateEnum::STATE_NEW) {
1165
						$this->save($reference);
1166
					}
1167
				}
1168
1169
				// Let's first get the primary keys
1170
				$tableName = $dbRow->_getDbTableName();
1171
				$dbRowData = $dbRow->_getDbRow();
1172
1173
				$schema = $this->tdbmSchemaAnalyzer->getSchema();
1174
				$tableDescriptor = $schema->getTable($tableName);
1175
1176
				$primaryKeys = $dbRow->_getPrimaryKeys();
1177
1178
				$types = [];
1179
1180
				foreach ($dbRowData as $columnName => $value) {
1181
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
1182
					$types[] = $columnDescriptor->getType();
1183
				}
1184
				foreach ($primaryKeys as $columnName => $value) {
1185
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
1186
					$types[] = $columnDescriptor->getType();
1187
				}
1188
1189
				$this->connection->update($tableName, $dbRowData, $primaryKeys, $types);
1190
1191
				// Let's check if the primary key has been updated...
1192
				$needsUpdatePk = false;
1193
				foreach ($primaryKeys as $column => $value) {
1194
					if (!isset($dbRowData[$column]) || $dbRowData[$column] != $value) {
1195
						$needsUpdatePk = true;
1196
						break;
1197
					}
1198
				}
1199
				if ($needsUpdatePk) {
1200
					$this->objectStorage->remove($tableName, $this->getObjectHash($primaryKeys));
1201
					$newPrimaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
1202
					$dbRow->_setPrimaryKeys($newPrimaryKeys);
1203
					$this->objectStorage->set($tableName, $this->getObjectHash($primaryKeys), $dbRow);
1204
				}
1205
1206
				// Let's remove this object from the list of objects to save.
1207
				$this->removeFromToSaveObjectList($dbRow);
1208
			}
1209
1210
			$object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
1211
1212
		} elseif ($status === TDBMObjectStateEnum::STATE_DELETED) {
1213
			throw new TDBMInvalidOperationException("This object has been deleted. It cannot be saved.");
1214
		}
1215
1216
        // Finally, let's save all the many to many relationships to this bean.
1217
        $this->persistManyToManyRelationships($object);
1218
	}
1219
1220
    private function persistManyToManyRelationships(AbstractTDBMObject $object) {
1221
        foreach ($object->_getCachedRelationships() as $pivotTableName => $storage) {
1222
            $tableDescriptor = $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
1223
            list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $object);
1224
1225
            foreach ($storage as $remoteBean) {
1226
                /* @var $remoteBean AbstractTDBMObject */
1227
                $statusArr = $storage[$remoteBean];
1228
                $status = $statusArr['status'];
1229
                $reverse = $statusArr['reverse'];
1230
                if ($reverse) {
1231
                    continue;
1232
                }
1233
1234
                if ($status === 'new') {
1235
                    $remoteBeanStatus = $remoteBean->_getStatus();
1236
                    if ($remoteBeanStatus === TDBMObjectStateEnum::STATE_NEW || $remoteBeanStatus === TDBMObjectStateEnum::STATE_DETACHED) {
1237
                        // Let's save remote bean if needed.
1238
                        $this->save($remoteBean);
1239
                    }
1240
1241
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
1242
1243
                    $types = [];
1244
1245
                    foreach ($filters as $columnName => $value) {
1246
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
1247
                        $types[] = $columnDescriptor->getType();
1248
                    }
1249
1250
                    $this->connection->insert($pivotTableName, $filters, $types);
1251
1252
                    // Finally, let's mark relationships as saved.
1253
                    $statusArr['status'] = 'loaded';
1254
                    $storage[$remoteBean] = $statusArr;
1255
                    $remoteStorage = $remoteBean->_getCachedRelationships()[$pivotTableName];
1256
                    $remoteStatusArr = $remoteStorage[$object];
1257
                    $remoteStatusArr['status'] = 'loaded';
1258
                    $remoteStorage[$object] = $remoteStatusArr;
1259
1260
                } elseif ($status === 'delete') {
1261
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
1262
1263
                    $types = [];
1264
1265
                    foreach ($filters as $columnName => $value) {
1266
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
1267
                        $types[] = $columnDescriptor->getType();
1268
                    }
1269
1270
                    $this->connection->delete($pivotTableName, $filters, $types);
1271
1272
                    // Finally, let's remove relationships completely from bean.
1273
                    $storage->detach($remoteBean);
1274
                    $remoteBean->_getCachedRelationships()[$pivotTableName]->detach($object);
1275
                }
1276
            }
1277
        }
1278
    }
1279
1280
    private function getPivotFilters(AbstractTDBMObject $localBean, AbstractTDBMObject $remoteBean, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk) {
1281
        $localBeanPk = $this->getPrimaryKeyValues($localBean);
1282
        $remoteBeanPk = $this->getPrimaryKeyValues($remoteBean);
1283
        $localColumns = $localFk->getLocalColumns();
1284
        $remoteColumns = $remoteFk->getLocalColumns();
1285
1286
        $localFilters = array_combine($localColumns, $localBeanPk);
1287
        $remoteFilters = array_combine($remoteColumns, $remoteBeanPk);
1288
1289
        return array_merge($localFilters, $remoteFilters);
1290
    }
1291
1292
    /**
1293
     * Returns the "values" of the primary key.
1294
     * This returns the primary key from the $primaryKey attribute, not the one stored in the columns.
1295
     *
1296
     * @param AbstractTDBMObject $bean
1297
     * @return array numerically indexed array of values.
1298
     */
1299
    private function getPrimaryKeyValues(AbstractTDBMObject $bean) {
1300
        $dbRows = $bean->_getDbRows();
1301
        $dbRow = reset($dbRows);
1302
        return array_values($dbRow->_getPrimaryKeys());
1303
    }
1304
1305
	/**
1306
	 * Returns a unique hash used to store the object based on its primary key.
1307
	 * If the array contains only one value, then the value is returned.
1308
	 * Otherwise, a hash representing the array is returned.
1309
	 *
1310
	 * @param array $primaryKeys An array of columns => values forming the primary key
1311
	 * @return string
1312
	 */
1313
	public function getObjectHash(array $primaryKeys) {
1314
		if (count($primaryKeys) === 1) {
1315
			return reset($primaryKeys);
1316
		} else {
1317
			ksort($primaryKeys);
1318
			return md5(json_encode($primaryKeys));
1319
		}
1320
	}
1321
1322
	/**
1323
	 * Returns an array of primary keys from the object.
1324
	 * The primary keys are extracted from the object columns and not from the primary keys stored in the
1325
	 * $primaryKeys variable of the object.
1326
	 *
1327
	 * @param DbRow $dbRow
1328
	 * @return array Returns an array of column => value
1329
	 */
1330
	public function getPrimaryKeysForObjectFromDbRow(DbRow $dbRow) {
1331
		$table = $dbRow->_getDbTableName();
1332
		$dbRowData = $dbRow->_getDbRow();
1333
		return $this->_getPrimaryKeysFromObjectData($table, $dbRowData);
1334
	}
1335
1336
	/**
1337
	 * Returns an array of primary keys for the given row.
1338
	 * The primary keys are extracted from the object columns.
1339
	 *
1340
	 * @param $table
1341
	 * @param array $columns
1342
	 * @return array
1343
	 */
1344
	public function _getPrimaryKeysFromObjectData($table, array $columns) {
1345
		$primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1346
		$values = array();
1347
		foreach ($primaryKeyColumns as $column) {
1348
			if (isset($columns[$column])) {
1349
				$values[$column] = $columns[$column];
1350
			}
1351
		}
1352
		return $values;
1353
	}
1354
1355
	/**
1356
	 * Attaches $object to this TDBMService.
1357
	 * The $object must be in DETACHED state and will pass in NEW state.
1358
	 *
1359
	 * @param AbstractTDBMObject $object
1360
	 * @throws TDBMInvalidOperationException
1361
	 */
1362
	public function attach(AbstractTDBMObject $object) {
1363
		$object->_attach($this);
1364
	}
1365
1366
	/**
1367
	 * Returns an associative array (column => value) for the primary keys from the table name and an
1368
	 * indexed array of primary key values.
1369
	 *
1370
	 * @param string $tableName
1371
	 * @param array $indexedPrimaryKeys
1372
	 */
1373
	public function _getPrimaryKeysFromIndexedPrimaryKeys($tableName, array $indexedPrimaryKeys) {
1374
		$primaryKeyColumns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getPrimaryKeyColumns();
1375
1376
		if (count($primaryKeyColumns) !== count($indexedPrimaryKeys)) {
1377
			throw new TDBMException(sprintf('Wrong number of columns passed for primary key. Expected %s columns for table "%s",
1378
			got %s instead.', count($primaryKeyColumns), $tableName, count($indexedPrimaryKeys)));
1379
		}
1380
1381
		return array_combine($primaryKeyColumns, $indexedPrimaryKeys);
1382
	}
1383
1384
	/**
1385
	 * Return the list of tables (from child to parent) joining the tables passed in parameter.
1386
	 * Tables must be in a single line of inheritance. The method will find missing tables.
1387
	 *
1388
	 * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1389
	 * we must be able to find all other tables.
1390
	 *
1391
	 * @param string[] $tables
1392
	 * @return string[]
1393
	 */
1394
	public function _getLinkBetweenInheritedTables(array $tables)
1395
	{
1396
		sort($tables);
1397
		return $this->fromCache($this->cachePrefix.'_linkbetweeninheritedtables_'.implode('__split__', $tables),
1398
			function() use ($tables) {
1399
				return $this->_getLinkBetweenInheritedTablesWithoutCache($tables);
1400
			});
1401
	}
1402
1403
	/**
1404
	 * Return the list of tables (from child to parent) joining the tables passed in parameter.
1405
	 * Tables must be in a single line of inheritance. The method will find missing tables.
1406
	 *
1407
	 * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1408
	 * we must be able to find all other tables.
1409
	 *
1410
	 * @param string[] $tables
1411
	 * @return string[]
1412
	 */
1413
	private function _getLinkBetweenInheritedTablesWithoutCache(array $tables) {
1414
		$schemaAnalyzer = $this->schemaAnalyzer;
1415
1416
		foreach ($tables as $currentTable) {
1417
			$allParents = [ $currentTable ];
1418
			$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...
1419
			while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1420
				$currentTable = $currentFk->getForeignTableName();
1421
				$allParents[] = $currentTable;
1422
			};
1423
1424
			// Now, does the $allParents contain all the tables we want?
1425
			$notFoundTables = array_diff($tables, $allParents);
1426
			if (empty($notFoundTables)) {
1427
				// We have a winner!
1428
				return $allParents;
1429
			}
1430
		}
1431
1432
		throw new TDBMException(sprintf("The tables (%s) cannot be linked by an inheritance relationship.", implode(', ', $tables)));
1433
	}
1434
1435
	/**
1436
	 * Returns the list of tables related to this table (via a parent or child inheritance relationship)
1437
	 * @param string $table
1438
	 * @return string[]
1439
	 */
1440
	public function _getRelatedTablesByInheritance($table)
1441
	{
1442
		return $this->fromCache($this->cachePrefix."_relatedtables_".$table, function() use ($table) {
1443
			return $this->_getRelatedTablesByInheritanceWithoutCache($table);
1444
		});
1445
	}
1446
1447
	/**
1448
	 * Returns the list of tables related to this table (via a parent or child inheritance relationship)
1449
	 * @param string $table
1450
	 * @return string[]
1451
	 */
1452
	private function _getRelatedTablesByInheritanceWithoutCache($table) {
1453
		$schemaAnalyzer = $this->schemaAnalyzer;
1454
1455
1456
		// Let's scan the parent tables
1457
		$currentTable = $table;
1458
1459
		$parentTables = [ ];
1460
1461
		// Get parent relationship
1462
		while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1463
			$currentTable = $currentFk->getForeignTableName();
1464
			$parentTables[] = $currentTable;
1465
		};
1466
1467
		// Let's recurse in children
1468
		$childrenTables = $this->exploreChildrenTablesRelationships($schemaAnalyzer, $table);
1469
1470
		return array_merge($parentTables, $childrenTables);
1471
	}
1472
1473
	/**
1474
	 * Explore all the children and descendant of $table and returns ForeignKeyConstraints on those.
1475
	 *
1476
	 * @param string $table
1477
	 * @return string[]
1478
	 */
1479
	private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyzer, $table) {
1480
		$tables = [$table];
1481
		$keys = $schemaAnalyzer->getChildrenRelationships($table);
1482
1483
		foreach ($keys as $key) {
1484
			$tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, $key->getLocalTableName()));
1485
		}
1486
1487
		return $tables;
1488
	}
1489
1490
	/**
1491
	 * Casts a foreign key into SQL, assuming table name is used with no alias.
1492
	 * The returned value does contain only one table. For instance:
1493
	 *
1494
	 * " LEFT JOIN table2 ON table1.id = table2.table1_id"
1495
	 *
1496
	 * @param ForeignKeyConstraint $fk
1497
	 * @param bool $leftTableIsLocal
1498
	 * @return string
1499
	 */
1500
	/*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...
1501
		$onClauses = [];
1502
		$foreignTableName = $this->connection->quoteIdentifier($fk->getForeignTableName());
1503
		$foreignColumns = $fk->getForeignColumns();
1504
		$localTableName = $this->connection->quoteIdentifier($fk->getLocalTableName());
1505
		$localColumns = $fk->getLocalColumns();
1506
		$columnCount = count($localTableName);
1507
1508
		for ($i = 0; $i < $columnCount; $i++) {
1509
			$onClauses[] = sprintf("%s.%s = %s.%s",
1510
				$localTableName,
1511
				$this->connection->quoteIdentifier($localColumns[$i]),
1512
				$foreignColumns,
1513
				$this->connection->quoteIdentifier($foreignColumns[$i])
1514
				);
1515
		}
1516
1517
		$onClause = implode(' AND ', $onClauses);
1518
1519
		if ($leftTableIsLocal) {
1520
			return sprintf(" LEFT JOIN %s ON (%s)", $foreignTableName, $onClause);
1521
		} else {
1522
			return sprintf(" LEFT JOIN %s ON (%s)", $localTableName, $onClause);
1523
		}
1524
	}*/
1525
1526
	/**
1527
	 * Returns an identifier for the group of tables passed in parameter.
1528
	 *
1529
	 * @param string[] $relatedTables
1530
	 * @return string
1531
	 */
1532
	private function getTableGroupName(array $relatedTables) {
1533
		sort($relatedTables);
1534
		return implode('_``_', $relatedTables);
1535
	}
1536
1537
	/**
1538
	 *
1539
	 * @param string $mainTable The name of the table queried
1540
	 * @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)
1541
	 * @param array $parameters
1542
	 * @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)
1543
	 * @param array $additionalTablesFetch
1544
	 * @param string $mode
1545
	 * @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.
1546
	 * @return ResultIterator An object representing an array of results.
1547
	 * @throws TDBMException
1548
	 */
1549
	public function findObjects($mainTable, $filter=null, array $parameters = array(), $orderString=null, array $additionalTablesFetch = array(), $mode = null, $className=null) {
1550
		// $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1551
		if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1552
			throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1553
		}
1554
1555
		list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter);
1556
1557
		$parameters = array_merge($parameters, $additionalParameters);
1558
1559
		// From the table name and the additional tables we want to fetch, let's build a list of all tables
1560
		// that must be part of the select columns.
1561
1562
		$tableGroups = [];
1563
		$allFetchedTables = $this->_getRelatedTablesByInheritance($mainTable);
1564
		$tableGroupName = $this->getTableGroupName($allFetchedTables);
1565
		foreach ($allFetchedTables as $table) {
1566
			$tableGroups[$table] = $tableGroupName;
1567
		}
1568
1569
		foreach ($additionalTablesFetch as $additionalTable) {
1570
			$relatedTables = $this->_getRelatedTablesByInheritance($additionalTable);
1571
			$tableGroupName = $this->getTableGroupName($relatedTables);
1572
			foreach ($relatedTables as $table) {
1573
				$tableGroups[$table] = $tableGroupName;
1574
			}
1575
			$allFetchedTables = array_merge($allFetchedTables, $relatedTables);
1576
		}
1577
1578
		// Let's remove any duplicate
1579
		$allFetchedTables = array_flip(array_flip($allFetchedTables));
1580
1581
		$columnsList = [];
1582
		$columnDescList = [];
1583
		$schema = $this->tdbmSchemaAnalyzer->getSchema();
1584
1585
		// Now, let's build the column list
1586
		foreach ($allFetchedTables as $table) {
1587
			foreach ($schema->getTable($table)->getColumns() as $column) {
1588
				$columnName = $column->getName();
1589
				$columnDescList[] = [
1590
					'as' => $table.'____'.$columnName,
1591
					'table' => $table,
1592
					'column' => $columnName,
1593
					'type' => $column->getType(),
1594
					'tableGroup' => $tableGroups[$table]
1595
				];
1596
				$columnsList[] = $this->connection->quoteIdentifier($table).'.'.$this->connection->quoteIdentifier($columnName).' as '.
1597
					$this->connection->quoteIdentifier($table.'____'.$columnName);
1598
			}
1599
		}
1600
1601
		$sql = "SELECT DISTINCT ".implode(', ', $columnsList)." FROM MAGICJOIN(".$mainTable.")";
1602
		$countSql = "SELECT COUNT(1) FROM MAGICJOIN(".$mainTable.")";
1603
1604
		if (!empty($filterString)) {
1605
			$sql .= " WHERE ".$filterString;
1606
			$countSql .= " WHERE ".$filterString;
1607
		}
1608
1609
		if (!empty($orderString)) {
1610
			$sql .= " ORDER BY ".$orderString;
1611
			$countSql .= " ORDER BY ".$orderString;
1612
		}
1613
1614 View Code Duplication
		if ($mode !== null && $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...
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...
1615
			throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
1616
		}
1617
1618
		$mode = $mode?:$this->mode;
1619
1620
		return new ResultIterator($sql, $countSql, $parameters, $columnDescList, $this->objectStorage, $className, $this, $this->magicQuery, $mode);
1621
	}
1622
1623
	/**
1624
	 * @param $table
1625
	 * @param array $primaryKeys
1626
	 * @param array $additionalTablesFetch
1627
	 * @param bool $lazy Whether to perform lazy loading on this object or not.
1628
	 * @param string $className
1629
	 * @return AbstractTDBMObject
1630
	 * @throws TDBMException
1631
	 */
1632
	public function findObjectByPk($table, array $primaryKeys, array $additionalTablesFetch = array(), $lazy = false, $className=null) {
1633
		$primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys);
1634
		$hash = $this->getObjectHash($primaryKeys);
1635
1636
		if ($this->objectStorage->has($table, $hash)) {
1637
			$dbRow = $this->objectStorage->get($table, $hash);
1638
			$bean = $dbRow->getTDBMObject();
1639
			if ($className !== null && !is_a($bean, $className)) {
1640
				throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'");
1641
			}
1642
			return $bean;
1643
		}
1644
1645
		// Are we performing lazy fetching?
1646
		if ($lazy === true) {
1647
			// Can we perform lazy fetching?
1648
			$tables = $this->_getRelatedTablesByInheritance($table);
1649
			// Only allowed if no inheritance.
1650
			if (count($tables) === 1) {
1651
				if ($className === null) {
1652
					$className = isset($this->tableToBeanMap[$table])?$this->tableToBeanMap[$table]:"Mouf\\Database\\TDBM\\TDBMObject";
1653
				}
1654
1655
				// Let's construct the bean
1656
				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...
1657
					$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...
1658
				}
1659
				// Let's bypass the constructor when creating the bean!
1660
				$bean = $reflectionClassCache[$className]->newInstanceWithoutConstructor();
1661
				/* @var $bean AbstractTDBMObject */
1662
				$bean->_constructLazy($table, $primaryKeys, $this);
1663
			}
1664
		}
1665
1666
		// Did not find the object in cache? Let's query it!
1667
		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...
1668
	}
1669
1670
	/**
1671
	 * Returns a unique bean (or null) according to the filters passed in parameter.
1672
	 *
1673
	 * @param string $mainTable The name of the table queried
1674
	 * @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)
1675
	 * @param array $parameters
1676
	 * @param array $additionalTablesFetch
1677
	 * @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.
1678
	 * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters.
1679
	 * @throws TDBMException
1680
	 */
1681
	public function findObject($mainTable, $filterString=null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null) {
1682
		$objects = $this->findObjects($mainTable, $filterString, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className);
1683
		$page = $objects->take(0, 2);
1684
		$count = $page->count();
1685
		if ($count > 1) {
1686
			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.");
1687
		} elseif ($count === 0) {
1688
			return null;
1689
		}
1690
		return $objects[0];
1691
	}
1692
1693
	/**
1694
	 * Returns a unique bean according to the filters passed in parameter.
1695
	 * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1696
	 *
1697
	 * @param string $mainTable The name of the table queried
1698
	 * @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)
1699
	 * @param array $parameters
1700
	 * @param array $additionalTablesFetch
1701
	 * @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.
1702
	 * @return AbstractTDBMObject The object we want
1703
	 * @throws TDBMException
1704
	 */
1705
	public function findObjectOrFail($mainTable, $filterString=null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null) {
1706
		$bean = $this->findObject($mainTable, $filterString, $parameters, $additionalTablesFetch, $className);
1707
		if ($bean === null) {
1708
			throw new NoBeanFoundException("No result found for query on table '".$mainTable."'");
1709
		}
1710
		return $bean;
1711
	}
1712
1713
	/**
1714
	 * @param array $beanData An array of data: array<table, array<column, value>>
1715
	 * @return array an array with first item = class name and second item = table name
1716
	 */
1717
	public function _getClassNameFromBeanData(array $beanData) {
1718
		if (count($beanData) === 1) {
1719
			$tableName = array_keys($beanData)[0];
1720
		} else {
1721
			foreach ($beanData as $table => $row) {
1722
				$tables = [];
1723
				$primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1724
				$pkSet = false;
1725
				foreach ($primaryKeyColumns as $columnName) {
1726
					if ($row[$columnName] !== null) {
1727
						$pkSet = true;
1728
						break;
1729
					}
1730
				}
1731
				if ($pkSet) {
1732
					$tables[] = $table;
1733
				}
1734
			}
1735
1736
			// $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1737
			$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...
1738
			$tableName = $allTables[0];
1739
		}
1740
1741
		// Only one table in this bean. Life is sweat, let's look at its type:
1742
		if (isset($this->tableToBeanMap[$tableName])) {
1743
			return [$this->tableToBeanMap[$tableName], $tableName];
1744
		} else {
1745
			return ["Mouf\\Database\\TDBM\\TDBMObject", $tableName];
1746
		}
1747
	}
1748
1749
	/**
1750
	 * Returns an item from cache or computes it using $closure and puts it in cache.
1751
	 *
1752
	 * @param string   $key
1753
	 * @param callable $closure
1754
	 *
1755
	 * @return mixed
1756
	 */
1757
	private function fromCache($key, callable $closure)
1758
	{
1759
		$item = $this->cache->fetch($key);
1760
		if ($item === false) {
1761
			$item = $closure();
1762
			$this->cache->save($key, $item);
1763
		}
1764
1765
		return $item;
1766
	}
1767
1768
	/**
1769
	 * Returns the foreign key object.
1770
	 * @param string $table
1771
	 * @param string $fkName
1772
	 * @return ForeignKeyConstraint
1773
	 */
1774
	public function _getForeignKeyByName($table, $fkName) {
1775
		return $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getForeignKey($fkName);
1776
	}
1777
1778
	/**
1779
	 * @param $pivotTableName
1780
	 * @param AbstractTDBMObject $bean
1781
	 * @return AbstractTDBMObject[]
1782
	 */
1783
	public function _getRelatedBeans($pivotTableName, AbstractTDBMObject $bean) {
1784
1785
        list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $bean);
1786
        /* @var $localFk ForeignKeyConstraint */
1787
        /* @var $remoteFk ForeignKeyConstraint */
1788
        $remoteTable = $remoteFk->getForeignTableName();
1789
1790
1791
        $primaryKeys = $this->getPrimaryKeyValues($bean);
1792
        $columnNames = array_map(function($name) use ($pivotTableName) { return $pivotTableName.'.'.$name; }, $localFk->getLocalColumns());
1793
1794
        $filter = array_combine($columnNames, $primaryKeys);
1795
1796
        return $this->findObjects($remoteTable, $filter);
1797
	}
1798
1799
    /**
1800
     * @param $pivotTableName
1801
     * @param AbstractTDBMObject $bean The LOCAL bean
1802
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean.
1803
     * @throws TDBMException
1804
     */
1805
    private function getPivotTableForeignKeys($pivotTableName, AbstractTDBMObject $bean) {
1806
        $fks = array_values($this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
1807
        $table1 = $fks[0]->getForeignTableName();
1808
        $table2 = $fks[1]->getForeignTableName();
1809
1810
        $beanTables = array_map(function(DbRow $dbRow) { return $dbRow->_getDbTableName(); }, $bean->_getDbRows());
1811
1812
        if (in_array($table1, $beanTables)) {
1813
            return [$fks[0], $fks[1]];
1814
        } elseif (in_array($table2, $beanTables)) {
1815
            return [$fks[1], $fks[0]];
1816
        } else {
1817
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2}");
1818
        }
1819
    }
1820
}
1821