@@ -78,11 +78,129 @@ The return type of the interface method selects how the HTTP response is handled
7878| ` string ` | Raw response body |
7979| PSR-7 ` MessageInterface ` | The HTTP message object |
8080
81+ ### Mapping responses to a domain object
82+
83+ Instead of a raw array, a method can return typed, immutable domain objects.
84+ Give ` #[WebQuery] ` a ` factory ` (and a ` type ` ) — this is the same factory
85+ mechanism ` ray/media-query ` provides for ` #[DbQuery] ` , applied to HTTP
86+ responses.
87+
88+ ``` php
89+ <?php
90+ use Ray\MediaQuery\Annotation\WebQuery;
91+
92+ interface ProductApiInterface
93+ {
94+ #[WebQuery('product_item', type: 'row', factory: ProductFactory::class)]
95+ public function get(string $id): Product;
96+
97+ #[WebQuery('product_list', factory: ProductFactory::class)]
98+ /** @return array<Product > */
99+ public function list(string $status): array;
100+ }
101+ ```
102+
103+ The factory is resolved through the DI injector, so it can depend on domain
104+ services and apply business logic while building the object:
105+
106+ ``` php
107+ <?php
108+ final class ProductFactory
109+ {
110+ public function __construct(
111+ private TaxCalculator $tax,
112+ ) {
113+ }
114+
115+ public function factory(string $name, int $price): Product
116+ {
117+ return new Product($name, $this->tax->applyTax($price));
118+ }
119+ }
120+
121+ final class Product
122+ {
123+ public function __construct(
124+ public readonly string $name,
125+ public readonly int $price,
126+ ) {
127+ }
128+ }
129+ ```
130+
131+ The decoded JSON is passed to the factory method as ** named arguments** : each
132+ JSON key is matched to a parameter by name, unknown keys are ignored, and a
133+ missing required argument throws ` InvalidWebFactoryKeyException ` . (This is the
134+ web counterpart of media-query's positional ` PDO::FETCH_FUNC ` binding.)
135+
136+ ` type ` selects single object vs. list:
137+
138+ | ` type ` | JSON response | Result |
139+ | --------------| --------------------------| -----------------|
140+ | ` 'row' ` | object ` {...} ` | one object |
141+ | ` 'row_list' ` | array ` [{...}, {...}] ` | ` array<Object> ` |
142+
143+ ` type ` defaults to ` 'row_list' ` . A ` 'row' ` method whose response is a list
144+ takes the first element; a ` 'row_list' ` method whose response is a single
145+ object wraps it into a one-element list.
146+
147+ You can also map straight to an entity ** without** a factory: when the return
148+ type (or the ` @return array<Entity> ` docblock) is a class, each response is
149+ hydrated through the entity constructor — or through public properties if the
150+ class has no constructor.
151+
152+ ``` php
153+ #[WebQuery('product_item', type: 'row')]
154+ public function get(string $id): Product; // built via Product::__construct
155+ ```
156+
157+ ### Composing results with PostFetch
158+
159+ To wrap or aggregate the fetched objects into another type (totals, metadata,
160+ …), let the return type implement ` PostFetchInterface ` . Its static
161+ ` fromContext() ` receives the fetch result and returns the final object. It runs
162+ after the factory, carries no dependencies by design, and is the web analogue
163+ of media-query's ` PostQueryInterface ` (named * PostFetch* because a web call is a
164+ single fetch, with no multi-statement query context to span).
165+
166+ ``` php
167+ <?php
168+ use Ray\MediaQuery\PostFetchContext;
169+ use Ray\MediaQuery\PostFetchInterface;
170+
171+ final class ProductList implements PostFetchInterface
172+ {
173+ /** @param array<Product > $items */
174+ public function __construct(
175+ public readonly array $items,
176+ public readonly int $total,
177+ ) {
178+ }
179+
180+ public static function fromContext(PostFetchContext $context): static
181+ {
182+ /** @var array<Product > $items */
183+ $items = is_array($context->result) ? $context->result : [];
184+
185+ return new self($items, count($items));
186+ }
187+ }
188+ ```
189+
190+ ``` php
191+ #[WebQuery('product_list', factory: ProductFactory::class)]
192+ public function listAggregate(string $status): ProductList;
193+ ```
194+
195+ ` PostFetchContext ` exposes the fetch ` result ` , the original method arguments
196+ (` query ` ), and the ` #[WebQuery] ` annotation (` webQuery ` ).
197+
81198## Features
82199
83200- ** Web API Queries** : Execute HTTP requests via interface methods
84201- ** URI Template Support** : Dynamic URL parameter binding with ` {param} ` syntax
85202- ** Multiple Response Types** : JSON array, string, or PSR-7 message
203+ - ** Domain Object Mapping (BDR)** : Map responses to typed domain objects via an injectable factory, with optional ` PostFetch ` composition
86204- ** Parameter Injection** : Automatic parameter conversion and injection
87205- ** HTTP Client Integration** : Built on the Guzzle HTTP client
88206
0 commit comments