src/Collection.php

Summary

Maintainability
C
1 day
Test Coverage
<?php namespace Clean\Data;

use Closure;
use InvalidArgumentException;
use LogicException;
use RuntimeException;

class Collection extends \ArrayIterator
{
    /**
     * Constructs Collection object
     *
     * @param mixed $data data
     */
    public function __construct($data = null)
    {
        parent::__construct([]);
        if ($data) {
            $this->append($data);
        }
    }

    /**
     * Returns new instance of collection of the same type
     *
     * @return static
     */
    public function getNewCollection()
    {
        $class = get_called_class();
        return new $class;
    }

    /**
     * Extend collection with entities from another one
     *
     * @param Collection $data
     * @access public
     *
     * @return static
     */
    public function extend(Collection $collection)
    {
        foreach ($collection as $entity) {
            $this->append($entity);
        }
        return $this;
    }

    /**
     * Append entities to collection
     *
     * @param Entity|Collection|Traversable|array $data entity or list of entities to append
     * @access public
     *
     * @return static
     */
    public function append($data)
    {
        if (is_array($data) || $data instanceof \Traversable) {
            foreach ($data as $entity) {
                if (!$entity instanceof Entity) {
                    throw new InvalidArgumentException('Collection can contain only Entities class');
                }
                parent::append($entity);
            }
        } else {
            parent::append($data);
        }
        return $this;
    }

    /**
     * Prepand entity to the begining of collection
     *
     * @param Entity $entity
     *
     * @return static
     */
    public function prepend($entity)
    {
        $collection = $this->getNewCollection();
        $collection->append($entity);
        foreach ($this as $entity) {
            $collection->append($entity);
        }
        $this->clear();
        $this->append($collection);
        return $this;
    }

    public function __call($name, $args)
    {
        switch ($this->count()) {
            case 0:
                throw new LogicException('Cannot operate on empty collection');
            case 1:
                $entity = $this->first();
                if (!method_exists($entity, $name)) {
                    throw new RuntimeException(
                        sprintf("Class %s does not have a method '%s'", get_class($entity), $name)
                    );
                }
                return call_user_func_array([$entity, $name], $args);
            default:
                throw new LogicException(
                    'Collection has more then one element, you cannot call entity method directly'
                );
        }
    }

    /**
     * Returns direct value from entity when collection has only one element
     *
     * @param string $name property name
     *
     * @throws LogicException when collection has more then one element
     *
     * @return mixed
     */
    public function __get($name)
    {
        if (!$this->__isset($name)) {
            return null;
        }
        return $this->first()->$name;
    }

    /**
     * Returns isset on entity property when collection has only one element
     *
     * @param string $name property name
     *
     * @throws LogicException when collection has more then one element
     *
     * @return mixed
     */
    public function __isset($name)
    {
        if ($this->count() > 1) {
            throw new LogicException(
                'Collection has more then one element, you cannot get entity property directly'
            );
        }
        $current = $this->first();
        if (!$current) {
            return false;
        }
        return isset($current->$name);
    }

    /**
     * Returns first entity from collection
     *
     * @return Entity
     */
    public function first()
    {
        $this->rewind();
        return $this->current();
    }

    /**
     * Returns last entity from collection
     *
     * @return Entity
     */
    public function last()
    {
        $this->seek($this->count() - 1);
        $entity = $this->current();
        $this->rewind();
        return $entity;
    }

    /**
     * Returns element moved by offset from the current key
     *
     * @param integer $offset offset
     *
     * @return Entity
     */
    private function getElementMovedByOffset($offset)
    {
        if (!is_int($this->key())) {
            throw new LogicException(sprintf("Can't get element moved by %s as current key is not numeric", $offset));
        }
        $oldPosition = $this->key();
        $newPosition = $oldPosition + $offset;
        $this->seek($newPosition);
        if (!is_int($this->key())) {
            throw new LogicException(sprintf("Can't get element moved by %s as next key is not numeric", $offset));
        }
        $value = $this->current();
        $this->seek($oldPosition);
        return $value;
    }

    /**
     * Returns next entity from collection
     *
     * @return Entity
     */
    public function getNext()
    {
        return $this->getElementMovedByOffset(1);
    }

    /**
     * Returns previous entity from collection
     *
     * @return Entity
     */
    public function getPrevious()
    {
        return $this->getElementMovedByOffset(-1);
    }

    /**
     * returns random entity from collection
     *
     * @return Entity
     */
    public function getRandom()
    {
        $randomKey = array_rand($this->getArrayCopy());
        return $this[$randomKey];
    }

    /**
     * Returns true if collection is not empty
     *
     * @return bool
     */
    public function isNotEmpty()
    {
        return !$this->isEmpty();
    }

    /**
     * Returns true if collection is empty
     *
     * @return bool
     */
    public function isEmpty()
    {
        return (0 === $this->count());
    }


    /**
     * Checks if collection has entity with field equals to given value
     *
     * @param string $field field
     * @param mixed $value value
     *
     * @return bool
     */
    public function has($field, $value, $strict = false)
    {
        return !(false === $this->search($field, $value, $strict));
    }

    /**
     * Search for an element with given property and value
     *
     * @param string $field name of property
     * @param mixed $value value to compare
     * @param bool $strict compare value and type of property
     *
     * @return integer|string|false
     */
    public function search($field, $value, $strict = false)
    {
        $value = is_array($value) ? $value : array($value);
        foreach ($this as $key => $entity) {
            if (isset($entity->$field) && in_array($entity->$field, $value, $strict)) {
                return $key;
            }
        }

        return false;
    }

    /**
     * Shift an entity off the begining of collection
     *
     * @return Entity
     */
    public function shift()
    {
        $slice = $this->slice(0, 1);
        $offset = $slice->getKeys();
        $this->offsetUnset($offset[0]);
        return $slice->first();
    }

    /**
     * Filter collection from entities not matching criteria given in callback
     *
     * @param \Closure $callback callback
     *
     * @return static
     */
    public function filter(\Closure $callback)
    {
        $offsetToRemove = [];
        foreach ($this as $offset => $entity) {
            if (!$callback($entity)) {
                $offsetToRemove[] = $offset;
            }
        }
        $this->offsetUnset($offsetToRemove);
        return $this;
    }

    /**
     * Get collection of entities matching criteria given in callback
     *
     * Usage example:
     *
     * $colleciton->getBy(function($entity) {
     *    return $entity->name == 'John';
     * });
     *
     * @param Closure $callback
     * @return static
     */
    public function getBy(\Closure $callback)
    {
        $instanceCopy = $this->getNewCollection();
        foreach ($this as $offset => $entity) {
            if ($callback($entity)) {
                $instanceCopy->append($entity);
            }
        }

        return $instanceCopy;
    }

    /**
     * Returns collection keys
     *
     * @return array
     */
    public function getKeys()
    {
        $keys = array();
        foreach ($this as $key => $entity) {
            $keys[] = $key;
        }
        return $keys;
    }

    /**
     * Splits collection into chunks
     *
     * @param integer $size
     *
     * @return static
     */
    public function chunk($size)
    {
        $collection = $this->getNewCollection();

        foreach (array_chunk($this->getKeys(), $size) as $chunkIndex => $keys) {
            $collection[$chunkIndex] = $this->getNewCollection();

            foreach ($keys as $key) {
                $collection[$chunkIndex]->append($this[$key]);
            }
        }

        return $collection;
    }


    /**
     * Extract a slice of the collection
     *
     * @param integer $offset
     * @param integer|null $length
     *
     * @return static
     */
    public function slice($offset, $length = null)
    {
        $keys = $this->getKeys();

        $keys = array_slice($keys, $offset, $length);

        $collection = $this->getNewCollection();

        foreach ($this as $key => $entity) {
            if (in_array($key, $keys)) {
                $collection[$key] = $entity;
            }
        }

        return $collection;
    }

    /**
     * Returns collection of collections created by spliting first Collection to a parts
     *
     * Example:
     *
     * When collection has 10 element and we would like to split to 3 separate collections:
     *
     * $splitted = $collection->split(3);
     * $splitted->count(); // = 3
     * $splitted[0]->count(); // = 4
     * $splitted[1]->count(); // = 4
     * $splitted[3]->count(); // = 2
     * 
     * @param integer $parts
     * 
     * @return static
     */
    public function split($parts)
    {
        $elementsPerChunk = ceil($this->count() / $parts);

        return $this->chunk($elementsPerChunk);
    }

    /**
     * Remove all entities form collection
     *
     * @return static
     */
    public function clear()
    {
        $this->offsetUnset($this->getKeys());
        return $this;
    }

    /**
     * Unset values from an offset or offsets
     *
     * @param string|array $index Offsets to remove
     *
     * @return static
     */
    public function offsetUnset($index)
    {
        $index = is_array($index) ? $index : (array)$index;
        foreach ($index as $key) {
            parent::offsetUnset($key);
        }
        return $this;
    }

    /**
     * Tranform collection to array
     *
     * @return array
     */
    public function toArray()
    {
        $result = [];
        foreach ($this as $entity) {
            $result[] = (array)$entity;
        }
        return $result;
    }

    /**
     * Renumber collection keys (from zero to n), keeping values in the same place
     * @return static
     */
    public function reindex()
    {
        $data = $this->getArrayCopy();
        $this->clear();

        $index = 0;
        foreach ($data as $value) {
            $this[$index++] = $value;
        }
        $this->rewind();

        return $this;
    }

    /**
     * Eliminates entities that contains the same value in given property
     *
     * @param string $propertyName Name of the property
     *
     * @return static
     */
    public function distinctOn($propertyName)
    {
        $values = [];
        $keys = [];
        foreach ($this as $key => $entity) {
            if (in_array($entity->$propertyName, $values)) {
                $keys[] = $key;
            } else {
                $values[] = $entity->$propertyName;
            }
        }
        $this->offsetUnset($keys);
        return $this;
    }

    /**
     * Return an collection with elements in reverse order
     *
     * @return static
     */
    public function reverse()
    {
        $positions = array_flip($this->getKeys());
        $this->uksort(
            function ($a, $b) use ($positions) {
                return ($positions[$a] < $positions[$b] ? 1 : -1);
            }
        );

        return $this;
    }

    /**
     * Apply a user supplied function to every member of an Collection
     *
     * @param Closure $callback user supplied function
     *
     * @return static
     */
    public function walk(Closure $callback)
    {
        foreach ($this as $entity) {
            $callback($entity);
        }

        return $this;
    }

    /**
     * Return values from all entities from given property
     *
     * @param string $name name
     *
     * @return array
     */
    public function getAllValuesForProperty($name)
    {
        $values = [];
        foreach ($this as $entity) {
            if (isset($entity->$name)) {
                $value = $entity->$name;
                if (!($value === null || $value instanceof Collection && $value->isEmpty())) {
                    if (is_scalar($value)) {
                        $values[$value] = $value;
                    } else {
                        $values[] = $value;
                    }
                }
            }
        }

        return array_values($values);
    }

    /**
     * Bind two collections
     *
     * @param Collection $collection collection
     * @param array $compareKeys The name of the key to compare with from target Collection
     * @param string $propertyName The nae of new property that will be created in source Collection
     *
     * @return static
     */
    public function bindCollection(Collection $collection, array $compareKeys, $propertyName)
    {
        $reflection = new \ReflectionClass($collection);

        $fromKey = key($compareKeys);
        $toKey = $compareKeys[$fromKey];

        $collection = $collection->groupByField($fromKey);

        foreach ($this as $entity) {
            if (!isset($entity->$propertyName)) {
                $entity->$propertyName = new $reflection->name;
            }

            if (!isset($entity->$toKey) || ($value = $entity->$toKey) === null) {
                continue; //do not try to bind to null or non existing property
            }

            if (isset($collection[$value])) {
                $entity->$propertyName->append($collection[$value]);
            }
        }
        return $this;
    }

    /**
     * Group entities inside collection to sepearate Collections
     *
     * @param string $name Property name to group by
     *
     * @return static
     */
    private function groupByField($name)
    {
        $collection = $this->getNewCollection();
        foreach ($this as $entity) {
            if (!isset($entity->$name) || $entity->$name === null) {
                continue; //when entity dosen't have set property it will be omitted
            }

            $value = $entity->$name;

            if (!isset($collection[$value])) {
                $collection[$value] = $this->getNewCollection();
            }
            $collection[$value]->append($entity);
        }
        return $collection;
    }
}