Web API access mapping framework for Ray.MediaQuery.
composer require ray/web-queryNote: This package builds on ray/media-query, which provides the core query infrastructure (parameter injection, logging, etc.).
Annotate the methods with #[WebQuery], giving each one a query ID:
<?php
use Ray\MediaQuery\Annotation\WebQuery;
interface UserApiInterface
{
#[WebQuery('user_item')]
public function get(string $id): array;
}web_query.json maps each query ID to an HTTP method and a URI template:
{
"webQuery": [
{"id": "user_item", "method": "GET", "path": "https://{domain}/users/{id}"}
]
}MediaQueryWebModule is installed into MediaQueryBaseModule. The interfaces are
registered with Queries::fromClasses():
<?php
use Ray\Di\Injector;
use Ray\MediaQuery\MediaQueryBaseModule;
use Ray\MediaQuery\MediaQueryWebModule;
use Ray\MediaQuery\Queries;
use Ray\MediaQuery\WebQueryConfig;
$queries = Queries::fromClasses([UserApiInterface::class]);
$webConfig = new WebQueryConfig('web_query.json', ['domain' => 'api.example.com']);
$module = new MediaQueryBaseModule($queries);
$module->install(new MediaQueryWebModule($webConfig));
$injector = new Injector($module);
$api = $injector->getInstance(UserApiInterface::class);
$user = $api->get('123'); // GET https://api.example.com/users/123URI template variables are filled from the method arguments and the bindings
passed to WebQueryConfig (here {domain} comes from the bindings and {id}
from the get() argument).
The return type of the interface method selects how the HTTP response is handled:
| Return type | Result |
|---|---|
array |
JSON body decoded to an array |
string |
Raw response body |
PSR-7 MessageInterface |
The HTTP message object |
Instead of a raw array, a method can return typed, immutable domain objects.
Give #[WebQuery] a factory (and a type) — this is the same factory
mechanism ray/media-query provides for #[DbQuery], applied to HTTP
responses.
<?php
use Ray\MediaQuery\Annotation\WebQuery;
interface ProductApiInterface
{
#[WebQuery('product_item', type: 'row', factory: ProductFactory::class)]
public function get(string $id): Product;
#[WebQuery('product_list', factory: ProductFactory::class)]
/** @return array<Product> */
public function list(string $status): array;
}The factory is resolved through the DI injector, so it can depend on domain services and apply business logic while building the object:
<?php
final class ProductFactory
{
public function __construct(
private TaxCalculator $tax,
) {
}
public function factory(string $name, int $price): Product
{
return new Product($name, $this->tax->applyTax($price));
}
}
final class Product
{
public function __construct(
public readonly string $name,
public readonly int $price,
) {
}
}The decoded JSON is passed to the factory method as named arguments: each
JSON key is matched to a parameter by name, unknown keys are ignored, and a
missing required argument throws InvalidWebFactoryKeyException. (This is the
web counterpart of media-query's positional PDO::FETCH_FUNC binding.)
type selects single object vs. list:
type |
JSON response | Result |
|---|---|---|
'row' |
object {...} |
one object |
'row_list' |
array [{...}, {...}] |
array<Object> |
type defaults to 'row_list'. A 'row' method whose response is a list
takes the first element; a 'row_list' method whose response is a single
object wraps it into a one-element list.
You can also map straight to an entity without a factory: when the return
type (or the @return array<Entity> docblock) is a class, each response is
hydrated through the entity constructor.
#[WebQuery('product_item', type: 'row')]
public function get(string $id): Product; // built via Product::__constructTo wrap or aggregate the fetched objects into another type (totals, metadata,
…), let the return type implement PostFetchInterface. Its static
fromContext() receives the fetch result and returns the final object. It runs
after the factory, carries no dependencies by design, and is the web analogue
of media-query's PostQueryInterface (named PostFetch because a web call is a
single fetch, with no multi-statement query context to span).
<?php
use Ray\MediaQuery\PostFetchContext;
use Ray\MediaQuery\PostFetchInterface;
final class ProductList implements PostFetchInterface
{
/** @param array<Product> $items */
public function __construct(
public readonly array $items,
public readonly int $total,
) {
}
public static function fromContext(PostFetchContext $context): static
{
/** @var array<Product> $items */
$items = is_array($context->result) ? $context->result : [];
return new self($items, count($items));
}
}#[WebQuery('product_list', factory: ProductFactory::class)]
public function listAggregate(string $status): ProductList;PostFetchContext exposes the fetch result, the original method arguments
(query), and the #[WebQuery] annotation (webQuery).
- Web API Queries: Execute HTTP requests via interface methods
- URI Template Support: Dynamic URL parameter binding with
{param}syntax - Multiple Response Types: JSON array, string, or PSR-7 message
- Domain Object Mapping (BDR): Map responses to typed domain objects via an injectable factory, with optional
PostFetchcomposition - Parameter Injection: Automatic parameter conversion and injection
- HTTP Client Integration: Built on the Guzzle HTTP client
- PHP 8.2+
- ray/media-query ^1.0