$npx -y skills add Lonsdale201/wp-agent-skills --skill bd-data-objectAdd or modify DataObject subclasses inside the better-data library — the immutable, attribute-decorated DTOs the whole library is built around. Every DTO is final readonly class extends DataObject with constructor-promoted typed parameters; sources hydrate via ::fromArray, sinks
| 1 | # better-data: Adding a DataObject |
| 2 | |
| 3 | For library maintainers and downstream contributors who add or modify a `DataObject` subclass inside [better-data](../../README.md). Every typed shape — production DTOs in `src/`, test fixtures in `tests/Fixtures/`, plugin-level DTOs in the companion testbed — extends the abstract `DataObject` ([src/DataObject.php:36](DataObject.php)) and is the foundation that every engine (sources, sinks, validation, Presenter, REST schema, better-route bridge) reads against. |
| 4 | |
| 5 | ## Misconception this skill corrects |
| 6 | |
| 7 | > "I'll just declare the constructor parameters with the types I want and set the values when I instantiate the class — defaults are optional." |
| 8 | |
| 9 | In better-data, defaults are **load-bearing**. The hydration entry point `DataObject::fromArray` ([src/DataObject.php:47-79](DataObject.php)) iterates `ReflectionParameter`s and treats any parameter without `isDefaultValueAvailable()` (and without `allowsNull()`) as REQUIRED — throwing `MissingRequiredFieldException`. PHP's Reflection silently demotes earlier-positioned defaults to "required" if a later parameter has no default — so a single missing default at the end cascades and breaks `::fromArray` for the whole DTO. |
| 10 | |
| 11 | Other AI-prone misconceptions: |
| 12 | |
| 13 | - "I'll add `encrypt: true` to the `MetaKey` and store the property as a plain `string`." Wrong shape — `#[Encrypted]` writes ciphertext but the in-memory value is still a plain string that leaks via `var_dump` / `print_r` / `serialize`. Use `Secret` as the property type. |
| 14 | - "I'll add a public mutator method (`setEmail()`) to make consumer code more ergonomic." Wrong — every DTO is `final readonly class`. Mutation is `$dto->with(['email' => 'new@example.com'])` which returns a NEW instance. Mutators break the immutability contract that `Secret`, route-side projection, and Presenter caching all depend on. |
| 15 | |
| 16 | ## When to use this skill |
| 17 | |
| 18 | Trigger when ANY of the following is true: |
| 19 | |
| 20 | - Adding a new `final readonly class extends DataObject` under `src/`, `tests/Fixtures/`, or the companion plugin's `Dto/`. |
| 21 | - Adding, removing, or retyping a constructor parameter on an existing DTO. |
| 22 | - The diff or PR title mentions: "new DTO", "add Dto", "add field to <X>Dto", "introduce <Foo>Dto". |
| 23 | - Reviewing a class that extends `DataObject` — use this skill's checklist before approving. |
| 24 | - Hitting `MissingRequiredFieldException` at runtime — usually the cause is a trailing parameter without a default. |
| 25 | |
| 26 | ## Workflow |
| 27 | |
| 28 | ### 1. Choose the file location |
| 29 | |
| 30 | | What you're building | Path | |
| 31 | |---|---| |
| 32 | | Production DTO (library users hydrate it) | `src/<area>/<Name>Dto.php` | |
| 33 | | Test-only fixture | `tests/Fixtures/<Name>Dto.php` | |
| 34 | | Plugin-level DTO (companion testbed) | `wp-content/plugins/better-data-plugin-test/src/Dto/<Name>Dto.php` | |
| 35 | |
| 36 | ### 2. Declare the class |
| 37 | |
| 38 | ```php |
| 39 | namespace MyNamespace; |
| 40 | |
| 41 | use BetterData\DataObject; |
| 42 | use BetterData\Source\HasWpSources; |
| 43 | use BetterData\Sink\HasWpSinks; |
| 44 | use BetterData\Presenter\HasPresenter; |
| 45 | |
| 46 | final readonly class ProductDto extends DataObject |
| 47 | { |
| 48 | use HasWpSources; |
| 49 | use HasWpSinks; |
| 50 | use HasPresenter; |
| 51 | |
| 52 | public function __construct( |
| 53 | public int $id = 0, |
| 54 | public string $post_title = '', |
| 55 | public string $post_status = 'publish', |
| 56 | ) {} |
| 57 | } |
| 58 | ``` |
| 59 | |
| 60 | Three non-negotiables: |
| 61 | |
| 62 | - `final` — never extended. The library does not support subclass-of-DTO patterns. |
| 63 | - `readonly` — every property is immutable. Hydration writes once via `newInstanceArgs`; consumers mutate via `->with(...)`. |
| 64 | - `extends DataObject` — gives you `::fromArray`, `::fromArrayValidated`, `->toArray()`, `->with()`, attribute-aware coercion. |
| 65 | |
| 66 | ### 3. Constructor-promoted parameters with defaults on every trailing one |
| 67 | |
| 68 | The single |