diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index b18fe5e097..748dfdba90 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.46 under development ------------------------ +- Bug #19272: Fix bug in dirty attributes check on multidimensional array (speedplli) - Bug #19349: Fix PHP 8.1 error when attribute and label of `yii\grid\DataColumn` are empty (githubjeka) - Bug #19243: Handle `finfo_open` for tar.xz as `application/octet-stream` on PHP 8.1 (longthanhtran) - Bug #19235: Fix return type compatibility of `yii\web\SessionIterator` class methods for PHP 8.1 (virtual-designer) diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 0e2407ca60..a784d18e1a 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -639,7 +639,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface } } else { foreach ($this->_attributes as $name => $value) { - if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) { + if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $this->isAttributeDirty($name, $value))) { $attributes[$name] = $value; } } @@ -1754,4 +1754,20 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface $this->setRelationDependencies($name, $viaQuery, $viaRelationName); } } + + /** + * @param string $attribute + * @param mixed $value + * @return bool + */ + private function isAttributeDirty($attribute, $value) + { + $old_attribute = $this->oldAttributes[$attribute]; + if (is_array($value) && is_array($this->oldAttributes[$attribute])) { + $value = ArrayHelper::recursiveSort($value); + $old_attribute = ArrayHelper::recursiveSort($old_attribute); + } + + return $value !== $old_attribute; + } } diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index 5e0b0d76ab..c6ee50ee83 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -7,9 +7,9 @@ namespace yii\helpers; +use Yii; use ArrayAccess; use Traversable; -use Yii; use yii\base\Arrayable; use yii\base\InvalidArgumentException; @@ -999,4 +999,29 @@ class BaseArrayHelper return $result; } + + /** + * Sorts array recursively. + * + * @param array $array An array passing by reference. + * @param callable|null $sorter The array sorter. If omitted, sort index array by values, sort assoc array by keys. + * @return array + */ + public static function recursiveSort(array &$array, $sorter = null) + { + foreach ($array as &$value) { + if (is_array($value)) { + self::recursiveSort($value, $sorter); + } + } + unset($value); + + if ($sorter === null) { + $sorter = static::isIndexed($array) ? 'sort' : 'ksort'; + } + + call_user_func_array($sorter, [&$array]); + + return $array; + } } diff --git a/tests/framework/helpers/ArrayHelperTest.php b/tests/framework/helpers/ArrayHelperTest.php index 11f6ead433..57ab1bf0fa 100644 --- a/tests/framework/helpers/ArrayHelperTest.php +++ b/tests/framework/helpers/ArrayHelperTest.php @@ -1436,6 +1436,72 @@ class ArrayHelperTest extends TestCase $this->assertEquals(42, ArrayHelper::getValue($model, 'magic')); $this->assertEquals('ta-da', ArrayHelper::getValue($model, 'moreMagic')); } + + /** + * @dataProvider dataProviderRecursiveSort + * + * @return void + */ + public function testRecursiveSort($expected_result, $input_array) + { + $actual = ArrayHelper::recursiveSort($input_array); + $this->assertEquals($expected_result, $actual); + } + + /** + * Data provider for [[testRecursiveSort()]]. + * @return array test data + */ + public function dataProviderRecursiveSort() + { + return [ + //Normal index array + [ + [1, 2, 3, 4], + [4, 1, 3, 2] + ], + //Normal associative array + [ + ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], + ['b' => 2, 'a' => 1, 'd' => 4, 'c' => 3], + ], + //Normal index array + [ + [1, 2, 3, 4], + [4, 1, 3, 2] + ], + //Multidimensional associative array + [ + [ + 'a' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], + 'b' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], + 'c' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], + 'd' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], + ], + [ + 'b' => ['a' => 1, 'd' => 4, 'b' => 2, 'c' => 3], + 'd' => ['b' => 2, 'c' => 3, 'a' => 1, 'd' => 4], + 'c' => ['c' => 3, 'a' => 1, 'd' => 4, 'b' => 2], + 'a' => ['d' => 4, 'b' => 2, 'c' => 3, 'a' => 1], + ], + ], + //Multidimensional associative array + [ + [ + 'a' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]], + 'b' => ['a' => 1, 'b' => 2, 'c' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], 'd' => 4], + 'c' => ['a' => 1, 'b' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], 'c' => 3, 'd' => 4], + 'd' => ['a' => ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], 'b' => 2, 'c' => 3, 'd' => 4], + ], + [ + 'b' => ['a' => 1, 'd' => 4, 'b' => 2, 'c' => ['b' => 2, 'c' => 3, 'a' => 1, 'd' => 4]], + 'd' => ['b' => 2, 'c' => 3, 'a' => ['a' => 1, 'd' => 4, 'b' => 2, 'c' => 3], 'd' => 4], + 'c' => ['c' => 3, 'a' => 1, 'd' => 4, 'b' => ['c' => 3, 'a' => 1, 'd' => 4, 'b' => 2]], + 'a' => ['d' => ['d' => 4, 'b' => 2, 'c' => 3, 'a' => 1], 'b' => 2, 'c' => 3, 'a' => 1], + ] + ], + ]; + } } class Post1