diff --git a/src/AbstractActiveRecord.php b/src/AbstractActiveRecord.php index 701780b4d..1afdc90ba 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,26 @@ 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 + || $this->column($name)->dbTypecast($newValue) != + $this->column($name)->dbTypecast($oldValues[$name])) { + $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..efb529183 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 UTC'), 'profile_id' => null, ], $customer); diff --git a/tests/ActiveQueryTest.php b/tests/ActiveQueryTest.php index 4298019d0..2497db507 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 UTC'), '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 UTC'), '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 UTC'), $customer->get('registered_at')); $this->assertFalse($customer->isNew()); $this->assertEmpty($customer->newValues()); diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 75e0a6b91..2854e2b14 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+00:00'); + $registeredAt_1_1 = new DateTimeImmutable('2022-02-12T12:12:12.121212+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..2fe9514d5 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+00: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+00: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/mssql.sql b/tests/data/mssql.sql index 707c04e25..2915dd91d 100644 --- a/tests/data/mssql.sql +++ b/tests/data/mssql.sql @@ -51,6 +51,7 @@ CREATE TABLE [dbo].[customer] ( [address] [text], [status] [int] DEFAULT 0, [bool_status] [bit] DEFAULT 0, + [registered_at] DATETIME2(6) DEFAULT NULL, [profile_id] [int], CONSTRAINT [PK_customer] PRIMARY KEY CLUSTERED ( [id] ASC @@ -243,9 +244,9 @@ INSERT INTO [dbo].[animal] (type) VALUES ('Yiisoft\ActiveRecord\Tests\Stubs\Acti INSERT INTO [dbo].[profile] ([description]) VALUES ('profile customer 1'); INSERT INTO [dbo].[profile] ([description]) VALUES ('profile customer 3'); -INSERT INTO [dbo].[customer] ([email], [name], [address], [status], [bool_status], [profile_id]) VALUES ('user1@example.com', 'user1', 'address1', 1, 1, 1); -INSERT INTO [dbo].[customer] ([email], [name], [address], [status], [bool_status]) VALUES ('user2@example.com', 'user2', 'address2', 1, 1); -INSERT INTO [dbo].[customer] ([email], [name], [address], [status], [bool_status], [profile_id]) VALUES ('user3@example.com', 'user3', 'address3', 2, 0, 2); +INSERT INTO [dbo].[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+00:00', 1); +INSERT INTO [dbo].[customer] ([email], [name], [address], [status], [bool_status], [registered_at]) VALUES ('user2@example.com', 'user2', 'address2', 1, 1, '2022-02-02 02:02:02.222222+00:00'); +INSERT INTO [dbo].[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+00:00', 2); INSERT INTO [dbo].[category] ([name]) VALUES ('Books'); INSERT INTO [dbo].[category] ([name]) VALUES ('Movies'); diff --git a/tests/data/mysql.sql b/tests/data/mysql.sql index e1ece6abf..a9028bded 100644 --- a/tests/data/mysql.sql +++ b/tests/data/mysql.sql @@ -62,6 +62,7 @@ CREATE TABLE `customer` ( `address` text, `status` int (11) DEFAULT 0, `bool_status` bit(1) DEFAULT 0, + `registered_at` DATETIME(6) DEFAULT NULL, `profile_id` int(11), PRIMARY KEY (`id`), CONSTRAINT `FK_customer_profile_id` FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`) @@ -269,9 +270,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+00: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+00: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+00:00', 2); INSERT INTO `category` (name) VALUES ('Books'); INSERT INTO `category` (name) VALUES ('Movies'); diff --git a/tests/data/pgsql.sql b/tests/data/pgsql.sql index bbb18fcc0..a17e87247 100644 --- a/tests/data/pgsql.sql +++ b/tests/data/pgsql.sql @@ -74,6 +74,7 @@ CREATE TABLE "customer" ( address text, status integer DEFAULT 0, bool_status boolean DEFAULT FALSE, + registered_at TIMESTAMP(6) WITHOUT TIME ZONE DEFAULT NULL, profile_id integer ); @@ -281,9 +282,9 @@ INSERT INTO "profile" (description) VALUES ('profile customer 3'); INSERT INTO "schema1"."profile" (description) VALUES ('profile customer 1'); INSERT INTO "schema1"."profile" (description) VALUES ('profile customer 3'); -INSERT INTO "customer" (email, name, address, status, bool_status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, true, 1); -INSERT INTO "customer" (email, name, address, status, bool_status) VALUES ('user2@example.com', 'user2', 'address2', 1, true); -INSERT INTO "customer" (email, name, address, status, bool_status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, false, 2); +INSERT INTO "customer" (email, name, address, status, bool_status, registered_at, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, true, '2011-01-01 01:01:01.111111', 1); +INSERT INTO "customer" (email, name, address, status, bool_status, registered_at) VALUES ('user2@example.com', 'user2', 'address2', 1, true, '2022-02-02 02:02:02.222222'); +INSERT INTO "customer" (email, name, address, status, bool_status, registered_at, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, false, '2023-03-03 03:03:03.333333', 2); INSERT INTO "category" (name) VALUES ('Books'); INSERT INTO "category" (name) VALUES ('Movies'); diff --git a/tests/data/sqlite.sql b/tests/data/sqlite.sql index a6f822ae7..9a95cb487 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+00: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+00: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+00:00', 2); INSERT INTO "category" (name) VALUES ('Books'); INSERT INTO "category" (name) VALUES ('Movies');