<?php
namespace XF\Mvc\Entity;
use function array_key_exists, array_slice, count, func_get_args, is_array;
abstract class AbstractCollection implements \Countable, \IteratorAggregate, \ArrayAccess
{
/**
* @var Entity[]
*/
protected $entities = [];
/**
* @var bool
*/
protected $populated = false;
abstract protected function populateInternal();
public function populate()
{
if (!$this->populated)
{
$this->populated = true;
$this->populateInternal();
}
return $this;
}
public function toArray()
{
$this->populate();
return $this->entities;
}
public function toApiResults($verbosity = Entity::VERBOSITY_NORMAL, array $options = [], $maintainKeys = false)
{
$this->populate();
$results = [];
foreach ($this->entities AS $k => $entity)
{
$result = $entity->toApiResult($verbosity, $options);
if ($maintainKeys)
{
$results[$k] = $result;
}
else
{
$results[] = $result;
}
}
return new \XF\Api\Result\EntityResults($results);
}
/**
* @param string $key
*
* @return Entity|null
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->entities[$key];
}
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
{
$this->entities[$key] = $value;
}
#[\ReturnTypeWillChange]
public function offsetExists($key)
{
return isset($this->entities[$key]);
}
#[\ReturnTypeWillChange]
public function offsetUnset($key)
{
unset($this->entities[$key]);
}
#[\ReturnTypeWillChange]
public function getIterator()
{
$this->populate();
return new \ArrayIterator($this->entities);
}
#[\ReturnTypeWillChange]
public function count()
{
$this->populate();
return count($this->entities);
}
public function keys()
{
$this->populate();
return array_keys($this->entities);
}
public function first()
{
$this->populate();
return reset($this->entities);
}
public function last()
{
$this->populate();
return end($this->entities);
}
/**
* @param callable $callback
* @param bool $collectionOnEmpty If true, an empty plucking will return a collection; otherwise, an array
*
* @return array|ArrayCollection
*/
public function pluck(\Closure $callback, $collectionOnEmpty = true)
{
$this->populate();
$output = [];
$newCollection = true;
foreach ($this->entities AS $key => $entity)
{
$res = $callback($entity, $key);
if (is_array($res))
{
$output[$res[0]] = $res[1];
if (!($res[1] instanceof Entity))
{
$newCollection = false;
}
}
}
if (!$output)
{
return $collectionOnEmpty ? new ArrayCollection([]) : [];
}
else
{
return $newCollection ? new ArrayCollection($output) : $output;
}
}
public function pluckNamed($valueField, $keyField = null)
{
$i = 0;
$f = function(Entity $e) use($keyField, $valueField, &$i)
{
if ($keyField !== null)
{
$key = $e->$keyField;
}
else
{
$key = $i;
$i++;
}
$value = $e->$valueField;
return [$key, $value];
};
// starts with upper case letter means pulling an entity so give a collection (by convention)
$collectionOnEmpty = preg_match('/^[A-Z]/', $valueField);
return $this->pluck($f, $collectionOnEmpty);
}
/**
* @param callable $callback
*
* @return ArrayCollection
*/
public function filter(\Closure $callback)
{
return new ArrayCollection(array_filter($this->toArray(), $callback));
}
/**
* Applys the equivalent of array_slice to a collection
*
* @param int $offset
* @param null|int $length
* @param bool $preserveKeys
*
* @return ArrayCollection
*/
public function slice($offset, $length = null, $preserveKeys = true)
{
return new ArrayCollection(array_slice($this->toArray(), $offset, $length, $preserveKeys));
}
public function sliceToPage($page, $perPage, $preserveKeys = true)
{
return $this->slice(($page - 1) * $perPage, $perPage, $preserveKeys);
}
/**
* Returns a new collection with the argument merged.
* Existing elements will be before the other collection's elements.
*
* @param AbstractCollection $other
*
* @return ArrayCollection
*/
public function merge(AbstractCollection $other)
{
$elements = $this->toArray();
foreach ($other->toArray() AS $k => $v)
{
$elements[$k] = $v;
}
return new ArrayCollection($elements);
}
/**
* @return ArrayCollection
*/
public function reverse($preserveKeys = true)
{
return new ArrayCollection(array_reverse($this->toArray(), $preserveKeys));
}
/**
* Given a list of ordered keys in a collection, return a new collection
* sorted in that order. Ignores keys without corresponding values in the collection.
*
* @param array $keys
*
* @return ArrayCollection
*/
public function sortByList(array $keys)
{
$values = [];
$elements = $this->toArray();
foreach ($keys AS $key)
{
if (array_key_exists($key, $elements))
{
$values[$key] = $elements[$key];
}
}
return new ArrayCollection($values);
}
public function pop()
{
$entities = $this->toArray();
array_pop($entities);
return new ArrayCollection($entities);
}
public function shuffle()
{
$entities = $this->toArray();
shuffle($entities);
return new ArrayCollection($entities);
}
/**
* @return Entity|ArrayCollection|null
*/
public function shift($returnType = 'entity')
{
$entities = $this->toArray();
$shifted = array_shift($entities);
if ($shifted)
{
if ($returnType == 'entity')
{
return $shifted;
}
else
{
return new ArrayCollection($entities);
}
}
else
{
return null;
}
}
public function unshift()
{
$args = array_reverse(func_get_args());
$entities = $this->toArray();
foreach ($args AS $arg)
{
array_unshift($entities, $arg);
}
return new ArrayCollection($entities);
}
/**
* @return ArrayCollection
*/
public function filterViewable()
{
return $this->filter(function($entity)
{
// TODO: ideally type hint the viewable interface
return $entity->canView();
});
}
public function groupBy($grouper, $childKeyFn = null)
{
$this->populate();
if ($grouper instanceof \Closure)
{
$callback = $grouper;
}
else
{
$callback = function ($e) use ($grouper) { return $e[$grouper]; };
}
if ($childKeyFn && !($childKeyFn instanceof \Closure))
{
$childKey = $childKeyFn;
$childKeyFn = function ($e) use ($childKey) { return $e[$childKey]; };
}
$output = [];
foreach ($this->entities AS $key => $entity)
{
$groupKey = $callback($entity);
if ($childKeyFn)
{
$key = $childKeyFn($entity);
}
$output[$groupKey][$key] = $entity;
}
return $output;
}
}