Skip to content

ArrayAccess 接口

概述

ArrayAccess(也称为 ArrayObject 风格接口)允许对象像数组一样使用方括号语法进行访问。实现该接口后,可以通过 $obj['key'] 的方式读写对象的内部数据,使对象具有类似数组的操作体验。

该接口定义了 4 个方法,分别对应数组操作的 4 个基本行为:检查、读取、设置和删除。

版本说明

ArrayAccess 接口从 PHP 5.0.0 起可用。PHP 8.0+ 对参数和返回类型有更严格的建议。PHP 8.1 中方法签名使用 mixed 类型替代了旧的无类型声明。

基础概念

接口定义

php
<?php

declare(strict_types=1);

// ArrayAccess 接口定义
// interface ArrayAccess
// {
//     public function offsetExists(mixed $offset): bool;
//     public function offsetGet(mixed $offset): mixed;
//     public function offsetSet(mixed $offset, mixed $value): void;
//     public function offsetUnset(mixed $offset): void;
// }

四个方法说明

方法对应操作说明
offsetExists($offset)isset($obj[$offset])检查指定偏移是否存在
offsetGet($offset)$obj[$offset]获取指定偏移的值
offsetSet($offset, $value)$obj[$offset] = $value设置指定偏移的值
offsetUnset($offset)unset($obj[$offset])删除指定偏移

命名说明

方法名中的 "offset" 指的是数组的键名(key),而非指针偏移。offsetExists 语义等价于 array_key_existsisset

语法与代码

基本实现

php
<?php

declare(strict_types=1);

class Config implements ArrayAccess
{
    /** @var array<string, mixed> */
    private array $data = [];

    public function __construct(array $defaults = [])
    {
        $this->data = $defaults;
    }

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->data[$offset]);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->data[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $this->data[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->data[$offset]);
    }
}

// 使用方式与数组一致
$config = new Config(['debug' => true, 'timeout' => 30]);
$config['debug'] = false;
echo $config['timeout'];     // 输出: 30
isset($config['debug']);     // bool(true)
unset($config['debug']);

支持追加操作

php
<?php

declare(strict_types=1);

class LogCollection implements ArrayAccess
{
    /** @var array<int, string> */
    private array $logs = [];

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->logs[$offset]);
    }

    public function offsetGet(mixed $offset): string
    {
        if (!$this->offsetExists($offset)) {
            throw new OutOfBoundsException("日志索引 {$offset} 不存在");
        }
        return $this->logs[$offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        // $offset 为 null 时表示追加(如 $logs[] = 'message')
        if ($offset === null) {
            $this->logs[] = $value;
        } else {
            $this->logs[$offset] = $value;
        }
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->logs[$offset]);
    }
}

$logs = new LogCollection();
$logs[] = '用户登录成功';        // 追加操作
$logs[] = '订单已创建';
$logs[10] = '自定义索引日志';
echo $logs[0];                  // 用户登录成功
echo $logs[10];                 // 自定义索引日志

详细说明

与 isset / empty 的交互

php
<?php

declare(strict_types=1);

class StrictArrayAccess implements ArrayAccess
{
    /** @var array<string, mixed> */
    private array $data = [];

    public function offsetExists(mixed $offset): bool
    {
        // isset() 会调用此方法
        // 注意:isset 返回 false 的情况:
        //   1. 键不存在
        //   2. 值为 null
        // 如果需要区分这两种情况,需要额外处理
        return array_key_exists($offset, $this->data);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->data[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $this->data[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->data[$offset]);
    }
}

$obj = new StrictArrayAccess();
$obj['name'] = null;

isset($obj['name']);     // false(值是 null)
isset($obj['missing']);  // false(键不存在)

// 若需区分 null 值和缺失键,使用 array_key_exists
// 或提供独立方法如 has($key)

与 foreach 的配合

php
<?php

declare(strict_types=1);

class DataStore implements ArrayAccess, IteratorAggregate
{
    /** @var array<string, mixed> */
    private array $store = [];

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->store);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->store[$offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $this->store[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->store[$offset]);
    }

    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->store);
    }
}

$store = new DataStore();
$store['app'] = 'MyApp';
$store['version'] = '2.0';

// ArrayAccess 不支持 foreach,需要配合 IteratorAggregate
foreach ($store as $key => $value) {
    echo "{$key}: {$value}\n";
}

ArrayAccess 与 foreach

ArrayAccess 接口本身不支持 foreach 遍历。如果需要同时支持方括号访问和 foreach 遍历,需要同时实现 IteratorAggregate(或 Iterator)接口。

只读实现

php
<?php

declare(strict_types=1);

class ReadOnlyConfig implements ArrayAccess
{
    /** @var array<string, mixed> */
    private array $config;

    public function __construct(array $config)
    {
        $this->config = $config;
    }

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->config);
    }

    public function offsetGet(mixed $offset): mixed
    {
        if (!$this->offsetExists($offset)) {
            throw new OutOfBoundsException("配置项 {$offset} 不存在");
        }
        return $this->config[$offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        throw new RuntimeException('配置为只读,不允许修改');
    }

    public function offsetUnset(mixed $offset): void
    {
        throw new RuntimeException('配置为只读,不允许删除');
    }
}

嵌套 ArrayAccess

php
<?php

declare(strict_types=1);

class NestedData implements ArrayAccess
{
    /** @var array<string, mixed> */
    private array $data = [];

    public function __construct(array $data = [])
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $this->data[$key] = new self($value);
            } else {
                $this->data[$key] = $value;
            }
        }
    }

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->data);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->data[$offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $this->data[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->data[$offset]);
    }
}

$data = new NestedData([
    'database' => [
        'host' => 'localhost',
        'port' => 3306,
    ],
    'cache' => [
        'driver' => 'redis',
        'ttl'    => 3600,
    ],
]);

echo $data['database']['host'];  // localhost
echo $data['cache']['ttl'];      // 3600

实战示例

带类型约束的 Session 包装器

php
<?php

declare(strict_types=1);

class SessionBag implements ArrayAccess
{
    private const ALLOWED_TYPES = ['string', 'int', 'float', 'bool', 'array', 'null'];

    public function __construct(
        private readonly string $namespace = 'app'
    ) {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        if (!isset($_SESSION[$this->namespace])) {
            $_SESSION[$this->namespace] = [];
        }
    }

    public function offsetExists(mixed $offset): bool
    {
        return isset($_SESSION[$this->namespace][$offset]);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $_SESSION[$this->namespace][$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $typeName = gettype($value);
        if (!in_array($typeName, self::ALLOWED_TYPES, true)) {
            throw new InvalidArgumentException("Session 不支持存储 {$typeName} 类型");
        }
        $_SESSION[$this->namespace][$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($_SESSION[$this->namespace][$offset]);
    }

    public function clear(): void
    {
        $_SESSION[$this->namespace] = [];
    }

    public function all(): array
    {
        return $_SESSION[$this->namespace];
    }
}

注意事项

不能直接在 foreach 中使用

php
<?php

declare(strict_types=1);

class OnlyArrayAccess implements ArrayAccess
{
    private array $data = ['a' => 1, 'b' => 2];

    public function offsetExists(mixed $offset): bool { return isset($this->data[$offset]); }
    public function offsetGet(mixed $offset): mixed { return $this->data[$offset] ?? null; }
    public function offsetSet(mixed $offset, mixed $value): void { $this->data[$offset] = $value; }
    public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); }
}

$obj = new OnlyArrayAccess();
foreach ($obj as $key => $value) {
    // 不会遍历自定义数据!
    // 只遍历对象的公开属性,会得到 Notice
}

count() 不兼容

php
<?php

declare(strict_types=1);

class MyArrayAccess implements ArrayAccess
{
    private array $data = [1, 2, 3, 4, 5];

    public function offsetExists(mixed $offset): bool { return isset($this->data[$offset]); }
    public function offsetGet(mixed $offset): mixed { return $this->data[$offset] ?? null; }
    public function offsetSet(mixed $offset, mixed $value): void { $this->data[$offset] = $value; }
    public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); }
}

$obj = new MyArrayAccess();
echo count($obj);  // 输出: 1(对象数量为1,而非数组元素数量)
// 需要同时实现 Countable 接口

isset() 与 null 值

php
<?php

declare(strict_types=1);

// isset() 对 null 值返回 false
// 如果键存在但值为 null,isset 会误判
// 解决方案:offsetExists 使用 array_key_exists
class SmartArrayAccess implements ArrayAccess
{
    private array $data = [];

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists((string) $offset, $this->data);
    }

    public function offsetGet(mixed $offset): mixed
    {
        if (!array_key_exists((string) $offset, $this->data)) {
            throw new OutOfBoundsException("键 {$offset} 不存在");
        }
        return $this->data[(string) $offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $this->data[(string) $offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->data[(string) $offset]);
    }
}

最佳实践

  1. 同时实现 IteratorAggregate 和 Countable:让对象具备完整的数组操作体验
  2. offsetExists 使用 array_key_exists:以正确区分"键不存在"和"值为 null"
  3. offsetGet 对不存在的键抛出异常或返回 null:根据业务需求选择
  4. 提供额外辅助方法:如 has()get()set()remove(),以提供更语义化的 API
  5. 考虑不可变实现:对于配置类等场景,offsetSetoffsetUnset 抛出异常
php
<?php

declare(strict_types=1);

// 完整的最佳实践模板
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
    /** @var array<string|int, mixed> */
    private array $items = [];

    // ArrayAccess
    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->items);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->items[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        if ($offset === null) {
            $this->items[] = $value;
        } else {
            $this->items[$offset] = $value;
        }
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->items[$offset]);
    }

    // Countable
    public function count(int $mode = COUNT_NORMAL): int
    {
        return count($this->items, $mode);
    }

    // IteratorAggregate
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->items);
    }

    // 辅助方法
    public function has(string|int $key): bool
    {
        return array_key_exists($key, $this->items);
    }

    public function get(string|int $key): mixed
    {
        return $this->items[$key] ?? null;
    }
}

参考链接