Fix #18026: Fix ArrayHelper::getValue() did not work with ArrayAccess objects

This commit is contained in:
Mikk Tendermann
2020-05-18 15:18:09 +03:00
committed by GitHub
parent 3b87c5f31a
commit 35fb9c6248
3 changed files with 191 additions and 17 deletions

View File

@ -6,6 +6,7 @@ Yii Framework 2 Change Log
- Bug #18028: Fix division by zero exception in Table.php::calculateRowHeight (fourhundredfour) - Bug #18028: Fix division by zero exception in Table.php::calculateRowHeight (fourhundredfour)
- Enh #18019: Allow jQuery 3.5.0 to be installed (wouter90) - Enh #18019: Allow jQuery 3.5.0 to be installed (wouter90)
- Bug #18026: Fix `ArrayHelper::getValue()` did not work with `ArrayAccess` objects (mikk150)
2.0.35 May 02, 2020 2.0.35 May 02, 2020

View File

@ -7,6 +7,8 @@
namespace yii\helpers; namespace yii\helpers;
use ArrayAccess;
use Traversable;
use Yii; use Yii;
use yii\base\Arrayable; use yii\base\Arrayable;
use yii\base\InvalidArgumentException; use yii\base\InvalidArgumentException;
@ -194,7 +196,7 @@ class BaseArrayHelper
$key = $lastKey; $key = $lastKey;
} }
if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { if (static::keyExists($key, $array)) {
return $array[$key]; return $array[$key];
} }
@ -203,12 +205,13 @@ class BaseArrayHelper
$key = substr($key, $pos + 1); $key = substr($key, $pos + 1);
} }
if (static::isArrayAccess($array)) {
return static::keyExists($key, $array) ? $array[$key] : $default;
}
if (is_object($array)) { if (is_object($array)) {
// this is expected to fail if the property does not exist, or __get() is not implemented // this is expected to fail if the property does not exist, or __get() is not implemented
// it is not reliably possible to check whether a property is accessible beforehand // it is not reliably possible to check whether a property is accessible beforehand
return $array->$key; return $array->$key;
} elseif (is_array($array)) {
return (isset($array[$key]) || array_key_exists($key, $array)) ? $array[$key] : $default;
} }
return $default; return $default;
@ -593,7 +596,7 @@ class BaseArrayHelper
* This method enhances the `array_key_exists()` function by supporting case-insensitive * This method enhances the `array_key_exists()` function by supporting case-insensitive
* key comparison. * key comparison.
* @param string $key the key to check * @param string $key the key to check
* @param array $array the array with keys to check * @param array|ArrayAccess $array the array with keys to check
* @param bool $caseSensitive whether the key comparison should be case-sensitive * @param bool $caseSensitive whether the key comparison should be case-sensitive
* @return bool whether the array contains the specified key * @return bool whether the array contains the specified key
*/ */
@ -602,7 +605,15 @@ class BaseArrayHelper
if ($caseSensitive) { if ($caseSensitive) {
// Function `isset` checks key faster but skips `null`, `array_key_exists` handles this case // Function `isset` checks key faster but skips `null`, `array_key_exists` handles this case
// https://secure.php.net/manual/en/function.array-key-exists.php#107786 // https://secure.php.net/manual/en/function.array-key-exists.php#107786
return isset($array[$key]) || array_key_exists($key, $array); if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) {
return true;
}
// Cannot use `array_has_key` on Objects for PHP 7.4+, therefore we need to check using [[ArrayAccess::offsetExists()]]
return $array instanceof ArrayAccess && $array->offsetExists($key);
}
if ($array instanceof ArrayAccess) {
throw new InvalidArgumentException('Second parameter($array) cannot be ArrayAccess in case insensitive mode');
} }
foreach (array_keys($array) as $k) { foreach (array_keys($array) as $k) {
@ -805,12 +816,12 @@ class BaseArrayHelper
} }
/** /**
* Check whether an array or [[\Traversable]] contains an element. * Check whether an array or [[Traversable]] contains an element.
* *
* This method does the same as the PHP function [in_array()](https://secure.php.net/manual/en/function.in-array.php) * This method does the same as the PHP function [in_array()](https://secure.php.net/manual/en/function.in-array.php)
* but additionally works for objects that implement the [[\Traversable]] interface. * but additionally works for objects that implement the [[Traversable]] interface.
* @param mixed $needle The value to look for. * @param mixed $needle The value to look for.
* @param array|\Traversable $haystack The set of values to search. * @param array|Traversable $haystack The set of values to search.
* @param bool $strict Whether to enable strict (`===`) comparison. * @param bool $strict Whether to enable strict (`===`) comparison.
* @return bool `true` if `$needle` was found in `$haystack`, `false` otherwise. * @return bool `true` if `$needle` was found in `$haystack`, `false` otherwise.
* @throws InvalidArgumentException if `$haystack` is neither traversable nor an array. * @throws InvalidArgumentException if `$haystack` is neither traversable nor an array.
@ -819,7 +830,7 @@ class BaseArrayHelper
*/ */
public static function isIn($needle, $haystack, $strict = false) public static function isIn($needle, $haystack, $strict = false)
{ {
if ($haystack instanceof \Traversable) { if ($haystack instanceof Traversable) {
foreach ($haystack as $value) { foreach ($haystack as $value) {
if ($needle == $value && (!$strict || $needle === $value)) { if ($needle == $value && (!$strict || $needle === $value)) {
return true; return true;
@ -835,27 +846,43 @@ class BaseArrayHelper
} }
/** /**
* Checks whether a variable is an array or [[\Traversable]]. * Checks whether a variable is an array or [[Traversable]].
* *
* This method does the same as the PHP function [is_array()](https://secure.php.net/manual/en/function.is-array.php) * This method does the same as the PHP function [is_array()](https://secure.php.net/manual/en/function.is-array.php)
* but additionally works on objects that implement the [[\Traversable]] interface. * but additionally works on objects that implement the [[Traversable]] interface.
* @param mixed $var The variable being evaluated. * @param mixed $var The variable being evaluated.
* @return bool whether $var is array-like * @return bool whether $var can be traversed via foreach
* @see https://secure.php.net/manual/en/function.is-array.php * @see https://secure.php.net/manual/en/function.is-array.php
* @since 2.0.8 * @since 2.0.8
*/ */
public static function isTraversable($var) public static function isTraversable($var)
{ {
return is_array($var) || $var instanceof \Traversable; return is_array($var) || $var instanceof Traversable;
} }
/** /**
* Checks whether an array or [[\Traversable]] is a subset of another array or [[\Traversable]]. * Checks whether a variable is an array or [[\ArrayAccess]].
*
* This method does the same as the PHP function [is_array()](https://secure.php.net/manual/en/function.is-array.php)
* but additionally works on objects that implement the [[\ArrayAccess]] interface.
* @param mixed $var The variable being evaluated.
* @return bool whether data on $var can be accessed as arrays
* @see https://secure.php.net/manual/en/function.is-array.php
* @since 2.0.36
*/
public static function isArrayAccess($var)
{
return is_array($var) || $var instanceof \ArrayAccess;
}
/**
* Checks whether an array or [[Traversable]] is a subset of another array or [[Traversable]].
* *
* This method will return `true`, if all elements of `$needles` are contained in * This method will return `true`, if all elements of `$needles` are contained in
* `$haystack`. If at least one element is missing, `false` will be returned. * `$haystack`. If at least one element is missing, `false` will be returned.
* @param array|\Traversable $needles The values that must **all** be in `$haystack`. * @param array|Traversable $needles The values that must **all** be in `$haystack`.
* @param array|\Traversable $haystack The set of value to search. * @param array|Traversable $haystack The set of value to search.
* @param bool $strict Whether to enable strict (`===`) comparison. * @param bool $strict Whether to enable strict (`===`) comparison.
* @throws InvalidArgumentException if `$haystack` or `$needles` is neither traversable nor an array. * @throws InvalidArgumentException if `$haystack` or `$needles` is neither traversable nor an array.
* @return bool `true` if `$needles` is a subset of `$haystack`, `false` otherwise. * @return bool `true` if `$needles` is a subset of `$haystack`, `false` otherwise.
@ -863,7 +890,7 @@ class BaseArrayHelper
*/ */
public static function isSubset($needles, $haystack, $strict = false) public static function isSubset($needles, $haystack, $strict = false)
{ {
if (is_array($needles) || $needles instanceof \Traversable) { if (is_array($needles) || $needles instanceof Traversable) {
foreach ($needles as $needle) { foreach ($needles as $needle) {
if (!static::isIn($needle, $haystack, $strict)) { if (!static::isIn($needle, $haystack, $strict)) {
return false; return false;

View File

@ -7,6 +7,8 @@
namespace yiiunit\framework\helpers; namespace yiiunit\framework\helpers;
use ArrayAccess;
use Iterator;
use yii\base\BaseObject; use yii\base\BaseObject;
use yii\data\Sort; use yii\data\Sort;
use yii\helpers\ArrayHelper; use yii\helpers\ArrayHelper;
@ -41,6 +43,84 @@ class Post3 extends BaseObject
} }
} }
class ArrayAccessibleObject implements ArrayAccess
{
protected $container = [];
public function __construct($container)
{
$this->container = $container;
}
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->container[] = $value;
} else {
$this->container[$offset] = $value;
}
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->container);
}
public function offsetUnset($offset)
{
unset($this->container[$offset]);
}
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->container[$offset] : null;
}
}
class TraversableArrayAccessibleObject extends ArrayAccessibleObject implements Iterator
{
private $position = 0;
public function __construct($container)
{
$this->position = 0;
parent::__construct($container);
}
protected function getContainerKey($keyIndex)
{
$keys = array_keys($this->container);
return array_key_exists($keyIndex, $keys) ? $keys[$keyIndex] : false;
}
public function rewind()
{
$this->position = 0;
}
public function current()
{
return $this->offsetGet($this->getContainerKey($this->position));
}
public function key()
{
return $this->getContainerKey($this->position);
}
public function next()
{
++$this->position;
}
public function valid()
{
$key = $this->getContainerKey($this->position);
return !(!$key || !$this->offsetExists($key));
}
}
/** /**
* @group helpers * @group helpers
*/ */
@ -734,6 +814,33 @@ class ArrayHelperTest extends TestCase
$this->assertFalse(ArrayHelper::keyExists('c', $array, false)); $this->assertFalse(ArrayHelper::keyExists('c', $array, false));
} }
public function testKeyExistsArrayAccess()
{
$array = new TraversableArrayAccessibleObject([
'a' => 1,
'B' => 2,
]);
$this->assertTrue(ArrayHelper::keyExists('a', $array));
$this->assertFalse(ArrayHelper::keyExists('b', $array));
$this->assertTrue(ArrayHelper::keyExists('B', $array));
$this->assertFalse(ArrayHelper::keyExists('c', $array));
}
/**
* @expectedException \yii\base\InvalidArgumentException
* @expectedExceptionMessage Second parameter($array) cannot be ArrayAccess in case insensitive mode
*/
public function testKeyExistsArrayAccessCaseInsensitiveThrowsError()
{
$array = new TraversableArrayAccessibleObject([
'a' => 1,
'B' => 2,
]);
ArrayHelper::keyExists('a', $array, false);
}
public function valueProvider() public function valueProvider()
{ {
return [ return [
@ -830,6 +937,45 @@ class ArrayHelperTest extends TestCase
$this->assertEquals(23, ArrayHelper::getValue($arrayObject, 'nonExisting')); $this->assertEquals(23, ArrayHelper::getValue($arrayObject, 'nonExisting'));
} }
public function testGetValueFromArrayAccess()
{
$arrayAccessibleObject = new ArrayAccessibleObject([
'one' => 1,
'two' => 2,
'three' => 3,
'key.with.dot' => 'dot',
'null' => null,
]);
$this->assertEquals(1, ArrayHelper::getValue($arrayAccessibleObject, 'one'));
}
public function testGetValueWithDotsFromArrayAccess()
{
$arrayAccessibleObject = new ArrayAccessibleObject([
'one' => 1,
'two' => 2,
'three' => 3,
'key.with.dot' => 'dot',
'null' => null,
]);
$this->assertEquals('dot', ArrayHelper::getValue($arrayAccessibleObject, 'key.with.dot'));
}
public function testGetValueNonexistingArrayAccess()
{
$arrayAccessibleObject = new ArrayAccessibleObject([
'one' => 1,
'two' => 2,
'three' => 3,
'key.with.dot' => 'dot',
'null' => null,
]);
$this->assertEquals(null, ArrayHelper::getValue($arrayAccessibleObject, 'four'));
}
/** /**
* Data provider for [[testSetValue()]]. * Data provider for [[testSetValue()]].
* @return array test data * @return array test data