Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ The base model gives you the methods most applications reach for first:

If your table includes `created_at` and `updated_at`, they are populated automatically on insert and update.

Timestamps are generated in UTC using the `Y-m-d H:i:s` format. SQLite stores those values as text, while MySQL/MariaDB and PostgreSQL accept them in timestamp-style columns.

### Dynamic finders and counters

Build expressive queries straight from method names:
Expand Down Expand Up @@ -157,6 +159,8 @@ $statement = Freshsauce\Model\Model::execute(
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
```

If you change a table schema at runtime and need the model to see the new columns without reconnecting, call `YourModel::refreshTableMetadata()`.

### Validation hooks

Use instance-aware hooks when writes need application rules:
Expand Down Expand Up @@ -226,6 +230,8 @@ Freshsauce\Model\Model::connectDb(

SQLite is supported in the library and covered by the automated test suite.

Schema-qualified table names such as `reporting.categories` are supported for PostgreSQL models.

## Built for real projects

The repository includes:
Expand Down
68 changes: 60 additions & 8 deletions src/Model/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,16 @@ protected static function getFieldnames(): array
return self::$_tableColumns[$class];
}

/**
* Refresh cached table metadata for the current model class.
*
* @return void
*/
public static function refreshTableMetadata(): void
{
unset(self::$_tableColumns[static::class]);
}

/**
* Split a table name into schema and table, defaulting schema to public.
*
Expand Down Expand Up @@ -985,7 +995,7 @@ protected static function fetchWhereWithSuffix(string $SQLfragment = '', array $
* returns an array of objects of the sub-class which match the conditions
*
* @param string $SQLfragment conditions, sorting, grouping and limit to apply (to right of WHERE keywords)
* @param array<int, mixed> $params optional params to be escaped and injected into the SQL query (standrd PDO syntax)
* @param array<int, mixed> $params optional params to be escaped and injected into the SQL query (standard PDO syntax)
* @param bool $limitOne if true the first match will be returned
*
* @return array|static|null
Expand All @@ -999,7 +1009,7 @@ public static function fetchWhere(string $SQLfragment = '', array $params = [],
* returns an array of objects of the sub-class which match the conditions
*
* @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)
* @param array $params optional params to be escaped and injected into the SQL query (standard PDO syntax)
*
* @return array object[] of objects of calling class
*/
Expand All @@ -1014,7 +1024,7 @@ public static function fetchAllWhere(string $SQLfragment = '', array $params = [
* returns an object of the sub-class which matches the conditions
*
* @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)
* @param array $params optional params to be escaped and injected into the SQL query (standard PDO syntax)
*
* @return static|null object of calling class
*/
Expand Down Expand Up @@ -1137,7 +1147,7 @@ public function delete()
* Delete records based on an SQL conditions
*
* @param string $where SQL fragment of conditions
* @param array $params optional params to be escaped and injected into the SQL query (standrd PDO syntax)
* @param array $params optional params to be escaped and injected into the SQL query (standard PDO syntax)
*
* @return \PDOStatement
*/
Expand Down Expand Up @@ -1206,6 +1216,16 @@ protected function runUpdateValidation(): void
$this->validateForUpdate();
}

/**
* Return a UTC timestamp string suitable for the built-in timestamp columns.
*
* @return string
*/
protected static function currentTimestamp(): string
{
return gmdate('Y-m-d H:i:s');
}

/**
* insert a row into the database table, and update the primary key field with the one generated on insert
*
Expand All @@ -1219,7 +1239,7 @@ protected function runUpdateValidation(): void
public function insert(bool $autoTimestamp = true, bool $allowSetPrimaryKey = false): bool
{
$pk = static::$_primary_column_name;
$timeStr = gmdate('Y-m-d H:i:s');
$timeStr = static::currentTimestamp();
if ($autoTimestamp && in_array('created_at', static::getFieldnames())) {
$this->created_at = $timeStr;
}
Expand Down Expand Up @@ -1291,7 +1311,7 @@ public function insert(bool $autoTimestamp = true, bool $allowSetPrimaryKey = fa
public function update(bool $autoTimestamp = true): bool
{
if ($autoTimestamp && in_array('updated_at', static::getFieldnames())) {
$this->updated_at = gmdate('Y-m-d H:i:s');
$this->updated_at = static::currentTimestamp();
}
$this->runUpdateValidation();
$set = $this->setString();
Expand All @@ -1305,10 +1325,11 @@ public function update(bool $autoTimestamp = true): bool
$query,
$set['params']
);
if ($st->rowCount() == 1) {
if ($this->updateSucceeded($st)) {
$this->clearDirtyFields();
return true;
}
return ($st->rowCount() == 1);
return false;
}

/**
Expand Down Expand Up @@ -1417,6 +1438,37 @@ protected function hasPrimaryKeyValue(): bool
return $this->$primaryKey !== null;
}

/**
* Determine whether an update succeeded even when the driver reports zero changed rows.
*
* @param \PDOStatement $statement
*
* @return bool
*/
protected function updateSucceeded(\PDOStatement $statement): bool
{
$count = $statement->rowCount();

if ($count === 1) {
return true;
}

if ($count === 0) {
return static::existsWhere(
static::_quote_identifier(static::$_primary_column_name) . ' = ?',
[$this->{static::$_primary_column_name}]
);
}

throw new ModelException(
sprintf(
'Update affected %d rows for %s; expected at most one row.',
$count,
static::class
)
);
}

/**
* @param mixed $match
*
Expand Down
16 changes: 16 additions & 0 deletions test-src/Model/CodedCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Model;

/**
* @property int|null $code
* @property string|null $name
*/
class CodedCategory extends \Freshsauce\Model\Model
{
protected static $_primary_column_name = 'code';

protected static $_tableName = 'coded_categories';
}
15 changes: 15 additions & 0 deletions test-src/Model/MetadataRefreshCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Model;

/**
* @property int|null $id
* @property string|null $name
* @property string|null $description
*/
class MetadataRefreshCategory extends \Freshsauce\Model\Model
{
protected static $_tableName = 'metadata_refresh_categories';
}
14 changes: 14 additions & 0 deletions test-src/Model/SchemaQualifiedCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace App\Model;

/**
* @property int|null $id
* @property string|null $name
*/
class SchemaQualifiedCategory extends \Freshsauce\Model\Model
{
protected static $_tableName = 'orm_phase3.schema_categories';
}
14 changes: 14 additions & 0 deletions test-src/Model/UntimedCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace App\Model;

/**
* @property int|null $id
* @property string|null $name
*/
class UntimedCategory extends \Freshsauce\Model\Model
{
protected static $_tableName = 'untimed_categories';
}
Loading