aboutsummaryrefslogtreecommitdiffhomepage
path: root/WpfTest/Model.php
diff options
context:
space:
mode:
Diffstat (limited to 'WpfTest/Model.php')
-rw-r--r--WpfTest/Model.php130
1 files changed, 122 insertions, 8 deletions
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 @@
<?php
namespace WpfTest;
+use WpfTest\Model\FetchableModel;
+
abstract class Model {
+ private bool $saved = false;
+
+ protected function setOwnerId(int $id) {
+ }
+
+ /**
+ * @return ?string a condition for a WHERE clause if this object already
+ * exists in the database, or null otherwise
+ */
+ protected function getUpdateWhereClause(\PDO $db): ?string {
+ if (isset($this->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