SIGNALS Documentation
API Reference

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:

  • payment for receipt
  • fleet_vehicle and vehicle for fleet_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.