Build a read-only query endpoint
The reference app's GET /api/client/orders/{order} authorizes the Client at the Controller then calls one read-only UseCase. That UseCase calls FindOrder; it owns no transaction.
The route combines a Controller, Query, Model, Policy, and Resource.
Before you begin
Create an order through the HTTP write workflow. Read the Controller entrypoint rule and Query ownership rules.
Route the read through a UseCase
The Controller checks the Policy before it enters ViewOrder. The Resource converts the returned Model to JSON after the UseCase returns.
examples/reference-app/app/Pulsar/Services/Client/Modules/Orders/Controllers/OrderController.php — Authorized delivery boundary for an order read.
<?php
namespace App\Pulsar\Services\Client\Modules\Orders\Controllers;
use App\Pulsar\Domain\Orders\DTOs\CreateOrderData;
use App\Pulsar\Domain\Orders\Models\Order;
use App\Pulsar\Services\Client\Modules\Orders\Requests\StoreOrderRequest;
use App\Pulsar\Services\Client\Modules\Orders\Resources\OrderResource;
use App\Pulsar\Services\Client\Modules\Orders\UseCases\PlaceOrder;
use App\Pulsar\Services\Client\Modules\Orders\UseCases\ViewOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Gate;
final class OrderController
{
// #region order-store-controller
public function store(StoreOrderRequest $request, PlaceOrder $useCase): JsonResponse
{
$order = $useCase->execute(new CreateOrderData(
tenantId: $request->user()->tenant_id,
reference: $request->string('reference')->toString(),
amountCents: $request->integer('amount_cents'),
));
return (new OrderResource($order))->response()->setStatusCode(201);
}
// #endregion
// #region order-show-controller
public function show(Order $order, ViewOrder $useCase): OrderResource
{
Gate::authorize('view', $order);
return new OrderResource($useCase->execute($order->id));
}
// #endregion
}examples/reference-app/app/Pulsar/Services/Client/Modules/Orders/UseCases/ViewOrder.php — Read-only UseCase delegates to one Query.
<?php
namespace App\Pulsar\Services\Client\Modules\Orders\UseCases;
use App\Pulsar\Domain\Orders\Models\Order;
use App\Pulsar\Domain\Orders\Queries\FindOrder;
final class ViewOrder
{
public function __construct(private readonly FindOrder $findOrder) {}
// #region view-order-workflow
public function execute(int $orderId): Order
{
return $this->findOrder->execute($orderId);
}
// #endregion
}Interpretation. Authorization occurs at the inbound boundary, while Query execution remains inside the read-only workflow and Resource shaping stays outside it.
Verify access and response shape
Run composer run reference:test:http. The group asserts the authorized response fields and a forbidden response when the authenticated Client belongs to a different tenant.
Troubleshoot
Do not inject a Query into the Controller. A Controller calls the UseCase; the UseCase calls the Query and returns a delivery-neutral Model or value.
Related tasks
Read Query, Policy, and the HTTP write workflow.