Skip to content
Snippets Groups Projects
Select Git revision
  • d62f1b7480c51a290c168c79a6f8ea05757a6e77
  • main default protected
  • biest-00109
  • biest-00520
  • biest-00519
  • biest-00521
  • biest-00525
  • biest-00324
  • biest-00110
  • biest-00104
  • biest-00109b
  • biest-00396
  • biest-00098
  • biest-00230
  • biest-00111
  • demo-va-hauptseite
  • biest-00123
  • 9-anmeldesets-gultigskeitsdauer-von-regeln
  • 5.0
19 results

SimpleCollection.class.php

Blame
  • Forked from Stud.IP / Stud.IP
    Source project has a limited visibility.
    SimpleCollection.class.php 23.71 KiB
    <?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
     *
     * @template T
     */
    class SimpleCollection extends StudipArrayObject
    {
        /**
         * callable to initialize collection
         *
         * @var ?callable(): array<T>
         */
        protected $finder;
    
        /**
         * number of records after last init
         *
         * @var int
         */
        protected $last_count;
    
        /**
         * collection with deleted records
         * @var static
         */
        protected $deleted;
    
        /**
         * creates a collection from an array of arrays
         * all arrays should contain same keys, but is not enforced
         *
         * @param array<T> $data array containing assoc arrays
         * @return SimpleCollection<T>
         */
        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 StudipArrayObject|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|callable(mixed, mixed|array): bool  $operator
         * @param mixed|array $args
         * @throws InvalidArgumentException
         * @return callable(mixed): bool comparison function
         */
        public static function getCompFunc($operator, $args)
        {
            if (is_callable($operator)) {
                $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 array<T>|callable(): array<T> $data array or closure to fill collection
         */
        public function __construct($data = [])
        {
            parent::__construct();
            $this->finder = is_callable($data) ? $data : null;
            $this->deleted = clone $this;
            if (is_callable($data)) {
                $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)
        {
            parent::append(static::arrayToArrayObject($newval));
        }
    
        /**
         * Sets the value at the specified index
         * ensures the value has ArrayAccess
         *
         * @see ArrayObject::offsetSet()
         * @return void
         */
        public function offsetSet($index, $newval)
        {
            if (is_numeric($index)) {
                $index = (int) $index;
            }
            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);
            }
            parent::offsetUnset($index);
        }
    
        /**
         * sets the finder function
         *
         * @param callable(): array<T> $finder
         * @return void
         */
        public function setFinder(callable $finder)
        {
            $this->finder = $finder;
        }
    
        /**
         * get deleted records collection
         * @return SimpleCollection<T>
         */
        public function getDeleted()
        {
            return $this->deleted;
        }
    
        /**
         * reloads the elements of the collection
         * by calling the finder function
         *
         * @return ?int 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 $values value to search for
         * @param string|callable $op operator to find
         * @return SimpleCollection<T> 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 $values value to search for,
         * @param string|callable $op operator to find
         * @return ?T 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 callable(T): int $func the function to call
         * @return int|false addition of return values
         */
        public function each(callable $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 callable(T, mixed): mixed $func the function to call
         * @return array<mixed>
         */
        public function map(callable $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 ?callable(T, mixed): bool $func the function to call
         * @param ?integer $limit limit number of found records
         * @return SimpleCollection<T> containing filtered elements
         */
        public function filter(callable $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  callable(T, mixed): bool $func the function to call
         * @return bool
         */
        public function any(callable $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  callable(T, mixed): bool $func the function to call
         * @return bool
         */
        public function every(callable $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 string|array|null $only_these_fields limit returned fields
         * @param ?callable $group_func closure to aggregate grouped entries
         * @return array assoc array
         */
        public function toGroupedArray($group_by = 'id', $only_these_fields = null, callable $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 ?T 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 ?T 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
         *
         * @param string $key
         * @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 string|callable(mixed, mixed|array): bool $op operator to find elements
         * @return int|false 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 $this 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 (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<T>
         */
        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 literal-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 literal-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<T> $a_collection
         * @return void
         */
        public function merge(SimpleCollection $a_collection)
        {
            $this->storage = array_merge($this->storage, $a_collection->getArrayCopy());
        }
    }