|
| 1 | +--- |
| 2 | +applyTo: "pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/**/*.inc" |
| 3 | +--- |
| 4 | + |
| 5 | +# Models — `RESTAPI\Models\*` |
| 6 | + |
| 7 | +Models hold the actual feature behavior of the package. Endpoints are thin adapters; everything that _does_ something belongs here. |
| 8 | + |
| 9 | +For full guidance and worked examples see: |
| 10 | + |
| 11 | +- `.github/skills/endpoint-model-field.md` |
| 12 | +- `.github/skills/anatomy-of-a-feature.md` |
| 13 | +- `docs/BUILDING_CUSTOM_MODEL_CLASSES.md` |
| 14 | + |
| 15 | +## Constructor signature is fixed |
| 16 | + |
| 17 | +```php |
| 18 | +public function __construct( |
| 19 | + mixed $id = null, |
| 20 | + mixed $parent_id = null, |
| 21 | + mixed $data = [], |
| 22 | + mixed ...$options, |
| 23 | +) { |
| 24 | + # 1. Set Model attributes (config_path, many, subsystem, ...) |
| 25 | + # 2. Define Field objects on $this |
| 26 | + # 3. parent::__construct(...) MUST be the last statement |
| 27 | + parent::__construct($id, $parent_id, $data, ...$options); |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +Order matters. Field objects must exist before the parent constructor runs. |
| 32 | + |
| 33 | +## Naming |
| 34 | + |
| 35 | +- Class: PascalCase, singular noun. `FirewallAlias`, `DNSResolverHostOverrideAlias`, `SystemHostname`. |
| 36 | +- File: `<ClassName>.inc`. |
| 37 | +- One class per file. |
| 38 | + |
| 39 | +## Schema lives on Fields |
| 40 | + |
| 41 | +Define every property as a typed `Field` (`StringField`, `IntegerField`, `BooleanField`, `ForeignModelField`, `NestedModelField`, etc.). Use Field constructor arguments for `required`, `default`, `choices`, `unique`, `sensitive`, `read_only`, `write_only`, `representation_only`, `many`, `delimiter`, `internal_name`, `internal_namespace`, `conditions`, `validators`, `verbose_name`, `help_text`. Do not validate inside the constructor — let Fields and Validators do it. |
| 42 | + |
| 43 | +## Validation |
| 44 | + |
| 45 | +| Need | Mechanism | |
| 46 | +| ---------------------------- | ---------------------------------------------------------------------- | |
| 47 | +| Reusable rule | A class in `RESTAPI/Validators/` attached via `validators: [...]`. | |
| 48 | +| Field-specific, not reusable | `validate_<field_name>($value): <type>` returning the validated value. | |
| 49 | +| Cross-field | `validate_extra(): void`. | |
| 50 | + |
| 51 | +All three throw `RESTAPI\Responses\ValidationError` with a stable, namespaced `response_id`. |
| 52 | + |
| 53 | +## `apply()` and Dispatchers |
| 54 | + |
| 55 | +If the Model mutates a service (filter, DNS, services, certs, HA), implement: |
| 56 | + |
| 57 | +```php |
| 58 | +public function apply(): void { |
| 59 | + (new <Area>ApplyDispatcher(async: $this->async))->spawn_process(); |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +Set `$this->always_apply = true;` for singleton settings Models that should apply on every successful update. |
| 64 | + |
| 65 | +Never call `filter_configure*()`, `services_*_configure()`, or other long-running pfSense functions directly from a Model — wrap them in a Dispatcher. |
| 66 | + |
| 67 | +## Non-config-backed Models |
| 68 | + |
| 69 | +If the Model does not store data in `$config`, set `$this->internal_callable = 'method_name';` and provide that method returning an array (single object, or list of arrays for `many` Models). Override `_create()` / `_update()` / `_delete()` only when needed. |
| 70 | + |
| 71 | +## Hard rules |
| 72 | + |
| 73 | +- No bare `exec()` / `shell_exec()` / `passthru()`. Use `RESTAPI\Core\Command`. |
| 74 | +- No pfSense Plus-only functions or paths. |
| 75 | +- Sensitive values must use `sensitive: true` on the Field. |
| 76 | +- Errors throw `RESTAPI\Responses\*` with a stable `UPPER_SNAKE_CASE` `response_id`. |
| 77 | +- Never edit generated files in the pfSense webroot — they are produced by `manage.php buildendpoints` from your Endpoint class. |
0 commit comments