<?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>Simple Data Objects v2: from a fast PHP DTO hydrator to a full Typed Data Boundary</title>
      <dc:creator>Yurii Zub</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:56:58 +0000</pubDate>
      <link>https://dev.to/yuriizee/simple-data-objects-v2-from-a-fast-php-dto-hydrator-to-a-full-typed-data-boundary-101n</link>
      <guid>https://dev.to/yuriizee/simple-data-objects-v2-from-a-fast-php-dto-hydrator-to-a-full-typed-data-boundary-101n</guid>
      <description>&lt;h2&gt;
  
  
  Simple Data Objects v2: from a fast PHP DTO hydrator to a full Typed Data Boundary
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/yuriizee/compiling-php-dtos-from-reflection-to-45m-hydrations-per-second-3jic"&gt;previous article&lt;/a&gt; I wrote about an experiment compiling Reflection metadata in PHP to reach extremely fast DTO hydration. That experiment proved one thing: DTOs in PHP can be incredibly fast without sacrificing typing.&lt;/p&gt;

&lt;p&gt;But speed in a vacuum is only part of the puzzle. While preparing the &lt;strong&gt;Simple Data Objects v2 (SDO)&lt;/strong&gt; release, I asked a deeper question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What does a developer actually need from a DTO library in a real, large PHP or Laravel application, beyond a blazing-fast &lt;code&gt;from(array)&lt;/code&gt; method?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer turned out to be broad: convenient validation, input normalization, support for nested collections, safe error accumulation, flexible serialization, frontend contracts, and framework integration. And, crucially, all of it has to work &lt;strong&gt;without losing the legendary performance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Version 2 grew from a "fast hydrator" into a full, production-ready &lt;strong&gt;Typed Data Boundary&lt;/strong&gt;. Let's look at how it's built, and why performance here isn't just a number in a table but an architectural philosophy.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why plain arrays aren't enough anymore
&lt;/h2&gt;

&lt;p&gt;Arrays in PHP are a great, flexible tool — right up until the moment they start crossing the boundaries between different layers of a system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Request ➔ Controller ➔ Domain Service ➔ Queue Job ➔ Database ➔ API Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At every one of these steps, we're forced to make dozens of micro-assumptions:&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;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These assumptions gradually get duplicated across controllers, jobs, webhooks and tests. If the array's shape changes on the frontend, the chain breaks in a completely unpredictable place.&lt;/p&gt;

&lt;p&gt;A DTO solves this by creating a clear, named contract. All the conversion and validation magic happens once — &lt;strong&gt;at the boundary where data enters the system&lt;/strong&gt;. From then on, your code works with clean, typed objects.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Anatomy of a DTO in Simple Data Objects v2
&lt;/h2&gt;

&lt;p&gt;Installation is standard, via Composer:&lt;br&gt;
&lt;/p&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;p&gt;A basic DTO stays a maximally simple, readable PHP class:&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="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;StdOut\SimpleDataObjects\BaseData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerData&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&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;$email&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;$name&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hydration is direct and safe:&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;$customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CustomerData&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="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'42'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// The string is automatically cast to int&lt;/span&gt;
    &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ada@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Ada Lovelace'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// int(42)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your class stays clean, declarative code. The hydration compiler and metadata cache are SDO's under-the-hood work — invisible to the developer, but critical for the CPU.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important note:&lt;/strong&gt; while most examples in this article target Laravel (since it's one of the most popular frameworks in the ecosystem), the &lt;strong&gt;Simple Data Objects&lt;/strong&gt; library itself is completely framework-independent! It works great on any other framework (Symfony, Slim, for example) or even on plain (vanilla) PHP.&lt;/p&gt;

&lt;p&gt;That said, the &lt;strong&gt;Laravel&lt;/strong&gt; ecosystem gets powerful, opt-in support for a lot of native features! For example, you can automatically inject a DTO directly into a controller method instead of a standard Request object, use a DTO as an Eloquent attribute cast, plug in Livewire integration (&lt;code&gt;WireableData&lt;/code&gt;), and use ready-made Artisan commands to warm the metadata cache (&lt;code&gt;sdo:warm&lt;/code&gt;) or generate TypeScript. All of these extras are opt-in and never weigh down the core package.&lt;/p&gt;

&lt;p&gt;One caveat: the core package still pulls in &lt;code&gt;illuminate/contracts&lt;/code&gt;, &lt;code&gt;illuminate/support&lt;/code&gt; and &lt;code&gt;illuminate/validation&lt;/code&gt; as hard dependencies (without a full Laravel application) — so on Symfony or Slim these packages will show up in your project's &lt;code&gt;composer.json&lt;/code&gt;, even if you never touch an &lt;code&gt;Illuminate\*&lt;/code&gt; class directly.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Data boundaries and flexible usage scenarios
&lt;/h2&gt;

&lt;p&gt;In real life, data arrives with varying levels of trust. SDO v2 offers a distinct approach, splitting the hydration API by scenario:&lt;/p&gt;

&lt;h3&gt;
  
  
  A. HTTP requests: fast fail-fast validation
&lt;/h3&gt;

&lt;p&gt;When a user submits a form, we need to check the rules quickly and, on failure, return a response:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;StdOut\SimpleDataObjects\Attributes\Rules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;StdOut\SimpleDataObjects\Attributes\InferRules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;InferRules&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// Automatically infers baseline rules from PHP types&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegisterData&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="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;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;#[Rules(['required', 'email', 'unique:users,email'])]&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;$email&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="c1"&gt;// Runs Laravel validation and throws ValidationException on failure&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegisterData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromValidated&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;h3&gt;
  
  
  B. Importing large files (CSV/JSON): error accumulation
&lt;/h3&gt;

&lt;p&gt;If you're importing 100,000 rows from a partner's CSV, failing on the first invalid row is a bad idea. You need to collect a detailed report of every error instead:&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CsvUsers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Returns a HydrationResult object instead of throwing&lt;/span&gt;
    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserImportData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromValidatedResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Store the error for the report&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;ImportUserJob&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// Process the successful DTO&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  C. Lazy typed collections
&lt;/h3&gt;

&lt;p&gt;So that huge imports don't eat up all the available memory, SDO v2 supports lazy hydration through generators:&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="c1"&gt;// Each item is only hydrated at the moment it's iterated over&lt;/span&gt;
&lt;span class="nv"&gt;$lazyCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserImportData&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;lazyCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CsvUsers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lazyCollection&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Process one DTO at a time, keeping memory usage flat&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. The SDO v2 feature map: everything in one place
&lt;/h2&gt;

&lt;p&gt;SDO v2 uses PHP attributes as the single source of truth for the IDE, static analysis, validation and serialization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pipes for input cleanup
&lt;/h3&gt;

&lt;p&gt;Pipes run &lt;strong&gt;before&lt;/strong&gt; the constructor and casts are invoked. They're ideal for mechanical normalization (trimming stray whitespace, lowercasing, and the like):&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;StdOut\SimpleDataObjects\Attributes\Pipe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;StdOut\SimpleDataObjects\Pipes\TrimStringsPipe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;StdOut\SimpleDataObjects\Pipes\NullifyEmptyStringsPipe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Pipe(TrimStringsPipe::class, NullifyEmptyStringsPipe::class)]&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContactData&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="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;$email&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;$note&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="c1"&gt;// An empty string becomes 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;h3&gt;
  
  
  Built-in and custom casts
&lt;/h3&gt;

&lt;p&gt;SDO ships with a large set of ready-made casts: &lt;code&gt;BooleanCast&lt;/code&gt;, &lt;code&gt;IntegerCast&lt;/code&gt;, &lt;code&gt;DateTimeImmutableCast&lt;/code&gt;, &lt;code&gt;MoneyCast&lt;/code&gt;, &lt;code&gt;JsonCast&lt;/code&gt;, &lt;code&gt;EncryptedCast&lt;/code&gt; (backed by Sodium), and others. You can just as easily write your own by implementing the &lt;code&gt;CastsValue&lt;/code&gt; contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  PATCH API: working with the &lt;code&gt;Optional&lt;/code&gt; type
&lt;/h3&gt;

&lt;p&gt;The eternal REST API problem: how do you tell the difference between a client sending &lt;code&gt;'email' =&amp;gt; null&lt;/code&gt; (they want to clear the field) and them not sending the &lt;code&gt;'email'&lt;/code&gt; key at all (they don't want to touch it)? SDO solves this elegantly:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;StdOut\SimpleDataObjects\Optional&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateProfileData&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="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="o"&gt;|&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt; &lt;span class="nv"&gt;$name&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="o"&gt;|&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt; &lt;span class="nv"&gt;$bio&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a key is missing from the request, the property receives an &lt;code&gt;Optional&lt;/code&gt; object instead. During subsequent serialization, such fields are automatically skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contextual serialization: &lt;code&gt;#[Hidden(except: [...])]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A classic problem: one DTO needs to serve both a public API and an admin panel, but not every field should be visible everywhere. Instead of duplicating classes or manually calling &lt;code&gt;except()&lt;/code&gt; in every controller:&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;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserData&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="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;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;#[Hidden(except: ['admin'])]&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;$email&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;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                  &lt;span class="c1"&gt;// no email&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// includes email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each context compiles into its own specialized serializer and is cached separately, the same way the default one is — no runtime branching per field.&lt;/p&gt;

&lt;h3&gt;
  
  
  One contract for both backend and frontend (TypeScript)
&lt;/h3&gt;

&lt;p&gt;No more manually keeping types in sync between PHP and JS/TS. SDO can generate JSON Schemas and TypeScript types directly from your DTOs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan sdo:typescript app/Data &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;resources/js/types/dto.d.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Performance: numbers you can verify
&lt;/h2&gt;

&lt;p&gt;SDO's performance is built on the principle of &lt;strong&gt;metadata compilation&lt;/strong&gt;. Instead of parsing classes through the heavy Reflection API on every request at runtime (as most libraries do), SDO analyzes classes once, compiles optimized PHP files for hydration, which get cached by OPcache and run at native-code speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparative benchmark: SDO vs spatie/laravel-data
&lt;/h3&gt;

&lt;p&gt;For a fair comparison, we built identical fixtures (the same field types, nested objects, and 20-item collections) and ran them in a clean Docker environment (&lt;code&gt;php:8.4-cli-alpine&lt;/code&gt;, PHP 8.4.23, Laravel 12.65, Spatie 4.23, OPcache + JIT (tracing) enabled for the CLI SAPI — the base image ships with both off). We measured the pure library overhead (no I/O, DB, or network).&lt;/p&gt;

&lt;p&gt;Here are the median throughput results (&lt;strong&gt;operations per second&lt;/strong&gt;, higher is better):&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 (SDO)&lt;/th&gt;
&lt;th&gt;spatie/laravel-data&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Flat hydration&lt;/strong&gt; (simple DTO)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,162,551&lt;/strong&gt; ops/s&lt;/td&gt;
&lt;td&gt;83,789 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.9×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nested hydration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3,070,989&lt;/strong&gt; ops/s&lt;/td&gt;
&lt;td&gt;94,773 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32.4×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Collection hydration&lt;/strong&gt; (20 items)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;183,041&lt;/strong&gt; ops/s&lt;/td&gt;
&lt;td&gt;10,570 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.3×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Date cast hydration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,347,300&lt;/strong&gt; ops/s&lt;/td&gt;
&lt;td&gt;119,791 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.2×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Flat serialization&lt;/strong&gt; (to array)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2,892,779&lt;/strong&gt; ops/s&lt;/td&gt;
&lt;td&gt;171,100 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.9×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nested serialization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;4,674,271&lt;/strong&gt; ops/s&lt;/td&gt;
&lt;td&gt;164,027 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28.5×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Collection serialization&lt;/strong&gt; (20 items)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;312,904&lt;/strong&gt; ops/s&lt;/td&gt;
&lt;td&gt;27,263 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.5×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CSV streaming&lt;/strong&gt; (100k rows)*&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;71,147&lt;/strong&gt; rows/s&lt;/td&gt;
&lt;td&gt;38,866 rows/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.83×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;*Note: the CSV-streaming gap is smaller because a large share of the time is spent parsing the file through native &lt;code&gt;fgetcsv&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CPU load and memory usage
&lt;/h3&gt;

&lt;p&gt;Speed also means smaller server bills. Let's look at pure CPU time (&lt;strong&gt;CPU microseconds per operation&lt;/strong&gt;, lower is better):&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;SDO CPU us/op&lt;/th&gt;
&lt;th&gt;laravel-data CPU us/op&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flat hydration&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.84 us&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11.86 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested hydration&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.34 us&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10.63 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collection hydration&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.45 us&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96.64 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And memory usage (&lt;strong&gt;retained memory per kept result&lt;/strong&gt;, lower is better)?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flat hydration&lt;/strong&gt;: SDO uses &lt;strong&gt;517 bytes/op&lt;/strong&gt; vs &lt;strong&gt;533 bytes/op&lt;/strong&gt; for Spatie.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Smart trade-offs
&lt;/h2&gt;

&lt;p&gt;An engineering approach demands honesty. Our benchmark compares against &lt;code&gt;spatie/laravel-data&lt;/code&gt;, a very popular and mature library. That package deserves enormous respect in the PHP community. In fact, while designing and building Simple Data Objects, I was partly inspired by Spatie's own decisions — their attention to detail and excellent developer experience (DX).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spatie/laravel-data&lt;/code&gt; is an extremely powerful tool with deep integration into the Laravel ecosystem. If you need highly complex dynamic transformations, on-the-fly magic type resolution, or ready-made solutions for many Laravel-specific components out of the box — Spatie remains a great choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple Data Objects&lt;/strong&gt;, on the other hand, was designed from day one under a completely different philosophy — it's &lt;strong&gt;tuned for uncompromising performance and low runtime overhead&lt;/strong&gt;. If your hot paths (high-throughput APIs, queue workers, parsing large files, or data imports) suffer from noticeable CPU load and high memory usage — SDO offers a unique alternative. You get a strict, safe, and blazing-fast contract at a minimal execution cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself!
&lt;/h2&gt;

&lt;p&gt;All code examples, Docker configs, and the full test tooling live in a dedicated &lt;strong&gt;companion repository&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/std-out/simple-data-objects-benchmark" rel="noopener noreferrer"&gt;simple-data-objects-benchmark on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can fork it, run the benchmarks on your own hardware, or swap in your own payload shapes to see the difference in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/std-out/simple-data-objects-benchmark.git
&lt;span class="nb"&gt;cd &lt;/span&gt;simple-data-objects-benchmark
make bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'd love to hear your feedback, pull requests, or a star on GitHub!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package GitHub:&lt;/strong&gt; &lt;a href="https://github.com/std-out/simple-data-objects" rel="noopener noreferrer"&gt;std-out/simple-data-objects&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://github.com/std-out/simple-data-objects" rel="noopener noreferrer"&gt;Simple Data Objects Docs&lt;/a&gt; (or on Packagist)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrading from v1?&lt;/strong&gt; The changelog and migration notes are in &lt;a href="https://github.com/std-out/simple-data-objects/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  What do you think of this approach to compiling DTOs in PHP? Share your experience in the comments!
&lt;/h3&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>webdev</category>
      <category>dto</category>
    </item>
    <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>
