diff options
Diffstat (limited to 'engine/model.php')
-rw-r--r-- | engine/model.php | 331 |
1 files changed, 331 insertions, 0 deletions
diff --git a/engine/model.php b/engine/model.php new file mode 100644 index 0000000..e967dc2 --- /dev/null +++ b/engine/model.php @@ -0,0 +1,331 @@ +<?php + +enum ModelFieldType { + case STRING; + case INTEGER; + case FLOAT; + case ARRAY; + case BOOLEAN; + case JSON; + case SERIALIZED; + case BITFIELD; + case BACKED_ENUM; +} + +abstract class model { + + const DB_TABLE = null; + const DB_KEY = 'id'; + + /** @var $SpecCache ModelSpec[] */ + protected static array $SpecCache = []; + + public static function create_instance(...$args) { + $cl = get_called_class(); + return new $cl(...$args); + } + + public function __construct(array $raw) { + if (!isset(self::$SpecCache[static::class])) + self::$SpecCache[static::class] = static::get_spec(); + + foreach (self::$SpecCache[static::class]->getProperties() as $prop) + $this->{$prop->getModelName()} = $prop->fromRawValue($raw[$prop->getDbName()]); + + if (is_null(static::DB_TABLE)) + trigger_error('class '.get_class($this).' doesn\'t have DB_TABLE defined'); + } + + /** + * TODO: support adding or subtracting (SET value=value+1) + */ + public function edit(array $fields) { + $db = DB(); + + $model_upd = []; + $db_upd = []; + + $spec_db_name_map = self::$SpecCache[static::class]->getDbNameMap(); + $spec_props = self::$SpecCache[static::class]->getProperties(); + + foreach ($fields as $name => $value) { + $index = $spec_db_name_map[$name] ?? null; + if (is_null($index)) { + logError(__METHOD__.': field `'.$name.'` not found in '.static::class); + continue; + } + + $field = $spec_props[$index]; + if ($field->isNullable() && is_null($value)) { + $model_upd[$field->getModelName()] = $value; + $db_upd[$name] = $value; + continue; + } + + switch ($field->getType()) { + case ModelFieldType::ARRAY: + if (is_array($value)) { + $db_upd[$name] = implode(',', $value); + $model_upd[$field->getModelName()] = $value; + } else { + logError(__METHOD__.': field `'.$name.'` is expected to be array. skipping.'); + } + break; + + case ModelFieldType::INTEGER: + $value = (int)$value; + $db_upd[$name] = $value; + $model_upd[$field->getModelName()] = $value; + break; + + case ModelFieldType::FLOAT: + $value = (float)$value; + $db_upd[$name] = $value; + $model_upd[$field->getModelName()] = $value; + break; + + case ModelFieldType::BOOLEAN: + $db_upd[$name] = $value ? 1 : 0; + $model_upd[$field->getModelName()] = $value; + break; + + case ModelFieldType::JSON: + $db_upd[$name] = jsonEncode($value); + $model_upd[$field->getModelName()] = $value; + break; + + case ModelFieldType::SERIALIZED: + $db_upd[$name] = serialize($value); + $model_upd[$field->getModelName()] = $value; + break; + + case ModelFieldType::BITFIELD: + $db_upd[$name] = $value; + $model_upd[$field->getModelName()] = $value; + break; + + default: + $value = (string)$value; + $db_upd[$name] = $value; + $model_upd[$field->getModelName()] = $value; + break; + } + } + + if (!empty($db_upd) && !$db->update(static::DB_TABLE, $db_upd, static::DB_KEY."=?", $this->get_id())) { + logError(__METHOD__.': failed to update database'); + return; + } + + if (!empty($model_upd)) { + foreach ($model_upd as $name => $value) + $this->{$name} = $value; + } + } + + public function get_id() { + return $this->{to_camel_case(static::DB_KEY)}; + } + + public function as_array(array $properties = [], array $custom_getters = []): array { + if (empty($properties)) + $properties = static::$SpecCache[static::class]->getPropNames(); + + $array = []; + foreach ($properties as $field) { + if (isset($custom_getters[$field]) && is_callable($custom_getters[$field])) { + $array[$field] = $custom_getters[$field](); + } else { + $array[$field] = $this->{to_camel_case($field)}; + } + } + + return $array; + } + + protected static function get_spec(): ModelSpec { + $rc = new ReflectionClass(static::class); + $props = $rc->getProperties(ReflectionProperty::IS_PUBLIC); + + $list = []; + $index = 0; + + $db_name_map = []; + + foreach ($props as $prop) { + if ($prop->isStatic()) + continue; + + $name = $prop->getName(); + if (str_starts_with($name, '_')) + continue; + + $real_type = null; + $type = $prop->getType(); + $phpdoc = $prop->getDocComment(); + + /** @var ?ModelFieldType $mytype */ + $mytype = null; + if (!$prop->hasType() && !$phpdoc) + $mytype = ModelFieldType::STRING; + else { + $typename = $type->getName(); + switch ($typename) { + case 'string': + $mytype = ModelFieldType::STRING; + break; + case 'int': + $mytype = ModelFieldType::INTEGER; + break; + case 'float': + $mytype = ModelFieldType::FLOAT; + break; + case 'array': + $mytype = ModelFieldType::ARRAY; + break; + case 'bool': + $mytype = ModelFieldType::BOOLEAN; + break; + case 'mysql_bitfield': + $mytype = ModelFieldType::BITFIELD; + break; + default: + if (enum_exists($typename)) { + $mytype = ModelFieldType::BACKED_ENUM; + $real_type = $typename; + } + break; + } + + if ($phpdoc != '') { + $pos = strpos($phpdoc, '@'); + if ($pos === false) + continue; + + if (substr($phpdoc, $pos+1, 4) == 'json') + $mytype = ModelFieldType::JSON; + else if (substr($phpdoc, $pos+1, 5) == 'array') + $mytype = ModelFieldType::ARRAY; + else if (substr($phpdoc, $pos+1, 10) == 'serialized') + $mytype = ModelFieldType::SERIALIZED; + } + } + + if (is_null($mytype)) + logError(__METHOD__.": ".$name." is still null in ".static::class); + + // $dbname = from_camel_case($name); + $model_descr = new ModelProperty( + type: $mytype, + realType: $real_type, + nullable: $type->allowsNull(), + modelName: $name, + dbName: from_camel_case($name) + ); + $list[] = $model_descr; + $db_name_map[$model_descr->getDbName()] = $index++; + } + + return new ModelSpec($list, $db_name_map); + } + +} + +class ModelSpec { + + public function __construct( + /** @var ModelProperty[] */ + protected array $properties, + protected array $dbNameMap + ) {} + + /** + * @return ModelProperty[] + */ + public function getProperties(): array { + return $this->properties; + } + + public function getDbNameMap(): array { + return $this->dbNameMap; + } + + public function getPropNames(): array { + return array_keys($this->dbNameMap); + } + +} + +class ModelProperty { + + public function __construct( + protected ?ModelFieldType $type, + protected mixed $realType, + protected bool $nullable, + protected string $modelName, + protected string $dbName + ) {} + + public function getDbName(): string { + return $this->dbName; + } + + public function getModelName(): string { + return $this->modelName; + } + + public function isNullable(): bool { + return $this->nullable; + } + + public function getType(): ?ModelFieldType { + return $this->type; + } + + public function fromRawValue(mixed $value): mixed { + if ($this->nullable && is_null($value)) + return null; + + switch ($this->type) { + case ModelFieldType::BOOLEAN: + return (bool)$value; + + case ModelFieldType::INTEGER: + return (int)$value; + + case ModelFieldType::FLOAT: + return (float)$value; + + case ModelFieldType::ARRAY: + return array_filter(explode(',', $value)); + + case ModelFieldType::JSON: + $val = jsonDecode($value); + if (!$val) + $val = null; + return $val; + + case ModelFieldType::SERIALIZED: + $val = unserialize($value); + if ($val === false) + $val = null; + return $val; + + case ModelFieldType::BITFIELD: + return new mysql_bitfield($value); + + case ModelFieldType::BACKED_ENUM: + try { + return $this->realType::from($value); + } catch (ValueError $e) { + if ($this->nullable) + return null; + throw $e; + } + + default: + return (string)$value; + } + } + +}
\ No newline at end of file |