Completed
Push — 4.0 ( 984e8a...f02fa4 )
by David
08:59 queued 18s
created

AbstractTDBMObject::_constructLazy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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