Коллекции объектов в PHP

Проблемы с массивами

В PHP массивы - это очень гибкая структура данных, которая принимает практически все и может быть объединена любым способом. Это хорошо!

В отличие от других языков PHP позволяет комбинировать ассоциативные и числовые массивы, предоставляя большую свободу. Но бывают случаи, когда просто необходима безопасность типов.

Одним из способов решения этой проблемы, безусловно, была бы проверка каждого элемента массива при его использовании, что позволило бы создать несколько фрагментов кода, близких к этому:

$array = [1,2,3,4,5]

foreach ($array as $item) {
    if (!is_int($item)) {
        throw new \Exception('wrong type');
    }

    // ваш код
}

Хотя это, очевидно, работает, это немного странно:

  • Для того, чтобы разложить на множители все вхождения, нужна какая-то глобально доступная функция
  • На самом деле, это неподходящее место для проверки. В идеале нужно проверять тип значения при создании массива и не беспокоиться о его использовании.

Решение: коллекции

Коллекция - это класс, который абстрагирует массив. Базовый класс коллекции будет выглядеть следующим образом:

class IntCollection {
    private $values = [];

    // указание типа гарантирует, что не будет никаких float значений
    public function addValue (int $val, $key = null): void
    {
        if ($key === null) {
            $this->values[] = $val;
        } else {
            $this->values[$key] = $val;
        }
    }

    public function deleteValue($key): void
    {
        unset($this->values[$key]);
    }

    public function get($key): int
    {
        return $this->values[$key];
    }
}

Это уже довольно круто, но недостаточно! Это нельзя использовать как массив и означает, например, что:

  • если провести рефакторинг, скорее всего, придется изменить несколько частей кода, чтобы заменить массив объектом
  • не получиться сосчитать элементы в нем
  • не получиться использовать его в стандартных структурах управления PHP, таких как foreach

Хорошее решение

К счастью, PHP предоставляет инструменты позволяющие сделать все это возможным. Для этого нужно реорганизовать класс IntCollection, чтобы использовать некоторые основные интерфейсы языка.

Интерфейс - это определение API для класса. При реализации интерфейса сообщается, что класс будет работать определенным, стандартизированным и предопределенным образом. Если взглянуть на это в первый раз, это может показаться "абстрактным классом", но это не совсем так:

  • Все методы, объявленные в интерфейсе, должны быть общедоступными
  • Абстрактные классы могут содержать переменные и реализованные методы, интерфейсы не могут
  • Класс может расширять только один другой класс, но может реализовывать столько интерфейсов, сколько хочется

Чтобы создать нашу массивоподобную типобезопасную коллекцию, мы рассмотрим три интерфейса PHP

  • Countable => Сообщает системе, что класс может использоваться в таких функциях, как 'count ()'
  • Iterator => Сообщает системе, что объект может быть повторен
  • ArrayAccess => реализуя это, возможно использовать коллекцию, как массив (например, доступ к нему через $collection[$key];)

Теперь коллекция IntCollection будет выглядеть так:

class IntCollection implements \Countable, \Iterator, \ArrayAccess
{
    private $values = [];
    private $position = 0;

    /**
     * This constructor is there in order to be able to create a collection with
     * its values already added
     */
    public function __construct(array $values = [])
    {
        foreach ($values as $value) {
            $this->offsetSet('', $value);
        }
    }

    /**
     * Implementation of method declared in \Countable.
     * Provides support for count()
     */
    public function count()
    {
        return count($this->values);
    }

    /**
     * Implementation of method declared in \Iterator
     * Resets the internal cursor to the beginning of the array
     */
    public function rewind()
    {
        $this->position = 0;
    }

    /**
     * Implementation of method declared in \Iterator
     * Used to get the current key (as for instance in a foreach()-structure
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * Implementation of method declared in \Iterator
     * Used to get the value at the current cursor position
     */
    public function current()
    {
        return $this->values[$this->position];
    }

    /**
     * Implementation of method declared in \Iterator
     * Used to move the cursor to the next position
     */
    public function next()
    {
        $this->position++;
    }

    /**
     * Implementation of method declared in \Iterator
     * Checks if the current cursor position is valid
     */
    public function valid()
    {
        return isset($this->values[$this->position]);
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used to be able to use functions like isset()
     */
    public function offsetExists($offset)
    {
        return isset($this->values[$offset]);
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used for direct access array-like ($collection[$offset]);
     */
    public function offsetGet($offset)
    {
        return $this->values[$offset];
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used for direct setting of values
     */
    public function offsetSet($offset, $value)
    {
        if (!is_int($value)) {
            throw new \InvalidArgumentException("Must be an int");
        }

        if (empty($offset)) { //this happens when you do $collection[] = 1;
            $this->values[] = $value;
        } else {
            $this->values[$offset] = $value;
        }
    }

    /**
     * Implementation of method declared in \ArrayAccess
     * Used for unset()
     */
    public function offsetUnset($offset)
    {
        unset($this->values[$offset]);
    }
}

Лучшее решение: использование SPL

SPL - это стандартная библиотека PHP, в которой найдется много очень полезных вещей. Для решения текущей задачи существует класс ArrayObject который выполняет все, что делалось вручную раньше. Теперь требуемый класс будет написан буквально в мгновение ока:

class IntCollection extends \ArrayObject
{
    public function offsetSet($index, $newval)
    {
        if (!is_int($newval)) {
            throw new \InvalidArgumentException("Must be int");
        }

        parent::offsetSet($index, $newval);
    }
}
Написать комментарий