$npx -y skills add Lonsdale201/wp-agent-skills --skill bd-attributeAdd a new declarative attribute to the better-data library (e.g. #[ArrayOf], #[Default], domain hint). Attributes live in src/Attribute/ as final readonly classes with constructor-promoted public properties — pure data carriers, never business logic. The failure mode that catches
| 1 | # better-data: Adding a new attribute |
| 2 | |
| 3 | For library maintainers introducing a new declarative hint that DTO authors will place on constructor parameters / properties — `#[ArrayOf]`, `#[Default]`, `#[ListOf]`-style markers, domain-specific decorators. The attribute itself is just a data carrier; the work is wiring it into every engine that reads attributes, because partial wiring silently degrades. |
| 4 | |
| 5 | ## Misconception this skill corrects |
| 6 | |
| 7 | > "I'll add `src/Attribute/Foo.php` and read it in `PostSink` — done." |
| 8 | |
| 9 | Attributes are read by **multiple** engines, and missing one leaves a feature that works on a happy path and fails subtly elsewhere. The original `encrypt` flag was meta-only; when options needed the same semantics, the partial wiring left a footgun until `#[Encrypted]` replaced it across `OptionSink`, `PostSink`, `AttributeDrivenHydrator`, `RestSchemaBuilder`, and Presenter — all in one go. |
| 10 | |
| 11 | The engines that read attributes today, all need to know about a new attribute relevant to their concern: |
| 12 | |
| 13 | - **Read-side hydration:** `AttributeDrivenHydrator` ([src/Internal/AttributeDrivenHydrator.php](AttributeDrivenHydrator.php)) and `DataObject::coerceParameter` ([src/DataObject.php:168](DataObject.php)). |
| 14 | - **Write-side projection:** `SinkProjection::prepareValue` ([src/Internal/SinkProjection.php:193](SinkProjection.php)) and `OptionSink::projectForStorage` ([src/Sink/OptionSink.php:119](OptionSink.php)). |
| 15 | - **REST / OpenAPI schema:** `RestSchemaBuilder::buildProperty` ([src/Internal/RestSchemaBuilder.php](RestSchemaBuilder.php)). |
| 16 | - **Output / display:** `Presenter::sensitiveFieldNames` and friends in [src/Presenter/Presenter.php](Presenter.php). |
| 17 | |
| 18 | If your attribute is a write-time concern (encryption, formatting, slashing), all four still need to coordinate — read-side has to invert what write-side did. |
| 19 | |
| 20 | ## When to use this skill |
| 21 | |
| 22 | Trigger when ANY of the following is true: |
| 23 | |
| 24 | - Creating a new file under `src/Attribute/`. |
| 25 | - Adding `#[Attribute(...)]` to any class. |
| 26 | - The diff adds a new attribute reference (`#[NewThing]`) to a DTO and to the consumer engine. |
| 27 | - Reviewing a PR that wires an attribute into ONE engine — flag every other relevant engine as missing. |
| 28 | |
| 29 | ## Workflow |
| 30 | |
| 31 | ### 1. File and shape |
| 32 | |
| 33 | ```php |
| 34 | <?php |
| 35 | |
| 36 | declare(strict_types=1); |
| 37 | |
| 38 | namespace BetterData\Attribute; |
| 39 | |
| 40 | use Attribute; |
| 41 | |
| 42 | #[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)] |
| 43 | final readonly class Foo |
| 44 | { |
| 45 | public function __construct( |
| 46 | public string $name = '', |
| 47 | public bool $required = false, |
| 48 | ) {} |
| 49 | } |
| 50 | ``` |
| 51 | |
| 52 | Three structural rules: |
| 53 | |
| 54 | - **`TARGET_PARAMETER | Attribute::TARGET_PROPERTY`** — better-data DTOs use constructor-promoted parameters which appear as both. Restricting to just one breaks DTO authors who happen to use the other style. |
| 55 | - **`final readonly class`** — same immutability promise the DTOs make. Attribute instances are constructed once per Reflection lookup; mutation is meaningless. |
| 56 | - **Constructor-promoted public properties** — the attribute reads its own data via `$reflectionAttribute->newInstance()->propertyName`. No methods, no business logic. |
| 57 | |
| 58 | ### 2. Repeatable attributes |
| 59 | |
| 60 | If a single parameter can carry the attribute multiple times (validation rules, multiple format hints), add `Attribute::IS_REPEATABLE`: |
| 61 | |
| 62 | ```php |
| 63 | #[Attribute( |
| 64 | Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE |
| 65 | )] |
| 66 | final readonly class Tag |
| 67 | { |
| 68 | public function __construct(public string $name) {} |
| 69 | } |
| 70 | ``` |
| 71 | |
| 72 | Then on the read side, use `getAttributes(Tag::class)` (which returns `array<ReflectionAttribute>`) and iterate, instead of `getAttributes(Tag::class)[0] ?? null`. |
| 73 | |
| 74 | ### 3. Decide which engines need to know |
| 75 | |
| 76 | | Engine | Read it when… | File | |
| 77 | |---|---|---| |
| 78 | | `Attr |