Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Skip it if you need relationship graphs, migrations, or a chainable query builde
- PDO-first: keep the convenience methods, keep full access to SQL, keep control.
- Framework-agnostic: use it in custom apps, legacy codebases, small services, or greenfield projects.
- Productive defaults: CRUD helpers, dynamic finders, counters, hydration, and timestamp handling are ready out of the box.
- Practical opt-ins: transaction helpers, configurable timestamp columns, and attribute casting stay lightweight but cover common app needs.
- Portable across databases: exercised against MySQL/MariaDB, PostgreSQL, and SQLite.

## Install in minutes
Expand Down Expand Up @@ -111,11 +112,72 @@ The base model gives you the methods most applications reach for first:
- `first()`
- `last()`
- `count()`
- `transaction()`
- `beginTransaction()`
- `commit()`
- `rollBack()`

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.

### Transactions without leaving the model

Use the built-in transaction helper when several writes should succeed or fail together:

```php
Category::transaction(function (): void {
$first = new Category(['name' => 'Sci-Fi']);
$first->save();

$second = new Category(['name' => 'Fantasy']);
$second->save();
});
```

If you need lower-level control, the model also exposes `beginTransaction()`, `commit()`, and `rollBack()` as thin wrappers around the current PDO connection.

### Timestamp columns can be configured per model

The default convention remains `created_at` and `updated_at`, but models can now opt into different column names or disable automatic timestamps entirely:

```php
class AuditLog extends Freshsauce\Model\Model
{
protected static $_tableName = 'audit_logs';
protected static ?string $_created_at_column = 'created_on';
protected static ?string $_updated_at_column = 'modified_on';
}

class LegacyCategory extends Freshsauce\Model\Model
{
protected static $_tableName = 'legacy_categories';
protected static bool $_auto_timestamps = false;
}
```

### Attribute casting

Cast common fields to application-friendly PHP types:

```php
class Product extends Freshsauce\Model\Model
{
protected static $_tableName = 'products';

protected static array $_casts = [
'stock' => 'integer',
'price' => 'float',
'is_active' => 'boolean',
'published_at' => 'datetime',
'tags' => 'array',
'settings' => 'object',
];
}
```

Supported cast types are `integer`, `float`, `boolean`, `datetime`, `array`, and `object`.

### Dynamic finders and counters

Build expressive queries straight from method names:
Expand Down
45 changes: 43 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ protected static $_primary_column_name = 'code';

Optional. Defaults to `false`. When enabled, unknown assignments throw `UnknownFieldException`.

### `protected static bool $_auto_timestamps`

Optional. Defaults to `true`. Set to `false` to disable built-in automatic timestamp handling for the model.

### `protected static ?string $_created_at_column`

Optional. Defaults to `created_at`. Set to a different column name to customise insert timestamps, or `null` to disable created-at writes.

### `protected static ?string $_updated_at_column`

Optional. Defaults to `updated_at`. Set to a different column name to customise insert/update timestamps, or `null` to disable updated-at writes.

### `protected static array $_casts`

Optional. Field cast map. Supported cast types are `integer`, `float`, `boolean`, `datetime`, `array`, and `object`.

### `public static $_db`

Inherited shared PDO connection. Redeclare this in a subclass only when that subclass needs an isolated connection.
Expand Down Expand Up @@ -90,12 +106,15 @@ Behavior:

- in strict mode, resolves the name against real fields first
- creates the internal data object on first assignment
- applies configured attribute casts before storing the value
- marks the field as dirty

### `__get(string $name): mixed`

Returns a field value from the internal data store.

When a field is configured in `$_casts`, the returned value is the cast PHP value.

Throws:

- `MissingDataException` when data has not been initialised
Expand Down Expand Up @@ -131,6 +150,28 @@ Use this after runtime schema changes.

Prepares and executes a statement, returning the `PDOStatement`.

### `beginTransaction(): bool`

Begins a transaction on the current model connection.

### `commit(): bool`

Commits the current transaction on the current model connection.

### `rollBack(): bool`

Rolls back the current transaction on the current model connection.

### `transaction(callable $callback): mixed`

Runs the callback inside a transaction and returns the callback result.

Behavior:

- begins and commits a transaction when no transaction is active
- rolls back automatically when the callback throws
- reuses an already-open outer transaction instead of nesting another one

### `datetimeToMysqldatetime(int|string $dt): string`

Converts a Unix timestamp or date string into `Y-m-d H:i:s`.
Expand Down Expand Up @@ -160,7 +201,7 @@ Inserts the current model as a new row.

Behavior:

- auto-fills `created_at` and `updated_at` when enabled and the fields exist
- auto-fills the configured created/update timestamp columns when enabled and the fields exist
- runs `validateForSave()` and `validateForInsert()`
- clears dirty flags on success
- updates the model's primary key from the database when the key is generated by the database
Expand All @@ -174,7 +215,7 @@ Updates the current row by primary key.

Behavior:

- auto-fills `updated_at` when enabled and the field exists
- auto-fills the configured update timestamp column when enabled and the field exists
- runs `validateForSave()` and `validateForUpdate()`
- updates only dirty known fields
- returns `false` when there are no dirty fields to write
Expand Down
87 changes: 80 additions & 7 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class Category extends Freshsauce\Model\Model
protected static $_tableName = 'categories';
protected static $_primary_column_name = 'id';
protected static bool $_strict_fields = false;
protected static bool $_auto_timestamps = true;
}
```

Expand All @@ -88,6 +89,10 @@ Available configuration members:
- `protected static $_tableName`: required; the database table to use
- `protected static $_primary_column_name`: defaults to `id`
- `protected static bool $_strict_fields`: defaults to `false`
- `protected static bool $_auto_timestamps`: defaults to `true`
- `protected static ?string $_created_at_column`: defaults to `created_at`
- `protected static ?string $_updated_at_column`: defaults to `updated_at`
- `protected static array $_casts`: optional field cast map
- `public static $_db`: only redeclare this when a subclass needs its own isolated connection

Custom primary keys are supported:
Expand Down Expand Up @@ -159,8 +164,8 @@ $category->insert();

Notes:

- if `created_at` exists, `insert()` sets it automatically
- if `updated_at` exists, `insert()` sets it automatically
- if the configured created timestamp column exists, `insert()` sets it automatically
- if the configured updated timestamp column exists, `insert()` sets it automatically
- timestamps are generated in UTC using `Y-m-d H:i:s`
- `insert(false)` disables automatic timestamps
- `insert(false, true)` allows you to include an explicit primary key value
Expand Down Expand Up @@ -225,7 +230,7 @@ $category->update();

Update behavior:

- `updated_at` is refreshed automatically when that column exists
- the configured updated timestamp column is refreshed automatically when it exists
- `update(false)` disables automatic timestamp updates
- only dirty known fields are included in the SQL `SET` clause
- `update()` returns `false` when there is nothing dirty to write
Expand Down Expand Up @@ -495,16 +500,84 @@ Serialisation is supported:
- `serialize()` and `unserialize()` preserve values
- dirty state is preserved across serialisation round-trips

## Transactions

Use `transaction()` when several writes should succeed or fail together:

```php
Category::transaction(function (): void {
$first = new Category(['name' => 'Sci-Fi']);
$first->save();

$second = new Category(['name' => 'Fantasy']);
$second->save();
});
```

What it does:

- starts and commits a transaction when none is active
- rolls back automatically if the callback throws
- reuses an existing outer transaction instead of nesting another one

If you need manual control, the model also exposes `beginTransaction()`, `commit()`, and `rollBack()`.

## Timestamp behavior

Automatic timestamp handling is convention-based:
Automatic timestamp handling is configurable:

- `created_at` is filled on insert when the column exists
- `updated_at` is filled on insert and update when the column exists
- `created_at` is filled on insert by default when that column exists
- `updated_at` is filled on insert and update by default when that column exists
- timestamps are generated in UTC with `gmdate('Y-m-d H:i:s')`
- models without those columns save normally
- set `protected static bool $_auto_timestamps = false;` to disable the feature for a model
- set `protected static ?string $_created_at_column` or `$_updated_at_column` to use custom column names

Example:

```php
class AuditLog extends Freshsauce\Model\Model
{
protected static $_tableName = 'audit_logs';
protected static ?string $_created_at_column = 'created_on';
protected static ?string $_updated_at_column = 'modified_on';
}
```

## Attribute casting

Use `$_casts` to normalise fields to PHP types on assignment and when rows are loaded from the database:

```php
class Product extends Freshsauce\Model\Model
{
protected static $_tableName = 'products';

protected static array $_casts = [
'stock' => 'integer',
'price' => 'float',
'is_active' => 'boolean',
'published_at' => 'datetime',
'tags' => 'array',
'settings' => 'object',
];
}
```

Supported cast types:

- `integer`
- `float`
- `boolean`
- `datetime`
- `array`
- `object`

Notes:

If you need custom timestamp columns, that is currently outside the built-in feature set.
- `datetime` returns `DateTimeImmutable`
- `array` and `object` are stored as JSON strings in the database
- `toArray()` returns the current cast PHP values

## Exceptions

Expand Down
Loading