La interfaz ArrayAccess de PHP permite que un objeto responda a la sintaxis de corchetes ($obj['clave']) sin ser un array. Es útil para crear clases de configuración, colecciones tipadas o cualquier estructura que quiera ofrecer una API familiar sin exponer el array interno directamente.
Los cuatro métodos de ArrayAccess
ArrayAccess obliga a implementar cuatro métodos que PHP llama internamente cuando se usan los corchetes sobre el objeto: offsetExists(), offsetGet(), offsetSet() y offsetUnset().
<?php
class Mapa implements ArrayAccess
{
private array $datos = [];
// isset($mapa['clave']) o array_key_exists
public function offsetExists(mixed $offset): bool
{
return isset($this->datos[$offset]);
}
// $valor = $mapa['clave']
public function offsetGet(mixed $offset): mixed
{
return $this->datos[$offset] ?? null;
}
// $mapa['clave'] = $valor
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
$this->datos[] = $value; // $mapa[] = 'valor'
} else {
$this->datos[$offset] = $value;
}
}
// unset($mapa['clave'])
public function offsetUnset(mixed $offset): void
{
unset($this->datos[$offset]);
}
}
$mapa = new Mapa();
$mapa['nombre'] = 'Ana';
$mapa['edad'] = 30;
echo $mapa['nombre']; // Ana
echo isset($mapa['edad']) ? 'existe' : 'no existe'; // existe
unset($mapa['edad']);
?>
Clase de configuración con validación
Una de las aplicaciones más prácticas de ArrayAccess es encapsular la configuración de una aplicación. La clase puede validar las claves, lanzar excepciones si se accede a claves inexistentes y cargar valores por defecto.
<?php
class Config implements ArrayAccess
{
private array $datos;
public function __construct(array $datos = [])
{
$this->datos = $datos;
}
public static function desdeArray(array $datos): static
{
return new static($datos);
}
public function offsetExists(mixed $offset): bool
{
return array_key_exists($offset, $this->datos);
}
public function offsetGet(mixed $offset): mixed
{
if (!array_key_exists($offset, $this->datos)) {
throw new InvalidArgumentException("Clave de configuración no encontrada: '$offset'");
}
return $this->datos[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->datos[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->datos[$offset]);
}
// Acceso con valor por defecto
public function get(string $clave, mixed $porDefecto = null): mixed
{
return $this->datos[$clave] ?? $porDefecto;
}
}
$config = Config::desdeArray([
'db_host' => 'localhost',
'db_name' => 'mi_base',
'debug' => false,
]);
echo $config['db_host']; // localhost
echo $config->get('puerto', 3306); // 3306 (valor por defecto)
// echo $config['no_existe']; // lanza InvalidArgumentException
?>
Colección tipada
Una colección tipada usa ArrayAccess junto con validación del tipo del valor en offsetSet(), garantizando que el array interno solo contiene objetos del tipo esperado.
<?php
class ColeccionUsuarios implements ArrayAccess, Countable
{
private array $usuarios = [];
public function offsetExists(mixed $offset): bool
{
return isset($this->usuarios[$offset]);
}
public function offsetGet(mixed $offset): ?Usuario
{
return $this->usuarios[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
if (!$value instanceof Usuario) {
throw new InvalidArgumentException(
'Solo se permiten instancias de Usuario; recibido: ' . get_class($value)
);
}
if ($offset === null) {
$this->usuarios[] = $value;
} else {
$this->usuarios[$offset] = $value;
}
}
public function offsetUnset(mixed $offset): void
{
unset($this->usuarios[$offset]);
}
// Countable: count($coleccion)
public function count(): int
{
return count($this->usuarios);
}
}
$coleccion = new ColeccionUsuarios();
$coleccion[] = new Usuario(1, 'Ana', '[email protected]');
$coleccion[] = new Usuario(2, 'Luis', '[email protected]');
echo count($coleccion); // 2
echo $coleccion[0]->nombre; // Ana
?>
Combinar ArrayAccess, Countable e Iterator
Las tres interfaces se pueden implementar juntas para obtener un objeto que funcione como array en todos los contextos habituales: acceso con corchetes, count() y foreach.
<?php
class Registro implements ArrayAccess, Countable, Iterator
{
private array $datos = [];
private int $posicion = 0;
private array $claves = [];
public function offsetExists(mixed $o): bool { return array_key_exists($o, $this->datos); }
public function offsetGet(mixed $o): mixed { return $this->datos[$o] ?? null; }
public function offsetSet(mixed $o, mixed $v): void {
if ($o === null) { $this->datos[] = $v; } else { $this->datos[$o] = $v; }
$this->claves = array_keys($this->datos);
}
public function offsetUnset(mixed $o): void {
unset($this->datos[$o]);
$this->claves = array_keys($this->datos);
}
public function count(): int { return count($this->datos); }
public function current(): mixed { return $this->datos[$this->claves[$this->posicion]]; }
public function key(): mixed { return $this->claves[$this->posicion]; }
public function next(): void { $this->posicion++; }
public function rewind(): void { $this->posicion = 0; $this->claves = array_keys($this->datos); }
public function valid(): bool { return isset($this->claves[$this->posicion]); }
}
$r = new Registro();
$r['a'] = 1; $r['b'] = 2; $r['c'] = 3;
echo count($r); // 3
foreach ($r as $k => $v) { echo "$k=$v "; } // a=1 b=2 c=3
?>
La documentación oficial de ArrayAccess detalla los tipos exactos de los parámetros según la versión de PHP, el comportamiento con null como offset y las consideraciones de rendimiento frente a arrays nativos.
