zero-to-prod/data-model
DataModel hydrates PHP objects from external data with a single from($data) call using reflection and type hints. Validation and casting live in #[Describe] attributes (required, nullable, default, assign), replacing manual property wiring and scattered null checks.
Architecture fit. The package occupies a well-defined seam in a Laravel application: the boundary between transport-shaped data (validated FormRequest output, queued job payloads, third-party API responses, Model::toArray()) and typed domain objects. Laravel naturally produces associative arrays at that seam, and from() converts them into typed objects with one call. Because integration is a trait rather than a base class or interface, it composes with existing DTOs and imposes no inheritance constraint — a meaningful advantage in a codebase that already has base controllers, base jobs, and base models competing for the single-inheritance slot. The recursive hydration of class-typed properties maps cleanly onto nested request payloads. It does not fit where validation is the primary concern (spatie/laravel-data's rule integration, cuyz/valinor's schema-first mapping) or where DTOs are already standardized.
Integration feasibility. composer require, add a trait, done. No service provider, no config file, no migrations, no published assets, no queue/event wiring. The adoption cost is essentially zero at the technical level; the real cost is entirely social — establishing a team convention for when a DTO is warranted and how #[Describe] keys are used. That is a favorable cost profile for a pilot.
Technical risk. The concerns, roughly ordered by impact:
| Risk | Assessment |
|---|---|
Reflection on every from() |
No documented metadata cache. Cost scales with property count × payload depth × collection size. Unacceptable on hot paths without benchmarking. |
| Silent misconfiguration | Unrecognized #[Describe] keys are captured in $extra rather than raising. A typo ('requird' => true) degrades to "no constraint" instead of failing. Only three exception types exist, none for this. |
| Eight-level precedence ladder | assign → default → cast → post → method-cast → class-cast → via → direct. Documented and deterministic, but debugging a wrong value means walking a stack of resolvers. The README's "predictable" claim is true at the property level and less true at the class level. |
| Single-vendor dependency | Small org, 139 stars, 0 dependents on Packagist. Stars without dependents suggests interest, not production adoption. Bus factor is effectively 1. |
| Recursion without a depth guard | Nested from() calls are unbounded and undocumented for cycles or pathological payloads. |
| Composer hook side effect | The documented post-install-cmd/post-update-cmd pattern writes README into ./docs/. Vendor binaries mutating the working tree during install is an operational smell; do not enable it. |
| Version chasing | First-class-callable (PHP 8.5) syntax in examples implies a moving minimum PHP version. Confirm the floor before committing. |
Positive signals worth weighting: MIT license, test.yml and backwards_compatibility.yml CI workflows (the latter is a genuine semver-discipline indicator), no runtime dependencies, and — most valuable — near-zero lock-in. DTOs are plain PHP classes with public typed properties. Removing the package means deleting attributes and writing constructors, not rewriting call sites. That exit ramp materially lowers the cost of being wrong.
Key questions for the maintainer / spike.
from() at 1k / 10k / 100k objects, flat and 3-level nested, with and without casts.readonly and constructor-promoted properties?Foo[]-style collections — the README pushes arrays-of-models to the separate DataModelHelper package. How much of the capability surface is actually in sibling packages?Stack fit. Best fit is a Laravel app on PHP 8.2+ with an existing actions/services layer, where controllers currently pass arrays into services and the team wants typed boundaries. Worst fit is an app already invested in spatie/laravel-data, or one where DTOs must carry validation rules, Eloquent casting, or resource transformation — those capabilities live in sibling packages (DataModelHelper, DataModelFactory, Transformable), each an independent dependency with its own maturity profile. Adopting four small single-maintainer packages to get one capability is a materially different risk posture than adopting one.
Migration path. There is no migration — this is additive. Existing array-passing code stays; DTOs are introduced at one boundary at a time. Coexistence is trivial because nothing is registered globally.
Compatibility. Verify against the team's PHP floor first, then against Larastan. The package's own compatibility workflow covers its public API, not its behavior under your static analysis configuration. Laravel version is largely irrelevant given no framework coupling — worth confirming that the "Laravel Validation" and "Hydrating From a Laravel Model" examples are documentation, not integrations.
Sequencing.
pre/post hooks to a reviewed allowlist — they are the highest-blast-radius feature.Do not standardize org-wide on the strength of the README. The 29/100 composite score and 18.19 opportunity score both reflect a crowded category with an entrenched incumbent; this package's case rests on ergonomics and low lock-in, not on differentiation or ecosystem depth.
Maintenance. The package itself is small and stable-looking; the maintenance burden is the convention layer you build on top. Expect to own a Casts/ directory of static helper methods, because the attribute model encourages callable references to project code. Those helpers become de facto platform code and need an owner. Upstream maintenance risk is concentrated in one maintainer with no visible commercial backing — plan for the possibility of vendor-forking.
Support. Community-only: GitHub issues, no SLA, no security policy documented in the README. For an internal utility with a cheap exit ramp, that is tolerable; the mitigation is to keep DTO usage confined to layers you can rewrite in a sprint.
Scaling. Two axes. Throughput: reflection cost multiplies by object count and nesting depth — this is the single most likely reason the pilot fails, and it must be measured before expansion. Payload shape: unbounded recursion on nested input means a deeply nested or maliciously shaped JSON body drives correspondingly deep hydration work before validation rejects it. Ensure FormRequest validation runs before hydration, always.
Failure modes.
from() receiving a string/null (documented as "empty context") — yields a partially populated DTO rather than an error. Most dangerous with optional properties.'30' becomes int 30; PHP's native type juggling under reflection is not always what reviewers expect. Strict-types behavior per property should be verified.PropertyRequiredException and InvalidValue are unhandled by default and surface as 500s unless mapped to 422s in the exception handler. This must be wired on day one of the pilot.assign semantics: value always null for callables and always wins over context — a property that can never be overridden by input. Intentional, but a trap for anyone reading the DTO without reading the attribute.DuplicateDescribeAttributeException — a rare case of fail-fast.Ramp-up. Low for the base case (a developer is productive in an hour). Moderate for casts and hooks: the four-parameter callable signature, auto-detected arity, and the precedence table are learnable but not obvious, and the failure mode of getting them wrong is a wrong value, not an exception. Budget a short internal write-up and a code-review checklist. Junior developers will copy-paste cast callables; treat that as the primary source of drift.
Bottom line for a TPM. Low-cost, low-lock-in, low-differentiation. Worth a bounded pilot on the strength of the exit ramp alone; not worth a standardized dependency without a passing benchmark and a named internal owner.
How can I help you explore Laravel packages today?