$npx -y skills add Lonsdale201/wp-agent-skills --skill bd-better-route-bridgeCompose better-data DTOs with the better-route library — use BetterRouteBridge::{get, post, put, patch, delete} to register a REST route that hydrates the request into a DTO, validates, calls the handler with (DataObject, mixed $request), and presents returned DataObject values t
| 1 | # better-data: Composing with better-route |
| 2 | |
| 3 | For developers using better-data and better-route together — DTO-backed REST endpoints, OpenAPI generation from DTO schemas, request hydration into typed `DataObject` instances inside route handlers. The integration seam is the optional `BetterRouteBridge` ([src/Route/BetterRouteBridge.php](BetterRouteBridge.php)); using it correctly keeps the data layer free of router concerns and the router layer free of data-shape concerns. |
| 4 | |
| 5 | ## Misconception this skill corrects |
| 6 | |
| 7 | > "I'll just use `register_rest_route` directly inside my better-data consumer code, parse `WP_REST_Request` myself, and call `MyDto::fromArray($request->get_params())`." |
| 8 | |
| 9 | That works for one route. For an API of 10+ routes, it duplicates the request-parsing, validation, route-owned-field, and Presenter-projection wiring at every callsite. The bridge centralizes that pipeline: |
| 10 | |
| 11 | 1. Register the route on `better-route`'s `Router` via the appropriate HTTP-verb method. |
| 12 | 2. On request, hydrate a `WP_REST_Request`-shaped object into the DTO (URL params, JSON body, query string — buckets resolved per `source` option). |
| 13 | 3. Reject collisions: a route-owned field like `id` (in the URL `/posts/{id}`) MUST NOT also appear in the JSON body — `RequestParamCollisionException` ([line 194, 680](BetterRouteBridge.php)). |
| 14 | 4. Validate the DTO via the `BuiltInValidator`. |
| 15 | 5. Call the handler with `(DataObject $dto, mixed $request)`. |
| 16 | 6. If the handler returns a `DataObject`, present through `Presenter::for($dto)->context(PresentationContext::rest())`. |
| 17 | |
| 18 | Other AI-prone misconceptions: |
| 19 | |
| 20 | - "I'll add `better-route/better-route` as a hard runtime dep of better-data so the bridge always works." Wrong — the bridge is deliberately duck-typed by method name ([class docblock at lines 21-26](BetterRouteBridge.php)) so better-data installs without better-route. Don't break that. |
| 21 | - "Permission and middleware concerns are data-layer, so I'll put `permissionCallback` inside the DTO." Wrong — those are route-owned and pass through the bridge's `$options` to better-route's `RouteBuilder`. The data layer doesn't care who's allowed; the route layer does. |
| 22 | - "I'll reimplement better-route's Resource DSL inside better-data so consumers only need one library." Wrong — the bridge composes; it doesn't replace either side. Keep the boundary. |
| 23 | |
| 24 | ## When to use this skill |
| 25 | |
| 26 | Trigger when ANY of the following is true: |
| 27 | |
| 28 | - The diff or PR registers a REST route AND uses a `DataObject` for request parsing or response shaping. |
| 29 | - Calls to `BetterRouteBridge::{get, post, put, patch, delete}`. |
| 30 | - The diff modifies `src/Route/BetterRouteBridge.php`. |
| 31 | - OpenAPI exporter setup that includes DTO schemas. |
| 32 | - The consumer asks "how do I get a DTO from `WP_REST_Request`?" or "how do I avoid duplicating request parsing across 20 routes?". |
| 33 | |
| 34 | ## Workflow |
| 35 | |
| 36 | ### 1. Read better-route's flow first when behavior is unclear |
| 37 | |
| 38 | Don't edit `better-route` from a better-data PR. The relevant files in the sibling repo: |
| 39 | |
| 40 | - `../better-route/README.md` — overview. |
| 41 | - `../better-route/src/Router/Router.php` — main entry. |
| 42 | - `../better-route/src/Router/RouteBuilder.php` — fluent builder for one route. |
| 43 | - `../better-route/src/OpenApi/OpenApiExporter.php` — schema export. |
| 44 | |
| 45 | Use the bridge as the integration seam; if you find yourself wanting to change better-route to make the bridge work, that's a sign the bridge needs to absorb the concern instead. |
| 46 | |
| 47 | ### 2. Register a read route |
| 48 | |
| 49 | ```php |
| 50 | use BetterData\Route\BetterRouteBridge; |
| 51 | use MyPlugin\Dto\PostDto; |
| 52 | |
| 53 | $router = my_plugin_get_router(); // returns the better-route Router instance |
| 54 | |
| 55 | BetterRouteBridge::get( |
| 56 | $router, |
| 57 | '/posts/{id}', |
| 58 | PostDto::class, |
| 59 | func |