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

AbstractTDBMObject::_getCachedRelationships()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
namespace Mouf\Database\TDBM;
3
4
/*
5
 Copyright (C) 2006-2016 David Négrier - THE CODING MACHINE
6
7
 This program is free software; you can redistribute it and/or modify
8
 it under the terms of the GNU General Public License as published by
9
 the Free Software Foundation; either version 2 of the License, or
10
 (at your option) any later version.
11
12
 This program is distributed in the hope that it will be useful,
13
 but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 GNU General Public License for more details.
16
17
 You should have received a copy of the GNU General Public License
18
 along with this program; if not, write to the Free Software
19
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20
 */
21
use Doctrine\DBAL\Driver\Connection;
22
23
24
/**
25
 * Instances of this class represent a "bean". Usually, a bean is mapped to a row of one table.
26
 * In some special cases (where inheritance is used), beans can be scattered on several tables.
27
 * Therefore, a TDBMObject is really a set of DbRow objects that represent one row in a table.
28
 *
29
 * @author David Negrier
30
 */
31
abstract class AbstractTDBMObject {
32
33
	/**
34
	 * The service this object is bound to.
35
	 * 
36
	 * @var TDBMService
37
	 */
38
	protected $tdbmService;
39
40
	/**
41
	 * An array of DbRow, indexed by table name.
42
	 * @var DbRow[]
43
	 */
44
	protected $dbRows = array();
45
46
	/**
47
	 * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
48
	 * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
49
	 * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
50
	 * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
51
	 *
52
	 * @var string
53
	 */
54
	private $status;
55
56
	/**
57
	 * Array storing beans related via many to many relationships (pivot tables)
58
	 * @var \SplObjectStorage[] Key: pivot table name, value: SplObjectStorage
59
	 */
60
	private $relationships = [];
61
62
	/**
63
	 *
64
	 * @var bool[] Key: pivot table name, value: whether a query was performed to load the data.
65
	 */
66
	private $loadedRelationships = [];
67
68
	/**
69
	 * Used with $primaryKeys when we want to retrieve an existing object
70
	 * and $primaryKeys=[] if we want a new object
71
	 *
72
	 * @param string $tableName
73
	 * @param array $primaryKeys
74
	 * @param TDBMService $tdbmService
75
	 * @throws TDBMException
76
	 * @throws TDBMInvalidOperationException
77
	 */
78
	public function __construct($tableName=null, array $primaryKeys=array(), TDBMService $tdbmService=null) {
79
		// FIXME: lazy loading should be forbidden on tables with inheritance and dynamic type assignation...
80
		if (!empty($tableName)) {
81
			$this->dbRows[$tableName] = new DbRow($this, $tableName, $primaryKeys, $tdbmService);
82
		}
83
84
		if ($tdbmService === null) {
85
			$this->_setStatus(TDBMObjectStateEnum::STATE_DETACHED);
86
		} else {
87
			$this->_attach($tdbmService);
88
			if (!empty($primaryKeys)) {
89
				$this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED);
90
			} else {
91
				$this->_setStatus(TDBMObjectStateEnum::STATE_NEW);
92
			}
93
		}
94
	}
95
96
	/**
97
	 * Alternative constructor called when data is fetched from database via a SELECT.
98
	 *
99
	 * @param array $beanData array<table, array<column, value>>
100
	 * @param TDBMService $tdbmService
101
	 */
102
	public function _constructFromData(array $beanData, TDBMService $tdbmService) {
103
		$this->tdbmService = $tdbmService;
104
105
		foreach ($beanData as $table => $columns) {
106
			$this->dbRows[$table] = new DbRow($this, $table, $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns);
107
		}
108
109
		$this->status = TDBMObjectStateEnum::STATE_LOADED;
110
	}
111
112
	/**
113
	 * Alternative constructor called when bean is lazily loaded.
114
	 *
115
	 * @param string $tableName
116
	 * @param array $primaryKeys
117
	 * @param TDBMService $tdbmService
118
	 */
119
	public function _constructLazy($tableName, array $primaryKeys, TDBMService $tdbmService) {
120
		$this->tdbmService = $tdbmService;
121
122
		$this->dbRows[$tableName] = new DbRow($this, $tableName, $primaryKeys, $tdbmService);
123
124
		$this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
125
	}
126
127
	public function _attach(TDBMService $tdbmService) {
128
		if ($this->status !== TDBMObjectStateEnum::STATE_DETACHED) {
129
			throw new TDBMInvalidOperationException('Cannot attach an object that is already attached to TDBM.');
130
		}
131
		$this->tdbmService = $tdbmService;
132
133
		// If we attach this object, we must work to make sure the tables are in ascending order (from low level to top level)
134
		$tableNames = array_keys($this->dbRows);
135
		$tableNames = $this->tdbmService->_getLinkBetweenInheritedTables($tableNames);
136
		$tableNames = array_reverse($tableNames);
137
138
		$newDbRows = [];
139
140
		foreach ($tableNames as $table) {
141
			if (!isset($this->dbRows[$table])) {
142
				$this->registerTable($table);
143
			}
144
			$newDbRows[$table] = $this->dbRows[$table];
145
		}
146
		$this->dbRows = $newDbRows;
147
148
		$this->status = TDBMObjectStateEnum::STATE_NEW;
149
		foreach ($this->dbRows as $dbRow) {
150
			$dbRow->_attach($tdbmService);
151
		}
152
	}
153
154
	/**
155
	 * Sets the state of the TDBM Object
156
	 * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
157
	 * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
158
	 * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
159
	 * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
160
	 * @param string $state
161
	 */
162
	public function _setStatus($state){
163
		$this->status = $state;
164
165
		// TODO: we might ignore the loaded => dirty state here! dirty status comes from the db_row itself.
166
		foreach ($this->dbRows as $dbRow) {
167
			$dbRow->_setStatus($state);
168
		}
169
	}
170
171 View Code Duplication
	public function get($var, $tableName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
172
		if ($tableName === null) {
173
			if (count($this->dbRows) > 1) {
174
				throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
175
			} elseif (count($this->dbRows) === 1) {
176
				$tableName = array_keys($this->dbRows)[0];
177
			}
178
		}
179
180
		if (!isset($this->dbRows[$tableName])) {
181
			if (count($this->dbRows[$tableName] === 0)) {
182
				throw new TDBMException('Object is not yet bound to any table.');
183
			} else {
184
				throw new TDBMException('Unknown table "'.$tableName.'"" in object.');
185
			}
186
		}
187
188
		return $this->dbRows[$tableName]->get($var);
189
	}
190
191
	/**
192
	 * Returns true if a column is set, false otherwise.
193
	 * 
194
	 * @param string $var
195
	 * @return boolean
196
	 */
197 View Code Duplication
	public function has($var, $tableName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
198
		if ($tableName === null) {
199
			if (count($this->dbRows) > 1) {
200
				throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
201
			} elseif (count($this->dbRows) === 1) {
202
				$tableName = array_keys($this->dbRows)[0];
203
			}
204
		}
205
206
		if (!isset($this->dbRows[$tableName])) {
207
			if (count($this->dbRows[$tableName] === 0)) {
208
				throw new TDBMException('Object is not yet bound to any table.');
209
			} else {
210
				throw new TDBMException('Unknown table "'.$tableName.'"" in object.');
211
			}
212
		}
213
214
		return $this->dbRows[$tableName]->has($var);
0 ignored issues
show
Bug introduced by
The method has() does not seem to exist on object<Mouf\Database\TDBM\DbRow>.

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

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

Loading history...
215
	}
216
	
217 View Code Duplication
	public function set($var, $value, $tableName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
218
		if ($tableName === null) {
219
			if (count($this->dbRows) > 1) {
220
				throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
221
			} elseif (count($this->dbRows) === 1) {
222
				$tableName = array_keys($this->dbRows)[0];
223
			} else {
224
				throw new TDBMException("Please specify a table for this object.");
225
			}
226
		}
227
228
		if (!isset($this->dbRows[$tableName])) {
229
			$this->registerTable($tableName);
230
		}
231
232
		$this->dbRows[$tableName]->set($var, $value);
233
		if ($this->dbRows[$tableName]->_getStatus() === TDBMObjectStateEnum::STATE_DIRTY) {
234
			$this->status = TDBMObjectStateEnum::STATE_DIRTY;
235
		}
236
	}
237
238
	/**
239
	 * @param string $foreignKeyName
240
	 * @param AbstractTDBMObject $bean
241
	 */
242 View Code Duplication
	public function setRef($foreignKeyName, AbstractTDBMObject $bean, $tableName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
243
		if ($tableName === null) {
244
			if (count($this->dbRows) > 1) {
245
				throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
246
			} elseif (count($this->dbRows) === 1) {
247
				$tableName = array_keys($this->dbRows)[0];
248
			} else {
249
				throw new TDBMException("Please specify a table for this object.");
250
			}
251
		}
252
253
		if (!isset($this->dbRows[$tableName])) {
254
			$this->registerTable($tableName);
255
		}
256
257
		$this->dbRows[$tableName]->setRef($foreignKeyName, $bean);
258
		if ($this->dbRows[$tableName]->_getStatus() === TDBMObjectStateEnum::STATE_DIRTY) {
259
			$this->status = TDBMObjectStateEnum::STATE_DIRTY;
260
		}
261
	}
262
263
	/**
264
	 * @param string $foreignKeyName A unique name for this reference
265
	 * @return AbstractTDBMObject|null
266
	 */
267 View Code Duplication
	public function getRef($foreignKeyName, $tableName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
268
		if ($tableName === null) {
269
			if (count($this->dbRows) > 1) {
270
				throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
271
			} elseif (count($this->dbRows) === 1) {
272
				$tableName = array_keys($this->dbRows)[0];
273
			}
274
		}
275
276
		if (!isset($this->dbRows[$tableName])) {
277
			if (count($this->dbRows[$tableName] === 0)) {
278
				throw new TDBMException('Object is not yet bound to any table.');
279
			} else {
280
				throw new TDBMException('Unknown table "'.$tableName.'"" in object.');
281
			}
282
		}
283
284
		return $this->dbRows[$tableName]->getRef($foreignKeyName);
285
	}
286
287
	/**
288
	 * Adds a many to many relationship to this bean.
289
	 * @param string $pivotTableName
290
	 * @param AbstractTDBMObject $remoteBean
291
	 */
292
	protected function addRelationship($pivotTableName, AbstractTDBMObject $remoteBean) {
293
		$this->setRelationship($pivotTableName, $remoteBean, 'new');
294
	}
295
296
	/**
297
	 * Returns true if there is a relationship to this bean.
298
	 * @param string $pivotTableName
299
	 * @param AbstractTDBMObject $remoteBean
300
	 * @return bool
301
	 */
302
	protected function hasRelationship($pivotTableName, AbstractTDBMObject $remoteBean) {
303
		$storage = $this->retrieveRelationshipsStorage($pivotTableName);
304
305
		if ($storage->contains($remoteBean)) {
306
			if ($storage[$remoteBean]['status'] !== 'delete') {
307
				return true;
308
			}
309
		}
310
		return false;
311
	}
312
313
	/**
314
	 * Internal TDBM method. Removes a many to many relationship from this bean.
315
	 * @param string $pivotTableName
316
	 * @param AbstractTDBMObject $remoteBean
317
	 */
318
	public function _removeRelationship($pivotTableName, AbstractTDBMObject $remoteBean) {
319
		if (isset($this->relationships[$pivotTableName][$remoteBean]) && $this->relationships[$pivotTableName][$remoteBean]['status'] === 'new') {
320
			unset($this->relationships[$pivotTableName][$remoteBean]);
321
			unset($remoteBean->relationships[$pivotTableName][$this]);
322
		} else {
323
			$this->setRelationship($pivotTableName, $remoteBean, 'delete');
324
		}
325
	}
326
327
	/**
328
	 * Returns the list of objects linked to this bean via $pivotTableName
329
	 * @param $pivotTableName
330
	 * @return \SplObjectStorage
331
	 */
332
	private function retrieveRelationshipsStorage($pivotTableName) {
333
		$storage = $this->getRelationshipStorage($pivotTableName);
334
		if ($this->status === TDBMObjectStateEnum::STATE_DETACHED || $this->status === TDBMObjectStateEnum::STATE_NEW || isset($this->loadedRelationships[$pivotTableName]) && $this->loadedRelationships[$pivotTableName]) {
335
			return $storage;
336
		}
337
338
		$beans = $this->tdbmService->_getRelatedBeans($pivotTableName, $this);
339
		$this->loadedRelationships[$pivotTableName] = true;
340
341
		foreach ($beans as $bean) {
342
			if (isset($storage[$bean])) {
343
				$oldStatus = $storage[$bean]['status'];
344
				if ($oldStatus === 'delete') {
345
					// Keep deleted things deleted
346
					continue;
347
				}
348
			}
349
			$this->setRelationship($pivotTableName, $bean, "loaded");
350
		}
351
352
		return $storage;
353
354
	}
355
356
	/**
357
	 * Internal TDBM method. Returns the list of objects linked to this bean via $pivotTableName
358
	 * @access private
359
	 * @param $pivotTableName
360
	 * @return AbstractTDBMObject[]
361
	 */
362
	public function _getRelationships($pivotTableName) {
363
		return $this->relationshipStorageToArray($this->retrieveRelationshipsStorage($pivotTableName));
364
	}
365
366
	private function relationshipStorageToArray(\SplObjectStorage $storage) {
367
		$beans = [];
368
		foreach ($storage as $bean) {
369
			$statusArr = $storage[$bean];
370
			if ($statusArr['status'] !== 'delete') {
371
				$beans[] = $bean;
372
			}
373
		}
374
		return $beans;
375
	}
376
377
	/**
378
	 * Declares a relationship between
379
	 * @param string $pivotTableName
380
	 * @param AbstractTDBMObject $remoteBean
381
	 * @param string $status
382
	 */
383
	private function setRelationship($pivotTableName, AbstractTDBMObject $remoteBean, $status) {
384
		$storage = $this->getRelationshipStorage($pivotTableName);
385
		$storage->attach($remoteBean, [ 'status' => $status, 'reverse' => false ]);
386
		if ($this->status === TDBMObjectStateEnum::STATE_LOADED) {
387
			$this->_setStatus(TDBMObjectStateEnum::STATE_DIRTY);
388
		}
389
390
		$remoteStorage = $remoteBean->getRelationshipStorage($pivotTableName);
391
		$remoteStorage->attach($this, [ 'status' => $status, 'reverse' => true ]);
392
	}
393
394
	/**
395
	 * Returns the SplObjectStorage associated to this relationship (creates it if it does not exists)
396
	 * @param $pivotTableName
397
	 * @return \SplObjectStorage
398
	 */
399
	private function getRelationshipStorage($pivotTableName) {
400
		if (isset($this->relationships[$pivotTableName])) {
401
			$storage = $this->relationships[$pivotTableName];
402
		} else {
403
			$storage = new \SplObjectStorage();
404
			$this->relationships[$pivotTableName] = $storage;
405
		}
406
		return $storage;
407
	}
408
409
	/**
410
	 * Reverts any changes made to the object and resumes it to its DB state.
411
	 * This can only be called on objects that come from database and that have not been deleted.
412
	 * Otherwise, this will throw an exception.
413
	 *
414
	 * @throws TDBMException
415
	 */
416
	public function discardChanges() {
417
		if ($this->status === TDBMObjectStateEnum::STATE_NEW || $this->status === TDBMObjectStateEnum::STATE_DETACHED) {
418
			throw new TDBMException("You cannot call discardChanges() on an object that has been created with the 'new' keyword and that has not yet been saved.");
419
		}
420
421
		if ($this->status === TDBMObjectStateEnum::STATE_DELETED) {
422
			throw new TDBMException('You cannot call discardChanges() on an object that has been deleted.');
423
		}
424
			
425
		$this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED);
426
	}
427
428
	/**
429
	 * Method used internally by TDBM. You should not use it directly.
430
	 * This method returns the status of the TDBMObject.
431
	 * This is one of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
432
	 * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
433
	 * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
434
	 * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
435
	 *
436
	 * @return string
437
	 */
438
	public function _getStatus() {
439
		return $this->status;
440
	}
441
442
    /**
443
     * Override the native php clone function for TDBMObjects
444
     */
445
    public function __clone() {
446
		// Let's clone the many to many relationships
447
		if ($this->status === TDBMObjectStateEnum::STATE_DETACHED) {
448
			$pivotTableList = array_keys($this->relationships);
449
		} else {
450
			$pivotTableList = $this->tdbmService->_getPivotTablesLinkedToBean($this);
451
		}
452
453
		foreach ($pivotTableList as $pivotTable) {
454
			$storage = $this->retrieveRelationshipsStorage($pivotTable);
455
456
			// Let's duplicate the reverse side of the relationship
457
			foreach ($storage as $remoteBean) {
458
				$metadata = $storage[$remoteBean];
459
460
				$remoteStorage = $remoteBean->getRelationshipStorage($pivotTable);
461
				$remoteStorage->attach($this, [ 'status' => $metadata['status'], 'reverse' => !$metadata['reverse'] ]);
462
			}
463
		}
464
465
		// Let's clone each row
466
		foreach ($this->dbRows as $key=>$dbRow) {
467
			$this->dbRows[$key] = clone $dbRow;
468
		}
469
470
		// Let's set the status to new (to enter the save function)
471
        $this->status = TDBMObjectStateEnum::STATE_DETACHED;
472
473
474
    }
475
476
	/**
477
	 * Returns raw database rows.
478
	 *
479
	 * @return DbRow[] Key: table name, Value: DbRow object
480
	 */
481
	public function _getDbRows() {
482
		return $this->dbRows;
483
	}
484
485
	private function registerTable($tableName) {
486
		$dbRow = new DbRow($this, $tableName);
487
488
		if (in_array($this->status, [ TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DIRTY ])) {
489
			// Let's get the primary key for the new table
490
			$anotherDbRow = array_values($this->dbRows)[0];
491
			/* @var $anotherDbRow DbRow */
492
			$indexedPrimaryKeys = array_values($anotherDbRow->_getPrimaryKeys());
493
			$primaryKeys = $this->tdbmService->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $indexedPrimaryKeys);
494
			$dbRow->_setPrimaryKeys($primaryKeys);
495
		}
496
497
		$dbRow->_setStatus($this->status);
498
499
		$this->dbRows[$tableName] = $dbRow;
500
		// TODO: look at status (if not new)=> get primary key from tdbmservice
501
	}
502
503
	/**
504
	 * Internal function: return the list of relationships
505
	 * @return \SplObjectStorage[]
506
	 */
507
	public function _getCachedRelationships() {
508
		return $this->relationships;
509
	}
510
}
511