diff options
Diffstat (limited to 'lib/models/SimpleCollection.class.php')
| -rw-r--r-- | lib/models/SimpleCollection.class.php | 768 |
1 files changed, 768 insertions, 0 deletions
diff --git a/lib/models/SimpleCollection.class.php b/lib/models/SimpleCollection.class.php new file mode 100644 index 0000000..49ebf6c --- /dev/null +++ b/lib/models/SimpleCollection.class.php @@ -0,0 +1,768 @@ +<?php +if (!defined('SORT_NATURAL')) { + define('SORT_NATURAL', 6); +} +if (!defined('SORT_FLAG_CASE')) { + define('SORT_FLAG_CASE', 8); +} + +/** + * SimpleCollection.class.php + * collection of assoc arrays with convenience + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * @author André Noack <noack@data-quest.de> + * @copyright 2013 Stud.IP Core-Group + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +class SimpleCollection extends StudipArrayObject +{ + /** + * callable to initialize collection + * + * @var Closure + */ + protected $finder; + + /** + * number of records after last init + * + * @var int + */ + protected $last_count; + + /** + * collection with deleted records + * @var SimpleCollection + */ + protected $deleted; + + /** + * creates a collection from an array of arrays + * all arrays should contain same keys, but is not enforced + * + * @param array $data array containing assoc arrays + * @return SimpleCollection + */ + public static function createFromArray(Array $data) + { + return new self($data); + } + + /** + * converts arrays or objects to ArrayObject objects + * if ArrayAccess interface is not available + * + * @param mixed $a + * @return ArrayAccess + */ + public static function arrayToArrayObject($a) + { + if ($a instanceof StudipArrayObject) { + $a->setFlags(StudipArrayObject::ARRAY_AS_PROPS); + return $a; + } + + if ($a instanceof ArrayObject) { + return new StudipArrayObject($a->getArrayCopy(), StudipArrayObject::ARRAY_AS_PROPS); + } + + if ($a instanceof ArrayAccess) { + return $a; + } + + return new StudipArrayObject((array) $a, StudipArrayObject::ARRAY_AS_PROPS); + } + + /** + * returns closure to compare a value against given arguments + * using given operator + * + * @param string $operator + * @param mixed $args + * @throws InvalidArgumentException + * @return Closure comparison function + */ + public static function getCompFunc($operator, $args) + { + if ($operator instanceOf Closure) { + $comp_func = function ($a) use ($args, $operator) { + return $operator($a, $args); + }; + } else { + if (!is_array($args)) { + $args = [$args]; + } + switch ($operator) { + case '==': + $comp_func = function ($a) use ($args) { + return in_array($a, $args); + }; + break; + case '===': + $comp_func = function ($a) use ($args) { + return in_array($a, $args, true); + }; + break; + case '!=': + case '<>': + $comp_func = function ($a) use ($args) { + return !in_array($a, $args); + }; + break; + case '!==': + $comp_func = function ($a) use ($args) { + return !in_array($a, $args, true); + }; + break; + case '<': + case '>': + case '<=': + case '>=': + $op_func = function ($a, $b) use ($operator) { + if ($operator === '<') { + return $a < $b; + } elseif ($operator === '<=') { + return $a <= $b; + } elseif ($operator === '>=') { + return $a >= $b; + } elseif ($operator === '>') { + return $a > $b; + } + }; + $comp_func = function ($a) use ($op_func, $args) { + return $op_func($a, $args[0]); + }; + break; + case '><': + $comp_func = function ($a) use ($args) { + return $a > $args[0] && $a < $args[1]; + }; + break; + case '>=<=': + $comp_func = function ($a) use ($args) { + return $a >= $args[0] && $a <= $args[1]; + }; + break; + case '%=': + $comp_func = function ($a) use ($args) { + $a = mb_strtolower(static::translitLatin1($a)); + $args = array_map('static::translitLatin1', $args); + $args = array_map('mb_strtolower', $args); + return in_array($a, $args); + }; + break; + case '*=': + $comp_func = function ($a) use ($args) { + foreach ($args as $arg) { + if (mb_strpos($a, $arg) !== false) { + return true; + } + } + return false; + }; + break; + case '^=': + $comp_func = function ($a) use ($args) { + foreach ($args as $arg) { + if (mb_strpos($a, $arg) === 0) { + return true; + } + } + return false; + }; + break; + case '$=': + $comp_func = function ($a) use ($args) { + foreach ($args as $arg) { + $found = mb_strrpos($a, $arg); + if ($found !== false && ($found + mb_strlen($arg)) === mb_strlen($a)) { + return true; + } + } + return false; + }; + break; + case '~=': + $comp_func = function ($a) use ($args) { + foreach ($args as $arg) { + if (preg_match($arg, $a) === 1) { + return true; + } + } + return false; + }; + break; + default: + throw new InvalidArgumentException('unknown operator: ' . $operator); + } + } + return $comp_func; + } + + /** + * transliterates latin1 string to ascii + * + * @param string $text + * @return string + */ + public static function translitLatin1($text) + { + if (!preg_match('/[\200-\377]/', $text)) { + return $text; + } + $text = str_replace(['ä','Ä','ö','Ö','ü','Ü','ß'], ['a','A','o','O','u','U','s'], $text); + $text = str_replace(['À','Á','Â','Ã','Å','Æ'], 'A' , $text); + $text = str_replace(['à','á','â','ã','å','æ'], 'a' , $text); + $text = str_replace(['È','É','Ê','Ë'], 'E' , $text); + $text = str_replace(['è','é','ê','ë'], 'e' , $text); + $text = str_replace(['Ì','Í','Î','Ï'], 'I' , $text); + $text = str_replace(['ì','í','î','ï'], 'i' , $text); + $text = str_replace(['Ò','Ó','Õ','Ô','Ø'], 'O' , $text); + $text = str_replace(['ò','ó','ô','õ','ø'], 'o' , $text); + $text = str_replace(['Ù','Ú','Û'], 'U' , $text); + $text = str_replace(['ù','ú','û'], 'u' , $text); + $text = str_replace(['Ç','ç','Ð','Ñ','Ý','ñ','ý','ÿ'], ['C','c','D','N','Y','n','y','y'] , $text); + return $text; + } + + /** + * Constructor + * + * @param mixed $data array or closure to fill collection + */ + public function __construct($data = []) + { + parent::__construct(); + $this->finder = $data instanceof Closure ? $data : null; + $this->deleted = clone $this; + if ($data instanceof Closure) { + $this->refresh(); + } else { + $this->exchangeArray($data); + } + } + + /** + * @param array $input + * @return array + */ + public function exchangeArray($input) + { + return parent::exchangeArray(array_map('static::arrayToArrayObject', $input)); + } + + /** + * converts the object and all elements to plain arrays + * + * @return array + */ + public function toArray() + { + $args = func_get_args(); + return $this->map(function ($a) use ($args) { + if (method_exists($a, 'toArray')) { + return call_user_func_array([$a, 'toArray'], $args); + } + if (method_exists($a, 'getArrayCopy')) { + return $a->getArrayCopy(); + } + return (array) $a; + } + ); + } + + /** + * + * @see ArrayObject::append() + */ + public function append($newval) + { + return parent::append(static::arrayToArrayObject($newval)); + } + + /** + * Sets the value at the specified index + * ensures the value has ArrayAccess + * + * @see ArrayObject::offsetSet() + */ + public function offsetSet($index, $newval) + { + if (is_numeric($index)) { + $index = (int) $index; + } + return parent::offsetSet($index, static::arrayToArrayObject($newval)); + } + + /** + * Unsets the value at the specified index + * value is moved to internal deleted collection + * + * @see ArrayObject::offsetUnset() + * @throws InvalidArgumentException + */ + public function offsetUnset($index) + { + if ($this->offsetExists($index)) { + $this->deleted[] = $this->offsetGet($index); + } + return parent::offsetUnset($index); + } + + /** + * sets the finder function + * + * @param Closure $finder + */ + public function setFinder(Closure $finder) + { + $this->finder = $finder; + } + + /** + * get deleted records collection + * @return SimpleCollection + */ + public function getDeleted() + { + return $this->deleted; + } + + /** + * reloads the elements of the collection + * by calling the finder function + * + * @return number of records after refresh + */ + public function refresh() + { + if (is_callable($this->finder)) { + $data = call_user_func($this->finder); + $this->exchangeArray($data); + $this->deleted->exchangeArray([]); + return $this->last_count = $this->count(); + } + } + + /** + * returns a new collection containing all elements + * where given columns value matches given value(s) using passed operator + * pass array for multiple values + * + * operators: + * == equal, like php + * === identical, like php + * !=,<> not equal, like php + * !== not identical, like php + * <,>,<=,>= less,greater,less or equal,greater or equal + * >< between without borders, needs two arguments + * >=<= between including borders, needs two arguments + * %= like string, transliterate to ascii,case insensitive + * *= contains string + * ^= begins with string + * $= ends with string + * ~= regex + * + * @param string $key the column name + * @param mixed $value value to search for + * @param mixed $op operator to find + * @return SimpleCollection with found records + */ + public function findBy($key, $values, $op = '==') + { + $comp_func = self::getCompFunc($op, $values); + return $this->filter(function ($record) use ($comp_func, $key) { + return $comp_func($record[$key]); + }); + } + + /** + * returns the first element + * where given column has given value(s) + * pass array for multiple values + * + * @param string $key the column name + * @param mixed $value value to search for, + * @param mixed $op operator to find + * @return SimpleORMap found record + */ + public function findOneBy($key, $values, $op = '==') + { + $comp_func = self::getCompFunc($op, $values); + return $this->filter(function ($record) use ($comp_func, $key) { + return $comp_func($record[$key]); + }, 1)->first(); + } + + /** + * apply given callback to all elements of + * collection + * + * @param Closure $func the function to call + * @return int addition of return values + */ + public function each(Closure $func) + { + $result = false; + foreach ($this->storage as $record) { + $result += call_user_func($func, $record); + } + return $result; + } + + /** + * apply given callback to all elements of + * collection and give back array of return values + * + * @param Closure $func the function to call + * @return array + */ + public function map(Closure $func) + { + $results = []; + foreach ($this->storage as $key => $value) { + $results[$key] = call_user_func($func, $value, $key); + } + return $results; + } + + /** + * filter elements + * if given callback returns true + * + * @param Closure $func the function to call + * @param integer $limit limit number of found records + * @return SimpleCollection containing filtered elements + */ + public function filter(Closure $func = null, $limit = null) + { + $results = []; + $found = 0; + foreach ($this->storage as $key => $value) { + if (call_user_func($func, $value, $key)) { + $results[$key] = $value; + if ($limit && (++$found == $limit)) { + break; + } + } + } + return self::createFromArray($results); + } + + /** + * Returns whether any element of the collection returns true for the + * given callback. + * + * @param Closure $func the function to call + * @return bool + */ + public function any(Closure $func) + { + foreach ($this->storage as $key => $value) { + if (call_user_func($func, $value, $key)) { + return true; + } + } + return false; + } + + /** + * Returns whether every element of the collection returns true for the + * given callback. + * + * @param Closure $func the function to call + * @return bool + */ + public function every(Closure $func) + { + foreach ($this->storage as $key => $value) { + if (!call_user_func($func, $value, $key)) { + return false; + } + } + return true; + } + + /** + * extract array of columns values + * pass array or space-delimited string for multiple columns + * + * @param string|array $columns the column(s) to extract + * @return array of extracted values + */ + public function pluck($columns) + { + if (!is_array($columns)) { + $columns = words($columns); + } + $func = function ($r) use ($columns) { + $result = []; + foreach ($columns as $c) { + $result[] = $r[$c]; + } + return $result; + }; + $result = $this->map($func); + return count($columns) === 1 ? array_map('current', $result) : $result; + } + + /** + * returns the collection as grouped array + * first param is the column to group by, it becomes the key in + * the resulting array, default is pk. Limit returned fields with second param + * The grouped entries can optoionally go through the given + * callback. If no callback is provided, only the first grouped + * entry is returned, suitable for grouping by unique column + * + * @param string $group_by the column to group by, pk if ommitted + * @param mixed $only_these_fields limit returned fields + * @param Closure $group_func closure to aggregate grouped entries + * @return array assoc array + */ + public function toGroupedArray($group_by = 'id', $only_these_fields = null, Closure $group_func = null) + { + $result = []; + if (is_string($only_these_fields)) { + $only_these_fields = words($only_these_fields); + } + foreach ($this->toArray() as $record) { + $key = $record[$group_by]; + $ret = []; + if (is_array($only_these_fields)) { + $result[$key][] = array_intersect_key($record, array_flip($only_these_fields)); + } else { + $result[$key][] = $record; + } + } + if ($group_func === null) { + $group_func = 'current'; + } + return array_map($group_func, $result); + } + + /** + * get the first element + * + * @return Array first element or null + */ + public function first() + { + $keys = array_keys($this->storage); + $first_offset = reset($keys); + return $this->offsetGet($first_offset ?: 0); + } + + /** + * get the last element + * + * @return Array last element or null + */ + public function last() + { + $keys = array_keys($this->storage); + $last_offset = end($keys); + return $this->offsetGet($last_offset ?: 0); + } + + /** + * get the the value from given key from first element + * + * @return mixed + */ + public function val($key) + { + $first = $this->first(); + return $first[$key] ?: null; + } + + /** + * mark element(s) for deletion + * where given column has given value(s) + * element(s) are moved to + * internal deleted collection + * pass array for multiple values + * + * operators: + * == equal, like php + * === identical, like php + * !=,<> not equal, like php + * !== not identical, like php + * <,>,<=,>= less,greater,less or equal,greater or equal + * >< between without borders, needs two arguments + * >=<= between including borders, needs two arguments + * %= like string, transliterate to ascii,case insensitive + * *= contains string + * ^= begins with string + * $= ends with string + * ~= regex + * + * @param string $key + * @param mixed $values + * @param mixed $op operator to find elements + * @return number of unsetted elements + */ + public function unsetBy($key, $values, $op = '==') + { + $ret = false; + $comp_func = self::getCompFunc($op, $values); + foreach ($this->storage as $k => $record) { + if ($comp_func($record[$key])) { + $this->offsetunset($k); + $ret += 1; + } + } + return $ret; + } + + /** + * sorts the collection by columns of contained elements and returns it + * + * works like sql order by: + * first param is a string containing combinations of column names + * and sort direction, separated by comma e.g. + * 'name asc, nummer desc ' + * sorts first by name ascending and then by nummer descending + * second param denotes the sort type (using PHP sort constants): + * SORT_LOCALE_STRING: + * compare items as strings, transliterate latin1 to ascii, case insensitiv, natural order for numbers + * SORT_NUMERIC: + * compare items as integers + * SORT_STRING: + * compare items as strings + * SORT_NATURAL: + * compare items as strings using "natural ordering" + * SORT_FLAG_CASE: + * can be combined (bitwise OR) with SORT_STRING or SORT_NATURAL to sort strings case-insensitively + * + * @param string $order columns to order by + * @param integer $sort_flags + * @return SimpleCollection the sorted collection + */ + public function orderBy($order, $sort_flags = SORT_LOCALE_STRING) + { + //('name asc, nummer desc ') + $sort_locale = false; + switch ($sort_flags) { + case SORT_NATURAL: + $sort_func = 'strnatcmp'; + break; + case SORT_NATURAL | SORT_FLAG_CASE: + $sort_func = 'strnatcasecmp'; + break; + case SORT_STRING | SORT_FLAG_CASE: + $sort_func = 'strcasecmp'; + break; + case SORT_STRING: + $sort_func = 'strcmp'; + break; + case SORT_NUMERIC: + $sort_func = function ($a, $b) { + return (int) $a - (int) $b; + }; + break; + case SORT_LOCALE_STRING: + default: + $sort_func = 'strnatcasecmp'; + $sort_locale = true; + } + + $sorter = []; + foreach (explode(',', $order) as $one) { + $sorter[] = array_values(array_filter(array_map('trim', explode(' ', $one)))); + } + + $func = function ($d1, $d2) use ($sorter, $sort_func, $sort_locale) { + do { + list($field, $dir) = current($sorter); + if (!$sort_locale) { + $value1 = $d1[$field]; + $value2 = $d2[$field]; + } else { + $value1 = static::translitLatin1(mb_substr($d1[$field], 0, 100)); + $value2 = static::translitLatin1(mb_substr($d2[$field], 0, 100)); + } + $ret = $sort_func($value1, $value2); + if (mb_strtolower($dir) == 'desc') $ret = $ret * -1; + } while ($ret === 0 && next($sorter)); + + return $ret; + }; + if (count($sorter)) { + $this->uasort($func); + } + return $this; + } + + /** + * returns a new collection contaning a sequence of original collection + * mimics the sql limit constrain: + * used with one parameter, the first x elements are extracted + * used with two parameters, the first parameter denotes the offset, the second the + * number of elements + * + * @param integer $arg1 + * @param integer $arg2 + * @return SimpleCollection + */ + public function limit($arg1, $arg2 = null) + { + if (is_null($arg2)) { + if ($arg1 > 0) { + $row_count = $arg1; + $offset = 0; + } else { + $row_count = abs($arg1); + $offset = $arg1; + } + } else { + $offset = $arg1; + $row_count = $arg2; + } + return self::createFromArray(array_slice($this->storage, $offset, $row_count, true)); + } + + /** + * calls the given method on all elements + * of the collection + * @param string $method methodname to call + * @param array $params parameters for methodcall + * @return array of all return values + */ + public function sendMessage($method, $params = []) { + $results = []; + foreach ($this->storage as $record) { + $results[] = call_user_func_array([$record, $method], $params); + } + return $results; + } + + /** + * magic version of sendMessage + * calls undefineds methods on all elements of the collection + * But beware of the dark side... + * + * @param string $method methodname to call + * @param array $params parameters for methodcall + * @return array of all return values + */ + public function __call($method, $params) + { + return $this->sendMessage($method, $params); + } + + /** + * merge in another collection, elements are appended + * + * @param SimpleCollection $a_collection + */ + public function merge(SimpleCollection $a_collection) + { + $this->storage = array_merge($this->storage, $a_collection->getArrayCopy()); + } +} |
