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; } } }