From 4c7bde1400820f36caf8b2a5374007384c3018f3 Mon Sep 17 00:00:00 2001 From: winter Date: Mon, 9 Dec 2024 21:31:54 +0000 Subject: rename to Digitigrade / PawPub :3 --- Digitigrade/Model.php | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 Digitigrade/Model.php (limited to 'Digitigrade/Model.php') diff --git a/Digitigrade/Model.php b/Digitigrade/Model.php new file mode 100644 index 0000000..8a593eb --- /dev/null +++ b/Digitigrade/Model.php @@ -0,0 +1,219 @@ +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]); + for ($i = 0; $i < count($chars); $i++) { + $c = $chars[$i]; + if (ctype_upper($c)) { + array_splice($chars, $i, 1, ['_', strtolower($c)]); + } + } + 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++) { + if ($chars[$i] == '_') { + array_splice($chars, $i, 2, [strtoupper($chars[$i + 1])]); + } + } + return implode($chars); + } + + public function toDbRow(): array { + $refl = new \ReflectionObject($this); + $props = $refl->getProperties(); + $result = []; + foreach ($props as $p) { + $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; + } + + public static function fromDbRow(array|false $columns): ?static { + if ($columns === false) + return null; + // create the model + $obj = new static(); + $refl = new \ReflectionObject($obj); + // for each field of the db row ... + foreach (array_keys($columns) as $name) { + $value = $columns[$name]; + $name = self::unmangleName($name); // guess the appropriate field in the model + try { + // try to assign it directly (works for simple values) + $obj->{$name} = $value; + } catch (\TypeError $e) { + // if it's not the right type we have to try to wrangle it into the right one + // there's a few strategies for this + $type = $refl->getProperty($name)->getType(); + if ($type == null || !is_a($type, \ReflectionNamedType::class)) + throw $e; + $typeClass = $type->getName(); + if (enum_exists($typeClass)) { + // 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); + } else { + // otherwise try to instantiate the correct class and pass it the simple value + $obj->{$name} = new $typeClass($value); + } + } + } + // set up any referenced fields if needed + $obj->hydrate(); + $obj->saved = true; + return $obj; + } + + /** + * Initialises fields of the model that contain referenced / foreign keyed objects + * which couldn't be initialised automatically + * @return void + */ + 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)); + } + + public static function findAllWhere(string $whereClause, array $parameters): array { + $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) { + return static::fromDbRow($row); + }, $stmt->fetchAll(\PDO::FETCH_ASSOC)); + } + + 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