Passed
Pull Request — master (#172)
by David
06:43
created

DbRow::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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