Document Resolver Registry
Contract for registering document data resolvers, merge-field metadata, and end-of-resolve extenders.
Overview
App\Services\Documents\DocumentResolverRegistry is the document engine seam that maps a resolver type to a concrete App\Contracts\Documents\DocumentDataResolver.
The same registry backs:
- document rendering through
DocumentRenderPipeline - the template field browser through
SchemaRegistry::resolveDocumentType() - the grouped field API at
/api/v1/document_types/{type}/fields
Core binds the registry as a singleton in AppServiceProvider and pre-registers the built-in resolvers there. As a factual note about the current integration, PluginRegistrar::documentResolver() and PluginRegistrar::extendDocumentResolver() forward registrations into this registry; this is not SDK usage guidance.
Public surface
| Method | Purpose |
|---|---|
register() |
Store one resolver under its type() and invalidate cached metadata for that type |
extendResolve() |
Append a post-resolve callback for one resolver type |
get() |
Return the resolver instance for a type or throw if it is missing |
has() |
Check whether a resolver type is registered |
types() |
Return {type, label, description} summaries for registered resolvers |
fieldDefinitions() |
Return the flat merge-field map for a resolver type |
fieldTree() |
Return the grouped merge-field tree for a resolver type |
load() |
Load the source model for a resolver type and source id |
resolve() |
Load the source model, resolve its data array, then run extenders |
invalidate() |
Clear cached field metadata for one type or all types |
Accepted contract and identity
Every resolver implements App\Contracts\Documents\DocumentDataResolver:
public function type(): string;
public function label(): string;
public function description(): string;
public function registerFields(SchemaBuilder $builder): void;
public function loadSource(int $sourceId): Model;
public function resolve(Model $source): array;
type() is the stable registry identity. label() and description() drive the summaries returned by types(). registerFields() is the source of truth for merge-field metadata. loadSource() owns record loading. resolve() turns the loaded model into the nested data array used by the template renderer.
The resolver contract also defines the loading and output boundary. loadSource() must eager-load every relation required by resolve() so resolution does not discover unloaded data as it walks the source graph. The returned array is template-ready and normalized: Money values are decimal strings, dates are formatted for the warehouse timezone, and enums are rendered as human labels. Implementations should not expose money objects, date objects, or enum instances for templates to interpret.
Current core registrations
AppServiceProvider currently registers these resolver types:
rentalreturn_liststatementreceiptinvoicecredit_notepurchase_orderlabelequipment_test_certificatecatalogue_item_specasset_labelvirtual_stock_summaryrepair_reportstock_check_reportwarehouse_transfer_manifestactivity_summaryfleet_vehicle_summaryequipment_test_summaryscan_session_reportflightcase_manifest
Some document types share one resolver. For example, quote, delivery_note, pick_list, and damage_report all map to the rental resolver, while invoice, pro_forma, and deposit all map to invoice.
Field metadata, loading, and resolution
fieldDefinitions($type) creates a fresh SchemaBuilder, calls the resolver's registerFields(), and memoises the resulting flat map by merge-field path.
fieldTree($type) groups those definitions by FieldDefinition::$group. Fields without a group fall back to General. SchemaRegistry::resolveDocumentType() delegates to this same metadata source so document-field search, API output, and template editing all see the same contract.
load($type, $sourceId) only calls loadSource(). resolve($type, $sourceId) does more:
- look up the resolver by type
- load the source model once
- call the resolver's
resolve()method - run each registered extender for that type in registration order
Each extender receives (array $data, Model $source) and must return the replacement data array.
Invalidation, collisions, ordering, and failures
Resolver collisions
register() stores resolvers in an associative array keyed by type(). Re-registering the same type replaces the earlier resolver. The registry does not reject duplicates.
Extender accumulation
extendResolve() appends callbacks to a per-type list. Extenders accumulate; they are not deduplicated or replaced.
The extender list is independent from the resolver map. An extender may be registered before a resolver for that type exists. It remains stored without forcing an immediate resolver lookup, will survive resolver replacement, and will run in registration order when that type is resolved later. Registering or replacing the resolver invalidates field metadata but does not clear its extenders.
Cache invalidation
Re-registering a resolver automatically calls invalidate($type). That clears the local field-definition and field-tree caches for that type and then calls SchemaRegistry::invalidateDocumentType($type).
Calling invalidate() with no type clears all local document-field caches. Unlike the typed form, that all-local-caches branch does not call SchemaRegistry; callers that also need its document-type cache cleared must invalidate that service separately.
Lookup failures
get() throws InvalidArgumentException with Unknown document type: {$type} when the resolver type is missing. fieldDefinitions(), fieldTree(), load(), and resolve() all rely on get(), so they fail the same way.
Ordering
types() returns summaries in the registry's current insertion order. There is no additional sort step.
resolve() runs extenders in the order they were registered for that type.
Worked example
The document rendering pipeline consumes the current core rental resolver like this:
$resolved = app(DocumentResolverRegistry::class)
->resolve('rental', $rental->id);
The registry loads the source through the registered resolver, resolves its base field data, applies any registered end-of-resolve extenders, and returns the completed payload to the renderer.