Skip to content

AbstractActiveRecord::newValues() incorrectly marks DateTimeInterface attribute as changed #522

@darealfive

Description

@darealfive

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions