Skip to content

Commit 8ef11f5

Browse files
authored
docs: add comprehensive ORM docs (#27)
* Add comprehensive ORM docs * Clarify datetime helper timezone docs
1 parent 842f12f commit 8ef11f5

3 files changed

Lines changed: 923 additions & 0 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ $loaded->delete();
8787

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

90+
## Documentation
91+
92+
Use the docs based on how much detail you need:
93+
94+
- [docs/guide.md](docs/guide.md) for setup, model definition, CRUD, querying, validation, strict fields, and database notes
95+
- [docs/api-reference.md](docs/api-reference.md) for method-by-method behavior and return types
96+
- [EXAMPLE.md](EXAMPLE.md) for shorter copy-paste examples
97+
9098
## What you get
9199

92100
### Full record lifecycle helpers
@@ -244,6 +252,7 @@ The repository includes:
244252

245253
## Learn more
246254

255+
- Need fuller ORM docs? Start with [docs/guide.md](docs/guide.md) and [docs/api-reference.md](docs/api-reference.md).
247256
- Want to see planned improvements? See [ROADMAP.md](ROADMAP.md).
248257
- Want fuller usage examples? See [EXAMPLE.md](EXAMPLE.md).
249258
- Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md).

docs/api-reference.md

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
# API Reference
2+
3+
This reference documents the public API of `Freshsauce\Model\Model` as implemented in [`src/Model/Model.php`](../src/Model/Model.php).
4+
5+
## Class configuration
6+
7+
These are the static members you are expected to override in subclasses.
8+
9+
### `protected static $_tableName`
10+
11+
Required. The database table for the model.
12+
13+
```php
14+
protected static $_tableName = 'categories';
15+
```
16+
17+
### `protected static $_primary_column_name`
18+
19+
Optional. Defaults to `id`.
20+
21+
```php
22+
protected static $_primary_column_name = 'code';
23+
```
24+
25+
### `protected static bool $_strict_fields`
26+
27+
Optional. Defaults to `false`. When enabled, unknown assignments throw `UnknownFieldException`.
28+
29+
### `public static $_db`
30+
31+
Inherited shared PDO connection. Redeclare this in a subclass only when that subclass needs an isolated connection.
32+
33+
## Construction and state
34+
35+
### `__construct(array $data = [])`
36+
37+
Initialises the model and hydrates known columns from the provided array.
38+
39+
### `hasData(): bool`
40+
41+
Returns whether the internal data object exists.
42+
43+
### `dataPresent(): bool`
44+
45+
Returns `true` when data is present, otherwise throws `MissingDataException`.
46+
47+
### `hydrate(array $data): void`
48+
49+
Maps known table columns from the input array onto the model. Unknown keys are ignored.
50+
51+
### `clear(): void`
52+
53+
Sets all known columns to `null` and resets dirty tracking.
54+
55+
### `toArray()`
56+
57+
Returns an associative array of known table columns and their current values.
58+
59+
### `markFieldDirty(string $name): void`
60+
61+
Marks a field dirty manually.
62+
63+
### `isFieldDirty(string $name): bool`
64+
65+
Returns whether a field is dirty.
66+
67+
### `clearDirtyFields(): void`
68+
69+
Clears the dirty-field tracking state.
70+
71+
### `__sleep()`
72+
73+
Serialises the `data` and `dirty` properties.
74+
75+
### `__serialize(): array`
76+
77+
Returns a normalised serialisable payload containing `data` and `dirty`.
78+
79+
### `__unserialize(array $data): void`
80+
81+
Restores serialised model state.
82+
83+
## Property access
84+
85+
### `__set(string $name, mixed $value): void`
86+
87+
Assigns a value to the model.
88+
89+
Behavior:
90+
91+
- in strict mode, resolves the name against real fields first
92+
- creates the internal data object on first assignment
93+
- marks the field as dirty
94+
95+
### `__get(string $name): mixed`
96+
97+
Returns a field value from the internal data store.
98+
99+
Throws:
100+
101+
- `MissingDataException` when data has not been initialised
102+
- `UnknownFieldException` when the field is not present in the current data object
103+
104+
### `__isset(string $name): bool`
105+
106+
Returns whether the current data object contains the field.
107+
108+
## Connection and database helpers
109+
110+
### `connectDb(string $dsn, string $username, string $password, array $driverOptions = []): void`
111+
112+
Creates and stores the PDO connection for the current model class hierarchy.
113+
114+
Notes:
115+
116+
- sets `PDO::ATTR_ERRMODE` to `PDO::ERRMODE_EXCEPTION`
117+
- clears cached prepared statements for the previous connection
118+
- clears cached column metadata
119+
120+
### `driverName(): string`
121+
122+
Returns the active PDO driver name.
123+
124+
### `refreshTableMetadata(): void`
125+
126+
Clears the cached table-column list for the current model class.
127+
128+
Use this after runtime schema changes.
129+
130+
### `execute(string $query, array $params = []): PDOStatement`
131+
132+
Prepares and executes a statement, returning the `PDOStatement`.
133+
134+
### `datetimeToMysqldatetime(int|string $dt): string`
135+
136+
Converts a Unix timestamp or date string into `Y-m-d H:i:s`.
137+
138+
Invalid date strings are converted as timestamp `0`.
139+
140+
### `createInClausePlaceholders(array $params): string`
141+
142+
Returns a comma-separated placeholder string for `IN (...)` clauses.
143+
144+
Examples:
145+
146+
- `[1, 2, 3]` -> `?,?,?`
147+
- `[]` -> `NULL`
148+
149+
## Record lifecycle
150+
151+
### `save(): bool`
152+
153+
Calls `update()` when the primary key is non-`null`; otherwise calls `insert()`.
154+
155+
Primary key values `0` and `'0'` count as existing primary keys and therefore use the update path.
156+
157+
### `insert(bool $autoTimestamp = true, bool $allowSetPrimaryKey = false): bool`
158+
159+
Inserts the current model as a new row.
160+
161+
Behavior:
162+
163+
- auto-fills `created_at` and `updated_at` when enabled and the fields exist
164+
- runs `validateForSave()` and `validateForInsert()`
165+
- clears dirty flags on success
166+
- updates the model's primary key from the database when the key is generated by the database
167+
- supports default-values inserts when there are no dirty fields
168+
169+
Set `$allowSetPrimaryKey` to `true` to include the current primary key value in the insert.
170+
171+
### `update(bool $autoTimestamp = true): bool`
172+
173+
Updates the current row by primary key.
174+
175+
Behavior:
176+
177+
- auto-fills `updated_at` when enabled and the field exists
178+
- runs `validateForSave()` and `validateForUpdate()`
179+
- updates only dirty known fields
180+
- returns `false` when there are no dirty fields to write
181+
- treats zero changed rows as success when the target row still exists
182+
183+
### `delete()`
184+
185+
Deletes the current row by primary key. Returns the result of `deleteById()`.
186+
187+
### `deleteById(int|string $id): bool`
188+
189+
Deletes one row by primary key. Returns `true` only when one row was removed.
190+
191+
### `deleteAllWhere(string $where, array $params = []): PDOStatement`
192+
193+
Deletes rows matching a condition fragment. Returns the raw statement.
194+
195+
Pass only the expression that belongs after `WHERE`.
196+
197+
## Reading records
198+
199+
### `getById(int|string $id): ?static`
200+
201+
Returns one model instance for the matching primary key, or `null`.
202+
203+
### `first(): ?static`
204+
205+
Returns the row with the lowest primary key value, or `null`.
206+
207+
### `last(): ?static`
208+
209+
Returns the row with the highest primary key value, or `null`.
210+
211+
### `find($id)`
212+
213+
Returns an array of model instances matching the primary key value.
214+
215+
This is intentionally different from `getById()`, which returns a single instance or `null`.
216+
217+
### `count(): int`
218+
219+
Returns the total row count for the model table.
220+
221+
### `exists(): bool`
222+
223+
Returns whether the table contains at least one row.
224+
225+
## Conditional reads
226+
227+
### `fetchWhere(string $SQLfragment = '', array $params = [], bool $limitOne = false): array|static|null`
228+
229+
Base helper for conditional reads.
230+
231+
Pass only the fragment that belongs after `WHERE`.
232+
233+
### `fetchAllWhere(string $SQLfragment = '', array $params = []): array`
234+
235+
Returns all matching rows as model instances.
236+
237+
### `fetchOneWhere(string $SQLfragment = '', array $params = []): ?static`
238+
239+
Returns the first matching row as a model instance, or `null`.
240+
241+
### `fetchAllWhereOrderedBy(string $orderByField, string $direction = 'ASC', string $SQLfragment = '', array $params = [], ?int $limit = null): array`
242+
243+
Returns all matching rows ordered by a real model field.
244+
245+
Constraints:
246+
247+
- `$orderByField` must resolve to a real model field
248+
- `$direction` must be `ASC` or `DESC`
249+
- `$limit`, when provided, must be greater than `0`
250+
251+
### `fetchOneWhereOrderedBy(string $orderByField, string $direction = 'ASC', string $SQLfragment = '', array $params = []): ?static`
252+
253+
Returns the first matching row using explicit ordering.
254+
255+
### `pluck(string $fieldname, string $SQLfragment = '', array $params = [], ?string $orderByField = null, string $direction = 'ASC', ?int $limit = null): array`
256+
257+
Returns one column from matching rows as a plain array.
258+
259+
Both `$fieldname` and `$orderByField` must resolve to real model fields.
260+
261+
### `countAllWhere(string $SQLfragment = '', array $params = []): int`
262+
263+
Returns the number of rows matching the condition fragment.
264+
265+
### `existsWhere(string $SQLfragment = '', array $params = []): bool`
266+
267+
Returns whether at least one row matches the condition fragment.
268+
269+
## Dynamic static methods
270+
271+
The model supports a dynamic method family through `__callStatic()`.
272+
273+
Preferred forms:
274+
275+
- `findBy<Field>($match)`
276+
- `findOneBy<Field>($match)`
277+
- `firstBy<Field>($match)`
278+
- `lastBy<Field>($match)`
279+
- `countBy<Field>($match)`
280+
281+
Examples:
282+
283+
```php
284+
Category::findByName('Fiction');
285+
Category::findOneByUpdatedAt('2026-03-08 12:00:00');
286+
Category::countByName(['Fiction', 'Fantasy']);
287+
```
288+
289+
Rules:
290+
291+
- field names are resolved against real columns
292+
- camelCase field names can map to snake_case columns
293+
- scalar matches become `= ?`
294+
- array matches become `IN (...)`
295+
- empty arrays short-circuit without querying the database
296+
297+
Legacy snake_case dynamic methods remain supported temporarily:
298+
299+
- `find_by_name($match)`
300+
- `findOne_by_name($match)`
301+
- `first_by_name($match)`
302+
- `last_by_name($match)`
303+
- `count_by_name($match)`
304+
305+
Those methods emit `E_USER_DEPRECATED`.
306+
307+
### `fetchOneWhereMatchingSingleField(string $fieldname, mixed $match, string $order): ?static`
308+
309+
Returns one row for a single-column match.
310+
311+
### `fetchAllWhereMatchingSingleField(string $fieldname, mixed $match): array`
312+
313+
Returns all rows for a single-column match.
314+
315+
## Validation extension points
316+
317+
### `validate(): bool`
318+
319+
Legacy static validation hook. Returns `true` by default.
320+
321+
### `validateForSave(): void`
322+
323+
Shared instance-level validation hook for both insert and update. By default it calls `static::validate()`.
324+
325+
### `validateForInsert(): void`
326+
327+
Insert-specific validation hook. No-op by default.
328+
329+
### `validateForUpdate(): void`
330+
331+
Update-specific validation hook. No-op by default.
332+
333+
Preferred customisation is to override the instance methods, not the legacy static method.
334+
335+
## Strict field controls
336+
337+
### `useStrictFields(bool $strict = true): void`
338+
339+
Enables or disables strict field mode for the current model class.
340+
341+
### `strictFieldsEnabled(): bool`
342+
343+
Returns whether strict field mode is currently enabled.
344+
345+
## Exceptions raised by the ORM
346+
347+
The API can raise these ORM-specific exceptions:
348+
349+
- `Freshsauce\Model\Exception\ConfigurationException`
350+
- `Freshsauce\Model\Exception\ConnectionException`
351+
- `Freshsauce\Model\Exception\InvalidDynamicMethodException`
352+
- `Freshsauce\Model\Exception\MissingDataException`
353+
- `Freshsauce\Model\Exception\ModelException`
354+
- `Freshsauce\Model\Exception\UnknownFieldException`
355+
356+
PDO exceptions still bubble up for database-level errors.

0 commit comments

Comments
 (0)