Passed
Push — master ( 614fe6...dab8d6 )
by Sergei
02:58
created

BaseActiveRecord   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 26
eloc 57
c 8
b 0
f 0
dl 0
loc 186
ccs 14
cts 14
cp 1
rs 10

10 Methods

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