DbRow::getRef()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 7
nop 3
dl 0
loc 35
rs 8.6506
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace TheCodingMachine\TDBM;
6
7
/*
8
 Copyright (C) 2006-2017 David Négrier - THE CODING MACHINE
9
10
 This program is free software; you can redistribute it and/or modify
11
 it under the terms of the GNU General Public License as published by
12
 the Free Software Foundation; either version 2 of the License, or
13
 (at your option) any later version.
14
15
 This program is distributed in the hope that it will be useful,
16
 but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 GNU General Public License for more details.
19
20
 You should have received a copy of the GNU General Public License
21
 along with this program; if not, write to the Free Software
22
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
23
 */
24
25
use TheCodingMachine\TDBM\Schema\ForeignKeys;
26
27
/**
28
 * Instances of this class represent a row in a database.
29
 *
30
 * @author David Negrier
31
 */
32
class DbRow
33
{
34
    /**
35
     * The service this object is bound to.
36
     *
37
     * @var TDBMService|null
38
     */
39
    protected $tdbmService;
40
41
    /**
42
     * The object containing this db row.
43
     *
44
     * @var AbstractTDBMObject
45
     */
46
    private $object;
47
48
    /**
49
     * The name of the table the object if issued from.
50
     *
51
     * @var string
52
     */
53
    private $dbTableName;
54
55
    /**
56
     * The array of columns returned from database.
57
     *
58
     * @var mixed[]
59
     */
60
    private $dbRow = [];
61
62
    /**
63
     * The array of beans this bean points to, indexed by foreign key name.
64
     *
65
     * @var AbstractTDBMObject[]
66
     */
67
    private $references = [];
68
69
    /**
70
     * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
71
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
72
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
73
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
74
     *
75
     * @var string
76
     */
77
    private $status;
78
79
    /**
80
     * The values of the primary key.
81
     * This is set when the object is in "loaded" state.
82
     *
83
     * @var array An array of column => value
84
     */
85
    private $primaryKeys;
86
87
    /**
88
     * A list of modified columns, indexed by column name. Value is always true.
89
     *
90
     * @var array
91
     */
92
    private $modifiedColumns = [];
93
94
    /**
95
     * A list of modified references, indexed by foreign key name. Value is always true.
96
     *
97
     * @var array
98
     */
99
    private $modifiedReferences = [];
100
    /**
101
     * @var ForeignKeys
102
     */
103
    private $foreignKeys;
104
105
    /**
106
     * You should never call the constructor directly. Instead, you should use the
107
     * TDBMService class that will create TDBMObjects for you.
108
     *
109
     * Used with id!=false when we want to retrieve an existing object
110
     * and id==false if we want a new object
111
     *
112
     * @param AbstractTDBMObject $object The object containing this db row
113
     * @param string $tableName
114
     * @param mixed[] $primaryKeys
115
     * @param TDBMService $tdbmService
116
     * @param mixed[] $dbRow
117
     * @throws TDBMException
118
     */
119
    public function __construct(AbstractTDBMObject $object, string $tableName, ForeignKeys $foreignKeys, array $primaryKeys = array(), TDBMService $tdbmService = null, array $dbRow = [])
120
    {
121
        $this->object = $object;
122
        $this->dbTableName = $tableName;
123
        $this->foreignKeys = $foreignKeys;
124
125
        $this->status = TDBMObjectStateEnum::STATE_DETACHED;
126
127
        if ($tdbmService === null) {
128
            if (!empty($primaryKeys)) {
129
                throw new TDBMException('You cannot pass an id to the DbRow constructor without passing also a TDBMService.');
130
            }
131
        } else {
132
            $this->tdbmService = $tdbmService;
133
134
            if (!empty($primaryKeys)) {
135
                $this->_setPrimaryKeys($primaryKeys);
136
                if (!empty($dbRow)) {
137
                    $this->dbRow = $dbRow;
138
                    $this->status = TDBMObjectStateEnum::STATE_LOADED;
139
                } else {
140
                    $this->status = TDBMObjectStateEnum::STATE_NOT_LOADED;
141
                }
142
                $tdbmService->_addToCache($this);
143
            } else {
144
                $this->status = TDBMObjectStateEnum::STATE_NEW;
145
                $this->tdbmService->_addToToSaveObjectList($this);
146
            }
147
        }
148
    }
149
150
    public function _attach(TDBMService $tdbmService): void
151
    {
152
        if ($this->status !== TDBMObjectStateEnum::STATE_DETACHED) {
153
            throw new TDBMInvalidOperationException('Cannot attach an object that is already attached to TDBM.');
154
        }
155
        $this->tdbmService = $tdbmService;
156
        $this->status = TDBMObjectStateEnum::STATE_NEW;
157
        $this->tdbmService->_addToToSaveObjectList($this);
158
    }
159
160
    /**
161
     * Sets the state of the TDBM Object
162
     * One of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
163
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with the "new" keyword.
164
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
165
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
166
     *
167
     * @param string $state
168
     */
169
    public function _setStatus(string $state): void
170
    {
171
        $this->status = $state;
172
        if ($state === TDBMObjectStateEnum::STATE_LOADED) {
173
            // after saving we are back to a loaded state, hence unmodified.
174
            $this->modifiedColumns = [];
175
            $this->modifiedReferences = [];
176
        } elseif ($state === TDBMObjectStateEnum::STATE_NOT_LOADED) {
177
            $this->references = []; // We clear already set references to force re-fetch of those
178
        }
179
    }
180
181
    /**
182
     * This is an internal method. You should not call this method yourself. The TDBM library will do it for you.
183
     * If the object is in state 'not loaded', this method performs a query in database to load the object.
184
     *
185
     * A TDBMException is thrown is no object can be retrieved (for instance, if the primary key specified
186
     * cannot be found).
187
     */
188
    public function _dbLoadIfNotLoaded(): void
189
    {
190
        if ($this->status === TDBMObjectStateEnum::STATE_NOT_LOADED) {
191
            if ($this->tdbmService === null) {
192
                throw new TDBMException('DbRow initialization failed. tdbmService is null but status is STATE_NOT_LOADED'); // @codeCoverageIgnore
193
            }
194
            $connection = $this->tdbmService->getConnection();
195
196
            list($sql_where, $parameters) = $this->tdbmService->buildFilterFromFilterBag($this->primaryKeys, $connection->getDatabasePlatform());
197
198
            $sql = 'SELECT * FROM '.$connection->quoteIdentifier($this->dbTableName).' WHERE '.$sql_where;
199
            $result = $connection->executeQuery($sql, $parameters);
200
201
            $row = $result->fetch(\PDO::FETCH_ASSOC);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Result::fetch() has been deprecated: Use {@see fetchNumeric()}, {@see fetchAssociative()} or {@see fetchOne()} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

201
            $row = /** @scrutinizer ignore-deprecated */ $result->fetch(\PDO::FETCH_ASSOC);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
202
203
            if ($row === false) {
204
                throw new TDBMException("Could not retrieve object from table \"$this->dbTableName\" using filter \".$sql_where.\" with data \"".var_export($parameters, true)."\".");
205
            }
206
207
            $this->dbRow = [];
208
            $types = $this->tdbmService->_getColumnTypesForTable($this->dbTableName);
209
210
            foreach ($row as $key => $value) {
211
                $this->dbRow[$key] = $types[$key]->convertToPHPValue($value, $connection->getDatabasePlatform());
212
            }
213
214
            $result->free();
215
216
            $this->status = TDBMObjectStateEnum::STATE_LOADED;
217
        }
218
    }
219
220
    /**
221
     * @return mixed|null
222
     */
223
    public function get(string $var)
224
    {
225
        if (!isset($this->primaryKeys[$var])) {
226
            $this->_dbLoadIfNotLoaded();
227
        }
228
229
        return $this->dbRow[$var] ?? null;
230
    }
231
232
    /**
233
     * @param string $var
234
     * @param mixed $value
235
     * @throws TDBMException
236
     */
237
    public function set(string $var, $value): void
238
    {
239
        $this->_dbLoadIfNotLoaded();
240
241
        /*if ($var == $this->getPrimaryKey() && isset($this->dbRow[$var]))
242
            throw new TDBMException("Error! Changing primary key value is forbidden.");*/
243
        $this->dbRow[$var] = $value;
244
        $this->modifiedColumns[$var] = true;
245
        if ($this->tdbmService !== null && $this->status === TDBMObjectStateEnum::STATE_LOADED) {
246
            $this->status = TDBMObjectStateEnum::STATE_DIRTY;
247
            $this->tdbmService->_addToToSaveObjectList($this);
248
        }
249
    }
250
251
    /**
252
     * @param string             $foreignKeyName
253
     * @param AbstractTDBMObject $bean
254
     */
255
    public function setRef(string $foreignKeyName, AbstractTDBMObject $bean = null): void
256
    {
257
        $this->references[$foreignKeyName] = $bean;
258
        $this->modifiedReferences[$foreignKeyName] = true;
259
260
        if ($this->tdbmService !== null && $this->status === TDBMObjectStateEnum::STATE_LOADED) {
261
            $this->status = TDBMObjectStateEnum::STATE_DIRTY;
262
            $this->tdbmService->_addToToSaveObjectList($this);
263
        }
264
    }
265
266
    /**
267
     * @param string $foreignKeyName A unique name for this reference
268
     * @param class-string $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
269
     * @param class-string $resultIteratorClass
270
     *
271
     * @return AbstractTDBMObject|null
272
     */
273
    public function getRef(string $foreignKeyName, string $className, string $resultIteratorClass): ?AbstractTDBMObject
274
    {
275
        if (array_key_exists($foreignKeyName, $this->references)) {
276
            return $this->references[$foreignKeyName];
277
        } elseif ($this->status === TDBMObjectStateEnum::STATE_NEW || $this->tdbmService === null) {
278
            // If the object is new and has no property, then it has to be empty.
279
            return null;
280
        } else {
281
            $this->_dbLoadIfNotLoaded();
282
283
            // Let's match the name of the columns to the primary key values
284
            $fk = $this->foreignKeys->getForeignKey($foreignKeyName);
285
286
            $values = [];
287
            foreach ($fk->getUnquotedLocalColumns() as $column) {
288
                if (!isset($this->dbRow[$column])) {
289
                    return null;
290
                }
291
                $values[] = $this->dbRow[$column];
292
            }
293
294
            $foreignColumns = $fk->getUnquotedForeignColumns();
295
            $foreignTableName = $fk->getForeignTableName();
296
297
            $filter = array_combine($foreignColumns, $values);
298
299
            // If the foreign key points to the primary key, let's use findObjectByPk
300
            if ($this->tdbmService->getPrimaryKeyColumns($foreignTableName) === $foreignColumns) {
301
                $reference = $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true, $className, $resultIteratorClass);
302
            } else {
303
                $reference = $this->tdbmService->findObject($foreignTableName, $filter, [], [], $className, $resultIteratorClass);
304
            }
305
306
            $this->references[$foreignKeyName] = $reference;
307
            return $reference;
308
        }
309
    }
310
311
    /**
312
     * Returns the name of the table this object comes from.
313
     *
314
     * @return string
315
     */
316
    public function _getDbTableName(): string
317
    {
318
        return $this->dbTableName;
319
    }
320
321
    /**
322
     * Method used internally by TDBM. You should not use it directly.
323
     * This method returns the status of the TDBMObject.
324
     * This is one of TDBMObjectStateEnum::STATE_NEW, TDBMObjectStateEnum::STATE_NOT_LOADED, TDBMObjectStateEnum::STATE_LOADED, TDBMObjectStateEnum::STATE_DELETED.
325
     * $status = TDBMObjectStateEnum::STATE_NEW when a new object is created with DBMObject:getNewObject.
326
     * $status = TDBMObjectStateEnum::STATE_NOT_LOADED when the object has been retrieved with getObject but when no data has been accessed in it yet.
327
     * $status = TDBMObjectStateEnum::STATE_LOADED when the object is cached in memory.
328
     *
329
     * @return string
330
     */
331
    public function _getStatus(): string
332
    {
333
        return $this->status;
334
    }
335
336
    /**
337
     * Override the native php clone function for TDBMObjects.
338
     */
339
    public function __clone()
340
    {
341
        // Let's load the row (before we lose the ID!)
342
        $this->_dbLoadIfNotLoaded();
343
344
        //Let's set the status to detached
345
        $this->status = TDBMObjectStateEnum::STATE_DETACHED;
346
347
        $this->primaryKeys = [];
348
349
        //Now unset the PK from the row
350
        if ($this->tdbmService) {
351
            $pk_array = $this->tdbmService->getPrimaryKeyColumns($this->dbTableName);
352
            foreach ($pk_array as $pk) {
353
                unset($this->dbRow[$pk]);
354
            }
355
        }
356
    }
357
358
    /**
359
     * Returns raw database row.
360
     *
361
     * @return mixed[]
362
     *
363
     * @throws TDBMMissingReferenceException
364
     */
365
    public function _getDbRow(): array
366
    {
367
        return $this->buildDbRow($this->dbRow, $this->references);
368
    }
369
370
    /**
371
     * Returns raw database row that needs to be updated.
372
     *
373
     * @return mixed[]
374
     *
375
     * @throws TDBMMissingReferenceException
376
     */
377
    public function _getUpdatedDbRow(): array
378
    {
379
        $dbRow = \array_intersect_key($this->dbRow, $this->modifiedColumns);
380
        $references = \array_intersect_key($this->references, $this->modifiedReferences);
381
        return $this->buildDbRow($dbRow, $references);
382
    }
383
384
    /**
385
     * Builds a raw db row from dbRow and references passed in parameters.
386
     *
387
     * @param mixed[] $dbRow
388
     * @param array<string,AbstractTDBMObject|null> $references
389
     * @return mixed[]
390
     * @throws TDBMMissingReferenceException
391
     */
392
    private function buildDbRow(array $dbRow, array $references): array
393
    {
394
        if ($this->tdbmService === null) {
395
            throw new TDBMException('DbRow initialization failed. tdbmService is null.'); // @codeCoverageIgnore
396
        }
397
398
        // Let's merge $dbRow and $references
399
        foreach ($references as $foreignKeyName => $reference) {
400
            // Let's match the name of the columns to the primary key values
401
            $fk = $this->foreignKeys->getForeignKey($foreignKeyName);
402
            $localColumns = $fk->getUnquotedLocalColumns();
403
404
            if ($reference !== null) {
405
                $refDbRows = $reference->_getDbRows();
406
                $refDbRow = $refDbRows[$fk->getForeignTableName()] ?? false;
407
408
                if ($refDbRow === false) {
409
                    throw new \RuntimeException('Unexpected error: empty refDbRows'); // @codeCoverageIgnore
410
                }
411
                if ($refDbRow->_getStatus() === TDBMObjectStateEnum::STATE_DELETED) {
412
                    throw TDBMMissingReferenceException::referenceDeleted($this->dbTableName, $reference);
413
                }
414
                $foreignColumns = $fk->getUnquotedForeignColumns();
415
                $refBeanValues = $refDbRow->dbRow;
416
                for ($i = 0, $count = \count($localColumns); $i < $count; ++$i) {
417
                    $dbRow[$localColumns[$i]] = $refBeanValues[$foreignColumns[$i]];
418
                }
419
            } else {
420
                for ($i = 0, $count = \count($localColumns); $i < $count; ++$i) {
421
                    $dbRow[$localColumns[$i]] = null;
422
                }
423
            }
424
        }
425
426
        return $dbRow;
427
    }
428
429
    /**
430
     * Returns references array.
431
     *
432
     * @return AbstractTDBMObject[]
433
     */
434
    public function _getReferences(): array
435
    {
436
        return $this->references;
437
    }
438
439
    /**
440
     * Returns the values of the primary key.
441
     * This is set when the object is in "loaded" state.
442
     *
443
     * @return mixed[]
444
     */
445
    public function _getPrimaryKeys(): array
446
    {
447
        return $this->primaryKeys;
448
    }
449
450
    /**
451
     * Sets the values of the primary key.
452
     * This is set when the object is in "loaded" or "not loaded" state.
453
     *
454
     * @param mixed[] $primaryKeys
455
     */
456
    public function _setPrimaryKeys(array $primaryKeys): void
457
    {
458
        $this->primaryKeys = $primaryKeys;
459
        foreach ($this->primaryKeys as $column => $value) {
460
            // 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)
461
            $this->dbRow[$column] = $value;
462
        }
463
    }
464
465
    /**
466
     * Returns the TDBMObject this bean is associated to.
467
     *
468
     * @return AbstractTDBMObject
469
     */
470
    public function getTDBMObject(): AbstractTDBMObject
471
    {
472
        return $this->object;
473
    }
474
475
    /**
476
     * Sets the TDBMObject this bean is associated to.
477
     * Only used when cloning.
478
     *
479
     * @param AbstractTDBMObject $object
480
     */
481
    public function setTDBMObject(AbstractTDBMObject $object): void
482
    {
483
        $this->object = $object;
484
    }
485
}
486