jooservices / dto
PHP 8.5+ DTO and Data library with immutable DTOs and mutable Data objects
Requires
- php: >=8.5
- ext-dom: *
- ext-libxml: *
- psr/http-message: ^2.0
Requires (Dev)
- captainhook/captainhook: ^5.23
- captainhook/plugin-composer: ^5.3
- fakerphp/faker: ^1.24
- friendsofphp/php-cs-fixer: ^3.65
- laravel/pint: ^1.18
- phpbench/phpbench: ^1.6
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^12.0 || ^13.0
- squizlabs/php_codesniffer: ^3.8 || ^4.0
Suggests
- psr/log: Enables debug logging during DTO hydration via Engine constructor injection.
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v3.2.0
- v3.1.0
- v3.0.0
- v2.0.0
- v1.6.0
- v1.5.1
- v1.5.0
- v1.4.0
- v1.3.0
- v1.2.0
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- 1.0.4
- 1.0.3
- 1.0.1
- 1.0.0
- dev-develop
- dev-ci/final-cleanup
- dev-ci/drop-composite-prepare
- dev-hotfix/drop-packagist-notify
- dev-hotfix/trivy-db-ghcr-mirror
- dev-hotfix/scorecard-runner-restrictions
- dev-release/3.0.0
- dev-hotfix/first-interaction-inputs
- dev-fix/ci-sync-master-and-workflows
This package is auto-updated.
Last update: 2026-09-04 21:37:08 UTC
README
A PHP 8.5+ attribute-driven DTO and Data library: immutable Dto and mutable Data objects, constructor-first hydration, opt-in validation, serialization control, collections, and JSON Schema / OpenAPI generation. One runtime dependency: psr/http-message (interface-only).
Warning
v3.0.0 is a complete ground-up rebuild of this package and is NOT backward compatible with any previous version (v1.x, v2.x).
Every line was rewritten against a new architecture. There are no legacy shims, no deprecation bridges, and no compatibility code.
Upgrading means rewriting your DTO classes against the new API — see About v3.0.0 and the changelog.
About v3.0.0
| Status | v3.2.0 — current release |
| First public line | v3.0.0 (the archived v1.x / v2.x implementation is a separate codebase lineage, not an ancestor of this one) |
| Git history | Fresh repository — clean rewrite, old repo untouched in archive |
| Compatibility | None with older versions. Class layout, behavior contracts, exception hierarchy, and engine internals all changed |
| Runtime dependencies | psr/http-message (interface-only) for fromRequest(); exceptions are vendorized; PSR-3 logging is optional via Composer suggest |
Highlights vs the previous line
| Area | Previous (v2.x) |
This rebuild (v3.0.0) |
|---|---|---|
| Dependencies | jooservices/exceptions ^1.0 runtime require |
Vendorized exceptions; psr/http-message for fromRequest() |
| Engine | Per-class static engine store | One process-wide Engine + LRU ClassMeta caches + worker reset() |
with() / clone() / merge() |
toArray() → from() round-trip (dropped #[Hidden], mixed key spaces) |
State view through the constructor, property-name keys only, patched values cast, named args supported |
| Hashing | Order-dependent serialize(toArray()) |
Canonical sorted-key JSON over the state view |
| Framework coupling | Laravel config() inside #[DefaultFrom] |
Pluggable resolver (env + static method); Laravel adapters fully dropped |
| HTTP input | — | fromRequest(ServerRequestInterface) (PSR-7) |
| Coding standards | Pint laravel preset, partial PSR-12 |
Strict PSR-1 / PSR-4 / PSR-12 (PER-CS 3.0), Pint per preset |
| Correctness & security | Known defect register (C1–C19, S1–S6) | All fixed with named regression tests |
Features
Core
Dto(immutable, readonly) andData(mutable counterpart) sharing one baseContext,CastMode,SerializationOptions,Optional,PartialDtoBuilder- Lazy derived serialization via
ComputesLazyProperties
Factories
from(),fromArray(),fromJson(),fromObject()— external input hydrationtryFrom()— non-throwing variantfromRequest(ServerRequestInterface)— PSR-7 parsed body + query stringcollection()for lists,partial()for partial-payload builders
Instance helpers
with()— immutable copy through the constructor; array or named args ($dto->with(email: $v))merge(),mergeRecursive(),clone()/replicate()diff(),equals(),hash()— state-view comparisons with canonical hashingvalidate(),when()/unless()
Attributes
- Mapping:
MapFrom,MapTo,Hidden,DefaultFrom, class-levelDiscriminatorMap - Casting / transforms:
CastWith,TransformWith,StrictType,Pipeline(constructor-spread step options) - Validation:
Required,RequiredIf,Email,Url,Regex,Length,Min,Max,Between,Valid
Hydration & casting
- Arrays, JSON strings, simple objects, PSR-7 requests
- Input naming strategies (camelCase / snake_case); output-side naming opt-in via Context
- Global + property pipelines with options; input normalizers
- Scalar, enum,
DateTimeInterface, nested DTO, untypedarraypass-through, PHPDoc typed arrays (Type[],array<Type>,array<K, V>,list<Type>on@varor constructor@param), native union types in stable documented order
Validation, normalization, collections
- Opt-in validation via Context plus standalone instance validation; rule registry extensible via attributes
toArray()/toJson()/jsonSerialize(), transformers, lazy properties- Serialization filters:
only/except/maxDepth/wrap/includeLazy DataCollection(toArray()/jsonSerialize()/all()) andPaginatedCollection(duck-typed paginator support)
Schema, meta, exceptions
JsonSchemaGeneratorandOpenApiGeneratoremitting self-contained recursive$refgraphs- Reflection-based metadata: true-LRU memory cache + file cache with content-hash freshness envelope
- Structured exception hierarchy (hydration / mapping / cast / validation) with path support and payload redaction
Requirements
- PHP
>= 8.5 - Extensions:
dom,libxml psr/http-message(fromRequest()), optional:psr/log(Engine debug logging)- Docker (recommended — all local tooling runs in
php:8.5-cli-bookworm)
Installation
composer require jooservices/dto:^3.0
Quick start
use JOOservices\Dto\Attributes\MapFrom; use JOOservices\Dto\Core\Dto; final class UserDto extends Dto { public function __construct( public readonly string $id, #[MapFrom('email_address')] public readonly string $email, public readonly \DateTimeImmutable $createdAt, ) {} } // External input — source keys resolved through MapFrom / naming strategy $user = UserDto::from([ 'id' => 'u_123', 'email_address' => 'john@example.com', 'createdAt' => '2026-01-15T10:30:00+00:00', ]); // Immutable copy — property-name keys only, patched value cast to string $updated = $user->with(email: 'other@example.com'); $updated->toArray(); // ['id' => 'u_123', 'email' => 'other@example.com', ...] $updated->toJson();
Design contract
- Every DTO declares a constructor with public promoted properties; constructor-less classes are unsupported.
- Each entry point owns exactly one key space:
| Entry point | Key space |
|---|---|
new UserDto(...) |
Property names — already-typed values |
UserDto::from() / fromArray() / fromJson() / fromObject() / fromRequest() |
Source keys — resolved via MapFrom / naming strategy |
$dto->with(...) / merge(...) |
Property names only — immutable copies built through the constructor |
- Validation is opt-in via
Context; casting and pipelines never silently change scope when a Context argument is omitted.
Documentation
- Changelog — starts at
v3.0.0; earlier releases belong to the retired implementation AGENTS.md— contributor/agent working agreement
Development
All PHP tooling runs inside Docker (php:8.5-cli-bookworm via Docker Compose).
make build # build the tooling image make install # composer install in the container make shell # interactive container shell
| Command | Purpose |
|---|---|
make validate |
composer validate --strict |
make lint |
Pint, PHPCS, PHPStan, PHPMD, PHP-CS-Fixer |
make test |
PHPUnit (Unit + Integration) |
make test-coverage |
PHPUnit with Clover coverage |
make audit |
Composer audit |
make bench |
phpbench |
make ci |
lint + coverage run (local CI parity) |
Every linter runs at maximum strictness with no ignore lists — fix issues at the source instead of suppressing them.
IDE setup: Cursor / VS Code — install recommended workspace extensions; format-on-save runs Pint via tools/pint (Docker). PHPStorm — inspection profile + Pint file watcher.
Branch model & CI
master— production;develop— integration- Feature/fix branches from
develop, PR back intodevelop; releases viarelease/<version>→master; hotfixes frommaster; tags frommaster - PRs required, all CI checks green before merge
Required CI flow (dedicated workflows on self-hosted Linux X64 runners, PHP jobs in Docker):
validate → lint matrix → test matrix ┐
dependency security ├→ coverage gate (85%) → Codecov + Sonar
secret scan │
SAST ┘
Workflows run on self-hosted Linux X64 runners:
| Workflow | Purpose |
|---|---|
ci.yml |
validate → lint/test matrices + parallel security jobs → 85% coverage → Codecov + Sonar |
commitlint.yml |
Conventional Commits on every PR commit |
codeql.yml |
CodeQL analysis for GitHub Actions workflows |
workflow-audit.yml |
actionlint + zizmor on workflow files |
release.yml |
tag gates, Trivy, SBOM, GitHub Release |
semantic-pr.yml |
Conventional Commits PR title |
pr-labeler.yml / pr-size-labeler.yml |
path and size labels |
scorecard.yml |
OpenSSF Scorecard |
link-check.yml |
weekly Markdown link check |
stale.yml / first-interaction.yml |
housekeeping and contributor welcome |
Also: Dependabot (Composer + GitHub Actions), CODEOWNERS, labeler config.
CI secrets (organization level): CODECOV_TOKEN and SONAR_TOKEN live under jooservices organization secrets — not per-repo. SONAR_HOST_URL is optional and defaults to https://sonarcloud.io. Grant this repository access when onboarding. No release tag is required to test CI; any push or PR to develop or master runs the pipeline.
Quality gates: Pint (per preset) · PHPCS full PSR12 · PHPStan max level, zero ignores · PHPMD · PHP-CS-Fixer (PHPDoc-only).
Community
- Contributing guide — setup, git workflow, commit convention, quality gates, PR rules
- Security policy — how to report vulnerabilities privately
- Code of Conduct
- Support
- Governance
License
MIT — see LICENSE.