$npx -y skills add Lonsdale201/wp-agent-skills --skill bd-presenterExtend the better-data Presenter — add a fluent builder
| 1 | # better-data: Extending the Presenter |
| 2 | |
| 3 | For library maintainers adding output transformations to the better-data Presenter — a new fluent builder method (`mask`, `formatPhone`, `hideIfBlank`), a `PresentationContext` flag (admin vs REST vs export), or a Formatter helper. The Presenter is the read-side projection layer between a `DataObject` and whatever consumes it (admin UI, REST response, audit log). |
| 4 | |
| 5 | ## Misconception this skill corrects |
| 6 | |
| 7 | > "The Presenter is `readonly` like the DataObject — every fluent method must clone and return a new instance." |
| 8 | |
| 9 | Wrong. The DataObject is `readonly`, the Presenter is intentionally a **mutable builder**. Look at [src/Presenter/Presenter.php:65-89](Presenter.php) — `class Presenter` (not `final readonly class`), with private mutable properties `$only`, `$hidden`, `$rename`, `$computed`, `$presets`, `$includeSensitive`. Each fluent method assigns to `$this->...` and returns `$this`. This is deliberate — chained calls share state, repeated calls override, and there's no clone overhead. |
| 10 | |
| 11 | What IS immutable: `$this->dto` is `protected readonly DataObject` and never gets reassigned. The builder mutates ITS state to control HOW the DTO is rendered; the DTO itself stays untouched. |
| 12 | |
| 13 | The "don't mutate permanently" rule from older docs means: don't introduce a method whose effect can't be reset by a subsequent method or context swap. A method that pushes to a private array is fine; a method that writes to a static cache or to the wrapped DTO would be a regression. |
| 14 | |
| 15 | Other AI-prone misconceptions: |
| 16 | |
| 17 | - "I'll add `getSecretRevealed()` so consumers don't need `->reveal()` boilerplate." Wrong — the explicit `$dto->field->reveal()` inside a `compute()` closure IS the security audit point. Adding a bypass method is the same regression as a debug-mode log of secrets. |
| 18 | - "CollectionPresenter is a separate Presenter; I just add the method there too with copy/paste logic." Wrong — `CollectionPresenter` records each configurer as a closure on `$this->configurers` ([src/Presenter/CollectionPresenter.php:30-44](CollectionPresenter.php)) and replays it on every item via the per-item Presenter. The pattern is `$this->configurers[] = static fn (Presenter $p) => $p->yourMethod(...);`. No business logic on the collection side. |
| 19 | |
| 20 | ## When to use this skill |
| 21 | |
| 22 | Trigger when ANY of the following is true: |
| 23 | |
| 24 | - Adding a fluent method to `Presenter` / `CollectionPresenter` (`hideIfBlank`, `truncate`, `defaultIfNull`). |
| 25 | - Adding a flag / accessor to `PresentationContext` (`isExport()`, `currentUserId()`). |
| 26 | - Adding a Formatter helper under `src/Presenter/Formatter/`. |
| 27 | - Reviewing a PR that emits DTO values without going through `sensitiveFieldNames()` redaction. |
| 28 | |
| 29 | ## Workflow |
| 30 | |
| 31 | ### 1. Decide: per-call (Presenter) or per-context (PresentationContext) |
| 32 | |
| 33 | - **Per-call (fluent method):** the caller decides each invocation. Examples: `only(['email'])`, `hide('apiKey')`, `formatDate('createdAt', 'Y-m-d')`. Lives on `Presenter` + mirrored on `CollectionPresenter`. |
| 34 | - **Per-context (PresentationContext):** the entire rendering environment changes. Examples: REST response vs admin table vs export CSV. Lives on `PresentationContext`. |
| 35 | |
| 36 | Most additions are per-call. Per-context additions are rarer and require updating `PresentationContext::rest()`, `::admin()`, `::none()`, etc. |
| 37 | |
| 38 | ### 2. Per-call method shape |
| 39 | |
| 40 | Look at existing methods like `hide` ([Presenter.php:164](Presenter.php)) or `rename` ([Presenter.php:204](Presenter.php)): |
| 41 | |
| 42 | ```php |
| 43 | public function hideIfBlank(string|array $field): static |
| 44 | { |
| 45 | $fields = (array) $field; |
| 46 | foreach ($fields as $f) { |
| 47 | $this->hidden[$f] = static fn (PresentationContext $ctx, mixed $value): bool |
| 48 | => $value === null || $value === '' || $value === []; |
| 49 | } |
| 50 | return $this; |
| 51 | } |
| 52 | ``` |
| 53 | |
| 54 | Six stru |