Skip to content

feat(resources): agnostic resource transformer registry - #285

Merged
roncodes merged 2 commits into
release/v1.6.68from
feature/agnostic-resource-transformers
Oct 3, 2026
Merged

roncodes merged 2 commits into
release/v1.6.68from
feature/agnostic-resource-transformers

Conversation

@roncodes

@roncodes roncodes commented Oct 3, 2026

Copy link
Copy Markdown
Member

Summary

ResourceTransformerRegistry only worked when a resource opted in (the User resource was the single call site), matched by re-resolving the resource class by naming convention, allowed one transformer per target, and was a copy of NotificationRegistry's static array. This rewrites it so any extension can register a transformer and have it applied without modifying the resource or model.

  • No opt-in required. FleetbaseResource::resolve() applies transformers to every resource, nested resource and collection item. Laravel routes responses, nested jsonSerialize() and paginated responses through resolve(), so hand-built toArray() methods are covered. Output is byte-identical when nothing is registered.
  • Container singleton registry. Targets match a resource class, model class, interface or '*' (subclasses match). Transformers chain in ascending priority; contexts (http, webhook, broadcast) and only (internal, public) scope them. Classes, instances, closures and batch forms are accepted; re-registering a class replaces it.
  • Batch loading. PreparesResourceTransformation::prepare() runs once per collection (or once per singular resolve) with every model about to be serialized, with a shared ResourceTransformerContext to hand data to transform().
  • Webhooks and broadcasts. ResourceLifecycleEvent::getEventData() / broadcastWith(), chat participant broadcasts, Utils::serializeJsonResource() and the cached internal user payload serialize through resolve(). The cached user payload stops emitting "company": {} from an unfiltered MissingValue.
  • Provider wiring. CoreServiceProvider::$transformers, registerTransformers() and registerTransformersFrom() mirror expansion discovery. Extensions call $this->registerTransformersFrom(__DIR__ . '/../Http/Transformers') in boot().
  • Find::httpResourceForModel caches internal and public resolutions separately. It consults the request only when a model has a dedicated Internal resource, so explicit-resource models never touch the request (a UserControllerTest route stub builds a controller inside its route resolver and recurses otherwise).

Breaking changes

  • Legacy duck-typed transformers ($target property + static output($model, $data)), ResourceTransformerRegistry::transform(Model, array), resolveByTarget(), fixClassName() and the static $transformers array are removed. The only consumer was the deprecated aws-marketplace extension, which is not updated.
  • FleetbaseResourceCollection resolves items instead of calling toArray(); a hand-built collection with a manually set preserveKeys now filters item arrays with the item's flag.

See the README "Resource transformers" section and CHANGELOG for usage.

Testing

  • vendor/bin/pest: 1947 passed at the default 512M limit.
  • New: tests/Unit/Support/ResourceTransformerRegistryTest.php, tests/Unit/Http/FleetbaseResourceTransformersTest.php, tests/Unit/Providers/CoreProviderTransformersTest.php, tests/Unit/Support/FindHttpResourceForModelTest.php; extended EventsAndExceptionsTest and FleetbaseResourceCollectionTest.
  • phpstan and php-cs-fixer clean on changed files.

Rewrite ResourceTransformerRegistry as a container singleton that any extension can
register against without modifying the resource or model. Transformers target a
resource class, model class, interface or '*' (subclasses match), chain in ascending
priority, and can be scoped by channel (http, webhook, broadcast) and audience
(internal, public). Closures and instances are accepted alongside classes.

FleetbaseResource::resolve() applies registered transformers to every resource,
nested resource and collection item, re-filtering conditional values and honouring
without(). FleetbaseResourceCollection shares one context per collection so the new
PreparesResourceTransformation::prepare() hook can batch-load once and avoid N+1
queries. ResourceLifecycleEvent, chat participant broadcasts, Utils::serializeJsonResource
and the cached internal user payload serialize through resolve(), so transformers reach
webhook and broadcast payloads and conditional MissingValues are no longer emitted as {}.

CoreServiceProvider binds the registry, adds a $transformers property, and
registerTransformers() / registerTransformersFrom() mirroring expansion discovery.
Find::httpResourceForModel caches internal and public resolutions separately, consulting
the request only when a model has a dedicated Internal resource.

Removes the legacy duck-typed transformer API ($target + static output()), the
transform(Model) entry point, resolveByTarget(), fixClassName() and the static
$transformers array. The User resource no longer calls the registry directly.
…erage

CI enforces 100% line and method coverage. Covers the remaining branches: the
registry's empty-registration short circuits, invokable objects and array
callables as transformers (and their generated ids), rejecting non-callable
objects, collections wrapping items in non-resource classes, collections with
no resolvable item resource class, and resolving without a bound request.

Two defensive throws that normalize() makes unreachable are marked
@codeCoverageIgnore with an explanation, and callableId() drops a branch that
could never run because string transformers are always treated as classes.
@roncodes
roncodes merged commit ab33100 into release/v1.6.68 Oct 3, 2026
3 checks passed
@roncodes
roncodes deleted the feature/agnostic-resource-transformers branch October 3, 2026 07:05
@codecov

codecov Bot commented Oct 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a698292) to head (56163c6).
⚠️ Report is 4 commits behind head on release/v1.6.68.

Additional details and impacted files
@@                 Coverage Diff                  @@
##             release/v1.6.68      #285    +/-   ##
====================================================
  Coverage             100.00%   100.00%            
- Complexity              7772      7931   +159     
====================================================
  Files                    436       438     +2     
  Lines                  25334     25665   +331     
====================================================
+ Hits                   25334     25665   +331     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant