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
64 changes: 64 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,74 @@ 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`.

`datetime` casts assume stored strings are UTC wall-time values. If you do not want implicit timezone conversion by the database, prefer `DATETIME`-style columns or ensure the connection session timezone is UTC before using `TIMESTAMP` columns.

### Dynamic finders and counters

Build expressive queries straight from method names:
Expand Down
119 changes: 8 additions & 111 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,124 +1,21 @@
# Roadmap

This roadmap tracks the improvement work for `freshsauce/model`.
This roadmap now tracks only the remaining optional backlog for `freshsauce/model`.

Current status:
Phases 1 through 3 are complete, and Phase 4.1 through 4.3 have been delivered.

1. Phase 1 is complete.
2. Phase 2 is complete.
3. Phase 3 is complete.
4. Phase 4 remains optional and has not been started.

The sequencing remains intentional:

1. Fix correctness issues before expanding the API.
2. Improve developer ergonomics without turning the library into a heavyweight ORM.
3. Tighten quality and portability before considering broader feature growth.
4. Add optional features only where they preserve the package's lightweight position.

## Principles

- Keep PDO-first escape hatches intact.
- Prefer additive changes with low migration cost.
- Tighten behavior with tests before changing public APIs.
- Avoid feature growth that pushes the library toward framework-scale complexity.

## Phase 1: Core correctness and safety

Status: completed

Summary:

- Fixed serialization, zero-like primary key handling, invalid dynamic finder failures, and empty-array query behavior.
- Replaced generic exceptions with a small library exception hierarchy.
- Added regression coverage for the above edge cases.

## Phase 2: API ergonomics and typing

Status: completed

Summary:

- Added instance-aware validation hooks with legacy compatibility.
- Added optional strict field handling and focused query helpers.
- Tightened typing, static analysis, and public documentation around the preferred API.

## Phase 3: Quality, portability, and maintenance

Status: completed

Summary:

- Expanded cross-driver integration coverage for connection sharing, custom keys, metadata refresh, timestamp behavior, and PostgreSQL schema-qualified tables.
- Added `refreshTableMetadata()` and made UTC timestamp behavior explicit.
- Normalized no-op update handling while preserving single-row primary key update expectations.

## Phase 4: Optional feature expansion

Goal: add features that help real applications, but only if they fit the package's lightweight position.

Priority: lower

Phases 1 through 3 are complete, so this is now the remaining backlog.

### Candidate 4.1: Transaction helpers

Possible scope:

- `transaction(callable $callback)`
- pass through `beginTransaction()`, `commit()`, `rollBack()` wrappers

Why:
This adds practical value without changing the core model shape.

### Candidate 4.2: Configurable timestamp columns

Possible scope:

- opt-in timestamp column names
- disable automatic timestamps per model

Why:
The current `created_at` / `updated_at` convention is convenient but rigid.

### Candidate 4.3: Attribute casting

Possible scope:

- integer
- float
- boolean
- datetime
- JSON array/object

Why:
Casting improves ergonomics substantially without requiring relationships or a large query layer.

### Candidate 4.4: Composite keys or relationship support
## Remaining item: 4.4 Composite keys or relationship support

Why this is last:
This is where complexity rises sharply. It should only happen if the maintainer wants the library to move beyond lightweight active-record usage.

- This is where complexity rises sharply.
- It should only happen if the maintainer wants the library to move beyond lightweight active-record usage.

Recommendation:

- Do not start here by default.
- Re-evaluate only after the earlier phases have shipped and real user demand is clear.

## Suggested issue order

If this work is split into GitHub issues, the most practical order is:

1. Add transaction helpers.
2. Add configurable timestamp column support.
3. Add attribute casting.
4. Re-evaluate whether composite keys or relationship support are warranted.

## Suggested release strategy

- Release 1: Phase 1 correctness and exception work. Shipped.
- Release 2: Phase 2 ergonomics, typing, and documentation updates. Shipped.
- Release 3: Phase 3 portability and maintenance hardening. Shipped.
- Release 4: optional feature work only if it still fits the package scope.
- Re-evaluate only after clear real-world demand.
- Preserve the package's lightweight, PDO-first position if any further expansion happens.

## Out of scope unless demand changes

Expand Down
47 changes: 45 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ 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`.

For `datetime`, string values are interpreted as UTC wall-time values. Prefer `DATETIME`-style columns, or ensure the connection session timezone is UTC when using database types that perform timezone conversion.

### `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 +108,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 +152,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 +203,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 +217,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
Loading