honeybee/trellis

View on GitHub
src/Common/Collection/Collection.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Trellis\Common\Collection;

use ArrayIterator;
use Trellis\Common\BaseObject;
use Trellis\Common\BaseObjectInterface;
use Trellis\Common\Error\RuntimeException;

/**
 * Generic base implementation of the CollectionInterface interface.
 */
abstract class Collection extends BaseObject implements CollectionInterface
{
    /**
     * An array of ListenerInterface that are notified upon collection changes.
     * We can't (re)use our CollectionInterface stuff here as this is the lowest level of it's implementation.
     *
     * @var array
     */
    private $collection_listeners = [];

    /**
     * Holds the collection's current items.
     *
     * @var array
     */
    protected $items = [];

    // Php Interface - Countable

    /**
     * Clone the collection.
     */
    public function __clone()
    {
        $new_items = [];

        foreach ($this->items as $item) {
            $new_items[] = $this->cloneItem($item);
        }

        $this->items = $new_items;
    }

    /**
     * Implementation of php's 'countable' interface's 'count' method.
     *
     * @return int
     *
     * @see http://php.net/manual/en/class.countable.php
     */
    public function count()
    {
        return count($this->items);
    }

    // Php Interface - ArrayAccess

    /**
     * Tells whether or not an offset exists.
     *
     * @param mixed $offset
     *
     * @return boolean
     *
     * @see http://php.net/manual/en/class.arrayaccess.php
     */
    public function offsetExists($offset)
    {
        if (!is_int($offset) && !is_string($offset)) {
            throw new RuntimeException('Invalid array offset type: '. gettype($offset));
        }
        return array_key_exists($offset, $this->items);
    }

    /**
     * Returns the value at specified offset.
     *
     * @param mixed $offset
     *
     * @return mixed
     *
     * @see http://php.net/manual/en/class.arrayaccess.php
     */
    public function offsetGet($offset)
    {
        if ($this->offsetExists($offset)) {
            return $this->items[$offset];
        }
        if ($this instanceof MandatoryKeyInterface) {
            throw new RuntimeException('Item does not exist at key: ' . $offset);
        }
        return null;
    }

    /**
     * Assigns a value to the specified offset.
     *
     * @param mixed $offset
     * @param mixed $value
     *
     * @see http://php.net/manual/en/class.arrayaccess.php
     */
    public function offsetSet($offset, $value)
    {
        if ($this instanceof UniqueKeyInterface && $this->offsetExists($offset)) {
            throw new RuntimeException('Item already exists at key: ' . $offset);
        }
        if ($this instanceof UniqueValueInterface) {
            if (false !== ($item_key = array_search($value, $this->items, true))) {
                throw new RuntimeException('Item has already been added to the collection at key: ' . $item_key);
            }
        }
        $this->items[$offset] = $value;
        $this->propagateCollectionChangedEvent(
            new CollectionChangedEvent($value, CollectionChangedEvent::ITEM_ADDED)
        );
    }

    /**
     * Unsets the value at the given offset.
     *
     * @param mixed $offset
     *
     * @see http://php.net/manual/en/class.arrayaccess.php
     */
    public function offsetUnset($offset)
    {
        if ($this->offsetExists($offset)) {
            $removed_items = array_splice($this->items, $offset, 1);
            $this->propagateCollectionChangedEvent(
                new CollectionChangedEvent($removed_items[0], CollectionChangedEvent::ITEM_REMOVED)
            );
        } elseif ($this instanceof MandatoryKeyInterface) {
            throw new RuntimeException('Item to be unset not found at key: ' . $offset);
        }
    }

    /**
     * Returns the key for our current internal-pointer position.
     *
     * @return mixed
     *
     * @see http://php.net/manual/en/class.iterator.php
     */
    public function key()
    {
        return key($this->items);
    }

    // Php Interface - IteratorAggregate

    public function getIterator()
    {
        return new ArrayIterator($this->items);
    }

    // Interface - CollectionInterface

    /**
     * Return a specific item from the collection for the given key.
     *
     * @param mixed $key
     *
     * @return mixed
     */
    public function getItem($key)
    {
        return $this->offsetGet($key);
    }

    /**
     * Return a list of specific items from the collection for the given keys.
     *
     * @param array $keys
     *
     * @return mixed
     */
    public function getItems(array $keys = [])
    {
        $items = [];
        foreach ($keys as $key) {
            $items[] = $this->offsetGet($key);
        }
        if (empty($keys)) {
            $items = $this->items;
        }

        return $items;
    }

    /**
     * Remove the given item from the collection.
     *
     * @param mixed $item
     */
    public function removeItem($item)
    {
        $key = $this->getKey($item);
        if ($key !== false) {
            $this->offsetUnset($key);
        }
    }

    /**
     * Remove the given items from the collection.
     *
     * @param array $items
     */
    public function removeItems(array $items)
    {
        foreach ($items as $item) {
            $this->removeItem($item);
        }
    }

    /**
     * Tells if the collection has an item set for the given key.
     *
     * @param mixed $key
     *
     * @return boolean
     */
    public function hasKey($key)
    {
        return $this->offsetExists($key);
    }

    /**
     * Return the key for the given item.
     * If the collection contains the given item more than once,
     * the first key will be returned.
     * If you wish to receive all set the '$return_all' parameter to true.
     *
     * @param mixed $item
     * @param boolean $return_all
     *
     * @return mixed Returns false, if the item is not contained.
     */
    public function getKey($item, $return_all = false)
    {
        $keys = array_keys($this->items, $item, true);
        if (count($keys) > 1 && $this instanceof UniqueValueInterface) {
            throw new RuntimeException('Unexpected number of unique items in collection');
        }
        if ($return_all) {
            return $keys;
        } else {
            return count($keys) > 0 ? $keys[0] : false;
        }
    }

    /**
     * Tells if the collection contains the given item.
     *
     * @param mixed $item
     *
     * @return boolean
     */
    public function hasItem($item)
    {
        return $this->getKey($item) !== false;
    }

    /**
     * Returns the collection size.
     *
     * @return int
     */
    public function getSize()
    {
        return $this->count();
    }

    public function clear()
    {
        $this->items = [];
    }

    public function isEmpty()
    {
        return $this->getSize() === 0;
    }

    /**
     * Attaches the given listener, so it will be informed about all future changes.
     *
     * @param ListenerInterface $listener
     */
    public function addListener(ListenerInterface $listener)
    {
        if (!in_array($listener, $this->collection_listeners, true)) {
            $this->collection_listeners[] = $listener;
        }
    }

    /**
     * Removes the given listener from our list of collection-changed listeners.
     *
     * @param ListenerInterface $listener
     */
    public function removeListener(ListenerInterface $listener)
    {
        if (false !== ($pos = array_search($listener, $this->collection_listeners, true))) {
            array_splice($this->collection_listeners, $pos, 1);
        }
    }

    /**
     * Returns the collection's underlying array.
     *
     * @return array
     */
    public function toArray()
    {
        $data = [];
        foreach ($this->items as $key => $value) {
            if ($value instanceof BaseObjectInterface) {
                $value = $value->toArray();
            }
            $data[$key] = $value;
        }

        return $data;
    }

    /**
     * Propagate the given collection-changed event to all currently attached listeners.
     */
    protected function propagateCollectionChangedEvent(CollectionChangedEvent $event)
    {
        foreach ($this->collection_listeners as $listener) {
            $listener->onCollectionChanged($event);
        }
    }

    protected function cloneItem($item)
    {
        if (is_object($item)) {
            return clone $item;
        }

        return $item;
    }
}