diff options
Diffstat (limited to 'localwebsite/engine')
-rw-r--r-- | localwebsite/engine/database.php | 51 | ||||
-rw-r--r-- | localwebsite/engine/model.php | 243 |
2 files changed, 294 insertions, 0 deletions
diff --git a/localwebsite/engine/database.php b/localwebsite/engine/database.php index 186d2ef..33f36cf 100644 --- a/localwebsite/engine/database.php +++ b/localwebsite/engine/database.php @@ -7,13 +7,19 @@ class database { protected SQLite3 $link; public function __construct(string $db_path) { + $will_create = !file_exists($db_path); $this->link = new SQLite3($db_path); + if ($will_create) + setperm($db_path); $this->link->enableExceptions(true); $this->upgradeSchema(); } protected function upgradeSchema() { $cur = $this->getSchemaVersion(); + if ($cur == self::SCHEMA_VERSION) + return; + if ($cur < 1) { $this->link->exec("CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -77,4 +83,49 @@ class database { return $this->link->querySingle($this->prepareQuery($sql, ...$params), true); } + protected function performInsert(string $command, string $table, array $fields): SQLite3Result { + $names = []; + $values = []; + $count = 0; + foreach ($fields as $k => $v) { + $names[] = $k; + $values[] = $v; + $count++; + } + + $sql = "{$command} INTO `{$table}` (`" . implode('`, `', $names) . "`) VALUES (" . implode(', ', array_fill(0, $count, '?')) . ")"; + array_unshift($values, $sql); + + return call_user_func_array([$this, 'query'], $values); + } + + public function insert(string $table, array $fields): SQLite3Result { + return $this->performInsert('INSERT', $table, $fields); + } + + public function replace(string $table, array $fields): SQLite3Result { + return $this->performInsert('REPLACE', $table, $fields); + } + + public function insertId(): int { + return $this->link->lastInsertRowID(); + } + + public function update($table, $rows, ...$cond): SQLite3Result { + $fields = []; + $args = []; + foreach ($rows as $row_name => $row_value) { + $fields[] = "`{$row_name}`=?"; + $args[] = $row_value; + } + $sql = "UPDATE `$table` SET " . implode(', ', $fields); + if (!empty($cond)) { + $sql .= " WHERE " . $cond[0]; + if (count($cond) > 1) + $args = array_merge($args, array_slice($cond, 1)); + } + return $this->query($sql, ...$args); + } + + }
\ No newline at end of file diff --git a/localwebsite/engine/model.php b/localwebsite/engine/model.php new file mode 100644 index 0000000..4dd981c --- /dev/null +++ b/localwebsite/engine/model.php @@ -0,0 +1,243 @@ +<?php + +abstract class model { + + const DB_TABLE = null; + const DB_KEY = 'id'; + + const STRING = 0; + const INTEGER = 1; + const FLOAT = 2; + const ARRAY = 3; + const BOOLEAN = 4; + const JSON = 5; + const SERIALIZED = 6; + + 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])) { + list($fields, $model_name_map, $db_name_map) = static::get_spec(); + self::$SpecCache[static::class] = [ + 'fields' => $fields, + 'model_name_map' => $model_name_map, + 'db_name_map' => $db_name_map + ]; + } + + foreach (self::$SpecCache[static::class]['fields'] as $field) + $this->{$field['model_name']} = self::cast_to_type($field['type'], $raw[$field['db_name']]); + + if (is_null(static::DB_TABLE)) + trigger_error('class '.get_class($this).' doesn\'t have DB_TABLE defined'); + } + + /** + * @param $fields + * + * TODO: support adding or subtracting (SET value=value+1) + */ + public function edit($fields) { + $db = getDB(); + + $model_upd = []; + $db_upd = []; + + foreach ($fields as $name => $value) { + $index = self::$SpecCache[static::class]['db_name_map'][$name] ?? null; + if (is_null($index)) { + debugError(__METHOD__.': field `'.$name.'` not found in '.static::class); + continue; + } + + $field = self::$SpecCache[static::class]['fields'][$index]; + switch ($field['type']) { + case self::ARRAY: + if (is_array($value)) { + $db_upd[$name] = implode(',', $value); + $model_upd[$field['model_name']] = $value; + } else { + debugError(__METHOD__.': field `'.$name.'` is expected to be array. skipping.'); + } + break; + + case self::INTEGER: + $value = (int)$value; + $db_upd[$name] = $value; + $model_upd[$field['model_name']] = $value; + break; + + case self::FLOAT: + $value = (float)$value; + $db_upd[$name] = $value; + $model_upd[$field['model_name']] = $value; + break; + + case self::BOOLEAN: + $db_upd[$name] = $value ? 1 : 0; + $model_upd[$field['model_name']] = $value; + break; + + case self::JSON: + $db_upd[$name] = jsonEncode($value); + $model_upd[$field['model_name']] = $value; + break; + + case self::SERIALIZED: + $db_upd[$name] = serialize($value); + $model_upd[$field['model_name']] = $value; + break; + + default: + $value = (string)$value; + $db_upd[$name] = $value; + $model_upd[$field['model_name']] = $value; + break; + } + } + + if (!empty($db_upd) && !$db->update(static::DB_TABLE, $db_upd, static::DB_KEY."=?", $this->get_id())) { + debugError(__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 $fields = [], array $custom_getters = []): array { + if (empty($fields)) + $fields = array_keys(static::$SpecCache[static::class]['db_name_map']); + + $array = []; + foreach ($fields 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 cast_to_type(int $type, $value) { + switch ($type) { + case self::BOOLEAN: + return (bool)$value; + + case self::INTEGER: + return (int)$value; + + case self::FLOAT: + return (float)$value; + + case self::ARRAY: + return array_filter(explode(',', $value)); + + case self::JSON: + $val = jsonDecode($value); + if (!$val) + $val = null; + return $val; + + case self::SERIALIZED: + $val = unserialize($value); + if ($val === false) + $val = null; + return $val; + + default: + return (string)$value; + } + } + + protected static function get_spec(): array { + $rc = new ReflectionClass(static::class); + $props = $rc->getProperties(ReflectionProperty::IS_PUBLIC); + + $list = []; + $index = 0; + + $model_name_map = []; + $db_name_map = []; + + foreach ($props as $prop) { + if ($prop->isStatic()) + continue; + + $name = $prop->getName(); + if (startsWith($name, '_')) + continue; + + $type = $prop->getType(); + $phpdoc = $prop->getDocComment(); + + $mytype = null; + if (!$prop->hasType() && !$phpdoc) + $mytype = self::STRING; + else { + $typename = $type->getName(); + switch ($typename) { + case 'string': + $mytype = self::STRING; + break; + case 'int': + $mytype = self::INTEGER; + break; + case 'float': + $mytype = self::FLOAT; + break; + case 'array': + $mytype = self::ARRAY; + break; + case 'bool': + $mytype = self::BOOLEAN; + break; + } + + if ($phpdoc != '') { + $pos = strpos($phpdoc, '@'); + if ($pos === false) + continue; + + if (substr($phpdoc, $pos+1, 4) == 'json') + $mytype = self::JSON; + else if (substr($phpdoc, $pos+1, 5) == 'array') + $mytype = self::ARRAY; + else if (substr($phpdoc, $pos+1, 10) == 'serialized') + $mytype = self::SERIALIZED; + } + } + + if (is_null($mytype)) + debugError(__METHOD__.": ".$name." is still null in ".static::class); + + $dbname = from_camel_case($name); + $list[] = [ + 'type' => $mytype, + 'model_name' => $name, + 'db_name' => $dbname + ]; + + $model_name_map[$name] = $index; + $db_name_map[$dbname] = $index; + + $index++; + } + + return [$list, $model_name_map, $db_name_map]; + } + +} |