Completed
Push — 4.0 ( 243a2b...1abf1e )
by David
50:20 queued 27:28
created

TDBMService::buildFilterFromFilterBag()   D

Complexity

Conditions 9
Paths 9

Size

Total Lines 97
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
c 6
b 1
f 0
dl 0
loc 97
rs 4.9219
cc 9
eloc 32
nc 9
nop 1

How to fix   Long Method   

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
/*
4
 Copyright (C) 2006-2016 David Négrier - THE CODING MACHINE
5
6
This program is free software; you can redistribute it and/or modify
7
it under the terms of the GNU General Public License as published by
8
the Free Software Foundation; either version 2 of the License, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
GNU General Public License for more details.
15
16
You should have received a copy of the GNU General Public License
17
along with this program; if not, write to the Free Software
18
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
*/
20
21
namespace Mouf\Database\TDBM;
22
23
use Doctrine\Common\Cache\Cache;
24
use Doctrine\Common\Cache\VoidCache;
25
use Doctrine\DBAL\Connection;
26
use Doctrine\DBAL\Schema\Column;
27
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
28
use Doctrine\DBAL\Schema\Schema;
29
use Doctrine\DBAL\Schema\Table;
30
use Mouf\Database\MagicQuery;
31
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
32
use Mouf\Database\TDBM\Filters\OrderBySQLString;
33
use Mouf\Database\TDBM\Utils\TDBMDaoGenerator;
34
35
/**
36
 * The TDBMService class is the main TDBM class. It provides methods to retrieve TDBMObject instances
37
 * from the database.
38
 *
39
 * @author David Negrier
40
 * @ExtendedAction {"name":"Generate DAOs", "url":"tdbmadmin/", "default":false}
41
 */
42
class TDBMService
43
{
44
    const MODE_CURSOR = 1;
45
    const MODE_ARRAY = 2;
46
47
    /**
48
     * The database connection.
49
     *
50
     * @var Connection
51
     */
52
    private $connection;
53
54
    /**
55
     * @var SchemaAnalyzer
56
     */
57
    private $schemaAnalyzer;
58
59
    /**
60
     * @var MagicQuery
61
     */
62
    private $magicQuery;
63
64
    /**
65
     * @var TDBMSchemaAnalyzer
66
     */
67
    private $tdbmSchemaAnalyzer;
68
69
    /**
70
     * @var string
71
     */
72
    private $cachePrefix;
73
74
    /**
75
     * The default autosave mode for the objects
76
     * True to automatically save the object.
77
     * If false, the user must explicitly call the save() method to save the object.
78
     *
79
     * @var bool
80
     */
81
    //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...
82
83
    /**
84
     * Cache of table of primary keys.
85
     * Primary keys are stored by tables, as an array of column.
86
     * For instance $primary_key['my_table'][0] will return the first column of the primary key of table 'my_table'.
87
     *
88
     * @var string[]
89
     */
90
    private $primaryKeysColumns;
91
92
    /**
93
     * Service storing objects in memory.
94
     * Access is done by table name and then by primary key.
95
     * If the primary key is split on several columns, access is done by an array of columns, serialized.
96
     *
97
     * @var StandardObjectStorage|WeakrefObjectStorage
98
     */
99
    private $objectStorage;
100
101
    /**
102
     * The fetch mode of the result sets returned by `getObjects`.
103
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY or TDBMObjectArray::MODE_COMPATIBLE_ARRAY.
104
     *
105
     * In 'MODE_ARRAY' mode (default), the result is an array. Use this mode by default (unless the list returned is very big).
106
     * 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,
107
     * and it cannot be accessed via key. Use this mode for large datasets processed by batch.
108
     * In 'MODE_COMPATIBLE_ARRAY' mode, the result is an old TDBMObjectArray (used up to TDBM 3.2).
109
     * You can access the array by key, or using foreach, several times.
110
     *
111
     * @var int
112
     */
113
    private $mode = self::MODE_ARRAY;
114
115
    /**
116
     * Table of new objects not yet inserted in database or objects modified that must be saved.
117
     *
118
     * @var \SplObjectStorage of DbRow objects
119
     */
120
    private $toSaveObjects;
121
122
    /// The timestamp of the script startup. Useful to stop execution before time limit is reached and display useful error message.
123
    public static $script_start_up_time;
124
125
    /**
126
     * The content of the cache variable.
127
     *
128
     * @var array<string, mixed>
129
     */
130
    private $cache;
131
132
    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...
133
134
    /**
135
     * Map associating a table name to a fully qualified Bean class name.
136
     *
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
    {
154
        //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...
155
        if (extension_loaded('weakref')) {
156
            $this->objectStorage = new WeakrefObjectStorage();
157
        } else {
158
            $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...
159
        }
160
        $this->connection = $connection;
161
        if ($cache !== null) {
162
            $this->cache = $cache;
163
        } else {
164
            $this->cache = new VoidCache();
165
        }
166
        if ($schemaAnalyzer) {
167
            $this->schemaAnalyzer = $schemaAnalyzer;
168
        } else {
169
            $this->schemaAnalyzer = new SchemaAnalyzer($this->connection->getSchemaManager(), $this->cache, $this->getConnectionUniqueId());
170
        }
171
172
        $this->magicQuery = new MagicQuery($this->connection, $this->cache, $this->schemaAnalyzer);
173
174
        $this->tdbmSchemaAnalyzer = new TDBMSchemaAnalyzer($connection, $this->cache, $this->schemaAnalyzer);
175
        $this->cachePrefix = $this->tdbmSchemaAnalyzer->getCachePrefix();
176
177
        if (self::$script_start_up_time === null) {
178
            self::$script_start_up_time = microtime(true);
179
        }
180
        $this->toSaveObjects = new \SplObjectStorage();
181
    }
182
183
    /**
184
     * Returns the object used to connect to the database.
185
     *
186
     * @return Connection
187
     */
188
    public function getConnection()
189
    {
190
        return $this->connection;
191
    }
192
193
    /**
194
     * Creates a unique cache key for the current connection.
195
     *
196
     * @return string
197
     */
198
    private function getConnectionUniqueId()
199
    {
200
        return hash('md4', $this->connection->getHost().'-'.$this->connection->getPort().'-'.$this->connection->getDatabase().'-'.$this->connection->getDriver()->getName());
201
    }
202
203
    /**
204
     * Sets the default fetch mode of the result sets returned by `getObjects`.
205
     * Can be one of: TDBMObjectArray::MODE_CURSOR or TDBMObjectArray::MODE_ARRAY.
206
     *
207
     * 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).
208
     * 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
209
     * several times. In cursor mode, you cannot access the result set by key. Use this mode for large datasets processed by batch.
210
     *
211
     * @param int $mode
212
     *
213
     * @return $this
214
     *
215
     * @throws TDBMException
216
     */
217
    public function setFetchMode($mode)
218
    {
219 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...
220
            throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
221
        }
222
        $this->mode = $mode;
223
224
        return $this;
225
    }
226
227
    /**
228
     * Returns a TDBMObject associated from table "$table_name".
229
     * If the $filters parameter is an int/string, the object returned will be the object whose primary key = $filters.
230
     * $filters can also be a set of TDBM_Filters (see the getObjects method for more details).
231
     *
232
     * For instance, if there is a table 'users', with a primary key on column 'user_id' and a column 'user_name', then
233
     * 			$user = $tdbmService->getObject('users',1);
234
     * 			echo $user->name;
235
     * will return the name of the user whose user_id is one.
236
     *
237
     * 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.
238
     * For instance:
239
     * 			$group = $tdbmService->getObject('groups',array(1,2));
240
     *
241
     * Note that TDBMObject performs caching for you. If you get twice the same object, the reference of the object you will get
242
     * will be the same.
243
     *
244
     * For instance:
245
     * 			$user1 = $tdbmService->getObject('users',1);
246
     * 			$user2 = $tdbmService->getObject('users',1);
247
     * 			$user1->name = 'John Doe';
248
     * 			echo $user2->name;
249
     * will return 'John Doe'.
250
     *
251
     * You can use filters instead of passing the primary key. For instance:
252
     * 			$user = $tdbmService->getObject('users',new EqualFilter('users', 'login', 'jdoe'));
253
     * This will return the user whose login is 'jdoe'.
254
     * Please note that if 2 users have the jdoe login in database, the method will throw a TDBM_DuplicateRowException.
255
     *
256
     * Also, you can specify the return class for the object (provided the return class extends TDBMObject).
257
     * For instance:
258
     *  	$user = $tdbmService->getObject('users',1,'User');
259
     * will return an object from the "User" class. The "User" class must extend the "TDBMObject" class.
260
     * Please be sure not to override any method or any property unless you perfectly know what you are doing!
261
     *
262
     * @param string $table_name   The name of the table we retrieve an object from.
263
     * @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)
264
     * @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.
265
     * @param bool   $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.
266
     *
267
     * @return TDBMObject
268
     */
269
/*	public function getObject($table_name, $filters, $className = null, $lazy_loading = false) {
270
271
        if (is_array($filters) || $filters instanceof FilterInterface) {
272
            $isFilterBag = false;
273
            if (is_array($filters)) {
274
                // Is this a multiple primary key or a filter bag?
275
                // Let's have a look at the first item of the array to decide.
276
                foreach ($filters as $filter) {
277
                    if (is_array($filter) || $filter instanceof FilterInterface) {
278
                        $isFilterBag = true;
279
                    }
280
                    break;
281
                }
282
            } else {
283
                $isFilterBag = true;
284
            }
285
286
            if ($isFilterBag == true) {
287
                // If a filter bag was passer in parameter, let's perform a getObjects.
288
                $objects = $this->getObjects($table_name, $filters, null, null, null, $className);
289
                if (count($objects) == 0) {
290
                    return null;
291
                } elseif (count($objects) > 1) {
292
                    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.");
293
                }
294
                // Return the first and only object.
295
                if ($objects instanceof \Generator) {
296
                    return $objects->current();
297
                } else {
298
                    return $objects[0];
299
                }
300
            }
301
        }
302
        $id = $filters;
303
        if ($this->connection == null) {
304
            throw new TDBMException("Error while calling TdbmService->getObject(): No connection has been established on the database!");
305
        }
306
        $table_name = $this->connection->toStandardcase($table_name);
307
308
        // If the ID is null, let's throw an exception
309
        if ($id === null) {
310
            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.");
311
        }
312
313
        // If the primary key is split over many columns, the IDs are passed in an array. Let's serialize this array to store it.
314
        if (is_array($id)) {
315
            $id = serialize($id);
316
        }
317
318
        if ($className === null) {
319
            if (isset($this->tableToBeanMap[$table_name])) {
320
                $className = $this->tableToBeanMap[$table_name];
321
            } else {
322
                $className = "Mouf\\Database\\TDBM\\TDBMObject";
323
            }
324
        }
325
326
        if ($this->objectStorage->has($table_name, $id)) {
327
            $obj = $this->objectStorage->get($table_name, $id);
328
            if (is_a($obj, $className)) {
329
                return $obj;
330
            } else {
331
                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'");
332
            }
333
        }
334
335
        if ($className != "Mouf\\Database\\TDBM\\TDBMObject" && !is_subclass_of($className, "Mouf\\Database\\TDBM\\TDBMObject")) {
336
            if (!class_exists($className)) {
337
                throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." does not exist.");
338
            } else {
339
                throw new TDBMException("Error while calling TDBMService->getObject: The class ".$className." should extend TDBMObject.");
340
            }
341
        }
342
        $obj = new $className($this, $table_name, $id);
343
344
        if ($lazy_loading == false) {
345
            // If we are not doing lazy loading, let's load the object:
346
            $obj->_dbLoadIfNotLoaded();
347
        }
348
349
        $this->objectStorage->set($table_name, $id, $obj);
350
351
        return $obj;
352
    }*/
353
354
    /**
355
     * Removes the given object from database.
356
     * This cannot be called on an object that is not attached to this TDBMService
357
     * (will throw a TDBMInvalidOperationException).
358
     *
359
     * @param AbstractTDBMObject $object the object to delete.
360
     *
361
     * @throws TDBMException
362
     * @throws TDBMInvalidOperationException
363
     */
364
    public function delete(AbstractTDBMObject $object)
365
    {
366
        switch ($object->_getStatus()) {
367
            case TDBMObjectStateEnum::STATE_DELETED:
368
                // Nothing to do, object already deleted.
369
                return;
370
            case TDBMObjectStateEnum::STATE_DETACHED:
371
                throw new TDBMInvalidOperationException('Cannot delete a detached object');
372
            case TDBMObjectStateEnum::STATE_NEW:
373
                $this->deleteManyToManyRelationships($object);
374
                foreach ($object->_getDbRows() as $dbRow) {
375
                    $this->removeFromToSaveObjectList($dbRow);
376
                }
377
                break;
378
            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...
379
                foreach ($object->_getDbRows() as $dbRow) {
380
                    $this->removeFromToSaveObjectList($dbRow);
381
                }
382
            case TDBMObjectStateEnum::STATE_NOT_LOADED:
383
            case TDBMObjectStateEnum::STATE_LOADED:
384
                $this->deleteManyToManyRelationships($object);
385
                // Let's delete db rows, in reverse order.
386
                foreach (array_reverse($object->_getDbRows()) as $dbRow) {
387
                    $tableName = $dbRow->_getDbTableName();
388
                    $primaryKeys = $dbRow->_getPrimaryKeys();
389
                    $this->connection->delete($tableName, $primaryKeys);
390
                    $this->objectStorage->remove($dbRow->_getDbTableName(), $this->getObjectHash($primaryKeys));
391
                }
392
                break;
393
            // @codeCoverageIgnoreStart
394
            default:
395
                throw new TDBMInvalidOperationException('Unexpected status for bean');
396
            // @codeCoverageIgnoreEnd
397
        }
398
399
        $object->_setStatus(TDBMObjectStateEnum::STATE_DELETED);
400
    }
401
402
    /**
403
     * Removes all many to many relationships for this object.
404
     *
405
     * @param AbstractTDBMObject $object
406
     */
407
    private function deleteManyToManyRelationships(AbstractTDBMObject $object)
408
    {
409
        foreach ($object->_getDbRows() as $tableName => $dbRow) {
410
            $pivotTables = $this->tdbmSchemaAnalyzer->getPivotTableLinkedToTable($tableName);
411
            foreach ($pivotTables as $pivotTable) {
412
                $remoteBeans = $object->_getRelationships($pivotTable);
413
                foreach ($remoteBeans as $remoteBean) {
414
                    $object->_removeRelationship($pivotTable, $remoteBean);
415
                }
416
            }
417
        }
418
        $this->persistManyToManyRelationships($object);
419
    }
420
421
    /**
422
     * This function removes the given object from the database. It will also remove all objects relied to the one given
423
     * by parameter before all.
424
     *
425
     * Notice: if the object has a multiple primary key, the function will not work.
426
     *
427
     * @param AbstractTDBMObject $objToDelete
428
     */
429
    public function deleteCascade(AbstractTDBMObject $objToDelete)
430
    {
431
        $this->deleteAllConstraintWithThisObject($objToDelete);
432
        $this->delete($objToDelete);
433
    }
434
435
    /**
436
     * This function is used only in TDBMService (private function)
437
     * It will call deleteCascade function foreach object relied with a foreign key to the object given by parameter.
438
     *
439
     * @param AbstractTDBMObject $obj
440
     */
441
    private function deleteAllConstraintWithThisObject(AbstractTDBMObject $obj)
442
    {
443
        $dbRows = $obj->_getDbRows();
444
        foreach ($dbRows as $dbRow) {
445
            $tableName = $dbRow->_getDbTableName();
446
            $pks = array_values($dbRow->_getPrimaryKeys());
447
            if (!empty($pks)) {
448
                $incomingFks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($tableName);
449
450
                foreach ($incomingFks as $incomingFk) {
451
                    $filter = array_combine($incomingFk->getLocalColumns(), $pks);
452
453
                    $results = $this->findObjects($incomingFk->getLocalTableName(), $filter);
454
455
                    foreach ($results as $bean) {
456
                        $this->deleteCascade($bean);
457
                    }
458
                }
459
            }
460
        }
461
    }
462
463
    /**
464
     * This function performs a save() of all the objects that have been modified.
465
     */
466
    public function completeSave()
467
    {
468
        foreach ($this->toSaveObjects as $dbRow) {
469
            $this->save($dbRow->getTDBMObject());
470
        }
471
    }
472
473
    /**
474
     * Returns an array of objects of "table_name" kind filtered from the filter bag.
475
     *
476
     * The getObjects method should be the most used query method in TDBM if you want to query the database for objects.
477
     * (Note: if you want to query the database for an object by its primary key, use the getObject method).
478
     *
479
     * The getObjects method takes in parameter:
480
     * 	- table_name: the kinf of TDBMObject you want to retrieve. In TDBM, a TDBMObject matches a database row, so the
481
     * 			$table_name parameter should be the name of an existing table in database.
482
     *  - filter_bag: The filter bag is anything that you can use to filter your request. It can be a SQL Where clause,
483
     * 			a series of TDBM_Filter objects, or even TDBMObjects or TDBMObjectArrays that you will use as filters.
484
     *  - order_bag: The order bag is anything that will be used to order the data that is passed back to you.
485
     * 			A SQL Order by clause can be used as an order bag for instance, or a OrderByColumn object
486
     * 	- from (optionnal): The offset from which the query should be performed. For instance, if $from=5, the getObjects method
487
     * 			will return objects from the 6th rows.
488
     * 	- limit (optionnal): The maximum number of objects to return. Used together with $from, you can implement
489
     * 			paging mechanisms.
490
     *  - hint_path (optionnal): EXPERTS ONLY! The path the request should use if not the most obvious one. This parameter
491
     * 			should be used only if you perfectly know what you are doing.
492
     *
493
     * The getObjects method will return a TDBMObjectArray. A TDBMObjectArray is an array of TDBMObjects that does behave as
494
     * a single TDBMObject if the array has only one member. Refer to the documentation of TDBMObjectArray and TDBMObject
495
     * to learn more.
496
     *
497
     * More about the filter bag:
498
     * A filter is anything that can change the set of objects returned by getObjects.
499
     * There are many kind of filters in TDBM:
500
     * A filter can be:
501
     * 	- A SQL WHERE clause:
502
     * 		The clause is specified without the "WHERE" keyword. For instance:
503
     * 			$filter = "users.first_name LIKE 'J%'";
504
     *     	is a valid filter.
505
     * 	   	The only difference with SQL is that when you specify a column name, it should always be fully qualified with
506
     * 		the table name: "country_name='France'" is not valid, while "countries.country_name='France'" is valid (if
507
     * 		"countries" is a table and "country_name" a column in that table, sure.
508
     * 		For instance,
509
     * 				$french_users = TDBMObject::getObjects("users", "countries.country_name='France'");
510
     * 		will return all the users that are French (based on trhe assumption that TDBM can find a way to connect the users
511
     * 		table to the country table using foreign keys, see the manual for that point).
512
     * 	- A TDBMObject:
513
     * 		An object can be used as a filter. For instance, we could get the France object and then find any users related to
514
     * 		that object using:
515
     * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
516
     * 				$french_users = TDBMObject::getObjects("users", $france);
517
     *  - A TDBMObjectArray can be used as a filter too.
518
     * 		For instance:
519
     * 				$french_groups = TDBMObject::getObjects("groups", $french_users);
520
     * 		might return all the groups in which french users can be found.
521
     *  - Finally, TDBM_xxxFilter instances can be used.
522
     * 		TDBM provides the developer a set of TDBM_xxxFilters that can be used to model a SQL Where query.
523
     * 		Using the appropriate filter object, you can model the operations =,<,<=,>,>=,IN,LIKE,AND,OR, IS NULL and NOT
524
     * 		For instance:
525
     * 				$french_users = TDBMObject::getObjects("users", new EqualFilter('countries','country_name','France');
526
     * 		Refer to the documentation of the appropriate filters for more information.
527
     *
528
     * The nice thing about a filter bag is that it can be any filter, or any array of filters. In that case, filters are
529
     * 'ANDed' together.
530
     * So a request like this is valid:
531
     * 				$france = TDBMObject::getObjects("country", "countries.country_name='France'");
532
     * 				$french_administrators = TDBMObject::getObjects("users", array($france,"role.role_name='Administrators'");
533
     * This requests would return the users that are both French and administrators.
534
     *
535
     * Finally, if filter_bag is null, the whole table is returned.
536
     *
537
     * More about the order bag:
538
     * The order bag contains anything that can be used to order the data that is passed back to you.
539
     * The order bag can contain two kinds of objects:
540
     * 	- A SQL ORDER BY clause:
541
     * 		The clause is specified without the "ORDER BY" keyword. For instance:
542
     * 			$orderby = "users.last_name ASC, users.first_name ASC";
543
     *     	is a valid order bag.
544
     * 		The only difference with SQL is that when you specify a column name, it should always be fully qualified with
545
     * 		the table name: "country_name ASC" is not valid, while "countries.country_name ASC" is valid (if
546
     * 		"countries" is a table and "country_name" a column in that table, sure.
547
     * 		For instance,
548
     * 				$french_users = TDBMObject::getObjects("users", null, "countries.country_name ASC");
549
     * 		will return all the users sorted by country.
550
     *  - A OrderByColumn object
551
     * 		This object models a single column in a database.
552
     *
553
     * @param string       $table_name  The name of the table queried
554
     * @param mixed        $filter_bag  The filter bag (see above for complete description)
555
     * @param mixed        $orderby_bag The order bag (see above for complete description)
556
     * @param int          $from        The offset
557
     * @param int          $limit       The maximum number of rows returned
558
     * @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.
559
     * @param unknown_type $hint_path   Hints to get the path for the query (expert parameter, you should leave it to null).
560
     *
561
     * @return TDBMObjectArray A TDBMObjectArray containing the resulting objects of the query.
562
     */
563
/*	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...
564
        if ($this->connection == null) {
565
            throw new TDBMException("Error while calling TDBMObject::getObject(): No connection has been established on the database!");
566
        }
567
        return $this->getObjectsByMode('getObjects', $table_name, $filter_bag, $orderby_bag, $from, $limit, $className, $hint_path);
568
    }*/
569
570
    /**
571
     * Takes in input a filter_bag (which can be about anything from a string to an array of TDBMObjects... see above from documentation),
572
     * and gives back a proper Filter object.
573
     *
574
     * @param mixed $filter_bag
575
     *
576
     * @return array First item: filter string, second item: parameters.
577
     *
578
     * @throws TDBMException
579
     */
580
    public function buildFilterFromFilterBag($filter_bag)
581
    {
582
        $counter = 1;
583
        if ($filter_bag === null) {
584
            return ['', []];
585
        } elseif (is_string($filter_bag)) {
586
            return [$filter_bag, []];
587
        } elseif (is_array($filter_bag)) {
588
            $sqlParts = [];
589
            $parameters = [];
590
            foreach ($filter_bag as $column => $value) {
591
                $paramName = 'tdbmparam'.$counter;
592
                if (is_array($value)) {
593
                    $sqlParts[] = $this->connection->quoteIdentifier($column).' IN :'.$paramName;
594
                } else {
595
                    $sqlParts[] = $this->connection->quoteIdentifier($column).' = :'.$paramName;
596
                }
597
                $parameters[$paramName] = $value;
598
                ++$counter;
599
            }
600
601
            return [implode(' AND ', $sqlParts), $parameters];
602
        } elseif ($filter_bag instanceof AbstractTDBMObject) {
603
            $dbRows = $filter_bag->_getDbRows();
604
            $dbRow = reset($dbRows);
605
            $primaryKeys = $dbRow->_getPrimaryKeys();
606
607
            foreach ($primaryKeys as $column => $value) {
608
                $paramName = 'tdbmparam'.$counter;
609
                $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...
610
                $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...
611
                ++$counter;
612
            }
613
614
            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...
615
        } elseif ($filter_bag instanceof \Iterator) {
616
            return $this->buildFilterFromFilterBag(iterator_to_array($filter_bag));
617
        } else {
618
            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.');
619
        }
620
621
//		// 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...
622
//		if ($filter_bag === null) {
623
//			$filter_bag = array();
624
//		} elseif (!is_array($filter_bag)) {
625
//			$filter_bag = array($filter_bag);
626
//		}
627
//		elseif (is_a($filter_bag, 'Mouf\\Database\\TDBM\\TDBMObjectArray')) {
628
//			$filter_bag = array($filter_bag);
629
//		}
630
//
631
//		// Second, let's take all the objects out of the filter bag, and let's make filters from them
632
//		$filter_bag2 = array();
633
//		foreach ($filter_bag as $thing) {
634
//			if (is_a($thing,'Mouf\\Database\\TDBM\\Filters\\FilterInterface')) {
635
//				$filter_bag2[] = $thing;
636
//			} elseif (is_string($thing)) {
637
//				$filter_bag2[] = new SqlStringFilter($thing);
638
//			} elseif (is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && count($thing)>0) {
639
//				// Get table_name and column_name
640
//				$filter_table_name = $thing[0]->_getDbTableName();
641
//				$filter_column_names = $thing[0]->getPrimaryKey();
642
//
643
//				// If there is only one primary key, we can use the InFilter
644
//				if (count($filter_column_names)==1) {
645
//					$primary_keys_array = array();
646
//					$filter_column_name = $filter_column_names[0];
647
//					foreach ($thing as $TDBMObject) {
648
//						$primary_keys_array[] = $TDBMObject->$filter_column_name;
649
//					}
650
//					$filter_bag2[] = new InFilter($filter_table_name, $filter_column_name, $primary_keys_array);
651
//				}
652
//				// else, we must use a (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx) OR (xxx AND xxx AND xxx)...
653
//				else
654
//				{
655
//					$filter_bag_and = array();
656
//					foreach ($thing as $TDBMObject) {
657
//						$filter_bag_temp_and=array();
658
//						foreach ($filter_column_names as $pk) {
659
//							$filter_bag_temp_and[] = new EqualFilter($TDBMObject->_getDbTableName(), $pk, $TDBMObject->$pk);
660
//						}
661
//						$filter_bag_and[] = new AndFilter($filter_bag_temp_and);
662
//					}
663
//					$filter_bag2[] = new OrFilter($filter_bag_and);
664
//				}
665
//
666
//
667
//			} elseif (!is_a($thing,'Mouf\\Database\\TDBM\\TDBMObjectArray') && $thing!==null) {
668
//				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.");
669
//			}
670
//		}
671
//
672
//		// Third, let's take all the filters and let's apply a huge AND filter
673
//		$filter = new AndFilter($filter_bag2);
674
//
675
//		return $filter;
676
    }
677
678
    /**
679
     * Takes in input an order_bag (which can be about anything from a string to an array of OrderByColumn objects... see above from documentation),
680
     * and gives back an array of OrderByColumn / OrderBySQLString objects.
681
     *
682
     * @param unknown_type $orderby_bag
683
     *
684
     * @return array
685
     */
686
    public function buildOrderArrayFromOrderBag($orderby_bag)
687
    {
688
        // Fourth, let's apply the same steps to the orderby_bag
689
        // 4-1 orderby_bag should be an array, if it is a singleton, let's put it in an array.
690
691
        if (!is_array($orderby_bag)) {
692
            $orderby_bag = array($orderby_bag);
693
        }
694
695
        // 4-2, let's take all the objects out of the orderby bag, and let's make objects from them
696
        $orderby_bag2 = array();
697
        foreach ($orderby_bag as $thing) {
698
            if (is_a($thing, 'Mouf\\Database\\TDBM\\Filters\\OrderBySQLString')) {
699
                $orderby_bag2[] = $thing;
700
            } elseif (is_a($thing, 'Mouf\\Database\\TDBM\\Filters\\OrderByColumn')) {
701
                $orderby_bag2[] = $thing;
702
            } elseif (is_string($thing)) {
703
                $orderby_bag2[] = new OrderBySQLString($thing);
704
            } elseif ($thing !== null) {
705
                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.');
706
            }
707
        }
708
709
        return $orderby_bag2;
710
    }
711
712
    /**
713
     * @param string $table
714
     *
715
     * @return string[]
716
     */
717
    public function getPrimaryKeyColumns($table)
718
    {
719
        if (!isset($this->primaryKeysColumns[$table])) {
720
            $this->primaryKeysColumns[$table] = $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getPrimaryKeyColumns();
721
722
            // TODO TDBM4: See if we need to improve error reporting if table name does not exist.
723
724
            /*$arr = array();
725
            foreach ($this->connection->getPrimaryKey($table) as $col) {
726
                $arr[] = $col->name;
727
            }
728
            // The primaryKeysColumns contains only the column's name, not the DB_Column object.
729
            $this->primaryKeysColumns[$table] = $arr;
730
            if (empty($this->primaryKeysColumns[$table]))
731
            {
732
                // Unable to find primary key.... this is an error
733
                // Let's try to be precise in error reporting. Let's try to find the table.
734
                $tables = $this->connection->checkTableExist($table);
735
                if ($tables === true)
736
                throw new TDBMException("Could not find table primary key for table '$table'. Please define a primary key for this table.");
737
                elseif ($tables !== null) {
738
                    if (count($tables)==1)
739
                    $str = "Could not find table '$table'. Maybe you meant this table: '".$tables[0]."'";
740
                    else
741
                    $str = "Could not find table '$table'. Maybe you meant one of those tables: '".implode("', '",$tables)."'";
742
                    throw new TDBMException($str);
743
                }
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
    {
758
        $primaryKey = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
759
        $hash = $this->getObjectHash($primaryKey);
760
        $this->objectStorage->set($dbRow->_getDbTableName(), $hash, $dbRow);
761
    }
762
763
    /**
764
     * This is an internal function, you should not use it in your application.
765
     * This is used internally by TDBM to remove the object from the list of objects that have been
766
     * created/updated but not saved yet.
767
     *
768
     * @param DbRow $myObject
769
     */
770
    private function removeFromToSaveObjectList(DbRow $myObject)
771
    {
772
        unset($this->toSaveObjects[$myObject]);
773
    }
774
775
    /**
776
     * This is an internal function, you should not use it in your application.
777
     * This is used internally by TDBM to add an object to the list of objects that have been
778
     * created/updated but not saved yet.
779
     *
780
     * @param AbstractTDBMObject $myObject
781
     */
782
    public function _addToToSaveObjectList(DbRow $myObject)
783
    {
784
        $this->toSaveObjects[$myObject] = true;
785
    }
786
787
    /**
788
     * Generates all the daos and beans.
789
     *
790
     * @param string $daoFactoryClassName The classe name of the DAO factory
791
     * @param string $daonamespace        The namespace for the DAOs, without trailing \
792
     * @param string $beannamespace       The Namespace for the beans, without trailing \
793
     * @param bool   $storeInUtc          If the generated daos should store the date in UTC timezone instead of user's timezone.
794
     *
795
     * @return \string[] the list of tables
796
     */
797
    public function generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc)
798
    {
799
        $tdbmDaoGenerator = new TDBMDaoGenerator($this->schemaAnalyzer, $this->tdbmSchemaAnalyzer->getSchema(), $this->tdbmSchemaAnalyzer);
800
801
        return $tdbmDaoGenerator->generateAllDaosAndBeans($daoFactoryClassName, $daonamespace, $beannamespace, $storeInUtc);
802
    }
803
804
    /**
805
     * @param array<string, string> $tableToBeanMap
806
     */
807
    public function setTableToBeanMap(array $tableToBeanMap)
808
    {
809
        $this->tableToBeanMap = $tableToBeanMap;
810
    }
811
812
    /**
813
     * Saves $object by INSERTing or UPDAT(E)ing it in the database.
814
     *
815
     * @param AbstractTDBMObject $object
816
     *
817
     * @throws TDBMException
818
     */
819
    public function save(AbstractTDBMObject $object)
820
    {
821
        $status = $object->_getStatus();
822
823
        // Let's attach this object if it is in detached state.
824
        if ($status === TDBMObjectStateEnum::STATE_DETACHED) {
825
            $object->_attach($this);
826
            $status = $object->_getStatus();
827
        }
828
829
        if ($status === TDBMObjectStateEnum::STATE_NEW) {
830
            $dbRows = $object->_getDbRows();
831
832
            $unindexedPrimaryKeys = array();
833
834
            foreach ($dbRows as $dbRow) {
835
                $tableName = $dbRow->_getDbTableName();
836
837
                $schema = $this->tdbmSchemaAnalyzer->getSchema();
838
                $tableDescriptor = $schema->getTable($tableName);
839
840
                $primaryKeyColumns = $this->getPrimaryKeyColumns($tableName);
841
842
                if (empty($unindexedPrimaryKeys)) {
843
                    $primaryKeys = $this->getPrimaryKeysForObjectFromDbRow($dbRow);
844
                } else {
845
                    // First insert, the children must have the same primary key as the parent.
846
                    $primaryKeys = $this->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $unindexedPrimaryKeys);
847
                    $dbRow->_setPrimaryKeys($primaryKeys);
848
                }
849
850
                $references = $dbRow->_getReferences();
851
852
                // Let's save all references in NEW or DETACHED state (we need their primary key)
853
                foreach ($references as $fkName => $reference) {
854
                    $refStatus = $reference->_getStatus();
855
                    if ($refStatus === TDBMObjectStateEnum::STATE_NEW || $refStatus === TDBMObjectStateEnum::STATE_DETACHED) {
856
                        $this->save($reference);
857
                    }
858
                }
859
860
                $dbRowData = $dbRow->_getDbRow();
861
862
                // Let's see if the columns for primary key have been set before inserting.
863
                // We assume that if one of the value of the PK has been set, the PK is set.
864
                $isPkSet = !empty($primaryKeys);
865
866
                /*if (!$isPkSet) {
867
                    // if there is no autoincrement and no pk set, let's go in error.
868
                    $isAutoIncrement = true;
869
870
                    foreach ($primaryKeyColumns as $pkColumnName) {
871
                        $pkColumn = $tableDescriptor->getColumn($pkColumnName);
872
                        if (!$pkColumn->getAutoincrement()) {
873
                            $isAutoIncrement = false;
874
                        }
875
                    }
876
877
                    if (!$isAutoIncrement) {
878
                        $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.";
879
                        throw new TDBMException($msg);
880
                    }
881
882
                }*/
883
884
                $types = [];
885
886
                foreach ($dbRowData as $columnName => $value) {
887
                    $columnDescriptor = $tableDescriptor->getColumn($columnName);
888
                    $types[] = $columnDescriptor->getType();
889
                }
890
891
                $this->connection->insert($tableName, $dbRowData, $types);
892
893
                if (!$isPkSet && count($primaryKeyColumns) == 1) {
894
                    $id = $this->connection->lastInsertId();
895
                    $primaryKeys[$primaryKeyColumns[0]] = $id;
896
                }
897
898
                // TODO: change this to some private magic accessor in future
899
                $dbRow->_setPrimaryKeys($primaryKeys);
900
                $unindexedPrimaryKeys = array_values($primaryKeys);
901
902
                /*
903
                 * When attached, on "save", we check if the column updated is part of a primary key
904
                 * If this is part of a primary key, we call the _update_id method that updates the id in the list of known objects.
905
                 * This method should first verify that the id is not already used (and is not auto-incremented)
906
                 *
907
                 * In the object, the key is stored in an array of  (column => value), that can be directly used to update the record.
908
                 *
909
                 *
910
                 */
911
912
                /*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...
913
                    $this->db_connection->exec($sql);
914
                } catch (TDBMException $e) {
915
                    $this->db_onerror = true;
916
917
                    // Strange..... if we do not have the line below, bad inserts are not catched.
918
                    // It seems that destructors are called before the registered shutdown function (PHP >=5.0.5)
919
                    //if ($this->tdbmService->isProgramExiting())
920
                    //	trigger_error("program exiting");
921
                    trigger_error($e->getMessage(), E_USER_ERROR);
922
923
                    if (!$this->tdbmService->isProgramExiting())
924
                        throw $e;
925
                    else
926
                    {
927
                        trigger_error($e->getMessage(), E_USER_ERROR);
928
                    }
929
                }*/
930
931
                // Let's remove this object from the $new_objects static table.
932
                $this->removeFromToSaveObjectList($dbRow);
933
934
                // TODO: change this behaviour to something more sensible performance-wise
935
                // Maybe a setting to trigger this globally?
936
                //$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...
937
                //$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...
938
                //$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...
939
940
                // Let's add this object to the list of objects in cache.
941
                $this->_addToCache($dbRow);
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
        } elseif ($status === TDBMObjectStateEnum::STATE_DELETED) {
1001
            throw new TDBMInvalidOperationException('This object has been deleted. It cannot be saved.');
1002
        }
1003
1004
        // Finally, let's save all the many to many relationships to this bean.
1005
        $this->persistManyToManyRelationships($object);
1006
    }
1007
1008
    private function persistManyToManyRelationships(AbstractTDBMObject $object)
1009
    {
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
                } elseif ($status === 'delete') {
1049
                    $filters = $this->getPivotFilters($object, $remoteBean, $localFk, $remoteFk);
1050
1051
                    $types = [];
1052
1053
                    foreach ($filters as $columnName => $value) {
1054
                        $columnDescriptor = $tableDescriptor->getColumn($columnName);
1055
                        $types[] = $columnDescriptor->getType();
1056
                    }
1057
1058
                    $this->connection->delete($pivotTableName, $filters, $types);
1059
1060
                    // Finally, let's remove relationships completely from bean.
1061
                    $storage->detach($remoteBean);
1062
                    $remoteBean->_getCachedRelationships()[$pivotTableName]->detach($object);
1063
                }
1064
            }
1065
        }
1066
    }
1067
1068
    private function getPivotFilters(AbstractTDBMObject $localBean, AbstractTDBMObject $remoteBean, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk)
1069
    {
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
     *
1087
     * @return array numerically indexed array of values.
1088
     */
1089
    private function getPrimaryKeyValues(AbstractTDBMObject $bean)
1090
    {
1091
        $dbRows = $bean->_getDbRows();
1092
        $dbRow = reset($dbRows);
1093
1094
        return array_values($dbRow->_getPrimaryKeys());
1095
    }
1096
1097
    /**
1098
     * Returns a unique hash used to store the object based on its primary key.
1099
     * If the array contains only one value, then the value is returned.
1100
     * Otherwise, a hash representing the array is returned.
1101
     *
1102
     * @param array $primaryKeys An array of columns => values forming the primary key
1103
     *
1104
     * @return string
1105
     */
1106
    public function getObjectHash(array $primaryKeys)
1107
    {
1108
        if (count($primaryKeys) === 1) {
1109
            return reset($primaryKeys);
1110
        } else {
1111
            ksort($primaryKeys);
1112
1113
            return md5(json_encode($primaryKeys));
1114
        }
1115
    }
1116
1117
    /**
1118
     * Returns an array of primary keys from the object.
1119
     * The primary keys are extracted from the object columns and not from the primary keys stored in the
1120
     * $primaryKeys variable of the object.
1121
     *
1122
     * @param DbRow $dbRow
1123
     *
1124
     * @return array Returns an array of column => value
1125
     */
1126
    public function getPrimaryKeysForObjectFromDbRow(DbRow $dbRow)
1127
    {
1128
        $table = $dbRow->_getDbTableName();
1129
        $dbRowData = $dbRow->_getDbRow();
1130
1131
        return $this->_getPrimaryKeysFromObjectData($table, $dbRowData);
1132
    }
1133
1134
    /**
1135
     * Returns an array of primary keys for the given row.
1136
     * The primary keys are extracted from the object columns.
1137
     *
1138
     * @param $table
1139
     * @param array $columns
1140
     *
1141
     * @return array
1142
     */
1143
    public function _getPrimaryKeysFromObjectData($table, array $columns)
1144
    {
1145
        $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1146
        $values = array();
1147
        foreach ($primaryKeyColumns as $column) {
1148
            if (isset($columns[$column])) {
1149
                $values[$column] = $columns[$column];
1150
            }
1151
        }
1152
1153
        return $values;
1154
    }
1155
1156
    /**
1157
     * Attaches $object to this TDBMService.
1158
     * The $object must be in DETACHED state and will pass in NEW state.
1159
     *
1160
     * @param AbstractTDBMObject $object
1161
     *
1162
     * @throws TDBMInvalidOperationException
1163
     */
1164
    public function attach(AbstractTDBMObject $object)
1165
    {
1166
        $object->_attach($this);
1167
    }
1168
1169
    /**
1170
     * Returns an associative array (column => value) for the primary keys from the table name and an
1171
     * indexed array of primary key values.
1172
     *
1173
     * @param string $tableName
1174
     * @param array  $indexedPrimaryKeys
1175
     */
1176
    public function _getPrimaryKeysFromIndexedPrimaryKeys($tableName, array $indexedPrimaryKeys)
1177
    {
1178
        $primaryKeyColumns = $this->tdbmSchemaAnalyzer->getSchema()->getTable($tableName)->getPrimaryKeyColumns();
1179
1180
        if (count($primaryKeyColumns) !== count($indexedPrimaryKeys)) {
1181
            throw new TDBMException(sprintf('Wrong number of columns passed for primary key. Expected %s columns for table "%s",
1182
			got %s instead.', count($primaryKeyColumns), $tableName, count($indexedPrimaryKeys)));
1183
        }
1184
1185
        return array_combine($primaryKeyColumns, $indexedPrimaryKeys);
1186
    }
1187
1188
    /**
1189
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
1190
     * Tables must be in a single line of inheritance. The method will find missing tables.
1191
     *
1192
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1193
     * we must be able to find all other tables.
1194
     *
1195
     * @param string[] $tables
1196
     *
1197
     * @return string[]
1198
     */
1199
    public function _getLinkBetweenInheritedTables(array $tables)
1200
    {
1201
        sort($tables);
1202
1203
        return $this->fromCache($this->cachePrefix.'_linkbetweeninheritedtables_'.implode('__split__', $tables),
1204
            function () use ($tables) {
1205
                return $this->_getLinkBetweenInheritedTablesWithoutCache($tables);
1206
            });
1207
    }
1208
1209
    /**
1210
     * Return the list of tables (from child to parent) joining the tables passed in parameter.
1211
     * Tables must be in a single line of inheritance. The method will find missing tables.
1212
     *
1213
     * Algorithm: one of those tables is the ultimate child. From this child, by recursively getting the parent,
1214
     * we must be able to find all other tables.
1215
     *
1216
     * @param string[] $tables
1217
     *
1218
     * @return string[]
1219
     */
1220
    private function _getLinkBetweenInheritedTablesWithoutCache(array $tables)
1221
    {
1222
        $schemaAnalyzer = $this->schemaAnalyzer;
1223
1224
        foreach ($tables as $currentTable) {
1225
            $allParents = [$currentTable];
1226
            $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...
1227
            while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1228
                $currentTable = $currentFk->getForeignTableName();
1229
                $allParents[] = $currentTable;
1230
            };
1231
1232
            // Now, does the $allParents contain all the tables we want?
1233
            $notFoundTables = array_diff($tables, $allParents);
1234
            if (empty($notFoundTables)) {
1235
                // We have a winner!
1236
                return $allParents;
1237
            }
1238
        }
1239
1240
        throw new TDBMException(sprintf('The tables (%s) cannot be linked by an inheritance relationship.', implode(', ', $tables)));
1241
    }
1242
1243
    /**
1244
     * Returns the list of tables related to this table (via a parent or child inheritance relationship).
1245
     *
1246
     * @param string $table
1247
     *
1248
     * @return string[]
1249
     */
1250
    public function _getRelatedTablesByInheritance($table)
1251
    {
1252
        return $this->fromCache($this->cachePrefix.'_relatedtables_'.$table, function () use ($table) {
1253
            return $this->_getRelatedTablesByInheritanceWithoutCache($table);
1254
        });
1255
    }
1256
1257
    /**
1258
     * Returns the list of tables related to this table (via a parent or child inheritance relationship).
1259
     *
1260
     * @param string $table
1261
     *
1262
     * @return string[]
1263
     */
1264
    private function _getRelatedTablesByInheritanceWithoutCache($table)
1265
    {
1266
        $schemaAnalyzer = $this->schemaAnalyzer;
1267
1268
        // Let's scan the parent tables
1269
        $currentTable = $table;
1270
1271
        $parentTables = [];
1272
1273
        // Get parent relationship
1274
        while ($currentFk = $schemaAnalyzer->getParentRelationship($currentTable)) {
1275
            $currentTable = $currentFk->getForeignTableName();
1276
            $parentTables[] = $currentTable;
1277
        };
1278
1279
        // Let's recurse in children
1280
        $childrenTables = $this->exploreChildrenTablesRelationships($schemaAnalyzer, $table);
1281
1282
        return array_merge(array_reverse($parentTables), $childrenTables);
1283
    }
1284
1285
    /**
1286
     * Explore all the children and descendant of $table and returns ForeignKeyConstraints on those.
1287
     *
1288
     * @param string $table
1289
     *
1290
     * @return string[]
1291
     */
1292
    private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyzer, $table)
1293
    {
1294
        $tables = [$table];
1295
        $keys = $schemaAnalyzer->getChildrenRelationships($table);
1296
1297
        foreach ($keys as $key) {
1298
            $tables = array_merge($tables, $this->exploreChildrenTablesRelationships($schemaAnalyzer, $key->getLocalTableName()));
1299
        }
1300
1301
        return $tables;
1302
    }
1303
1304
    /**
1305
     * Casts a foreign key into SQL, assuming table name is used with no alias.
1306
     * The returned value does contain only one table. For instance:.
1307
     *
1308
     * " LEFT JOIN table2 ON table1.id = table2.table1_id"
1309
     *
1310
     * @param ForeignKeyConstraint $fk
1311
     * @param bool                 $leftTableIsLocal
1312
     *
1313
     * @return string
1314
     */
1315
    /*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...
1316
        $onClauses = [];
1317
        $foreignTableName = $this->connection->quoteIdentifier($fk->getForeignTableName());
1318
        $foreignColumns = $fk->getForeignColumns();
1319
        $localTableName = $this->connection->quoteIdentifier($fk->getLocalTableName());
1320
        $localColumns = $fk->getLocalColumns();
1321
        $columnCount = count($localTableName);
1322
1323
        for ($i = 0; $i < $columnCount; $i++) {
1324
            $onClauses[] = sprintf("%s.%s = %s.%s",
1325
                $localTableName,
1326
                $this->connection->quoteIdentifier($localColumns[$i]),
1327
                $foreignColumns,
1328
                $this->connection->quoteIdentifier($foreignColumns[$i])
1329
                );
1330
        }
1331
1332
        $onClause = implode(' AND ', $onClauses);
1333
1334
        if ($leftTableIsLocal) {
1335
            return sprintf(" LEFT JOIN %s ON (%s)", $foreignTableName, $onClause);
1336
        } else {
1337
            return sprintf(" LEFT JOIN %s ON (%s)", $localTableName, $onClause);
1338
        }
1339
    }*/
1340
1341
    /**
1342
     * Returns an identifier for the group of tables passed in parameter.
1343
     *
1344
     * @param string[] $relatedTables
1345
     *
1346
     * @return string
1347
     */
1348
    private function getTableGroupName(array $relatedTables)
1349
    {
1350
        sort($relatedTables);
1351
1352
        return implode('_``_', $relatedTables);
1353
    }
1354
1355
    /**
1356
     * @param string            $mainTable             The name of the table queried
1357
     * @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)
1358
     * @param array             $parameters
1359
     * @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)
1360
     * @param array             $additionalTablesFetch
1361
     * @param string            $mode
1362
     * @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.
1363
     *
1364
     * @return ResultIterator An object representing an array of results.
1365
     *
1366
     * @throws TDBMException
1367
     */
1368
    public function findObjects($mainTable, $filter = null, array $parameters = array(), $orderString = null, array $additionalTablesFetch = array(), $mode = null, $className = null)
1369
    {
1370
        // $mainTable is not secured in MagicJoin, let's add a bit of security to avoid SQL injection.
1371
        if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $mainTable)) {
1372
            throw new TDBMException(sprintf("Invalid table name: '%s'", $mainTable));
1373
        }
1374
1375
        list($filterString, $additionalParameters) = $this->buildFilterFromFilterBag($filter);
1376
1377
        $parameters = array_merge($parameters, $additionalParameters);
1378
1379
        // From the table name and the additional tables we want to fetch, let's build a list of all tables
1380
        // that must be part of the select columns.
1381
1382
        $tableGroups = [];
1383
        $allFetchedTables = $this->_getRelatedTablesByInheritance($mainTable);
1384
        $tableGroupName = $this->getTableGroupName($allFetchedTables);
1385
        foreach ($allFetchedTables as $table) {
1386
            $tableGroups[$table] = $tableGroupName;
1387
        }
1388
1389
        foreach ($additionalTablesFetch as $additionalTable) {
1390
            $relatedTables = $this->_getRelatedTablesByInheritance($additionalTable);
1391
            $tableGroupName = $this->getTableGroupName($relatedTables);
1392
            foreach ($relatedTables as $table) {
1393
                $tableGroups[$table] = $tableGroupName;
1394
            }
1395
            $allFetchedTables = array_merge($allFetchedTables, $relatedTables);
1396
        }
1397
1398
        // Let's remove any duplicate
1399
        $allFetchedTables = array_flip(array_flip($allFetchedTables));
1400
1401
        $columnsList = [];
1402
        $columnDescList = [];
1403
        $schema = $this->tdbmSchemaAnalyzer->getSchema();
1404
1405
        // Now, let's build the column list
1406
        foreach ($allFetchedTables as $table) {
1407
            foreach ($schema->getTable($table)->getColumns() as $column) {
1408
                $columnName = $column->getName();
1409
                $columnDescList[] = [
1410
                    'as' => $table.'____'.$columnName,
1411
                    'table' => $table,
1412
                    'column' => $columnName,
1413
                    'type' => $column->getType(),
1414
                    'tableGroup' => $tableGroups[$table],
1415
                ];
1416
                $columnsList[] = $this->connection->quoteIdentifier($table).'.'.$this->connection->quoteIdentifier($columnName).' as '.
1417
                    $this->connection->quoteIdentifier($table.'____'.$columnName);
1418
            }
1419
        }
1420
1421
        $sql = 'SELECT DISTINCT '.implode(', ', $columnsList).' FROM MAGICJOIN('.$mainTable.')';
1422
        $countSql = 'SELECT COUNT(1) FROM MAGICJOIN('.$mainTable.')';
1423
1424
        if (!empty($filterString)) {
1425
            $sql .= ' WHERE '.$filterString;
1426
            $countSql .= ' WHERE '.$filterString;
1427
        }
1428
1429
        if (!empty($orderString)) {
1430
            $sql .= ' ORDER BY '.$orderString;
1431
            $countSql .= ' ORDER BY '.$orderString;
1432
        }
1433
1434 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...
1435
            throw new TDBMException("Unknown fetch mode: '".$this->mode."'");
1436
        }
1437
1438
        $mode = $mode ?: $this->mode;
1439
1440
        return new ResultIterator($sql, $countSql, $parameters, $columnDescList, $this->objectStorage, $className, $this, $this->magicQuery, $mode);
1441
    }
1442
1443
    /**
1444
     * @param $table
1445
     * @param array  $primaryKeys
1446
     * @param array  $additionalTablesFetch
1447
     * @param bool   $lazy                  Whether to perform lazy loading on this object or not.
1448
     * @param string $className
1449
     *
1450
     * @return AbstractTDBMObject
1451
     *
1452
     * @throws TDBMException
1453
     */
1454
    public function findObjectByPk($table, array $primaryKeys, array $additionalTablesFetch = array(), $lazy = false, $className = null)
1455
    {
1456
        $primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys);
1457
        $hash = $this->getObjectHash($primaryKeys);
1458
1459
        if ($this->objectStorage->has($table, $hash)) {
1460
            $dbRow = $this->objectStorage->get($table, $hash);
1461
            $bean = $dbRow->getTDBMObject();
1462
            if ($className !== null && !is_a($bean, $className)) {
1463
                throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'");
1464
            }
1465
1466
            return $bean;
1467
        }
1468
1469
        // Are we performing lazy fetching?
1470
        if ($lazy === true) {
1471
            // Can we perform lazy fetching?
1472
            $tables = $this->_getRelatedTablesByInheritance($table);
1473
            // Only allowed if no inheritance.
1474
            if (count($tables) === 1) {
1475
                if ($className === null) {
1476
                    $className = isset($this->tableToBeanMap[$table]) ? $this->tableToBeanMap[$table] : 'Mouf\\Database\\TDBM\\TDBMObject';
1477
                }
1478
1479
                // Let's construct the bean
1480
                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...
1481
                    $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...
1482
                }
1483
                // Let's bypass the constructor when creating the bean!
1484
                $bean = $reflectionClassCache[$className]->newInstanceWithoutConstructor();
1485
                /* @var $bean AbstractTDBMObject */
1486
                $bean->_constructLazy($table, $primaryKeys, $this);
1487
            }
1488
        }
1489
1490
        // Did not find the object in cache? Let's query it!
1491
        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...
1492
    }
1493
1494
    /**
1495
     * Returns a unique bean (or null) according to the filters passed in parameter.
1496
     *
1497
     * @param string      $mainTable             The name of the table queried
1498
     * @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)
1499
     * @param array       $parameters
1500
     * @param array       $additionalTablesFetch
1501
     * @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.
1502
     *
1503
     * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters.
1504
     *
1505
     * @throws TDBMException
1506
     */
1507
    public function findObject($mainTable, $filterString = null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null)
1508
    {
1509
        $objects = $this->findObjects($mainTable, $filterString, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className);
1510
        $page = $objects->take(0, 2);
1511
        $count = $page->count();
1512
        if ($count > 1) {
1513
            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.");
1514
        } elseif ($count === 0) {
1515
            return;
1516
        }
1517
1518
        return $objects[0];
1519
    }
1520
1521
    /**
1522
     * Returns a unique bean according to the filters passed in parameter.
1523
     * Throws a NoBeanFoundException if no bean was found for the filter passed in parameter.
1524
     *
1525
     * @param string      $mainTable             The name of the table queried
1526
     * @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)
1527
     * @param array       $parameters
1528
     * @param array       $additionalTablesFetch
1529
     * @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.
1530
     *
1531
     * @return AbstractTDBMObject The object we want
1532
     *
1533
     * @throws TDBMException
1534
     */
1535
    public function findObjectOrFail($mainTable, $filterString = null, array $parameters = array(), array $additionalTablesFetch = array(), $className = null)
1536
    {
1537
        $bean = $this->findObject($mainTable, $filterString, $parameters, $additionalTablesFetch, $className);
1538
        if ($bean === null) {
1539
            throw new NoBeanFoundException("No result found for query on table '".$mainTable."'");
1540
        }
1541
1542
        return $bean;
1543
    }
1544
1545
    /**
1546
     * @param array $beanData An array of data: array<table, array<column, value>>
1547
     *
1548
     * @return array an array with first item = class name and second item = table name
1549
     */
1550
    public function _getClassNameFromBeanData(array $beanData)
1551
    {
1552
        if (count($beanData) === 1) {
1553
            $tableName = array_keys($beanData)[0];
1554
        } else {
1555
            foreach ($beanData as $table => $row) {
1556
                $tables = [];
1557
                $primaryKeyColumns = $this->getPrimaryKeyColumns($table);
1558
                $pkSet = false;
1559
                foreach ($primaryKeyColumns as $columnName) {
1560
                    if ($row[$columnName] !== null) {
1561
                        $pkSet = true;
1562
                        break;
1563
                    }
1564
                }
1565
                if ($pkSet) {
1566
                    $tables[] = $table;
1567
                }
1568
            }
1569
1570
            // $tables contains the tables for this bean. Let's view the top most part of the hierarchy
1571
            $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...
1572
            $tableName = $allTables[0];
1573
        }
1574
1575
        // Only one table in this bean. Life is sweat, let's look at its type:
1576
        if (isset($this->tableToBeanMap[$tableName])) {
1577
            return [$this->tableToBeanMap[$tableName], $tableName];
1578
        } else {
1579
            return ['Mouf\\Database\\TDBM\\TDBMObject', $tableName];
1580
        }
1581
    }
1582
1583
    /**
1584
     * Returns an item from cache or computes it using $closure and puts it in cache.
1585
     *
1586
     * @param string   $key
1587
     * @param callable $closure
1588
     *
1589
     * @return mixed
1590
     */
1591
    private function fromCache($key, callable $closure)
1592
    {
1593
        $item = $this->cache->fetch($key);
1594
        if ($item === false) {
1595
            $item = $closure();
1596
            $this->cache->save($key, $item);
1597
        }
1598
1599
        return $item;
1600
    }
1601
1602
    /**
1603
     * Returns the foreign key object.
1604
     *
1605
     * @param string $table
1606
     * @param string $fkName
1607
     *
1608
     * @return ForeignKeyConstraint
1609
     */
1610
    public function _getForeignKeyByName($table, $fkName)
1611
    {
1612
        return $this->tdbmSchemaAnalyzer->getSchema()->getTable($table)->getForeignKey($fkName);
1613
    }
1614
1615
    /**
1616
     * @param $pivotTableName
1617
     * @param AbstractTDBMObject $bean
1618
     *
1619
     * @return AbstractTDBMObject[]
1620
     */
1621
    public function _getRelatedBeans($pivotTableName, AbstractTDBMObject $bean)
1622
    {
1623
        list($localFk, $remoteFk) = $this->getPivotTableForeignKeys($pivotTableName, $bean);
1624
        /* @var $localFk ForeignKeyConstraint */
1625
        /* @var $remoteFk ForeignKeyConstraint */
1626
        $remoteTable = $remoteFk->getForeignTableName();
1627
1628
        $primaryKeys = $this->getPrimaryKeyValues($bean);
1629
        $columnNames = array_map(function ($name) use ($pivotTableName) { return $pivotTableName.'.'.$name; }, $localFk->getLocalColumns());
1630
1631
        $filter = array_combine($columnNames, $primaryKeys);
1632
1633
        return $this->findObjects($remoteTable, $filter);
1634
    }
1635
1636
    /**
1637
     * @param $pivotTableName
1638
     * @param AbstractTDBMObject $bean The LOCAL bean
1639
     *
1640
     * @return ForeignKeyConstraint[] First item: the LOCAL bean, second item: the REMOTE bean.
1641
     *
1642
     * @throws TDBMException
1643
     */
1644
    private function getPivotTableForeignKeys($pivotTableName, AbstractTDBMObject $bean)
1645
    {
1646
        $fks = array_values($this->tdbmSchemaAnalyzer->getSchema()->getTable($pivotTableName)->getForeignKeys());
1647
        $table1 = $fks[0]->getForeignTableName();
1648
        $table2 = $fks[1]->getForeignTableName();
1649
1650
        $beanTables = array_map(function (DbRow $dbRow) { return $dbRow->_getDbTableName(); }, $bean->_getDbRows());
1651
1652
        if (in_array($table1, $beanTables)) {
1653
            return [$fks[0], $fks[1]];
1654
        } elseif (in_array($table2, $beanTables)) {
1655
            return [$fks[1], $fks[0]];
1656
        } else {
1657
            throw new TDBMException("Unexpected bean type in getPivotTableForeignKeys. Awaiting beans from table {$table1} and {$table2} for pivot table {$pivotTableName}");
1658
        }
1659
    }
1660
1661
    /**
1662
     * Returns a list of pivot tables linked to $bean.
1663
     *
1664
     * @param AbstractTDBMObject $bean
1665
     *
1666
     * @return string[]
1667
     */
1668
    public function _getPivotTablesLinkedToBean(AbstractTDBMObject $bean)
1669
    {
1670
        $junctionTables = [];
1671
        $allJunctionTables = $this->schemaAnalyzer->detectJunctionTables();
1672
        foreach ($bean->_getDbRows() as $dbRow) {
1673
            foreach ($allJunctionTables as $table) {
1674
                // There are exactly 2 FKs since this is a pivot table.
1675
                $fks = array_values($table->getForeignKeys());
1676
1677
                if ($fks[0]->getForeignTableName() === $dbRow->_getDbTableName() || $fks[1]->getForeignTableName() === $dbRow->_getDbTableName()) {
1678
                    $junctionTables[] = $table->getName();
1679
                }
1680
            }
1681
        }
1682
1683
        return $junctionTables;
1684
    }
1685
}
1686