diff --git a/src/BaseActiveRecord.php b/src/BaseActiveRecord.php index cc19dbf40..485bc7ccf 100644 --- a/src/BaseActiveRecord.php +++ b/src/BaseActiveRecord.php @@ -29,12 +29,15 @@ use function array_intersect_key; use function array_key_exists; use function array_keys; +use function array_merge; use function array_search; use function array_values; use function count; +use function get_object_vars; use function in_array; use function is_array; use function is_int; +use function property_exists; use function reset; /** @@ -187,11 +190,8 @@ public function getOldAttribute(string $name): mixed */ public function getDirtyAttributes(array $names = null): array { - if ($names === null) { - $attributes = $this->attributes; - } else { - $attributes = array_intersect_key($this->attributes, array_flip($names)); - } + $attributes = array_merge($this->attributes, get_object_vars($this)); + $attributes = array_intersect_key($attributes, array_flip($names ?? $this->attributes())); if ($this->oldAttributes === null) { return $attributes; @@ -570,21 +570,11 @@ public function optimisticLock(): string|null */ public function populateRecord(array|object $row): void { - $columns = array_flip($this->attributes()); - - /** - * @psalm-var string $name - * @psalm-var mixed $value - */ foreach ($row as $name => $value) { - if (isset($columns[$name])) { - $this->attributes[$name] = $value; - } elseif ($this->canSetProperty($name)) { - $this->$name = $value; - } + $this->populateAttribute($name, $value); + $this->oldAttributes[$name] = $value; } - $this->oldAttributes = $this->attributes; $this->related = []; $this->relationsDependencies = []; } @@ -1252,4 +1242,13 @@ public function getTableName(): string return $this->tableName; } + + private function populateAttribute(string $name, mixed $value): void + { + if (property_exists($this, $name)) { + $this->$name = $value; + } else { + $this->attributes[$name] = $value; + } + } } diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index a85d2bbcf..5ccc4b8ef 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -13,6 +13,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerClosureField; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerForArrayable; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerWithAlias; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerWithProperties; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Dog; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Item; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NoExist; @@ -597,14 +598,9 @@ public function testAttributeAccess(): void $this->assertTrue($customer->canGetProperty('orderItems')); $this->assertFalse($customer->canSetProperty('orderItems')); - try { - /** @var $itemClass ActiveRecordInterface */ - $customer->orderItems = [new Item($this->db)]; - $this->fail('setter call above MUST throw Exception'); - } catch (Exception $e) { - /** catch exception "Setting read-only property" */ - $this->assertInstanceOf(InvalidCallException::class, $e); - } + $this->expectException(UnknownPropertyException::class); + $this->expectExceptionMessage('Setting unknown property: ' . Customer::class . '::orderItems'); + $customer->orderItems = [new Item($this->db)]; /** related attribute $customer->orderItems didn't change cause it's read-only */ $this->assertSame([], $customer->orderItems); @@ -855,7 +851,7 @@ public function testGetDirtyAttributesOnNewRecord(): void ); $this->assertEquals( ['email' => 'adam@example.com', 'address' => null], - $customer->getDirtyAttributes(['id', 'email', 'address', 'status']), + $customer->getDirtyAttributes(['id', 'email', 'address', 'status', 'unknown']), ); $this->assertTrue($customer->save()); @@ -885,7 +881,37 @@ public function testGetDirtyAttributesAfterFind(): void ); $this->assertEquals( ['email' => 'adam@example.com', 'address' => null], - $customer->getDirtyAttributes(['id', 'email', 'address', 'status']), + $customer->getDirtyAttributes(['id', 'email', 'address', 'status', 'unknown']), + ); + } + + public function testGetDirtyAttributesWithProperties(): void + { + $this->checkFixture($this->db, 'customer'); + + $customer = new CustomerWithProperties($this->db); + $this->assertSame([ + 'name' => null, + 'address' => null, + ], $customer->getDirtyAttributes()); + + $customerQuery = new ActiveQuery(CustomerWithProperties::class, $this->db); + $customer = $customerQuery->findOne(1); + + $this->assertSame([], $customer->getDirtyAttributes()); + + $customer->setEmail('adam@example.com'); + $customer->setName('Adam'); + $customer->setAddress(null); + $customer->setStatus(null); + + $this->assertEquals( + ['email' => 'adam@example.com', 'name' => 'Adam', 'address' => null, 'status' => null], + $customer->getDirtyAttributes(), + ); + $this->assertEquals( + ['email' => 'adam@example.com', 'address' => null], + $customer->getDirtyAttributes(['id', 'email', 'address', 'unknown']), ); } } diff --git a/tests/Stubs/ActiveRecord/CustomerWithProperties.php b/tests/Stubs/ActiveRecord/CustomerWithProperties.php new file mode 100644 index 000000000..4aba20cc6 --- /dev/null +++ b/tests/Stubs/ActiveRecord/CustomerWithProperties.php @@ -0,0 +1,79 @@ +id; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getName(): string|null + { + return $this->name; + } + + public function getAddress(): string|null + { + return $this->address; + } + + public function getStatus(): int|null + { + return $this->getAttribute('status'); + } + + public function getProfile(): ActiveQuery + { + return $this->hasOne(Profile::class, ['id' => 'profile_id']); + } + + public function getOrders(): ActiveQuery + { + return $this->hasMany(Order::class, ['customer_id' => 'id'])->orderBy('[[id]]'); + } + + public function setEmail(string $email): void + { + $this->email = $email; + } + + public function setName(string|null $name): void + { + $this->name = $name; + } + + public function setAddress(string|null $address): void + { + $this->address = $address; + } + + public function setStatus(int|null $status): void + { + $this->setAttribute('status', $status); + } +}