Laravel integration
quittio/laravel connects the country-neutral core with Laravel 13: a facade, the
configuration in config/quittio.php, Blade templates, responses, mail attachments,
storage on filesystem disks, queued rendering, events, a browser preview, Artisan commands
and testing helpers. It needs PHP 8.3 and Laravel 13.
composer require quittio/quittio # core, Laravel integration and country pack Germany
php artisan quittio:install # publishes config/quittio.php
Service provider
The package registers itself through Laravel’s package discovery
(composer.json: extra.laravel): the provider Quittio\Laravel\QuittioServiceProvider
and the alias Quittio for Quittio\Laravel\Facades\Quittio. With
"dont-discover": ["quittio/laravel"] in your composer.json, register both yourself in
bootstrap/providers.php and config/app.php.
| Registered | Details |
|---|---|
| Configuration | config/quittio.php merged with the package defaults (see Configuration) |
Quittio\Laravel\QuittioManager |
Singleton behind the facade, also available as app('quittio') |
Quittio\Laravel\Support\Preview |
Singleton that renders the sample scenarios (see Preview route) |
Quittio\Core\Serialization\Snapshots\SnapshotUpcasterChain |
Singleton with the upcasters of quittio.snapshots.upcasters; also the default chain of InvoiceData::fromSnapshot() |
| Views | Namespace quittio (quittio::templates.default.layout, its partials, quittio::preview.index) |
| Translations | Namespace quittio with the labels and messages of the core; the core uses Laravel’s translator (see Translations) |
| Routes | The preview routes, when enabled |
| Commands | quittio:* and make:quittio-template (console only, see Artisan commands) |
| Publish tags | quittio-config, quittio-views, quittio-lang, quittio-fonts (see Artisan commands) |
php artisan about |
Section “Quittio” with the active country packs and profiles |
The manager builds the core lazily, on the first call that needs it, from the configuration, the country packs, the Blade template engine, the PDF renderer, the logger and your extensions.
Facade
use Quittio\Laravel\Facades\Quittio;
Instead of the facade you can inject Quittio\Laravel\QuittioManager; both are the same
singleton.
| Method | Returns | Description |
|---|---|---|
make(InvoiceData\|ProvidesInvoiceData\|InvoiceBuilder $data) |
PendingInvoice |
Starts a render call. A builder is built, a ProvidesInvoiceData model converted with toInvoiceData(). |
document(DocumentData\|DocumentBuilder $document) |
PendingDocument |
Starts a render call for a quote, order confirmation, delivery note, proforma invoice, payment reminder or account statement (see Documents). |
cancellation(InvoiceData $original, string $number, ?string $reason = null, ?string $strategy = null, ?Date $issueDate = null, ?array $lines = null, ?CancellationHistory $history = null) |
InvoiceBuilder |
Builder for the cancellation of an issued invoice; render it with make(). See Cancellations. |
check(InvoiceData $invoice, RenderOptions $options) |
ValidationResult |
Validates like a render run; the fluent form is Quittio::make($invoice)->check(). |
prepare(InvoiceData $invoice, RenderOptions $options) |
PreparedInvoice |
Calculates and validates without rendering; fluent form ->prepare(). |
read(string $content) |
ReadResult |
Reads an incoming XML or PDF e-invoice from its content (bytes, never a path). See Reading incoming invoices. |
readFile(string $path) |
ReadResult |
The same from a file |
validateFile(string $content) |
ValidationResult |
Reads and validates an incoming XML or PDF (content) with all configured levels. See Validation. |
validatePath(string $path) |
ValidationResult |
The same from a file |
renderInvoice(InvoiceData $invoice, RenderOptions $options) |
RenderedInvoice |
The render run behind PendingInvoice::render(), with events |
renderDocument(DocumentData $document, RenderOptions $options) |
RenderedDocument |
The render run behind PendingDocument::render() |
extend(string $type, string $name, Closure $factory) |
QuittioManager |
Registers a renderer, validator, serializer or format resolver (see Extensions) |
registerCountry(CountryPack $pack) |
QuittioManager |
Registers a country pack; it wins over configured and discovered packs with the same code |
countries() |
CountryRegistry |
Active country packs: all(), has($code), get($code), getOrGeneric($code) |
profiles() |
ProfileRegistry |
Profiles of the core and the country packs: all(), has($key), get($key), bySpecificationId($id) |
formats() |
FormatRegistry |
Output formats: all(), has($key), get($key) |
settings() |
Settings |
Typed access to config('quittio') (see Settings) |
flush() |
QuittioManager |
Forgets the built core; the next call builds it again from the current configuration |
core() |
Quittio\Core\Quittio |
The core instance, e.g. Quittio::core()->config() or ->validators() |
coreWithLevels(array $levels) |
Quittio\Core\Quittio |
A new core (the cached one stays unchanged) that validates with validation.levels plus $levels, e.g. ['kosit', 'verapdf'], every driver with its settings of validation.drivers.*; used by quittio:validate --kosit --verapdf |
macro(), mixin(), hasMacro(), flushMacros() |
– | Macros of the manager (see below) |
fake() |
QuittioFake |
Replaces rendering in tests (see Testing) |
assertRendered(), assertRenderedCount(), assertNothingRendered(), assertDocumentRendered(), rendered(), documents() |
– | Assertions and recordings of the fake, only after fake() |
QuittioManager and PendingInvoice are macroable:
use Quittio\Laravel\Rendering\PendingInvoice;
PendingInvoice::macro('forPublicBuyer', function () {
/** @var PendingInvoice $this */
return $this->format('xrechnung-cii')->locale('de');
});
Quittio::make($invoice)->forPublicBuyer()->render();
Rendering invoices
$result = Quittio::make($invoice)->render(); // automatic choice
$result = Quittio::make($order)->format('zugferd')->profile('en16931')->locale('de')->render();
$result = Quittio::make($invoice)->formats(['zugferd', 'xrechnung-cii'])->render();
A run calculates the invoice (totals, VAT breakdown, due date from defaults.due_days),
chooses format and profile, validates, writes the XML, renders the Blade template and
produces the PDF/A with the embedded XML. All formats of one call share the same
calculation.
PendingInvoice
Quittio\Laravel\Rendering\PendingInvoice collects the options of one call. Every setter
returns the same object.
| Method | Effect |
|---|---|
format(string $format) |
One output format: zugferd (factur-x, facturx), xrechnung-cii (xrechnung), xrechnung-ubl, cii, ubl, peppol, pdf, html, json. Replaces earlier formats. |
formats(array $formats) |
Several formats from one calculation; duplicates are removed. The first is the main result. |
profile(Profile\|string $profile) |
Profile key (minimum, basic-wl, basic, en16931, extended, peppol; xrechnung and xrechnung-extension with the German country pack; aliases such as comfort) or a Profile object. Every requested format must allow it, otherwise the run throws a ConfigurationException. |
locale(string $locale) |
Locale of labels and formatting |
country(string $country) |
Leading country pack (upper-cased), instead of the seller’s country |
template(string $template) |
Blade view or plain PHP template name; only allowed views (see Blade templates) |
presentation(Presentation $presentation) |
Design for this call, merged over the configured design property by property; the invoice’s own presentation wins (see Templates and design) |
validate(bool $throwOnError = true) |
Validation mode error (true) or warn (false) for this call |
withoutValidation() |
Validation mode off for this call |
option(string $key, mixed $value) |
Option for the view model and the validation rules (see below) |
render() |
Renders: RenderedInvoice |
check() |
Validates like render() without template and PDF: ValidationResult |
prepare() |
Calculates and validates without rendering: PreparedInvoice |
queue(?string $connection = null, ?string $queue = null) |
Renders in a queued job: QueuedInvoice (see Queueing) |
data() |
The invoice data passed to make() (not yet calculated) |
options() |
The collected Quittio\Core\Rendering\RenderOptions |
Without an explicit value, each choice falls back in this order:
| Choice | Order |
|---|---|
| Format | format()/formats() → defaults.format → automatic choice (defaults.format null or 'auto'; Formats and profiles) |
| Profile | profile() → profile of the automatic choice → default profile of the format → defaults.profile; CII formats switch to EXTENDED when EXTENDED features are used and nothing was requested (format_resolver.auto_upgrade_profile) |
| Locale | locale() → invoice locale (InvoiceBuilder::locale()) → defaults.locale → country pack |
| Country pack | country() → seller country (BT-40) → defaults.country → generic EN 16931 pack |
| Template | template() → Presentation::$template of the invoice → pdf.template (the template of a presentation passed with presentation() is not used) |
| Validation | validate()/withoutValidation() → validation.mode |
Options of option() (they override configured values of the same name):
| Key | Value | Effect |
|---|---|---|
watermark |
string | Watermark text, e.g. 'ENTWURF'. Only for the formats pdf and html: with any format that produces e-invoice XML the run throws a ConfigurationException. Watermarked PDFs are not PDF/A. |
paper |
string | Paper size (like pdf.paper) |
markdown |
bool | Line descriptions and notes as Markdown (like pdf.markdown) |
fold_marks |
bool | Fold and punch marks (like pdf.fold_marks) |
girocode_amount |
'due'|'discounted' |
Amount in the GiroCode (like pdf.payment_qr.girocode_amount) |
Validation per call
In mode error, a run with errors (or with warnings and validation.block_on_warnings)
throws Quittio\Core\Validation\ValidationException; its result property holds the
ValidationResult. In mode warn the result is attached and rendering continues; in mode
off nothing is validated.
use Quittio\Core\Validation\ValidationException;
try {
$result = Quittio::make($invoice)->format('xrechnung-cii')->render();
} catch (ValidationException $e) {
return back()->withErrors(array_map(fn ($m) => $m->message, $e->result->errors()));
}
$result = Quittio::make($invoice)->validate(false)->render(); // never throws on findings
$result->validation()->warnings();
check() always validates with the configured levels, whatever the mode, and never throws
on findings; it does not render the template or the PDF (so the PDF/A check L6 does not run):
$report = Quittio::make($invoice)->format('xrechnung-cii')->check();
$report->isValid();
prepare() returns a Quittio\Core\Rendering\PreparedInvoice with the decisions of a run:
| Member | Content |
|---|---|
data |
Calculated InvoiceData |
formats |
Format key => Profile (null for formats without XML) |
country, buyerCountry |
Leading country pack and the buyer’s pack (or null) |
locale |
Chosen locale |
validation |
ValidationResult of the data and XML levels |
format(), profile() |
First format and its profile |
Validation levels and messages: Validation.
RenderedInvoice
render() returns Quittio\Laravel\Rendering\RenderedInvoice for the first format. It
implements Illuminate\Contracts\Mail\Attachable and Illuminate\Contracts\Support\Responsable.
| Method | Returns |
|---|---|
data() |
Calculated InvoiceData (totals, VAT breakdown, due date) |
format() |
Format key of this result |
formats() |
All format keys of the run |
as(string $format) |
The result of another format of the same run; a format that was not rendered throws a LogicException |
profile() |
Profile of the XML, null for formats without XML |
validation() |
ValidationResult of this format (warnings; in mode warn also errors) |
hasPdf(), hasXml() |
Whether the format produced a PDF or XML |
pdf() |
PDF bytes (zugferd, pdf); other formats throw a LogicException |
xml() |
XML (zugferd, xrechnung-cii, xrechnung-ubl, cii, ubl, peppol, pdf with b2c_embed_xml); other formats throw |
html() |
HTML (format html only); other formats throw |
json() |
The calculated invoice as JSON (InvoiceData::toJson()), for every format |
contents() |
Main output: PDF, otherwise XML, HTML or JSON |
mimeType() |
application/pdf, application/xml, text/html or application/json |
filename(?string $extension = null) |
File name from pdf.filename, e.g. Rechnung_RE-2026-0042.pdf; with $extension for another output, e.g. filename('xml') |
hashes() |
SHA-256 of the outputs: ['pdf' => …, 'xml' => …, 'html' => …] (only those produced) |
download(?string $filename = null) |
HTTP response as attachment |
stream(?string $filename = null) |
HTTP response for inline display in the browser |
toResponse($request) |
Same as download(): return the result from a controller |
toMailAttachment() |
Illuminate\Mail\Attachment with file name and MIME type |
save(?string $disk = null, ?string $path = null) |
Stores the main output on a filesystem disk, returns the path |
core() |
The core result Quittio\Core\Rendering\RenderedInvoice (adds extension() and save(string $localPath) for local files) |
File names
The pattern pdf.filename (default {title}_{number}.{ext}) knows {title} (document
title in the render locale, e.g. “Rechnung”, “Stornorechnung”, “Invoice”), {number},
{date} (issue date Y-m-d), {format} and {ext}. Title and number are reduced to
letters, digits, ., _ and -.
// 'filename' => '{date}_{number}.{ext}'
$result->filename(); // 2026-10-03_RE-2026-0042.pdf
$result->as('xrechnung-cii')->filename(); // 2026-10-03_RE-2026-0042.xml
Responses
public function show(Invoice $invoice)
{
$result = Quittio::make($invoice)->format('zugferd')->render();
return $result; // download (Content-Disposition: attachment)
// return $result->download('invoice.pdf'); // own file name
// return $result->stream(); // inline, e.g. in a browser tab or iframe
}
The response carries the content as body, Content-Type from mimeType(),
Content-Disposition with an ASCII fallback name and the UTF-8 name (filename*=), and
Cache-Control: private, no-store. The fallback transliterates (Rechnung Über.pdf becomes
Rechnung_Uber.pdf, ß becomes ss); every other character outside letters, digits, .,
_ and - becomes one _. stream() sends the complete file inline; it is not a
chunked StreamedResponse.
Storage
$path = $result->save(); // quittio.storage.disk, file name of the result
$path = $result->save('invoices', '2026/'); // directory (ends with "/"): 2026/Rechnung_RE-2026-0042.pdf
$path = $result->as('xrechnung-cii')->save('invoices', '2026/RE-2026-0042.xml');
save() stores contents() (the main output) with Storage::disk($disk)->put(). The disk
defaults to quittio.storage.disk, and to the default disk when that is empty; the path
defaults to filename(). Existing files are overwritten (unlike the archive of the Managed
layer). For a local path without a disk use $result->core()->save('/var/invoices').
Mail attachments
RenderedInvoice and RenderedDocument are attachable:
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Quittio\Laravel\Rendering\RenderedInvoice;
final class InvoiceMail extends Mailable
{
public function __construct(private RenderedInvoice $invoice) {}
public function envelope(): Envelope
{
return new Envelope(subject: 'Invoice '.$this->invoice->data()->number);
}
public function content(): Content
{
return new Content(view: 'mail.invoice');
}
public function attachments(): array
{
return [$this->invoice]; // file name and MIME type of the result
}
}
Mail::to($customer)->send(new InvoiceMail(Quittio::make($invoice)->format('zugferd')->render()));
->attach($result) works in build() and in notification mail messages as well. For an
XRechnung, attach the XML result ($result->as('xrechnung-cii')); add a PDF visualisation
with a second pdf format of the same run if the recipient wants one. For queued mailables,
consider storing the file first (save()) and attaching it with
Attachment::fromStorageDisk(), so that the file does not travel through the queue.
The Managed layer ships its own mailable and delivery tracking (see Managed layer).
Queueing
$job = Quittio::make($invoice)->format('zugferd')->queue()->save('invoices', '2026/');
$job = Quittio::make($invoice)->queue('redis', 'invoices')->save();
queue() returns a Quittio\Laravel\Rendering\QueuedInvoice; its only method,
save(?string $disk = null, ?string $path = null), dispatches a
Quittio\Laravel\Jobs\RenderInvoice job and returns it.
| Detail | Behaviour |
|---|---|
| Connection, queue | Arguments of queue(), else quittio.queue.connection and quittio.queue.queue (default queue quittio: start a worker with --queue=quittio,…) |
| Payload | The invoice as versioned JSON snapshot (invoice), the RenderOptions of the call (options), disk and path (public properties) |
| Execution | handle() renders with the stored options (events included) and calls save($disk, $path); a null disk is resolved when the job runs |
| Retries | $tries = 3 |
| Upgrades | Snapshots of older data model versions are migrated by quittio.snapshots.upcasters; data() returns the restored invoice |
A validation error makes the job fail like any exception. The job returns nothing; listen
for InvoiceRendered if you need the result. With the sync connection the job runs
immediately.
Cancellations
$builder = Quittio::cancellation($original, 'ST-2026-0001', reason: 'Charged twice');
Quittio::make($builder)->render();
// partial cancellation of line 2 (one unit), with the earlier partial cancellations
Quittio::cancellation($original, 'ST-2026-0002', lines: [['line' => '2', 'quantity' => 1]], history: $history);
The strategy (credit_note or negative_invoice) comes from the argument, then
corrections.cancellation_strategy, then the country pack; the locale from the original,
then defaults.locale, then the country pack. Details:
Getting started and, for stored invoices,
Managed layer.
Documents
use Quittio\Core\Documents\DocumentBuilder;
use Quittio\Core\Enums\VatCategory;
use Quittio\Laravel\Seller;
$quote = DocumentBuilder::quote()->number('AN-2026-0007')->issueDate('2026-10-01')
->seller(Seller::fromConfig())
->buyer(fn ($b) => $b->name('Kunde AG')->address('Weg 1', '54321', 'Beispielstadt', 'DE'))
->line(fn ($l) => $l->name('Beratung')->quantity(2, 'HUR')->netPrice('100.00')->vat(VatCategory::StandardRate, '19'));
return Quittio::document($quote)->locale('de')->render()->download();
Quittio\Laravel\Rendering\PendingDocument:
| Method | Effect |
|---|---|
template(string $template) |
Template (same allowlist as invoices) |
locale(string $locale) |
Locale |
country(string $country) |
Leading country pack |
presentation(Presentation $presentation) |
Design for this call |
htmlOnly() |
Render HTML only, no PDF |
render() |
RenderedDocument |
Documents never contain XML and are not validated. RenderedDocument offers data()
(DocumentData), hasPdf() (false after htmlOnly()), pdf() (throws after
htmlOnly()), html(), contents(), mimeType(),
filename(?string $extension = null), core() and, like invoices, download(), stream(),
toResponse(), toMailAttachment() and save(). Document types and their data:
Quotes and other documents.
Events
All events are in the namespace Quittio\Laravel\Events and dispatched by
QuittioManager::renderInvoice(), i.e. by PendingInvoice::render(), queued
RenderInvoice jobs and the rendering of the Managed layer.
| Event | Dispatched | Payload |
|---|---|---|
InvoiceRendering |
Before the invoice is calculated, validated and rendered | invoice (InvoiceData as passed, not calculated), options (RenderOptions) |
InvoiceValidated |
After all formats were rendered, before InvoiceRendered, when the run validated and validation did not block it: in mode error and in mode warn; not in mode off (nothing was validated) |
invoice (calculated InvoiceData), result (ValidationResult of the main format; in mode warn it may contain errors, so check isValid()) |
InvoiceValidationFailed |
When validation blocked the run (mode error); the ValidationException is thrown afterwards |
invoice (InvoiceData as passed), result (ValidationResult) |
InvoiceRendered |
After all requested formats were produced | result (Quittio\Laravel\Rendering\RenderedInvoice) |
MandatoryFieldMissing |
When the template omitted mandatory values, in the guard modes exception (before the exception is thrown) and log |
invoice (calculated InvoiceData), fields (list of Quittio\Core\Countries\MandatoryField with accessKey, description, legalBasis) |
No events are dispatched for documents, check(), prepare(), reading or validating files,
or while Quittio::fake() is active.
use Illuminate\Support\Facades\Event;
use Quittio\Laravel\Events\InvoiceRendered;
Event::listen(function (InvoiceRendered $event) {
Log::info('Invoice rendered', [
'number' => $event->result->data()->number,
'format' => $event->result->format(),
'hashes' => $event->result->hashes(),
]);
});
The Managed layer dispatches further events for the lifecycle of stored invoices (see Managed layer).
Seller from the configuration
Quittio\Laravel\Seller turns the seller section of config/quittio.php (see
Configuration) into invoice data.
| Method | Effect |
|---|---|
Seller::fromConfig(): SellerParty |
Builds the seller: name, VAT identifier, tax number, address, legal registration, identifiers, additional legal information, contact (when one of its values is set) and electronic address (when set) |
Seller::apply(InvoiceBuilder $builder): InvoiceBuilder |
Sets the seller on the builder, adds a credit transfer with the first bank account that has an IBAN and, with seller.small_business, flags the invoice as small business invoice (Meta::FLAG_SMALL_BUSINESS). Returns the same builder. |
$invoice = Seller::apply(InvoiceBuilder::make())
->number('RE-2026-0042')
// ...
->build();
$quote = DocumentBuilder::quote()->seller(Seller::fromConfig());
With the Managed layer, issuer profiles hold the seller instead ($issuer->builder()).
Blade templates
The core renders the view model with Quittio\Laravel\Templates\BladeTemplateEngine. A view
receives one variable, $invoice (Quittio\Core\View\InvoiceViewModel).
| View | Use |
|---|---|
quittio::templates.default.layout |
Default template (pdf.template) |
quittio::templates.default.partials.* |
Its partials (letterhead, recipient, lines, totals, payment, …) |
invoices.{name}.layout |
Templates created by make:quittio-template {name} |
Customise the design in levels (see Templates and design): configuration
(pdf.*), published partials (php artisan vendor:publish --tag=quittio-views copies them
to resources/views/vendor/quittio/templates, which Laravel prefers over the package
views), an own template (make:quittio-template) or an own renderer.
Template names may come from invoice data (Presentation::$template) or issuer branding,
so only these views can be rendered:
default, which means the view ofpdf.template,- the view configured in
pdf.template, - the shipped views under
quittio::templates.(not their partials), - the views of
pdf.templates(default['invoices.*']; an entry ending in*allows every view below that prefix, e.g.mytheme::*). Views containing.partials.never qualify.
Any other existing view fails with an InvalidArgumentException. A name that is no Blade
view falls back to the plain PHP templates of the core, which resolve names inside their
template directories only (never file paths).
// config/quittio.php
'pdf' => [
'template' => 'invoices.acme.layout', // every invoice
'templates' => ['invoices.*', 'mytheme::*'], // selectable per call or per invoice
],
Quittio::make($invoice)->template('mytheme::invoice')->render();
Preview route
With quittio.preview.enabled (default: only in the local environment) the provider
registers two routes under quittio.preview.path with the middleware
quittio.preview.middleware:
| Route | Name | Response |
|---|---|---|
GET /_quittio/preview |
quittio.preview.index |
List of the sample scenarios with HTML and PDF links |
GET /_quittio/preview/{scenario} |
quittio.preview.show |
The scenario as HTML, with ?format=pdf as inline PDF |
Query parameters of show: format (pdf, anything else means HTML), template (the
views that rendering accepts, see Blade templates: default,
pdf.template, the shipped views under quittio::templates. and the views of
pdf.templates, never partials; the view must exist) and locale (a locale code such as
en or de_CH). Unknown scenarios, other or missing templates and invalid locales return
404, so no request can name a file or an arbitrary view.
The preview renders the sample scenarios with the configured design,
validation mode warn and a watermark (“PREVIEW”, in German “VORSCHAU”); the locale defaults
to the scenario’s locale. The routes are not registered while routes are cached
(php artisan route:cache). Enabled outside local, protect them with middleware such as
['web', 'auth']. On the command line: php artisan quittio:preview (see
Artisan commands).
Quittio\Laravel\Support\Preview is the service behind route and command:
| Method | Description |
|---|---|
scenarios(): array |
Names of the sample scenarios |
allowsTemplate(string $template): bool |
Whether the preview may render this template: an existing view that rendering accepts (BladeTemplateEngine::fromSettings()), or default |
allowsLocale(string $locale): bool |
Whether the locale code is well-formed |
render(string $scenario, string $format = 'pdf', ?string $template = null, ?string $locale = null): RenderedInvoice |
Renders a scenario (pdf or html) with the watermark; invalid arguments throw an InvalidArgumentException |
Translations
Labels, rule messages, code names and terms come from the core’s language files
(labels.php, rules.php, codes.php, terms.php; German, English and French, French
without rule messages) in the translation namespace quittio. The core translates through
Laravel’s translator, so the usual overrides work:
php artisan vendor:publish --tag=quittio-lang # lang/vendor/quittio/{locale}/*.php
A published file only needs the keys you change; Laravel merges it over the package file.
// lang/vendor/quittio/de/labels.php
return ['amount_due' => 'Bitte überweisen'];
// or at runtime
app('translator')->addLines(['labels.amount_due' => 'Bitte überweisen'], 'de', 'quittio');
Country packs add their own namespace quittio-{code}, e.g. quittio-de (rules.php,
tax.php), overridable in lang/vendor/quittio-de/{locale}/. The Managed layer uses
quittio-managed (--tag=quittio-managed-lang). Labels of a single invoice can be changed
with Presentation::$labels. Invoices are translated in the render locale, not in the
application locale.
Logging
The manager passes Laravel’s logger (Psr\Log\LoggerInterface, the default log channel) to
the core. It receives the warnings of the mandatory field guard in mode log
(validation.mandatory_field_guard; default in production). To route them elsewhere,
listen for the MandatoryFieldMissing event, which is dispatched in the modes log and
exception.
Extensions
Quittio::extend(string $type, string $name, Closure $factory) registers a driver. The
factory receives the container and the driver’s configuration array and must return an
instance of the expected interface, otherwise building the core fails with a
ConfigurationException. Unknown types throw an InvalidArgumentException. extend()
forgets the built core, so call it in the boot() method of a service provider.
| Type | Interface | Used when | Configuration passed |
|---|---|---|---|
renderer |
Quittio\Core\Pdf\PdfRenderer |
pdf.renderer is {name} |
pdf.renderers.{name} |
validator |
Quittio\Core\Validation\ValidationDriver |
{name} is listed in validation.levels |
validation.drivers.{name} |
serializer |
Quittio\Core\Xml\XmlSerializer |
always; replaces the writer of its syntax | serializers.{name} |
format_resolver |
Quittio\Core\Formats\FormatResolver |
format_resolver.driver is {name} |
format_resolver (including peppol_lookup) |
use Illuminate\Contracts\Container\Container;
use Quittio\Core\Pdf\Renderers\CallbackRenderer;
public function boot(): void
{
Quittio::extend('renderer', 'gotenberg', fn (Container $app, array $config) => new CallbackRenderer(
fn (string $html) => $app->make(GotenbergClient::class)->pdf($html, $config),
));
}
// config/quittio.php: 'pdf' => ['renderer' => 'gotenberg', 'renderers' => ['gotenberg' => ['url' => '…']]]
The callback of Quittio\Core\Pdf\Renderers\CallbackRenderer receives the HTML and the
RenderRequest and returns any PDF; CallbackRenderer then turns it into PDF/A-3 and
embeds the XML itself (design level 4 in Templates and design). A class
implementing PdfRenderer directly receives the RenderRequest (html, title, author,
paper, pdfa, embedded, font) and must produce PDF/A and the embedded file itself. A
validator driver returns its levels, decides with supports() which subjects it checks and
returns a ValidationResult (see Validation).
Peppol lookup
The default format resolver chooses peppol only with a lookup (see
Formats and profiles). Name an invokable class in
format_resolver.peppol_lookup; it is resolved from the container, receives the
BuyerParty and returns whether the buyer is reachable via Peppol:
use Quittio\Core\Data\Party\BuyerParty;
final class PeppolDirectoryLookup
{
public function __construct(private PeppolDirectory $directory) {}
public function __invoke(BuyerParty $buyer): bool
{
$address = $buyer->electronicAddress; // BT-49
if ($address === null) {
return false;
}
$participant = $address->scheme->value.':'.$address->value; // e.g. 0208:0123456749
return Cache::remember('peppol:'.$participant, now()->addDay(), fn (): bool => $this->directory->isRegistered($participant));
}
}
// config/quittio.php: 'format_resolver' => ['peppol_lookup' => PeppolDirectoryLookup::class, …]
The lookup runs for every automatic choice with a business buyer that no domestic country
rule decided (for German sellers: cross-border invoices), so cache its answers. Quittio ships
no Peppol directory client. A resolver registered with Quittio::extend('format_resolver', …)
receives the value in its configuration and decides itself whether to use it.
Settings
Quittio::settings() returns Quittio\Laravel\Support\Settings, typed read access to
config('quittio') with dot notation. Wrong types throw a ConfigurationException naming
the key.
| Method | Returns |
|---|---|
get(string $key, mixed $default = null) |
Raw value |
string(string $key, string $default) |
String; null and '' give the default |
nullableString(string $key) |
String or null |
bool(string $key, bool $default) |
Boolean |
int(string $key, int $default) |
Integer |
array(string $key) |
Array (missing = []) |
strings(string $key) |
The string values of a list |
section(string $key) |
Settings for a sub-array, e.g. section('validation.drivers.kosit') |
Country packs
Country packs are discovered from Composer metadata, listed in quittio.countries or
registered with Quittio::registerCountry() (see
Configuration). Their translations, profiles, formats and
rules are registered with them. Available packs and writing your own:
Country packs.
Managed layer
quittio/managed stores invoices in the database and builds on this package: it renders
through the same QuittioManager (so configuration, templates, events and Quittio::fake()
apply), reads pdf.template and pdf.templates to check issuer branding, uses
corrections.cancellation_strategy and adds commands. It implements two contracts of this
package:
| Contract | Used by |
|---|---|
Quittio\Laravel\Contracts\ManagedInstaller |
php artisan quittio:install --managed (migrations, first issuer profile) |
Quittio\Laravel\Contracts\ManagedInvoiceRenderer |
php artisan quittio:render {invoice} (render a stored invoice again) |
Issuer profiles replace the seller section, and the Managed archive uses its own disk and
queue settings (config/quittio-managed.php). See Managed layer.