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

Castable Request Laravel Package

lionix/castable-request

Laravel package for casting incoming request data into typed objects and structured values, helping keep controllers clean and reducing manual array handling when working with validated input.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the platform before anything else. lionix/castable-request 2.0.0 requires PHP 8.2+ and Laravel 12 or 13 — Laravel 11 is explicitly unsupported (EOL, open security advisories). If your app is still on 11, the framework upgrade is the gate; this package cannot enter the dependency tree until that's done. Because 2.0.0 depends on illuminate/database, illuminate/http and illuminate/support rather than laravel/framework, a stale framework pin surfaces as a Composer conflict here rather than as a confusing runtime failure — composer why-not lionix/castable-request ^2.0 tells you quickly.

composer require lionix/castable-request:^2.0

Read, in order: UPGRADE.md (short, enumerates every break), then the three contracts — CasterInterface, CastsRegistryInterface, RequestInputCasterInterface — which in 2.0.0 carry native parameter and return types and therefore serve as the API reference. Then grep your providers and container bindings: Handlers\RequestResolvingHandler is gone (RequestAfterResolvingHandler now applies both global and request casts) and ImMemoryCastsRegistry was renamed to InMemoryCastsRegistry. A stale spelling is a fatal error, not a deprecation.

The first use case worth wiring is a form request that hands the controller typed values instead of strings:

class StoreOrderRequest extends FormRequest
{
    protected function casts(): array
    {
        return [
            'status'     => OrderStatus::class, // enum
            'placed_at'  => 'datetime',
            'meta'       => 'array',
            'items.*.id' => 'int',
        ];
    }

    public function rules(): array
    {
        return [
            'status'     => ['required', Rule::enum(OrderStatus::class)],
            'placed_at'  => ['required', 'date'],
            'meta'       => ['array'],
            'items'      => ['array'],
            'items.*.id' => ['integer'],
        ];
    }
}

The controller reads $request->validated()['status'] and gets an OrderStatus instance. The mental model to lock in immediately: rules validate the raw input, casts run after validation. Everything else in the package follows from that ordering.

Implementation Patterns

Global defaults in the registry, per-request overrides on top. Register app-wide casts once in a service provider for the shapes that recur across every request (timestamps, meta blobs), then let individual form requests override:

public function boot(CastsRegistryInterface $casts): void
{
    $casts->register('meta', 'array');
    $casts->register('placed_at', 'datetime');
}

Request-level casts are not written into the registry in 2.0.0. They're merged at cast time, and the request's own cast wins. Treat the registry as a genuine default layer, not a side-channel — that's what makes it safe under Octane workers, queued jobs and long-lived test processes.

DTOs from nested payloads. The everyday win is collapsing a nested array into a typed object without hand-written hydration:

class CreateShipmentRequest extends FormRequest
{
    protected function casts(): array
    {
        return [
            'recipient'         => RecipientData::class,
            'items'             => ShipmentItems::class, // collection-style
            'items.*.weight_kg' => 'float',
            'items.*.sku'       => 'string',
        ];
    }
}

public function store(CreateShipmentRequest $request, ShipmentService $service)
{
    $data = $request->safe()->all(); // already typed
    return $service->create($data['recipient'], $data['items']);
}

Controllers stay thin — no RecipientData::fromArray($request->input('recipient')) boilerplate, no array_map over line items.

Value objects via CasterInterface. For domain types that aren't plain DTOs (money, phone numbers, external IDs), implement the 2.0.0 signature and register by name:

final class MoneyCaster implements CasterInterface
{
    public function cast(mixed $value, string $cast): Money
    {
        return Money::fromMinorUnits((int) $value, $cast); // $cast carries the currency
    }
}

Note the native mixed in / mixed out types — a pre-2.0.0 caster without them fatals at resolution rather than misbehaving quietly, so any copied-in caster from another project needs the signature updated.

List payloads with top-level wildcards. *.id works for a top-level array of records, which covers bulk endpoints without wrapping them in an artificial items key. The 2.0.0 fix means class-based casters now apply per item on wildcards instead of returning the first casted value for every match — delete any index-keyed workaround you built for the old behaviour.

Batch casting outside the form-request lifecycle. RequestInputCasterInterface::castAttributes() casts several attributes in one pass and replaces request input once per request rather than once per attribute. Use it in middleware, internal sub-requests, or jobs that rehydrate a payload — not a loop of castAttribute(), which is slower and leaves the input array in a half-cast state mid-loop.

JSON-like casts tolerate decoded arrays. array, json, object and collection now accept already-decoded input. This is what makes casts sane in feature tests that build requests by hand and in internal sub-requests where the body was parsed upstream rather than deserialised from a form post.

Testing. Assert on the cast, not on the raw input: $this->postJson(...)->assertJsonPath('data.status', 'shipped') still works because the enum serialises back, but your unit-level assertion should be assertInstanceOf(OrderStatus::class, $request->validated()['status']). Call CastsRegistryInterface::forget() in tearDown() for any test that registers a global cast — preferable to re-registering a dummy value, and it keeps the singleton clean for the next test.

Gotchas and Tips

The validation/cast reorder bites in production, not in the test suite. Rules that only passed because the value had already been cast — an enum instance satisfying a rule, a Carbon instance satisfying date — now receive raw strings. Anything that previously relied on $request->input() being typed (custom rules, prepareForValidation(), middleware) needs re-checking. The upside is real: unknown enum cases and unparsable dates become 422 field errors instead of 500s thrown from inside the caster. Delete the try/catch blocks and exception-handler mappings that translated InvalidArgumentException into a 422 — they're dead code now.

validated() and safe() return cast values. Convenient, and a silent behaviour change for anything downstream doing strict comparisons: 'pending' === $data['status'] quietly flips truthiness when $data['status'] is an enum. Grep for strict comparisons against validated payloads during the upgrade.

$request->input() still routes through Symfony's InputBag. Cast values are written into the request, so $request->all() may now contain enums and objects where it contained strings, and InputBag rejects non-stringable objects. Read from validated()/safe(), and don't try to write cast values back via $request->merge().

The request always wins over the registry. When a cast "didn't happen," the usual cause is an attribute-level cast in the form request shadowing the global one you expected to apply. Check the request's casts() map before the provider.

Don't call forget() at runtime. It mutates a shared singleton. Global casts registered in a provider are visible to every request, including queued jobs and Octane workers, so a runtime forget() outlives the request that made it. If one request needs a different cast, override it on that request.

The InMemoryCastsRegistry rename is trivial and fatal. When the old spelling survives only in a container bind() call or a test bootstrap rather than a use statement, IDE renaming and static analysis both miss it. Search the whole codebase — config/, tests/, providers.

Date casts no longer need a database connection. The format is inferred without one, so casting works in queue workers and console commands that never open a DB — but a format that previously came from the connection grammar is now guessed. Verify non-standard inputs ('next tuesday', '2026-01-01 10:00:00+02:00') still produce the instances you expect, and prefer an explicit date_format:... rule when the client's wire format is unusual.

EloquentModelCaster no longer extends Illuminate\Database\Eloquent\Model. If you were type-hinting Model, calling model methods on it, or persisting through it, that code is broken. It's a caster: it produces a model instance, it is not the model's base class.

Debugging order for a cast that "didn't run": (1) attribute name matches exactly, wildcards included; (2) the request's own cast isn't shadowing the global one — the request always wins; (3) validation actually passed, because nothing is cast when it fails; (4) for custom casters, the class implements the typed 2.0.0 signature.

Vendor-risk hygiene. 0 dependents and a small star count means few real-world upgrade reports to lean on. Pin the version in composer.json, and mirror the package's own CI matrix — HTTP-level tests, Pint and Larastan across each supported PHP/Laravel pair — in your pipeline. That's the cheapest way to catch the ordering and typing regressions above before a release does.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource