{"id":17191660,"url":"https://github.com/fab2s/dt0","last_synced_at":"2026-02-09T00:07:18.451Z","repository":{"id":229494218,"uuid":"776882220","full_name":"fab2s/dt0","owner":"fab2s","description":"Dt0 is a PHP implementation of a DTO that can both secure mutability and implement convenient ways to take control over input and output in various formats.","archived":false,"fork":false,"pushed_at":"2025-04-12T16:21:23.000Z","size":76,"stargazers_count":4,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-24T12:21:24.890Z","etag":null,"topics":["data-transfer-object","dto","immutability","immutable","laravel","php","readonly"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fab2s.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-03-24T17:50:03.000Z","updated_at":"2024-05-09T14:44:21.000Z","dependencies_parsed_at":"2024-04-25T23:27:22.691Z","dependency_job_id":"2f22a277-99bd-4418-8a10-3e7410030b68","html_url":"https://github.com/fab2s/dt0","commit_stats":null,"previous_names":["fab2s/dt0"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/fab2s/dt0","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fab2s%2Fdt0","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fab2s%2Fdt0/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fab2s%2Fdt0/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fab2s%2Fdt0/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fab2s","download_url":"https://codeload.github.com/fab2s/dt0/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fab2s%2Fdt0/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279006109,"owners_count":26084026,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-11T02:00:06.511Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-transfer-object","dto","immutability","immutable","laravel","php","readonly"],"created_at":"2024-10-15T01:27:00.884Z","updated_at":"2026-02-09T00:07:18.434Z","avatar_url":"https://github.com/fab2s.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dt0\n\n[![CI](https://github.com/fab2s/dt0/actions/workflows/ci.yml/badge.svg)](https://github.com/fab2s/dt0/actions/workflows/ci.yml) [![QA](https://github.com/fab2s/dt0/actions/workflows/qa.yml/badge.svg)](https://github.com/fab2s/dt0/actions/workflows/qa.yml) [![codecov](https://codecov.io/gh/fab2s/dt0/graph/badge.svg?token=VRX16UUB7Y)](https://codecov.io/gh/fab2s/dt0) [![PHPStan](https://img.shields.io/badge/PHPStan-level%209-brightgreen.svg?style=flat)](https://phpstan.org/) [![Latest Stable Version](http://poser.pugx.org/fab2s/dt0/v)](https://packagist.org/packages/fab2s/dt0) [![Total Downloads](http://poser.pugx.org/fab2s/dt0/downloads)](https://packagist.org/packages/fab2s/dt0) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) [![License](http://poser.pugx.org/fab2s/dt0/license)](https://packagist.org/packages/fab2s/dt0)\n\n`Dt0` (_DeeTO_ or _DeTZerO_) is a PHP 8.1+ [Data Transfer Object](https://en.wikipedia.org/wiki/Data_transfer_object) implementation built for true immutability through `readonly` properties, with powerful bidirectional casting and validation.\n\n## Table of Contents\n\n- [Why Dt0](#why-dt0)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Creating Instances](#creating-instances)\n  - [Factory Methods](#factory-methods)\n  - [Using Constructors](#using-constructors)\n  - [`new` vs Factory Methods](#new-vs-factory-methods)\n- [Output](#output)\n  - [Output Filtering](#output-filtering)\n- [Immutable Operations](#immutable-operations)\n- [Casting](#casting)\n  - [Property-Level Casting](#property-level-casting)\n  - [Bidirectional Casting](#bidirectional-casting)\n  - [Class-Level Casting](#class-level-casting)\n  - [Available Casters](#available-casters)\n  - [Built-in Type Support](#built-in-type-support)\n  - [Custom Casters](#custom-casters)\n- [Property Renaming](#property-renaming)\n- [Default Values](#default-values)\n- [Attribute Inheritance](#attribute-inheritance)\n  - [Property Attribute Inheritance](#property-attribute-inheritance)\n  - [Class Attribute Inheritance](#class-attribute-inheritance)\n- [Validation](#validation)\n- [Type System Integration](#type-system-integration)\n- [Extending Attributes](#extending-attributes)\n- [Performance](#performance)\n- [Exceptions](#exceptions)\n- [Requirements](#requirements)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Why Dt0\n\n**Real immutability, enforced by PHP.** Most DTO packages simulate immutability with magic methods. Dt0 uses native `readonly` properties - the language itself prevents modifications. Accidental writes cause fatal errors, not silent bugs.\n\n**One attribute to rule them all.** Where other packages require a dozen attributes for casting, defaults, and renaming, Dt0's `#[Cast]` handles input transformation, output formatting, defaults, and property renaming in a single, composable attribute.\n\n**Framework-agnostic core.** Use it anywhere PHP runs. For Laravel projects, [laravel-dt0](https://github.com/fab2s/laravel-dt0) adds validation and model casting integration.\n\n**Compiled once, fast always.** Reflection and attribute metadata are processed once per class, then cached. Every subsequent instantiation reuses compiled data with zero reflection overhead.\n\n```php\n// One attribute does it all\n#[Cast(\n    in: DateTimeCaster::class,              // Transform on input\n    out: new DateTimeFormatCaster('Y-m-d'), // Format on output\n    both: JsonCaster::class,                // Same caster for both directions\n    default: new DateTime(),                // Default value\n    renameFrom: 'created_at',              // Accept external name\n)]\npublic readonly DateTime $createdAt;\n```\n\n**Flexible, not dogmatic.** While immutability is the core feature, Dt0 doesn't force it. Use mutable properties when needed. Expose protected properties via `with()`. The package provides capabilities; you decide how to use them.\n\n## Installation\n\n```shell\ncomposer require fab2s/dt0\n```\n\nFor Laravel, see [Laravel Dt0](https://github.com/fab2s/laravel-dt0) for validation and model attribute casting integration.\n\n## Quick Start\n\n```php\nuse fab2s\\Dt0\\Dt0;\nuse fab2s\\Dt0\\Attribute\\Cast;\n\nclass UserDto extends Dt0\n{\n    public readonly int $id;\n    public readonly string $name;\n    public readonly string $email;\n\n    #[Cast(default: 'user')]\n    public readonly string $role;\n}\n\n// Create with named arguments\n$user = UserDto::make(\n    id: 42,\n    name: 'John Doe',\n    email: 'john@example.com',\n);\n\n// Access properties\n$user-\u003eid;    // 42\n$user-\u003ename;  // 'John Doe'\n$user-\u003erole;  // 'user' (default applied)\n\n// Convert to array/JSON\n$user-\u003etoArray();  // ['id' =\u003e 42, 'name' =\u003e 'John Doe', 'email' =\u003e 'john@example.com', 'role' =\u003e 'user']\n$user-\u003etoJson();   // {\"id\":42,\"name\":\"John Doe\",\"email\":\"john@example.com\",\"role\":\"user\"}\n\n// Immutable update\n$admin = $user-\u003eupdate(role: 'admin');\n$user-\u003erole;   // 'user' (unchanged)\n$admin-\u003erole;  // 'admin' (new instance)\n```\n\n## Creating Instances\n\n### Factory Methods\n\nDt0 provides multiple ways to create instances:\n\n```php\n// Named arguments - order doesn't matter\n$dto = UserDto::make(email: 'a@b.com', name: 'John', id: 1);\n\n// From associative array\n$dto = UserDto::fromArray([\n    'id'    =\u003e 1,\n    'name'  =\u003e 'John',\n    'email' =\u003e 'john@example.com',\n]);\n\n// From JSON string\n$dto = UserDto::fromJson('{\"id\": 1, \"name\": \"John\", \"email\": \"john@example.com\"}');\n\n// Polymorphic - accepts array, JSON string, or existing Dt0 instance\n$dto = UserDto::from($input);       // throws Dt0Exception on failure\n$dto = UserDto::tryFrom($input);    // returns null on failure\n\n// From gzipped JSON\n$dto = UserDto::fromGz($gzippedData);\n```\n\n| Method | Input | On Failure |\n|--------|-------|------------|\n| `make(...$args)` | Named/positional args | Throws |\n| `fromArray(array)` | Associative array | Throws |\n| `fromJson(string)` | JSON string | Throws |\n| `fromString(string)` | JSON string (alias) | Throws |\n| `fromGz(string)` | Gzipped JSON | Throws |\n| `from(mixed)` | Array, JSON, or Dt0 | Throws |\n| `tryFrom(mixed)` | Array, JSON, or Dt0 | Returns `null` |\n\n### Using Constructors\n\nDt0 classes can have custom constructors with promoted properties:\n\n```php\nclass OrderDto extends Dt0\n{\n    public readonly string $notes;\n    public readonly float $total;\n\n    public function __construct(\n        public readonly string $orderId,\n\n        #[Cast(in: DateTimeCaster::class)]\n        public readonly DateTime $placedAt,\n\n        // Non-promoted parameters can also be casted\n        #[Cast(in: ScalarCaster::class)]\n        float $subtotal = 0,\n\n        // Required: captures remaining args for other properties\n        mixed ...$args,\n    ) {\n        // Custom logic here\n        $this-\u003etotal = $subtotal * 1.2; // Add tax\n\n        parent::__construct(...$args);\n    }\n}\n\n// Constructor parameters maintain their order\n$order = new OrderDto(\n    orderId: 'ORD-123',\n    placedAt: new DateTime(),\n    subtotal: 100.00,\n    notes: 'Gift wrap please',  // Goes to props via ...$args\n);\n\n// Factory methods don't care about order\n$order = OrderDto::make(\n    notes: 'Gift wrap please',\n    subtotal: 100.00,\n    orderId: 'ORD-123',\n    placedAt: '2024-01-15 10:30:00',\n);\n```\n\n### `new` vs Factory Methods\n\nWhen using `new` directly with **promoted readonly properties that have a default value**, PHP initializes them immediately, **before** Dt0 can apply casting. Promoted properties without defaults behave normally.\n\n```php\nclass EventDto extends Dt0\n{\n    public function __construct(\n        // Has default = casting won't apply with `new`\n        #[Cast(in: DateTimeCaster::class)]\n        public readonly DateTime $date = new DateTime(),\n\n        // No default = casting works fine with `new`\n        #[Cast(in: DateTimeCaster::class)]\n        public readonly DateTime $endDate,\n\n        mixed ...$args,\n    ) {\n        parent::__construct(...$args);\n    }\n}\n\n// ❌ Casting won't apply to $date (has default)\n$event = new EventDto(date: '2024-01-15', endDate: new DateTime());  // TypeError for $date\n\n// ✅ Casting works for $endDate (no default)\n$event = new EventDto(endDate: '2024-01-15');  // Works, $date uses its default\n\n// ✅ Factory methods always work - casting applies to all properties\n$event = EventDto::make(date: '2024-01-15', endDate: '2024-01-16');  // Both cast correctly\n```\n\n**Best practice**: Use factory methods (`make`, `from`, `fromArray`, etc.) for full casting support. Reserve `new` for cases where you're passing already-correct types or relying on defaults.\n\n## Output\n\n```php\n$dto-\u003etoArray();      // Array with objects intact\n$dto-\u003etoJsonArray();  // Array with objects serialized (JsonSerializable called)\n$dto-\u003ejsonSerialize();// Same as toJsonArray()\n$dto-\u003etoJson();       // JSON string\n$dto-\u003etoGz();         // Gzipped JSON string\njson_encode($dto);    // JSON string (implements JsonSerializable)\n(string) $dto;        // JSON string (implements Stringable)\n```\n\n### Output Filtering\n\nControl which properties appear in output using `with()`, `without()`, and `only()`.\n\n#### Adding Properties with `with()`\n\nBy default, only **public** properties are included in output. Use `with()` to add protected properties, call getters, or create computed values.\n\n**Include a protected property:**\n\n```php\nclass UserDto extends Dt0\n{\n    public readonly int $id;\n    public readonly string $name;\n    protected string $internalScore;\n\n    public function setInternalScore(string $score): static\n    {\n        $this-\u003einternalScore = $score;\n        return $this;\n    }\n}\n\n$user = UserDto::make(id: 1, name: 'John');\n$user-\u003esetInternalScore('A+');\n\n$user-\u003etoArray();  // ['id' =\u003e 1, 'name' =\u003e 'John'] - no internalScore\n\n$user-\u003ewith('internalScore')-\u003etoArray();\n// ['id' =\u003e 1, 'name' =\u003e 'John', 'internalScore' =\u003e 'A+']\n```\n\n**Call a getter method:**\n\n```php\nclass ProductDto extends Dt0\n{\n    public readonly int $price;\n    public readonly int $quantity;\n\n    public function getTotal(): int\n    {\n        return $this-\u003eprice * $this-\u003equantity;\n    }\n}\n\n$product = ProductDto::make(price: 100, quantity: 3);\n\n// with('total', true) calls getTotal() automatically\n$product-\u003ewith('total', true)-\u003etoArray();\n// ['price' =\u003e 100, 'quantity' =\u003e 3, 'total' =\u003e 300]\n\n// Or specify a custom method name\n$product-\u003ewith('total', 'getTotal')-\u003etoArray();\n// Same result\n```\n\n**Add computed values with closures:**\n\n```php\nclass PersonDto extends Dt0\n{\n    public readonly string $firstName;\n    public readonly string $lastName;\n}\n\n$person = PersonDto::make(firstName: 'John', lastName: 'Doe');\n\n$person-\u003ewith('fullName', fn(PersonDto $dto) =\u003e \"{$dto-\u003efirstName} {$dto-\u003elastName}\")\n    -\u003etoArray();\n// ['firstName' =\u003e 'John', 'lastName' =\u003e 'Doe', 'fullName' =\u003e 'John Doe']\n```\n\n**Declarative with `#[With]` attribute:**\n\n```php\nuse fab2s\\Dt0\\Attribute\\With;\nuse fab2s\\Dt0\\Attribute\\WithProp;\n\n#[With(\n    new WithProp(name: 'internalScore'),\n    new WithProp(name: 'total', getter: 'getTotal'),\n)]\nclass OrderDto extends Dt0\n{\n    public readonly int $price;\n    public readonly int $quantity;\n    protected string $internalScore = 'pending';\n\n    public function getTotal(): int\n    {\n        return $this-\u003eprice * $this-\u003equantity;\n    }\n}\n\n$order = OrderDto::make(price: 50, quantity: 2);\n$order-\u003etoArray();\n// ['price' =\u003e 50, 'quantity' =\u003e 2, 'internalScore' =\u003e 'pending', 'total' =\u003e 100]\n```\n\n**`with()` getter options:**\n\n| Call | Behavior |\n|------|----------|\n| `with('name')` | Access `$this-\u003ename` directly |\n| `with('name', false)` | Access `$this-\u003ename` directly |\n| `with('name', true)` | Call `$this-\u003egetName()` |\n| `with('name', 'customMethod')` | Call `$this-\u003ecustomMethod()` |\n| `with('name', fn($dto) =\u003e ...)` | Call the closure with `$this` |\n\n#### Excluding Properties with `without()`\n\n```php\nclass UserDto extends Dt0\n{\n    public readonly int $id;\n    public readonly string $name;\n    public readonly string $password;\n    public readonly string $apiKey;\n}\n\n$user = UserDto::make(/* ... */);\n\n$user-\u003ewithout('password', 'apiKey')-\u003etoJson();\n// {\"id\":1,\"name\":\"John\"}\n```\n\n#### Selecting Specific Properties with `only()`\n\n```php\n$user-\u003eonly('id', 'name')-\u003etoArray();\n// ['id' =\u003e 1, 'name' =\u003e 'John']\n```\n\n#### Resetting Filters\n\n```php\n$user-\u003eclearWith();     // Remove all with() additions\n$user-\u003eclearWithout();  // Remove all without() exclusions\n```\n\n## Immutable Operations\n\n```php\n// Clone creates an identical copy\n$copy = $dto-\u003eclone();\n$dto-\u003eequals($copy);  // true\n\n// Update creates a new instance with changed values\n$updated = $dto-\u003eupdate(name: 'Jane', role: 'admin');\n$dto-\u003eequals($updated);  // false\n\n// Original unchanged\n$dto-\u003ename;      // 'John'\n$updated-\u003ename;  // 'Jane'\n\n// Compare instances\n$dto-\u003eequals($other);  // true if all properties match\n\n// Serialization round-trip\n$restored = unserialize(serialize($dto));\n$dto-\u003eequals($restored);  // true\n```\n\n## Casting\n\nDt0 supports bidirectional casting: transform values on the way **in** (hydration) and **out** (serialization).\n\n### Property-Level Casting\n\nUse the `#[Cast]` attribute on individual properties:\n\n```php\nuse fab2s\\Dt0\\Dt0;\nuse fab2s\\Dt0\\Attribute\\Cast;\nuse fab2s\\Dt0\\Caster\\DateTimeCaster;\nuse fab2s\\Dt0\\Caster\\DateTimeFormatCaster;\nuse fab2s\\Dt0\\Caster\\ScalarCaster;\nuse fab2s\\Dt0\\Caster\\ScalarType;\n\nclass ArticleDto extends Dt0\n{\n    public readonly string $title;\n\n    #[Cast(in: new ScalarCaster(ScalarType::int))]\n    public readonly int $viewCount;\n\n    #[Cast(\n        in: DateTimeCaster::class,                                // string -\u003e DateTime\n        out: new DateTimeFormatCaster(DateTimeFormatCaster::ISO), // DateTime -\u003e ISO string\n    )]\n    public readonly DateTime $publishedAt;\n\n    #[Cast(\n        in: DateTimeCaster::class,\n        out: new DateTimeFormatCaster('Y-m-d'),  // Custom format\n    )]\n    public readonly ?DateTime $updatedAt;\n}\n\n$article = ArticleDto::make(\n    title: 'Hello World',\n    viewCount: '42',              // String cast to int\n    publishedAt: '2024-01-15',    // String cast to DateTime\n    updatedAt: null,\n);\n\n$article-\u003eviewCount;              // 42 (int)\n$article-\u003epublishedAt;            // DateTime instance\n\n$article-\u003etoArray();\n// ['title' =\u003e 'Hello World', 'viewCount' =\u003e 42, 'publishedAt' =\u003e DateTime, 'updatedAt' =\u003e null]\n\n$article-\u003ejsonSerialize();\n// ['title' =\u003e 'Hello World', 'viewCount' =\u003e 42, 'publishedAt' =\u003e '2024-01-15T00:00:00.000000Z', 'updatedAt' =\u003e null]\n```\n\n### Bidirectional Casting\n\nWhen a caster applies to both input and output, use the `both` parameter instead of repeating the same caster for `in` and `out`:\n\n```php\nuse fab2s\\Dt0\\Dt0;\nuse fab2s\\Dt0\\Attribute\\Cast;\nuse fab2s\\Dt0\\Caster\\JsonCaster;\nuse fab2s\\Dt0\\Caster\\Base64Caster;\n\nclass PayloadDto extends Dt0\n{\n    #[Cast(both: JsonCaster::class)]\n    public readonly array $metadata;\n\n    #[Cast(both: Base64Caster::class)]\n    public readonly string $data;\n}\n```\n\n`both` can be combined with `in` and/or `out` for layered casting. When combined, casters are chained using onion ordering:\n\n- **Input:** `both` → `in`\n- **Output:** `out` → `both`\n\n```php\nuse fab2s\\Dt0\\Attribute\\Cast;\nuse fab2s\\Dt0\\Caster\\DateTimeCaster;\nuse fab2s\\Dt0\\Caster\\DateTimeFormatCaster;\nuse fab2s\\Dt0\\Caster\\TrimCaster;\n\nclass EventDto extends Dt0\n{\n    #[Cast(\n        both: new TrimCaster,                          // Trims on input AND output\n        in: DateTimeCaster::class,                     // Input: trim → parse DateTime\n        out: new DateTimeFormatCaster('Y-m-d H:i:s'), // Output: format → trim\n    )]\n    public readonly DateTime $startsAt;\n}\n```\n\n### Class-Level Casting\n\nDefine multiple casts at the class level with `#[Casts]`:\n\n```php\nuse fab2s\\Dt0\\Dt0;\nuse fab2s\\Dt0\\Attribute\\Casts;\nuse fab2s\\Dt0\\Attribute\\Cast;\nuse fab2s\\Dt0\\Caster\\DateTimeCaster;\n\n#[Casts(\n    // Using named arguments (property name =\u003e Cast)\n    status: new Cast(default: 'pending'),\n    priority: new Cast(default: 0),\n    createdAt: new Cast(in: DateTimeCaster::class),\n\n    // Or using positional with explicit propName\n    new Cast(default: false, propName: 'isArchived'),\n)]\nclass TaskDto extends Dt0\n{\n    public readonly string $title;\n    public readonly string $status;\n    public readonly int $priority;\n    public readonly DateTime $createdAt;\n    public readonly bool $isArchived;\n}\n\n$task = TaskDto::make(title: 'Review PR', createdAt: 'now');\n$task-\u003estatus;     // 'pending'\n$task-\u003epriority;   // 0\n$task-\u003eisArchived; // false\n```\n\n**Combining class and property casts**: You can use both. In case of overlap, property-level `#[Cast]` takes precedence over class-level `#[Casts]`.\n\n```php\n#[Casts(\n    name: new Cast(default: 'Anonymous'),  // Fallback if no property-level Cast\n)]\nclass PersonDto extends Dt0\n{\n    #[Cast(default: 'Unknown')]  // Takes precedence\n    public readonly string $name;\n\n    #[Cast(default: 0)]  // Applied (no conflict)\n    public readonly int $age;\n}\n```\n\n### Available Casters\n\n| Caster | Description |\n|--------|-------------|\n| [`ScalarCaster`](./src/Caster/ScalarCaster.php) | Cast to `int`, `float`, `bool`, `string` |\n| [`JsonCaster`](./src/Caster/JsonCaster.php) | Decode JSON on input, encode on output |\n| [`TrimCaster`](./src/Caster/TrimCaster.php) | Trim strings (supports `ltrim`, `rtrim`, custom characters) |\n| [`Base64Caster`](./src/Caster/Base64Caster.php) | Decode base64 on input, encode on output |\n| [`DateTimeCaster`](./src/Caster/DateTimeCaster.php) | Parse strings/arrays to `DateTime` or `DateTimeImmutable` |\n| [`DateTimeFormatCaster`](./src/Caster/DateTimeFormatCaster.php) | Format DateTime for output |\n| [`CarbonCaster`](./src/Caster/CarbonCaster.php) | Parse to Carbon (requires `nesbot/carbon`) |\n| [`Dt0Caster`](./src/Caster/Dt0Caster.php) | Cast to nested Dt0 instances |\n| [`ArrayOfCaster`](./src/Caster/ArrayOfCaster.php) | Cast arrays of typed items |\n| [`ClassCaster`](./src/Caster/ClassCaster.php) | Instantiate arbitrary classes |\n| [`MathCaster`](./src/Caster/MathCaster.php) | Precision numbers (requires `fab2s/math`) |\n| [`CasterCollection`](./src/Caster/CasterCollection.php) | Chain multiple casters in a pipeline |\n\nSee [Casters Documentation](./docs/casters.md) for detailed usage of each caster.\n\n### Built-in Type Support\n\nThese types are handled automatically without explicit casters:\n\n**Enums** - Both `UnitEnum` and `BackedEnum`:\n\n```php\nenum Status: string {\n    case Draft = 'draft';\n    case Published = 'published';\n}\n\nclass PostDto extends Dt0\n{\n    public readonly string $title;\n    public readonly Status $status;  // No caster needed\n}\n\n$post = PostDto::make(title: 'Hello', status: 'published');\n$post-\u003estatus;           // Status::Published\n$post-\u003ejsonSerialize();  // ['title' =\u003e 'Hello', 'status' =\u003e 'published']\n```\n\n**Nested Dt0** - Child Dt0 classes are recognized automatically:\n\n```php\nclass AddressDto extends Dt0\n{\n    public readonly string $street;\n    public readonly string $city;\n}\n\nclass PersonDto extends Dt0\n{\n    public readonly string $name;\n    public readonly AddressDto $address;  // No caster needed\n}\n\n$person = PersonDto::make(\n    name: 'John',\n    address: ['street' =\u003e '123 Main St', 'city' =\u003e 'Boston'],\n);\n\n$person-\u003eaddress-\u003ecity;  // 'Boston'\n```\n\n### Custom Casters\n\nImplement [`CasterInterface`](./src/Caster/CasterInterface.php) or extend [`CasterAbstract`](./src/Caster/CasterAbstract.php):\n\n```php\nuse fab2s\\Dt0\\Caster\\CasterAbstract;\nuse fab2s\\Dt0\\Dt0;\n\nclass UpperCaseCaster extends CasterAbstract\n{\n    public function cast(mixed $value, array|Dt0|null $data = null): ?string\n    {\n        return is_string($value) ? strtoupper($value) : null;\n    }\n}\n```\n\nThe `$data` parameter provides context:\n- On **input**: The full input array being hydrated\n- On **output**: The Dt0 instance being serialized\n\nThis enables casters that need multiple values:\n\n```php\nclass FullNameCaster extends CasterAbstract\n{\n    public function cast(mixed $value, array|Dt0|null $data = null): ?string\n    {\n        if (is_array($data)) {\n            // Input: combine first and last name\n            return trim(($data['firstName'] ?? '') . ' ' . ($data['lastName'] ?? ''));\n        }\n\n        if ($data instanceof Dt0) {\n            // Output: same logic with object access\n            return trim($data-\u003efirstName . ' ' . $data-\u003elastName);\n        }\n\n        return $value;\n    }\n}\n```\n\nSee [Casters Documentation](./docs/casters.md) for more examples.\n\n## Property Renaming\n\nMap between external names (APIs, databases) and internal property names:\n\n```php\nclass ApiResponseDto extends Dt0\n{\n    #[Cast(\n        renameFrom: 'created_at',  // Accept this name on input\n        renameTo: 'createdAtStr',     // Use this name on output\n    )]\n    public readonly string $createdAt;\n\n    #[Cast(renameFrom: 'user_id')]\n    public readonly int $userId;\n}\n\n// Input uses external names\n$dto = ApiResponseDto::make(\n    created_at: '2024-01-15',\n    user_id: 42,\n);\n\n// Properties use internal names\n$dto-\u003ecreatedAt;  // '2024-01-15'\n$dto-\u003euserId;     // 42\n\n// Output uses renamed keys\n$dto-\u003etoArray();  // ['createdAtStr' =\u003e '2024-01-15', 'userId' =\u003e 42]\n```\n\n**Multiple input aliases** - Accept several names for the same property:\n\n```php\nclass UserDto extends Dt0\n{\n    // First match wins\n    #[Cast(renameFrom: ['user_name', 'username', 'login', 'userName'])]\n    public readonly string $userName;\n}\n\n// All of these work\nUserDto::make(user_name: 'john');\nUserDto::make(username: 'john');\nUserDto::make(login: 'john');\nUserDto::make(userName: 'john');\n```\n\n**Round-trip consistency**: All `renameTo` values are automatically added to `renameFrom`, ensuring output can always be used as input:\n\n```php\n$dto = ApiResponseDto::make(created_at: '2024-01-15', user_id: 42);\n$array = $dto-\u003etoArray();  // Uses renameTo keys\n\n// This always works\n$dto-\u003eequals(ApiResponseDto::fromArray($array));  // true\n```\n\n## Default Values\n\nReadonly properties can't have default values unless they're promoted constructor parameters. Casts solve this:\n\n```php\nclass ConfigDto extends Dt0\n{\n    #[Cast(default: 3600)]\n    public readonly int $ttl;\n\n    #[Cast(default: null)]\n    public readonly ?string $prefix;\n\n    #[Cast(default: [])]\n    public readonly array $tags;\n\n    #[Cast(default: true)]\n    public readonly bool $enabled;\n}\n\n$config = ConfigDto::make();  // No arguments needed\n$config-\u003ettl;      // 3600\n$config-\u003eprefix;   // null\n$config-\u003etags;     // []\n$config-\u003eenabled;  // true\n\n// Override defaults\n$config = ConfigDto::make(ttl: 7200, enabled: false);\n$config-\u003ettl;      // 7200\n$config-\u003eenabled;  // false\n```\n\n**Default resolution order**:\n1. Value provided during instantiation\n2. Default from `Cast` attribute\n3. Default from type (nullable types default to `null`)\n4. Default from promoted constructor parameter\n\n### The Nil Concept\n\nPHP has no native way to express \"never set\" vs \"set to null\". Dt0 uses a null byte (`\"\\0\"`) internally as a sentinel to distinguish these states. This means any value except `\"\\0\"` can be used as a default.\n\nIf you genuinely need `\"\\0\"` as a default value (extremely rare), use a promoted constructor parameter instead.\n\n## Attribute Inheritance\n\nDt0 supports attribute inheritance across class hierarchies, enabling powerful patterns for code reuse.\n\n### Property Attribute Inheritance\n\nWhen a property doesn't have an attribute, Dt0 walks up the parent class chain looking for the same property with that attribute. This is particularly useful for base DTOs:\n\n```php\nuse fab2s\\Dt0\\Dt0;\nuse fab2s\\Dt0\\Attribute\\Cast;\nuse fab2s\\Dt0\\Caster\\DateTimeCaster;\nuse fab2s\\Dt0\\Caster\\DateTimeFormatCaster;\n\n// Base DTO with common timestamp handling\nclass TimestampedDto extends Dt0\n{\n    #[Cast(\n        in: DateTimeCaster::class,\n        out: new DateTimeFormatCaster(DateTimeFormatCaster::ISO),\n    )]\n    public readonly DateTime $createdAt;\n\n    #[Cast(\n        in: DateTimeCaster::class,\n        out: new DateTimeFormatCaster(DateTimeFormatCaster::ISO),\n    )]\n    public readonly ?DateTime $updatedAt;\n}\n\n// Child inherits the Cast attributes automatically\nclass ArticleDto extends TimestampedDto\n{\n    public readonly string $title;\n    public readonly string $content;\n\n    // createdAt and updatedAt inherit their Cast from TimestampedDto\n    // Prior to PHP 8.4, you need to redeclare the properties:\n    public readonly DateTime $createdAt;\n    public readonly ?DateTime $updatedAt;\n}\n\n$article = ArticleDto::make(\n    title: 'Hello',\n    content: 'World',\n    createdAt: '2024-01-15 10:30:00',  // String -\u003e DateTime via inherited Cast\n    updatedAt: null,\n);\n\n$article-\u003ecreatedAt;      // DateTime instance\n$article-\u003ejsonSerialize();\n// createdAt formatted as ISO string thanks to inherited 'out' caster\n```\n\n\u003e **PHP 8.4+**: Property hooks make inheritance even cleaner. You no longer need to redeclare parent properties in child classes - they're inherited automatically along with their attributes. The examples above show property redeclaration for compatibility with PHP 8.1-8.3.\n\n**Override inherited attributes** - Child classes can override parent attributes:\n\n```php\nclass TimestampedDto extends Dt0\n{\n    #[Cast(\n        in: DateTimeCaster::class,\n        out: new DateTimeFormatCaster(DateTimeFormatCaster::ISO),\n    )]\n    public readonly DateTime $createdAt;\n}\n\nclass CustomArticleDto extends TimestampedDto\n{\n    public readonly string $title;\n\n    // Override with different output format\n    #[Cast(\n        in: DateTimeCaster::class,\n        out: new DateTimeFormatCaster('Y-m-d'),  // Different format\n    )]\n    public readonly DateTime $createdAt;\n}\n```\n\n**Multi-level inheritance** - Attributes are resolved up the entire chain:\n\n```php\nclass BaseDto extends Dt0\n{\n    #[Cast(default: 'active')]\n    public readonly string $status;\n}\n\nclass MiddleDto extends BaseDto\n{\n    // status inherits Cast from BaseDto\n    public readonly string $status;  // Redeclare for PHP \u003c 8.4\n    public readonly string $type;\n}\n\nclass FinalDto extends MiddleDto\n{\n    // status still inherits Cast from BaseDto (through MiddleDto)\n    public readonly string $status;  // Redeclare for PHP \u003c 8.4\n    public readonly string $type;    // Redeclare for PHP \u003c 8.4\n    public readonly string $name;\n}\n\n$dto = FinalDto::make(name: 'Test', type: 'example');\n$dto-\u003estatus;  // 'active' (default inherited from BaseDto)\n```\n\n### Class Attribute Inheritance\n\nClass-level attributes (`#[Casts]`, `#[With]`, `#[Validate]`, `#[Rules]`) also inherit from parent classes:\n\n```php\nuse fab2s\\Dt0\\Attribute\\Casts;\nuse fab2s\\Dt0\\Attribute\\Cast;\nuse fab2s\\Dt0\\Attribute\\Validate;\n\n#[Casts(\n    status: new Cast(default: 'pending'),\n)]\n#[Validate(BaseValidator::class)]\nclass BaseTaskDto extends Dt0\n{\n    public readonly string $title;\n    public readonly string $status;\n}\n\n// Inherits Casts and Validate from BaseTaskDto\nclass PriorityTaskDto extends BaseTaskDto\n{\n    public readonly string $title;   // Redeclare for PHP \u003c 8.4\n    public readonly string $status;  // Redeclare for PHP \u003c 8.4\n\n    #[Cast(default: 0)]\n    public readonly int $priority;\n}\n\n$task = PriorityTaskDto::make(title: 'Review PR');\n$task-\u003estatus;    // 'pending' (from inherited Casts)\n$task-\u003epriority;  // 0 (from own Cast)\n```\n\n**Override class attributes** - Define the attribute on the child to override:\n\n```php\n#[Casts(\n    status: new Cast(default: 'pending'),\n)]\nclass BaseTaskDto extends Dt0\n{\n    public readonly string $status;\n}\n\n#[Casts(\n    status: new Cast(default: 'urgent'),  // Override parent's default\n)]\nclass UrgentTaskDto extends BaseTaskDto\n{\n    public readonly string $status;  // Redeclare for PHP \u003c 8.4\n}\n\n$task = UrgentTaskDto::make();\n$task-\u003estatus;  // 'urgent'\n```\n\n## Validation\n\nDt0 provides validation architecture without imposing a specific implementation:\n\n```php\nuse fab2s\\Dt0\\Dt0;\nuse fab2s\\Dt0\\Attribute\\Validate;\nuse fab2s\\Dt0\\Attribute\\Rule;\nuse fab2s\\Dt0\\Attribute\\Rules;\n\n#[Validate(MyValidator::class)]\n#[Rules(\n    email: new Rule(['required', 'email']),\n)]\nclass ContactDto extends Dt0\n{\n    public readonly string $email;\n\n    #[Rule(['required', 'min:2', 'max:100'])]\n    public readonly string $name;\n\n    #[Rule(['string', 'max:1000'])]\n    public readonly ?string $message;\n}\n\n$contact = ContactDto::make(\n    email: 'test@example.com',\n    name: 'Jo',\n    message: 'Hello!',\n);\n\n// Run validation (throws on failure)\n$contact-\u003ewithValidation();\n```\n\n**Rule priority**: When rules are defined at multiple levels, property-level `#[Rule]` takes precedence over class-level `#[Rules]`, which takes precedence over rules defined in `#[Validate]`.\n\nFor a complete implementation with Laravel's validator, see [Laravel Dt0](https://github.com/fab2s/laravel-dt0).\n\n## Type System Integration\n\nDt0 works with PHP's type system, not against it. Casters attempt conversion and return `null` on failure. Your property types decide what's acceptable:\n\n```php\nclass StrictDto extends Dt0\n{\n    public readonly string $required;   // null → TypeError\n    public readonly ?string $optional;  // null → accepted\n    public readonly int|string $flexible; // int or string accepted\n}\n```\n\nThis approach:\n- Avoids duplicating validation logic\n- Lets you declare acceptance criteria via types\n- Produces clear errors from PHP itself\n\n```php\n$dto = StrictDto::make(\n    required: null,  // TypeError: cannot be null\n    // ...\n);\n```\n\n## Extending Attributes\n\nDt0's attributes are extensible. Implement the appropriate interface or extend the abstract class:\n\n| Attribute Type | Interface | Abstract Class |\n|----------------|-----------|----------------|\n| Class casts | [`CastsInterface`](./src/Attribute/CastsInterface.php) | [`CastsAbstract`](./src/Attribute/CastsAbstract.php) |\n| Property cast | [`CastInterface`](./src/Attribute/CastInterface.php) | [`CastAbstract`](./src/Attribute/CastAbstract.php) |\n| Validation | [`ValidateInterface`](./src/Attribute/ValidateInterface.php) | [`ValidateAbstract`](./src/Attribute/ValidateAbstract.php) |\n| Class rules | [`RulesInterface`](./src/Attribute/RulesInterface.php) | [`RulesAbstract`](./src/Attribute/RulesAbstract.php) |\n| Property rule | [`RuleInterface`](./src/Attribute/RuleInterface.php) | [`RuleAbstract`](./src/Attribute/RuleAbstract.php) |\n| Output control | [`WithInterface`](./src/Attribute/WithInterface.php) | [`WithAbstract`](./src/Attribute/WithAbstract.php) |\n\n**Access compiled property metadata:**\n\n```php\n$properties = MyDto::compile();          // Properties instance (cached)\n$properties-\u003etoArray();                  // Property[] indexed by name\n$property = $properties-\u003eget('fieldName'); // Single Property instance\n\n// Inspect a property\n$property-\u003ename;        // 'fieldName'\n$property-\u003etypes;       // Types instance with type information\n$property-\u003ecast;        // The Cast attribute (or null)\n$property-\u003ein;          // Input caster instance (or null)\n$property-\u003eout;         // Output caster instance (or null)\n$property-\u003eisDt0;       // true if property type is a Dt0\n$property-\u003eisEnum;      // true if property type is an Enum\n$property-\u003ehasDefault(); // true if a default value exists\n$property-\u003egetDefault(); // The default value\n```\n\n## Performance\n\nDt0 compiles reflection and attribute metadata **once per class, per process**. The first instantiation of a Dt0 class triggers compilation; subsequent instantiations reuse the cached data with zero reflection overhead.\n\n```php\n// First call: reflection + attribute parsing\n$user1 = UserDto::make(/* ... */);\n\n// All subsequent calls: cached metadata, no reflection\n$user2 = UserDto::make(/* ... */);\n$user3 = UserDto::fromArray(/* ... */);\n$user4 = UserDto::fromJson(/* ... */);\n```\n\nThe cache is bounded by the number of Dt0 classes in your application, not by usage. If you have 20 Dt0 classes, you get 20 cache entries - regardless of how many instances you create.\n\n### Benchmarks\n\nRun the benchmark:\n\n```shell\nphp benchmark/compare-spatie.php\n```\n\n#### Dt0 vs spatie/laravel-data (PHP 8.4, 10,000 iterations)\n\n| Operation | Dt0 | spatie/laravel-data | Speedup |\n|-----------|-----|---------------------|--------|\n| Simple DTO (8 props, 5 casts) | 141.6 µs | 1,158 µs | **~8.2x faster** |\n| Complex DTO (nested + arrays) | 741.9 µs | 3,628 µs | **~4.9x faster** |\n| Round-trip (json→dto→json) | 248.4 µs | 2,004 µs | **~8.1x faster** |\n\n**Repeated serialization (same instance):**\n\n| Operation | Dt0 | spatie/laravel-data | Speedup |\n|-----------|-----|---------------------|--------|\n| toArray() (simple) | 3.6 µs | 679.4 µs | **~188.7x faster** |\n| toArray() (nested) | 3.6 µs | 2,056 µs | **~571.1x faster** |\n| toJson() | 2.8 µs | 681.8 µs | **~243.5x faster** |\n\nThe extreme serialization speedup (188-571x) applies when serializing the same instance multiple times - Dt0 caches the output structure on first call. Real-world scenarios where this matters:\n\n- **API + logging**: serialize response, then log the same DTO\n- **Event sourcing**: serialize for storage, broadcast, and audit trail\n- **Queue jobs**: serialize for the queue, then again for monitoring\n- **Caching layers**: serialize for Redis and for the HTTP response\n\nFor single-use serialization, expect ~10x improvement, consistent with hydration benchmarks.\n\n## Exceptions\n\nAll Dt0 exceptions extend [`ContextException`](https://github.com/fab2s/ContextException), providing structured context for logging and debugging:\n\n| Exception | Usage |\n|-----------|-------|\n| `Dt0Exception` | General DTO errors (missing properties, invalid input) |\n| `CasterException` | Casting failures |\n| `AttributeException` | Attribute configuration errors |\n\n```php\ntry {\n    $dto = UserDto::from($invalidInput);\n} catch (Dt0Exception $e) {\n    $e-\u003egetMessage();   // Human-readable message\n    $e-\u003egetContext();   // Array with debugging information\n}\n```\n\n## Requirements\n\n- PHP 8.1, 8.2, 8.3, or 8.4\n\n## Dependencies\n\n- [`fab2s/context-exception`](https://github.com/fab2s/ContextException) - Contextual exceptions\n- [`fab2s/enumerate`](https://github.com/fab2s/Enumerate) - Enum utilities\n\n### Optional\n\n- [`nesbot/carbon`](https://github.com/briannesbitt/Carbon) - For `CarbonCaster`\n- [`fab2s/math`](https://github.com/fab2s/Math) - For `MathCaster`\n\n## Contributing\n\nContributions are welcome. Please open issues and submit pull requests.\n\n```shell\n# fix code style\ncomposer fix\n\n# run tests\ncomposer test\n\n# run tests with coverage\ncomposer cov\n\n# static analysis (src, level 9)\ncomposer stan\n\n# static analysis (tests, level 5)\ncomposer stan-tests\n```\n\n## License\n\nDt0 is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffab2s%2Fdt0","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffab2s%2Fdt0","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffab2s%2Fdt0/lists"}