Skip to main content

v1.0.0

First stable release of better-data — PHP 8.3+ DTO and Presenter library for WordPress. Released on the main branch as Composer tag v1.0.0.

Install

{
"require": {
"better-data/better-data": "^1.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/Lonsdale201/better-data"
}
],
"prefer-stable": true
}

Highlights

DataObject layer

  • final readonly class DTOs with constructor-promoted properties
  • Type coercion: string → int/float/bool, ISO strings → DateTimeImmutable, scalar → BackedEnum, array → nested DataObject, string → Secret, arrays → typed lists via #[ListOf]
  • with() for immutable updates that preserve Secret / DateTime / Enum rich types
  • Attribute-driven validation: Required, Email, Url, Uuid, Regex, Min/Max, MinLength/MaxLength, OneOf, Callback
  • Pluggable ValidationEngineInterface for swapping in Symfony Validator / Respect / Laravel

Sources (read path)

  • PostSource, UserSource, TermSource, OptionSource, RowSource, RequestSource
  • WP-independent engine — DTO hydration is unit-testable without bootstrapping WordPress
  • Bulk hydration helpers (fromPosts, fromUsers, fromTerms, fromRows) prewarm post + meta in two SQL queries
  • RequestSource guards: requireNonce, requireCapability, bodyOnly / jsonOnly / queryOnly / urlOnly, noCollision for route-vs-body collision detection

Sinks (write path)

  • PostSink, UserSink, TermSink, OptionSink, RowSink
  • Projection-first API (toArgs / toMeta / toArray) for unit tests
  • Partial writes via only: [...]; strict whitelist (UnknownFieldException on typos); skipNullDeletes for PATCH semantics
  • Convenience methods slash payloads via wp_slash(); projection methods leave values raw
  • UserSink excludes user_pass, user_activation_key; TermSink excludes term_taxonomy_id, count

Presenter

  • Presenter::for($dto)->context(PresentationContext::rest()) and Presenter::forCollection
  • Fluent only / hide / hideUnlessCan / showOnlyFor / rename / compute / preset / formatDate / formatCurrency / includeSensitive
  • Locale-aware (switch_to_locale() for date/currency formatting)
  • Subclassable: override configure() for reusable presenters

Security primitives

  • Secret — leak-proof in-memory container (blocks __toString, json_encode, var_dump, print_r, serialize)
  • #[Sensitive] — Presenter-level redaction for plain-string PII (includeSensitive() opt-in)
  • #[Encrypted] — at-rest AES-256-GCM for meta and option values; transparent decrypt on read
  • Key rotation via BETTER_DATA_ENCRYPTION_KEY + BETTER_DATA_ENCRYPTION_KEY_PREVIOUS
  • Loud failures: MissingEncryptionKeyException, DecryptionFailedException with no oracle leak

Registration

  • MetaKeyRegistry::register(Dto::class, 'post', 'subtype') — drives register_meta() from DTO shape
  • toJsonSchema($dto) — root-object JSON Schema for OpenAPI components
  • toRestArgs($dto) — flat per-field map for register_rest_route(['args' => …])
  • _doing_it_wrong guards: protected-meta showInRest without authCapability, encrypt: true + showInRest: true

better-route bridge

  • BetterData\Route\BetterRouteBridge — hydrates and validates DTOs from query / JSON / body / URL params
  • Marks route-owned fields as URL-authoritative; rejects body/query collisions
  • Feeds MetaKeyRegistry::toRestArgs() into the route builder's args()
  • Feeds generated requestSchema, responseSchema, tags, scopes into the route builder's meta()
  • Presents returned DataObject values through Presenter with PresentationContext::rest()
  • Optional — better-route is not a hard Composer dependency

Requirements

  • PHP 8.3+ (readonly classes, typed class constants, #[\Override], json_validate())
  • WordPress (no version floor; sources fall back gracefully when optional WP functions are absent)
  • ext-openssl only when using #[Encrypted]

Versioning

Semver from v1.0 onwards. The public API surface — DataObject, attributes, sources, sinks, Presenter, MetaKeyRegistry, BetterRouteBridge — is stable. Breaking changes require a major bump and migration notes.

Source