Passed
Push — master ( 537eca...d2535c )
by Sergei
02:59
created

ActiveRecord::delete()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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