From 9517203418a3cd38cc253af2bc6b41e8d59d2806 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Sun, 30 Nov 2025 13:56:31 +0100 Subject: [PATCH] feat: add database `Controller` class for manipulating entity data (#13) This PR adds a new database boilerplate for manipulating data stored in a database through an instanced database Model. The only method implemented by this class so far is `Controller()->update()`. It can be used to patch column values for a database row given an instanced `vlw\Database\Model`. An associative array of column keys and values are then passed to the update method. Reviewed-on: https://codeberg.org/vlw/scaffold/pulls/13 --- src/Database/Controller.php | 115 ++++++++++++++++++++++++++++++++++++ src/Database/Model.php | 21 +++---- 2 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 src/Database/Controller.php diff --git a/src/Database/Controller.php b/src/Database/Controller.php new file mode 100644 index 0000000..e709cdd --- /dev/null +++ b/src/Database/Controller.php @@ -0,0 +1,115 @@ + An array containing the allowed property types (literals and classes) + */ + private static function get_types(ReflectionNamedType|ReflectionUnionType $type): array { + $types = []; + + // Property value is nullable + if ($type->allowsNull()) { + $types[] = "null"; + } + + // Property only accepts one type + if ($type instanceof ReflectionNamedType) { + // Add type name to output and trim shorthand nullable from name if present + $types[] = ltrim($type->getName(), "?"); + return $types; + } + + return array_merge($types, $type->getTypes()); + } + + /** + * Creates a new instance of a class-like object + * + * @param string Namespaced class name to instance + * @param array Arguments to pass to the class constructor + * @return object Instanced class + */ + private static function instance_class(string $class_name, array $args): object { + $class = new ReflectionClass($class_name); + + // Return new instance of Enum from case name + if ($class->isEnum()) { + return $class_name::fromName(...$args); + } + + return $class->newInstance(...$args); + } + + /** + * Instance a new Controller for a given database Model + * + * @param Model An instance of a database Model entity + */ + public function __construct(private readonly Model $model) {} + + /** + * Update a database entity by passing an assoc array of column => value + * + * @param array Associative array of column keys and column values to update + * @return void + */ + public function update(array $values): void { + foreach($values as $key => $value) { + // Bail out if we're trying to update a column that has not been implemented + // by the passed database Model + if (!in_array($key, $this->model->columns)) { + continue; + } + + $property = new ReflectionProperty($this->model, $key); + $types = self::get_types($property->getSettableType()); + + // Value is definitely a primitive + if (!is_string($value)) { + // Property does not accept this primitive as value, bail out + if (!in_array(gettype($value), $types)) { + continue; + } + + // Set primitve value and move on + $this->model->{$key} = $value; + continue; + } + + // Returns the name of the first class object + $class_name = array_find($types, fn(string $type): bool => class_exists($type)); + + // No class object found, string value must be literal + if (!$class_name) { + // Property does not accept string literals as value, bail out + if (!in_array("string", $types)) { + continue; + } + + // Set string literal value and move on + $this->model->{$key} = $value; + continue; + } + + // Wrap single value in an array (as one constructor argument) + if (!is_array($value)) { + $value = [$value]; + } + + $this->model->{$key} = self::instance_class($class_name, $value); + } + } + } \ No newline at end of file diff --git a/src/Database/Model.php b/src/Database/Model.php index 0b12e91..6327812 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -10,7 +10,8 @@ * Abstract reading, creating, and updating of database entities. */ abstract class Model { - const DATE_FORMAT = Database::DATE_FORMAT; + public const DATE_FORMAT = Database::DATE_FORMAT; + public const DATETIME_FORMAT = Database::DATETIME_FORMAT; abstract public string $id { get; } @@ -21,7 +22,7 @@ /** * Insert values into a database table - * + * * @param string $table The target database table */ protected static function create(string $table, array $values): bool { @@ -30,22 +31,22 @@ /** * Create a new Model to retrieve a specific entity - * + * * @param string $table Target table where the entity is stored * @param array $columns An array of strings for each target database column * @param array $where vlw/MySQL->where() to locate the target entity */ public function __construct( - private readonly string $table, - private readonly array $columns, - private readonly array $where + public readonly string $table, + public readonly array $columns, + public readonly array $where ) { $this->db = new Database(); } /** * Fetch and store column values for instanced row - * + * * @return ?array The column and value of each target database column */ private ?array $row { @@ -69,7 +70,7 @@ /** * Returns true if instanced target ebity exists - * + * * @return bool Entity exists */ public function isset(): bool { @@ -83,7 +84,7 @@ /** * Get current row column value - * + * * @param string $key Target column * @return mixed Value of target column */ @@ -93,7 +94,7 @@ /** * Update current row column with value - * + * * @param string $key Target column to update * @param mixed $value New value of target column * @return bool Update was successful