|
| 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