From 6b7a9f8e1dcc9a504d31e3fdd8638000cbeedc61 Mon Sep 17 00:00:00 2001 From: Dave Barnwell Date: Fri, 30 Jan 2026 22:25:29 +0000 Subject: [PATCH] Fix model edge cases and add tests --- src/Model/Model.php | 48 +++++++++++++++++++++++++----------- tests/Model/CategoryTest.php | 36 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/Model/Model.php b/src/Model/Model.php index 607fd59..bc3ac72 100644 --- a/src/Model/Model.php +++ b/src/Model/Model.php @@ -228,6 +228,9 @@ public static function connectDb(string $dsn, string $username, string $password { static::$_db = new \PDO($dsn, $username, $password, $driverOptions); static::$_db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); // Set Errorhandling to Exception + static::$_identifier_quote_character = null; + static::$_stmt = array(); + self::$_tableColumns = array(); static::_setup_identifier_quote_character(); } @@ -443,9 +446,9 @@ public function toArray() * * @param int|string $id * - * @return static + * @return static|null */ - public static function getById(int|string $id): static + public static function getById(int|string $id): ?static { return static::fetchOneWhere(static::_quote_identifier(static::$_primary_column_name) . ' = ?', array($id)); } @@ -453,9 +456,9 @@ public static function getById(int|string $id): static /** * Get the first record in the table * - * @return static + * @return static|null */ - public static function first(): static + public static function first(): ?static { return static::fetchOneWhere('1=1 ORDER BY ' . static::_quote_identifier(static::$_primary_column_name) . ' ASC'); } @@ -463,9 +466,9 @@ public static function first(): static /** * Get the last record in the table * - * @return static + * @return static|null */ - public static function last(): static + public static function last(): ?static { return static::fetchOneWhere('1=1 ORDER BY ' . static::_quote_identifier(static::$_primary_column_name) . ' DESC'); } @@ -539,7 +542,7 @@ public static function __callStatic($name, $arguments) * @param string|array $match * @param string $order ASC|DESC * - * @return object of calling class + * @return static|null object of calling class */ public static function fetchOneWhereMatchingSingleField($fieldname, $match, $order) { @@ -626,9 +629,9 @@ protected static function addWherePrefix($SQLfragment) * @param array $params optional params to be escaped and injected into the SQL query (standrd PDO syntax) * @param bool $limitOne if true the first match will be returned * - * @return array|static object[]|object of objects of calling class + * @return array|static|null object[]|object of objects of calling class */ - public static function fetchWhere($SQLfragment = '', $params = array(), $limitOne = false): array|static + public static function fetchWhere($SQLfragment = '', $params = array(), $limitOne = false): array|static|null { $class = get_called_class(); $SQLfragment = self::addWherePrefix($SQLfragment); @@ -638,7 +641,11 @@ public static function fetchWhere($SQLfragment = '', $params = array(), $limitOn ); $st->setFetchMode(\PDO::FETCH_ASSOC); if ($limitOne) { - $instance = new $class($st->fetch()); + $row = $st->fetch(); + if ($row === false) { + return null; + } + $instance = new $class($row); $instance->clearDirtyFields(); return $instance; } @@ -672,9 +679,9 @@ public static function fetchAllWhere($SQLfragment = '', $params = array()): arra * @param string $SQLfragment conditions, sorting, grouping and limit to apply (to right of WHERE keywords) * @param array $params optional params to be escaped and injected into the SQL query (standrd PDO syntax) * - * @return static object of calling class + * @return static|null object of calling class */ - public static function fetchOneWhere($SQLfragment = '', $params = array()): static + public static function fetchOneWhere($SQLfragment = '', $params = array()): ?static { /** @var static $result */ $result = static::fetchWhere($SQLfragment, $params, true); @@ -780,8 +787,18 @@ public function insert($autoTimestamp = true, $allowSetPrimaryKey = false) return false; } - $query = 'INSERT INTO ' . static::_quote_identifier(static::$_tableName) . ' SET ' . $set['sql']; - $st = static::execute($query, $set['params']); + if ($set['sql'] === '') { + if ($driver === 'sqlite' || $driver === 'sqlite2') { + $query = 'INSERT INTO ' . static::_quote_identifier(static::$_tableName) . ' DEFAULT VALUES'; + $st = static::execute($query); + } else { + $query = 'INSERT INTO ' . static::_quote_identifier(static::$_tableName) . ' () VALUES ()'; + $st = static::execute($query); + } + } else { + $query = 'INSERT INTO ' . static::_quote_identifier(static::$_tableName) . ' SET ' . $set['sql']; + $st = static::execute($query, $set['params']); + } if ($st->rowCount() == 1) { if ($allowSetPrimaryKey !== true) { $db = static::$_db; @@ -809,6 +826,9 @@ public function update($autoTimestamp = true) } $this->validate(); $set = $this->setString(); + if ($set['sql'] === '') { + return false; + } $limitClause = static::supportsUpdateLimit() ? ' LIMIT 1' : ''; $query = 'UPDATE ' . static::_quote_identifier(static::$_tableName) . ' SET ' . $set['sql'] . ' WHERE ' . static::_quote_identifier(static::$_primary_column_name) . ' = ?' . $limitClause; $set['params'][] = $this->{static::$_primary_column_name}; diff --git a/tests/Model/CategoryTest.php b/tests/Model/CategoryTest.php index 474b07c..983ad0c 100644 --- a/tests/Model/CategoryTest.php +++ b/tests/Model/CategoryTest.php @@ -110,12 +110,22 @@ public function testCreateAndGetById(): void // read category back into a new object $read_category = App\Model\Category::getById($category->id); + $this->assertNotNull($read_category); $this->assertEquals($read_category->name, $_name); $this->assertEquals($read_category->id, $category->id); $this->assertNotEmpty($category->created_at); $this->assertNotEmpty($category->updated_at); } + /** + * @covers ::fetchOneWhere + */ + public function testFetchOneWhereReturnsNullWhenMissing(): void + { + $missing = App\Model\Category::fetchOneWhere('name = ?', ['__missing__' . uniqid('', true)]); + $this->assertNull($missing); + } + /** * @covers ::save */ @@ -147,6 +157,32 @@ public function testCreateAndModify(): void } + /** + * @covers ::insert + */ + public function testInsertWithDefaultValuesWhenNoDirtyFields(): void + { + $category = new App\Model\Category(); + $category->clearDirtyFields(); + + $this->assertTrue($category->insert(false)); + $this->assertNotEmpty($category->id); + } + + /** + * @covers ::update + */ + public function testUpdateWithoutDirtyFieldsReturnsFalse(): void + { + $category = new App\Model\Category(array( + 'name' => 'Nonfiction' + )); + $category->save(); + $category->clearDirtyFields(); + + $this->assertFalse($category->update(false)); + } + /** * @covers ::__callStatic * @covers ::fetchAllWhereMatchingSingleField