Ключи, значения и проверяемый контракт коллекции
Разделяем вставку под явным ключом, добавление в конец и замену. Нулевой ключ не означает отсутствие ключа; сохранённый null не означает отсутствие записи. Эти различия важны для клиентских идентификаторов и необязательных полей.
/**
* A small array-backed collection for PHP 8.3+.
* Keys follow PHP array rules: integer 0 and string '0' name the same entry.
* Explicit keys must be int|string; null is reserved for [] append syntax.
* Iteration and clone copy the container shallowly; object values stay shared.
* @implements ArrayAccess<int|string, mixed>
* @implements IteratorAggregate<int|string, mixed>
*/
class ObjectsArr implements ArrayAccess, Countable, IteratorAggregate
{
/** @var array<int|string, mixed> */
private array $items = [];
protected function validateValue(mixed $value): void
{
// Subclasses may enforce a value policy without narrowing public APIs.
}
private function checkedKey(mixed $key): int|string
{
if (!is_int($key) && !is_string($key)) {
throw new InvalidArgumentException('Key must be an integer or a string.');
}
return $key;
}
public function has(mixed $key): bool
{
return array_key_exists($this->checkedKey($key), $this->items);
}
public function get(mixed $key): mixed
{
$key = $this->checkedKey($key);
if (!array_key_exists($key, $this->items)) {
throw new OutOfBoundsException('No entry for this key.');
}
return $this->items[$key];
}
/** Add an explicit key. Duplicates are errors, including null-valued entries. */
public function add(mixed $value, mixed $key): void
{
$key = $this->checkedKey($key);
if (array_key_exists($key, $this->items)) {
throw new LogicException('Key is already in use.');
}
$this->validateValue($value);
$this->items[$key] = $value;
}
/** Append under PHP's next integer key, returning the assigned key. */
public function append(mixed $value): int
{
$this->validateValue($value);
$this->items[] = $value;
return array_key_last($this->items);
}
/** Replace an existing value; a missing key is an error. */
public function replace(mixed $value, mixed $key): void
{
$key = $this->checkedKey($key);
if (!array_key_exists($key, $this->items)) {
throw new OutOfBoundsException('No entry to replace.');
}
$this->validateValue($value);
$this->items[$key] = $value;
}
public function remove(mixed $key): void
{
$key = $this->checkedKey($key);
if (!array_key_exists($key, $this->items)) {
throw new OutOfBoundsException('No entry to remove.');
}
unset($this->items[$key]);
}
/** @return list<int|string> */
public function keys(): array
{
return array_keys($this->items);
}
public function count(): int
{
return count($this->items);
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
/** Match isset(array[key]): null is a stored value, but is not "set". */
public function offsetExists(mixed $offset): bool
{
return $this->has($offset) && $this->get($offset) !== null;
}
public function offsetGet(mixed $offset): mixed
{
return $this->get($offset);
}
/** [] appends; [key] assignment inserts or replaces, like an ordinary array. */
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);
}
}Только клиенты, без нарушения сигнатур
Фильтр значения вызывается базовым классом до записи. Публичные сигнатуры остаются одинаковыми у родителя и наследника; конструктор и строковое представление Client определены явно.
final class Client
{
public function __construct(public string $name)
{
}
public function __toString(): string
{
return 'Клиент — "' . $this->name . '"';
}
}
final class Clients extends ObjectsArr
{
protected function validateValue(mixed $value): void
{
if (!$value instanceof Client) {
throw new InvalidArgumentException('Clients accepts only Client objects.');
}
}
}
$clients = new Clients();
$clients->add(new Client('Ваня'), 'ivan');
$clients->add(new Client('Саша'), 'alexander');
$clients[] = new Client('Петя');
echo $clients->get('ivan'), PHP_EOL;
$clients['ivan'] = new Client('Иван Иванович'); // Explicit replacement via [].
foreach ($clients as $key => $client) {
echo $key, ' -> ', $client, PHP_EOL;
}
echo 'Всего: ', count($clients), PHP_EOL;
$clients->remove('alexander');
try {
$clients->add(new Client('Другой Иван'), 'ivan');
} catch (LogicException $error) {
echo 'Повторный add отклонён: ', $error->getMessage(), PHP_EOL;
}