Skip to main content

v0.3.0

Released on the main branch as Composer tag v0.3.0.

Upgrade summary

{
"require": {
"better-route/better-route": "^0.3.0"
}
}

Most consumers can upgrade with composer update better-route/better-route. The only behavior change that is likely to require an explicit decision is the OpenAPI document endpoint default permission — see the breaking change list below.

Security and hardening

  • Route id from URL preferred over merged params. Resource and Woo handlers read id via get_url_params() first; query/body id is only used when the URL does not provide one.
  • Hardened error responses. For non-ApiException failures with status >= 500, the response is normalized to "Unexpected error." with empty details — internal exception class and message no longer leak.
  • JWT hardening. Hs256JwtVerifier now requires exp by default (requireExpiration: true), supports optional expectedIssuer, expectedAudience, maxLifetimeSeconds, and a maxTokenLength (default 8192 bytes).
  • WpClaimsUserMapper no longer maps sub by default. Default idClaims is ['user_id', 'uid', 'wp_user_id']. Re-add 'sub' explicitly if you depend on it.
  • Custom table resources are deny-by-default. Table resources now require an explicit policy() (or ResourcePolicy preset) before any read or write succeeds. CPT resources keep WordPress visibility for reads.
  • Identity-aware default keys. CachingMiddleware, IdempotencyMiddleware, and RateLimitMiddleware derive default keys from auth.userId / auth.subject, falling back to client IP (rate-limit only) or 'guest'. Existing custom keyResolvers are unaffected.
  • Woo customer endpoints are customer-only. assertCustomerUser() rejects users without the customer role. Create/update/delete on customers requires create_users / edit_user / delete_user.
  • Protected Woo meta keys (_...) are not writable or returned by default. Pass $allowProtected = true only when you need it.
  • CPT writes go through WP capability checks. Publish/status/author/delete operations are gated by core capabilities.
  • WpdbAdapter rejects cross-database table names (containing .) and structured write payloads.
  • OpenAPI document endpoint defaults to manage_options. Previously public. Pass 'permissionCallback' => static fn (): bool => true to keep it public.
  • X-Request-ID is sanitized to ^[A-Za-z0-9._:-]{1,128}$. Anything else is replaced with a generated id.

New features

Resource layer

  • Resource::writeSchema() / Resource::payloadSchema() for write validation. Supports types (int, float, bool, string, date, email, url, enum, array, object), required, min/max, minLength/maxLength, regex, enum.values, sanitize, nullable. Validation failure returns 400 validation_failed with details.fieldErrors.
  • Resource::fieldPolicy() for field-level write authorization (per action, per field).
  • ResourcePolicy presets:
    • ResourcePolicy::adminOnly(string $cap = 'manage_options')
    • ResourcePolicy::publicReadPrivateWrite(string|array $writeCap = 'manage_options')
    • ResourcePolicy::capabilities(array $permissions)
    • ResourcePolicy::callbacks(array $callbacks)
  • deleteMode('trash'|'force') for CPT resources (default: 'force').

WooCommerce

  • deleteMode option ('force' or 'trash') for orders, products, and coupons.
  • Capability-gated writes; protected meta keys (_...) hidden from output by default.

OpenAPI

  • strictSchemas => true causes OpenApiExporter::export() to throw InvalidArgumentException on missing component references instead of substituting a permissive placeholder.

HTTP infrastructure

  • BetterRoute\Http\ClientIpResolver with trusted-proxy + trusted-header support (defaults to HTTP_X_FORWARDED_FOR, HTTP_CF_CONNECTING_IP, HTTP_X_REAL_IP).
  • BetterRoute\Middleware\Cache\ETagMiddleware with If-None-Match / 304 Not Modified and optional weak validators.

Stores and limiters

  • BetterRoute\Middleware\RateLimit\WpObjectCacheRateLimiter — uses the WP object cache. Throws RuntimeException if wp_cache_* is unavailable.
  • BetterRoute\Middleware\Write\WpdbIdempotencyStorewpdb-backed idempotency store with auto-installSchema().

Developer experience

  • Composer scripts run tools through php vendor/bin/... so missing executable bits on Windows / shared mounts do not break runs.
  • Expanded regression coverage for security defaults, resource validation, OpenAPI strict mode, ETag handling, and WP-backed stores.

Breaking change checklist

ChangeAction
OpenAPI doc default manage_optionsAdd 'permissionCallback' => static fn (): bool => true if the doc must stay public
Custom table resources deny-by-defaultAdd ->policy(ResourcePolicy::publicReadPrivateWrite()) (or another preset) for affected table resources
JWT exp requiredEither include exp in tokens, or pass requireExpiration: false to Hs256JwtVerifier
sub removed from WpClaimsUserMapper defaultsRe-add 'sub' to idClaims if your tokens rely on it
Woo customer endpoints customer-onlyConfirm callers operate on customer-role users; update pipelines that touched non-customer users
Protected Woo meta keys hiddenRead explicitly with $allowProtected = true if you stored business data under _... keys
Identity-aware default cache/idempotency/rate-limit keysIf your existing keys assumed pre-v0.3.0 defaults, expect a one-time cache miss; pass an explicit keyResolver to keep keys stable
X-Request-ID sanitizationClients sending arbitrary characters will receive a generated id instead — acceptable in almost all cases