diff --git a/src/AbstractActiveRecord.php b/src/AbstractActiveRecord.php index 701780b4d..b5107ceae 100644 --- a/src/AbstractActiveRecord.php +++ b/src/AbstractActiveRecord.php @@ -5,6 +5,7 @@ namespace Yiisoft\ActiveRecord; use Closure; +use DateTimeInterface; use InvalidArgumentException; use LogicException; use ReflectionClass; @@ -116,21 +117,25 @@ public function oldValue(string $propertyName): mixed public function newValues(?array $propertyNames = null): array { - $values = $this->propertyValues($propertyNames); - - if ($this->oldValues === null) { - return $values; + $currentValues = $this->propertyValues($propertyNames); + if (($oldValues = $this->oldValues()) === []) { + return $currentValues; } - $result = array_diff_key($values, $this->oldValues); + $newValues = array_diff_key($currentValues, $oldValues); - foreach (array_diff_key($values, $result) as $name => $value) { - if ($value !== $this->oldValues[$name]) { - $result[$name] = $value; + foreach (array_diff_key($currentValues, $newValues) as $name => $newValue) { + if ($newValue instanceof DateTimeInterface) { + if ($oldValues[$name] === null + || $newValue->format('Y-m-d\TH:i:s.uP') != $oldValues[$name]->format('Y-m-d\TH:i:s.uP')) { + $newValues[$name] = $newValue; + } + } elseif ($newValue !== $oldValues[$name]) { + $newValues[$name] = $newValue; } } - return $result; + return $newValues; } public function oldValues(): array diff --git a/tests/ActiveQueryFindTest.php b/tests/ActiveQueryFindTest.php index 95e64c97a..3b07627b2 100644 --- a/tests/ActiveQueryFindTest.php +++ b/tests/ActiveQueryFindTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests; +use DateTimeImmutable; use Yiisoft\ActiveRecord\ActiveQueryInterface; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerQuery; @@ -12,7 +13,6 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type; use InvalidArgumentException; use Yiisoft\Db\Exception\InvalidConfigException; -use Yiisoft\Db\QueryBuilder\Condition\In; use function ksort; @@ -177,6 +177,7 @@ public function testFindAsArray(): void 'address' => 'address2', 'status' => 1, 'bool_status' => true, + 'registered_at' => new DateTimeImmutable('2022-02-02 02:02:02.222222 Europe/Kyiv'), 'profile_id' => null, ], $customer); diff --git a/tests/ActiveQueryTest.php b/tests/ActiveQueryTest.php index 4298019d0..0c6433c5b 100644 --- a/tests/ActiveQueryTest.php +++ b/tests/ActiveQueryTest.php @@ -5,13 +5,15 @@ namespace Yiisoft\ActiveRecord\Tests; use Closure; +use DateInterval; +use DateTimeImmutable; +use DateTimeInterface; use InvalidArgumentException; use LogicException; use PHPUnit\Framework\Attributes\DataProvider; use Yiisoft\ActiveRecord\ActiveQuery; use Yiisoft\ActiveRecord\ActiveQueryInterface; use Yiisoft\ActiveRecord\Internal\ArArrayHelper; -use Yiisoft\ActiveRecord\JoinWith; use Yiisoft\ActiveRecord\OptimisticLockException; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\BitValues; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Category; @@ -31,7 +33,6 @@ use Yiisoft\ActiveRecord\Tests\Support\DbHelper; use Yiisoft\ActiveRecord\UnknownPropertyException; use Yiisoft\Db\Command\AbstractCommand; -use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidCallException; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Expression\Expression; @@ -2094,6 +2095,7 @@ public function testPropertyValues(): void 'address' => 'address1', 'status' => 1, 'bool_status' => true, + 'registered_at' => new DateTimeImmutable('2011-01-01 01:01:01.111111 Europe/Berlin'), 'profile_id' => 1, ]; @@ -2117,7 +2119,7 @@ public function testPropertyValuesExcept(): void { $customer = Customer::query(); - $values = $customer->findByPk(1)->propertyValues(null, ['status', 'bool_status', 'profile_id']); + $values = $customer->findByPk(1)->propertyValues(null, ['status', 'bool_status', 'registered_at', 'profile_id']); $this->assertEquals( ['id' => 1, 'email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1'], @@ -2148,6 +2150,7 @@ public function testGetOldValues(): void 'address' => 'address1', 'status' => 1, 'bool_status' => true, + 'registered_at' => new DateTimeImmutable('2011-01-01 01:01:01.111111 Europe/Berlin'), 'profile_id' => 1, ]; @@ -2392,6 +2395,41 @@ public function testUnlinkAllAndConditionDelete(): void $this->assertCount(0, $customer->getExpensiveOrders()); } + public function testNewValuesStaysEmptyOnSameMomentInTime() + { + $this->reloadFixtureAfterTest(); + + $customerQuery = Customer::query(); + $customer = $customerQuery->findByPk(2); + $this->assertEmpty($customer->newValues()); + + /** @var DateTimeInterface $customerRegisteredAt */ + $customerRegisteredAt = $customer->get('registered_at'); + $equalRegisteredAt = $customerRegisteredAt->add(new DateInterval('PT0S')); + $this->assertEquals($equalRegisteredAt->format(($format = 'Y-m-d\TH:i:s.uP')), $customerRegisteredAt->format($format)); + + $customer->set('registered_at', $equalRegisteredAt); + $this->assertEmpty($customer->newValues()); + } + + public function testNewValuesStaysNotEmptyOnDifferentMomentInTime() + { + $this->reloadFixtureAfterTest(); + + $customerQuery = Customer::query(); + $customer = $customerQuery->findByPk(2); + $this->assertEmpty($customer->newValues()); + + /** @var DateTimeInterface $customerRegisteredAt */ + $customerRegisteredAt = $customer->get('registered_at'); + $differentRegisteredAt = $customerRegisteredAt->add(new DateInterval('PT1S')); + $this->assertNotEquals($differentRegisteredAt->format(($format = 'Y-m-d\TH:i:s.uP')), $customerRegisteredAt->format($format)); + + $customer->set('registered_at', $differentRegisteredAt); + $this->assertSame($customer->oldValues()['registered_at'], $customerRegisteredAt); + $this->assertSame($customer->newValues()['registered_at'], $differentRegisteredAt); + } + public function testUpdate(): void { $this->reloadFixtureAfterTest(); @@ -2400,6 +2438,7 @@ public function testUpdate(): void $customer = $customerQuery->findByPk(2); $this->assertInstanceOf(Customer::class, $customer); $this->assertEquals('user2', $customer->get('name')); + $this->assertEquals(new DateTimeImmutable('2022-02-02 02:02:02.222222 Europe/Kyiv'), $customer->get('registered_at')); $this->assertFalse($customer->isNew()); $this->assertEmpty($customer->newValues()); diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 75e0a6b91..e54f02687 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -5,6 +5,7 @@ namespace Yiisoft\ActiveRecord\Tests; use ArgumentCountError; +use DateTimeImmutable; use DivisionByZeroError; use InvalidArgumentException; use LogicException; @@ -896,6 +897,56 @@ public function testPrimaryKeyOldValuesWithoutPrimaryKey(): void $orderItem->primaryKeyOldValues(); } + public static function providerForNewValues(): array + { + /* + * Defines the same moment in time but in different timezones. + */ + $registeredAt_1 = new DateTimeImmutable('2011-01-01T01:01:01.111111+01:00'); + $registeredAt_1_1 = new DateTimeImmutable('2011-01-01T00:01:01.111111+00:00'); + + return [ + 'new record 1.0' => [ + 'id' => null, + 'propertyValues' => ['name' => 'user1'], + 'expect' => ['name' => 'user1'], + ], + 'new record 1.1' => [ + 'id' => null, + 'propertyValues' => ['name' => 'user1', 'registered_at' => $registeredAt_1], + 'expect' => ['name' => 'user1', 'registered_at' => $registeredAt_1], + ], + /* + * This record already has the "$registeredAt_1" date set. + */ + 'old record 1.0' => [ + 'id' => 1, + 'propertyValues' => ['name' => 'user1'], + 'expect' => [], + ], + 'old record 1.1' => [ + 'id' => 1, + 'propertyValues' => ['name' => 'user1.1', 'registered_at' => $registeredAt_1], + // We set the same value, so we do not expect any change here. + 'expect' => ['name' => 'user1.1'], + ], + 'old record 1.2' => [ + 'id' => 1, + 'propertyValues' => ['name' => 'user1.2', 'registered_at' => $registeredAt_1_1], + // We set the same moment in time but with different timezone. It will be detected as a new value. + 'expect' => ['name' => 'user1.2', 'registered_at' => $registeredAt_1_1], + ], + ]; + } + + #[DataProvider('providerForNewValues')] + public function testNewValues(?int $id, array $propertyValues, array $expect): void + { + $customer = $id === null ? new Customer() : Customer::findByPk($id); + array_walk($propertyValues, static fn($value, string $key) => $customer->set($key, $value)); + $this->assertSame($expect, $customer->newValues(array_keys($propertyValues))); + } + public function testGetDirtyValuesOnNewRecord(): void { $this->reloadFixtureAfterTest(); @@ -908,6 +959,7 @@ public function testGetDirtyValuesOnNewRecord(): void 'address' => null, 'status' => 0, 'bool_status' => false, + 'registered_at' => null, 'profile_id' => null, ], $customer->newValues(), @@ -916,6 +968,7 @@ public function testGetDirtyValuesOnNewRecord(): void $customer->set('name', 'Adam'); $customer->set('email', 'adam@example.com'); $customer->set('address', null); + $customer->set('registered_at', ($registeredAt = new DateTimeImmutable('2026-01-01 12:12:12.123456 Europe/Berlin'))); $this->assertSame([], $customer->newValues([])); @@ -926,6 +979,7 @@ public function testGetDirtyValuesOnNewRecord(): void 'address' => null, 'status' => 0, 'bool_status' => false, + 'registered_at' => $registeredAt, 'profile_id' => null, ], $customer->newValues(), diff --git a/tests/ArrayableTraitTest.php b/tests/ArrayableTraitTest.php index 9c201cdb2..a70fd22be 100644 --- a/tests/ArrayableTraitTest.php +++ b/tests/ArrayableTraitTest.php @@ -25,6 +25,7 @@ public function testFields(): void 'address' => 'address', 'status' => 'status', 'bool_status' => 'bool_status', + 'registered_at' => 'registered_at', 'profile_id' => 'profile_id', 'item' => 'item', 'items' => 'items', @@ -46,6 +47,7 @@ public function testToArray(): void 'address' => 'address1', 'status' => 1, 'bool_status' => true, + 'registered_at' => '2011-01-01T01:01:01.111111+01:00', 'profile_id' => 1, ], $customer->toArray(), @@ -65,6 +67,7 @@ public function testToArrayWithClosure(): void 'address' => 'address1', 'status' => 'active', 'bool_status' => true, + 'registered_at' => '2011-01-01T01:01:01.111111+01:00', 'profile_id' => 1, ], $customer->toArray(), diff --git a/tests/Stubs/ActiveRecord/Customer.php b/tests/Stubs/ActiveRecord/Customer.php index 8a12a078a..8aa8436ce 100644 --- a/tests/Stubs/ActiveRecord/Customer.php +++ b/tests/Stubs/ActiveRecord/Customer.php @@ -4,6 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord; +use DateTimeImmutable; use Yiisoft\ActiveRecord\ActiveQuery; use Yiisoft\ActiveRecord\ActiveQueryInterface; use Yiisoft\ActiveRecord\ActiveRecordInterface; @@ -29,6 +30,7 @@ class Customer extends ArrayableActiveRecord protected ?string $address = null; protected ?int $status = 0; protected bool|int|null $bool_status = false; + protected ?DateTimeImmutable $registered_at = null; protected ?int $profile_id = null; public function tableName(): string @@ -36,6 +38,13 @@ public function tableName(): string return 'customer'; } + public function fields(): array + { + return array_merge(parent::fields(), [ + 'registered_at' => static fn(self $customer) => $customer->registered_at?->format('Y-m-d\TH:i:s.uP'), + ]); + } + public function relationQuery(string $name): ActiveQueryInterface { return match ($name) { @@ -88,6 +97,11 @@ public function getBoolStatus(): ?bool return $this->bool_status; } + public function getRegisteredAt(): ?DateTimeImmutable + { + return $this->registered_at; + } + public function getProfileId(): ?int { return $this->profile_id; @@ -127,6 +141,11 @@ public function setBoolStatus(?bool $bool_status): void $this->bool_status = $bool_status; } + public function setRegisteredAt(?DateTimeImmutable $registered_at): void + { + $this->registered_at = $registered_at; + } + public function setProfileId(?int $profile_id): void { $this->set('profile_id', $profile_id); diff --git a/tests/Stubs/ActiveRecord/CustomerClosureField.php b/tests/Stubs/ActiveRecord/CustomerClosureField.php index 4be58c9e6..5f3084077 100644 --- a/tests/Stubs/ActiveRecord/CustomerClosureField.php +++ b/tests/Stubs/ActiveRecord/CustomerClosureField.php @@ -4,6 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord; +use DateTimeImmutable; use Yiisoft\ActiveRecord\Tests\Stubs\ArrayableActiveRecord; /** @@ -17,6 +18,7 @@ final class CustomerClosureField extends ArrayableActiveRecord protected ?string $address = null; protected ?int $status = 0; protected bool|string|null $bool_status = false; + protected ?DateTimeImmutable $registered_at = null; protected ?int $profile_id = null; public function tableName(): string @@ -26,10 +28,12 @@ public function tableName(): string public function fields(): array { - $fields = parent::fields(); - - $fields['status'] = static fn(self $customer) => $customer->status === 1 ? 'active' : 'inactive'; - - return $fields; + return array_merge( + parent::fields(), + [ + 'status' => static fn(self $customer) => $customer->status === 1 ? 'active' : 'inactive', + 'registered_at' => static fn(self $customer) => $customer->registered_at?->format('Y-m-d\TH:i:s.uP'), + ], + ); } } diff --git a/tests/data/sqlite.sql b/tests/data/sqlite.sql index a6f822ae7..9a4e3a0da 100644 --- a/tests/data/sqlite.sql +++ b/tests/data/sqlite.sql @@ -50,6 +50,7 @@ CREATE TABLE "customer" ( address text, status INTEGER DEFAULT 0, bool_status bool DEFAULT FALSE, + registered_at DATETIME DEFAULT NULL, profile_id INTEGER, PRIMARY KEY (id) ); @@ -221,9 +222,9 @@ INSERT INTO "animal" ("type") VALUES ('Yiisoft\ActiveRecord\Tests\Stubs\ActiveRe INSERT INTO "profile" (description) VALUES ('profile customer 1'); INSERT INTO "profile" (description) VALUES ('profile customer 3'); -INSERT INTO "customer" (email, name, address, status, bool_status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, 1, 1); -INSERT INTO "customer" (email, name, address, status, bool_status) VALUES ('user2@example.com', 'user2', 'address2', 1, 1); -INSERT INTO "customer" (email, name, address, status, bool_status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, 0, 2); +INSERT INTO "customer" (email, name, address, status, bool_status, registered_at, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, 1, '2011-01-01 01:01:01.111111+01:00',1); +INSERT INTO "customer" (email, name, address, status, bool_status, registered_at) VALUES ('user2@example.com', 'user2', 'address2', 1, 1,'2022-02-02 02:02:02.222222+02:00'); +INSERT INTO "customer" (email, name, address, status, bool_status, registered_at, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, 0,'2023-03-03 03:03:03.333333+03:00', 2); INSERT INTO "category" (name) VALUES ('Books'); INSERT INTO "category" (name) VALUES ('Movies');