zjkiza/response-processor
A Laravel package for building and processing standardized API responses, helping you return consistent JSON structures with less boilerplate.
This is a response-processing package (ZJKiza\ResponseProcessor) that hydrates properties on resources/DTOs/controllers via PHP attributes, with the actual data resolution delegated to "fetchers" (services implementing DataFetcherInterface). You describe what you want injected; the fetcher decides how to get it.
Install and locate the moving parts
composer require zjkiza/response-processor # confirm exact vendor/name from Packagist
php artisan vendor:publish --provider="ZJKiza\ResponseProcessor\ResponseProcessorServiceProvider"
Start reading in this order:
DataFetcherInterface — the contract every fetcher implements (fetch(iterable $identifiers): array as of v0.7.0).Attribute/InjectDataAbstract — the base class for every injectable attribute; this is where the $fetcher wiring and ProcessorAttributeInterface implementation live.InjectData-style attribute) — copy their shape rather than inventing your own.First use case
The fastest win is injecting related data into a resource without N+1 queries:
class OrderResource extends JsonResource
{
#[InjectData(OrderItemsFetcher::class)] // use the package's shipped attribute
public array $items = [];
}
Then verify the resolution path with a single request in php artisan tinker or a feature test — do not assume the fetcher was picked up. On v0.7.0 the registration rules changed, so a silently-unregistered fetcher is the #1 cause of "why is this property empty."
Pin the version. v0.7.0 is pre-1.0 and contains a hard break (see Gotchas). Use a caret constraint you've tested ("^0.7"), not *.
iterable signatureThis is the change that will bite you. Every existing fetcher must be updated:
// BEFORE (v0.6.x)
public function fetch(array $identifiers): array
{
return Order::with('items')->findMany($identifiers)->all();
}
// AFTER (v0.7.0)
public function fetch(iterable $identifiers): array
{
return Order::with('items')
->whereIn('id', is_array($identifiers) ? $identifiers : iterator_to_array($identifiers, false))
->get()
->all();
}
Design fetchers around the assumption that $identifiers is single-pass. The iterable widening exists so the processor can hand you a lazy generator instead of materialising an array — which is the whole point. If you find yourself calling iterator_to_array() in every fetcher, the batching benefit is gone.
A robust fetcher shape:
public function fetch(iterable $identifiers): array
{
$ids = [];
foreach ($identifiers as $id) {
$ids[$id] = true; // dedupe + single pass
}
if ($ids === []) {
return [];
}
return $this->repository->findMany(array_keys($ids));
}
The return contract is still array — keyed by identifier, ideally, so the processor can map results back onto properties.
The way services implementing DataFetcherInterface are registered has changed. Two things to do on upgrade:
public function test_fetchers_are_registered(): void
{
foreach ([OrderItemsFetcher::class, UserFetcher::class] as $class) {
$this->assertInstanceOf(DataFetcherInterface::class, app($class));
}
}
If the processor resolves fetchers by container lookup, an unregistered fetcher degrades to empty/null data rather than throwing — so this test is cheap insurance.
The headline ergonomic win. Any presentation concern can get its own attribute with almost no boilerplate:
use ZJKiza\ResponseProcessor\Attribute\InjectDataAbstract;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class InjectPresenter extends InjectDataAbstract {}
Then use it like any other attribute:
class ArticleResource extends JsonResource
{
#[InjectPresenter(ArticlePresenterFetcher::class)]
public array $presenter = [];
}
Reach for a custom attribute whenever the attribute name itself should carry meaning in your codebase (#[InjectPermissions], #[InjectTenant], #[InjectPresenter]) — it reads better than a generic #[InjectData(Fetcher::class)] and gives you a place to add behaviour later. Add a constructor only when you need parameters:
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class InjectPresenter extends InjectDataAbstract
{
public function __construct(string $fetcher, public readonly bool $withMeta = false)
{
parent::__construct($fetcher);
}
}
Keep attribute classes stateless and cheap — they're constructed by reflection each time properties are inspected.
The iterable + array $identifiers contract is a batching hint: one fetch() call should service many properties. Group by fetcher, collect identifiers, and let the fetcher do one whereIn / one API call. If your fetcher internally loops and queries per identifier, you've defeated the design.
The iterable change is a fatal, not a deprecation. PHP requires contravariant parameters: a class implementing fetch(iterable $identifiers) cannot narrow to fetch(array $identifiers). It's not a warning — the class fatals at load time. Grep your whole app for function fetch(array $identifiers) the moment you bump to 0.7.
Fetcher registration changes fail silently. Because the fetch path typically returns empty data rather than throwing, an upgrade that drops your fetcher registration looks like "the presenter just stopped working." Bisect by dumping the resolved fetcher from the container in a test, then compare the service provider against the 0.6.x version.
#[\Attribute] is mandatory. If you extend InjectDataAbstract and forget the #[\Attribute] declaration, PHP won't treat your class as an attribute at all — you'll get a "Attribute class not found" style fatal on any file using it. The target flag matters too: \Attribute::TARGET_PROPERTY is the one you want for property injection; TARGET_ALL is overkill and will allow misuse.
Don't re-implement the interface. InjectDataAbstract already provides the $fetcher property and the ProcessorAttributeInterface implementation. Overriding those is the most common way to break the processor's reflection logic. Extend, add a constructor if needed, stop there.
Clear config cache after changing registration.
php artisan config:clear && php artisan cache:clear && composer dump-autoload -o
New #[Attribute] classes in particular need the autoloader refreshed — stale classmaps produce confusing "class does not exist" fatals that look like a package bug.
Generators are single-pass. If the processor hands your fetcher a generator (the likely reason for iterable), iterating it twice yields nothing the second time. Never do count($identifiers), never foreach it after building a collection from it.
Debugging empty properties. Work down this list in order: (1) is the attribute class loadable and properly flagged? (2) is the fetcher bound/registered in the container? (3) does the fetcher return keys that match the identifiers the processor sent? (4) is the property actually reachable and non-static/typed correctly for the processor's reflection pass? Nine times out of ten it's (1) or (2) after this release.
Extension points worth knowing: custom attribute constructors (parameters flow through to your fetcher selection), overriding fetch batching strategy per fetcher, and replacing a shipped attribute with your own subclass when you want a namespace-appropriate name. The abstract class is a deliberate seam — use it rather than forking the package.
Serialisation and queues. If a resource with injected properties is serialised (queued job, cache payload, API response cached by middleware), the injected data must survive json_encode/serialize. Array properties are safe; objects are not, and closures definitely aren't. Keep injected properties as scalars/arrays unless you've verified the round-trip.
How can I help you explore Laravel packages today?