Passed
Pull Request — master (#172)
by David
02:56
created

DbRow::getTDBMObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM;
5
6
/*
7
 Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
8
9
 This program is free software; you can redistribute it and/or modify
10
 it under the terms of the GNU General Public License as published by
11
 the Free Software Foundation; either version 2 of the License, or
12
 (at your option) any later version.
13
14
 This program is distributed in the hope that it will be useful,
15
 but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 GNU General Public License for more details.
18
19
 You should have received a copy of the GNU General Public License
20
 along with this program; if not, write to the Free Software
21
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
22
 */
23
24
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\ManyToOnePartialQuery;
25
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\Query\PartialQuery;
26
use TheCodingMachine\TDBM\QueryFactory\SmartEagerLoad\StorageNode;
27
use TheCodingMachine\TDBM\Schema\ForeignKeys;
28
use function array_pop;
29
use function count;
30
use function var_export;
31
32
/**
33
 * Instances of this class represent a row in a database.
34
 *
35
 * @author David Negrier
36
 */
37
class DbRow
38
{
39
    /**
40
     * The service this object is bound to.
41
     *
42
     * @var TDBMService|null
43
     */
44
    protected $tdbmService;
45
46
    /**
47
     * The object containing this db row.
48
     *
49
     * @var AbstractTDBMObject
50
     */
51
    private $object;
52
53
    /**
54
     * The name of the table the object if issued from.
55
     *
56
     * @var string
57
     */
58
    private $dbTableName;
59
60
    /**
61
     * The array of columns returned from database.
62
     *
63
     * @var mixed[]
64
     */
65
    private $dbRow = [];
66
67
    /**
68
     * The array of beans this bean points to, indexed by foreign key name.
69
     *
70
     * @var AbstractTDBMObject[]
71
     */
72
    private $references = [];
73
74
    /**
75
     * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
76
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
77
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
78
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
79
     *
80
     * @var string
81
     */
82
    private $status;
83
84
    /**
85
     * The values of the primary key.
86
     * This is set when the object is in "loaded" or "not loaded" state.
87
     *
88
     * @var array An array of column => value
89
     */
90
    private $primaryKeys;
91
92
    /**
93
     * A list of modified columns, indexed by column name. Value is always true.
94
     *
95
     * @var array
96
     */
97
    private $modifiedColumns = [];
98
99
    /**
100
     * A list of modified references, indexed by foreign key name. Value is always true.
101
     *
102
     * @var array
103
     */
104
    private $modifiedReferences = [];
105
    /**
106
     * @var ForeignKeys
107
     */
108
    private $foreignKeys;
109
    /**
110
     * @var PartialQuery|null
111
     */
112
    private $partialQuery;
113
114
    /**
115
     * You should never call the constructor directly. Instead, you should use the
116
     * TDBMService class that will create TDBMObjects for you.
117
     *
118
     * Used with id!=false when we want to retrieve an existing object
119
     * and id==false if we want a new object
120
     *
121
     * @param AbstractTDBMObject $object The object containing this db row
122
     * @param string $tableName
123
     * @param mixed[] $primaryKeys
124
     * @param TDBMService $tdbmService
125
     * @param mixed[] $dbRow
126
     * @throws TDBMException
127
     */
128
    public function __construct(AbstractTDBMObject $object, string $tableName, ForeignKeys $foreignKeys, array $primaryKeys = array(), TDBMService $tdbmService = null, array $dbRow = [], ?PartialQuery $partialQuery = null)
129
    {
130
        $this->object = $object;
131
        $this->dbTableName = $tableName;
132
        $this->foreignKeys = $foreignKeys;
133
        $this->partialQuery = $partialQuery;
134
135
        $this->status = TDBMObjectStateEnum::STATE_DETACHED;
136
137
        if ($tdbmService === null) {
138
            if (!empty($primaryKeys)) {
139
                throw new TDBMException('You cannot pass an id to the DbRow constructor without passing also a TDBMService.');
140
            }
141
        } else {
142
            $this->tdbmService = $tdbmService;
143
144
            if (!empty($primaryKeys)) {
145
                $this->_setPrimaryKeys($primaryKeys);
146
                if (!empty($dbRow)) {
147
                    $this->dbRow = $dbRow;
148
                    $this->status = TDBMObjectStateEnum::STATE_LOADED;
149
                } else {
150
                    $this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
151
                }
152
                $tdbmService->_addToCache($this);
153
            } else {
154
                $this->status = TDBMObjectStateEnum::STATE_NEW;
155
                $this->tdbmService->_addToToSaveObjectList($this);
156
            }
157
        }
158
    }
159
160
    public function _attach(TDBMService $tdbmService): void
161
    {
162
        if ($this->status !== TDBMObjectStateEnum::STATE_DETACHED) {
163
            throw new TDBMInvalidOperationException('Cannot attach an object that is already attached to TDBM.');
164
        }
165
        $this->tdbmService = $tdbmService;
166
        $this->status = TDBMObjectStateEnum::STATE_NEW;
167
        $this->tdbmService->_addToToSaveObjectList($this);
168
    }
169
170
    /**
171
     * Sets the state of the TDBM Object
172
     * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
173
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with the "new" keyword.
174
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
175
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
176
     *
177
     * @param string $state
178
     */
179
    public function _setStatus(string $state) : void
180
    {
181
        $this->status = $state;
182
        if ($state === TDBMObjectStateEnum::STATE_LOADED) {
183
            // after saving we are back to a loaded state, hence unmodified.
184
            $this->modifiedColumns = [];
185
            $this->modifiedReferences = [];
186
        }
187
    }
188
189
    /**
190
     * When discarding a bean, we expect to reload data from the DB, not the cache.
191
     * Hence, we must disable smart eager load.
192
     */
193
    public function disableSmartEagerLoad(): void
194
    {
195
        $this->partialQuery = null;
196
    }
197
198
    /**
199
     * This is an internal method. You should not call this method yourself. The TDBM library will do it for you.
200
     * If the object is in state 'not loaded', this method performs a query in database to load the object.
201
     *
202
     * A TDBMException is thrown is no object can be retrieved (for instance, if the primary key specified
203
     * cannot be found).
204
     */
205
    public function _dbLoadIfNotLoaded(): void
206
    {
207
        if ($this->status === TDBMObjectStateEnum::STATE_NOT_LOADED) {
208
            if ($this->tdbmService === null) {
209
                throw new TDBMException('DbRow initialization failed. tdbmService is null but status is STATE_NOT_LOADED'); // @codeCoverageIgnore
210
            }
211
            $connection = $this->tdbmService->getConnection();
212
213
            if ($this->partialQuery !== null) {
214
                $this->partialQuery->registerDataLoader($connection);
215
216
                // Let's get the data loader.
217
                $dataLoader = $this->partialQuery->getStorageNode()->getManyToOneDataLoader($this->partialQuery->getKey());
218
219
                if (count($this->primaryKeys) !== 1) {
220
                    throw new \RuntimeException('Data-loader patterns only supports primary keys on one column. Table "'.$this->dbTableName.'" has a PK on '.count($this->primaryKeys). ' columns'); // @codeCoverageIgnore
221
                }
222
                $pks = $this->primaryKeys;
223
                $pkId = array_pop($pks);
224
225
                $row = $dataLoader->get((string) $pkId);
226
            } else {
227
                list($sql_where, $parameters) = $this->tdbmService->buildFilterFromFilterBag($this->primaryKeys, $connection->getDatabasePlatform());
228
229
                $sql = 'SELECT * FROM '.$connection->quoteIdentifier($this->dbTableName).' WHERE '.$sql_where;
230
                $result = $connection->executeQuery($sql, $parameters);
231
232
                $row = $result->fetch(\PDO::FETCH_ASSOC);
233
234
                $result->closeCursor();
235
236
                if ($row === false) {
237
                    throw new NoBeanFoundException("Could not retrieve object from table \"$this->dbTableName\" using filter \"$sql_where\" with data \"".var_export($parameters, true). '".');
238
                }
239
            }
240
241
242
243
            $this->dbRow = [];
244
            $types = $this->tdbmService->_getColumnTypesForTable($this->dbTableName);
245
246
            foreach ($row as $key => $value) {
247
                $this->dbRow[$key] = $types[$key]->convertToPHPValue($value, $connection->getDatabasePlatform());
248
            }
249
250
            $this->status = TDBMObjectStateEnum::STATE_LOADED;
251
        }
252
    }
253
254
    /**
255
     * @return mixed|null
256
     */
257
    public function get(string $var)
258
    {
259
        if (!isset($this->primaryKeys[$var])) {
260
            $this->_dbLoadIfNotLoaded();
261
        }
262
263
        return $this->dbRow[$var] ?? null;
264
    }
265
266
    /**
267
     * @param string $var
268
     * @param mixed $value
269
     * @throws TDBMException
270
     */
271
    public function set(string $var, $value): void
272
    {
273
        $this->_dbLoadIfNotLoaded();
274
275
        /*
276
        // Ok, let's start by checking the column type
277
        $type = $this->db_connection->getColumnType($this->dbTableName, $var);
278
279
        // Throws an exception if the type is not ok.
280
        if (!$this->db_connection->checkType($value, $type)) {
281
            throw new TDBMException("Error! Invalid value passed for attribute '$var' of table '$this->dbTableName'. Passed '$value', but expecting '$type'");
282
        }
283
        */
284
285
        /*if ($var == $this->getPrimaryKey() && isset($this->dbRow[$var]))
286
            throw new TDBMException("Error! Changing primary key value is forbidden.");*/
287
        $this->dbRow[$var] = $value;
288
        $this->modifiedColumns[$var] = true;
289
        if ($this->tdbmService !== null && $this->status === TDBMObjectStateEnum::STATE_LOADED) {
290
            $this->status = TDBMObjectStateEnum::STATE_DIRTY;
291
            $this->tdbmService->_addToToSaveObjectList($this);
292
        }
293
    }
294
295
    /**
296
     * @param string             $foreignKeyName
297
     * @param AbstractTDBMObject $bean
298
     */
299
    public function setRef(string $foreignKeyName, AbstractTDBMObject $bean = null): void
300
    {
301
        $this->references[$foreignKeyName] = $bean;
302
        $this->modifiedReferences[$foreignKeyName] = true;
303
304
        if ($this->tdbmService !== null && $this->status === TDBMObjectStateEnum::STATE_LOADED) {
305
            $this->status = TDBMObjectStateEnum::STATE_DIRTY;
306
            $this->tdbmService->_addToToSaveObjectList($this);
307
        }
308
    }
309
310
    /**
311
     * @param string $foreignKeyName A unique name for this reference
312
     *
313
     * @return AbstractTDBMObject|null
314
     */
315
    public function getRef(string $foreignKeyName) : ?AbstractTDBMObject
316
    {
317
        if (array_key_exists($foreignKeyName, $this->references)) {
318
            return $this->references[$foreignKeyName];
319
        } elseif ($this->status === TDBMObjectStateEnum::STATE_NEW || $this->tdbmService === null) {
320
            // If the object is new and has no property, then it has to be empty.
321
            return null;
322
        } else {
323
            $this->_dbLoadIfNotLoaded();
324
325
            // Let's match the name of the columns to the primary key values
326
            $fk = $this->foreignKeys->getForeignKey($foreignKeyName);
327
328
            $values = [];
329
            $localColumns = $fk->getUnquotedLocalColumns();
330
            foreach ($localColumns as $column) {
331
                if (!isset($this->dbRow[$column])) {
332
                    return null;
333
                }
334
                $values[] = $this->dbRow[$column];
335
            }
336
337
            $foreignColumns = $fk->getUnquotedForeignColumns();
338
            $foreignTableName = $fk->getForeignTableName();
339
340
            $filter = SafeFunctions::arrayCombine($foreignColumns, $values);
341
342
            // If the foreign key points to the primary key, let's use findObjectByPk
343
            if ($this->tdbmService->getPrimaryKeyColumns($foreignTableName) === $foreignColumns) {
344
                if ($this->partialQuery !== null && count($foreignColumns) === 1) {
345
                    // Optimisation: let's build the smart eager load query we need to fetch more than one object at once.
346
                    $newPartialQuery = new ManyToOnePartialQuery($this->partialQuery, $this->dbTableName, $fk->getForeignTableName(), $foreignColumns[0], $localColumns[0]);
347
                } else {
348
                    $newPartialQuery = null;
349
                }
350
                $ref = $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true, null, $newPartialQuery);
351
            } else {
352
                $ref = $this->tdbmService->findObject($foreignTableName, $filter);
353
            }
354
            $this->references[$foreignKeyName] = $ref;
355
            return $ref;
356
        }
357
    }
358
359
    /**
360
     * Returns the name of the table this object comes from.
361
     *
362
     * @return string
363
     */
364
    public function _getDbTableName(): string
365
    {
366
        return $this->dbTableName;
367
    }
368
369
    /**
370
     * Method used internally by TDBM. You should not use it directly.
371
     * This method returns the status of the TDBMObject.
372
     * This is one of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
373
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
374
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
375
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
376
     *
377
     * @return string
378
     */
379
    public function _getStatus(): string
380
    {
381
        return $this->status;
382
    }
383
384
    /**
385
     * Override the native php clone function for TDBMObjects.
386
     */
387
    public function __clone()
388
    {
389
        // Let's load the row (before we lose the ID!)
390
        $this->_dbLoadIfNotLoaded();
391
392
        //Let's set the status to detached
393
        $this->status = TDBMObjectStateEnum::STATE_DETACHED;
394
395
        $this->primaryKeys = [];
396
397
        //Now unset the PK from the row
398
        if ($this->tdbmService) {
399
            $pk_array = $this->tdbmService->getPrimaryKeyColumns($this->dbTableName);
400
            foreach ($pk_array as $pk) {
401
                unset($this->dbRow[$pk]);
402
            }
403
        }
404
    }
405
406
    /**
407
     * Returns raw database row.
408
     *
409
     * @return mixed[]
410
     *
411
     * @throws TDBMMissingReferenceException
412
     */
413
    public function _getDbRow(): array
414
    {
415
        return $this->buildDbRow($this->dbRow, $this->references);
416
    }
417
418
    /**
419
     * Returns raw database row that needs to be updated.
420
     *
421
     * @return mixed[]
422
     *
423
     * @throws TDBMMissingReferenceException
424
     */
425
    public function _getUpdatedDbRow(): array
426
    {
427
        $dbRow = \array_intersect_key($this->dbRow, $this->modifiedColumns);
428
        $references = \array_intersect_key($this->references, $this->modifiedReferences);
429
        return $this->buildDbRow($dbRow, $references);
430
    }
431
432
    /**
433
     * Builds a raw db row from dbRow and references passed in parameters.
434
     *
435
     * @param mixed[] $dbRow
436
     * @param array<string,AbstractTDBMObject|null> $references
437
     * @return mixed[]
438
     * @throws TDBMMissingReferenceException
439
     */
440
    private function buildDbRow(array $dbRow, array $references): array
441
    {
442
        if ($this->tdbmService === null) {
443
            throw new TDBMException('DbRow initialization failed. tdbmService is null.'); // @codeCoverageIgnore
444
        }
445
446
        // Let's merge $dbRow and $references
447
        foreach ($references as $foreignKeyName => $reference) {
448
            // Let's match the name of the columns to the primary key values
449
            $fk = $this->foreignKeys->getForeignKey($foreignKeyName);
450
            $localColumns = $fk->getUnquotedLocalColumns();
451
452
            if ($reference !== null) {
453
                $refDbRows = $reference->_getDbRows();
454
                $firstRefDbRow = reset($refDbRows);
455
                if ($firstRefDbRow === false) {
456
                    throw new \RuntimeException('Unexpected error: empty refDbRows'); // @codeCoverageIgnore
457
                }
458
                if ($firstRefDbRow->_getStatus() === TDBMObjectStateEnum::STATE_DELETED) {
459
                    throw TDBMMissingReferenceException::referenceDeleted($this->dbTableName, $reference);
460
                }
461
                $foreignColumns = $fk->getUnquotedForeignColumns();
462
                $refBeanValues = $firstRefDbRow->dbRow;
463
                for ($i = 0, $count = \count($localColumns); $i < $count; ++$i) {
464
                    $dbRow[$localColumns[$i]] = $refBeanValues[$foreignColumns[$i]];
465
                }
466
            } else {
467
                for ($i = 0, $count = \count($localColumns); $i < $count; ++$i) {
468
                    $dbRow[$localColumns[$i]] = null;
469
                }
470
            }
471
        }
472
473
        return $dbRow;
474
    }
475
476
    /**
477
     * Returns references array.
478
     *
479
     * @return AbstractTDBMObject[]
480
     */
481
    public function _getReferences(): array
482
    {
483
        return $this->references;
484
    }
485
486
    /**
487
     * Returns the values of the primary key.
488
     * This is set when the object is in "loaded" state.
489
     *
490
     * @return mixed[]
491
     */
492
    public function _getPrimaryKeys(): array
493
    {
494
        return $this->primaryKeys;
495
    }
496
497
    /**
498
     * Sets the values of the primary key.
499
     * This is set when the object is in "loaded" or "not loaded" state.
500
     *
501
     * @param mixed[] $primaryKeys
502
     */
503
    public function _setPrimaryKeys(array $primaryKeys): void
504
    {
505
        $this->primaryKeys = $primaryKeys;
506
        foreach ($this->primaryKeys as $column => $value) {
507
            // Warning: in case of multi-columns with one being a reference, the $dbRow will contain a reference column (which is not the case elsewhere in the application)
508
            $this->dbRow[$column] = $value;
509
        }
510
    }
511
512
    /**
513
     * Returns the TDBMObject this bean is associated to.
514
     *
515
     * @return AbstractTDBMObject
516
     */
517
    public function getTDBMObject(): AbstractTDBMObject
518
    {
519
        return $this->object;
520
    }
521
522
    /**
523
     * Sets the TDBMObject this bean is associated to.
524
     * Only used when cloning.
525
     *
526
     * @param AbstractTDBMObject $object
527
     */
528
    public function setTDBMObject(AbstractTDBMObject $object): void
529
    {
530
        $this->object = $object;
531
    }
532
}
533