From bda28640d6e066ae338c6f31407df274ed09f026 Mon Sep 17 00:00:00 2001 From: winter Date: Sun, 8 Dec 2024 23:18:35 +0000 Subject: implement remote object fetching! this is most of the tricky parts of inbound federation :3 --- WpfTest/Model.php | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 8 deletions(-) (limited to 'WpfTest/Model.php') diff --git a/WpfTest/Model.php b/WpfTest/Model.php index c006ffd..53b2d83 100644 --- a/WpfTest/Model.php +++ b/WpfTest/Model.php @@ -1,7 +1,28 @@ id) && static::find($this->{'id'}) != null) { + return 'id = ' . $this->{'id'}; + } + return null; + } + + /** + * Converts a field or type name into the version used in the database + */ private static function mangleName(string $name): string { $chars = str_split($name); $chars[0] = strtolower($chars[0]); @@ -14,6 +35,9 @@ abstract class Model { return implode($chars); } + /** + * Converts a database column name into the corresponding field name + */ private static function unmangleName(string $name): string { $chars = str_split($name); for ($i = 0; $i < count($chars); $i++) { @@ -29,7 +53,27 @@ abstract class Model { $props = $refl->getProperties(); $result = []; foreach ($props as $p) { - $result[self::mangleName($p->getName())] = $p->getValue(); + $pName = $p->getName(); + if (!isset($this->{$pName})) + continue; + // we need to determine the plain value to insert ... + $value = $this->{$pName}; + $discarded = false; + if ($value instanceof \DateTimeInterface) { + // datetime objects should be turned into iso 8601 timestamps + $value = $value->format('c'); + } elseif ($value instanceof Model) { + // models should generally be turned into their id if there is one or removed if not + if (property_exists($value, 'id') && isset($value->id)) + $value = $value->{'id'}; // doing it like this to shut up the warning (lol) + else + $discarded = true; + } elseif (is_array($value)) { + // arrays ought to be handled separately when saving (different db table) + $discarded = true; + } + if (!$discarded) + $result[self::mangleName($pName)] = $value; } return $result; } @@ -55,8 +99,8 @@ abstract class Model { throw $e; $typeClass = $type->getName(); if (enum_exists($typeClass)) { - // if it's an enum we can find the enum case with the same name - $obj->{$name} = (new \ReflectionEnum($typeClass))->getCase($value)->getValue(); + // if it's a backed enum we can find the enum case with the same value + $obj->{$name} = $typeClass::from($value); } elseif (is_subclass_of($typeClass, Model::class) && is_int($value)) { // if it's another model we can try to look it up by id automatically $obj->{$name} = $typeClass::find($value); @@ -66,23 +110,25 @@ abstract class Model { } } } - // set up any extra-complicated fields if needed - $obj->hydrate($columns); + // set up any referenced fields if needed + $obj->hydrate(); + $obj->saved = true; return $obj; } /** - * Initialises fields of the model that contain more complex objects - * @param array $row the row of the database + * Initialises fields of the model that contain referenced / foreign keyed objects + * which couldn't be initialised automatically * @return void */ - protected function hydrate(array $row) { + protected function hydrate() { } public static function findWhere(string $whereClause, array $parameters): ?static { $classNameParts = explode('\\', static::class); $className = $classNameParts[count($classNameParts) - 1]; $tableName = self::mangleName($className); + $stmt = Db::getInstance()->getPdo()->prepare("SELECT * FROM $tableName WHERE $whereClause LIMIT 1"); $stmt->execute($parameters); return static::fromDbRow($stmt->fetch(\PDO::FETCH_ASSOC)); @@ -92,6 +138,7 @@ abstract class Model { $classNameParts = explode('\\', static::class); $className = $classNameParts[count($classNameParts) - 1]; $tableName = self::mangleName($className); + $stmt = Db::getInstance()->getPdo()->prepare("SELECT * FROM $tableName WHERE $whereClause"); $stmt->execute($parameters); return array_map(function ($row) { @@ -102,4 +149,71 @@ abstract class Model { public static function find(int $id): ?static { return static::findWhere('id = ?', [$id]); } + + /** + * Inserts or updates the model's corresponding database record. + * Also sets the model's id field if it has one. + */ + public function save() { + // figure out all the parameters and such for the query + $paramData = $this->toDbRow(); + $columnNames = array_keys($paramData); + $parameterNames = array_map(function (string $column) { + return ":$column"; + }, $columnNames); + $columns = implode(', ', $columnNames); + $parameters = implode(', ', $parameterNames); + + $classNameParts = explode('\\', static::class); + $className = $classNameParts[count($classNameParts) - 1]; + $tableName = self::mangleName($className); + + $db = Db::getInstance()->getPdo(); + + $whereClause = $this->getUpdateWhereClause($db); + $shouldUpdate = $whereClause != null; + $stmt = $db->prepare( + $shouldUpdate + ? "UPDATE $tableName SET ($columns) = ($parameters) WHERE $whereClause" + : "INSERT INTO $tableName ($columns) VALUES ($parameters)" + ); + + // pdo is really stupid and casts `false`/0 to an empty string, which breaks the query + // so i have to cast `false`s and `0`s manually. hooray + // i also have to convert backed enums to their value because they dont implement Stringable + $paramData = array_map(function ($item) { + if ($item === false || $item === 0) + return '0'; + if (is_a($item, \BackedEnum::class)) + return $item->value; + return $item; + }, $paramData); + // actually save it !!!! yay + $stmt->execute($paramData); + + if (property_exists($this, 'id')) { + $this->id = $shouldUpdate + ? static::findWhere($whereClause, [])->id + : (int) $db->lastInsertId(); + } + + // recursively save inner models + // but not fetchable ones because they should really be saved separately + $reflProps = (new \ReflectionObject($this))->getProperties(); + foreach ($reflProps as $reflProp) { + assert($reflProp instanceof \ReflectionProperty); + $reflType = $reflProp->getType(); + $propName = $reflProp->getName(); + if (!($reflType instanceof \ReflectionNamedType)) + continue; + $type = $reflType->getName(); + if (is_subclass_of($type, Model::class) && !is_subclass_of($type, FetchableModel::class)) { + if (property_exists($this, 'id')) + $this->{$propName}->setOwnerId($this->id); + $this->{$propName}->save(); + } + } + + $this->saved = true; + } } \ No newline at end of file -- cgit v1.3