undabot/json-api-core
Core JSON:API building blocks for PHP/Laravel applications: resource objects, document serialization, relationships, and standards-compliant responses. Helps you expose clean, consistent JSON:API endpoints without reimplementing the spec.
The package installs like any modern Laravel dependency and is discovered automatically via its service provider — there is no manual providers entry to add in config/app.php on Laravel 9+.
composer require vendor/package:^2.1.8
php artisan vendor:publish --provider="Vendor\Package\PackageServiceProvider"
Where to look first:
config/package.php — the published config is the source of truth for driver, connection, and feature toggles. Read it top-to-bottom before writing any code; most "why doesn't this work" questions are answered there.src/Contracts directory — the interfaces tell you what is swappable and what you can bind in a service provider.README "Upgrade" section — check it even on patch bumps (see Gotchas for 2.1.8).First use case: resolve the package's main entry point from the container rather than instantiating it, so your code respects any config-driven driver selection:
use Vendor\Package\Contracts\Client;
public function __construct(private Client $client) {}
public function sync(Order $order): void
{
$this->client->push($order->toPayload());
}
That single injection point is the seam you will use for fakes in tests and for decorating behaviour later.
Constructor injection over facades. The package ships a facade for convenience, but constructor injection keeps your classes testable and makes the dependency graph explicit. Reserve the facade for Blade templates and one-off artisan commands.
Bind contracts in a dedicated provider. Rather than scattering app()->bind() calls, create an IntegrationsServiceProvider that maps the package contracts to your own decorators. This is the cleanest place to add logging, retries, or multi-tenant credential switching:
$this->app->extend(Client::class, function (Client $client) {
return new LoggingClient($client, $this->app['log']);
});
Push slow work to the queue. Any method that performs a network call should be wrapped in a job. The package's exceptions are generally not ShouldQueue-aware, so set $tries, backoff(), and retryUntil() explicitly on your job rather than relying on defaults.
Use the fake in tests. Most Laravel packages expose a Package::fake() helper plus assertion methods. Prefer that over Http::fake() — it asserts on package-level intent rather than raw HTTP shape, so tests survive internal transport changes:
Package::fake();
$this->post('/orders', $payload)->assertRedirect();
Package::assertPushed(fn ($payload) => $payload['id'] === 1);
Configure through env, validate at boot. Expose credentials as env vars with config/package.php defaults, then validate in a service provider's boot() using throw_if(blank(config('package.key'))). Failing loudly at boot beats failing mid-request in production.
Treat the config as part of your deploy surface. After any config change run php artisan config:cache in the deploy pipeline and confirm the package still resolves — cached config disables env() calls inside package config files, so any env() usage that leaked into runtime code paths will return null.
2.1.8 adds missing return types — audit your subclasses. This is a patch release and should be non-breaking for consumers who only use the public API, but PHP return types are enforced inheritance contracts. If you extend any of the package's classes (custom drivers, decorated clients, test doubles built with extends), a parent method that now declares : void, : array, or : static will fatal in your child if it omits a compatible type. Before bumping:
composer update vendor/package -W
php artisan test
If you get Declaration of App\...::method() must be compatible with Vendor\...::method(): X, add the matching return type to your override. Do not paper over it with #[ReturnTypeWillChange] — that attribute targets PHP's internal interfaces, not userland hierarchies, and it will not suppress this error.
Vendor your subclass surface. If a scan of your codebase shows extends against package classes, consider whether the contract + decorator pattern from the patterns section would let you drop the inheritance entirely. Decorators are immune to this class of breakage.
Lockstep patch pinning. Use ^2.1.8 rather than ^2.0 so you control when type-level changes land, and commit composer.lock. Patch releases that touch signatures deserve a CI run rather than a composer update on the server.
Debugging resolution issues. When the container returns something unexpected, php artisan about plus a quick dd(app(Client::class)::class) in tinker will show you which binding won. Remember $this->app->extend() runs after the original binding resolves — if you need to replace rather than wrap, use bind()/singleton() in a provider registered later.
Extension points that stay stable. Prefer the published config, the contracts, and the package's events over touching internals. Events especially: listen and react rather than overriding the classes that dispatch them, and you decouple yourself from exactly the kind of signature churn 2.1.8 represents.
Watch for deprecations, not just breaks. Even when the changelog says "added return types," scan the diff for @deprecated annotations on methods you call — they are the cheapest possible early warning for the next minor.
How can I help you explore Laravel packages today?