diff options
| author | Jan-Hendrik Willms <tleilax+studip@gmail.com> | 2024-05-06 08:59:03 +0000 |
|---|---|---|
| committer | David Siegfried <david.siegfried@uni-vechta.de> | 2024-05-06 08:59:03 +0000 |
| commit | 8474ee1063d11f1d5fd4b79674d2d734b82e18b0 (patch) | |
| tree | 2c38c416d67da8d49eb27d0fe63192f24bde1664 /lib/models | |
| parent | a944dacedff6511310d1c73b9854b450ed8996b2 (diff) | |
relocate sorm classes, fixes #4106
Closes #4106
Merge request studip/studip!2953
Diffstat (limited to 'lib/models')
| -rw-r--r-- | lib/models/SimpleCollection.class.php | 782 | ||||
| -rw-r--r-- | lib/models/SimpleORMap.class.php | 2476 | ||||
| -rw-r--r-- | lib/models/SimpleORMapCollection.class.php | 258 |
3 files changed, 0 insertions, 3516 deletions
diff --git a/lib/models/SimpleCollection.class.php b/lib/models/SimpleCollection.class.php deleted file mode 100644 index 6acc10d..0000000 --- a/lib/models/SimpleCollection.class.php +++ /dev/null @@ -1,782 +0,0 @@ -<?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::class, '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::class, '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 - * - * @param mixed $index - * @param mixed $newval - * - * @see ArrayObject::offsetSet() - */ - - public function offsetSet($index, $newval): void - { - 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): void - { - 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 { - $current_sorter = current($sorter); - $field = $current_sorter[0]; - $dir = $current_sorter[1] ?? ''; - 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()); - } -} diff --git a/lib/models/SimpleORMap.class.php b/lib/models/SimpleORMap.class.php deleted file mode 100644 index f86ec6f..0000000 --- a/lib/models/SimpleORMap.class.php +++ /dev/null @@ -1,2476 +0,0 @@ -<?php -/** - * SimpleORMap.class.php - * simple object-relational mapping - * - * 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 2010 Stud.IP Core-Group - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP -*/ - -class SimpleORMap implements ArrayAccess, Countable, IteratorAggregate -{ - /** - * Defines `_` as character used when joining composite primary keys. - */ - const ID_SEPARATOR = '_'; - - /** - * table row data - * @var array $content - */ - protected $content = []; - - /** - * table row data - * @var array $content_db - */ - protected $content_db = []; - - /** - * new state of entry - * @var boolean $is_new - */ - protected $is_new = true; - - /** - * deleted state of entry - * @var boolean $is_deleted - */ - protected $is_deleted = false; - - /** - * db table metadata - * @var ?array $schemes; - */ - public static $schemes = null; - - /** - * configuration data for subclasses - * @see self::configure() - * @var array $config; - */ - protected static $config = []; - - /** - * stores instantiated related objects - * @var array $relations - */ - protected $relations = []; - - /** - * assoc array for storing values for additional fields - * - * @var array $additional_data - */ - protected $additional_data = []; - - /** - * reserved indentifiers, fields with those names must not have an explicit getXXX() method - * @var array $reserved_slots - */ - protected static $reserved_slots = ['value','newid','iterator','tablemetadata', 'relationvalue','wherequery','relationoptions','data','new','id']; - - /** - * indicator for batch operations in findEachBySQL - * - * @var bool $performs_batch_operation - */ - protected static $performs_batch_operation = false; - - /** - * name of db table - * @return string - */ - protected static function db_table() - { - return static::config('db_table'); - } - - /** - * table columns - * @return array - */ - protected static function db_fields() - { - return static::config('db_fields'); - } - - /** - * primary key columns - * @return array - */ - protected static function pk() - { - return static::config('pk'); - } - - /** - * default values for columns - * @return array - */ - protected static function default_values() - { - return static::config('default_values'); - } - - /** - * list of columns to deserialize - * @return array key is name of column, value is name of ArrayObject class - */ - protected static function serialized_fields() - { - return static::config('serialized_fields'); - } - - /** - * aliases for columns - * alias => column - * @return array - */ - protected static function alias_fields() - { - return static::config('alias_fields'); - } - - /** - * multi-language fields - * name => boolean - * @return array - */ - protected static function i18n_fields() - { - return static::config('i18n_fields'); - } - - /** - * additional computed fields - * name => callable - * @return array - */ - protected static function additional_fields() - { - return static::config('additional_fields'); - } - - /** - * 1:n relation - * @return array - */ - protected static function has_many() - { - return static::config('has_many'); - } - - /** - * 1:1 relation - * @return array - */ - protected static function has_one() - { - return static::config('has_one'); - } - - /** - * n:1 relations - * @return array - */ - protected static function belongs_to() - { - return static::config('belongs_to'); - } - - /** - * n:m relations - * @return array - */ - protected static function has_and_belongs_to_many() - { - return static::config('has_and_belongs_to_many'); - } - - /** - * callbacks - * @return array<string, array<string|Closure>> - */ - protected static function registered_callbacks() - { - return static::config('registered_callbacks'); - } - - /** - * contains an array of all used identifiers for fields - * (db columns + aliased columns + additional columns + relations) - * @return array - */ - protected static function known_slots() - { - return static::config('known_slots'); - } - - /** - * assoc array used to map SORM callback to NotificationCenter - * keys are SORM callbacks, values notifications - * eg. 'after_create' => 'FooDidCreate' - * - * @return array - */ - protected static function notification_map() - { - return static::config('notification_map'); - } - - /** - * assoc array for mapping get/set Methods - * - * @return array - */ - protected static function getter_setter_map() - { - return static::config('getter_setter_map'); - } - - ////////////////////////////////////////////////// - - /** - * set configuration data from subclass - * - * @param ?array $config configuration data - * @return void - */ - protected static function configure($config = []) - { - $class = static::class; - - if (empty($config['db_table'])) { - $config['db_table'] = strtolower($class); - } - - if (!isset($config['db_fields'])) { - if (static::tableScheme($config['db_table'])) { - $config['db_fields'] = self::$schemes[$config['db_table']]['db_fields']; - $config['pk'] = self::$schemes[$config['db_table']]['pk']; - } - } - - if (isset($config['pk']) - && !isset($config['db_fields']['id']) - && !isset($config['alias_fields']['id']) - && !isset($config['additional_fields']['id']) - ) { - if (count($config['pk']) === 1) { - $config['alias_fields']['id'] = $config['pk'][0]; - } else { - $config['additional_fields']['id'] = ['get' => '_getId', - 'set' => '_setId']; - } - } - if (isset($config['additional_fields'])) { - foreach ($config['additional_fields'] as $a_field => $a_config) { - if (is_array($a_config) && !(isset($a_config['get']) || isset($a_config['set']))) { - $relation = $a_config[0] ?? ''; - $relation_field = $a_config[1] ?? ''; - if (!$relation) { - list($relation, $relation_field) = explode('_', $a_field); - } - if (!$relation_field || !$relation) { - throw new UnexpectedValueException('no relation found for autoget/set additional field: ' . $a_field); - } - $config['additional_fields'][$a_field] = ['get' => '_getAdditionalValueFromRelation', - 'set' => '_setAdditionalValue', - 'relation' => $relation, - 'relation_field' => $relation_field]; - } - } - } - if (isset($config['serialized_fields'])) { - foreach ($config['serialized_fields'] as $a_field => $object_type) { - if (!(is_subclass_of($object_type, 'StudipArrayObject'))) { - throw new UnexpectedValueException(sprintf('serialized field %s must use subclass of StudipArrayObject', $a_field)); - } - } - } - - foreach (['default_values', 'serialized_fields', 'alias_fields', 'i18n_fields', 'additional_fields'] as $fields) { - if (!isset($config[$fields])) { - $config[$fields] = []; - } - } - - foreach (['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) { - if (isset($config[$type]) && is_array($config[$type])) { - foreach (array_keys($config[$type]) as $one) { - $config['relations'][$one] = null; - } - } else { - $config[$type] = []; - } - } - - $callbacks = ['before_create', - 'before_update', - 'before_store', - 'before_delete', - 'before_initialize', - 'after_create', - 'after_update', - 'after_store', - 'after_delete', - 'after_initialize']; - - foreach ($callbacks as $callback) { - if (!isset($config['registered_callbacks'][$callback])) { - $config['registered_callbacks'][$callback] = []; - } - } - - $auto_notification_map['after_create'] = $class . 'DidCreate'; - $auto_notification_map['after_store'] = $class . 'DidStore'; - $auto_notification_map['after_delete'] = $class . 'DidDelete'; - $auto_notification_map['after_update'] = $class . 'DidUpdate'; - $auto_notification_map['before_create'] = $class . 'WillCreate'; - $auto_notification_map['before_store'] = $class . 'WillStore'; - $auto_notification_map['before_delete'] = $class . 'WillDelete'; - $auto_notification_map['before_update'] = $class . 'WillUpdate'; - - foreach ($auto_notification_map as $cb => $notification) { - if (isset($config['notification_map'][$cb])) { - if (strpos($config['notification_map'][$cb], $notification) !== false) { - $config['notification_map'][$cb] .= ' ' . $notification; - } - } else { - $config['notification_map'][$cb] = $notification; - } - } - - if (is_array($config['notification_map'])) { - foreach (array_keys($config['notification_map']) as $cb) { - $config['registered_callbacks'][$cb][] = 'cbNotificationMapper'; - } - } - - if (!I18N::isEnabled() || empty($config['i18n_fields'])) { - $config['i18n_fields'] = []; - } elseif (is_string($config['i18n_fields'])) { - $i18n_fields = words($config['i18n_fields']); - $config['i18n_fields'] = array_combine( - $i18n_fields, - array_fill(0, count($i18n_fields), true) - ); - } elseif (array_is_list($config['i18n_fields'])) { - $config['i18n_fields'] = array_combine( - $config['i18n_fields'], - array_fill(0, count($config['i18n_fields']), true) - ); - } - - array_unshift($config['registered_callbacks']['after_initialize'], 'cbAfterInitialize'); - - $config['known_slots'] = array_merge( - array_keys($config['db_fields']), - array_keys($config['alias_fields'] ?? []), - array_keys($config['additional_fields'] ?? []), - array_keys($config['relations'] ?? []) - ); - - foreach (array_map('strtolower', get_class_methods($class)) as $method) { - if (in_array(substr($method, 0, 3), ['get', 'set'])) { - $verb = substr($method, 0, 3); - $name = substr($method, 3); - if (in_array($name, $config['known_slots']) && !in_array($name, static::$reserved_slots) && !isset($config['additional_fields'][$name][$verb])) { - $config['getter_setter_map'][$name][$verb] = $method; - } - } - } - self::$config[$class] = $config; - } - - /** - * fetch config data for the called class - * - * @param string $key config key - * @return mixed value of config key (null if not set) - */ - protected static function config($key) - { - if (!array_key_exists(static::class, self::$config)) { - static::configure(); - } - - return self::$config[static::class][$key] ?? null; - } - - /** - * fetch table metadata from db or from local cache - * - * @param string $db_table - * @return bool true if metadata could be fetched - */ - public static function tableScheme($db_table) - { - if (self::$schemes === null) { - $cache = StudipCacheFactory::getCache(); - self::$schemes = unserialize($cache->read('DB_TABLE_SCHEMES')); - } - if (!isset(self::$schemes[$db_table])) { - $db = DBManager::get()->query("SHOW COLUMNS FROM $db_table"); - while($rs = $db->fetch(PDO::FETCH_ASSOC)){ - $db_fields[strtolower($rs['Field'])] = [ - 'name' => $rs['Field'], - 'null' => $rs['Null'], - 'default' => $rs['Default'], - 'type' => $rs['Type'], - 'extra' => $rs['Extra'] - ]; - if ($rs['Key'] == 'PRI'){ - $pk[] = strtolower($rs['Field']); - } - } - self::$schemes[$db_table]['db_fields'] = $db_fields; - self::$schemes[$db_table]['pk'] = $pk; - $cache = StudipCacheFactory::getCache(); - $cache->write('DB_TABLE_SCHEMES', serialize(self::$schemes)); - } - return isset(self::$schemes[$db_table]); - } - - /** - * force reload of cached table metadata - * @return void - */ - public static function expireTableScheme() - { - StudipCacheFactory::getCache()->expire('DB_TABLE_SCHEMES'); - self::$schemes = null; - self::$config = []; - } - - /** - * Returns new instance for given key when found in the database, else null. - * - * @param string $id primary key - * @return static|null - */ - public static function find($id) - { - $ref = new ReflectionClass(static::class); - /** @var static $record */ - $record = $ref->newInstanceArgs(func_get_args()); - if (!$record->isNew()) { - return $record; - } else { - return null; - } - } - - /** - * Returns true if given key exists in the database. - * - * @param string|array $id primary key - * @return boolean - */ - public static function exists($id) - { - $ret = false; - $db_table = static::db_table(); - $record = new static(); - $record->setId(...func_get_args()); - $where_query = $record->getWhereQuery(); - if ($where_query) { - $query = "SELECT 1 FROM `$db_table` WHERE " - . join(" AND ", $where_query); - $ret = (bool)DBManager::get()->query($query)->fetchColumn(); - } - return $ret; - } - - /** - * returns number of records - * - * @param ?string $sql sql clause to use on the right side of WHERE - * @param ?array $params params for query - * @return int - */ - public static function countBySql($sql = '1', $params = []) - { - $db_table = static::db_table(); - $db = DBManager::get(); - $has_join = stripos($sql, 'JOIN '); - if ($has_join === false || $has_join > 10) { - $sql = 'WHERE ' . $sql; - } - $sql = "SELECT count(*) FROM `" . $db_table . "` " . $sql; - $st = $db->prepare($sql); - $st->execute($params); - return (int)$st->fetchColumn(); - } - - /** - * creates new record with given data in db - * returns the new object or null - * @param array $data assoc array of record - * @return ?static - */ - public static function create($data) - { - $record = new static(); - $record->setData($data, false); - if ($record->store()) { - return $record; - } else { - return null; - } - } - - /** - * build object with given data - * - * @param array $data assoc array of record - * @param ?bool $is_new set object to new state - * @return static - */ - public static function build($data, $is_new = true) - { - $record = new static(); - $record->setData($data, !$is_new); - $record->setNew($is_new); - return $record; - } - - /** - * build object with given data and mark it as existing - * - * @param array $data assoc array of record - * @return static - */ - public static function buildExisting($data) - { - return static::build($data, false); - } - - /** - * generate SimpleORMap object structure from assoc array - * if given array contains data of related objects in sub-arrays - * they are also generated. Existing records are updated, new records are created - * (but changes are not yet stored) - * - * @param array $data - * @return static - */ - public static function import($data) - { - $record_data = []; - $relation_data = []; - foreach ($data as $key => $value) { - $temp = static::alias_fields()[$key] ?? $key; - if (isset(static::db_fields()[$temp])) { - $record_data[$key] = $value; - } else { - $relation_data[$key] = $value; - } - } - $record = static::toObject($record_data); - if (!$record instanceof static) { - $record = new static(); - $record->setData($record_data, true); - } else { - $record->setData($record_data); - } - foreach ($relation_data as $relation => $data) { - if (!$record->isRelation($relation)) { - continue; - } - - $options = $record->getRelationOptions($relation); - if ($options['type'] == 'has_one') { - $record->{$relation} = call_user_func([$options['class_name'], 'import'], $data); - } - if ($options['type'] == 'has_many' || $options['type'] == 'has_and_belongs_to_many') { - foreach ($data as $one) { - $current = call_user_func([$options['class_name'], 'import'], $one); - if ($options['type'] == 'has_many') { - $foreign_key_value = call_user_func($options['assoc_func_params_func'], $record); - call_user_func($options['assoc_foreign_key_setter'], $current, $foreign_key_value); - } - if ($current->id !== null) { - $existing = $record->{$relation}->find($current->id); - if ($existing) { - $existing->setData($current); - } else { - $record->{$relation}->append($current); - } - } else { - $record->{$relation}->append($current); - } - } - } - } - return $record; - } - - /** - * returns array of instances of given class filtered by given sql - * @param string $sql sql clause to use on the right side of WHERE - * @param ?array $params parameters for query - * @return static[] array of "self" objects - */ - public static function findBySQL($sql, $params = []) - { - $db_table = static::db_table(); - - $has_join = stripos($sql, 'JOIN '); - if ($has_join === false || $has_join > 10) { - $sql = 'WHERE ' . $sql; - } - $sql = "SELECT `" . $db_table . "`.* FROM `" . $db_table . "` " . $sql; - $stmt = DBManager::get()->prepare($sql); - $stmt->execute($params); - - $record = static::build([], false); - - $ret = []; - do { - $clone = clone $record; - $clone->setNew(false); - $stmt->setFetchMode(PDO::FETCH_INTO, $clone); - - if ($clone = $stmt->fetch()) { - $clone->applyCallbacks('after_initialize'); - $ret[] = $clone; - } - } while ($clone); - return $ret; - } - - /** - * returns one instance of given class filtered by given sql - * only first row of query is used - * @param string $where sql clause to use on the right side of WHERE - * @param ?array $params parameters for query - * @return ?static - */ - public static function findOneBySQL($where, $params = []) - { - if (stripos($where, 'LIMIT') === false) { - $where .= " LIMIT 1"; - } - $found = static::findBySQL($where, $params); - return isset($found[0]) ? $found[0] : null; - } - - /** - * find related records for a n:m relation (has_many_and_belongs_to_many) - * using a combination table holding the keys - * - * @param string $foreign_key_value value of foreign key to find related records - * @param array $options relation options from other side of relation - * @return static[] array of "self" objects - */ - public static function findThru($foreign_key_value, $options) - { - $thru_table = $options['thru_table']; - $thru_key = $options['thru_key']; - $thru_assoc_key = $options['thru_assoc_key']; - $assoc_foreign_key = $options['assoc_foreign_key']; - - $db_table = static::db_table(); - - $sql = "SELECT `$db_table`.* FROM `$thru_table` - INNER JOIN `$db_table` ON `$thru_table`.`$thru_assoc_key` = `$db_table`.`$assoc_foreign_key` - WHERE `$thru_table`.`$thru_key` = ? " . ($options['order_by'] ?? ''); - $st = DBManager::get()->prepare($sql); - $st->execute([$foreign_key_value]); - - $record = static::build([], false); - - $ret = []; - do { - $clone = clone $record; - $clone->setNew(false); - $st->setFetchMode(PDO::FETCH_INTO, $clone); - - if ($clone = $st->fetch()) { - $clone->applyCallbacks('after_initialize'); - $ret[] = $clone; - } - } while ($clone); - return $ret; - } - - /** - * passes objects for given sql through given callback - * - * @param callable $callable callback which gets the current record as param - * @param string $sql where clause of sql - * @param ?array $params sql statement parameters - * @return integer number of found records - */ - public static function findEachBySQL($callable, $sql, $params = []) - { - $has_join = stripos($sql, 'JOIN '); - if ($has_join === false || $has_join > 10) { - $sql = "WHERE {$sql}"; - } - - $db_table = static::db_table(); - $st = DBManager::get()->prepare("SELECT `{$db_table}`.* FROM `{$db_table}` {$sql}"); - $st->execute($params); - - // Indicate that we are performing a batch operation - static::$performs_batch_operation = true; - - $record = static::build([], false); - - $ret = 0; - do { - $clone = clone $record; - $clone->setNew(false); - $st->setFetchMode(PDO::FETCH_INTO, $clone); - - if ($clone = $st->fetch()) { - $clone->applyCallbacks('after_initialize'); - $callable($clone, $ret++); - } - } while ($clone); - - // Reset batch operation indicator - static::$performs_batch_operation = false; - - return $ret; - } - - /** - * returns array of instances of given class for by given pks - * @param ?array $pks array of primary keys - * @param ?string $order order by clause - * @param ?array $order_params - * @return static[] - */ - public static function findMany($pks = [], $order = '', $order_params = []) - { - $db_table = static::db_table(); - $pk = static::pk(); - $db = DBManager::get(); - if (count($pk) > 1) { - throw new Exception('not implemented yet'); - } - $where = "`$db_table`.`{$pk[0]}` IN (" . $db->quote($pks) . ") "; - return static::findBySQL($where . $order, $order_params); - } - - /** - * passes objects for by given pks through given callback - * - * @param callable $callable callback which gets the current record as param - * @param ?array $pks array of primary keys of called class - * @param ?string $order order by sql - * @param ?array $order_params - * @return integer number of found records - */ - public static function findEachMany($callable, $pks = [], $order = '', $order_params = []) - { - $db_table = static::db_table(); - $pk = static::pk(); - $db = DBManager::get(); - if (count($pk) > 1) { - throw new Exception('not implemented yet'); - } - $where = "`$db_table`.`{$pk[0]}` IN (" . $db->quote($pks) . ") "; - return static::findEachBySQL($callable, $where . $order, $order_params); - } - - /** - * passes objects for given sql through given callback - * and returns an array of callback return values - * - * @param callable $callable callback which gets the current record as param - * @param string $where where clause of sql - * @param array $params sql statement parameters - * @return array return values of callback - */ - public static function findAndMapBySQL($callable, $where, $params = []) - { - $ret = []; - $calleach = function($m) use (&$ret, $callable) { - $ret[] = $callable($m); - }; - static::findEachBySQL($calleach, $where, $params); - return $ret; - } - - /** - * passes objects for by given pks through given callback - * and returns an array of callback return values - * - * @param callable $callable callback which gets the current record as param - * @param ?array $pks array of primary keys of called class - * @param ?string $order order by sql - * @param ?array $order_params - * @return array return values of callback - */ - public static function findAndMapMany($callable, $pks = [], $order = '', $order_params = []) - { - $ret = []; - $calleach = function($m) use (&$ret, $callable) { - $ret[] = $callable($m); - }; - $db_table = static::db_table(); - $pk = static::pk(); - $db = DBManager::get(); - if (count($pk) > 1) { - throw new Exception('not implemented yet'); - } - $where = "`$db_table`.`{$pk[0]}` IN (" . $db->quote($pks) . ") "; - static::findEachBySQL($calleach, $where . $order, $order_params); - return $ret; - } - - /** - * deletes objects specified by sql clause - * @param string $where sql clause to use on the right side of WHERE - * @param ?array $params parameters for query - * @return integer number of deleted records - */ - public static function deleteBySQL($where, $params = []) - { - $killeach = function($record) {$record->delete();}; - return static::findEachBySQL($killeach, $where, $params); - } - - /** - * returns object of given class for given id or null - * the param could be a string, an assoc array containing primary key field - * or an already matching object. In all these cases an object is returned - * - * @param string|static|array $id_or_object id as string, object or assoc array - * @return static - */ - public static function toObject($id_or_object) - { - if ($id_or_object instanceof static) { - return $id_or_object; - } - if (is_array($id_or_object)) { - $pk = static::pk(); - $key_values = []; - foreach ($pk as $key) { - if (array_key_exists($key, $id_or_object)) { - $key_values[] = $id_or_object[$key]; - } - } - if (count($pk) === count($key_values)) { - if (count($pk) === 1) { - $id = $key_values[0]; - } else { - $id = $key_values; - } - } else { - $id = null; - } - } else { - $id = $id_or_object; - } - return static::find($id); - } - - /** - * interceptor for static findByColumn / findEachByColumn / countByColumn / - * deleteByColumn magic - * - * @param string $name - * @param array $arguments - * @throws BadMethodCallException - * @return int|static|static[] - */ - public static function __callStatic(string $name, array $arguments) - { - $db_table = static::db_table(); - $alias_fields = static::alias_fields(); - $db_fields = static::db_fields(); - $name = strtolower($name); - $order = ''; - $param_arr = []; - $where = ''; - $where_param = is_array($arguments[0]) ? $arguments[0] : [$arguments[0]]; - $action = strstr($name, 'by', true); - $field = substr($name, strlen($action) + 2); - switch ($action) { - case 'findone': - $order = $arguments[1] ?? ''; - $param_arr[0] =& $where; - $param_arr[1] = [$where_param]; - $method = 'findonebysql'; - break; - case 'find': - case 'findmany': - $order = $arguments[1] ?? ''; - $param_arr[0] =& $where; - $param_arr[1] = [$where_param]; - $method = 'findbysql'; - break; - case 'findeach': - case 'findeachmany': - $order = $arguments[2] ?? ''; - $param_arr[0] = $arguments[0]; - $param_arr[1] =& $where; - $param_arr[2] = [$arguments[1]]; - $method = 'findeachbysql'; - break; - case 'count': - case 'delete': - $param_arr[0] =& $where; - $param_arr[1] = [$where_param]; - $method = "{$action}bysql"; - break; - default: - throw new BadMethodCallException("Method " . static::class . "::$name not found"); - } - if (isset($alias_fields[$field])) { - $field = $alias_fields[$field]; - } - if (isset($db_fields[$field])) { - $where = "`$db_table`.`$field` IN(?) " . $order; - return call_user_func_array([static::class, $method], $param_arr); - } - throw new BadMethodCallException("Method " . static::class . "::$name not found"); - } - - /** - * constructor, give primary key of record as param to fetch - * corresponding record from db if available, if not preset primary key - * with given value. Give null to create new record - * - * @param null|int|string|array $id primary key of table - */ - function __construct($id = null) - { - foreach(['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) { - foreach (array_keys($this->$type()) as $one) { - $this->relations[$one] = null; - } - } - - if ($id) { - $this->setId($id); - } - $this->restore(); - } - - /** - * returns internal used id value (multiple keys concatenated with _) - * @param mixed $field unused parameter - * @return ?string - */ - protected function _getId($field) - { - return is_null($this->getId()) - ? null - : implode(self::ID_SEPARATOR, $this->getId()); - } - - /** - * sets internal used id value (multiple keys concatenated with _) - * @param string $field Field to set (unused since it's always the id) - * @param string $value Value for id field - * @return bool - */ - protected function _setId($field, $value) - { - return $this->setId(explode(self::ID_SEPARATOR, $value)); - } - - /** - * retrieves an additional field value from relation - * - * @param string $field - * @return mixed - */ - protected function _getAdditionalValueFromRelation($field) - { - list($relation, $relation_field) = [$this->additional_fields()[$field]['relation'], - $this->additional_fields()[$field]['relation_field']]; - if (!array_key_exists($field, $this->additional_data)) { - $this->_setAdditionalValue($field, $this->getRelationValue($relation, $relation_field)); - } - return $this->additional_data[$field]; - } - - /** - * sets additional value in field imported from relation - * - * @param string $field - * @param mixed $value - * @return mixed - */ - protected function _setAdditionalValueFromRelation($field, $value) - { - list($relation, $relation_field) = [$this->additional_fields()[$field]['relation'], - $this->additional_fields()[$field]['relation_field']]; - $this->$relation->$field = $value; - unset($this->additional_data[$field]); - return $this->_getAdditionalValueFromRelation($field); - } - - /** - * @param string $field - * @return mixed - */ - protected function _getAdditionalValue($field) - { - return $this->additional_data[$field]; - } - - /** - * @param string $field - * @param mixed $value - * @return mixed - */ - protected function _setAdditionalValue($field, $value) - { - return $this->additional_data[$field] = $value; - } - - /** - * clean up references after cloning - * @return void - */ - function __clone() - { - $this->setNew(true); - //all references link still to old object => reset all aliases - foreach ($this->alias_fields() as $alias => $field) { - if (isset($this->db_fields()[$field])) { - $content_value = $this->content[$field]; - $content_db_value = $this->content_db[$field]; - unset($this->content[$alias]); - unset($this->content_db[$alias]); - unset($this->content[$field]); - unset($this->content_db[$field]); - if (is_object($content_value)) { - $this->content[$field] = clone $content_value; - } else { - $this->content[$field] = $content_value; - } - if (is_object($content_db_value)) { - $this->content_db[$field] = clone $content_db_value; - } else { - $this->content_db[$field] = $content_db_value; - } - } - } - foreach ($this->alias_fields() as $alias => $field) { - if (isset($this->db_fields()[$field])) { - $this->content[$alias] =& $this->content[$field]; - $this->content_db[$alias] =& $this->content_db[$field]; - } - } - //unset all relations for now - //TODO: maybe a deep copy of all belonging objects is more appropriate - foreach(['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) { - foreach (array_keys($this->$type()) as $one) { - $this->relations[$one] = null; - } - } - //begun the clone war has... hmpf - } - - /** - * try to determine all needed options for a relationship from - * configured options - * - * @param string $type - * @param string $name - * @param array $options - * @throws Exception if options for thru_table could not be determined - * @return array - */ - protected function parseRelationOptions($type, $name, $options) - { - if (empty($options['class_name'])) { - throw new Exception('Option class_name not set for relation ' . $name); - } - if (empty($options['assoc_foreign_key'])) { - if ($type === 'has_many' || $type === 'has_one') { - $options['assoc_foreign_key'] = $this->pk()[0]; - } else if ($type === 'belongs_to') { - $options['assoc_foreign_key'] = 'id'; - } - } - if ($type === 'has_and_belongs_to_many') { - $thru_table = $options['thru_table']; - if (empty($options['thru_key'])) { - $options['thru_key'] = $this->pk()[0]; - } - if (empty($options['thru_assoc_key']) || empty($options['assoc_foreign_key'])) { - $class = $options['class_name']; - $record = new $class(); - $meta = $record->getTableMetadata(); - if (empty($options['thru_assoc_key'])) { - $options['thru_assoc_key'] = $meta['pk'][0]; - } - if (empty($options['assoc_foreign_key'])) { - $options['assoc_foreign_key']= $meta['pk'][0]; - } - } - static::tableScheme($thru_table); - if (is_array(self::$schemes[$thru_table])) { - $thru_key_ok = isset(self::$schemes[$thru_table]['db_fields'][$options['thru_key']]); - $thru_assoc_key_ok = isset(self::$schemes[$thru_table]['db_fields'][$options['thru_assoc_key']]); - } - if (!$thru_assoc_key_ok || !$thru_key_ok) { - throw new Exception("Could not determine keys for relation " . $name . " through table " . $thru_table); - } - if ($options['assoc_foreign_key'] instanceof Closure) { - throw new Exception("For relation " . $name . " assoc_foreign_key must be a name of a column"); - } - } - if (empty($options['assoc_func'])) { - if ($type !== 'has_and_belongs_to_many') { - $options['assoc_func'] = $options['assoc_foreign_key'] === 'id' ? 'find' : 'findBy' . $options['assoc_foreign_key']; - } else { - $options['assoc_func'] = 'findThru'; - } - } - if (empty($options['foreign_key'])) { - $options['foreign_key'] = 'id'; - } - if (isset($options['foreign_key']) && $options['foreign_key'] instanceof Closure) { - $options['assoc_func_params_func'] = function($record) use ($name, $options) { return call_user_func($options['foreign_key'], $record, $name, $options);}; - } else { - $options['assoc_func_params_func'] = function($record) use ($name, $options) { return $options['foreign_key'] === 'id' ? $record->getId() : $record->getValue($options['foreign_key']);}; - } - if (isset($options['assoc_foreign_key']) && $options['assoc_foreign_key'] instanceof Closure) { - if ($type === 'belongs_to') { - $options['assoc_foreign_key_getter'] = function($record, $that) use ($name, $options) { return call_user_func($options['assoc_foreign_key'], $record, $name, $options, $that);}; - } else { - $options['assoc_foreign_key_setter'] = function($record, $params) use ($name, $options) { return call_user_func($options['assoc_foreign_key'], $record, $params, $name, $options);}; - } - } elseif (!empty($options['assoc_foreign_key'])) { - if ($type === 'belongs_to') { - $options['assoc_foreign_key_getter'] = function($record, $that) use ($name, $options) { return $record->getValue($options['assoc_foreign_key']);}; - } else { - $options['assoc_foreign_key_setter'] = function($record, $value) use ($name, $options) { return $record->setValue($options['assoc_foreign_key'], $value);}; - } - } else { - throw new Exception("Could not determine assoc_foreign_key for relation " . $name); - } - return $options; - } - - /** - * returns array with option for given relation - * available options: - * 'type': relation type, on of 'has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many' - * 'class_name': name of class for related records - * 'foreign_key': name of column with foreign key - * or callback to retrieve foreign key value - * 'assoc_foreign_key': name of foreign key column in related class - * 'assoc_func': name of static method to call on related class to find related records - * 'assoc_func_params_func': callback to retrieve params for assoc_func - * 'thru_table': name of relation table for n:m relation - * 'thru_key': name of column holding foreign key in relation table - * 'thru_assoc_key': name of column holding foreign key from related class in relation table - * 'on_delete': contains simply 'delete' to indicate that related records should be deleted - * or callback to invoke before record gets deleted - * 'on_store': contains simply 'store' to indicate that related records should be stored - * or callback to invoke after record gets stored - * - * @param string $relation name of relation - * @return array assoc array containing options - */ - function getRelationOptions($relation) - { - $options = []; - foreach(['has_many', 'belongs_to', 'has_one', 'has_and_belongs_to_many'] as $type) { - if (isset($this->$type()[$relation])) { - $options = self::$config[get_class($this)][$type][$relation]; - if (!isset($options['type'])) { - $options = $this->parseRelationOptions($type, $relation, $options, $this->db_table()); - $options['type'] = $type; - self::$config[get_class($this)][$type][$relation] = $options; - } - break; - } - } - return $options; - } - - /** - * returns table and columns metadata - * - * @return array assoc array with columns, primary keys and name of table - */ - function getTableMetadata() - { - return ['fields' => $this->db_fields(), - 'pk' => $this->pk(), - 'table' => $this->db_table(), - 'additional_fields' => $this->additional_fields(), - 'alias_fields' => $this->alias_fields(), - 'relations' => array_keys($this->relations)]; - } - - /** - * returns true, if table has an auto_increment column - * - * @return boolean - */ - function hasAutoIncrementColumn() - { - return $this->db_fields()[$this->pk()[0]]['extra'] == 'auto_increment'; - } - - /** - * set primary key for entry, combined keys must be passed as array - * @param int|string|array $id primary key - * @throws InvalidArgumentException if given key is not complete - * @return boolean - */ - public function setId($id) - { - if (!is_array($id)){ - $id = [$id]; - } - if (count($this->pk()) != count($id)){ - throw new InvalidArgumentException("Invalid ID, Primary Key {$this->db_table()} is " .join(",",$this->pk())); - } else { - foreach ($this->pk() as $count => $key){ - $this->content[$key] = $id[$count]; - } - return true; - } - } - - /** - * returns primary key, multiple keys as array - * @return null|string|array current primary key, null if not set - */ - function getId() - { - if (count($this->pk()) == 1) { - return $this->content[$this->pk()[0]] ?? null; - } else { - $id = []; - foreach ($this->pk() as $key) { - if (array_key_exists($key, $this->content)) { - $id[] = $this->content[$key]; - } - } - return (count($this->pk()) == count($id) ? $id : null); - } - } - - /** - * create new unique pk as md5 hash - * if pk consists of multiple columns, false is returned - * @return boolean|string - */ - function getNewId() - { - $id = false; - if (count($this->pk()) == 1) { - do { - $id = md5(uniqid($this->db_table(), 1)); - $db = DBManager::get()->query("SELECT `{$this->pk()[0]}` FROM `{$this->db_table()}` " - . "WHERE `{$this->pk()[0]}` = '$id'"); - } while($db->fetch()); - } - return $id; - } - - /** - * returns data of table row as assoc array - * pass array of fieldnames or ws separated string to limit - * fields - * - * @param null|array|string $only_these_fields limit returned fields - * @return array - */ - function toArray($only_these_fields = null) - { - $ret = []; - if (is_string($only_these_fields)) { - $only_these_fields = words($only_these_fields); - } - $fields = array_diff($this->known_slots(), array_keys($this->relations)); - if (is_array($only_these_fields)) { - $only_these_fields = array_filter(array_map(function($s) { - return is_string($s) ? strtolower($s) : null; - }, $only_these_fields)); - $fields = array_intersect($only_these_fields, $fields); - } - foreach ($fields as $field) { - $ret[$field] = $this->getValue($field); - if ($ret[$field] instanceof StudipArrayObject) { - $ret[$field] = $ret[$field]->getArrayCopy(); - } - } - return $ret; - } - - /** - * Returns data of table row as assoc array with raw contents like - * they are in the database. - * Pass array of fieldnames or ws separated string to limit - * fields. - * - * @param null|array|string $only_these_fields - * @return array - */ - function toRawArray($only_these_fields = null) - { - $ret = []; - if (is_string($only_these_fields)) { - $only_these_fields = words($only_these_fields); - } - $fields = array_keys($this->db_fields()); - if (is_array($only_these_fields)) { - $only_these_fields = array_filter(array_map(function ($s) { - return is_string($s) ? strtolower($s) : null; - }, $only_these_fields)); - $fields = array_intersect($only_these_fields, $fields); - } - foreach ($fields as $field) { - if ($this->content[$field] instanceof I18NString) { - $ret[$field] = $this->content[$field]->original(); - } elseif ($this->content[$field] === null) { - $ret[$field] = null; - } else { - $ret[$field] = (string)$this->content[$field]; - } - } - return $ret; - } - - /** - * returns data of table row as assoc array - * including related records with a 'has*' relationship - * recurses one level without param - * - * $only_these_fields limits output for relationships in this way: - * $only_these_fields = array('field_1', - * 'field_2', - * 'relation1', - * 'relation2' => array('rel2_f1', - * 'rel2_f2', - * 'rel2_rel11' => array( - * rel2_rel1_f1) - * ) - * ) - * Here all fields of relation1 will be returned. - * - * @param null|array|string $only_these_fields limit returned fields - * @return array - */ - function toArrayRecursive($only_these_fields = null) - { - if (is_string($only_these_fields)) { - $only_these_fields = words($only_these_fields); - } - if (is_null($only_these_fields)) { - $only_these_fields = $this->known_slots(); - } - $ret = $this->toArray($only_these_fields); - $relations = []; - if (is_array($only_these_fields)) { - foreach ($only_these_fields as $key => $value) { - if (!is_array($value) && - array_key_exists(strtolower($value), $this->relations) - ) { - $relations[strtolower($value)] = 0; //not null|array|string to stop recursion - } - if (array_key_exists(strtolower($key), $this->relations)) { - $relations[strtolower($key)] = $value; - } - } - } - if (count($relations)) { - foreach ($relations as $relation_name => $relation_only_these_fields) { - $options = $this->getRelationOptions($relation_name); - if ($options['type'] === 'has_one' || - $options['type'] === 'belongs_to') { - $ret[$relation_name] = - $this->{$relation_name}-> - toArrayRecursive($relation_only_these_fields); - } - if ($options['type'] === 'has_many' || - $options['type'] === 'has_and_belongs_to_many') { - $ret[$relation_name] = - $this->{$relation_name}-> - sendMessage('toArrayRecursive', - [$relation_only_these_fields]); - } - } - } - return $ret; - } - - /** - * returns value of a column - * - * @throws InvalidArgumentException if column could not be found - * @throws BadMethodCallException if getter for additional field could not be found - * @param string $field - * @return null|string|SimpleORMapCollection - */ - public function getValue($field) - { - $field = strtolower($field); - - // No value defined, throw exception - if (!in_array($field, $this->known_slots())) { - throw new InvalidArgumentException(static::class . '::'.$field . ' not found.'); - } - - // Get value by getter - if (isset($this->getter_setter_map()[$field]['get'])) { - return call_user_func([$this, $this->getter_setter_map()[$field]['get']]); - } - - // Get value from content - if (array_key_exists($field, $this->content)) { - return $this->content[$field]; - } - - // Get value from relation - if (array_key_exists($field, $this->relations)) { - $this->initRelation($field); - return $this->relations[$field]; - } - - // Get value from additional_field - if (isset($this->additional_fields()[$field]['get'])) { - // Getter is defined as a closure - if ($this->additional_fields()[$field]['get'] instanceof Closure) { - return call_user_func_array($this->additional_fields()[$field]['get'], [$this, $field]); - } - - // Getter is defined as a method of this object - return call_user_func([$this, $this->additional_fields()[$field]['get']], $field); - } - - // No value found, throw exception - throw new RuntimeException('No value could be found for ' . static::class . '::' . $field); - } - - /** - * gets a value from a related object - * only possible, if the relation has cardinality 1 - * e.g. 'has_one' or 'belongs_to' - * - * @param string $relation name of relation - * @param string $field name of column - * @throws InvalidArgumentException if no relation with given name is found - * @return mixed the value from the related object - */ - function getRelationValue($relation, $field) - { - $field = strtolower($field); - $options = $this->getRelationOptions($relation); - if ($options['type'] === 'has_one' || $options['type'] === 'belongs_to') { - return $this->{$relation}->{$field} ?? null; - } else { - throw new InvalidArgumentException('Relation ' . $relation . ' not found or not applicable.'); - } - } - - /** - * returns default value for column - * - * @param string $field name of column - * @return mixed the default value - */ - function getDefaultValue($field) - { - $default_value = null; - if (!isset($this->default_values()[$field])) { - if (!in_array($field, $this->pk())) { - $meta = $this->db_fields()[$field]; - if (isset($meta['default'])) { - $default_value = $meta['default']; - } elseif ($meta['null'] == 'NO') { - if (strpos($meta['type'], 'text') !== false || strpos($meta['type'], 'char') !== false) { - $default_value = ''; - } - if (strpos($meta['type'], 'int') !== false) { - $default_value = '0'; - } - } - } - } else { - $default_value = $this->default_values()[$field]; - } - return $default_value; - } - - /** - * sets value of a column - * - * @throws InvalidArgumentException if column could not be found - * @throws BadMethodCallException if setter for additional field could not be found - * @param string $field - * @param mixed $value - * @return string - */ - function setValue($field, $value) - { - $field = strtolower($field); - $ret = false; - if (in_array($field, $this->known_slots())) { - if (isset($this->getter_setter_map()[$field]['set'])) { - return call_user_func([$this, $this->getter_setter_map()[$field]['set']], $value); - } - if (array_key_exists($field, $this->content)) { - if (array_key_exists($field, $this->serialized_fields())) { - $ret = $this->setSerializedValue($field, $value); - } elseif ($this->isI18nField($field)) { - $ret = $this->setI18nValue($field, $value); - } else { - $ret = ($this->content[$field] = $value); - } - } elseif (isset($this->additional_fields()[$field]['set'])) { - if ($this->additional_fields()[$field]['set'] instanceof Closure) { - return call_user_func_array($this->additional_fields()[$field]['set'], [$this, $field, $value]); - } else { - return call_user_func([$this, $this->additional_fields()[$field]['set']], $field, $value); - } - } elseif (array_key_exists($field, $this->relations)) { - $options = $this->getRelationOptions($field); - if ($options['type'] === 'has_one' || $options['type'] === 'belongs_to') { - if (is_a($value, $options['class_name'])) { - $this->relations[$field] = $value; - if ($options['type'] == 'has_one') { - $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this); - call_user_func($options['assoc_foreign_key_setter'], $value, $foreign_key_value); - } else { - $assoc_foreign_key_value = call_user_func($options['assoc_foreign_key_getter'], $value, $this); - if ($assoc_foreign_key_value === null) { - throw new InvalidArgumentException(sprintf('trying to set belongs_to object of type: %s, but assoc_foreign_key: %s is null', get_class($value), $options['assoc_foreign_key'])); - } - $this->setValue($options['foreign_key'], $assoc_foreign_key_value); - } - } else { - throw new InvalidArgumentException(sprintf('relation %s expects object of type: %s', $field, $options['class_name'])); - } - } - if ($options['type'] == 'has_many' || $options['type'] == 'has_and_belongs_to_many') { - if (is_array($value) || $value instanceof Traversable) { - $new_ids = []; - $old_ids = $this->{$field}->pluck('id'); - foreach ($value as $current) { - if (!is_a($current, $options['class_name'])) { - throw new InvalidArgumentException(sprintf('relation %s expects object of type: %s', $field, $options['class_name'])); - } - if ($options['type'] == 'has_many') { - $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this); - call_user_func($options['assoc_foreign_key_setter'], $current, $foreign_key_value); - } - if ($current->id !== null) { - $new_ids[] = $current->id; - $existing = $this->{$field}->find($current->id); - if ($existing) { - $existing->setData($current); - } else { - $this->{$field}->append($current); - } - } else { - $this->{$field}->append($current); - } - } - foreach (array_diff($old_ids, $new_ids) as $to_delete) { - $this->{$field}->unsetByPK($to_delete); - } - } else { - throw new InvalidArgumentException(sprintf('relation %s expects collection or array of objects of type: %s', $field, $options['class_name'])); - } - } - } - } else { - throw new InvalidArgumentException(get_class($this) . '::'. $field . ' not found.'); - } - return $ret; - } - - /** - * magic method for dynamic properties - * - * @throws InvalidArgumentException if column could not be found - * @throws BadMethodCallException if getter for additional field could not be found - * @param string $field the column or additional field - * @return null|string|SimpleORMapCollection - */ - function __get($field) - { - return $this->getValue($field); - } - /** - * magic method for dynamic properties - * - * @throws InvalidArgumentException if column could not be found - * @throws BadMethodCallException if setter for additional field could not be found - * @param string $field - * @param string $value - * @return string - */ - function __set($field, $value) - { - return $this->setValue($field, $value); - } - /** - * magic method for dynamic properties - * - * @param string $field - * @return bool - */ - function __isset($field) - { - $field = strtolower($field); - if (in_array($field, $this->known_slots())) { - $value = $this->getValue($field); - return $value instanceOf SimpleORMapCollection ? (bool)count($value) : !is_null($value); - } else { - return false; - } - } - - /** - * ArrayAccess: Check whether the given offset exists. - * - * @param string $offset - */ - public function offsetExists($offset): bool - { - return $this->__isset($offset); - } - - /** - * ArrayAccess: Get the value at the given offset. - * - * @throws InvalidArgumentException if column could not be found - * @throws BadMethodCallException if getter for additional field could not be found - * @param string $offset the column or additional field - * @return null|string|SimpleORMapCollection - */ - public function offsetGet($offset): mixed - { - return $this->getValue($offset); - } - - /** - * ArrayAccess: Set the value at the given offset. - * - * @throws InvalidArgumentException if column could not be found - * @throws BadMethodCallException if setter for additional field could not be found - * @param string $offset - * @param mixed $value - */ - public function offsetSet($offset, $value): void - { - $this->setValue($offset, $value); - } - - /** - * ArrayAccess: unset the value at the given offset (not applicable) - * - * @param string $offset - */ - public function offsetUnset($offset): void - { - - } - - /** - * IteratorAggregate - */ - public function getIterator(): ArrayIterator - { - return new ArrayIterator($this->toArray()); - } - - /** - * Countable - */ - public function count(): int - { - return count($this->known_slots()) - count($this->relations); - } - - /** - * check if given column exists in table - * @param string $field - * @return boolean - */ - function isField($field) - { - $field = strtolower($field); - return isset($this->db_fields()[$field]); - } - - /** - * check if given relation exists in this class - * @param string $field - * @return boolean - */ - function isRelation($field) - { - $field = strtolower($field); - return array_key_exists($field, $this->relations); - } - - /** - * check if given column is additional - * @param string $field - * @return boolean - */ - function isAdditionalField($field) - { - $field = strtolower($field); - return isset($this->additional_fields()[$field]); - } - - /** - * check if given column is an alias - * @param string $field - * @return boolean - */ - function isAliasField($field) - { - $field = strtolower($field); - return isset($this->alias_fields()[$field]); - } - - /** - * check if given column is a multi-language field - * @param string $field - * @return boolean - */ - function isI18nField($field) - { - $field = strtolower($field); - return isset($this->i18n_fields()[$field]); - } - - /** - * set multiple column values - * if second param is set, existing data in object will be - * discarded and dirty state is cleared, - * else new data overrides old data - * - * @param ?iterable $data assoc array - * @param ?boolean $reset existing data in object will be discarded - * @return int|bool number of columns changed - */ - function setData($data, $reset = false) - { - $count = 0; - if ($reset) { - if ($this->applyCallbacks('before_initialize') === false) { - return false; - } - $this->initializeContent(); - } - if (is_iterable($data)) { - foreach($data as $key => $value) { - $key = strtolower($key); - if (isset($this->db_fields()[$key]) - || isset($this->alias_fields()[$key]) - || isset($this->additional_fields()[$key]['set']) - ) { - $this->setValue($key, $value); - ++$count; - } - } - } - if ($reset) { - $this->applyCallbacks('after_initialize'); - } - return $count; - } - - /** - * check if object exists in database - * @return boolean - */ - function isNew() - { - return $this->is_new; - } - - /** - * check if object was deleted - * - * @return boolean - */ - function isDeleted() - { - return $this->is_deleted; - } - - /** - * set object to new state - * @param boolean $is_new - * @return boolean - */ - function setNew($is_new) - { - return $this->is_new = $is_new; - } - - /** - * returns sql clause with current table and pk - * @throws UnexpectedValueException if the primary key is incomplete - * @return boolean|array<string> - */ - function getWhereQuery() - { - $where_query = null; - $pk_not_set = []; - foreach ($this->pk() as $key) { - $pk = $this->content_db[$key] ?? $this->content[$key] ?? null; - if (isset($pk)) { - $where_query[] = "`{$this->db_table()}`.`{$key}` = " . DBManager::get()->quote($pk); - } else { - $pk_not_set[] = $key; - } - } - if (!$where_query || count($pk_not_set)){ - if ($this->isNew()) { - return false; - } else { - throw new UnexpectedValueException(sprintf("primary key incomplete: %s must not be null", join(',',$pk_not_set))); - } - } - return $where_query; - } - - /** - * restore entry from database - * @return boolean - */ - function restore() - { - $where_query = $this->getWhereQuery(); - $id = $this->getId(); - if ($where_query) { - if ($this->applyCallbacks('before_initialize') === false) { - return false; - } - $this->initializeContent(); - $query = "SELECT * FROM `{$this->db_table()}` WHERE " - . join(" AND ", $where_query); - $st = DBManager::get()->prepare($query); - $st->execute(); - $st->setFetchMode(PDO::FETCH_INTO , $this); - if ($st->fetch()) { - $this->setNew(false); - $this->applyCallbacks('after_initialize'); - return true; - } - } - $this->setData([], true); - $this->setNew(true); - if (isset($id)) { - $this->setId($id); - } - return false; - } - - /** - * store entry in database - * - * @throws UnexpectedValueException if there are forbidden NULL values - * @return number|boolean - */ - function store() - { - // Set id or prepare setting of id - if ($this->isNew() && $this->getId() === null) { - // Explicitly set id to 0 if auto increment pk is null - if ($this->hasAutoIncrementColumn()) { - $this->setId(0); - } else { - $this->setId($this->getNewId()); - } - } - - if ($this->applyCallbacks('before_store') === false) { - return false; - } - - $ret = 0; - - if (!$this->isDeleted() && ($this->isDirty() || $this->isNew())) { - $callback = $this->isNew() ? 'before_create' : 'before_update'; - if ($this->applyCallbacks($callback) === false) { - return false; - } - - // Collect i18n contents - $i18ncontent = []; - foreach (array_keys($this->i18n_fields()) as $field) { - if ($this->content[$field] instanceof I18NString) { - $i18ncontent[$field] = $this->content[$field]; - $this->content[$field] = $this->content[$field]->original(); - $this->content_db[$field] = $this->content_db[$field]->original(); - } - } - - // Create sql data assignment chunks - foreach ($this->db_fields() as $field => $meta) { - $value = $this->content[$field]; - if ($field == 'chdate' && !$this->isFieldDirty($field) && $this->isDirty()) { - $value = time(); - } - if ($field == 'mkdate') { - if ($this->isNew()) { - if (!$this->isFieldDirty($field)) { - $value = time(); - } - } else { - continue; - } - } - if ($value === null && $meta['null'] == 'NO') { - throw new UnexpectedValueException($this->db_table() . '.' . $field . ' must not be null.'); - } - if (is_float($value)) { - $value = str_replace(',', '.', $value); - } - $this->content[$field] = $value; - $query_part[] = "`$field` = " . DBManager::get()->quote($value) . " "; - } - - // Create store query - if (!$this->isNew()) { - $where_query = $this->getWhereQuery(); - $query = "UPDATE `{$this->db_table()}` SET " - . implode(',', $query_part); - $query .= " WHERE " . join(" AND ", $where_query); - } else { - $query = "INSERT INTO `{$this->db_table()}` SET " - . implode(',', $query_part); - } - $ret = DBManager::get()->exec($query); - - // Retrieve generated id from database if pk is an auto increment - // column - if ($this->isNew()) { - if ($this->hasAutoIncrementColumn() && !$this->getId()) { - $this->setId(DBManager::get()->lastInsertId()); - } - } - - // Store i18n contents - foreach ($i18ncontent as $field => $one) { - $meta = [ - 'object_id' => $this->getId(), - 'table' => $this->db_table(), - 'field' => $field - ]; - $one->setMetadata($meta); - $one->storeTranslations(); - if (!$this->content[$field] instanceof I18NString) { - $this->content[$field] = $one; - $this->content_db[$field] = clone $one; - } - } - - // Apply callbacks - $this->applyCallbacks($this->isNew() ? 'after_create' : 'after_update'); - } - $rel_ret = $this->storeRelations(); - - $this->applyCallbacks('after_store'); - - if ($ret || $rel_ret) { - $this->restore(); - } - return $ret + $rel_ret; - } - - /** - * sends a store message to all initialized related objects - * if a relation has a callback for 'on_store' configured, the callback - * is instead invoked - * - * @param null|array|string $only_these - * @return int|false number addition of all return values, false if none was called - */ - protected function storeRelations($only_these = null) - { - $ret = false; - if (is_string($only_these)) { - $only_these = words($only_these); - } - $relations = array_keys($this->relations); - if (is_array($only_these)) { - $only_these = array_filter(array_map(function ($s) { - return is_string($s) ? strtolower($s) : null; - }, $only_these)); - $relations = array_intersect($only_these, $relations); - } - foreach ($relations as $relation) { - $options = $this->getRelationOptions($relation); - if (isset($options['on_store']) && - ($options['type'] === 'has_one' || - $options['type'] === 'has_many' || - $options['type'] === 'has_and_belongs_to_many')) { - if ($options['on_store'] instanceof Closure) { - $ret += call_user_func($options['on_store'], $this, $relation); - } elseif (isset($this->relations[$relation])) { - $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this); - if ($options['type'] === 'has_one') { - call_user_func($options['assoc_foreign_key_setter'], $this->{$relation}, $foreign_key_value); - $ret = call_user_func([$this->{$relation}, 'store']); - } elseif ($options['type'] === 'has_many') { - foreach ($this->{$relation} as $r) { - call_user_func($options['assoc_foreign_key_setter'], $r, $foreign_key_value); - } - $ret += array_sum(call_user_func([$this->{$relation}, 'sendMessage'], 'store')); - $ret += array_sum(call_user_func([$this->{$relation}->getDeleted(), 'sendMessage'], 'delete')); - } else { - call_user_func([$this->{$relation}, 'sendMessage'], 'store'); - $to_delete = array_filter($this->{$relation}->getDeleted()->pluck($options['assoc_foreign_key'])); - $to_insert = array_filter($this->{$relation}->pluck($options['assoc_foreign_key'])); - $sql = "DELETE FROM `" . $options['thru_table'] ."` WHERE `" . $options['thru_key'] ."` = ? AND `" . $options['thru_assoc_key'] . "` = ?"; - $st = DBManager::get()->prepare($sql); - foreach ($to_delete as $one_value) { - $st->execute([$foreign_key_value, $one_value]); - $ret += $st->rowCount(); - } - $sql = "INSERT IGNORE INTO `" . $options['thru_table'] ."` SET `" . $options['thru_key'] ."` = ?, `" . $options['thru_assoc_key'] . "` = ?"; - $st = DBManager::get()->prepare($sql); - foreach ($to_insert as $one_value) { - $st->execute([$foreign_key_value, $one_value]); - $ret += $st->rowCount(); - } - } - } - } - } - return $ret; - } - - /** - * set chdate column to current timestamp - * @return boolean - */ - function triggerChdate() - { - if ($this->db_fields()['chdate']) { - $this->content['chdate'] = time(); - if ($where_query = $this->getWhereQuery()) { - DBManager::get()->exec("UPDATE `{$this->db_table()}` SET chdate={$this->content['chdate']} - WHERE ". join(" AND ", $where_query)); - return true; - } - } - - return false; - } - - /** - * delete entry from database - * the object is cleared, but is not(!) turned to new state - * @return bool|int number of deleted rows - */ - function delete() - { - $ret = false; - if (!$this->isDeleted() && !$this->isNew()) { - if ($this->applyCallbacks('before_delete') === false) { - return false; - } - $ret = $this->deleteRelations(); - $where_query = $this->getWhereQuery(); - if ($where_query) { - $query = "DELETE FROM `{$this->db_table()}` WHERE " - . join(" AND ", $where_query); - $ret += DBManager::get()->exec($query); - } - $this->is_deleted = true; - $this->applyCallbacks('after_delete'); - - // Remove i18n translations - if (I18N::isEnabled()) { - foreach (array_keys($this->i18n_fields()) as $field) { - if ($this->content[$field] instanceof I18NString) { - $this->content[$field]->removeTranslations(); - } - } - } - } - $this->setData([], true); - return $ret; - } - - /** - * sends a delete message to all related objects - * if a relation has a callback for 'on_delete' configured, the callback - * is invoked instead - * - * @return bool|int addition of all return values, false if none was called - */ - protected function deleteRelations() - { - $ret = false; - foreach (array_keys($this->relations) as $relation) { - $options = $this->getRelationOptions($relation); - if (isset($options['on_delete']) && - ($options['type'] === 'has_one' || - $options['type'] === 'has_many' || - $options['type'] === 'has_and_belongs_to_many')) { - if ($options['on_delete'] instanceof Closure) { - $ret += call_user_func($options['on_delete'], $this, $relation); - } else { - if ($options['type'] === 'has_one' || $options['type'] === 'has_many') { - $this->initRelation($relation); - if (isset($this->relations[$relation])) { - if ($options['type'] === 'has_one') { - $ret += call_user_func([$this->{$relation}, 'delete']); - } elseif ($options['type'] === 'has_many') { - $ret += array_sum(call_user_func([$this->{$relation}, 'sendMessage'], 'delete')); - } - } - } else { - $foreign_key_value = call_user_func($options['assoc_func_params_func'], $this); - $sql = "DELETE FROM `" . $options['thru_table'] ."` WHERE `" . $options['thru_key'] ."` = ?"; - $st = DBManager::get()->prepare($sql); - $st->execute([$foreign_key_value]); - $ret += $st->rowCount(); - } - } - $this->relations[$relation] = null; - } - } - return $ret; - } - - /** - * init internal content arrays with nulls or defaults - * - * @throws UnexpectedValueException if there is an unmatched alias - * @return void - */ - protected function initializeContent() - { - $this->content = []; - foreach (array_keys($this->db_fields()) as $field) { - $this->content[$field] = null; - $this->content_db[$field] = null; - $this->setValue($field, $this->getDefaultValue($field)); - } - foreach ($this->alias_fields() as $alias => $field) { - if (isset($this->db_fields()[$field])) { - $this->content[$alias] =& $this->content[$field]; - $this->content_db[$alias] =& $this->content_db[$field]; - } else { - throw new UnexpectedValueException(sprintf('Column %s not found for alias %s', $field, $alias)); - } - } - foreach (array_keys($this->relations) as $one) { - $this->relations[$one] = null; - } - $this->additional_data = []; - } - - /** - * checks if at least one field was modified since last restore - * - * @return boolean - */ - public function isDirty() - { - foreach (array_keys($this->db_fields()) as $field) { - if ($this->isFieldDirty($field)) { - return true; - } - } - return false; - } - - /** - * checks if given field was modified since last restore - * - * @param string $field - * @return boolean - */ - public function isFieldDirty($field) - { - $field = strtolower($field); - if ($this->content[$field] === null || $this->content_db[$field] === null) { - return $this->content[$field] !== $this->content_db[$field]; - } else if ($this->content[$field] instanceof I18NString || $this->content_db[$field] instanceof I18NString) { - return $this->content[$field] != $this->content_db[$field]; - } else { - return (string)$this->content[$field] !== (string)$this->content_db[$field]; - } - } - - /** - * reverts value of given field to last restored value - * - * @param string $field - * @return mixed the restored value - */ - public function revertValue($field) - { - $field = strtolower($field); - return ($this->content[$field] = $this->content_db[$field]); - } - - /** - * returns unmodified value of given field - * - * @param string $field - * @throws InvalidArgumentException - * @return mixed - */ - public function getPristineValue($field) - { - $field = strtolower($field); - if (array_key_exists($field, $this->content_db)) { - return $this->content_db[$field]; - } else { - throw new InvalidArgumentException(get_class($this) . '::'. $field . ' not found.'); - } - } - - /** - * intitalize a relationship and get related record(s) - * - * @param string $relation name of relation - * @throws InvalidArgumentException if the relation does not exists - * @return void - */ - public function initRelation($relation) - { - if (!array_key_exists($relation, $this->relations)) { - throw new InvalidArgumentException('Unknown relation: ' . $relation); - } - if ($this->relations[$relation] === null) { - $options = $this->getRelationOptions($relation); - $to_call = [$options['class_name'], $options['assoc_func']]; - if (!is_callable($to_call)) { - throw new RuntimeException('assoc_func: ' . join('::', $to_call) . ' is not callable.' ); - } - $params = $options['assoc_func_params_func']; - if ($options['type'] === 'has_many') { - $records = function($record) use ($to_call, $params, $options) { - $p = (array)$params($record); - return call_user_func_array($to_call, array_merge(count($p) ? $p : [null], [$options['order_by'] ?? null])); - }; - $this->relations[$relation] = new SimpleORMapCollection($records, $options, $this); - } elseif ($options['type'] === 'has_and_belongs_to_many') { - $records = function($record) use ($to_call, $params, $options) {$p = (array)$params($record); return call_user_func_array($to_call, array_merge(count($p) ? $p : [null], [$options]));}; - $this->relations[$relation] = new SimpleORMapCollection($records, $options, $this); - } else { - $p = (array)$params($this); - $records = call_user_func_array($to_call, count($p) ? $p : [null]); - $result = is_array($records) ? ($records[0] ?? null) : $records; - $this->relations[$relation] = $result; - } - } - } - - /** - * clear data for a relationship - * - * @param string $relation name of relation - * @throws InvalidArgumentException if teh relation does not exists - * @return void - */ - public function resetRelation($relation) - { - if (!array_key_exists($relation, $this->relations)) { - throw new InvalidArgumentException('Unknown relation: ' . $relation); - } - $this->relations[$relation] = null; - } - - /** - * invoke registered callbacks for given type - * if one callback returns false the following will not - * be invoked - * - * @param string $type type of callback - * @return bool return value from last callback - */ - protected function applyCallbacks($type) - { - $ok = true; - foreach ($this->registered_callbacks()[$type] as $cb) { - if ($cb instanceof Closure) { - $function = $cb; - $params = [$this, $type, $cb]; - } else { - $function = [$this, $cb]; - $params = [$type]; - }; - $ok = call_user_func_array($function, $params); - if ($ok === false) { - break; - } - } - return $ok; - } - - /** - * register given callback for one or many possible callback types - * callback param could be a closure or method name of current class - * - * @param string|array $types types to register callback for - * @param callable $cb callback - * @throws InvalidArgumentException if the callback type is not known - * @return number of registered callbacks - */ - protected static function registerCallback($types, $cb) - { - trigger_error(__METHOD__ . ' is deprecated. Please use the configuration in configure().', E_USER_DEPRECATED); - - $types = is_array($types) ? $types : words($types); - $reg = 0; - foreach ($types as $type) { - if (isset(static::registered_callbacks()[$type])) { - $found = array_search($cb, self::$config[static::class]['registered_callbacks'][$type], true); - if ($found === false) { - self::$config[static::class]['registered_callbacks'][$type][] = $cb; - $reg++; - } - } else { - throw new InvalidArgumentException('Unknown callback type: ' . $type); - } - } - return $reg; - } - - /** - * unregister given callback for one or many possible callback types - * - * @param string|array $types types to unregister callback for - * @param mixed $cb - * @throws InvalidArgumentException if the callback type is not known - * @return number of unregistered callbacks - */ - protected static function unregisterCallback($types, $cb) - { - trigger_error(__METHOD__ . ' is deprecated. Please use the configuration in configure().', E_USER_DEPRECATED); - - $types = is_array($types) ? $types : words($types); - foreach ($types as $type) { - if (isset(static::registered_callbacks()[$type])) { - $found = array_search($cb, self::$config[static::class]['registered_callbacks'][$type], true); - if ($found !== false) { - $unreg++; - unset(self::$config[static::class]['registered_callbacks'][$type][$found]); - } - } else { - throw new InvalidArgumentException('Unknown callback type: ' . $type); - } - } - return $unreg; - } - - /** - * default callback used to map specific callbacks to NotificationCenter - * - * @param string $cb_type callback type - * @return void|boolean - */ - protected function cbNotificationMapper($cb_type) - { - if (isset($this->notification_map()[$cb_type])) { - try { - foreach(words($this->notification_map()[$cb_type]) as $notification) { - NotificationCenter::postNotification($notification, $this); - } - } catch (NotificationVetoException $e) { - return false; - } - } - } - - /** - * default callback used to map specific callbacks to NotificationCenter - * - * @param string $cb_type callback type - * @return void|boolean - */ - protected function cbAfterInitialize($cb_type) - { - foreach (array_keys($this->db_fields()) as $field) { - if (is_object($this->content[$field])) { - $this->content_db[$field] = clone $this->content[$field]; - } else { - $this->content_db[$field] = $this->content[$field]; - } - } - } - - /** - * default setter used to proxy serialized fields with - * ArrayObjects - * - * @param string $field column name - * @param mixed $value value - * @return mixed - */ - protected function setSerializedValue($field, $value) - { - $object_type = $this->serialized_fields()[$field]; - if (is_null($value) || $value instanceof $object_type) { - $this->content[$field] = $value; - } else { - $this->content[$field] = new $object_type($value); - } - return $this->content[$field]; - } - - /** - * default setter used to proxy I18N fields with - * I18NString - * - * @param string $field column name - * @param mixed $value value - * @return mixed - */ - protected function setI18nValue($field, $value) - { - $meta = ['object_id' => $this->getId(), - 'table' => $this->db_table(), - 'field' => $field]; - if ($value instanceof I18NString) { - $value->setMetadata($meta); - $this->content[$field] = $value; - } else { - $this->content[$field] = new I18NString($value, null, $meta); - } - return $this->content[$field]; - } - - /** - * Cleans up this object. This essentially reset all relations of - * this object and marks them as unused so that the garbage collector may - * remove the objects. - * - * Use this function when you ran into memory problems and need to free - * some memory; - * - * @return void - */ - public function cleanup() - { - foreach ($this->relations as $relation => $object) { - if ($object instanceof SimpleORMap || $object instanceof SimpleORMapCollection) { - $this->relations[$relation]->cleanup(); - } - $this->resetRelation($relation); - } - } -} diff --git a/lib/models/SimpleORMapCollection.class.php b/lib/models/SimpleORMapCollection.class.php deleted file mode 100644 index 7debb80..0000000 --- a/lib/models/SimpleORMapCollection.class.php +++ /dev/null @@ -1,258 +0,0 @@ -<?php -/** - * SimpleORMapCollection.class.php - * simple object-relational mapping - * - * 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 2012 Stud.IP Core-Group - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * - * @extends SimpleCollection<SimpleORMap> - * - * @template T of SimpleORMap - */ -class SimpleORMapCollection extends SimpleCollection -{ - /** - * @var int Exception error code denoting a wrong type of objects. - */ - const WRONG_OBJECT_TYPE = 1; - - /** - * @var int Exception error code denoting that an object of this `id` already exists. - */ - const OBJECT_EXISTS = 2; - - /** - * the record object this collection belongs to - * - * @var ?SimpleORMap - */ - protected $related_record; - - /** - * relation options - * @var array - */ - protected $relation_options = []; - - /** - * creates a collection from an array of objects - * all objects should be of the same type - * - * @throws InvalidArgumentException if first entry is not SimpleOrMap - * @param T[] $data array with SimpleORMap objects - * @param bool $strict check every element for correct type and unique pk - * @return SimpleORMapCollection<T> - */ - public static function createFromArray(array $data, $strict = true) - { - $ret = new SimpleORMapCollection(); - if (count($data)) { - $first = current($data); - if ($first instanceof SimpleORMap) { - $ret->setClassName(get_class($first)); - if ($strict) { - foreach ($data as $one) { - $ret[] = $one; - } - } else { - $ret->exchangeArray($data); - } - } else { - throw new InvalidArgumentException('This collection only accepts objects derived from SimpleORMap', self::WRONG_OBJECT_TYPE); - } - } - return $ret; - } - - /** - * Constructor - * - * @param ?Closure $finder callable to fill collection - * @param ?array $options relationship options - * @param SimpleORMap|null $record related record - */ - public function __construct(Closure $finder = null, array $options = null, SimpleORMap $record = null) - { - $this->relation_options = $options; - $this->related_record = $record; - parent::__construct($finder === null ? [] : $finder); - } - - /** - * Sets the value at the specified index - * checks if the value is an object of specified class - * - * @see ArrayObject::offsetSet() - * @throws InvalidArgumentException if the given model does not fit (wrong type or id) - */ - public function offsetSet($index, $newval): void - { - if (!is_null($index)) { - $index = (int)$index; - } - if (!is_a($newval, $this->getClassName())) { - throw new InvalidArgumentException('This collection only accepts objects of type: ' . $this->getClassName(), self::WRONG_OBJECT_TYPE); - } - if ($this->related_record && $this->relation_options['type'] === 'has_many') { - $foreign_key_value = call_user_func($this->relation_options['assoc_func_params_func'], $this->related_record); - call_user_func($this->relation_options['assoc_foreign_key_setter'], $newval, $foreign_key_value); - } - if ($newval->id !== null) { - $exists = $this->find($newval->id); - if ($exists) { - throw new InvalidArgumentException('Element could not be appended, element with id: ' . $exists->id . ' is in the way', self::OBJECT_EXISTS); - } - } - parent::offsetSet($index, $newval); - } - - /** - * sets the allowed class name - * @param class-string $class_name - * @return void - */ - public function setClassName($class_name) - { - $this->relation_options['class_name'] = strtolower($class_name); - $this->deleted->relation_options['class_name'] = strtolower($class_name); - } - - /** - * sets the related record - * - * @param SimpleORMap $record - * @return void - */ - public function setRelatedRecord(SimpleORMap $record) - { - $this->related_record = $record; - } - - /** - * gets the allowed classname - * - * @return string - */ - public function getClassName() - { - return strtolower($this->relation_options['class_name']); - } - - /** - * reloads the elements of the collection - * by calling the finder function - * - * @throws InvalidArgumentException - * @return ?int number of records after refresh - */ - public function refresh() - { - if (is_callable($this->finder)) { - $data = call_user_func($this->finder, $this->related_record); - foreach ($data as $one) { - if (!is_a($one, $this->getClassName())) { - throw new InvalidArgumentException('This collection only accepts objects of type: ' . $this->getClassName(), self::WRONG_OBJECT_TYPE); - } - } - $this->exchangeArray($data); - $this->deleted->exchangeArray([]); - return $this->last_count = $this->count(); - } - - return null; - } - - /** - * returns element with given primary key value - * - * @param string $value primary key value to search for - * @return ?T - */ - public function find($value) - { - return $this->findOneBy('id', $value); - } - - /** - * 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 ?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 = []; - foreach ($this as $record) { - $key = $record->getValue($group_by); - if (is_array($key)) { - $key = join('_', $key); - } - $result[$key][] = $record->toArray($only_these_fields); - } - if ($group_func === null) { - $group_func = 'current'; - } - return array_map($group_func, $result); - } - - /** - * mark element(s) for deletion - * element(s) with given primary key are moved to - * internal deleted collection - * - * @param string $id primary key of element - * @return int number of unsetted elements - */ - public function unsetByPk($id) - { - return $this->unsetBy('id', $id); - } - - /** - * merge in another collection, elements must be of - * the same type, if an element already exists it is - * replaced or ignored depending on second param - * - * @param SimpleORMapCollection $a_collection - * @param string $mode 'replace' or 'ignore' - * @return void - */ - public function merge(SimpleCollection $a_collection, string $mode = 'ignore') - { - $mode = func_get_arg(1); - foreach ($a_collection as $element) { - try { - /** - * @throws InvalidArgumentException - * @see SimpleORMapCollection::offsetSet() - */ - $this[] = $element; - } catch (InvalidArgumentException $e) { - if ($e->getCode() === self::OBJECT_EXISTS) { - if ($mode === 'replace') { - $this->unsetByPk($element->id); - $this[] = $element; - } // else $mode means 'ignore' - } else { - throw $e; - } - } - } - $this->storage = array_values($this->storage); - } -} |
