summaryrefslogtreecommitdiff
path: root/engine/model.php
diff options
context:
space:
mode:
Diffstat (limited to 'engine/model.php')
-rw-r--r--engine/model.php331
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