基于__call魔法方法实现嵌套数组链式调用的复用问题
问题:ArrayCollection实例多次链式调用返回空值的解决办法
我在试验PHP的__call魔法方法,想把嵌套数组转成类似集合的结构,用数组键作为集合方法,链式调用后通过get()返回数组结果。但发现首次调用能得到正确结果,第二次调用因为实例内的$data被修改返回空值,希望实现多次调用都能获取相同结果。
原代码
declare(strict_types=1); class ArrayCollection { private const GET_LAST_ITEM = 'get'; private const SINGLE_KEY = '_'; protected array $data = []; public function __construct(array $data) { $this->data = $data; } /** * @return self|array */ public function __call(string $name, array $args) { if ($name === self::GET_LAST_ITEM) { return array_key_exists(self::SINGLE_KEY, $this->data) ? array_pop($this->data) : $this->data; } $offset = array_shift($args); $current = $this->data[$name] ?? null; $this->data = is_array($current) ? $current[$offset] ?? $current : ( is_string($current) || is_int($current) ? [self::SINGLE_KEY => $current] : $this->data ); return $this; } } $collection = (new ArrayCollection( [ 'a' => [ 'nested' => [ ['item' => 100], ['item' => 200], ['item' => 300], ['children' => [ 'first' => 325, 'last' => [350, 375] ]] ] ], 'b' => [ 'nested' => [ ['item' => 400], ['item' => 500], ], 'children' => [ 'xp' => PHP_INT_MAX ] ] ] )); print_r($collection->b()->nested(1)->item()->get());
问题示例
print_r($collection->b()->children()->xp()->get()); /* Array ( [0] => 350 [1] => 375 ) */ print_r($collection->b()->children()->xp()->get()); /* Array ( ) */
解决方案
核心问题是每次链式调用都直接修改了原实例的$data属性,导致后续调用基于已修改的数据而非初始数据。解决思路是让每次链式调用返回新的ArrayCollection实例,保留原实例的初始数据。
修改后的代码
declare(strict_types=1); class ArrayCollection { private const GET_LAST_ITEM = 'get'; private const SINGLE_KEY = '_'; protected array $data = []; public function __construct(array $data) { $this->data = $data; } /** * @return self|array */ public function __call(string $name, array $args) { if ($name === self::GET_LAST_ITEM) { // 直接读取值,不再修改原数据 return array_key_exists(self::SINGLE_KEY, $this->data) ? $this->data[self::SINGLE_KEY] : $this->data; } $offset = array_shift($args); $current = $this->data[$name] ?? null; // 计算新数据,但不修改当前实例的$data $newData = is_array($current) ? $current[$offset] ?? $current : ( is_string($current) || is_int($current) ? [self::SINGLE_KEY => $current] : $this->data ); // 返回新的ArrayCollection实例,而非当前实例 return new self($newData); } } $collection = (new ArrayCollection( [ 'a' => [ 'nested' => [ ['item' => 100], ['item' => 200], ['item' => 300], ['children' => [ 'first' => 325, 'last' => [350, 375] ]] ] ], 'b' => [ 'nested' => [ ['item' => 400], ['item' => 500], ], 'children' => [ 'xp' => PHP_INT_MAX ] ] ] )); // 测试多次调用 print_r($collection->b()->nested(1)->item()->get()); // 输出500 print_r($collection->b()->children()->xp()->get()); // 输出PHP_INT_MAX print_r($collection->b()->children()->xp()->get()); // 再次调用仍输出PHP_INT_MAX
关键改动说明
- 不修改原实例数据:将原代码中覆盖
$this->data的逻辑改为生成新数据,然后返回新的ArrayCollection实例。这样原实例始终保留初始数据,每次链式调用都是基于前一个实例的数据创建新实例。 - 调整get()方法逻辑:去掉
array_pop()(会修改数组),直接读取对应键的值,保证数据的不可变性。
这样无论调用多少次,每次都是从原实例的初始数据出发,经过链式调用生成新的中间实例,最终都能得到正确结果。
内容的提问来源于stack exchange,提问作者hyperplural
相关产品推荐
相关产品推荐

