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

Api Model Laravel Package

outrightvision/api-model

Outrightvision's api-model adds an API-ready base model for Laravel, simplifying how Eloquent resources are exposed, queried, and transformed for JSON endpoints—handy for teams building consistent, filterable REST APIs without repeating boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're actually installing. Despite “Generate Laravel-like Api Models” in the description, this package generates nothing: no artisan command, no controllers, no routes, no migrations, no config file, no service provider. It is a single abstract base class that takes a decoded JSON array and gives you Eloquent-flavoured, read-only property and relationship access. If the ticket reads “scaffold an API layer,” close the ticket — what you're buying is a nicer way to walk nested partner payloads.

Three steps to first value:

composer require outrightvision/api-model

There is nothing to publish or register. Next, create your own namespace — App\Api\Models is the convention — with one class per upstream resource:

namespace App\Api\Models;

use OutrightVision\ApiModel\ApiModel;

/**
 * @property-read string $id
 * @property-read string $name
 * @property-read Company|null $company
 */
class User extends ApiModel
{
    protected $cast_model = ['company' => Company::class];

    public function company()
    {
        return $this->belongsTo(Company::class, 'company');
    }
}

Then hydrate at the boundary:

$user = new App\Api\Models\User(Http::get($url)->throw()->json());

$user->company->geo->lat; // no $data['company']['geo']['lat'] ?? null

Read the base class source before your second model. get_data(), $cast_model, hasOne/belongsTo/hasMany, included_default, cast_dates, date_timezone, and $requiredParameters are the entire public surface — it fits in one sitting, and that reading replaces documentation that doesn't exist.

First use case: one low-traffic, deeply nested, read-only endpoint. Pick an integration where the payload is genuinely nested (data.company.geo.lat, posts.data[]) so the ergonomics are obvious. Prove $user->company->geo->lat works, then add a hasMany and a cast_dates field. Do not start with your most important customer-facing call — this is read-path convenience and you want to feel the shape before committing.

Add a fixture on day one. Check a real (redacted) response into tests/fixtures/ and assert every field your app reads. Unknown fields return null silently, so a fixture-based contract test is your only defence against upstream schema drift.

Implementation Patterns

Hydrate at the boundary; keep the package out of your domain. Treat App\Api\Models\* as a read-only adapter, never as a domain entity. Put it behind an interface — ApiGateway, a per-integration repository, or a plain service that returns native arrays or your own DTOs:

interface CrmGateway { public function user(string $id): UserData; }

class OutrightCrmGateway implements CrmGateway
{
    public function user(string $id): UserData
    {
        $payload = Http::retry(3, 100)
            ->withToken(config('services.crm.token'))
            ->get("/users/{$id}")
            ->throw()
            ->json();

        $model = new App\Api\Models\User($payload);

        return new UserData(
            name: $model->name,
            companyLat: $model->company?->geo?->lat,
        );
    }
}

This costs one extra class and buys you a one-file swap to spatie/laravel-data or Saloon if the dependency sours. Given four stars and zero dependents, that reversibility is the whole point.

Declare the mapping, don't generate it. Each model is 10–30 lines: $cast_model for nested objects, relationship methods for collections, cast_dates + date_timezone for temporal fields, $requiredParameters for “this must exist”. A reviewer sees the entire API contract on one screen — that reviewability is the strongest day-to-day argument for the package in a Laravel shop.

Prefer declared access over dotted paths. $cast_model and relationship methods are grep-able and refactor-safe; get_data('a->b.c') is not. Reserve get_data() for the odd path declared properties genuinely can't cover.

Lean on lazy relations, steer them with included_default. Relations only materialise on access, so you don't pay for a graph you never walk — a real win against fat vendor payloads. When you will traverse a relation on every item in a collection, set included_default so each model doesn't trigger its own walk:

$users = collect($payload['data'])
    ->map(fn ($p) => new App\Api\Models\User($p));

$users->each(fn ($u) => $u->company->name); // cheaper with included_default

Keep collections boring. A collect()->map() at the boundary is enough. Don't build helper plumbing around the package — that plumbing is precisely the layer you'd rewrite on a swap.

One flat namespace, one convention. App\Api\Models, one file per upstream resource, mirroring the API's naming rather than your domain's. Any Eloquent-literate engineer reads the codebase on day one, which is the practical onboarding argument.

Contract tests per model, not per integration. One fixture + one test per class, asserting every field the app touches plus every relation. When the vendor renames geo to location, the failing test names the exact models and fields instead of production silently returning null.

Keep transport, caching, and retries outside. The model knows nothing about the network and shouldn't learn. Retry, cache, then hydrate.

Gotchas and Tips

  • Silent nulls are the defining footgun. $user->company->geo->renamed_key returning null is by design, and it looks identical to a legitimately absent optional field. Compensate with $requiredParameters on required models plus fixture-based contract tests. There is no schema validation built in.
  • $requiredParameters is thin. Treat it as “this model must exist”, not field-level validation. Real validation, error semantics, and retry policy are yours to write.
  • No typed properties means no static analysis out of the box. Add @property-read docblocks to every model class from the very first one, or PHPStan and IDE completion vanish. Retrofitting them across a dozen models is tedious busywork.
  • Audit cast_dates against a real response. It routes through Carbon::parse(); empty strings, ambiguous formats, and timezone-less timestamps all behave unpredictably. Verify date_timezone defaults against an actual payload before trusting them.
  • Lazy loading has no published benchmark. The cost is real on large collections. Measure on an actual page of payloads before wiring it into a hot path, and confirm included_default actually helps for your shape rather than assuming it does.
  • Debug by dumping the decoded array first. Because relations materialise only on access, a “missing” relation is almost always a wrong key or a failed cast in the source array — not a hydration bug. dump($payload) before constructing the model.
  • Extension points are just inheritance. Override relationship resolution or add custom casts on subclasses. The base class is small enough to read in one sitting — which is also the argument for vendoring it.
  • Read-only, always. No round-trip serialisation, no request building, no write path. For DTOs that validate and transform outbound, spatie/laravel-data or native JsonResource remain the mature answers.
  • Compatibility is on you. No support matrix, thin changelog, four stars, zero dependents, and a companion “API Call library” that never shipped. MIT makes vendoring trivial: fork into packages/ or a Composer repositories entry the moment this touches anything that matters.
  • Timebox the evaluation. Spend two days building two or three real payloads behind your gateway and compare head-to-head with an equivalent laravel-data DTO. If the Eloquent ergonomics don't win clearly, you've lost two days instead of a quarter — and you've still got the gateway interface to show for it.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
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