Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Response Processor Laravel Package

zjkiza/response-processor

A Laravel package for building and processing standardized API responses, helping you return consistent JSON structures with less boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. DataFetcherInterface — the contract every fetcher implements (fetch(iterable $identifiers): array as of v0.7.0).
  2. Attribute/InjectDataAbstract — the base class for every injectable attribute; this is where the $fetcher wiring and ProcessorAttributeInterface implementation live.
  3. The service provider / package config — this is what changed in v0.7.0 (fetcher registration). Read it before writing your first fetcher, not after.
  4. Any shipped concrete attributes (e.g. an 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 *.

Implementation Patterns

Migrating fetchers to the iterable signature

This 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.

Registration after the v0.7.0 change

The way services implementing DataFetcherInterface are registered has changed. Two things to do on upgrade:

  • Re-read the service provider / config and confirm how fetchers are now collected (interface-based auto-resolution, explicit tag, or a config array).
  • Resolve every fetcher from the container in a test to prove registration works before you trust any response output:
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.

Custom attributes are now one-liners

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.

Batching is the point

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.

Gotchas and Tips

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings