carsdotcom/php-request-class
carsdotcom/php-request-class adds structured request objects to Laravel, letting you wrap input parsing, validation, and typed access in dedicated classes instead of cluttering controllers. Keeps request handling clean, explicit, and easy to test.
Architecture fit. The package sits at the HTTP boundary of the application: it wraps Guzzle transfers, owns response parsing/post-processing, and writes structured log entries per request. v2.0.0 does not change that architectural position or the public shape of the log entry — writeLog() still builds [$this->toGuzzle(), $this->response, $outcome, $this->requestStats]. The change is to when log() is invoked and what is appended to that array. That is a narrow architectural blast radius, but it lands squarely on the observability layer, which is the component teams tend to understand least and depend on most during incidents.
Integration feasibility. Feasible with a bounded audit. The release notes are unusually explicit about the migration surface and provide four concrete greps/checks:
function log( override that does not call parent::log();parseResponseString()/postProcess() exception messages and getExtendedData(), now written to the same log entry as the response body;postProcess() implementations that reject with anything other than a Throwable;The responseFromCache() pattern used by PreQualifyStrangerRequest continues to work unchanged with no double-write, which materially reduces migration risk for consumers who solved cache-hit logging the older way.
Technical risk. Two items are high severity, and both are silent failure modes rather than crashes:
LogFile::put()'s array_map(stringify_body, $contents) runs outside its own try/catch and aborts entirely on the first exception. If postProcess() rejects with a non-Throwable, non-serializable value (resource, closure, circular structure), nothing is written — including the Request/Response section that would otherwise have succeeded — and the logging exception becomes the promise's rejection reason instead of the actual error. This converts a formatting defect into lost diagnostics and a misleading error surface. Prior to this release that code path was only ever exercised with Guzzle's own Response/Throwable types, so existing test coverage is unlikely to catch it.getExtendedData()/->errors() now land in the same entry as the response body. Any payload or PII that was previously confined to exception handling now sits in the log sink. This is a data-handling review, not a code review, and may engage compliance.Medium risk: overrides of log() that replace base logic without parent::log() will now fire on cache hits, which can inflate log volume on high-cache-hit endpoints and, if the override emits its own entry, change file counts despite the note's reassurance that cache-miss counts are unchanged. Low risk (cosmetic): response body appearing twice in transfer-level exception entries, which breaks naive section-count assertions.
Key questions.
postProcess() rejecting with a non-Throwable a supported contract we should harden against, or is it "undefined behavior, don't do that"? This determines whether we need a defensive wrapper or just a lint rule.stringify_body() be made non-fatal (per-section try/catch) in a follow-up, so a single unserializable section cannot suppress the whole entry? This is the single highest-value hardening ask.responseIsFromCache publicly readable, so our overrides can add the guard without relying on an internal property?^1 constraint exists, we are unaffected until we opt in; if anything floats to * or dev, treat as urgent.Stack fit. Nothing in the release notes indicates Laravel-specific coupling — the affected seams are AbstractRequest::log(), responseFromCache(), writeResponseToCache(), parseResponseString(), postProcess(), and LogFile::put(), all plain PHP with Guzzle types. In a Laravel service this layer is typically reached through a service class or a queued job, so the integration work is confined to whichever application classes extend the package's base request. Those classes are the audit surface; count them first, as that number sizes the whole effort.
Migration path. Sequence for reversibility:
function log(, responseFromCache(, writeResponseToCache(, postProcess(, and parseResponseString( overrides. Produce a list with owner per file.if ($this->responseIsFromCache) { return; } to any log() override that does not call parent::log(). This is a no-op under v1 and correct under v2, so it can ship independently and de-risks the version bump.postProcess() rejections to throw only Throwable instances. Also beneficial under v1.getExtendedData() payload that reaches a log entry, with sign-off.responseFromCache() manual-logging classes to overriding log() directly, per the AbstractRequest::log() docblock. Do this after the bump, not as part of it, so the diff stays reviewable.Compatibility. getLogFolder()-only consumers see no behavioral change — that is likely the majority of the codebase and can be verified quickly. The PreQualifyStrangerRequest cache-hit pattern is explicitly preserved with no double-write. The genuine incompatibilities are all in the override layer described above. Confirm the composer constraint is exact or caret-pinned to ^1 before merge so the bump cannot arrive unscheduled via a transitive resolve.
Sequencing. Steps 1–4 are safe, independently shippable, and reversible; steps 5–6 are the atomic cutover and should land together with the parser changes. Step 7 is cleanup. If the override inventory comes back empty or limited to getLogFolder(), this collapses to a version bump plus a log-sample diff and can be done in a single change.
Maintenance. Low ongoing cost once migrated, but the migration itself creates a durable obligation: the log() contract is now a documented extension point with a guard the base class provides and subclasses can accidentally defeat. Add a code-review checklist item — "does this log() override call parent::log() or replicate the cache guard?" — and, if the codebase supports it, a static-analysis rule. Without that, the same defect will be reintroduced by the next developer who overrides log().
Support. Expect a short tail of confusing reports immediately after adoption: log entries that look different (extra appended section, duplicated body on transport errors), and — worse — endpoints where errors stopped producing log entries at all because a postProcess() rejection poisoned stringify_body(). Support and on-call runbooks that key off log shape should be updated before the bump, not after.
Scaling. File count for cache-miss traffic is unchanged, so steady-state log volume is roughly flat. The scaling risk is concentrated in two places: (a) log() overrides that now fire on cache hits, which puts volume on the cache-hit path that scales with traffic rather than with upstream calls, and (b) the deferred log() call now runs after parsing and post-processing, so it sits later in the request lifecycle — the log write lands closer to response time and includes heavier content (getExtendedData(), ->errors()), increasing per-entry size. Neither is a throughput cliff, but both shift cost from "log per upstream call" toward "log per request."
Failure modes. Ranked:
postProcess() rejection → array_map aborts → zero bytes written → logging exception replaces the real rejection reason. The observable symptom is absence of logs plus a wrong error message, which is the hardest class of incident to diagnose. Mitigation: enforce Throwable-only rejections; press upstream for per-section fault isolation in LogFile::put().log() overrides firing on every cache hit. Mitigation: the guard from step 2 above.Ramp-up. Small and well-scoped. The release notes effectively serve as the upgrade guide and are precise enough to hand to an engineer as a task list. Budget roughly half a day for the audit and guard work on a codebase with a handful of request classes, plus review time for the sensitive-data pass, plus whatever the log-parser and test updates cost. The main ramp-up cost is conceptual rather than technical: the team needs to internalize that log() is now invoked on every outcome and that the base-class guard is inherited only if you call parent::log(). That single sentence is the whole upgrade.
How can I help you explore Laravel packages today?