331 lines
9.9 KiB
PHP
331 lines
9.9 KiB
PHP
<?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;
|
|
}
|
|
}
|
|
|
|
} |