Completed
Pull Request — 3.4 (#46)
by David
23:45 queued 27s
created

TDBMService::save()   D

Complexity

Conditions 22
Paths 8

Size

Total Lines 197
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
c 7
b 0
f 0
dl 0
loc 197
rs 4.6625
cc 22
eloc 73
nc 8
nop 1

How to fix   Long Method    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 Doctrine\DBAL\Schema\Table;
30
use Mouf\Database\MagicQuery;
31
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
32
use Mouf\Database\TDBM\Filters\OrderBySQLString;
33
use Mouf\Database\TDBM\Utils\TDBMDaoGenerator;
34
use Mouf\Utils\Cache\CacheInterface;
35
use SQLParser\Node\ColRef;
36
37
/**
38
 * The TDBMService class is the main TDBM class. It provides methods to retrieve TDBMObject instances
39
 * from the database.
40
 *
41
 * @author David Negrier
42
 * @ExtendedAction {"name":"Generate DAOs", "url":"tdbmadmin/", "default":false}
43
 */
44
class TDBMService {
45
	
46
	const MODE_CURSOR = 1;
47
	const MODE_ARRAY = 2;
48
	
49
	/**
50
	 * The database connection.
51
	 *
52
	 * @var Connection
53
	 */
54
	private $connection;
55
56
	/**
57
	 * @var SchemaAnalyzer
58
	 */
59
	private $schemaAnalyzer;
60
61
	/**
62
	 * @var MagicQuery
63
	 */
64
	private $magicQuery;
65
66
	/**
67
	 * @var TDBMSchemaAnalyzer
68
	 */
69
	private $tdbmSchemaAnalyzer;
70
71
	/**
72
	 * @var string
73
	 */
74
	private $cachePrefix;
75
76
	/**
77
	 * The default autosave mode for the objects
78
	 * True to automatically save the object.
79
	 * If false, the user must explicitly call the save() method to save the object.
80
	 *
81
	 * @var boolean
82
	 */
83
	//private $autosave_default = false;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
158
		}
159
		$this->connection = $connection;
160
		if ($cache !== null) {
161
			$this->cache = $cache;
162
		} else {
163
			$this->cache = new VoidCache();
164
		}
165
		if ($schemaAnalyzer) {
166
			$this->schemaAnalyzer = $schemaAnalyzer;
167
		} else {
168
			$this->schemaAnalyzer = new SchemaAnalyzer($this->connection->getSchemaManager(), $this->cache, $this->getConnectionUniqueId());
169
		}
170
171
		$this->magicQuery = new MagicQuery($this->connection, $this->cache, $this->schemaAnalyzer);
172
173
		$this->tdbmSchemaAnalyzer = new TDBMSchemaAnalyzer($connection, $this->cache, $this->schemaAnalyzer);
174
		$this->cachePrefix = $this->tdbmSchemaAnalyzer->getCachePrefix();
175
176
		if (self::$script_start_up_time === null) {
177
			self::$script_start_up_time = microtime(true);
178
		}
179
		$this->toSaveObjects = new \SplObjectStorage();
180
	}
181
182
183
	/**
184
	 * Returns the object used to connect to the database.
185
	 *
186
	 * @return Connection
187
	 */
188
	public function getConnection() {
189
		return $this->connection;
190
	}
191
192
	/**
193
	 * Creates a unique cache key for the current connection.
194
	 * @return string
195
	 */
196
	private function getConnectionUniqueId() {
197
		return hash('md4', $this->connection->getHost()."-".$this->connection->getPort()."-".$this->connection->getDatabase()."-".$this->connection->getDriver()->getName());
198
	}
199
200
	/**
201
	 * Returns true if the objects will save automatically by default,
202
	 * false if an explicit call to save() is required.
203
	 *
204
	 * The behaviour can be overloaded by setAutoSaveMode on each object.
205
	 *
206
	 * @return boolean
207
	 */
208
	/*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...
209
		return $this->autosave_default;
210
	}*/
211
212
	/**
213
	 * Sets the autosave mode:
214
	 * true if the object will save automatically,
215
	 * false if an explicit call to save() is required.
216
	 *
217
	 * @Compulsory
218
	 * @param boolean $autoSave
219
	 */
220
	/*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...
221
		$this->autosave_default = $autoSave;
222
	}*/
223
224
	/**
225
	 * Sets the fetch mode of the result sets returned by `getObjects`.
226
	 * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY
227
	 *
228
	 * 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).
229
	 * 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
230
	 * several times. In cursor mode, you cannot access the result set by key. Use this mode for large datasets processed by batch.
231
	 *
232
	 * @param int $mode
233
	 */
234
	public function setFetchMode($mode) {
235 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...
236
			throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
237
		}
238
		$this->mode = $mode;
239
		return $this;
240
	}
241
242
	/**
243
	 * Returns a TDBMObject associated from table "$table_name".
244
	 * If the $filters parameter is an int/string, the object returned will be the object whose primary key = $filters.
245
	 * $filters can also be a set of TDBM_Filters (see the getObjects method for more details).
246
	 *
247
	 * For instance, if there is a table 'users', with a primary key on column 'user_id' and a column 'user_name', then
248
	 * 			$user = $tdbmService->getObject('users',1);
249
	 * 			echo $user->name;
250
	 * will return the name of the user whose user_id is one.
251
	 *
252
	 * 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.
253
	 * For instance:
254
	 * 			$group = $tdbmService->getObject('groups',array(1,2));
255
	 *
256
	 * Note that TDBMObject performs caching for you. If you get twice the same object, the reference of the object you will get
257
	 * will be the same.
258
	 *
259
	 * For instance:
260
	 * 			$user1 = $tdbmService->getObject('users',1);
261
	 * 			$user2 = $tdbmService->getObject('users',1);
262
	 * 			$user1->name = 'John Doe';
263
	 * 			echo $user2->name;
264
	 * will return 'John Doe'.
265
	 *
266
	 * You can use filters instead of passing the primary key. For instance:
267
	 * 			$user = $tdbmService->getObject('users',new EqualFilter('users', 'login', 'jdoe'));
268
	 * This will return the user whose login is 'jdoe'.
269
	 * Please note that if 2 users have the jdoe login in database, the method will throw a TDBM_DuplicateRowException.
270
	 *
271
	 * Also, you can specify the return class for the object (provided the return class extends TDBMObject).
272
	 * For instance:
273
	 *  	$user = $tdbmService->getObject('users',1,'User');
274
	 * will return an object from the "User" class. The "User" class must extend the "TDBMObject" class.
275
	 * Please be sure not to override any method or any property unless you perfectly know what you are doing!
276
	 *
277
	 * @param string $table_name The name of the table we retrieve an object from.
278
	 * @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)
279
	 * @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.
280
	 * @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.
281
	 * @return TDBMObject
282
	 */
283
/*	public function getObject($table_name, $filters, $className = null, $lazy_loading = false) {
284
285
		if (is_array($filters) || $filters instanceof FilterInterface) {
286
			$isFilterBag = false;
287
			if (is_array($filters)) {
288
				// Is this a multiple primary key or a filter bag?
289
				// Let's have a look at the first item of the array to decide.
290
				foreach ($filters as $filter) {
291
					if (is_array($filter) || $filter instanceof FilterInterface) {
292
						$isFilterBag = true;
293
					}
294
					break;
295
				}
296
			} else {
297
				$isFilterBag = true;
298
			}
299
				
300
			if ($isFilterBag == true) {
301
				// If a filter bag was passer in parameter, let's perform a getObjects.
302
				$objects = $this->getObjects($table_name, $filters, null, null, null, $className);
303
				if (count($objects) == 0) {
304
					return null;
305
				} elseif (count($objects) > 1) {
306
					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.");
307
				}
308
				// Return the first and only object.
309
				if ($objects instanceof \Generator) {
310
					return $objects->current();
311
				} else {
312
					return $objects[0];
313
				}
314
			}
315
		}
316
		$id = $filters;
317
		if ($this->connection == null) {
318
			throw new TDBMException("Error while calling TdbmService->getObject(): No connection has been established on the database!");
319
		}
320
		$table_name = $this->connection->toStandardcase($table_name);
321
322
		// If the ID is null, let's throw an exception
323
		if ($id === null) {
324
			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.");
325
		}
326
327
		// If the primary key is split over many columns, the IDs are passed in an array. Let's serialize this array to store it.
328
		if (is_array($id)) {
329
			$id = serialize($id);
330
		}
331
332
		if ($className === null) {
333
			if (isset($this->tableToBeanMap[$table_name])) {
334
				$className = $this->tableToBeanMap[$table_name];
335
			} else {
336
				$className = "Mouf\\Database\\TDBM\\TDBMObject";
337
			}
338
		}
339
340
		if ($this->objectStorage->has($table_name, $id)) {
341
			$obj = $this->objectStorage->get($table_name, $id);
342
			if (is_a($obj, $className)) {
343
				return $obj;
344
			} else {
345
				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'");
346
			}
347
		}
348
349
		if ($className != "Mouf\\Database\\TDBM\\TDBMObject" && !is_subclass_of($className, "Mouf\\Database\\TDBM\\TDBMObject")) {
350
			if (!class_exists($className)) {
351
				throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." does not exist.");
352
			} else {
353
				throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." should extend TDBMObject.");
354
			}
355
		}
356
		$obj = new $className($this, $table_name, $id);
357
358
		if ($lazy_loading == false) {
359
			// If we are not doing lazy loading, let's load the object:
360
			$obj->_dbLoadIfNotLoaded();
361
		}
362
363
		$this->objectStorage->set($table_name, $id, $obj);
364
365
		return $obj;
366
	}*/
367
368
	/**
369
	 * Removes the given object from database.
370
	 * This cannot be called on an object that is not attached to this TDBMService
371
	 * (will throw a TDBMInvalidOperationException)
372
	 *
373
	 * @param AbstractTDBMObject $object the object to delete.
374
	 * @throws TDBMException
375
	 * @throws TDBMInvalidOperationException
376
	 */
377
	public function delete(AbstractTDBMObject $object) {
378
		switch ($object->_getStatus()) {
379
			case TDBMObjectStateEnum::STATE_DELETED:
380
				// Nothing to do, object already deleted.
381
				return;
382
			case TDBMObjectStateEnum::STATE_DETACHED:
383
				throw new TDBMInvalidOperationException('Cannot delete a detached object');
384
			case TDBMObjectStateEnum::STATE_NEW:
385
                $this->deleteManyToManyRelationships($object);
386
				foreach ($object->_getDbRows() as $dbRow) {
387
					$this->removeFromToSaveObjectList($dbRow);
388
				}
389
				break;
390
			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...
391
				foreach ($object->_getDbRows() as $dbRow) {
392
					$this->removeFromToSaveObjectList($dbRow);
393
				}
394
			case TDBMObjectStateEnum::STATE_NOT_LOADED:
395
			case TDBMObjectStateEnum::STATE_LOADED:
396
                $this->deleteManyToManyRelationships($object);
397
				// Let's delete db rows, in reverse order.
398
				foreach (array_reverse($object->_getDbRows()) as $dbRow) {
399
					$tableName = $dbRow->_getDbTableName();
400
					$primaryKeys = $dbRow->_getPrimaryKeys();
401
					$this->connection->delete($tableName, $primaryKeys);
402
					$this->objectStorage->remove($dbRow->_getDbTableName(), $this->getObjectHash($primaryKeys));
403
				}
404
				break;
405
			// @codeCoverageIgnoreStart
406
			default:
407
				throw new TDBMInvalidOperationException('Unexpected status for bean');
408
			// @codeCoverageIgnoreEnd
409
		}
410
411
		$object->_setStatus(TDBMObjectStateEnum::STATE_DELETED);
412
	}
413
414
    /**
415
     * Removes all many to many relationships for this object.
416
     * @param AbstractTDBMObject $object
417
     */
418
    private function deleteManyToManyRelationships(AbstractTDBMObject $object) {
419
        foreach ($object->_getDbRows() as $tableName => $dbRow) {
420
            $pivotTables = $this->tdbmSchemaAnalyzer->getPivotTableLinkedToTable($tableName);
421
            foreach ($pivotTables as $pivotTable) {
422
                $remoteBeans = $object->_getRelationships($pivotTable);
423
                foreach ($remoteBeans as $remoteBean) {
424
                    $object->_removeRelationship($pivotTable, $remoteBean);
425
                }
426
            }
427
        }
428
        $this->persistManyToManyRelationships($object);
429
    }
430
431
432
    /**
433
     * This function removes the given object from the database. It will also remove all objects relied to the one given
434
     * by parameter before all.
435
     *
436
     * Notice: if the object has a multiple primary key, the function will not work.
437
     *
438
     * @param AbstractTDBMObject $objToDelete
439
     */
440
    public function deleteCascade(AbstractTDBMObject $objToDelete) {
441
        $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...
442
        $this->delete($objToDelete);
443
    }
444
445
    /**
446
     * This function is used only in TDBMService (private function)
447
     * It will call deleteCascade function foreach object relied with a foreign key to the object given by parameter
448
     *
449
     * @param TDBMObject $obj
450
     * @return TDBMObjectArray
451
     */
452
    private function deleteAllConstraintWithThisObject(TDBMObject $obj) {
453
        $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...
454
        $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...
455
        foreach ($constraints as $constraint) {
456
            $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...
457
            $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...
458
            $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...
459
            $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...
460
            $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...
461
            $sql = "SELECT DISTINCT ".$tableTo.".*"
462
                    ." FROM ".$tableFrom
463
                    ." LEFT JOIN ".$tableTo." ON ".$tableFrom.".".$colFrom." = ".$tableTo.".".$colTo
464
                    ." WHERE ".$tableFrom.".".$idVarName."=".$idValue;
465
            $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...
466
            foreach ($result as $tdbmObj) {
467
                $this->deleteCascade($tdbmObj);
468
            }
469
        }
470
    }
471
472
	/**
473
	 * This function performs a save() of all the objects that have been modified.
474
	 */
475
	public function completeSave() {
476
477
		foreach ($this->toSaveObjects as $dbRow)
478
		{
479
			$this->save($dbRow->getTDBMObject());
480
		}
481
482
	}
483
484
	/**
485
	 * Returns an array of objects of "table_name" kind filtered from the filter bag.
486
	 *
487
	 * The getObjects method should be the most used query method in TDBM if you want to query the database for objects.
488
	 * (Note: if you want to query the database for an object by its primary key, use the getObject method).
489
	 *
490
	 * The getObjects method takes in parameter:
491
	 * 	- table_name: the kinf of TDBMObject you want to retrieve. In TDBM, a TDBMObject matches a database row, so the
492
	 * 			$table_name parameter should be the name of an existing table in database.
493
	 *  - filter_bag: The filter bag is anything that you can use to filter your request. It can be a SQL Where clause,
494
	 * 			a series of TDBM_Filter objects, or even TDBMObjects or TDBMObjectArrays that you will use as filters.
495
	 *  - order_bag: The order bag is anything that will be used to order the data that is passed back to you.
496
	 * 			A SQL Order by clause can be used as an order bag for instance, or a OrderByColumn object
497
	 * 	- from (optionnal): The offset from which the query should be performed. For instance, if $from=5, the getObjects method
498
	 * 			will return objects from the 6th rows.
499
	 * 	- limit (optionnal): The maximum number of objects to return. Used together with $from, you can implement
500
	 * 			paging mechanisms.
501
	 *  - hint_path (optionnal): EXPERTS ONLY! The path the request should use if not the most obvious one. This parameter
502
	 * 			should be used only if you perfectly know what you are doing.
503
	 *
504
	 * The getObjects method will return a TDBMObjectArray. A TDBMObjectArray is an array of TDBMObjects that does behave as
505
	 * a single TDBMObject if the array has only one member. Refer to the documentation of TDBMObjectArray and TDBMObject
506
	 * to learn more.
507
	 *
508
	 * More about the filter bag:
509
	 * A filter is anything that can change the set of objects returned by getObjects.
510
	 * There are many kind of filters in TDBM:
511
	 * A filter can be:
512
	 * 	- A SQL WHERE clause:
513
	 * 		The clause is specified without the "WHERE" keyword. For instance:
514
	 * 			$filter = "users.first_name LIKE 'J%'";
515
	 *     	is a valid filter.
516
	 * 	   	The only difference with SQL is that when you specify a column name, it should always be fully qualified with
517
	 * 		the table name: "country_name='France'" is not valid, while "countries.country_name='France'" is valid (if
518
	 * 		"countries" is a table and "country_name" a column in that table, sure.
519
	 * 		For instance,
520
	 * 				$french_users = TDBMObject::getObjects("users", "countries.country_name='France'");
521
	 * 		will return all the users that are French (based on trhe assumption that TDBM can find a way to connect the users
522
	 * 		table to the country table using foreign keys, see the manual for that point).
523
	 * 	- A TDBMObject:
524
	 * 		An object can be used as a filter. For instance, we could get the France object and then find any users related to
525
	 * 		that object using:
526
	 * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
527
	 * 				$french_users = TDBMObject::getObjects("users", $france);
528
	 *  - A TDBMObjectArray can be used as a filter too.
529
	 * 		For instance:
530
	 * 				$french_groups = TDBMObject::getObjects("groups", $french_users);
531
	 * 		might return all the groups in which french users can be found.
532
	 *  - Finally, TDBM_xxxFilter instances can be used.
533
	 * 		TDBM provides the developer a set of TDBM_xxxFilters that can be used to model a SQL Where query.
534
	 * 		Using the appropriate filter object, you can model the operations =,<,<=,>,>=,IN,LIKE,AND,OR, IS NULL and NOT
535
	 * 		For instance:
536
	 * 				$french_users = TDBMObject::getObjects("users", new EqualFilter('countries','country_name','France');
537
	 * 		Refer to the documentation of the appropriate filters for more information.
538
	 *
539
	 * The nice thing about a filter bag is that it can be any filter, or any array of filters. In that case, filters are
540
	 * 'ANDed' together.
541
	 * So a request like this is valid:
542
	 * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
543
	 * 				$french_administrators = TDBMObject::getObjects("users", array($france,"role.role_name='Administrators'");
544
	 * This requests would return the users that are both French and administrators.
545
	 *
546
	 * Finally, if filter_bag is null, the whole table is returned.
547
	 *
548
	 * More about the order bag:
549
	 * The order bag contains anything that can be used to order the data that is passed back to you.
550
	 * The order bag can contain two kinds of objects:
551
	 * 	- A SQL ORDER BY clause:
552
	 * 		The clause is specified without the "ORDER BY" keyword. For instance:
553
	 * 			$orderby = "users.last_name ASC, users.first_name ASC";
554
	 *     	is a valid order bag.
555
	 * 		The only difference with SQL is that when you specify a column name, it should always be fully qualified with
556
	 * 		the table name: "country_name ASC" is not valid, while "countries.country_name ASC" is valid (if
557
	 * 		"countries" is a table and "country_name" a column in that table, sure.
558
	 * 		For instance,
559
	 * 				$french_users = TDBMObject::getObjects("users", null, "countries.country_name ASC");
560
	 * 		will return all the users sorted by country.
561
	 *  - A OrderByColumn object
562
	 * 		This object models a single column in a database.
563
	 *
564
	 * @param string $table_name The name of the table queried
565
	 * @param mixed $filter_bag The filter bag (see above for complete description)
566
	 * @param mixed $orderby_bag The order bag (see above for complete description)
567
	 * @param integer $from The offset
568
	 * @param integer $limit The maximum number of rows returned
569
	 * @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.
570
	 * @param unknown_type $hint_path Hints to get the path for the query (expert parameter, you should leave it to null).
571
	 * @return TDBMObjectArray A TDBMObjectArray containing the resulting objects of the query.
572
	 */
573
/*	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...
574
		if ($this->connection == null) {
575
			throw new TDBMException("Error while calling TDBMObject::getObject(): No connection has been established on the database!");
576
		}
577
		return $this->getObjectsByMode('getObjects', $table_name, $filter_bag, $orderby_bag, $from, $limit, $className, $hint_path);
578
	}*/
579
580
581
	/**
582
	 * Takes in input a filter_bag (which can be about anything from a string to an array of TDBMObjects... see above from documentation),
583
	 * and gives back a proper Filter object.
584
	 *
585
	 * @param mixed $filter_bag
586
	 * @return array First item: filter string, second item: parameters.
587
	 * @throws TDBMException
588
	 */
589
	public function buildFilterFromFilterBag($filter_bag) {
590
		$counter = 1;
591
		if ($filter_bag === null) {
592
			return ['', []];
593
		} elseif (is_string($filter_bag)) {
594
			return [$filter_bag, []];
595
		} elseif (is_array($filter_bag)) {
596
			$sqlParts = [];
597
			$parameters = [];
598
			foreach ($filter_bag as $column => $value) {
599
				$paramName = "tdbmparam".$counter;
600
				if (is_array($value)) {
601
					$sqlParts[] = $this->connection->quoteIdentifier($column)." IN :".$paramName;
602
				} else {
603
					$sqlParts[] = $this->connection->quoteIdentifier($column)." = :".$paramName;
604
				}
605
				$parameters[$paramName] = $value;
606
				$counter++;
607
			}
608
			return [implode(' AND ', $sqlParts), $parameters];
609
		} elseif ($filter_bag instanceof AbstractTDBMObject) {
610
			$dbRows = $filter_bag->_getDbRows();
611
			$dbRow = reset($dbRows);
612
			$primaryKeys = $dbRow->_getPrimaryKeys();
613
614
			foreach ($primaryKeys as $column => $value) {
615
				$paramName = "tdbmparam".$counter;
616
				$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...
617
				$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...
618
				$counter++;
619
			}
620
			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...
621
		} elseif ($filter_bag instanceof \Iterator) {
622
			return $this->buildFilterFromFilterBag(iterator_to_array($filter_bag));
623
		} else {
624
			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.");
625
		}
626
627
//		// 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...
628
//		if ($filter_bag === null) {
629
//			$filter_bag = array();
630
//		} elseif (!is_array($filter_bag)) {
631
//			$filter_bag = array($filter_bag);
632
//		}
633
//		elseif (is_a($filter_bag, 'Mouf\\Database\\TDBM\\TDBMObjectArray')) {
634
//			$filter_bag = array($filter_bag);
635
//		}
636
//
637
//		// Second, let's take all the objects out of the filter bag, and let's make filters from them
638
//		$filter_bag2 = array();
639
//		foreach ($filter_bag as $thing) {
640
//			if (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\FilterInterface')) {
641
//				$filter_bag2[] = $thing;
642
//			} elseif (is_string($thing)) {
643
//				$filter_bag2[] = new SqlStringFilter($thing);
644
//			} elseif (is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && count($thing)>0) {
645
//				// Get table_name and column_name
646
//				$filter_table_name = $thing[0]->_getDbTableName();
647
//				$filter_column_names = $thing[0]->getPrimaryKey();
648
//
649
//				// If there is only one primary key, we can use the InFilter
650
//				if (count($filter_column_names)==1) {
651
//					$primary_keys_array = array();
652
//					$filter_column_name = $filter_column_names[0];
653
//					foreach ($thing as $TDBMObject) {
654
//						$primary_keys_array[] = $TDBMObject->$filter_column_name;
655
//					}
656
//					$filter_bag2[] = new InFilter($filter_table_name, $filter_column_name, $primary_keys_array);
657
//				}
658
//				// else, we must use a (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx)...
659
//				else
660
//				{
661
//					$filter_bag_and = array();
662
//					foreach ($thing as $TDBMObject) {
663
//						$filter_bag_temp_and=array();
664
//						foreach ($filter_column_names as $pk) {
665
//							$filter_bag_temp_and[] = new EqualFilter($TDBMObject->_getDbTableName(), $pk, $TDBMObject->$pk);
666
//						}
667
//						$filter_bag_and[] = new AndFilter($filter_bag_temp_and);
668
//					}
669
//					$filter_bag2[] = new OrFilter($filter_bag_and);
670
//				}
671
//
672
//
673
//			} elseif (!is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && $thing!==null) {
674
//				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.");
675
//			}
676
//		}
677
//
678
//		// Third, let's take all the filters and let's apply a huge AND filter
679
//		$filter = new AndFilter($filter_bag2);
680
//
681
//		return $filter;
682
	}
683
684
	/**
685
	 * Takes in input an order_bag (which can be about anything from a string to an array of OrderByColumn objects... see above from documentation),
686
	 * and gives back an array of OrderByColumn / OrderBySQLString objects.
687
	 *
688
	 * @param unknown_type $orderby_bag
689
	 * @return array
690
	 */
691
	public function buildOrderArrayFromOrderBag($orderby_bag) {
692
		// Fourth, let's apply the same steps to the orderby_bag
693
		// 4-1 orderby_bag should be an array, if it is a singleton, let's put it in an array.
694
695
		if (!is_array($orderby_bag))
696
		$orderby_bag = array($orderby_bag);
697
698
		// 4-2, let's take all the objects out of the orderby bag, and let's make objects from them
699
		$orderby_bag2 = array();
700
		foreach ($orderby_bag as $thing) {
701
			if (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\OrderBySQLString')) {
702
				$orderby_bag2[] = $thing;
703
			} elseif (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\OrderByColumn')) {
704
				$orderby_bag2[] = $thing;
705
			} elseif (is_string($thing)) {
706
				$orderby_bag2[] = new OrderBySQLString($thing);
707
			} elseif ($thing !== null) {
708
				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.");
709
			}
710
		}
711
		return $orderby_bag2;
712
	}
713
714
	/**
715
	 * @param string $table
716
	 * @return string[]
717
	 */
718
	public function getPrimaryKeyColumns($table) {
719
		if (!isset($this->primaryKeysColumns[$table]))
720
		{
721
			$this->primaryKeysColumns[$table] = $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKeyColumns();
722
723
			// TODO TDBM4: See if we need to improve error reporting if table name does not exist.
724
725
			/*$arr = array();
726
			foreach ($this->connection->getPrimaryKey($table) as $col) {
727
				$arr[] = $col->name;
728
			}
729
			// The primaryKeysColumns contains only the column's name, not the DB_Column object.
730
			$this->primaryKeysColumns[$table] = $arr;
731
			if (empty($this->primaryKeysColumns[$table]))
732
			{
733
				// Unable to find primary key.... this is an error
734
				// Let's try to be precise in error reporting. Let's try to find the table.
735
				$tables = $this->connection->checkTableExist($table);
736
				if ($tables === true)
737
				throw new TDBMException("Could not find table primary key for table '$table'. Please define a primary key for this table.");
738
				elseif ($tables !== null) {
739
					if (count($tables)==1)
740
					$str = "Could not find table '$table'. Maybe you meant this table: '".$tables[0]."'";
741
					else
742
					$str = "Could not find table '$table'. Maybe you meant one of those tables: '".implode("', '",$tables)."'";
743
					throw new TDBMException($str);
744
				}
745
			}*/
746
		}
747
		return $this->primaryKeysColumns[$table];
748
	}
749
750
	/**
751
	 * This is an internal function, you should not use it in your application.
752
	 * This is used internally by TDBM to add an object to the object cache.
753
	 *
754
	 * @param DbRow $dbRow
755
	 */
756
	public function _addToCache(DbRow $dbRow) {
757
		$primaryKey = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
758
		$hash = $this->getObjectHash($primaryKey);
759
		$this->objectStorage->set($dbRow->_getDbTableName(), $hash, $dbRow);
760
	}
761
762
	/**
763
	 * This is an internal function, you should not use it in your application.
764
	 * This is used internally by TDBM to remove the object from the list of objects that have been
765
	 * created/updated but not saved yet.
766
	 *
767
	 * @param DbRow $myObject
768
	 */
769
	private function removeFromToSaveObjectList(DbRow $myObject) {
770
		unset($this->toSaveObjects[$myObject]);
771
	}
772
773
	/**
774
	 * This is an internal function, you should not use it in your application.
775
	 * This is used internally by TDBM to add an object to the list of objects that have been
776
	 * created/updated but not saved yet.
777
	 *
778
	 * @param AbstractTDBMObject $myObject
779
	 */
780
	public function _addToToSaveObjectList(DbRow $myObject) {
781
		$this->toSaveObjects[$myObject] = true;
782
	}
783
784
	/**
785
	 * Generates all the daos and beans.
786
	 *
787
	 * @param string $daoFactoryClassName The classe name of the DAO factory
788
	 * @param string $daonamespace The namespace for the DAOs, without trailing \
789
	 * @param string $beannamespace The Namespace for the beans, without trailing \
790
	 * @param bool $storeInUtc If the generated daos should store the date in UTC timezone instead of user's timezone.
791
	 * @return \string[] the list of tables
792
	 */
793
	public function generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc) {
794
		$tdbmDaoGenerator = new TDBMDaoGenerator($this->schemaAnalyzer, $this->tdbmSchemaAnalyzer->getSchema(), $this->tdbmSchemaAnalyzer);
795
		return $tdbmDaoGenerator->generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc);
796
	}
797
798
	/**
799
 	* @param array<string, string> $tableToBeanMap
800
 	*/
801
	public function setTableToBeanMap(array $tableToBeanMap) {
802
		$this->tableToBeanMap = $tableToBeanMap;
803
	}
804
805
	/**
806
	 * Saves $object by INSERTing or UPDAT(E)ing it in the database.
807
	 *
808
	 * @param AbstractTDBMObject $object
809
	 * @throws TDBMException
810
	 */
811
	public function save(AbstractTDBMObject $object) {
812
		$status = $object->_getStatus();
813
814
		// Let's attach this object if it is in detached state.
815
		if ($status === TDBMObjectStateEnum::STATE_DETACHED) {
816
			$object->_attach($this);
817
			$status = $object->_getStatus();
818
		}
819
820
		if ($status === TDBMObjectStateEnum::STATE_NEW) {
821
			$dbRows = $object->_getDbRows();
822
823
			$unindexedPrimaryKeys = array();
824
825
			foreach ($dbRows as $dbRow) {
826
827
				$tableName = $dbRow->_getDbTableName();
828
829
				$schema = $this->tdbmSchemaAnalyzer->getSchema();
830
				$tableDescriptor = $schema->getTable($tableName);
831
832
				$primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
833
834
				if (empty($unindexedPrimaryKeys)) {
835
					$primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
836
				} else {
837
					// First insert, the children must have the same primary key as the parent.
838
					$primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
839
					$dbRow->_setPrimaryKeys($primaryKeys);
840
				}
841
842
				$references = $dbRow->_getReferences();
843
844
				// Let's save all references in NEW or DETACHED state (we need their primary key)
845
				foreach ($references as $fkName => $reference) {
846
                    $refStatus = $reference->_getStatus();
847
					if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
848
						$this->save($reference);
849
					}
850
				}
851
852
				$dbRowData = $dbRow->_getDbRow();
853
854
				// Let's see if the columns for primary key have been set before inserting.
855
				// We assume that if one of the value of the PK has been set, the PK is set.
856
				$isPkSet = !empty($primaryKeys);
857
858
859
				/*if (!$isPkSet) {
860
                    // if there is no autoincrement and no pk set, let's go in error.
861
                    $isAutoIncrement = true;
862
863
                    foreach ($primaryKeyColumns as $pkColumnName) {
864
                        $pkColumn = $tableDescriptor->getColumn($pkColumnName);
865
                        if (!$pkColumn->getAutoincrement()) {
866
                            $isAutoIncrement = false;
867
                        }
868
                    }
869
870
                    if (!$isAutoIncrement) {
871
                        $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.";
872
                        throw new TDBMException($msg);
873
                    }
874
875
                }*/
876
877
				$types = [];
878
879
				foreach ($dbRowData as $columnName => $value) {
880
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
881
					$types[] = $columnDescriptor->getType();
882
				}
883
884
				$this->connection->insert($tableName, $dbRowData, $types);
885
886
				if (!$isPkSet && count($primaryKeyColumns) == 1) {
887
					$id = $this->connection->lastInsertId();
888
					$primaryKeys[$primaryKeyColumns[0]] = $id;
889
				}
890
891
				// TODO: change this to some private magic accessor in future
892
				$dbRow->_setPrimaryKeys($primaryKeys);
893
				$unindexedPrimaryKeys = array_values($primaryKeys);
894
895
896
897
898
				/*
899
                 * When attached, on "save", we check if the column updated is part of a primary key
900
                 * If this is part of a primary key, we call the _update_id method that updates the id in the list of known objects.
901
                 * This method should first verify that the id is not already used (and is not auto-incremented)
902
                 *
903
                 * In the object, the key is stored in an array of  (column => value), that can be directly used to update the record.
904
                 *
905
                 *
906
                 */
907
908
909
				/*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...
910
                    $this->db_connection->exec($sql);
911
                } catch (TDBMException $e) {
912
                    $this->db_onerror = true;
913
914
                    // Strange..... if we do not have the line below, bad inserts are not catched.
915
                    // It seems that destructors are called before the registered shutdown function (PHP >=5.0.5)
916
                    //if ($this->tdbmService->isProgramExiting())
917
                    //	trigger_error("program exiting");
918
                    trigger_error($e->getMessage(), E_USER_ERROR);
919
920
                    if (!$this->tdbmService->isProgramExiting())
921
                        throw $e;
922
                    else
923
                    {
924
                        trigger_error($e->getMessage(), E_USER_ERROR);
925
                    }
926
                }*/
927
928
				// Let's remove this object from the $new_objects static table.
929
				$this->removeFromToSaveObjectList($dbRow);
930
931
				// TODO: change this behaviour to something more sensible performance-wise
932
				// Maybe a setting to trigger this globally?
933
				//$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...
934
				//$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...
935
				//$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...
936
937
				// Let's add this object to the list of objects in cache.
938
				$this->_addToCache($dbRow);
939
			}
940
941
942
943
944
			$object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
945
		} elseif ($status === TDBMObjectStateEnum::STATE_DIRTY) {
946
			$dbRows = $object->_getDbRows();
947
948
			foreach ($dbRows as $dbRow) {
949
				$references = $dbRow->_getReferences();
950
951
				// Let's save all references in NEW state (we need their primary key)
952
				foreach ($references as $fkName => $reference) {
953
					if ($reference->_getStatus() === TDBMObjectStateEnum::STATE_NEW) {
954
						$this->save($reference);
955
					}
956
				}
957
958
				// Let's first get the primary keys
959
				$tableName = $dbRow->_getDbTableName();
960
				$dbRowData = $dbRow->_getDbRow();
961
962
				$schema = $this->tdbmSchemaAnalyzer->getSchema();
963
				$tableDescriptor = $schema->getTable($tableName);
964
965
				$primaryKeys = $dbRow->_getPrimaryKeys();
966
967
				$types = [];
968
969
				foreach ($dbRowData as $columnName => $value) {
970
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
971
					$types[] = $columnDescriptor->getType();
972
				}
973
				foreach ($primaryKeys as $columnName => $value) {
974
					$columnDescriptor = $tableDescriptor->getColumn($columnName);
975
					$types[] = $columnDescriptor->getType();
976
				}
977
978
				$this->connection->update($tableName, $dbRowData, $primaryKeys, $types);
979
980
				// Let's check if the primary key has been updated...
981
				$needsUpdatePk = false;
982
				foreach ($primaryKeys as $column => $value) {
983
					if (!isset($dbRowData[$column]) || $dbRowData[$column] != $value) {
984
						$needsUpdatePk = true;
985
						break;
986
					}
987
				}
988
				if ($needsUpdatePk) {
989
					$this->objectStorage->remove($tableName, $this->getObjectHash($primaryKeys));
990
					$newPrimaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
991
					$dbRow->_setPrimaryKeys($newPrimaryKeys);
992
					$this->objectStorage->set($tableName, $this->getObjectHash($primaryKeys), $dbRow);
993
				}
994
995
				// Let's remove this object from the list of objects to save.
996
				$this->removeFromToSaveObjectList($dbRow);
997
			}
998
999
			$object->_setStatus(TDBMObjectStateEnum::STATE_LOADED);
1000
1001
		} elseif ($status === TDBMObjectStateEnum::STATE_DELETED) {
1002
			throw new TDBMInvalidOperationException("This object has been deleted. It cannot be saved.");
1003
		}
1004
1005
        // Finally, let's save all the many to many relationships to this bean.
1006
        $this->persistManyToManyRelationships($object);
1007
	}
1008
1009
    private function persistManyToManyRelationships(AbstractTDBMObject $object) {
1010
        foreach ($object->_getCachedRelationships() as $pivotTableName => $storage) {
1011
            $tableDescriptor = $this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName);
1012
            list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $object);
1013
1014
            foreach ($storage as $remoteBean) {
1015
                /* @var $remoteBean AbstractTDBMObject */
1016
                $statusArr = $storage[$remoteBean];
1017
                $status = $statusArr['status'];
1018
                $reverse = $statusArr['reverse'];
1019
                if ($reverse) {
1020
                    continue;
1021
                }
1022
1023
                if ($status === 'new') {
1024
                    $remoteBeanStatus = $remoteBean->_getStatus();
1025
                    if ($remoteBeanStatus === TDBMObjectStateEnum::STATE_NEW || $remoteBeanStatus === TDBMObjectStateEnum::STATE_DETACHED) {
1026
                        // Let's save remote bean if needed.
1027
                        $this->save($remoteBean);
1028
                    }
1029
1030
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
1031
1032
                    $types = [];
1033
1034
                    foreach ($filters as $columnName => $value) {
1035
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
1036
                        $types[] = $columnDescriptor->getType();
1037
                    }
1038
1039
                    $this->connection->insert($pivotTableName, $filters, $types);
1040
1041
                    // Finally, let's mark relationships as saved.
1042
                    $statusArr['status'] = 'loaded';
1043
                    $storage[$remoteBean] = $statusArr;
1044
                    $remoteStorage = $remoteBean->_getCachedRelationships()[$pivotTableName];
1045
                    $remoteStatusArr = $remoteStorage[$object];
1046
                    $remoteStatusArr['status'] = 'loaded';
1047
                    $remoteStorage[$object] = $remoteStatusArr;
1048
1049
                } elseif ($status === 'delete') {
1050
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
1051
1052
                    $types = [];
1053
1054
                    foreach ($filters as $columnName => $value) {
1055
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
1056
                        $types[] = $columnDescriptor->getType();
1057
                    }
1058
1059
                    $this->connection->delete($pivotTableName, $filters, $types);
1060
1061
                    // Finally, let's remove relationships completely from bean.
1062
                    $storage->detach($remoteBean);
1063
                    $remoteBean->_getCachedRelationships()[$pivotTableName]->detach($object);
1064
                }
1065
            }
1066
        }
1067
    }
1068
1069
    private function getPivotFilters(AbstractTDBMObject $localBean, AbstractTDBMObject $remoteBean, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk) {
1070
        $localBeanPk = $this->getPrimaryKeyValues($localBean);
1071
        $remoteBeanPk = $this->getPrimaryKeyValues($remoteBean);
1072
        $localColumns = $localFk->getLocalColumns();
1073
        $remoteColumns = $remoteFk->getLocalColumns();
1074
1075
        $localFilters = array_combine($localColumns, $localBeanPk);
1076
        $remoteFilters = array_combine($remoteColumns, $remoteBeanPk);
1077
1078
        return array_merge($localFilters, $remoteFilters);
1079
    }
1080
1081
    /**
1082
     * Returns the "values" of the primary key.
1083
     * This returns the primary key from the $primaryKey attribute, not the one stored in the columns.
1084
     *
1085
     * @param AbstractTDBMObject $bean
1086
     * @return array numerically indexed array of values.
1087
     */
1088
    private function getPrimaryKeyValues(AbstractTDBMObject $bean) {
1089
        $dbRows = $bean->_getDbRows();
1090
        $dbRow = reset($dbRows);
1091
        return array_values($dbRow->_getPrimaryKeys());
1092
    }
1093
1094
	/**
1095
	 * Returns a unique hash used to store the object based on its primary key.
1096
	 * If the array contains only one value, then the value is returned.
1097
	 * Otherwise, a hash representing the array is returned.
1098
	 *
1099
	 * @param array $primaryKeys An array of columns => values forming the primary key
1100
	 * @return string
1101
	 */
1102
	public function getObjectHash(array $primaryKeys) {
1103
		if (count($primaryKeys) === 1) {
1104
			return reset($primaryKeys);
1105
		} else {
1106
			ksort($primaryKeys);
1107
			return md5(json_encode($primaryKeys));
1108
		}
1109
	}
1110
1111
	/**
1112
	 * Returns an array of primary keys from the object.
1113
	 * The primary keys are extracted from the object columns and not from the primary keys stored in the
1114
	 * $primaryKeys variable of the object.
1115
	 *
1116
	 * @param DbRow $dbRow
1117
	 * @return array Returns an array of column => value
1118
	 */
1119
	public function getPrimaryKeysForObjectFromDbRow(DbRow $dbRow) {
1120
		$table = $dbRow->_getDbTableName();
1121
		$dbRowData = $dbRow->_getDbRow();
1122
		return $this->_getPrimaryKeysFromObjectData($table, $dbRowData);
1123
	}
1124
1125
	/**
1126
	 * Returns an array of primary keys for the given row.
1127
	 * The primary keys are extracted from the object columns.
1128
	 *
1129
	 * @param $table
1130
	 * @param array $columns
1131
	 * @return array
1132
	 */
1133
	public function _getPrimaryKeysFromObjectData($table, array $columns) {
1134
		$primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1135
		$values = array();
1136
		foreach ($primaryKeyColumns as $column) {
1137
			if (isset($columns[$column])) {
1138
				$values[$column] = $columns[$column];
1139
			}
1140
		}
1141
		return $values;
1142
	}
1143
1144
	/**
1145
	 * Attaches $object to this TDBMService.
1146
	 * The $object must be in DETACHED state and will pass in NEW state.
1147
	 *
1148
	 * @param AbstractTDBMObject $object
1149
	 * @throws TDBMInvalidOperationException
1150
	 */
1151
	public function attach(AbstractTDBMObject $object) {
1152
		$object->_attach($this);
1153
	}
1154
1155
	/**
1156
	 * Returns an associative array (column => value) for the primary keys from the table name and an
1157
	 * indexed array of primary key values.
1158
	 *
1159
	 * @param string $tableName
1160
	 * @param array $indexedPrimaryKeys
1161
	 */
1162
	public function _getPrimaryKeysFromIndexedPrimaryKeys($tableName, array $indexedPrimaryKeys) {
1163
		$primaryKeyColumns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getPrimaryKeyColumns();
1164
1165
		if (count($primaryKeyColumns) !== count($indexedPrimaryKeys)) {
1166
			throw new TDBMException(sprintf('Wrong number of columns passed for primary key. Expected %s columns for table "%s",
1167
			got %s instead.', count($primaryKeyColumns), $tableName, count($indexedPrimaryKeys)));
1168
		}
1169
1170
		return array_combine($primaryKeyColumns, $indexedPrimaryKeys);
1171
	}
1172
1173
	/**
1174
	 * Return the list of tables (from child to parent) joining the tables passed in parameter.
1175
	 * Tables must be in a single line of inheritance. The method will find missing tables.
1176
	 *
1177
	 * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1178
	 * we must be able to find all other tables.
1179
	 *
1180
	 * @param string[] $tables
1181
	 * @return string[]
1182
	 */
1183
	public function _getLinkBetweenInheritedTables(array $tables)
1184
	{
1185
		sort($tables);
1186
		return $this->fromCache($this->cachePrefix.'_linkbetweeninheritedtables_'.implode('__split__', $tables),
1187
			function() use ($tables) {
1188
				return $this->_getLinkBetweenInheritedTablesWithoutCache($tables);
1189
			});
1190
	}
1191
1192
	/**
1193
	 * Return the list of tables (from child to parent) joining the tables passed in parameter.
1194
	 * Tables must be in a single line of inheritance. The method will find missing tables.
1195
	 *
1196
	 * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1197
	 * we must be able to find all other tables.
1198
	 *
1199
	 * @param string[] $tables
1200
	 * @return string[]
1201
	 */
1202
	private function _getLinkBetweenInheritedTablesWithoutCache(array $tables) {
1203
		$schemaAnalyzer = $this->schemaAnalyzer;
1204
1205
		foreach ($tables as $currentTable) {
1206
			$allParents = [ $currentTable ];
1207
			$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...
1208
			while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1209
				$currentTable = $currentFk->getForeignTableName();
1210
				$allParents[] = $currentTable;
1211
			};
1212
1213
			// Now, does the $allParents contain all the tables we want?
1214
			$notFoundTables = array_diff($tables, $allParents);
1215
			if (empty($notFoundTables)) {
1216
				// We have a winner!
1217
				return $allParents;
1218
			}
1219
		}
1220
1221
		throw new TDBMException(sprintf("The tables (%s) cannot be linked by an inheritance relationship.", implode(', ', $tables)));
1222
	}
1223
1224
	/**
1225
	 * Returns the list of tables related to this table (via a parent or child inheritance relationship)
1226
	 * @param string $table
1227
	 * @return string[]
1228
	 */
1229
	public function _getRelatedTablesByInheritance($table)
1230
	{
1231
		return $this->fromCache($this->cachePrefix."_relatedtables_".$table, function() use ($table) {
1232
			return $this->_getRelatedTablesByInheritanceWithoutCache($table);
1233
		});
1234
	}
1235
1236
	/**
1237
	 * Returns the list of tables related to this table (via a parent or child inheritance relationship)
1238
	 * @param string $table
1239
	 * @return string[]
1240
	 */
1241
	private function _getRelatedTablesByInheritanceWithoutCache($table) {
1242
		$schemaAnalyzer = $this->schemaAnalyzer;
1243
1244
1245
		// Let's scan the parent tables
1246
		$currentTable = $table;
1247
1248
		$parentTables = [ ];
1249
1250
		// Get parent relationship
1251
		while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1252
			$currentTable = $currentFk->getForeignTableName();
1253
			$parentTables[] = $currentTable;
1254
		};
1255
1256
		// Let's recurse in children
1257
		$childrenTables = $this->exploreChildrenTablesRelationships($schemaAnalyzer, $table);
1258
1259
		return array_merge(array_reverse($parentTables), $childrenTables);
1260
	}
1261
1262
	/**
1263
	 * Explore all the children and descendant of $table and returns ForeignKeyConstraints on those.
1264
	 *
1265
	 * @param string $table
1266
	 * @return string[]
1267
	 */
1268
	private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyzer, $table) {
1269
		$tables = [$table];
1270
		$keys = $schemaAnalyzer->getChildrenRelationships($table);
1271
1272
		foreach ($keys as $key) {
1273
			$tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, $key->getLocalTableName()));
1274
		}
1275
1276
		return $tables;
1277
	}
1278
1279
	/**
1280
	 * Casts a foreign key into SQL, assuming table name is used with no alias.
1281
	 * The returned value does contain only one table. For instance:
1282
	 *
1283
	 * " LEFT JOIN table2 ON table1.id = table2.table1_id"
1284
	 *
1285
	 * @param ForeignKeyConstraint $fk
1286
	 * @param bool $leftTableIsLocal
1287
	 * @return string
1288
	 */
1289
	/*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...
1290
		$onClauses = [];
1291
		$foreignTableName = $this->connection->quoteIdentifier($fk->getForeignTableName());
1292
		$foreignColumns = $fk->getForeignColumns();
1293
		$localTableName = $this->connection->quoteIdentifier($fk->getLocalTableName());
1294
		$localColumns = $fk->getLocalColumns();
1295
		$columnCount = count($localTableName);
1296
1297
		for ($i = 0; $i < $columnCount; $i++) {
1298
			$onClauses[] = sprintf("%s.%s = %s.%s",
1299
				$localTableName,
1300
				$this->connection->quoteIdentifier($localColumns[$i]),
1301
				$foreignColumns,
1302
				$this->connection->quoteIdentifier($foreignColumns[$i])
1303
				);
1304
		}
1305
1306
		$onClause = implode(' AND ', $onClauses);
1307
1308
		if ($leftTableIsLocal) {
1309
			return sprintf(" LEFT JOIN %s ON (%s)", $foreignTableName, $onClause);
1310
		} else {
1311
			return sprintf(" LEFT JOIN %s ON (%s)", $localTableName, $onClause);
1312
		}
1313
	}*/
1314
1315
	/**
1316
	 * Returns an identifier for the group of tables passed in parameter.
1317
	 *
1318
	 * @param string[] $relatedTables
1319
	 * @return string
1320
	 */
1321
	private function getTableGroupName(array $relatedTables) {
1322
		sort($relatedTables);
1323
		return implode('_``_', $relatedTables);
1324
	}
1325
1326
	/**
1327
	 *
1328
	 * @param string $mainTable The name of the table queried
1329
	 * @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)
1330
	 * @param array $parameters
1331
	 * @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)
1332
	 * @param array $additionalTablesFetch
1333
	 * @param string $mode
1334
	 * @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.
1335
	 * @return ResultIterator An object representing an array of results.
1336
	 * @throws TDBMException
1337
	 */
1338
	public function findObjects($mainTable, $filter=null, array $parameters = array(), $orderString=null, array $additionalTablesFetch = array(), $mode = null, $className=null) {
1339
		// $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1340
		if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1341
			throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1342
		}
1343
1344
		list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter);
1345
1346
		$parameters = array_merge($parameters, $additionalParameters);
1347
1348
		// From the table name and the additional tables we want to fetch, let's build a list of all tables
1349
		// that must be part of the select columns.
1350
1351
		$tableGroups = [];
1352
		$allFetchedTables = $this->_getRelatedTablesByInheritance($mainTable);
1353
		$tableGroupName = $this->getTableGroupName($allFetchedTables);
1354
		foreach ($allFetchedTables as $table) {
1355
			$tableGroups[$table] = $tableGroupName;
1356
		}
1357
1358
		foreach ($additionalTablesFetch as $additionalTable) {
1359
			$relatedTables = $this->_getRelatedTablesByInheritance($additionalTable);
1360
			$tableGroupName = $this->getTableGroupName($relatedTables);
1361
			foreach ($relatedTables as $table) {
1362
				$tableGroups[$table] = $tableGroupName;
1363
			}
1364
			$allFetchedTables = array_merge($allFetchedTables, $relatedTables);
1365
		}
1366
1367
		// Let's remove any duplicate
1368
		$allFetchedTables = array_flip(array_flip($allFetchedTables));
1369
1370
		$columnsList = [];
1371
		$columnDescList = [];
1372
		$schema = $this->tdbmSchemaAnalyzer->getSchema();
1373
1374
		// Now, let's build the column list
1375
		foreach ($allFetchedTables as $table) {
1376
			foreach ($schema->getTable($table)->getColumns() as $column) {
1377
				$columnName = $column->getName();
1378
				$columnDescList[] = [
1379
					'as' => $table.'____'.$columnName,
1380
					'table' => $table,
1381
					'column' => $columnName,
1382
					'type' => $column->getType(),
1383
					'tableGroup' => $tableGroups[$table]
1384
				];
1385
				$columnsList[] = $this->connection->quoteIdentifier($table).'.'.$this->connection->quoteIdentifier($columnName).' as '.
1386
					$this->connection->quoteIdentifier($table.'____'.$columnName);
1387
			}
1388
		}
1389
1390
		$sql = "SELECT DISTINCT ".implode(', ', $columnsList)." FROM MAGICJOIN(".$mainTable.")";
1391
		$countSql = "SELECT COUNT(1) FROM MAGICJOIN(".$mainTable.")";
1392
1393
		if (!empty($filterString)) {
1394
			$sql .= " WHERE ".$filterString;
1395
			$countSql .= " WHERE ".$filterString;
1396
		}
1397
1398
		if (!empty($orderString)) {
1399
			$sql .= " ORDER BY ".$orderString;
1400
			$countSql .= " ORDER BY ".$orderString;
1401
		}
1402
1403 View Code Duplication
		if ($mode !== null && $mode !== self::MODE_CURSOR && $mode !== self::MODE_ARRAY) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $mode (string) and self::MODE_CURSOR (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $mode (string) and self::MODE_ARRAY (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1404
			throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
1405
		}
1406
1407
		$mode = $mode?:$this->mode;
1408
1409
		return new ResultIterator($sql, $countSql, $parameters, $columnDescList, $this->objectStorage, $className, $this, $this->magicQuery, $mode);
1410
	}
1411
1412
	/**
1413
	 * @param $table
1414
	 * @param array $primaryKeys
1415
	 * @param array $additionalTablesFetch
1416
	 * @param bool $lazy Whether to perform lazy loading on this object or not.
1417
	 * @param string $className
1418
	 * @return AbstractTDBMObject
1419
	 * @throws TDBMException
1420
	 */
1421
	public function findObjectByPk($table, array $primaryKeys, array $additionalTablesFetch = array(), $lazy = false, $className=null) {
1422
		$primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys);
1423
		$hash = $this->getObjectHash($primaryKeys);
1424
1425
		if ($this->objectStorage->has($table, $hash)) {
1426
			$dbRow = $this->objectStorage->get($table, $hash);
1427
			$bean = $dbRow->getTDBMObject();
1428
			if ($className !== null && !is_a($bean, $className)) {
1429
				throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'");
1430
			}
1431
			return $bean;
1432
		}
1433
1434
		// Are we performing lazy fetching?
1435
		if ($lazy === true) {
1436
			// Can we perform lazy fetching?
1437
			$tables = $this->_getRelatedTablesByInheritance($table);
1438
			// Only allowed if no inheritance.
1439
			if (count($tables) === 1) {
1440
				if ($className === null) {
1441
					$className = isset($this->tableToBeanMap[$table])?$this->tableToBeanMap[$table]:"Mouf\\Database\\TDBM\\TDBMObject";
1442
				}
1443
1444
				// Let's construct the bean
1445
				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...
1446
					$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...
1447
				}
1448
				// Let's bypass the constructor when creating the bean!
1449
				$bean = $reflectionClassCache[$className]->newInstanceWithoutConstructor();
1450
				/* @var $bean AbstractTDBMObject */
1451
				$bean->_constructLazy($table, $primaryKeys, $this);
1452
			}
1453
		}
1454
1455
		// Did not find the object in cache? Let's query it!
1456
		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...
1457
	}
1458
1459
	/**
1460
	 * Returns a unique bean (or null) according to the filters passed in parameter.
1461
	 *
1462
	 * @param string $mainTable The name of the table queried
1463
	 * @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)
1464
	 * @param array $parameters
1465
	 * @param array $additionalTablesFetch
1466
	 * @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.
1467
	 * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters.
1468
	 * @throws TDBMException
1469
	 */
1470
	public function findObject($mainTable, $filterString=null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null) {
1471
		$objects = $this->findObjects($mainTable, $filterString, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className);
1472
		$page = $objects->take(0, 2);
1473
		$count = $page->count();
1474
		if ($count > 1) {
1475
			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.");
1476
		} elseif ($count === 0) {
1477
			return null;
1478
		}
1479
		return $objects[0];
1480
	}
1481
1482
	/**
1483
	 * Returns a unique bean according to the filters passed in parameter.
1484
	 * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1485
	 *
1486
	 * @param string $mainTable The name of the table queried
1487
	 * @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)
1488
	 * @param array $parameters
1489
	 * @param array $additionalTablesFetch
1490
	 * @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.
1491
	 * @return AbstractTDBMObject The object we want
1492
	 * @throws TDBMException
1493
	 */
1494
	public function findObjectOrFail($mainTable, $filterString=null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null) {
1495
		$bean = $this->findObject($mainTable, $filterString, $parameters, $additionalTablesFetch, $className);
1496
		if ($bean === null) {
1497
			throw new NoBeanFoundException("No result found for query on table '".$mainTable."'");
1498
		}
1499
		return $bean;
1500
	}
1501
1502
	/**
1503
	 * @param array $beanData An array of data: array<table, array<column, value>>
1504
	 * @return array an array with first item = class name and second item = table name
1505
	 */
1506
	public function _getClassNameFromBeanData(array $beanData) {
1507
		if (count($beanData) === 1) {
1508
			$tableName = array_keys($beanData)[0];
1509
		} else {
1510
			foreach ($beanData as $table => $row) {
1511
				$tables = [];
1512
				$primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1513
				$pkSet = false;
1514
				foreach ($primaryKeyColumns as $columnName) {
1515
					if ($row[$columnName] !== null) {
1516
						$pkSet = true;
1517
						break;
1518
					}
1519
				}
1520
				if ($pkSet) {
1521
					$tables[] = $table;
1522
				}
1523
			}
1524
1525
			// $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1526
			$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...
1527
			$tableName = $allTables[0];
1528
		}
1529
1530
		// Only one table in this bean. Life is sweat, let's look at its type:
1531
		if (isset($this->tableToBeanMap[$tableName])) {
1532
			return [$this->tableToBeanMap[$tableName], $tableName];
1533
		} else {
1534
			return ["Mouf\\Database\\TDBM\\TDBMObject", $tableName];
1535
		}
1536
	}
1537
1538
	/**
1539
	 * Returns an item from cache or computes it using $closure and puts it in cache.
1540
	 *
1541
	 * @param string   $key
1542
	 * @param callable $closure
1543
	 *
1544
	 * @return mixed
1545
	 */
1546
	private function fromCache($key, callable $closure)
1547
	{
1548
		$item = $this->cache->fetch($key);
1549
		if ($item === false) {
1550
			$item = $closure();
1551
			$this->cache->save($key, $item);
1552
		}
1553
1554
		return $item;
1555
	}
1556
1557
	/**
1558
	 * Returns the foreign key object.
1559
	 * @param string $table
1560
	 * @param string $fkName
1561
	 * @return ForeignKeyConstraint
1562
	 */
1563
	public function _getForeignKeyByName($table, $fkName) {
1564
		return $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getForeignKey($fkName);
1565
	}
1566
1567
	/**
1568
	 * @param $pivotTableName
1569
	 * @param AbstractTDBMObject $bean
1570
	 * @return AbstractTDBMObject[]
1571
	 */
1572
	public function _getRelatedBeans($pivotTableName, AbstractTDBMObject $bean) {
1573
1574
        list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $bean);
1575
        /* @var $localFk ForeignKeyConstraint */
1576
        /* @var $remoteFk ForeignKeyConstraint */
1577
        $remoteTable = $remoteFk->getForeignTableName();
1578
1579
1580
        $primaryKeys = $this->getPrimaryKeyValues($bean);
1581
        $columnNames = array_map(function($name) use ($pivotTableName) { return $pivotTableName.'.'.$name; }, $localFk->getLocalColumns());
1582
1583
        $filter = array_combine($columnNames, $primaryKeys);
1584
1585
        return $this->findObjects($remoteTable, $filter);
1586
	}
1587
1588
    /**
1589
     * @param $pivotTableName
1590
     * @param AbstractTDBMObject $bean The LOCAL bean
1591
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean.
1592
     * @throws TDBMException
1593
     */
1594
    private function getPivotTableForeignKeys($pivotTableName, AbstractTDBMObject $bean) {
1595
        $fks = array_values($this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
1596
        $table1 = $fks[0]->getForeignTableName();
1597
        $table2 = $fks[1]->getForeignTableName();
1598
1599
        $beanTables = array_map(function(DbRow $dbRow) { return $dbRow->_getDbTableName(); }, $bean->_getDbRows());
1600
1601
        if (in_array($table1, $beanTables)) {
1602
            return [$fks[0], $fks[1]];
1603
        } elseif (in_array($table2, $beanTables)) {
1604
            return [$fks[1], $fks[0]];
1605
        } else {
1606
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2} for pivot table {$pivotTableName}");
1607
        }
1608
    }
1609
1610
	/**
1611
	 * Returns a list of pivot tables linked to $bean.
1612
	 *
1613
	 * @access private
1614
	 * @param AbstractTDBMObject $bean
1615
	 * @return string[]
1616
	 */
1617
	public function _getPivotTablesLinkedToBean(AbstractTDBMObject $bean) {
1618
		$junctionTables = [];
1619
		$allJunctionTables = $this->schemaAnalyzer->detectJunctionTables();
1620
		foreach ($bean->_getDbRows() as $dbRow) {
1621
			foreach ($allJunctionTables as $table) {
1622
				// There are exactly 2 FKs since this is a pivot table.
1623
				$fks = array_values($table->getForeignKeys());
1624
1625
				if ($fks[0]->getForeignTableName() === $dbRow->_getDbTableName() || $fks[1]->getForeignTableName() === $dbRow->_getDbTableName()) {
1626
					$junctionTables[] = $table->getName();
1627
				}
1628
			}
1629
		}
1630
1631
		return $junctionTables;
1632
	}
1633
}
1634