<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yurii Zub</title>
    <description>The latest articles on DEV Community by Yurii Zub (@yuriizee).</description>
    <link>https://dev.to/yuriizee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4025916%2F8d001d1c-5f6c-4e77-9239-da91436eddcd.png</url>
      <title>DEV Community: Yurii Zub</title>
      <link>https://dev.to/yuriizee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuriizee"/>
    <language>en</language>
    <item>
      <title>Compiling PHP DTOs: from reflection to 4.5M hydrations per second</title>
      <dc:creator>Yurii Zub</dc:creator>
      <pubDate>Sun, 12 Jul 2026 10:15:26 +0000</pubDate>
      <link>https://dev.to/yuriizee/compiling-php-dtos-from-reflection-to-45m-hydrations-per-second-3jic</link>
      <guid>https://dev.to/yuriizee/compiling-php-dtos-from-reflection-to-45m-hydrations-per-second-3jic</guid>
      <description>&lt;p&gt;&lt;em&gt;TL;DR: attribute-driven DTOs are pleasant to write but usually pay a reflection-and-dispatch tax on every call. &lt;a href="https://github.com/std-out/simple-data-objects" rel="noopener noreferrer"&gt;Simple Data Objects&lt;/a&gt; compiles a specialized closure per data class instead — plain properties become inline array reads, the generated code is persisted into an opcache-served cache, and PHP 8.4 lazy ghosts defer hydration until first property access. Here's the architecture, step by step, with numbers at each stage.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every modern PHP codebase eventually grows a layer of data objects: typed, immutable classes that sit between raw input (a request, a DB row, an API payload) and your domain logic. Attribute-driven DTO libraries made this pleasant to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderData&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;#[Rules(['required', 'string', 'max:200'])]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="na"&gt;#[Cast(new DateTimeCast('Y-m-d'))]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nc"&gt;\DateTime&lt;/span&gt; &lt;span class="nv"&gt;$deliveryDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But pleasant to write usually means expensive to run. The typical implementation reflects over the constructor, reads the attributes, and walks a generic hydration pipeline for every property of every object — on every call. That's fine for one DTO per request. It stops being fine when you hydrate a few thousand rows from a report query, or when your API serializes nested object graphs on every response.&lt;/p&gt;

&lt;p&gt;I wanted the ergonomics without the tax. The result is a PHP 8.4+ package where the hot path contains &lt;strong&gt;no reflection, no attribute reads, and no generic dispatch loop&lt;/strong&gt; — because by the time production traffic hits it, hydration is a compiled function.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: reflect once, not per call
&lt;/h2&gt;

&lt;p&gt;The obvious first move: do all reflection up front. On the first &lt;code&gt;from()&lt;/code&gt; call for a class, the package builds a &lt;code&gt;ClassMeta&lt;/code&gt; — an immutable description of the constructor: parameter names, types, defaults, nullability, and everything the attributes declare (casts, input-key mappings, pipes, validation rules).&lt;/p&gt;

&lt;p&gt;Every subsequent call reuses that metadata. This is table stakes — most libraries do some version of it — and it still leaves you with an interpreted loop: for each parameter, check "does it have a cast? a mapped key? a default?" and branch accordingly. Thousands of objects × dozens of properties = millions of predictable, redundant branch decisions per request.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: compile the loop away
&lt;/h2&gt;

&lt;p&gt;The metadata is static per class. So instead of interpreting it on every call, the package &lt;strong&gt;generates a specialized closure per class&lt;/strong&gt; and lets PHP execute that.&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;CreateOrderData&lt;/code&gt; above, the generated hydrator is essentially:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pipes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;\App\Data\CreateOrderData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\App\Data\CreateOrderData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;\array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;DataHydrationException&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;missingField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateOrderData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;\array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'deliveryDate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;ValueCaster&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'deliveryDate'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;DataHydrationException&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;missingField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateOrderData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deliveryDate'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'notes'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note what happened to each kind of parameter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain properties&lt;/strong&gt; (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;notes&lt;/code&gt;) became direct array reads. No pipeline, no dispatch — the nullable-with-null-default case collapses all the way down to &lt;code&gt;$d['notes'] ?? null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Properties with behavior&lt;/strong&gt; (&lt;code&gt;deliveryDate&lt;/code&gt; has a cast) delegate to the same runtime &lt;code&gt;ValueCaster&lt;/code&gt; as before, with the metadata captured in &lt;code&gt;use&lt;/code&gt;. Semantics are identical to the interpreted path; only the per-parameter "what kind of property is this?" decision is gone, because it was made once at compile time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing-field errors&lt;/strong&gt; are inlined as &lt;code&gt;throw&lt;/code&gt; expressions, so the happy path carries no error-handling scaffolding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The serializer side (&lt;code&gt;toArray()&lt;/code&gt;) gets the same treatment: plain properties become inline property reads into an array literal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"You're &lt;code&gt;eval()&lt;/code&gt;-ing generated code?"&lt;/strong&gt; Yes, once per class per process — and the generated source is assembled &lt;em&gt;only&lt;/em&gt; from reflection metadata. Class and property names are valid PHP identifiers by definition; the single free-form string that can appear (an input key from &lt;code&gt;#[MapPropertyName]&lt;/code&gt;) is embedded via &lt;code&gt;var_export()&lt;/code&gt;. There is no path from runtime input into the generated code.&lt;/p&gt;

&lt;p&gt;Result of this step alone, measured on the same DTO shapes: &lt;strong&gt;hydration ~2.6× faster, serialization ~2.2× faster&lt;/strong&gt; than the interpreted pipeline it replaced.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: don't even compile at runtime
&lt;/h2&gt;

&lt;p&gt;A compiled closure still costs reflection + code generation + &lt;code&gt;eval()&lt;/code&gt; on the first call of each process. With PHP-FPM that's every worker after every deploy; with short-lived CLI jobs it's every run.&lt;/p&gt;

&lt;p&gt;So the metadata cache persists the compiled code. Point the registry at a directory:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;MetadataRegistry&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;setStoragePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'framework/data-objects'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;and each class gets a &lt;code&gt;.meta.php&lt;/code&gt; file containing the exported metadata &lt;strong&gt;plus the generated hydrator and serializer source&lt;/strong&gt;. Loading it is a PHP &lt;code&gt;include&lt;/code&gt; — which means &lt;strong&gt;opcache serves it as compiled opcodes, shared across all FPM workers&lt;/strong&gt;. A warmed worker pays neither reflection nor &lt;code&gt;eval()&lt;/code&gt;. The cache write is atomic (write-to-temp + rename), and a guard refuses to persist classes whose metadata isn't safely exportable.&lt;/p&gt;

&lt;p&gt;Pre-warm on deploy so even the first request is hot:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vendor/bin/sdo-warm storage/framework/data-objects app/Data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The warmer scans your PSR-4 sources for concrete data classes, compiles everything, and fails the deploy fast if any DTO definition is invalid — which is exactly when you want to find out.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: don't hydrate what nobody reads
&lt;/h2&gt;

&lt;p&gt;PHP 8.4 shipped &lt;a href="https://www.php.net/manual/en/language.oop5.lazy-objects.php" rel="noopener noreferrer"&gt;lazy objects&lt;/a&gt;, and they're a perfect fit for DTOs. &lt;code&gt;fromLazy()&lt;/code&gt; returns an uninitialized lazy ghost; the compiled argument resolver runs on &lt;strong&gt;first property access&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromLazy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// casts, nested DTOs, pipes — none of it has run yet.&lt;/span&gt;
&lt;span class="c1"&gt;// Touch one property and that object (only) hydrates:&lt;/span&gt;
&lt;span class="nv"&gt;$orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both the eager hydrator and the lazy argument resolver are generated from the same code builder, so hydration semantics can't drift between the two paths.&lt;/p&gt;

&lt;p&gt;When only ~10% of created objects are ever read — think "hydrate the page, render the visible rows" — this measures &lt;strong&gt;~3× faster on cast-heavy DTOs and ~6× with nested collections&lt;/strong&gt;. Honest footnote: for trivial flat DTOs the ghost bookkeeping costs about as much as it saves, so the docs say exactly that instead of pretending it's free speed.&lt;/p&gt;

&lt;p&gt;And for the "million rows through a pipeline" case there's &lt;code&gt;lazyCollection()&lt;/code&gt;, which hydrates one item at a time as the collection is consumed: streaming 50,000 rows peaks at &lt;strong&gt;~0.26 MB&lt;/strong&gt; instead of ~13 MB materialized.&lt;/p&gt;
&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Benchmarked against the most popular full-featured data-object library in the PHP/Laravel ecosystem — identical DTO shapes, 20,000 iterations per scenario, PHP 8.4:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Simple Data Objects&lt;/th&gt;
&lt;th&gt;Popular alternative&lt;/th&gt;
&lt;th&gt;Advantage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hydration — flat DTO&lt;/td&gt;
&lt;td&gt;~4,500,000 ops/s&lt;/td&gt;
&lt;td&gt;~130,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~35×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hydration — nested DTO&lt;/td&gt;
&lt;td&gt;~2,200,000 ops/s&lt;/td&gt;
&lt;td&gt;~74,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~30×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hydration — collection of 20&lt;/td&gt;
&lt;td&gt;~270,000 ops/s&lt;/td&gt;
&lt;td&gt;~7,500 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~36×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serialization — flat DTO&lt;/td&gt;
&lt;td&gt;~7,400,000 ops/s&lt;/td&gt;
&lt;td&gt;~200,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~37×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serialization — nested DTO&lt;/td&gt;
&lt;td&gt;~4,000,000 ops/s&lt;/td&gt;
&lt;td&gt;~117,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~34×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak memory — 50k rows&lt;/td&gt;
&lt;td&gt;0.26 MB (&lt;code&gt;lazyCollection()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;~13 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~50× less&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Absolute numbers vary with hardware; the ratios stay stable across runs. To be fair to the alternative: it does more — it's a framework-integrated toolkit with TypeScript transformers, wire formats, and a large extension surface. If you use that surface, use it; it's excellent. This package targets the case where DTOs are on your hot path and you mostly need hydrate/validate/serialize — the 80% — as fast as PHP can go.&lt;/p&gt;
&lt;h2&gt;
  
  
  What you give up
&lt;/h2&gt;

&lt;p&gt;Design honesty section. The speed comes from constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP 8.4 only.&lt;/strong&gt; Lazy ghosts, property hooks-era engine. No polyfills, no legacy branches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constructor-promoted, readonly properties&lt;/strong&gt; are the model. No setters, no mutable state — which is also just correct DTO design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No runtime magic.&lt;/strong&gt; Everything the DTO does is declared in attributes and compiled ahead of time. If you need to rewire behavior per instance at runtime, this is the wrong tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you don't give up: validation (Laravel rules, working standalone without a Laravel app), casts (dates, enums, JSON, encrypted fields via libsodium), input pipelines, key transforms, nested collections, immutable &lt;code&gt;with()&lt;/code&gt;/&lt;code&gt;diff()&lt;/code&gt;/&lt;code&gt;equals()&lt;/code&gt;. The test suite covers &lt;strong&gt;100% of lines, enforced in CI&lt;/strong&gt; — the coverage gate fails below 100, and there are no &lt;code&gt;@codeCoverageIgnore&lt;/code&gt; escapes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require std-out/simple-data-objects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/std-out" rel="noopener noreferrer"&gt;
        std-out
      &lt;/a&gt; / &lt;a href="https://github.com/std-out/simple-data-objects" rel="noopener noreferrer"&gt;
        simple-data-objects
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Blazing-fast attribute-driven DTOs for PHP 8.4+ — compiled hydrators, zero reflection in production, works standalone or with Laravel
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Simple Data Objects&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/std-out/simple-data-objects/actions/workflows/tests.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/std-out/simple-data-objects/actions/workflows/tests.yml/badge.svg" alt="Tests"&gt;&lt;/a&gt;
&lt;a href="https://github.com/std-out/simple-data-objects/actions/workflows/security.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/std-out/simple-data-objects/actions/workflows/security.yml/badge.svg" alt="Security"&gt;&lt;/a&gt;
&lt;a href="https://github.com/std-out/simple-data-objects/actions/workflows/tests.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/468feb1f3512fb4bc95db14a378da089b805617df3a05a8338669c903f947ccb/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f676973742e67697468756275736572636f6e74656e742e636f6d2f79757269697a65652f31613662646265613737643136306565616634353234623865313635643361632f7261772f636f7665726167652e6a736f6e" alt="Coverage"&gt;&lt;/a&gt;
&lt;a href="https://packagist.org/packages/std-out/simple-data-objects" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0ba1c6bbf85e5274c0bcf80c7270578198feee9cdbc7789de5ec2a014f1f85db/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7374642d6f75742f73696d706c652d646174612d6f626a656374732e737667" alt="Latest Version"&gt;&lt;/a&gt;
&lt;a href="https://packagist.org/packages/std-out/simple-data-objects" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e0123238d0052b235478d7a0f78594c7096fccbed0f276d68dd57b20e4716582/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7374642d6f75742f73696d706c652d646174612d6f626a656374732e737667" alt="Total Downloads"&gt;&lt;/a&gt;
&lt;a href="https://packagist.org/packages/std-out/simple-data-objects" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e9b4fc8e2edfe2ad83ab5e32546edefd37388dfa099b72dfe95f1a4820360f55/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253545382e342d3737374242343f6c6f676f3d706870266c6f676f436f6c6f723d7768697465" alt="PHP"&gt;&lt;/a&gt;
&lt;a href="https://github.com/std-out/simple-data-objects/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8bb50fd2278f18fc326bf71f6e88ca8f884f72f179d3e555e20ed30157190d0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e737667" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lightweight, attribute-driven DTOs for PHP 8.4+.&lt;/strong&gt;&lt;br&gt;
Works standalone or inside Laravel 10–13. No reflection in production.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;composer require std-out/simple-data-objects&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://std-out.github.io/simple-data-objects/" rel="nofollow noopener noreferrer"&gt;Full documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Simple Data Objects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hot path&lt;/td&gt;
&lt;td&gt;Compiled per-class closures — zero reflection, zero dispatch overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate&lt;/td&gt;
&lt;td&gt;None — constructor props + attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roundtrip&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;from(toArray())&lt;/code&gt; always works, mapped keys included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone&lt;/td&gt;
&lt;td&gt;Validation works without a Laravel app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipelines&lt;/td&gt;
&lt;td&gt;Middleware-style input preprocessing, class or property level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Performance&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Benchmarked against &lt;strong&gt;the most popular full-featured data-object library in the PHP/Laravel ecosystem&lt;/strong&gt; — identical DTO shapes, 20,000 iterations per scenario, PHP 8.4:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Simple Data Objects&lt;/th&gt;
&lt;th&gt;Popular alternative&lt;/th&gt;
&lt;th&gt;Advantage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hydration — flat DTO&lt;/td&gt;
&lt;td&gt;~4,500,000 ops/s&lt;/td&gt;
&lt;td&gt;~130,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~35× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hydration — nested DTO&lt;/td&gt;
&lt;td&gt;~2,200,000 ops/s&lt;/td&gt;
&lt;td&gt;~74,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~30× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hydration — collection of 20&lt;/td&gt;
&lt;td&gt;~270,000 ops/s&lt;/td&gt;
&lt;td&gt;~7,500 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~36× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serialization — flat DTO&lt;/td&gt;
&lt;td&gt;~7,400,000 ops/s&lt;/td&gt;
&lt;td&gt;~200,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~37× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serialization — nested DTO&lt;/td&gt;
&lt;td&gt;~4,000,000 ops/s&lt;/td&gt;
&lt;td&gt;~117,000 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~34× faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/std-out/simple-data-objects" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://std-out.github.io/simple-data-objects/" rel="noopener noreferrer"&gt;https://std-out.github.io/simple-data-objects/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Good first issues (new casts, pipes, cookbook recipes): &lt;a href="https://github.com/std-out/simple-data-objects/issues" rel="noopener noreferrer"&gt;https://github.com/std-out/simple-data-objects/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it on a real workload, I'd genuinely like to hear the numbers — especially where it &lt;em&gt;doesn't&lt;/em&gt; win. Benchmarks that survive contact with other people's production are the only ones that matter.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
