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

Technical Evaluation

Architecture fit. The package slots into the seam most Laravel services already have but implement inconsistently: the FormRequest → controller boundary. Instead of a base request class, a trait, or manual hydration in the controller, request attributes are declared in a casts() map and materialised as enums, dates, DTOs, value objects, or collections — including nested and wildcard paths (items.*.id). Architecturally this reinforces the thin-controller / typed-boundary pattern we already want, and it removes an undifferentiated piece of plumbing that tends to drift between teams. The 2.0.0 dependency surface is narrower than the description implies: illuminate/database, illuminate/http, illuminate/support instead of the full laravel/framework. That is a smaller SBOM and audit footprint than an in-house trait that reaches into framework internals, and it makes the build-vs-buy case defensible.

Where it does not fit. Any service on Laravel ≤11 is out — 2.0.0 requires PHP ^8.2 and Laravel ^12|^13, and the maintainers explicitly refuse Laravel 11 on security grounds. Applications that depend on the old ordering (cast first, validate the cast value) will behave differently at runtime. Applications that read $request->input() or $request->all() downstream expecting scalars will now see objects and enums in that array, and Symfony's InputBag rejects non-stringable objects. Any installation with a customised Handlers\RequestResolvingHandler has code that no longer has a home.

Integration feasibility. The public surface is small and documented as contracts: CasterInterface, CastsRegistryInterface, RequestInputCasterInterface, plus a single RequestAfterResolvingHandler that now applies both global and request casts. On a greenfield service this is an afternoon. On a mature service the install is trivial and the audit is the work: every rules() method, every prepareForValidation(), every custom rule, every strict comparison against validated payloads, and every exception-handler mapping that converts caster throws into 422s. Budget one sprint for a mid-sized service with broad request usage, weighted toward audit and regression testing rather than code change.

Technical risk. Ordered by blast radius:

  1. Cast-after-validation reordering. Rules now see raw wire input. Rules that only passed because a value had been cast (an enum instance satisfying a rule, a Carbon instance satisfying date) will start failing — or worse, passing for the wrong reason. Upside: bad input becomes a 422 field error instead of a 500. Downside: it changes the public error contract.
  2. validated() and safe() now return cast values. Convenient, but strict comparisons ('pending' === $data['status']) silently change truthiness. This fails quietly in tests that assert on response shape only.
  3. Fatal-on-upgrade spelling changes. ImMemoryCastsRegistry → InMemoryCastsRegistry, and the removed RequestResolvingHandler. These are fatal errors, not warnings, and a stale name hiding in a container binding or test bootstrap is easy to miss with a use-statement-only search.
  4. EloquentModelCaster no longer extends Model. Any type-hint, model method call, or persistence through the caster is broken.
  5. Ecosystem immaturity. 23 stars, zero dependents, single-maintainer release history. There is no third-party validation, no Stack Overflow corpus, and very few real-world upgrade reports to lean on. We would be an early adopter of a 2.0 line that already carries six breaking changes.

Mitigation for (5) is cheap and worth doing regardless: put the package behind a one-class internal adapter (or at minimum confine all casts() declarations and registry calls to a documented convention), so a swap back to hand-rolled hydration is a bounded change rather than a rewrite.

Key questions to close before committing:

  • Which of our services are already on Laravel 12/13, and what is the cost of gating this behind those upgrades rather than parallel to them?
  • Does composer why-not lionix/castable-request ^2.0 resolve cleanly in each target repo, or do we have pins that conflict with the split illuminate/* requirement?
  • How many rules() methods in the affected services currently rely on cast-before-validate semantics? (Grep for Rule::enum alongside date, and for rules receiving objects.)
  • Who owns the public API error contract, and does a 500 → 422 change for malformed enum/date input need client or SDK coordination?
  • Do we run Octane or long-lived queue workers anywhere? If so, the registry-leak fix is a reason to upgrade, but any runtime forget() calls become a hazard.
  • Are we willing to vendor or fork if the maintainer goes quiet during a framework major?

Integration Approach

Stack fit. Two hard gates and one soft gate. Hard: PHP ^8.2 and Laravel ^12|^13. Soft: whether the service is greenfield or brownfield — greenfield should adopt the casts() convention from day one with no migration cost; brownfield should treat this as a refactor with a regression-test budget. Run composer why-not per repo first; because 2.0.0 requires the split illuminate/* packages rather than laravel/framework, version conflicts surface at dependency resolution instead of as a runtime surprise. That is a genuine improvement over the 1.x arrangement and makes the go/no-go decision fast.

Migration path. Follow the package's own ordering, which is also the cheapest ordering:

  1. Read UPGRADE.md end to end — it is short and enumerates every breaking change.
  2. Read the three typed contracts; with 2.0.0's native parameter and return types they double as the API reference.
  3. Codebase-wide search for ImMemoryCastsRegistry, RequestResolvingHandler, and any custom handler customisation. Fix bindings and test bootstrap, not just use statements.
  4. Audit every rules() method against the raw wire format. Replace date with date_format:... where a cast is what normalises a non-standard string.
  5. Delete now-dead code: try/catch blocks around casting, exception-handler mappings that turned InvalidArgumentException into a 422, wildcard index-keyed workarounds, and manual forget() calls added to work around cross-request leakage.
  6. Re-verify prepareForValidation(), custom rules, and middleware that assumed input() was already typed.

Compatibility. After the fix for registry leakage, the package is safe across queued jobs, Octane workers, and long-lived test processes — that was the 1.x failure mode and it is the strongest argument for taking 2.0 rather than pinning an older line. JSON-like casts (array, json, object, collection) now tolerate already-decoded input, which matters for feature tests that construct requests by hand, internal sub-requests, and JSON bodies parsed upstream. Mirror the package's own CI matrix — HTTP-level tests, Pint, Larastan across every supported PHP/Laravel pair — in our pipeline; given how small the dependent base is, that matrix is our cheapest early-warning system for the ordering and typing regressions above.

Sequencing. Five phases, with an explicit exit gate on each:

  • Phase 0 — Platform gate. Finish the Laravel 12/13 upgrade in target services. Nothing else starts until composer why-not is clean. This is the roadmap-critical dependency; the package cannot be scheduled in parallel with a framework upgrade.
  • Phase 1 — Audit and inventory (read-only). Grep-driven pass over rules(), prepareForValidation(), downstream strict comparisons, handler customisations, and registry bindings. Output: a per-service risk list and an effort estimate. This phase can kill the initiative cheaply.
  • Phase 2 — Pilot. One low-risk request group, preferably a new endpoint rather than a modified one. Establish the convention: global defaults in a service provider via CastsRegistryInterface::register(), per-request overrides in casts() (request always wins), and castAttributes() for batch casting outside the form-request lifecycle.
  • Phase 3 — Convention and guardrails. Publish an internal ADR and a short cookbook. Add a lint or review checklist item: no forget() at runtime, no merge() of cast values, no strict comparisons against validated(). Wire the CI matrix.
  • Phase 4 — Rollout and contract update. Roll per service; simultaneously update API docs, client/SDK error handling, and support runbooks for the 500 → 422 change. Delete dead code as you go rather than in a later cleanup.
  • Phase 5 — Exit option. Decide up front whether to vendor the package. If we adopt it, the fork decision should be made deliberately now, not reactively at the next Laravel major.

Operational Impact

Maintenance. Small dependency tree, MIT licence, no committed composer.lock upstream, and a release cadence tied to Laravel majors (2.0.0 explicitly targets Laravel 13). Pin ^2.0 and treat the package as a versioned internal dependency. The real maintenance risk is bus factor: one maintainer, 23 stars, zero dependents means no crowd-sourced bug discovery. Allocate roughly 0.1 FTE ongoing for monitoring releases, verifying against our Laravel upgrade cadence, and being prepared to vendor. That is still cheaper than owning an in-house casting layer, but it is not zero and should be stated as such rather than assumed away.

Support. Net positive for support load: malformed enums and unparsable dates become 422 field errors instead of 500s from inside a caster. Support runbooks should be updated to reflect that, and the API docs and any client SDKs need to match before rollout — a status-code change is a user-visible contract change, not an internal refactor, and it belongs in a product/API review. The corresponding negative is a class of quiet regressions (strict comparisons against validated()) that will not page anyone and will surface as wrong behaviour, so expect a short tail of tickets after rollout rather than at rollout.

Scaling. Two concrete wins. First, request input is replaced once per request instead of once per casted attribute, and castAttributes() casts several attributes in a single pass — prefer it over looping castAttribute(), both for throughput and to avoid a half-cast intermediate input array. Second, Octane and queue-worker safety is restored, so we can run the package on a long-lived worker fleet without the 1.x cross-request leakage. Watch payload size on wildcard casts over large lists: wildcards are applied per item, and the fixed per-item custom-caster bug means custom casters now do real per-item work rather than silently reusing the first result.

Failure modes to document in the runbook.

  • Cast "didn't happen" — check in order: (1) attribute name and wildcard match exactly; (2) the request's own cast is shadowing a global one you expected to win; (3) validation passed at all, because nothing is cast when it fails; (4) for custom casters, the class implements the typed 2.0.0 signature — a pre-2.0.0 caster fatals at resolution rather than misbehaving quietly.
  • InvalidArgumentException from InputBag — caused by writing cast values back with $request->merge(). Read from validated()/safe(); never merge cast output.
  • Cross-request contamination under Octane — caused by calling forget() at runtime. forget() is a test-isolation tool; in tearDown(), in preference to re-registering dummy values. Override per request instead of mutating the singleton.
  • Date drift — date casts no longer derive format from the database connection's grammar. Verify that free-form inputs ('next tuesday', offset-bearing timestamps) still produce the expected instances, especially in queue workers and console commands that never open a DB connection.
  • Broken custom handler — if we customised RequestResolvingHandler, that customisation must move; the two-phase resolve/after-resolve split no longer exists.

Ramp-up. Engineers need roughly half a day to internalise the validation-then-cast mental model; the reference material is UPGRADE.md plus the three contracts, both of which are good. The gap is the absence of a community corpus — no tutorials, no Stack Overflow answers, no blog posts from other adopters. Budget for an internal write-up (the ADR plus cookbook from Phase 3) and treat it as a deliverable, not documentation debt. New joiners will otherwise re-derive the ordering rules and the wildcard-shadowing behaviour from the source.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
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