From dd9a6c42267a2671ff2534ff4f3f58475b6995b0 Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Thu, 17 Oct 2024 14:49:18 +0200 Subject: [PATCH 1/9] Optimise ArrayHelper::map + use array_column for faster mapping if no callbacks or grouping are required --- framework/helpers/BaseArrayHelper.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index 56411163e1e..a92aadd9915 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -595,6 +595,9 @@ public static function getColumn($array, $name, $keepKeys = true) */ public static function map($array, $from, $to, $group = null) { + if (is_string($from) && is_string($to) && $group === null) { + return array_column($array, $to, $from); + } $result = []; foreach ($array as $element) { $key = static::getValue($element, $from); From ca4bae91990f40d6ea2c61ceecab285b9392686f Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Thu, 17 Oct 2024 15:12:09 +0200 Subject: [PATCH 2/9] Optimise ArrayHelper::map + changelog --- framework/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 294810f9fc7..1935d097811 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 Change Log - Enh #20247: Support for variadic console controller action methods (brandonkelly) - Bug #20256: Add support for dropping views in MSSQL server when running migrate/fresh (ambrozt) - Enh #20248: Add support for attaching behaviors in configurations with Closure (timkelty) +- Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) 2.0.51 July 18, 2024 -------------------- From facef5a7db58a2879da875f7c091b644c4b8c51f Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Thu, 17 Oct 2024 15:23:09 +0200 Subject: [PATCH 3/9] improve ArrayHelper::map tests + also test with callbacks --- tests/framework/helpers/ArrayHelperTest.php | 40 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/framework/helpers/ArrayHelperTest.php b/tests/framework/helpers/ArrayHelperTest.php index f450b281f7a..a8afe19f6a6 100644 --- a/tests/framework/helpers/ArrayHelperTest.php +++ b/tests/framework/helpers/ArrayHelperTest.php @@ -734,6 +734,44 @@ public function testMap() '345' => 'ccc', ], ], $result); + + $result = ArrayHelper::map($array, + static function (array $group) { + return $group['id'] . $group['name']; + }, + static function (array $group) { + return $group['name'] . $group['class']; + } + ); + + $this->assertEquals([ + '123aaa' => 'aaax', + '124bbb' => 'bbbx', + '345ccc' => 'cccy', + ], $result); + + $result = ArrayHelper::map($array, + static function (array $group) { + return $group['id'] . $group['name']; + }, + static function (array $group) { + return $group['name'] . $group['class']; + }, + static function (array $group) { + return $group['class'] . '-' . $group['class']; + } + ); + + $this->assertEquals([ + 'x-x' => [ + '123aaa' => 'aaax', + '124bbb' => 'bbbx', + ], + 'y-y' => [ + '345ccc' => 'cccy', + ], + ], $result); + } public function testKeyExists() @@ -759,7 +797,7 @@ public function testKeyExistsWithFloat() if (version_compare(PHP_VERSION, '8.1.0', '>=')) { $this->markTestSkipped('Using floats as array key is deprecated.'); } - + $array = [ 1 => 3, 2.2 => 4, // Note: Floats are cast to ints, which means that the fractional part will be truncated. From 1fcadcd28e8439a7b0de9735884bfaeec8b904b2 Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Fri, 18 Oct 2024 09:04:47 +0200 Subject: [PATCH 4/9] fix ArrayHelper::map() for path strings ! don't use array_column if ArrayHelper strings are paths + add according tests + add BaseStringHelper::contains() helper function and according tests --- framework/CHANGELOG.md | 1 + framework/helpers/BaseArrayHelper.php | 2 +- framework/helpers/BaseStringHelper.php | 56 +++++++++++++++----- tests/framework/helpers/ArrayHelperTest.php | 13 +++++ tests/framework/helpers/StringHelperTest.php | 9 ++++ 5 files changed, 66 insertions(+), 15 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 1935d097811..dd4ff449450 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -10,6 +10,7 @@ Yii Framework 2 Change Log - Bug #20256: Add support for dropping views in MSSQL server when running migrate/fresh (ambrozt) - Enh #20248: Add support for attaching behaviors in configurations with Closure (timkelty) - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) +- Enh #20268: Add `\yii\helpers\BaseStringHelper::contains()` as a polyfill for `str_contains` (chriscpty) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index a92aadd9915..334f55112e0 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -595,7 +595,7 @@ public static function getColumn($array, $name, $keepKeys = true) */ public static function map($array, $from, $to, $group = null) { - if (is_string($from) && is_string($to) && $group === null) { + if (is_string($from) && is_string($to) && $group === null && !\yii\helpers\StringHelper::contains($from, '.') && !\yii\helpers\StringHelper::contains($to, '.')) { return array_column($array, $to, $from); } $result = []; diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index ec9252aa4c4..48f23c93ab7 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -226,6 +226,29 @@ protected static function truncateHtml($string, $count, $suffix, $encoding = fal return $generator->generateFromTokens($truncated) . ($totalCount >= $count ? $suffix : ''); } + /** + * @param string $string The input string. + * @param string $needle Part to search inside $string + * @param bool $caseSensitive Case sensitive search. Default is true. When case sensitive is enabled, `$with` must + * exactly match the starting of the string in order to get a true value. + * @return bool + */ + public static function contains($string, $needle, $caseSensitive = true) + { + $string = (string)$string; + $needle = (string)$needle; + + if ($caseSensitive) { + if (function_exists('str_contains')) { + return str_contains($string, $needle); + } + $encoding = Yii::$app ? Yii::$app->charset : 'UTF-8'; + return mb_strpos($string, $needle, 0, $encoding) !== false; + } + $encoding = Yii::$app ? Yii::$app->charset : 'UTF-8'; + return mb_stripos($string, $needle, 0, $encoding) !== false; + } + /** * Check if given string starts with specified substring. Binary and multibyte safe. * @@ -313,9 +336,14 @@ public static function explode($string, $delimiter = ',', $trim = true, $skipEmp } if ($skipEmpty) { // Wrapped with array_values to make array keys sequential after empty values removing - $result = array_values(array_filter($result, function ($value) { - return $value !== ''; - })); + $result = array_values( + array_filter( + $result, + function ($value) { + return $value !== ''; + } + ) + ); } return $result; @@ -343,7 +371,7 @@ public static function countWords($string) */ public static function normalizeNumber($value) { - $value = (string) $value; + $value = (string)$value; $localeInfo = localeconv(); $decimalSeparator = isset($localeInfo['decimal_point']) ? $localeInfo['decimal_point'] : null; @@ -396,7 +424,7 @@ public static function floatToString($number) { // . and , are the only decimal separators known in ICU data, // so its safe to call str_replace here - return str_replace(',', '.', (string) $number); + return str_replace(',', '.', (string)$number); } /** @@ -422,14 +450,14 @@ public static function matchWildcard($pattern, $string, $options = []) $replacements = [ '\\\\\\\\' => '\\\\', - '\\\\\\*' => '[*]', - '\\\\\\?' => '[?]', - '\*' => '.*', - '\?' => '.', - '\[\!' => '[^', - '\[' => '[', - '\]' => ']', - '\-' => '-', + '\\\\\\*' => '[*]', + '\\\\\\?' => '[?]', + '\*' => '.*', + '\?' => '.', + '\[\!' => '[^', + '\[' => '[', + '\]' => ']', + '\-' => '-', ]; if (isset($options['escape']) && !$options['escape']) { @@ -483,7 +511,7 @@ public static function mb_ucfirst($string, $encoding = 'UTF-8') */ public static function mb_ucwords($string, $encoding = 'UTF-8') { - $string = (string) $string; + $string = (string)$string; if (empty($string)) { return $string; } diff --git a/tests/framework/helpers/ArrayHelperTest.php b/tests/framework/helpers/ArrayHelperTest.php index a8afe19f6a6..a086503006b 100644 --- a/tests/framework/helpers/ArrayHelperTest.php +++ b/tests/framework/helpers/ArrayHelperTest.php @@ -772,6 +772,19 @@ static function (array $group) { ], ], $result); + $array = [ + ['id' => '123', 'name' => 'aaa', 'class' => 'x', 'map' => ['a' => '11', 'b' => '22']], + ['id' => '124', 'name' => 'bbb', 'class' => 'x', 'map' => ['a' => '33', 'b' => '44']], + ['id' => '345', 'name' => 'ccc', 'class' => 'y', 'map' => ['a' => '55', 'b' => '66']], + ]; + + $result = ArrayHelper::map($array, 'map.a', 'map.b'); + + $this->assertEquals([ + '11' => '22', + '33' => '44', + '55' => '66' + ], $result); } public function testKeyExists() diff --git a/tests/framework/helpers/StringHelperTest.php b/tests/framework/helpers/StringHelperTest.php index 5f222ec4e7a..49ffee0d876 100644 --- a/tests/framework/helpers/StringHelperTest.php +++ b/tests/framework/helpers/StringHelperTest.php @@ -150,6 +150,15 @@ public function testTruncateWords() $this->assertEquals('This is a test for...', StringHelper::truncateWords('This is a test for a sentance', 5, '...', true)); } + public function testContains() + { + $this->assertEquals(true, StringHelper::contains('Test', 'st')); + $this->assertEquals(false, StringHelper::contains('Test', 'ts')); + $this->assertEquals(false, StringHelper::contains('Test', 'St')); + $this->assertEquals(true, StringHelper::contains('Test', 'St', false)); + $this->assertEquals(false, StringHelper::contains('Test', 'Ste', false)); + } + /** * @dataProvider providerStartsWith * @param bool $result From 75385511351ebaadb408e908ca381eea8543d29f Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Fri, 18 Oct 2024 09:09:34 +0200 Subject: [PATCH 5/9] fix ArrayHelper::map() for path strings o code style fixes --- framework/helpers/BaseStringHelper.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index 48f23c93ab7..bcecca8485b 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -239,11 +239,12 @@ public static function contains($string, $needle, $caseSensitive = true) $needle = (string)$needle; if ($caseSensitive) { + // can be replaced with just the str_contains call when minimum supported PHP version is raised to 8.0 or higher if (function_exists('str_contains')) { return str_contains($string, $needle); } $encoding = Yii::$app ? Yii::$app->charset : 'UTF-8'; - return mb_strpos($string, $needle, 0, $encoding) !== false; + return mb_strpos($string, $needle, 0, $encoding) !== false; } $encoding = Yii::$app ? Yii::$app->charset : 'UTF-8'; return mb_stripos($string, $needle, 0, $encoding) !== false; From 4ed589f7a6b9972a684a1723a0638ff41c661696 Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Tue, 22 Oct 2024 10:35:15 +0200 Subject: [PATCH 6/9] use strpos rather than polyfill ! use strpos rather than polyfill - remove BaseStringHelper::contains --- framework/CHANGELOG.md | 1 - framework/helpers/BaseArrayHelper.php | 2 +- framework/helpers/BaseStringHelper.php | 24 -------------------- tests/framework/helpers/StringHelperTest.php | 9 -------- 4 files changed, 1 insertion(+), 35 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index dd4ff449450..1935d097811 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -10,7 +10,6 @@ Yii Framework 2 Change Log - Bug #20256: Add support for dropping views in MSSQL server when running migrate/fresh (ambrozt) - Enh #20248: Add support for attaching behaviors in configurations with Closure (timkelty) - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) -- Enh #20268: Add `\yii\helpers\BaseStringHelper::contains()` as a polyfill for `str_contains` (chriscpty) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index 334f55112e0..bc770f96cb7 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -595,7 +595,7 @@ public static function getColumn($array, $name, $keepKeys = true) */ public static function map($array, $from, $to, $group = null) { - if (is_string($from) && is_string($to) && $group === null && !\yii\helpers\StringHelper::contains($from, '.') && !\yii\helpers\StringHelper::contains($to, '.')) { + if (is_string($from) && is_string($to) && $group === null && strpos($from, '.') === false && strpos($to, '.') === false) { return array_column($array, $to, $from); } $result = []; diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index bcecca8485b..5854e29766d 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -226,30 +226,6 @@ protected static function truncateHtml($string, $count, $suffix, $encoding = fal return $generator->generateFromTokens($truncated) . ($totalCount >= $count ? $suffix : ''); } - /** - * @param string $string The input string. - * @param string $needle Part to search inside $string - * @param bool $caseSensitive Case sensitive search. Default is true. When case sensitive is enabled, `$with` must - * exactly match the starting of the string in order to get a true value. - * @return bool - */ - public static function contains($string, $needle, $caseSensitive = true) - { - $string = (string)$string; - $needle = (string)$needle; - - if ($caseSensitive) { - // can be replaced with just the str_contains call when minimum supported PHP version is raised to 8.0 or higher - if (function_exists('str_contains')) { - return str_contains($string, $needle); - } - $encoding = Yii::$app ? Yii::$app->charset : 'UTF-8'; - return mb_strpos($string, $needle, 0, $encoding) !== false; - } - $encoding = Yii::$app ? Yii::$app->charset : 'UTF-8'; - return mb_stripos($string, $needle, 0, $encoding) !== false; - } - /** * Check if given string starts with specified substring. Binary and multibyte safe. * diff --git a/tests/framework/helpers/StringHelperTest.php b/tests/framework/helpers/StringHelperTest.php index 49ffee0d876..5f222ec4e7a 100644 --- a/tests/framework/helpers/StringHelperTest.php +++ b/tests/framework/helpers/StringHelperTest.php @@ -150,15 +150,6 @@ public function testTruncateWords() $this->assertEquals('This is a test for...', StringHelper::truncateWords('This is a test for a sentance', 5, '...', true)); } - public function testContains() - { - $this->assertEquals(true, StringHelper::contains('Test', 'st')); - $this->assertEquals(false, StringHelper::contains('Test', 'ts')); - $this->assertEquals(false, StringHelper::contains('Test', 'St')); - $this->assertEquals(true, StringHelper::contains('Test', 'St', false)); - $this->assertEquals(false, StringHelper::contains('Test', 'Ste', false)); - } - /** * @dataProvider providerStartsWith * @param bool $result From 2c6f34d146b361e77011bf714688344bdf2363d4 Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Fri, 6 Dec 2024 14:40:59 +0100 Subject: [PATCH 7/9] refresh relations with BC break + implementation with BC break --- framework/db/ActiveRecord.php | 5 +++-- framework/db/BaseActiveRecord.php | 4 ++-- tests/framework/ar/ActiveRecordTestTrait.php | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 130285617ed..a8caacf7b5d 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -281,7 +281,7 @@ protected static function filterValidColumnNames($db, array $aliases) /** * {@inheritdoc} */ - public function refresh() + public function refresh(array $with = []) { $query = static::find(); $tableName = key($query->getTablesUsedInFrom()); @@ -290,7 +290,8 @@ public function refresh() foreach ($this->getPrimaryKey(true) as $key => $value) { $pk[$tableName . '.' . $key] = $value; } - $query->where($pk); + $query->where($pk) + ->with(...$with); /* @var $record BaseActiveRecord */ $record = $query->noCache()->one(); diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 10021e362cc..42480ef176b 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -1082,8 +1082,8 @@ protected function refreshInternal($record) $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null; } $this->_oldAttributes = $record->_oldAttributes; - $this->_related = []; - $this->_relationsDependencies = []; + $this->_related = $record->_related; + $this->_relationsDependencies = $record->_relationsDependencies; $this->afterRefresh(); return true; diff --git a/tests/framework/ar/ActiveRecordTestTrait.php b/tests/framework/ar/ActiveRecordTestTrait.php index 3bb161f7f8f..fc0b8ef83c8 100644 --- a/tests/framework/ar/ActiveRecordTestTrait.php +++ b/tests/framework/ar/ActiveRecordTestTrait.php @@ -288,6 +288,10 @@ public function testRefresh() $customer->name = 'to be refreshed'; $this->assertTrue($customer->refresh()); $this->assertEquals('user1', $customer->name); + $this->assertFalse($customer->isRelationPopulated('profile')); + + $this->assertTrue($customer->refresh(['profile'])); + $this->assertTrue($customer->isRelationPopulated('profile')); } public function testEquals() From 0058f0e3254a52fd3fa42706414f004a164faeea Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Fri, 6 Dec 2024 14:49:26 +0100 Subject: [PATCH 8/9] refresh relations without BC break + BC safe implementation --- framework/db/ActiveRecord.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index a8caacf7b5d..d591d38388a 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -280,8 +280,11 @@ protected static function filterValidColumnNames($db, array $aliases) /** * {@inheritdoc} + * + * @param array $with If an array is passed to this function, + * it will be interpreted as an array of relations to eagerly load for the refreshed record. */ - public function refresh(array $with = []) + public function refresh() { $query = static::find(); $tableName = key($query->getTablesUsedInFrom()); @@ -290,8 +293,11 @@ public function refresh(array $with = []) foreach ($this->getPrimaryKey(true) as $key => $value) { $pk[$tableName . '.' . $key] = $value; } - $query->where($pk) - ->with(...$with); + $args = func_get_args(); + if (array_key_exists(0, $args) && is_array($args[0])) { + $query->with(...$args[0]); + } + $query->where($pk); /* @var $record BaseActiveRecord */ $record = $query->noCache()->one(); From a92c9b360508324fbed4e7260cc1fd1f9fd116c2 Mon Sep 17 00:00:00 2001 From: Chris Reichel Date: Fri, 6 Dec 2024 14:52:42 +0100 Subject: [PATCH 9/9] refresh relations with BC break + changelog --- framework/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 1935d097811..3111daa5db1 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -10,6 +10,7 @@ Yii Framework 2 Change Log - Bug #20256: Add support for dropping views in MSSQL server when running migrate/fresh (ambrozt) - Enh #20248: Add support for attaching behaviors in configurations with Closure (timkelty) - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) +- Enh #20277: Eager loading in `\yii\db\ActiveRecord::refresh` (chriscpty) 2.0.51 July 18, 2024 --------------------