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

Non Eloquent Laravel Package

laravel-json-api/non-eloquent

Laravel JSON:API adapter for non-Eloquent data sources, letting you expose custom repositories or external APIs through the JSON:API spec with resources, relationships, filtering, sorting, and pagination—no Eloquent models required.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites — prove the base works first. This package is an extension of laravel-json-api/json-api, not a standalone. Before installing it you need:

  • laravel-json-api/json-api installed and configured
  • A registered Server class
  • At least one working Eloquent-backed resource returning a correct JSON:API document

Without that baseline you cannot distinguish a broken non-Eloquent schema from a broken base setup. Get an Eloquent resource green first.

Install and pin.

composer require laravel-json-api/non-eloquent

Then read composer.json in the vendor directory. The package tracks a specific laravel-json-api/* major; your PHP and Laravel constraints must line up too. With 4 stars and 0 dependents, you are the compatibility test suite — pin both this package and the parent.

"laravel-json-api/json-api": "4.x.x",
"laravel-json-api/non-eloquent": "1.x.x"

Where to look first, in order.

  1. vendor/laravel-json-api/non-eloquent/src/Schema.php — the abstract base class. Read it end to end before writing anything. It is small, and it is both schema and adapter, so its abstract method signatures are the contract. Return types and method names in the vendor source override anything you read online.
  2. The Store contract (or equivalent) that defines how records are retrieved, filtered, sorted, and paginated.
  3. The package README for the minimal example and the version matrix.
  4. The upstream laravel-json-api/json-api docs on Schemas, Adapters, Requests, and Filtering / Sorting / Pagination. Everything above the storage layer is unchanged, and that is where most of your day-to-day configuration lives.

Minimal steps.

  1. Create a schema under app/JsonApi/V1/<Resource>/Schema.php extending the package's base schema instead of the Eloquent Schema.
  2. Register it in Server::allResources() alongside your Eloquent schemas.
  3. Implement fields() using the parent's field classes. These read from a plain object/array through value resolvers (callbacks), not Eloquent attribute magic:
public function fields(): array
{
    return [
        ID::make('id', fn ($obj) => (string) $obj->id),
        Str::make('name', fn ($obj) => $obj->displayName()),
        DateTime::make('createdAt', fn ($obj) => $obj->createdAt),
    ];
}
  1. Implement only the storage methods you actually need: all() and find($id) for reads; create(), update(), delete() only if writable. Verify exact names and return types against the abstract class — the vendor source is the source of truth.
  2. Add routes exactly as usual:
JsonApiRoute::server('v1')->resources()->all();
  1. Reuse standard JsonApiRequest classes for query validation and sparse-fieldset handling.

First use case — a read-only reference resource. Countries, currencies, categories, feature flags, or a cached external API payload. Implement all() and find() and nothing else. This exercises the entire adapter path (routing, serialization, includes, fieldsets, pagination) without touching writes or relationship side effects.

Day-to-day mental model. Treat the package as a translation layer between an arbitrary data source and the schema conventions you already know. Your working loop is: pick a data source → wrap it in a repository → write a thin schema with resolvers → register it → hit the endpoint → assert on the JSON:API document.


Implementation Patterns

Wrap a repository, not an array. Put the schema in the role of a thin delegator. Keep persistence and fetch logic in a service the rest of your app already uses, so you can swap the backing source (static array → Redis → API) without touching the API layer:

class CountrySchema extends \LaravelJsonApi\NonEloquent\Schema
{
    public function __construct(private CountryRepository $repo) {}

    public function all(): iterable { return $this->repo->all(); }
    public function find(string $id): ?object { return $this->repo->find($id); }
}

This also keeps the schema small enough that if the package stalls, you can reimplement it as a custom adapter over the same repository — the day-to-day code you wrote does not move.

DTOs as first-class resources. For external API payloads, map the response into a readonly DTO inside the repository, then let schema resolvers read from it. This yields a stable field contract even when the upstream payload shifts; the resolvers absorb the translation. It also gives you a natural place to normalize IDs and types (CarbonImmutable, string, etc.) before they reach serialization.

Reuse upstream filter/sort/pagination configuration. Everything above the storage layer is unchanged. Register filters and sorts on the schema exactly as you would on an Eloquent schema, and use the standard pagination objects. The whole point is that page[number], filter[...], sort, include, and fields[...] behave identically to the rest of the API — do not hand-roll a parallel implementation in a controller.

Relationships to real Eloquent models. This is the highest-value and highest-risk pattern. A non-Eloquent resource can declare a to-one or to-many relation to an Eloquent resource, and includes resolve through the standard machinery. Implement the relation using the parent's relation classes, returning related identifiers or models from your own accessor. Spike this specifically — it is where the adapter's assumptions are most likely to leak.

Writes are optional and separate. Read-only? Do not implement create/update/delete. Writable? Pair them with request classes that call your service; treat the schema method as a thin delegator. Business rules do not belong in schemas.

Feature-flag the rollout. Register the non-Eloquent schema on its own Server (or behind a route group) initially, so you can A/B the response shape against any existing endpoint without disturbing Eloquent resources. Promote it once contract tests are green.

Contract tests, not unit tests. Assert on the serialized JSON:API document — type, id, attributes, relationships, links, meta for pagination — not on schema internals. These tests survive upstream refactors and catch the ID/linkage bugs you will actually hit.

Batch aggressively at the repository layer. No lazy loading exists here, so an include over a remote source fans out into a call per parent unless you preload in the repository. Fetch related records in bulk, cache the result of all(), and treat the upstream as slow by default.

A repeatable per-resource workflow. Once the first schema is green, each additional non-Eloquent resource follows the same checklist:

  1. Add/adjust the repository method (all, find, optionally create/update/delete).
  2. Add the DTO + normalization.
  3. Write the schema: fields() with resolvers, then filters/sorts/pagination config copied from an equivalent Eloquent schema.
  4. Register in allResources().
  5. Add one contract test asserting the document shape and one asserting a filtered/sorted/paginated query.

Keeping to this list means new non-Eloquent endpoints cost minutes, not a design review.


Gotchas and Tips

IDs are entirely your responsibility. Eloquent magically supplies a stable primary key; a DTO does not. A null, array-shaped, or unstable ID breaks linkage, find(), and includes in ways that surface far from the cause. Normalize to strings at the schema boundary and make find($id) defensive against type mismatches.

Authorization is not inherited. Eloquent resources get scoping and policies through the model layer. Non-Eloquent resources bypass all of that unless you wire it explicitly — in the repository, in the request's authorize(), or in a scoping hook. Audit every such endpoint for data leakage before it ships; this is the primary security risk of adopting the pattern.

Value resolvers are callbacks, not attributes. Field classes do not know about $model->name. Every field requires a resolver. Missing or wrong-typed resolvers show up as silently-blank attributes or type errors mid-serialization — check these first when a field disappears.

Sparse fieldsets change what runs. When a client requests fields[countries]=name, only name is resolved. Do not put required side effects or expensive computation inside a resolver that may never be called; do that in the repository fetch instead.

Data source failures are your error semantics. A timeout, a 5xx, or a partial payload must become a deliberate JSON:API error, not a 500 with a stack trace. Decide the contract — empty collection, error object, or partial — and test it. Upstream unavailability is a routine case, not an edge case.

Watch query-parameter support. If you implement filtering/sorting by hand instead of reusing the parent's configuration, you lose the validation and error semantics that the rest of the API already provides. Do not diverge; the value of this package is conformance, not novelty.

Debugging order. When something is wrong:

  1. Confirm the schema is registered in allResources().
  2. Confirm find() returns the object you think it does.
  3. Confirm each field resolver returns the expected type.
  4. Inspect the raw response for id / type linkage correctness.
  5. Only then look at the parent package's serialization.

The adapter surface is tiny; most bugs live in your resolvers or your IDs.

Pin and read the source. With this adoption level, treat the package as a small vendored utility. Budget an afternoon to read it fully and to be able to fork it — it is a few hundred lines of thin adapter. Do not make it a critical-path dependency without a named owner watching upstream.

Verify the version matrix. PHP, Laravel, and laravel-json-api versions must all line up. If constraints do not match, that is a fork decision, not a workaround.

Exit plan. Keep schemas thin and repositories thick. If the package stalls, each schema can be reimplemented as a custom adapter over the same repository, and your day-to-day code is unchanged.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core