Completed
Push — 4.0 ( e1def4...8f8e98 )
by David
15:31
created

AbstractTDBMObject::_getStatus()   A

Complexity

Conditions 1
Paths 1

Size

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