Skip to content

Add lightweight support data objects - #566

Merged
binaryfire merged 14 commits into
0.4from
feature/lightweight-data-object
Sep 5, 2026
Merged

Add lightweight support data objects#566
binaryfire merged 14 commits into
0.4from
feature/lightweight-data-object

Conversation

@binaryfire

Copy link
Copy Markdown
Member

Summary

This adds a lightweight Hypervel\Support\DataObject for typed mapping in hot paths that do not need the full Hypervel Data feature set.

The new class is complementary to Hypervel\Data\Data. Use DataObject for trusted arrays, internal message envelopes, SDK payloads, and per-item value objects. Use Data when the object owns validation, mapping, lazy properties, partials, resources, persistence, or collection behavior.

Design

DataObject has a deliberately small public API:

  • from(array) constructs an object from constructor parameter names.
  • Public promoted properties remain ordinary PHP properties.
  • toArray(), jsonSerialize(), and toJson() recursively normalize supported values.
  • flushState() clears worker-cached recipes through the existing global cleanup path.

Construction compiles one immutable reflection recipe per concrete class and retains it for the worker lifetime. Warm calls execute that compact recipe without container resolution, package metadata, PHPDoc parsing, or per-instance mapper state.

The mapper handles common scalar conversions, backed enums, dates, nested DataObject values, nullable parameters, and constructor defaults. Invalid scalar input fails instead of silently inventing values. Ambiguous unions and unsupported declarations remain subject to PHP's native type checks rather than introducing a second extensible conversion engine.

Transformation reads the current public property values, so ordinary mutation is reflected immediately. It does not retain an output cache on each instance.

The implementation is marked transient, contains no request-scoped state, and publishes compiled recipes only after they are complete.

Form Requests

DataObject implements the existing generic RequestCastable contract. Form requests can therefore cast an already-validated object or each member of a validated list without adding a Foundation special case:

protected function casts(): array
{
    return [
        'contact' => Contact::class,
        'contacts.*' => Contact::class,
    ];
}

The converted values remain available through the existing validated() and safe() APIs. Validation continues to run against the submitted arrays before casting.

Performance

Measurements below are the median p50 and p95 from three complete alternating-order runs on PHP 8.4.23 with CLI OPcache and JIT disabled. The committed harness warms each scenario and uses repeated samples. Times are nanoseconds per reported operation; the 1,000-item rows are normalized per item.

The acceptance baseline used a frozen copy of the removed 0.3 implementation. The rebuilt implementation is faster across the representative common paths, while fixing its known correctness problems. The historical fixture was deleted after the comparison so it does not become maintained production-adjacent code.

Scenario Removed implementation p50 Rebuilt implementation p50 Change Speedup
Flat construction 1,735 1,034 -40% 1.68x
Construction with defaults 1,137 614 -46% 1.85x
Scalar coercion 1,853 1,378 -26% 1.34x
Nested construction 3,788 1,895 -50% 2.00x
Deep construction 5,258 2,403 -54% 2.19x
Backed enum construction 1,387 760 -45% 1.83x
1,000-item construction loop, per item 1,747 1,041 -40% 1.68x
Flat transformation 653 495 -24% 1.32x
1,000-object transformation loop, per item 633 483 -24% 1.31x

The supported harness compares the rebuilt DataObject with full Data:

Construction

Scenario DataObject p50 Data p50 DataObject p95 Data p95
Flat, 5 scalars 1,034 4,054 1,164 4,187
With defaults 614 3,554 678 3,826
Wide, 20 scalars 3,755 8,018 3,881 8,299
Scalar coercion 1,378 5,047 1,406 5,773
Nested, 1 level 1,895 6,175 2,260 6,382
Deep, 3 levels 2,403 7,347 2,575 7,784
Backed enum 760 3,696 794 3,900
Date 3,357 5,418 3,535 5,877
Mixed API payload 6,277 11,272 6,563 12,157
Array of 25 data objects 21,818 124,830 22,566 130,451
1,000-item loop, per item 1,041 4,066 1,103 4,397

Transformation

Scenario DataObject p50 Data p50 DataObject p95 Data p95
Flat 495 1,790 519 1,863
Wide 1,670 2,444 1,700 2,624
Nested tree 1,124 4,384 1,154 4,649
Deep tree 1,453 6,342 1,508 6,434
JSON encode, nested 1,752 5,318 2,027 5,670
Array of 25 data objects 13,098 53,932 13,767 62,049
1,000-object loop, per item 483 1,704 555 1,773
Property read 31 30 35 34

Retained instance memory is comparable for untransformed objects and stays fixed after transformation because DataObject has no instance output cache. A small compiled recipe retained 1.7 KB in this run. Fresh-process first use measured 22.8 us p50 and 34.9 us p95.

These measurements compare two different contracts. They show the cost boundary for choosing the lightweight mapper; they are not a claim that DataObject replaces Data.

Documentation

The data object guide now explains the lightweight and full Data use cases side by side, including construction, conversion, nesting, serialization, and FormRequest casting. The benchmark harness remains in tests/Benchmarks/Data so future changes can be measured against both APIs.

The implementation plan records the design and verification boundary. The repository TODO also tracks a future audit of strict typed-input conversion across framework entry points.

Verification

  • Applied the repository formatter.
  • Ran PHPStan.
  • Ran the focused Support and Foundation test suites covering construction, conversion, inheritance, recursion, dates, serialization, worker-state cleanup, and FormRequest casting.
  • Ran the comparison benchmark in both measurement orders and checked warm throughput, tail latency, retained memory, and first-use cost.

Summary by CodeRabbit

  • New Features

    • Added lightweight typed data objects for creating, converting, and serializing structured data.
    • Added support for nested objects, enums, dates, defaults, strict scalar conversion, and recursive arrays.
    • Added form-request casting for individual and wildcard inputs.
    • Added documentation covering usage, serialization, validation, and limitations.
  • Documentation

    • Updated verification guidance to favor targeted checks and tests.
  • Tests

    • Added comprehensive coverage for data objects and request casting.
    • Updated benchmarks to compare lightweight data objects with full data classes.

Use focused formatting, analysis, and affected tests for isolated changes instead of requiring the full repository suite at every checkpoint.

Reserve composer fix for work that can affect code beyond focused tests, clarify when PHPUnit or ParaTest is appropriate, and point full static analysis at the dedicated composer analyse command.
Introduce Hypervel\Support\DataObject for trusted internal envelopes and high-throughput value mapping without pulling in the full Hypervel Data feature engine.

Compile a compact immutable recipe once per used class, then construct through exact named arguments with strict scalar conversion, backed-enum support, configured date handling, nested data objects, and recursive array or JSON output. Keep the class transient and retain no request data, reflection objects, or transformed instance cache.

Cover construction precedence, invalid declarations, scalar conversion, enums, relative and inherited object types, date targets, transformation, mutation, serialization, cache ownership, and native failure boundaries.
Add a Laravel-style guide for choosing Support DataObject when trusted internal values need fast typed construction and recursive array or JSON output.

Explain the exact-key contract, strict common conversions, nested object behavior, explicit list conversion, and the boundary with Data, Dto, Resource, and DataCollection so developers can select the smaller API without confusing it with the full data package.
Keep a reproducible comparison between the supported Support DataObject and full Hypervel Data across construction, transformation, retained memory, and first-use behavior.

Add reverse measurement order for checking ordering bias and preserve equal-output collection scenarios. Remove the frozen historical mapper now that acceptance measurements are complete, avoiding permanent retention of code with known recursion, coercion, and cache defects.
Record a framework-wide audit of integer, float, and boolean input contracts across InteractsWithData, Support DataObject, and Hypervel Data.

Only extract a neutral conversion primitive when public semantics genuinely converge, and keep any future Data behavior decision independent so this lightweight implementation does not create an accidental cross-package contract change.
Document the final Support DataObject contract, compiled-recipe design, strict conversion rules, date and nesting behavior, transformation semantics, documentation boundary, benchmark acceptance criteria, and verification coverage.

Capture the rejected overlapping or configurable designs so future work preserves the small complementary API without rebuilding a second Hypervel Data engine.
Implement Foundation's existing RequestCastable contract directly on Support DataObject subclasses. A small Support-owned caster now converts already-validated arrays through the concrete object's from() method, preserves null values, and reports the affected request key for invalid input shapes.\n\nKeep the mapper's construction and transformation hot paths unchanged, reject unsupported cast arguments consistently with Hypervel Data, and cover concrete caster selection, null handling, and the inherited public contract.
Exercise Support DataObject classes through FormRequest's generic cast pipeline. The coverage verifies exact and wildcard declarations, sparse list-key preservation, strict scalar conversion, nullable values, safe extraction, and unchanged raw request input.\n\nAlso pin the clear failure for a validated non-array value so request and cast rule drift identifies the affected input and target class.
Show how a FormRequest may return lightweight DataObject instances after it validates the submitted arrays. Document direct casts for one object and wildcard casts for lists, while keeping object-owned validation, mapping, resources, and collection abstractions with the full Data package.\n\nCross-link the data-object and validation guides so developers can choose the appropriate object type without restoring the removed casted() API or Foundation-specific adapters.
Record the final request-casting design, ownership boundary, public contract, documentation, tests, file changes, and verification steps. Replace the earlier blanket rejection of integration adapters with the narrow use of Foundation's existing generic RequestCastable extension point.\n\nKeep validation and richer object behavior in Hypervel Data while documenting that already-validated arrays may become lightweight DataObject instances without a Foundation special case.
Require float-like inputs for integer-backed enums to be finite, integral, and within the platform integer range before conversion. This prevents fractional request or object values from silently selecting a truncated enum case while retaining integral decimal and exponent forms.

Cover the shared helper together with the public DataObject and validation boundaries so downstream consumers inherit the corrected behavior without duplicate test matrices.
Clarify the integer-backed enum input contract for lightweight and full Data objects, FormRequest casting, and validation. Integral numeric values remain supported, while fractional values are rejected instead of being truncated to another case.
Feed Support DataObject and full Data the same ISO timestamp in the date and mixed-payload scenarios. This keeps the benchmark focused on framework cost instead of comparing different DateTimeImmutable parse inputs.
Record the shared integer-backed enum conversion rule, its tests and documentation, and the identical timestamp requirement for fair date benchmarks. Keep the plan aligned with the final reviewed implementation and verification boundary.
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 04abb257-b617-4467-a9a3-04c61a3018ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@binaryfire
binaryfire merged commit e0e2a3a into 0.4 Sep 5, 2026
38 checks passed
@binaryfire
binaryfire deleted the feature/lightweight-data-object branch September 5, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant