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/SimpleORMap.class.php | |
| parent | a944dacedff6511310d1c73b9854b450ed8996b2 (diff) | |
relocate sorm classes, fixes #4106
Closes #4106
Merge request studip/studip!2953
Diffstat (limited to 'lib/models/SimpleORMap.class.php')
| -rw-r--r-- | lib/models/SimpleORMap.class.php | 2476 |
1 files changed, 0 insertions, 2476 deletions
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); - } - } -} |
