Completed
Pull Request — 4.0 (#55)
by Huberty
03:08
created

AbstractTDBMObject::setRef()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 21
Code Lines 13

Duplication

Lines 20
Ratio 95.24 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 20
loc 21
rs 8.7624
cc 6
eloc 13
nc 10
nop 3
1
<?php
2
3
namespace Mouf\Database\TDBM;
4
5
/*
6
 Copyright (C) 2006-2016 David Négrier - THE CODING MACHINE
7
8
 This program is free software; you can redistribute it and/or modify
9
 it under the terms of the GNU General Public License as published by
10
 the Free Software Foundation; either version 2 of the License, or
11
 (at your option) any later version.
12
13
 This program is distributed in the hope that it will be useful,
14
 but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 GNU General Public License for more details.
17
18
 You should have received a copy of the GNU General Public License
19
 along with this program; if not, write to the Free Software
20
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21
 */
22
23
use JsonSerializable;
24
25
/**
26
 * Instances of this class represent a "bean". Usually, a bean is mapped to a row of one table.
27
 * In some special cases (where inheritance is used), beans can be scattered on several tables.
28
 * Therefore, a TDBMObject is really a set of DbRow objects that represent one row in a table.
29
 *
30
 * @author David Negrier
31
 */
32
abstract class AbstractTDBMObject implements JsonSerializable
33
{
34
    /**
35
     * The service this object is bound to.
36
     *
37
     * @var TDBMService
38
     */
39
    protected $tdbmService;
40
41
    /**
42
     * An array of DbRow, indexed by table name.
43
     *
44
     * @var DbRow[]
45
     */
46
    protected $dbRows = array();
47
48
    /**
49
     * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
50
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
51
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
52
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
53
     *
54
     * @var string
55
     */
56
    private $status;
57
58
    /**
59
     * Array storing beans related via many to many relationships (pivot tables).
60
     *
61
     * @var \SplObjectStorage[] Key: pivot table name, value: SplObjectStorage
62
     */
63
    private $relationships = [];
64
65
    /**
66
     * @var bool[] Key: pivot table name, value: whether a query was performed to load the data.
67
     */
68
    private $loadedRelationships = [];
69
70
    /**
71
     * Used with $primaryKeys when we want to retrieve an existing object
72
     * and $primaryKeys=[] if we want a new object.
73
     *
74
     * @param string      $tableName
75
     * @param array       $primaryKeys
76
     * @param TDBMService $tdbmService
77
     *
78
     * @throws TDBMException
79
     * @throws TDBMInvalidOperationException
80
     */
81
    public function __construct($tableName = null, array $primaryKeys = array(), TDBMService $tdbmService = null)
82
    {
83
        // FIXME: lazy loading should be forbidden on tables with inheritance and dynamic type assignation...
84
        if (!empty($tableName)) {
85
            $this->dbRows[$tableName] = new DbRow($this, $tableName, $primaryKeys, $tdbmService);
86
        }
87
88
        if ($tdbmService === null) {
89
            $this->_setStatus(TDBMObjectStateEnum::STATE_DETACHED);
90
        } else {
91
            $this->_attach($tdbmService);
92
            if (!empty($primaryKeys)) {
93
                $this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED);
94
            } else {
95
                $this->_setStatus(TDBMObjectStateEnum::STATE_NEW);
96
            }
97
        }
98
    }
99
100
    /**
101
     * Alternative constructor called when data is fetched from database via a SELECT.
102
     *
103
     * @param array       $beanData    array<table, array<column, value>>
104
     * @param TDBMService $tdbmService
105
     */
106
    public function _constructFromData(array $beanData, TDBMService $tdbmService)
107
    {
108
        $this->tdbmService = $tdbmService;
109
110
        foreach ($beanData as $table => $columns) {
111
            $this->dbRows[$table] = new DbRow($this, $table, $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns);
112
        }
113
114
        $this->status = TDBMObjectStateEnum::STATE_LOADED;
115
    }
116
117
    /**
118
     * Alternative constructor called when bean is lazily loaded.
119
     *
120
     * @param string      $tableName
121
     * @param array       $primaryKeys
122
     * @param TDBMService $tdbmService
123
     */
124
    public function _constructLazy($tableName, array $primaryKeys, TDBMService $tdbmService)
125
    {
126
        $this->tdbmService = $tdbmService;
127
128
        $this->dbRows[$tableName] = new DbRow($this, $tableName, $primaryKeys, $tdbmService);
129
130
        $this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
131
    }
132
133
    public function _attach(TDBMService $tdbmService)
134
    {
135
        if ($this->status !== TDBMObjectStateEnum::STATE_DETACHED) {
136
            throw new TDBMInvalidOperationException('Cannot attach an object that is already attached to TDBM.');
137
        }
138
        $this->tdbmService = $tdbmService;
139
140
        // If we attach this object, we must work to make sure the tables are in ascending order (from low level to top level)
141
        $tableNames = array_keys($this->dbRows);
142
        $tableNames = $this->tdbmService->_getLinkBetweenInheritedTables($tableNames);
143
        $tableNames = array_reverse($tableNames);
144
145
        $newDbRows = [];
146
147
        foreach ($tableNames as $table) {
148
            if (!isset($this->dbRows[$table])) {
149
                $this->registerTable($table);
150
            }
151
            $newDbRows[$table] = $this->dbRows[$table];
152
        }
153
        $this->dbRows = $newDbRows;
154
155
        $this->status = TDBMObjectStateEnum::STATE_NEW;
156
        foreach ($this->dbRows as $dbRow) {
157
            $dbRow->_attach($tdbmService);
158
        }
159
    }
160
161
    /**
162
     * Sets the state of the TDBM Object
163
     * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
164
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
165
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
166
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
167
     *
168
     * @param string $state
169
     */
170
    public function _setStatus($state)
171
    {
172
        $this->status = $state;
173
174
        // TODO: we might ignore the loaded => dirty state here! dirty status comes from the db_row itself.
175
        foreach ($this->dbRows as $dbRow) {
176
            $dbRow->_setStatus($state);
177
        }
178
    }
179
180
    /**
181
     * Checks that $tableName is ok, or returns the only possible table name if "$tableName = null"
182
     * or throws an error.
183
     *
184
     * @param string $tableName
185
     *
186
     * @return string
187
     */
188
    private function checkTableName($tableName = null)
189
    {
190
        if ($tableName === null) {
191
            if (count($this->dbRows) > 1) {
192
                throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
193
            } elseif (count($this->dbRows) === 1) {
194
                $tableName = array_keys($this->dbRows)[0];
195
            }
196
        }
197
198
        if (!isset($this->dbRows[$tableName])) {
199
            if (count($this->dbRows[$tableName] === 0)) {
200
                throw new TDBMException('Object is not yet bound to any table.');
201
            } else {
202
                throw new TDBMException('Unknown table "'.$tableName.'"" in object.');
203
            }
204
        }
205
206
        return $tableName;
207
    }
208
209
    protected function get($var, $tableName = null)
210
    {
211
        $tableName = $this->checkTableName($tableName);
212
213
        return $this->dbRows[$tableName]->get($var);
214
    }
215
216 View Code Duplication
    protected 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...
217
    {
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
    protected 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
    {
244
        if ($tableName === null) {
245
            if (count($this->dbRows) > 1) {
246
                throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.');
247
            } elseif (count($this->dbRows) === 1) {
248
                $tableName = array_keys($this->dbRows)[0];
249
            } else {
250
                throw new TDBMException('Please specify a table for this object.');
251
            }
252
        }
253
254
        if (!isset($this->dbRows[$tableName])) {
255
            $this->registerTable($tableName);
256
        }
257
258
        $this->dbRows[$tableName]->setRef($foreignKeyName, $bean);
259
        if ($this->dbRows[$tableName]->_getStatus() === TDBMObjectStateEnum::STATE_DIRTY) {
260
            $this->status = TDBMObjectStateEnum::STATE_DIRTY;
261
        }
262
    }
263
264
    /**
265
     * @param string $foreignKeyName A unique name for this reference
266
     *
267
     * @return AbstractTDBMObject|null
268
     */
269
    protected function getRef($foreignKeyName, $tableName = null)
270
    {
271
        $tableName = $this->checkTableName($tableName);
272
273
        return $this->dbRows[$tableName]->getRef($foreignKeyName);
274
    }
275
276
    /**
277
     * Adds a many to many relationship to this bean.
278
     *
279
     * @param string             $pivotTableName
280
     * @param AbstractTDBMObject $remoteBean
281
     */
282
    protected function addRelationship($pivotTableName, AbstractTDBMObject $remoteBean)
283
    {
284
        $this->setRelationship($pivotTableName, $remoteBean, 'new');
285
    }
286
287
    /**
288
     * Returns true if there is a relationship to this bean.
289
     *
290
     * @param string             $pivotTableName
291
     * @param AbstractTDBMObject $remoteBean
292
     *
293
     * @return bool
294
     */
295
    protected function hasRelationship($pivotTableName, AbstractTDBMObject $remoteBean)
296
    {
297
        $storage = $this->retrieveRelationshipsStorage($pivotTableName);
298
299
        if ($storage->contains($remoteBean)) {
300
            if ($storage[$remoteBean]['status'] !== 'delete') {
301
                return true;
302
            }
303
        }
304
305
        return false;
306
    }
307
308
    /**
309
     * Internal TDBM method. Removes a many to many relationship from this bean.
310
     *
311
     * @param string             $pivotTableName
312
     * @param AbstractTDBMObject $remoteBean
313
     */
314
    public function _removeRelationship($pivotTableName, AbstractTDBMObject $remoteBean)
315
    {
316
        if (isset($this->relationships[$pivotTableName][$remoteBean]) && $this->relationships[$pivotTableName][$remoteBean]['status'] === 'new') {
317
            unset($this->relationships[$pivotTableName][$remoteBean]);
318
            unset($remoteBean->relationships[$pivotTableName][$this]);
319
        } else {
320
            $this->setRelationship($pivotTableName, $remoteBean, 'delete');
321
        }
322
    }
323
324
    /**
325
     * Returns the list of objects linked to this bean via $pivotTableName.
326
     *
327
     * @param $pivotTableName
328
     *
329
     * @return \SplObjectStorage
330
     */
331
    private function retrieveRelationshipsStorage($pivotTableName)
332
    {
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
     * Internal TDBM method. Returns the list of objects linked to this bean via $pivotTableName.
357
     *
358
     * @param $pivotTableName
359
     *
360
     * @return AbstractTDBMObject[]
361
     */
362
    public function _getRelationships($pivotTableName)
363
    {
364
        return $this->relationshipStorageToArray($this->retrieveRelationshipsStorage($pivotTableName));
365
    }
366
367
    private function relationshipStorageToArray(\SplObjectStorage $storage)
368
    {
369
        $beans = [];
370
        foreach ($storage as $bean) {
371
            $statusArr = $storage[$bean];
372
            if ($statusArr['status'] !== 'delete') {
373
                $beans[] = $bean;
374
            }
375
        }
376
377
        return $beans;
378
    }
379
380
    /**
381
     * Declares a relationship between.
382
     *
383
     * @param string             $pivotTableName
384
     * @param AbstractTDBMObject $remoteBean
385
     * @param string             $status
386
     */
387
    private function setRelationship($pivotTableName, AbstractTDBMObject $remoteBean, $status)
388
    {
389
        $storage = $this->getRelationshipStorage($pivotTableName);
390
        $storage->attach($remoteBean, ['status' => $status, 'reverse' => false]);
391
        if ($this->status === TDBMObjectStateEnum::STATE_LOADED) {
392
            $this->_setStatus(TDBMObjectStateEnum::STATE_DIRTY);
393
        }
394
395
        $remoteStorage = $remoteBean->getRelationshipStorage($pivotTableName);
396
        $remoteStorage->attach($this, ['status' => $status, 'reverse' => true]);
397
    }
398
399
    /**
400
     * Returns the SplObjectStorage associated to this relationship (creates it if it does not exists).
401
     *
402
     * @param $pivotTableName
403
     *
404
     * @return \SplObjectStorage
405
     */
406
    private function getRelationshipStorage($pivotTableName)
407
    {
408
        if (isset($this->relationships[$pivotTableName])) {
409
            $storage = $this->relationships[$pivotTableName];
410
        } else {
411
            $storage = new \SplObjectStorage();
412
            $this->relationships[$pivotTableName] = $storage;
413
        }
414
415
        return $storage;
416
    }
417
418
    /**
419
     * Reverts any changes made to the object and resumes it to its DB state.
420
     * This can only be called on objects that come from database and that have not been deleted.
421
     * Otherwise, this will throw an exception.
422
     *
423
     * @throws TDBMException
424
     */
425
    public function discardChanges()
426
    {
427
        if ($this->status === TDBMObjectStateEnum::STATE_NEW || $this->status === TDBMObjectStateEnum::STATE_DETACHED) {
428
            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.");
429
        }
430
431
        if ($this->status === TDBMObjectStateEnum::STATE_DELETED) {
432
            throw new TDBMException('You cannot call discardChanges() on an object that has been deleted.');
433
        }
434
435
        $this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED);
436
    }
437
438
    /**
439
     * Method used internally by TDBM. You should not use it directly.
440
     * This method returns the status of the TDBMObject.
441
     * This is one of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
442
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
443
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
444
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
445
     *
446
     * @return string
447
     */
448
    public function _getStatus()
449
    {
450
        return $this->status;
451
    }
452
453
    /**
454
     * Override the native php clone function for TDBMObjects.
455
     */
456
    public function __clone()
457
    {
458
        // Let's clone the many to many relationships
459
        if ($this->status === TDBMObjectStateEnum::STATE_DETACHED) {
460
            $pivotTableList = array_keys($this->relationships);
461
        } else {
462
            $pivotTableList = $this->tdbmService->_getPivotTablesLinkedToBean($this);
463
        }
464
465
        foreach ($pivotTableList as $pivotTable) {
466
            $storage = $this->retrieveRelationshipsStorage($pivotTable);
0 ignored issues
show
Unused Code introduced by
$storage 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...
467
468
            // Let's duplicate the reverse side of the relationship // This is useless: already done by "retrieveRelationshipsStorage"!!!
469
            /*foreach ($storage as $remoteBean) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
66% 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...
470
                $metadata = $storage[$remoteBean];
471
472
                $remoteStorage = $remoteBean->getRelationshipStorage($pivotTable);
473
                $remoteStorage->attach($this, ['status' => $metadata['status'], 'reverse' => !$metadata['reverse']]);
474
            }*/
475
        }
476
477
        // Let's clone each row
478
        foreach ($this->dbRows as $key => &$dbRow) {
479
            $dbRow = clone $dbRow;
480
            $dbRow->setTDBMObject($this);
481
        }
482
483
        // Let's set the status to new (to enter the save function)
484
        $this->status = TDBMObjectStateEnum::STATE_DETACHED;
485
    }
486
487
    /**
488
     * Returns raw database rows.
489
     *
490
     * @return DbRow[] Key: table name, Value: DbRow object
491
     */
492
    public function _getDbRows()
493
    {
494
        return $this->dbRows;
495
    }
496
497
    private function registerTable($tableName)
498
    {
499
        $dbRow = new DbRow($this, $tableName);
500
501
        if (in_array($this->status, [TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DIRTY])) {
502
            // Let's get the primary key for the new table
503
            $anotherDbRow = array_values($this->dbRows)[0];
504
            /* @var $anotherDbRow DbRow */
505
            $indexedPrimaryKeys = array_values($anotherDbRow->_getPrimaryKeys());
506
            $primaryKeys = $this->tdbmService->_getPrimaryKeysFromIndexedPrimaryKeys($tableName, $indexedPrimaryKeys);
507
            $dbRow->_setPrimaryKeys($primaryKeys);
508
        }
509
510
        $dbRow->_setStatus($this->status);
511
512
        $this->dbRows[$tableName] = $dbRow;
513
        // TODO: look at status (if not new)=> get primary key from tdbmservice
514
    }
515
516
    /**
517
     * Internal function: return the list of relationships.
518
     *
519
     * @return \SplObjectStorage[]
520
     */
521
    public function _getCachedRelationships()
522
    {
523
        return $this->relationships;
524
    }
525
}
526