Document Type Registry
Contract for mapping printable document types onto resolver types, source models, permissions, categories, and aliases.
Overview
App\Services\Documents\DocumentTypeRegistry maps a printable document type such as quote or receipt onto:
- the resolver type that renders it
- the Eloquent source model class it accepts
- its label and description
- the permission checked by UI and generation flows
- an optional category and alias list
Core binds the registry as a singleton in AppServiceProvider, and the constructor registers the built-in type map. As a factual note about the current integration, PluginRegistrar::documentType() forwards registrations into this registry; this is not SDK usage guidance.
Public surface
| Method | Purpose |
|---|---|
register() |
Store or replace one document-type definition |
has() / get() |
Check for a type or fetch its full definition |
resolverType() |
Return the resolver type used for rendering |
modelType() |
Return the accepted source model class |
permission() |
Return the permission gate name associated with the type |
label() / filenameSlug() |
Return the display label or its slugified filename form |
all() |
Return all registered summaries |
forSourceModel() |
Return types whose source model matches a class or instance |
forSourceModelPermitted() |
Return only the types the current user may use |
typesInCategory() |
Return the types assigned to one category |
isRegisteredSourceModel() / registeredSourceModels() |
Inspect whether a model class participates in the registry |
allowedDocumentableTypeValues() |
Return the validation whitelist for documentable_type inputs |
resolveSourceModelClass() |
Resolve an input alias back to a concrete model class |
This metadata registry does not publish a separate registered-value interface. Its current framework contract is the typed public surface on this page plus the documented registration shape and runtime behavior below.
Definition shape
Each registration stores this definition:
[
'resolver_type' => string,
'model_type' => class-string<Model>,
'label' => string,
'description' => string,
'permission' => string,
'category' => ?string,
'aliases' => list<string>,
]
The registry does not infer the resolver type from the document type. Shared resolvers are normal and already used by core.
description defaults to an empty string, permission defaults to documents.create, category defaults to null, and aliases defaults to an empty list. Exact duplicate aliases are removed while preserving first appearance. These defaults are applied when the definition is stored.
The class-string<Model> annotation and the relationship between document type, resolver type, model, and permission are static contracts. register() does not preflight the model class, require the resolver type to be registered, or verify that the permission exists. An invalid relationship can therefore be stored and fail only when a later consumer tries to use it.
get() returns the complete stored definition, including aliases. all() returns summary rows and intentionally omits aliases.
Current core mappings
Rental-backed types
| Document type | Resolver type | Notes |
|---|---|---|
quote |
rental |
standard quote |
delivery_note |
rental |
warehouse dispatch output |
pick_list |
rental |
warehouse pick output |
return_list |
return_list |
dedicated resolver type |
damage_report |
rental |
staged type with no seeded system template |
Invoice and billing family
| Document type | Resolver type | Source model |
|---|---|---|
invoice |
invoice |
Invoice::class |
pro_forma |
invoice |
Invoice::class |
deposit |
invoice |
Invoice::class |
credit_note |
credit_note |
CreditNote::class |
receipt |
receipt |
InvoicePayment::class |
statement |
statement |
Account::class |
purchase_order |
purchase_order |
PurchaseOrder::class |
Specialist document types
Core also ships label, equipment_test_certificate, catalogue_item_spec, asset_label, virtual_stock_summary, repair_report, stock_check_report, warehouse_transfer_manifest, activity_summary, fleet_vehicle_summary, equipment_test_summary, scan_session_report, and flightcase_manifest.
Every built-in type currently uses the default permission documents.create.
Source-model resolution, permissions, and categories
forSourceModel() matches a type when its registered model_type is the same class as the supplied model or a parent class of it.
forSourceModelPermitted() applies one more filter:
return Gate::forUser($user)->allows($definition['permission']);
If there is no user, it returns an empty list. This is the path used by PrintMenu when it decides which document types are available for a record.
typesInCategory() is an exact category filter. Today the only core non-null category is certificate, so typesInCategory('certificate') returns equipment_test_certificate.
Type mapping and aliases
allowedDocumentableTypeValues() builds the finite request-validation allow-list used by GenerateDocumentData::rules(). For each currently registered source-model class it includes the exact fully qualified class name, basename, lowercase basename, snake-case basename, and every alias exactly as registered. It removes duplicates while preserving first appearance. The Rule::in(...) check does not admit other spellings.
Direct resolveSourceModelClass() calls are more permissive than that finite request-validation allow-list. They accept an exact fully qualified class name, compare basenames and registered aliases case-insensitively, and finally accept a string that ends with the model's exact basename. Consequently a direct call can resolve a differently cased alias or an otherwise unlisted namespace suffix that request validation rejects. Callers that bypass GenerateDocumentData own that distinction.
Current core aliases include:
paymentforreceiptfleet_vehicleandvehicleforfleet_vehicle_summary
So resolveSourceModelClass('payment') resolves to InvoicePayment::class, and resolveSourceModelClass('vehicle') resolves to Account::class.
filenameSlug() is separate from the registry key. It slugifies the current label, which is why receipt produces the filename stem from Payment Receipt rather than from the raw type name.
Collisions and failure modes
register() stores definitions in an associative array keyed by document type. Registering the same type again replaces the earlier definition. Duplicate keys are not rejected.
Cross-model alias collisions are not rejected either. Resolution walks the unique source-model classes in their current first-appearance order, gathers all aliases belonging to each model, and returns on the first match. If two different source models claim the same alias, the first matching model in current registry insertion order wins.
That insertion order is also externally visible. all(), forSourceModel(), forSourceModelPermitted(), and typesInCategory() do not sort their results; filtering retains the current registry order. PrintMenu consumes that order without sorting when it builds the permitted print choices for a record. Replacing an existing document-type key updates its definition without moving that key to the end of the map.
get(), resolverType(), modelType(), permission(), label(), and filenameSlug() all fail through get(), which throws InvalidArgumentException with Unknown document type [{$documentType}].
resolveSourceModelClass() throws InvalidArgumentException with Unsupported documentable type [{$documentableType}] when the supplied input does not match any registered model or alias.
Worked example
These are all valid current lookups:
$registry = app(DocumentTypeRegistry::class);
$registry->resolverType('deposit'); // invoice
$registry->modelType('receipt'); // InvoicePayment::class
$registry->resolveSourceModelClass('payment'); // InvoicePayment::class
$registry->resolveSourceModelClass('vehicle'); // Account::class
The important contract distinction is that deposit is a document type, while invoice is the resolver type that actually renders it.