Issues (291)

src/DbRow.php (1 issue)

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);
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