active-record icon indicating copy to clipboard operation
active-record copied to clipboard

`AbstractActiveRecord::newValues()` incorrectly marks DateTimeInterface attribute as changed

Open darealfive opened this issue 1 month ago • 11 comments

Description

The newValues() method fails to correctly detect changes for DateTimeType related types, e.g. DateTimeInterface respectively DateTimeImmutable.

Specifically:

  • DateTimeImmutable / DateTimeInterface values are compared using strict comparison (!==), causing two instances representing the same moment to be always treated as changed (incorrectly), because two objects are never object-equal.

This affects Active Record dirty-checking and can cause unnecessary record updates and therefore subsequent problems.


Steps to Reproduce

  1. Load an Active Record model from the database with at least one DateTime column and make sure it gets mapped to DateTimeImmutable.
  2. ...either assign a new DateTimeImmutable instance with the same date/time value. ... or leave the value as is.
  3. Call newValues() or attempt to save the model.Expected: The attribute should not be marked as dirty.

Actual: The attribute is considered as changed (dirty) incorrectly.


Expected Behavior

  • DateTimeInterface values should be compared using loose comparison (==) to compare actual date/time values rather than object identity.

Proposed Fix

/**
 * Returns the property values that have been modified since they're loaded or saved most recently.
 *
 * The comparison of new DateTimeInterface objects with old values uses `==`.
 * The comparison of all other values uses `===`.
 *
 * @param array|null $propertyNames The names of the properties whose values may be returned if they're changed
 *                                  recently. If `null`, {@see propertyNames()} will be used.
 *
 * @return array The changed property values (name-value pairs).
 *
 * @psalm-return array<string, mixed>
 */
#[Override]
public function newValues(array|null $propertyNames = null): array
{
    $values    = $this->propertyValues($propertyNames);
    $oldValues = $this->oldValues();
    if ($oldValues === []) {
        return $values;
    }

    $result = array_diff_key($values, $oldValues);
    foreach (array_diff_key($values, $result) as $name => $value) {
        if ($value instanceof DateTimeInterface) {
            if ($value != $oldValues[$name]) {
                $result[$name] = $value;
            }
        } else {
            if ($value !== $oldValues[$name]) {
                $result[$name] = $value;
            }
        }
    }

    return $result;
}

Notes

I think this behavior avoids false positives when using immutable date/time objects. You may update \Yiisoft\ActiveRecord\ActiveRecordInterface::newValues documentation with the provided one and leave the actual implementation documentation empty (as it is currently).

Package version

1.0.0

PHP version

PHP 8.4.15 (cli)

darealfive avatar Dec 18 '25 11:12 darealfive