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

Simple Data Objects Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit. The package sits at the boundary of the application, which is the right place for a DTO layer: request payloads in, API responses out, third-party webhooks parsed, CSV/queue batches streamed. It is not auto-discovered and works standalone, so it does not modify Laravel's container or boot sequence unless you opt in. That is architecturally clean — nothing in the domain core has to know about it. The one design decision to police is inheritance: every DTO extends StdOut\SimpleDataObjects\BaseData, so DTOs become vendor-coupled. That is acceptable at the edge, unacceptable in the domain model. Keep the rule explicit: no entity, aggregate, or domain service may extend BaseData. This rule now needs a mechanical enforcement point, because v2.8.0 ships a generator that will happily emit BaseData subclasses wherever you point it — constrain the output directory in the tooling config, not just in the style guide.

The feature set maps unusually well to work we already do badly. fromResult() with dot-path error accumulation replaces a large amount of hand-rolled 422 assembly. Optional gives correct PATCH semantics (absent vs. explicit null) that we currently approximate with array_key_exists checks. #[Hidden(except: [...])], #[Computed], and #[Flatten] cover most of what API Resources do, with less ceremony. jsonSchema() plus the TypeScript generator is the highest-value item in the package from a product standpoint: one annotated class becomes the runtime contract, the OpenAPI fragment, and the frontend .d.ts, which removes drift between backend and frontend teams.

v2.8.0 changes the shape of the adoption story, not the runtime. The release adds a document-driven class generator: sdo-generate from sample JSON/XML/stdin, DataClassGenerator::fromValue(), and make:data variants including --from-model=Order --id=1 --with=items. Three things follow from that:

  1. Runtime blast radius is zero. The generator is a dev-time CLI and a static API. No new production dependency, no new boot-path code, no new cache artifact. Our production risk posture is unchanged by this release.
  2. It attacks the actual cost centre. The assessment above priced adoption as "mechanically migratable but hand-authored." That is exactly the line item this release compresses. Converting an existing endpoint's request shape from a captured payload sample is now a generation-plus-review task rather than a transcription task. The estimate for inbound migration (stage 3) and outbound serialization (stage 2) should come down materially — plausibly 40–60% of the authoring effort, though not of the review and test effort.
  3. It introduces an inference-quality risk that did not exist before. A generator that infers types from values will faithfully reproduce the faults in your sample. A field that is string in the sample but sometimes an integer in production becomes a typed property with a cast that coerces or throws. A key absent from the sample is only marked T|Optional if you remember --optional. An empty array tells you nothing about the element type. A nullable field present as null is a different case from a field that is absent. These are precisely the silent-wrong-output failure modes we already flagged for stale metadata caches, arriving via a different door.

The honest framing for the decision log: the generator is a scaffolding tool, not a contract-authoring tool. Generated classes are a first draft that a human freezes, reviews, and then owns. Any workflow where regeneration happens continuously from a live sample is a liability, because it lets production shape drift into the type layer without review.

Integration feasibility. Low. Composer install plus MetadataRegistry::setStoragePath() in a service provider. Validation works with Laravel's validator, so existing rule strings, custom rules, and message translations should carry over — worth confirming for Rule::when, sometimes, and conditional rules before committing. from() accepts Eloquent models and Arrayable, so existing resource/transformer code has a mechanical migration path. The opt-in service provider means we can adopt it in one module without touching the rest of the app — the single most important property for approval. The 2.8.0 additions are explicitly backward compatible with 2.7.x and are purely additive, so nothing in the existing feasibility picture changes.

Technical risk. Four items dominate, none of them about code quality.

  1. PHP 8.4 floor. This is a hard prerequisite, not a preference. If our fleet is on 8.2/8.3, adoption is gated behind a runtime upgrade or a new service. That is a platform-level cost decision and should be surfaced to leadership first, because it will otherwise be discovered mid-sprint.
  2. Adoption risk. 35 stars, zero dependents, single maintainer. Positive signals exist — MIT, 100% coverage enforced in CI, a security workflow, published changelog, docs site, and a companion benchmark repo that lets us reproduce the numbers rather than trust them. But we would be an early adopter, meaning we effectively own our fork. The internals (compiled closures, eval/var_export-style cache files) are not trivially forkable by a generalist.
  3. Deploy correctness. Compiled metadata living on disk introduces a new deploy-time invariant. Stale cache after a DTO signature change is a silent-failure class of bug — the kind that produces wrong output rather than an exception. This must be treated as release-engineering work, not a footnote.
  4. Generator inference fidelity (new in 2.8.0). Type inference from a single sample is under-determined by nature. The risk is not that the generator is wrong; it is that it is confidently, plausibly wrong, and the resulting class looks like a reviewed decision. Two sub-risks deserve separate treatment: --from-model=Order --id=1 requires the tooling to open a database connection, which raises a governance question about which database we point it at (and whether generated files or fixtures can carry real customer data into the repo), and make:data --from-model with --with=items produces nested classes whose shape depends on the loaded relations of one record, so relationship coverage is whatever that record happened to have.

Key questions.

  • Is there a documented cache-invalidation contract, or is the metadata cache keyed only by class name? If a property is renamed and the old .meta.php survives, what happens?
  • Does #[Rules] accept full Laravel rule objects, Rule::when/Rule::unless, bail, and nested wildcards (items.*.price)? Do translations and :attribute substitution still work?
  • Does the generator emit only structural types, or does it also emit #[Rules], #[Hidden], #[Computed]? If it is structure-only, the saved effort is transcription, not design — the estimate should say so.
  • How does inference treat: null values, keys absent from some objects, empty arrays, heterogeneous arrays, numeric strings, ISO-8601 date strings versus arbitrary strings, and booleans arriving as 0/1? Each of these has a defensible-but-wrong default.
  • Is generator output deterministic — stable property ordering, stable class naming, stable file names — so that regeneration produces a reviewable diff rather than a whole-file rewrite?
  • What is the overwrite policy for an existing target file: refuse, merge, or clobber? Is there a --force, and does the tool warn when the destination class has been hand-edited?
  • Can it consume stdin (-) in CI, so we never need committed sample fixtures that might contain production data?
  • For --from-model: which connection does it use, does it respect our app config, and does it write any sampled values (fixtures, doc comments, defaults) into the generated source?
  • What is the maintainer's support model — issues only, or any response commitment? What is the stated BC policy across minor versions?
  • Where does sdo-warm write, and is that path shared/per-worker? How does blue/green or rolling deploy interact with a cache directory that persists across releases?
  • How are cast failures surfaced on the hot path — exception, fromResult() entry, or silent coercion? Especially IntegerCast/BooleanCast on "abc".
  • Does EncryptedCast (XSalsa20-Poly1305) expose key handling, and does it fit our KMS/key-rotation story?
  • What is the peak-memory claim under real payload shapes for lazyCollection() — the README's ~12 KB is credible only because it streams; confirm against our widest row.

Integration Approach

Stack fit. PHP 8.4+ and Laravel 12–13 only. If we are not already there, this becomes a two-phase initiative: platform upgrade, then package adoption. The package also claims Symfony/Slim/PSR-7 support and an opcache.preload recipe, which matters if the same DTOs need to appear in a worker, a queue consumer, or a CLI importer. The generator is a dev-dependency concern only; it does not appear in the production stack fit at all, which is a point in its favour.

Migration path. Do not attempt a big-bang replacement of FormRequests and API Resources. Five stages, each independently shippable and reversible:

  1. Spike (1 week). Reproduce the benchmarks on our own payload shapes with the companion repo, and prototype two real DTOs — one flat request, one nested response. Deliverable is measured throughput and memory, plus a written answer to the cache-invalidation question. Extend the spike by half a day: run sdo-generate and make:data --from-model against two real endpoints and grade the output. That is the cheapest possible test of whether the generator is a real accelerant or a novelty.
  2. Generator policy, not generator rollout. Before generating anything at scale, write down the rule: generation is a bootstrap step, output is reviewed and committed as hand-owned source, regeneration is a deliberate act with a diff review, never a CI build artifact. Decide the permitted output namespace (boundary only), the permitted input source (committed sanitized samples or a read-replica, never production with PII), and the review checklist for generated classes (types widened where inference is thin, Optional applied deliberately, validation added by hand).
  3. Outbound boundary first (low blast radius). Convert response serialization for one non-critical endpoint group. #[Hidden]/#[Computed]/#[Flatten] are drop-in against existing Resources. The generator is least useful here — response shapes usually need judgement — so this stays largely manual and that is fine; a regression here is visible in contract tests immediately.
  4. Inbound validation second, generator-assisted. This is where 2.8.0 earns its place. Bootstrap request DTOs from captured, sanitized payload samples, then review. Start with endpoints that already need fromResult()-style error accumulation. Publish a shared error-shape contract so the API error envelope stays consistent across converted and unconverted endpoints during the transition.
  5. High-volume paths last. lazyCollection() for CSV/queue imports and report generation, where the flat-memory property is the actual justification. This is where the ROI is largest and where a failure is most visible, so it goes last with load tests attached. Note that XML ingestion here pairs naturally with --from-xml, but only for the structural skeleton — streaming and memory behaviour must be validated empirically.

Compatibility. Keep BaseData at the boundary, keep existing FormRequests in place for endpoints with conditional authorization logic, and keep API Resources for anything with deeply conditional linking. Both can coexist indefinitely. Optional and PATCH handlers are worth adopting early because they fix a correctness gap, not just a performance one. 2.8.0 is additive and 2.7.x-compatible, so version pinning is unaffected; the generator is a new capability on an existing pinned version, not a reason to change the pin.

Sequencing with the schema pipeline. Ship sdo-typescript in CI early — it is cheap, and it makes the DTO layer visibly valuable to the frontend team before the backend migration is complete. Generate and diff the .d.ts in CI so contract drift fails the build rather than surfacing as a runtime bug. Do the inverse for the DTO generator: do not wire sdo-generate into CI regeneration. The asymmetry is deliberate — .d.ts is derived output, DTO classes are source.

Deploy sequencing. vendor/bin/sdo-warm must become a mandatory, fail-the-build step after composer install and before traffic. It already fails fast on invalid DTO definitions, which makes it a free static-analysis gate on the DTO layer — that is a genuine bonus, not just cache warming. Generated classes go through the same warm step as hand-written ones, so a bad generation fails at deploy rather than in production, provided the warm step is blocking.

Operational Impact

Maintenance. Per-DTO maintenance effort is near zero; the cost concentrates in four places: the deploy pipeline (warm step, cache directory lifecycle), the shared DTO conventions document (naming, when to use a pipe vs. a cast, output mapping rules), the fork, and — new — the ownership of generated code. Generated classes are unauthored code that we nonetheless maintain forever. The convention must be that whoever runs the generator owns the review, and that the diff, not the generator, is the unit of change. Budget a named owner — one engineer with enough PHP internals fluency to read the compiler code — or accept that an upstream bug blocks us until the maintainer responds.

Support. No commercial support, single maintainer, small community. That is a real but bounded exposure because the surface area is small and MIT-licensed. Mitigation: pin the version exactly, vendor the fork into our own Git namespace, and require an ADR before any upgrade. The 100% coverage and security workflow materially reduce the odds of a latent defect, but they do not reduce the odds of abandonment. The generator adds a small amount of new upstream surface to that fork obligation, but it is dev-time code — a bug there blocks authoring, not production, which is the cheapest possible place for an upstream defect to live.

Scaling. The performance story is a genuine capacity lever, not a vanity metric — the win is CPU-seconds per request, which translates into fewer instances for the same throughput. Treat the headline multipliers (~50×/60×) as upper bounds against a reflection-based alternative; the honest planning number is the streaming case (~87% on bulk import) plus a modest latency win on ordinary endpoints. The fromLazy() option is a real lever for cast-heavy DTOs where only ~10% of properties are read, but it hides hydration errors until first access — adopt it selectively, not by default. 2.8.0 changes none of this.

Failure modes, in rough order of likelihood and severity:

Failure Impact Detection Mitigation
Stale compiled cache after DTO change Wrong output, silently Contract tests only Warm step in deploy; clear cache dir per release; schema-diff in CI
Generated type narrower than reality (single-sample inference) Coercion or exception on real traffic, silently wrong for a while Contract/E2E tests on real shapes Widen types in review; sample from multiple records; prefer fromResult() on untrusted input
Generated class clobbers hand-edited DTO Lost work; possible silent behaviour change Code review of the diff only Generator output is a reviewed commit; never regenerate in CI; check overwrite policy first
Real customer data in generated source or fixtures (--from-model) Data-protection incident Manual review, if at all Point at read replica or committed sanitized samples; ban --from-model against production
Invalid DTO definition at deploy Deploy fails Immediate, loud Good failure — keep warm step blocking
Cast coercion on malformed input Bad data persisted E2E/contract tests Prefer fromResult() over from() for untrusted input
Cache dir permissions / disk Cold-path reflection on every request Latency dashboards Health check that warms and reads the cache path
Upstream abandonment Blocked on bugs n/a Fork, pin, ADR
Base class creep into domain Architectural erosion Code review Lint rule or architecture test forbidding BaseData outside the boundary namespace; generator output directory constrained to that namespace

Ramp-up. The attribute-driven API is small and learnable — most engineers will be productive in a day. Two things need real onboarding rather than docs: pipe authoring (ValuePipe/array-pipe contract, three lines but conceptually middleware) and the deploy/cache model, which is where mistakes are expensive and invisible. The generator lowers the floor further — an engineer can now produce a plausible DTO without understanding the package at all — which is a benefit for velocity and a hazard for judgement. Counter it directly: teach the review checklist (when to widen an inferred type, when to add Optional, when to reject the generated class entirely) as part of the 60–90 minute walkthrough, and make the "DTO cookbook" of five approved patterns explicitly cover generated output. The #[InferRules] attribute should be banned by convention initially — inferred validation is exactly the kind of implicit behaviour that produces a production incident nobody can trace, and an inference-from-sample generator sitting next to an inference-from-reflection validator is a combination that will eventually be used together by someone who has not read this document.

Verdict for the decision log. v2.8.0 does not change the gating conditions (PHP 8.4, cache-invalidation answer) and does not change the production risk profile, but it materially improves the business case by cutting the authoring cost of the migration we were already recommending. Adopt the generator in the same narrow, supervised role we would give any scaffolding tool: bootstrap only, output reviewed and committed as hand-owned source, never wired into CI regeneration, never pointed at production data. The performance claims remain the least interesting reason to adopt the package; the contract generation, the error accumulation, and the streaming memory profile are the durable ones. Hold off on lazyCollection() in critical paths and fromLazy() everywhere until the first two conversion phases have run in production for a release cycle.

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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager