std-out/simple-data-objects
Lightweight, attribute-driven DTOs for PHP 8.4+ with zero runtime reflection and compiled per-class closures for fast hydration and serialization. Works standalone or in Laravel 12–13.
Build vs. buy on the DTO / payload layer. Every Laravel product eventually grows a boundary layer: FormRequests, API Resources, hand-rolled fromArray() constructors, or a DTO library. This package is a credible "buy" option for that layer, and the decision it most directly informs is whether to keep hand-rolling hydration/serialization or adopt an attribute-driven library. The pitch is concrete: constructor properties plus attributes replace boilerplate, and fromValidated() collapses validate → pipe → cast → hydrate into one call. v2.8.0 materially changes the economics of this decision by adding code generation (vendor/bin/sdo-generate, DataClassGenerator::fromValue()/write(), and make:data --from-json|--from-xml|--from-model). The "buy" cost was previously boilerplate savings minus learning curve; now the learning curve itself is partly automated — you scaffold from a real payload instead of writing the attribute DSL cold.
Brownfield migration and integration scaffolding. The most immediately useful new decision this informs is how cheaply can we seed the boundary layer on an existing system? Point sdo-generate at a captured sample response, a webhook body, or a partner feed and get one class per object shape with nested DTOs, #[DataCollection] lists and key mapping inferred. That turns "we'd have to rewrite 40 payloads first" from a quarter-scale project into a day, and makes the tool relevant to third-party API integrations — a roadmap category where you consume someone else's schema rather than design your own.
XML and legacy-partner ingestion. XML attribute / text / element inference is a genuinely differentiating capability and a product-level unlock: partner feeds, SOAP-era integrations and sitemap/config ingestion are usually the reason a team writes a bespoke parser. If your roadmap has any XML source, this is now a stronger reason to look than the performance story alone.
Feature and roadmap bets on performance-sensitive paths. The compiled-closure, zero-reflection hot path (plus MetadataRegistry::setStoragePath() and sdo-warm for deploy-time pre-warming) makes this relevant wherever payload volume is a real cost center: high-throughput APIs, queue workers deserializing jobs, webhook ingestion, report exports. If your roadmap includes scaling traffic without scaling fleet size, this is a legitimate lever — but benchmark against your payload shapes via the companion benchmark repo, not the README table.
Streaming and bulk-ingestion initiatives. lazyCollection() and flat ~12 KB peak memory on 100k-row imports is an architectural claim (no full materialization), and that's the kind of thing that unblocks a CSV/ETL feature that would otherwise require chunking infrastructure or a separate service. This is arguably the strongest product story here: it enables a feature rather than just optimizing one.
API contract and frontend-type generation. jsonSchema() and sdo-typescript turn your DTOs into a single source of truth for JSON Schema (draft 2020-12) and .d.ts output. For teams with a TypeScript frontend, that's a roadmap item — shared contract generation — that would otherwise be a bespoke script or a separate codegen tool. With v2.8.0 this closes a loop: sample payload → generated DTO → generated schema/TS, all from one artefact.
PATCH semantics and partial-update correctness. The Optional type distinguishing "key absent" from "key sent as null" is a small feature with outsized product impact: it's the difference between PATCH silently wiping fields and PATCH behaving correctly. --optional now propagates that automatically during generation — keys missing from some objects in the sample become T|Optional, so the generated class round-trips the sample exactly. If your API surface has partial updates, this removes a class of bugs that's expensive to find in production.
Testing and error-UX decisions. fromResult() accumulating all field errors with dot-paths (including nested DTOs and collections) is a user-facing decision — you can return complete validation feedback in one round-trip instead of drip-feeding errors. Worth weighing against your existing validation error contract.
Consolidation of a fragmented internal pattern. If multiple teams have each written their own DTO/hydrator, adopting one library is a standardization decision. This package's attribute set (Cast, Rules, Pipe, Flatten, Hidden, MapPropertyName, Discriminator, WrapIn, TransformKeys) is broad enough to absorb most bespoke cases — polymorphic hydration via #[Discriminator] and #[DataCollection] in particular cover patterns people usually hand-roll. Generation makes the standard teachable: new teams copy a generated class rather than reading docs.
Adopt (or at minimum spike) when most of these hold:
make:data --from-model=Order --id=1 --with=items shows the pattern on your own domain.Look elsewhere when:
--from-model reads a real record but still emits DTOs, not Eloquent-coupled models — if you want the ORM to do the hydration, this is the wrong tool.from()/fromJson() behaviour).Caution on the new generation features — treat output as a draft, not a spec. Inference is sample-driven: types come from observed values, --optional only knows about keys missing from that sample, and nulls, empty arrays, stringly-typed numbers and mixed-type fields will be under-specified. Generated classes need a review pass and a second sample (an edge-case payload) before they go near production. The generator is an accelerator for the spike, not a substitute for modelling.
Recommended adoption path: timeboxed spike (now closer to a few days than two weeks) on one real, performance-relevant slice. Start by running sdo-generate against a captured payload from that slice — it gives you a working DTO, a feel for the attribute DSL, and a concrete artefact to review with the team in the first hour. Then benchmark against both your current code and the popular alternative on your own payloads. Gate full rollout on the spike clearing your latency/memory targets and on a documented fallback: DTOs behind an internal interface, library pinned, fork procedure written down.
To executives: This is a small, MIT-licensed PHP library that replaces the hand-written and library-based glue code between HTTP input, validation, and our internal data objects, and as of the latest release it also writes that glue code for us from a sample payload. Three things make it worth a look rather than a pass. First, it's measurably faster — the authors publish 20–60× throughput advantages over the most popular alternative in this category, with a public benchmark repo so we can verify on our own payloads. Less CPU per request means more headroom per server, a direct cost lever. Second, it generates JSON Schema and TypeScript types from the same definitions, reducing frontend/backend contract drift. Third, the new generator turns onboarding from a training exercise into a command: point it at a real API response or partner feed and it produces the classes, which cuts the cost and risk of trying it. It's a bounded, reversible decision: MIT license, no vendor lock-in, standard Composer dependency, isolated behind an internal interface so it can be swapped. The caveat I'd flag up front is unchanged: it's a young project with a very small maintainer footprint and essentially no known dependents. Generated code is a starting draft, not a validated data contract — it needs engineering review. That's why I'm recommending a scoped evaluation on one real workload rather than an org-wide standard, with the version pinned and a fork path documented.
To engineering: The technical case is a compiled per-class closure for from()/toArray() — plain properties become direct array reads, zero reflection and zero dispatch on the hot path. Enable the file cache and pre-warm on deploy with sdo-warm, and every worker starts with opcache-compiled metadata and hydration code. Streaming via lazyCollection() keeps peak memory flat regardless of row count — architecture, not micro-optimization. The feature surface is broad: casts, Laravel rules, middleware-style Pipe preprocessing at class and property level, #[Discriminator] for polymorphic hydration, typed collections with IDE generics, immutable with()/diff(), never-throw fromResult() with dot-path errors, Optional for correct PATCH semantics, and schema/TS generation off the same metadata. Standalone validation works with no Laravel app booted. New in 2.8.0: vendor/bin/sdo-generate <sample.json|sample.xml|-> <out-dir> and DataClassGenerator::fromValue()/write() emit one class per object shape with inferred property types, nested DTOs, #[DataCollection] lists, #[TransformKeys]/#[MapPropertyName] key mapping and XML attribute/text/element handling; --optional emits T|Optional for keys absent from some sample objects so the sample round-trips; make:data gains --from-json, --from-xml and --from-model=Order --id=1 --with=items, the last generating nested classes from loaded relations. Additive and backward compatible with 2.7.x. What you're accepting: a hard PHP 8.4+ floor, an attribute-heavy DSL that's a real convention commitment, a fast-moving release cadence (read the changelog), a very small community, and ownership of the dependency if it goes quiet. Also accept that inferred types are only as good as the sample — budget a review pass and a second, adversarial sample before trusting generated classes in production. Mitigations: wrap it behind our own interface, pin the version, keep the fork procedure in the ADR, and validate the performance claims on our payloads before committing.
How can I help you explore Laravel packages today?