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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ $loaded->delete();

That is the core promise of the library: minimal ceremony, direct results.

## Documentation

Use the docs based on how much detail you need:

- [docs/guide.md](docs/guide.md) for setup, model definition, CRUD, querying, validation, strict fields, and database notes
- [docs/api-reference.md](docs/api-reference.md) for method-by-method behavior and return types
- [EXAMPLE.md](EXAMPLE.md) for shorter copy-paste examples

## What you get

### Full record lifecycle helpers
Expand Down Expand Up @@ -244,6 +252,7 @@ The repository includes:

## Learn more

- Need fuller ORM docs? Start with [docs/guide.md](docs/guide.md) and [docs/api-reference.md](docs/api-reference.md).
- Want to see planned improvements? See [ROADMAP.md](ROADMAP.md).
- Want fuller usage examples? See [EXAMPLE.md](EXAMPLE.md).
- Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md).
356 changes: 356 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
# API Reference

This reference documents the public API of `Freshsauce\Model\Model` as implemented in [`src/Model/Model.php`](../src/Model/Model.php).

## Class configuration

These are the static members you are expected to override in subclasses.

### `protected static $_tableName`

Required. The database table for the model.

```php
protected static $_tableName = 'categories';
```

### `protected static $_primary_column_name`

Optional. Defaults to `id`.

```php
protected static $_primary_column_name = 'code';
```

### `protected static bool $_strict_fields`

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

### `public static $_db`

Inherited shared PDO connection. Redeclare this in a subclass only when that subclass needs an isolated connection.

## Construction and state

### `__construct(array $data = [])`

Initialises the model and hydrates known columns from the provided array.

### `hasData(): bool`

Returns whether the internal data object exists.

### `dataPresent(): bool`

Returns `true` when data is present, otherwise throws `MissingDataException`.

### `hydrate(array $data): void`

Maps known table columns from the input array onto the model. Unknown keys are ignored.

### `clear(): void`

Sets all known columns to `null` and resets dirty tracking.

### `toArray()`

Returns an associative array of known table columns and their current values.

### `markFieldDirty(string $name): void`

Marks a field dirty manually.

### `isFieldDirty(string $name): bool`

Returns whether a field is dirty.

### `clearDirtyFields(): void`

Clears the dirty-field tracking state.

### `__sleep()`

Serialises the `data` and `dirty` properties.

### `__serialize(): array`

Returns a normalised serialisable payload containing `data` and `dirty`.

### `__unserialize(array $data): void`

Restores serialised model state.

## Property access

### `__set(string $name, mixed $value): void`

Assigns a value to the model.

Behavior:

- in strict mode, resolves the name against real fields first
- creates the internal data object on first assignment
- marks the field as dirty

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

Returns a field value from the internal data store.

Throws:

- `MissingDataException` when data has not been initialised
- `UnknownFieldException` when the field is not present in the current data object

### `__isset(string $name): bool`

Returns whether the current data object contains the field.

## Connection and database helpers

### `connectDb(string $dsn, string $username, string $password, array $driverOptions = []): void`

Creates and stores the PDO connection for the current model class hierarchy.

Notes:

- sets `PDO::ATTR_ERRMODE` to `PDO::ERRMODE_EXCEPTION`
- clears cached prepared statements for the previous connection
- clears cached column metadata

### `driverName(): string`

Returns the active PDO driver name.

### `refreshTableMetadata(): void`

Clears the cached table-column list for the current model class.

Use this after runtime schema changes.

### `execute(string $query, array $params = []): PDOStatement`

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

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

Converts a Unix timestamp or date string into `Y-m-d H:i:s`.

Invalid date strings are converted as timestamp `0`.

### `createInClausePlaceholders(array $params): string`

Returns a comma-separated placeholder string for `IN (...)` clauses.

Examples:

- `[1, 2, 3]` -> `?,?,?`
- `[]` -> `NULL`

## Record lifecycle

### `save(): bool`

Calls `update()` when the primary key is non-`null`; otherwise calls `insert()`.

Primary key values `0` and `'0'` count as existing primary keys and therefore use the update path.

### `insert(bool $autoTimestamp = true, bool $allowSetPrimaryKey = false): bool`

Inserts the current model as a new row.

Behavior:

- auto-fills `created_at` and `updated_at` 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
- supports default-values inserts when there are no dirty fields

Set `$allowSetPrimaryKey` to `true` to include the current primary key value in the insert.

### `update(bool $autoTimestamp = true): bool`

Updates the current row by primary key.

Behavior:

- auto-fills `updated_at` 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
- treats zero changed rows as success when the target row still exists

### `delete()`

Deletes the current row by primary key. Returns the result of `deleteById()`.

### `deleteById(int|string $id): bool`

Deletes one row by primary key. Returns `true` only when one row was removed.

### `deleteAllWhere(string $where, array $params = []): PDOStatement`

Deletes rows matching a condition fragment. Returns the raw statement.

Pass only the expression that belongs after `WHERE`.

## Reading records

### `getById(int|string $id): ?static`

Returns one model instance for the matching primary key, or `null`.

### `first(): ?static`

Returns the row with the lowest primary key value, or `null`.

### `last(): ?static`

Returns the row with the highest primary key value, or `null`.

### `find($id)`

Returns an array of model instances matching the primary key value.

This is intentionally different from `getById()`, which returns a single instance or `null`.

### `count(): int`

Returns the total row count for the model table.

### `exists(): bool`

Returns whether the table contains at least one row.

## Conditional reads

### `fetchWhere(string $SQLfragment = '', array $params = [], bool $limitOne = false): array|static|null`

Base helper for conditional reads.

Pass only the fragment that belongs after `WHERE`.

### `fetchAllWhere(string $SQLfragment = '', array $params = []): array`

Returns all matching rows as model instances.

### `fetchOneWhere(string $SQLfragment = '', array $params = []): ?static`

Returns the first matching row as a model instance, or `null`.

### `fetchAllWhereOrderedBy(string $orderByField, string $direction = 'ASC', string $SQLfragment = '', array $params = [], ?int $limit = null): array`

Returns all matching rows ordered by a real model field.

Constraints:

- `$orderByField` must resolve to a real model field
- `$direction` must be `ASC` or `DESC`
- `$limit`, when provided, must be greater than `0`

### `fetchOneWhereOrderedBy(string $orderByField, string $direction = 'ASC', string $SQLfragment = '', array $params = []): ?static`

Returns the first matching row using explicit ordering.

### `pluck(string $fieldname, string $SQLfragment = '', array $params = [], ?string $orderByField = null, string $direction = 'ASC', ?int $limit = null): array`

Returns one column from matching rows as a plain array.

Both `$fieldname` and `$orderByField` must resolve to real model fields.

### `countAllWhere(string $SQLfragment = '', array $params = []): int`

Returns the number of rows matching the condition fragment.

### `existsWhere(string $SQLfragment = '', array $params = []): bool`

Returns whether at least one row matches the condition fragment.

## Dynamic static methods

The model supports a dynamic method family through `__callStatic()`.

Preferred forms:

- `findBy<Field>($match)`
- `findOneBy<Field>($match)`
- `firstBy<Field>($match)`
- `lastBy<Field>($match)`
- `countBy<Field>($match)`

Examples:

```php
Category::findByName('Fiction');
Category::findOneByUpdatedAt('2026-03-08 12:00:00');
Category::countByName(['Fiction', 'Fantasy']);
```

Rules:

- field names are resolved against real columns
- camelCase field names can map to snake_case columns
- scalar matches become `= ?`
- array matches become `IN (...)`
- empty arrays short-circuit without querying the database

Legacy snake_case dynamic methods remain supported temporarily:

- `find_by_name($match)`
- `findOne_by_name($match)`
- `first_by_name($match)`
- `last_by_name($match)`
- `count_by_name($match)`

Those methods emit `E_USER_DEPRECATED`.

### `fetchOneWhereMatchingSingleField(string $fieldname, mixed $match, string $order): ?static`

Returns one row for a single-column match.

### `fetchAllWhereMatchingSingleField(string $fieldname, mixed $match): array`

Returns all rows for a single-column match.

## Validation extension points

### `validate(): bool`

Legacy static validation hook. Returns `true` by default.

### `validateForSave(): void`

Shared instance-level validation hook for both insert and update. By default it calls `static::validate()`.

### `validateForInsert(): void`

Insert-specific validation hook. No-op by default.

### `validateForUpdate(): void`

Update-specific validation hook. No-op by default.

Preferred customisation is to override the instance methods, not the legacy static method.

## Strict field controls

### `useStrictFields(bool $strict = true): void`

Enables or disables strict field mode for the current model class.

### `strictFieldsEnabled(): bool`

Returns whether strict field mode is currently enabled.

## Exceptions raised by the ORM

The API can raise these ORM-specific exceptions:

- `Freshsauce\Model\Exception\ConfigurationException`
- `Freshsauce\Model\Exception\ConnectionException`
- `Freshsauce\Model\Exception\InvalidDynamicMethodException`
- `Freshsauce\Model\Exception\MissingDataException`
- `Freshsauce\Model\Exception\ModelException`
- `Freshsauce\Model\Exception\UnknownFieldException`

PDO exceptions still bubble up for database-level errors.
Loading