Интерфейсы и снимок перебора
ArrayAccess делает синтаксис удобнее, но не заменяет документированный контракт. Запись через квадратные скобки работает как у массива; строгий add остаётся способом запретить дубликаты. Countable возвращает число элементов, включая null. IteratorAggregate отдаёт отдельный снимок структуры.
public function offsetExists(mixed $offset): bool
{
return $this->has($offset) && $this->get($offset) !== null;
}
public function offsetGet(mixed $offset): mixed
{
return $this->get($offset);
}
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
$this->append($value);
} elseif ($this->has($offset)) {
$this->replace($value, $offset);
} else {
$this->add($value, $offset);
}
}
public function offsetUnset(mixed $offset): void
{
$this->remove($offset);
}
public function count(): int
{
return count($this->items);
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}Поверхностная копия: что именно независимо
Удаление ключа или добавление нового элемента в коллекцию не меняет ранее созданный итератор. Но Client — изменяемый объект, и ссылка на него общая. clone коллекции тоже не клонирует вложенные объекты: если нужна независимость их полей, потребуется отдельная политика глубокого копирования.
$copy = clone $clients;
$copy->add(new Client('Саша'), 'alexander'); // Новый ключ только в копии.
$copy['ivan']->name = 'Иван'; // Тот же Client меняется и в оригинале.Проверки границ
Тесты отдельно проверяют 0/"0", пустой ключ, null-значение, отклонение null-ключа, дубликаты, вставку/замену, порядок перебора, count, неверные типы ключей и поверхностное клонирование. empty не используется как проверка наличия.