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_exists 或 isset。
语法与代码
基本实现
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]);
}
}最佳实践
- 同时实现 IteratorAggregate 和 Countable:让对象具备完整的数组操作体验
- offsetExists 使用 array_key_exists:以正确区分"键不存在"和"值为 null"
- offsetGet 对不存在的键抛出异常或返回 null:根据业务需求选择
- 提供额外辅助方法:如
has()、get()、set()、remove(),以提供更语义化的 API - 考虑不可变实现:对于配置类等场景,
offsetSet和offsetUnset抛出异常
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;
}
}