Passed
Push — master ( c5a508...155a53 )
by Sergei
03:09
created

BaseActiveRecord   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 183
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 28
eloc 59
c 8
b 0
f 0
dl 0
loc 183
rs 10
ccs 14
cts 14
cp 1

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getObjectVars() 0 3 1
A filterCondition() 0 16 5
A getTableSchema() 0 9 2
A primaryKey() 0 3 1
A populateAttribute() 0 3 1
A filterValidAliases() 0 7 1
A populateRecord() 0 12 3
A attributes() 0 3 1
A filterValidColumnNames() 0 20 3
A loadDefaultValues() 0 9 5
A refresh() 0 15 2
A insertInternal() 0 20 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use Throwable;
8
use Yiisoft\Db\Exception\Exception;
9
use Yiisoft\Db\Exception\InvalidArgumentException;
10
use Yiisoft\Db\Exception\InvalidConfigException;
11
use Yiisoft\Db\Schema\TableSchemaInterface;
12
13
use function array_diff;
14
use function array_keys;
15
use function array_map;
16
use function array_values;
17
use function get_object_vars;
18
use function in_array;
19
use function is_array;
20
use function is_string;
21
use function key;
22
use function preg_replace;
23
24
/**
25
 * Base Active Record class which implements {@see ActiveRecordInterface} with the minimum set of methods.
26
 *
27
 * Active Record implements the [Active Record design pattern](https://en.wikipedia.org/wiki/Active_record).
28
 *
29
 * The premise behind Active Record is that an individual {@see ActiveRecord} object is associated with a specific row
30
 * in a database table. The object's attributes are mapped to the columns of the corresponding table.
31
 *
32
 * Referencing an Active Record attribute is equivalent to accessing the corresponding table column for that record.
33
 *
34
 * As an example, say that the `Customer` ActiveRecord class is associated with the `customer` table.
35
 *
36
 * This would mean that the class's `name` attribute is automatically mapped to the `name` column in `customer` table.
37
 * Thanks to Active Record, assuming the variable `$customer` is an object of type `Customer`, to get the value of the
38
 * `name` column for the table row, you can use the expression `$customer->name`.
39
 *
40
 * In this example, Active Record is providing an object-oriented interface for accessing data stored in the database.
41
 * But Active Record provides much more functionality than this.
42
 *
43
 * To declare an ActiveRecord class you need to extend {@see ActiveRecord} and implement the `getTableName` method:
44
 *
45
 * ```php
46
 * class Customer extends ActiveRecord
47
 * {
48
 *     public static function getTableName(): string
49
 *     {
50
 *         return 'customer';
51
 *     }
52
 * }
53
 * ```
54
 *
55
 * The `getTableName` method only has to return the name of the database table associated with the class.
56
 *
57
 * Class instances are obtained in one of two ways:
58
 *
59
 * Using the `new` operator to create a new, empty object.
60
 * Using a method to fetch an existing record (or records) from the database.
61
 *
62
 * Below is an example showing some typical usage of ActiveRecord:
63 719
 *
64
 * ```php
65 719
 * $user = new User($db);
66 719
 * $user->name = 'Qiang';
67 719
 * $user->save();  // a new row is inserted into user table
68
 *
69
 * // the following will retrieve the user 'CeBe' from the database
70
 * $userQuery = new ActiveQuery(User::class, $db);
71
 * $user = $userQuery->where(['name' => 'CeBe'])->one();
72
 *
73
 * // this will get related records from orders table when relation is defined
74
 * $orders = $user->orders;
75
 * ```
76
 *
77
 * For more details and usage information on ActiveRecord,
78
 * {@see the [guide article on ActiveRecord](guide:db-active-record)}
79
 */
80
class BaseActiveRecord extends AbstractActiveRecord
81
{
82
    public function attributes(): array
83
    {
84
        return $this->getTableSchema()->getColumnNames();
85
    }
86
87
    public function filterCondition(array $condition, array $aliases = []): array
88 1
    {
89
        $result = [];
90 1
91
        $columnNames = $this->filterValidColumnNames($aliases);
92
93
        foreach ($condition as $key => $value) {
94
            if (is_string($key) && !in_array($this->db()->getQuoter()->quoteSql($key), $columnNames, true)) {
95
                throw new InvalidArgumentException(
96
                    'Key "' . $key . '" is not a column name and can not be used as a filter'
97
                );
98
            }
99
            $result[$key] = is_array($value) ? array_values($value) : $value;
100
        }
101
102
        return $result;
103
    }
104
105
    public function filterValidAliases(ActiveQuery $query): array
106
    {
107
        $tables = $query->getTablesUsedInFrom();
108
109
        $aliases = array_diff(array_keys($tables), $tables);
110
111 1
        return array_map(static fn ($alias) => preg_replace('/{{([\w]+)}}/', '$1', $alias), array_values($aliases));
112
    }
113 1
114
    /**
115
     * Returns the schema information of the DB table associated with this AR class.
116
     *
117
     * @throws Exception
118
     * @throws InvalidConfigException If the table for the AR class does not exist.
119
     *
120
     * @return TableSchemaInterface The schema information of the DB table associated with this AR class.
121
     */
122
    public function getTableSchema(): TableSchemaInterface
123
    {
124
        $tableSchema = $this->db()->getSchema()->getTableSchema($this->getTableName());
125
126
        if ($tableSchema === null) {
127
            throw new InvalidConfigException('The table does not exist: ' . $this->getTableName());
128
        }
129
130
        return $tableSchema;
131
    }
132
133
    /**
134
     * Loads default values from database table schema.
135
     *
136 1
     * You may call this method to load default values after creating a new instance:
137
     *
138 1
     * ```php
139
     * // class Customer extends ActiveRecord
140
     * $customer = new Customer($db);
141
     * $customer->loadDefaultValues();
142
     * ```
143
     *
144
     * @param bool $skipIfSet Whether existing value should be preserved. This will only set defaults for attributes
145
     * that are `null`.
146
     *
147
     * @throws Exception
148
     * @throws InvalidConfigException
149
     *
150
     * @return self The active record instance itself.
151
     */
152
    public function loadDefaultValues(bool $skipIfSet = true): self
153
    {
154
        foreach ($this->getTableSchema()->getColumns() as $name => $column) {
155
            if ($column->getDefaultValue() !== null && (!$skipIfSet || $this->getAttribute($name) === null)) {
156
                $this->setAttribute($name, $column->getDefaultValue());
157
            }
158
        }
159
160
        return $this;
161
    }
162 41
163
    public function populateRecord(array|object $row): void
164 41
    {
165
        $columns = $this->getTableSchema()->getColumns();
166
167
        /** @psalm-var array[][] $row */
168
        foreach ($row as $name => $value) {
169
            if (isset($columns[$name])) {
170
                $row[$name] = $columns[$name]->phpTypecast($value);
171
            }
172
        }
173
174
        parent::populateRecord($row);
175
    }
176
177
    public function primaryKey(): array
178
    {
179
        return $this->getTableSchema()->getPrimaryKey();
180
    }
181
182
    /**
183
     * @throws Exception
184
     * @throws InvalidArgumentException
185
     * @throws InvalidConfigException
186
     * @throws Throwable
187
     */
188
    public function refresh(): bool
189
    {
190
        $query = $this->instantiateQuery(static::class);
191
192
        $tableName = key($query->getTablesUsedInFrom());
193
        $pk = [];
194
195
        /** disambiguate column names in case ActiveQuery adds a JOIN */
196
        foreach ($this->getPrimaryKey(true) as $key => $value) {
197
            $pk[$tableName . '.' . $key] = $value;
198 116
        }
199
200 116
        $query->where($pk);
201
202
        return $this->refreshInternal($query->onePopulate());
203
    }
204
205
    /**
206
     * Valid column names are table column names or column names prefixed with table name or table alias.
207
     *
208
     * @throws Exception
209
     * @throws InvalidConfigException
210
     */
211
    protected function filterValidColumnNames(array $aliases): array
212
    {
213
        $columnNames = [];
214
        $tableName = $this->getTableName();
215
        $quotedTableName = $this->db()->getQuoter()->quoteTableName($tableName);
216
217
        foreach ($this->getTableSchema()->getColumnNames() as $columnName) {
218
            $columnNames[] = $columnName;
219
            $columnNames[] = $this->db()->getQuoter()->quoteColumnName($columnName);
220
            $columnNames[] = "$tableName.$columnName";
221
            $columnNames[] = $this->db()->getQuoter()->quoteSql("$quotedTableName.[[$columnName]]");
222
223
            foreach ($aliases as $tableAlias) {
224
                $columnNames[] = "$tableAlias.$columnName";
225
                $quotedTableAlias = $this->db()->getQuoter()->quoteTableName($tableAlias);
226
                $columnNames[] = $this->db()->getQuoter()->quoteSql("$quotedTableAlias.[[$columnName]]");
227
            }
228
        }
229
230
        return $columnNames;
231
    }
232
233
    protected function getObjectVars(ActiveRecordInterface $object): array
234 202
    {
235
        return get_object_vars($object);
236 202
    }
237
238
    protected function insertInternal(array $attributes = null): bool
239
    {
240
        $values = $this->getDirtyAttributes($attributes);
241
        $primaryKeys = $this->db()->createCommand()->insertWithReturningPks($this->getTableName(), $values);
242
243
        if ($primaryKeys === false) {
244
            return false;
245
        }
246
247
        $columns = $this->getTableSchema()->getColumns();
248
249
        foreach ($primaryKeys as $name => $value) {
250
            $id = $columns[$name]->phpTypecast($value);
251 248
            $this->setAttribute($name, $id);
252
            $values[$name] = $id;
253 248
        }
254
255
        $this->setOldAttributes($values);
256
257
        return true;
258
    }
259
260
    protected function populateAttribute(string $name, mixed $value): void
261
    {
262
        $this->$name = $value;
263
    }
264
}
265