digitaldev-lx/laravel-moloni
Composer 安装命令:
composer require digitaldev-lx/laravel-moloni
包简介
A Laravel package for integrating with the Moloni invoicing API
README 文档
README
A Laravel package for integrating with the Moloni invoicing API. Provides a clean, fluent interface to manage companies, customers, products, invoices, and all other Moloni resources with full type safety through DTOs and enums.
Official page: digitaldev.pt/packages/laravel-moloni
Requirements
- PHP 8.4+
- Laravel 12 or 13
Installation
Install the package via Composer:
composer require digitaldev-lx/laravel-moloni
Publish the configuration file:
php artisan vendor:publish --tag=moloni-config
Run the migrations (required for OAuth token storage):
php artisan migrate
Configuration
Add the following environment variables to your .env file:
MOLONI_CLIENT_ID=your-client-id MOLONI_CLIENT_SECRET=your-client-secret MOLONI_USERNAME=your-username MOLONI_PASSWORD=your-password MOLONI_COMPANY_ID=your-company-id # Optional. Defaults to production. Override to point at sandbox. # Production: https://api.moloni.pt/v1/ # Sandbox: https://api.moloni.pt/sandbox/ MOLONI_BASE_URL=https://api.moloni.pt/v1/
You can obtain your API credentials from the Moloni Developer Portal.
Sandbox
Moloni provides a sandbox environment for testing API integrations without affecting production data. The sandbox is available at https://api.moloni.pt/sandbox/ and exposes the same endpoints as production.
Getting sandbox credentials
- You need an active Moloni account. If your login does not yet have access to the demonstration company, request it from
apoio@moloni.ptindicating the email associated with your login. - The OAuth2
client_id/client_secretare the same as in production. Only thebase_urlchanges. - Optionally, explore the API interactively at the Moloni API Explorer.
Switching environments
Set MOLONI_BASE_URL in your .env:
# Sandbox MOLONI_BASE_URL=https://api.moloni.pt/sandbox/ # Production (or omit the variable entirely) MOLONI_BASE_URL=https://api.moloni.pt/v1/
Important: token persistence between environments
OAuth2 tokens are stored in the moloni_tokens table and are not scoped per environment. When you switch MOLONI_BASE_URL, the existing token is no longer valid for the new host and the package will silently re-authenticate on the next call. To force a clean state, truncate the table:
php artisan tinker --execute="\DigitaldevLx\LaravelMoloni\Models\MoloniToken::truncate();"
A common pattern is to use a separate database for local/testing (already the default with SQLite) so production tokens are never reused against the sandbox host.
Usage
Using the Facade
All API interactions are available through the Moloni facade:
use DigitaldevLx\LaravelMoloni\Facades\Moloni;
Companies
// List all companies $companies = Moloni::companies()->getAll();
Customers
$companyId = config('moloni.company_id'); // List all customers $customers = Moloni::customers()->getAll($companyId); // Find a customer by VAT number $customer = Moloni::customers()->getByVat($companyId, '123456789'); // Create a customer using a DTO use DigitaldevLx\LaravelMoloni\DataTransferObjects\Customer as CustomerDto; $dto = new CustomerDto( vat: '123456789', number: 'C001', name: 'John Doe', email: 'john@example.com', address: '123 Main Street', city: 'Lisbon', zipCode: '1000-001', countryId: 1, ); $customer = Moloni::customers()->insert($companyId, $dto); // Create a customer using an array $customer = Moloni::customers()->insert($companyId, [ 'vat' => '123456789', 'number' => 'C001', 'name' => 'John Doe', 'email' => 'john@example.com', ]);
Products
// List all products $products = Moloni::products()->getAll($companyId); // Find a product by reference $product = Moloni::products()->getByReference($companyId, 'PROD-001'); // Create a product using a DTO use DigitaldevLx\LaravelMoloni\DataTransferObjects\Product as ProductDto; use DigitaldevLx\LaravelMoloni\Enums\ProductType; $dto = new ProductDto( name: 'Widget', reference: 'PROD-001', type: ProductType::Product, categoryId: 1, unitId: 1, price: 29.99, ); $product = Moloni::products()->insert($companyId, $dto);
Invoices
use DigitaldevLx\LaravelMoloni\DataTransferObjects\Document as DocumentDto; use DigitaldevLx\LaravelMoloni\DataTransferObjects\DocumentProduct; use DigitaldevLx\LaravelMoloni\DataTransferObjects\Payment; // Create an invoice $dto = new DocumentDto( documentSetId: 1, customerId: 1, date: '2026-03-31', expirationDate: '2026-04-30', products: [ new DocumentProduct( productId: 1, qty: 2, price: 29.99, ), ], payments: [ new Payment( paymentMethodId: 1, value: 59.98, date: '2026-03-31', ), ], ); $invoice = Moloni::invoices()->insert($companyId, $dto); // Get PDF link for a document $pdfLink = Moloni::invoices()->getPdfLink($companyId, $invoiceId);
Other Document Types
The package supports all Moloni document types through dedicated resources:
Moloni::receipts()- ReceiptsMoloni::creditNotes()- Credit NotesMoloni::debitNotes()- Debit NotesMoloni::simplifiedInvoices()- Simplified InvoicesMoloni::invoiceReceipts()- Invoice ReceiptsMoloni::deliveryNotes()- Delivery NotesMoloni::billsOfLading()- Bills of LadingMoloni::waybills()- WaybillsMoloni::estimates()- EstimatesMoloni::purchaseOrders()- Purchase OrdersMoloni::internalDocuments()- Internal DocumentsMoloni::paymentReturns()- Payment ReturnsMoloni::ownAssetsMovementGuides()- Own Asset Movement GuidesMoloni::customerReturnNotes()- Customer Return NotesMoloni::globalGuides()- Global GuidesMoloni::supplierInvoices()- Supplier InvoicesMoloni::supplierPurchaseOrders()- Supplier Purchase OrdersMoloni::supplierSimplifiedInvoices()- Supplier Simplified InvoicesMoloni::supplierCreditNotes()- Supplier Credit NotesMoloni::supplierDebitNotes()- Supplier Debit NotesMoloni::supplierReturnNotes()- Supplier Return NotesMoloni::supplierReceipts()- Supplier ReceiptsMoloni::supplierWarrantyRequests()- Supplier Warranty RequestsMoloni::documents()- Generic Documents
Settings Resources
Access configuration resources for taxes, payment methods, document sets, and more:
$taxes = Moloni::taxes()->getAll($companyId); $paymentMethods = Moloni::paymentMethods()->getAll($companyId); $documentSets = Moloni::documentSets()->getAll($companyId); $warehouses = Moloni::warehouses()->getAll($companyId); $units = Moloni::measurementUnits()->getAll($companyId); $maturityDates = Moloni::maturityDates()->getAll($companyId); $deliveryMethods = Moloni::deliveryMethods()->getAll($companyId); $bankAccounts = Moloni::bankAccounts()->getAll($companyId); // Global resources (no company_id needed) $countries = Moloni::countries()->getAll(); $currencies = Moloni::currencies()->getAll(); $languages = Moloni::languages()->getAll(); $exemptions = Moloni::taxExemptions()->getAll(); $fiscalZones = Moloni::fiscalZones()->getAll($countryId);
Using DTOs
The package provides readonly Data Transfer Objects for type-safe data handling:
Customer- Customer dataSupplier- Supplier dataSalesman- Salesman dataProduct- Product dataProductStock- Stock movement dataDocument- Document header dataDocumentProduct- Document line item dataPayment- Payment dataAddress- Address dataTax- Tax dataVehicle- Vehicle dataDeduction- Withholding tax dataPriceClass- Price class data
All DTOs are located in the DigitaldevLx\LaravelMoloni\DataTransferObjects namespace.
Using Events
The package dispatches events for all mutation operations:
| Event | Dispatched When |
|---|---|
DocumentCreated |
A document is successfully created |
DocumentUpdated |
A document is updated |
DocumentDeleted |
A document is deleted |
DocumentCancelled |
A document is cancelled |
DocumentClosed |
A document is closed/finalized |
CustomerCreated |
A new customer is created |
CustomerUpdated |
A customer is updated |
CustomerDeleted |
A customer is deleted |
SupplierCreated |
A new supplier is created |
SupplierUpdated |
A supplier is updated |
SupplierDeleted |
A supplier is deleted |
SalesmanCreated |
A new salesman is created |
SalesmanUpdated |
A salesman is updated |
SalesmanDeleted |
A salesman is deleted |
ProductCreated |
A new product is created |
ProductUpdated |
A product is updated |
ProductDeleted |
A product is deleted |
TokenRefreshed |
The OAuth token is refreshed |
DocumentCreated, DocumentUpdated and DocumentDeleted carry both the API response and a documentType string (e.g. 'invoices', 'creditNotes') so a single listener can route by document type.
All events are located in the DigitaldevLx\LaravelMoloni\Events namespace.
Auto-discovery (recommended)
Create a listener class in your app/Listeners directory. Laravel automatically discovers and registers listeners based on the type-hint in the handle method:
use DigitaldevLx\LaravelMoloni\Events\DocumentCreated; class SendInvoiceNotification { public function handle(DocumentCreated $event): void { // $event->data contains the API response // $event->documentType contains the document type } }
Manual registration
Register listeners manually in your AppServiceProvider:
use DigitaldevLx\LaravelMoloni\Events\DocumentCreated; use Illuminate\Support\Facades\Event; public function boot(): void { Event::listen( DocumentCreated::class, SendInvoiceNotification::class, ); }
Closure listeners
use DigitaldevLx\LaravelMoloni\Events\DocumentCreated; use Illuminate\Support\Facades\Event; Event::listen(function (DocumentCreated $event) { // ... });
Error Handling
The package provides granular exception handling that maps directly to Moloni's error system.
Exception Types
| Exception | When |
|---|---|
AuthenticationException |
OAuth2 errors (invalid credentials, expired tokens, invalid client) |
ValidationException |
Data validation errors (required fields, invalid NIF, invalid email, etc.) |
RateLimitException |
API rate limit exceeded (HTTP 429) |
MoloniException |
All other API errors |
All exceptions extend MoloniException, so you can catch them all with a single catch block or handle each type individually.
Authentication Errors
use DigitaldevLx\LaravelMoloni\Exceptions\AuthenticationException; use DigitaldevLx\LaravelMoloni\Enums\AuthError; try { $customers = Moloni::customers()->getAll($companyId); } catch (AuthenticationException $e) { $e->authError; // AuthError enum (e.g. AuthError::InvalidGrant) $e->errorDescription; // Moloni's error description string $e->getMessage(); // Full formatted message }
Available AuthError enum values: InvalidClient, InvalidUri, RedirectUriMismatch, InvalidRequest, UnsupportedGrantType, UnauthorizedClient, InvalidGrant, InvalidScope.
Validation Errors
use DigitaldevLx\LaravelMoloni\Exceptions\ValidationException; use DigitaldevLx\LaravelMoloni\Enums\ValidationErrorCode; try { $customer = Moloni::customers()->insert($companyId, $data); } catch (ValidationException $e) { $e->errors; // Array of ['code' => int, 'field' => string, 'description' => string] $e->getFieldErrors(); // ['field_name' => 'description', ...] $e->hasFieldError('vat'); // true/false }
Validation error codes (mapped via ValidationErrorCode enum):
| Code | Meaning |
|---|---|
| 1 | Campo obrigatorio |
| 2 | Campo numerico invalido |
| 3 | Endereco de email invalido |
| 4 | Valor deve ser unico |
| 5 | Valor invalido |
| 6 | URL invalido |
| 7 | Codigo postal invalido |
| 8 | NIF portugues invalido |
| 9 | Data deve estar no formato AAAA-MM-DD |
| 10 | Associacao de documento invalida |
| 11 | Documento nao pode ser enviado para a AT |
| 12 | Data invalida |
| 13 | Numero de telefone invalido |
| 14 | Artigo tem taxas conflituantes |
| 15 | Artigo tem multiplas entradas de IVA |
| 16 | Identificacao do cliente obrigatoria (Art. 36 CIVA) |
| 17 | Limite de caracteres excedido |
Catching All Errors
use DigitaldevLx\LaravelMoloni\Exceptions\MoloniException; use DigitaldevLx\LaravelMoloni\Exceptions\AuthenticationException; use DigitaldevLx\LaravelMoloni\Exceptions\ValidationException; use DigitaldevLx\LaravelMoloni\Exceptions\RateLimitException; try { $invoice = Moloni::invoices()->insert($companyId, $data); } catch (AuthenticationException $e) { // Handle auth errors (invalid credentials, expired tokens) } catch (ValidationException $e) { // Handle validation errors (invalid data) } catch (RateLimitException $e) { // Handle rate limiting (retry later) } catch (MoloniException $e) { // Handle all other API errors }
HasMoloniDocuments Trait
Add the HasMoloniDocuments trait to any Eloquent model to associate it with Moloni documents:
use DigitaldevLx\LaravelMoloni\Concerns\HasMoloniDocuments; class Order extends Model { use HasMoloniDocuments; } // Then use it $order->moloniDocuments;
Available Resources
Account & Profile
| Resource | Accessor Method |
|---|---|
| My Profile | Moloni::myProfile() |
Company
| Resource | Accessor Method |
|---|---|
| Companies | Moloni::companies() |
| Subscription | Moloni::subscription() |
| Users | Moloni::users() |
Entities
| Resource | Accessor Method |
|---|---|
| Customers | Moloni::customers() |
| Customer Alternate Addresses | Moloni::customerAlternateAddresses() |
| Suppliers | Moloni::suppliers() |
| Salesmen | Moloni::salesmen() |
Products
| Resource | Accessor Method |
|---|---|
| Products | Moloni::products() |
| Product Categories | Moloni::productCategories() |
| Product Stocks | Moloni::productStocks() |
| Product Properties | Moloni::productProperties() |
| Price Classes | Moloni::priceClasses() |
Documents
| Resource | Accessor Method |
|---|---|
| Documents | Moloni::documents() |
| Invoices | Moloni::invoices() |
| Receipts | Moloni::receipts() |
| Credit Notes | Moloni::creditNotes() |
| Debit Notes | Moloni::debitNotes() |
| Simplified Invoices | Moloni::simplifiedInvoices() |
| Invoice Receipts | Moloni::invoiceReceipts() |
| Delivery Notes | Moloni::deliveryNotes() |
| Bills of Lading | Moloni::billsOfLading() |
| Waybills | Moloni::waybills() |
| Estimates | Moloni::estimates() |
| Purchase Orders | Moloni::purchaseOrders() |
| Internal Documents | Moloni::internalDocuments() |
| Payment Returns | Moloni::paymentReturns() |
| Own Asset Movement Guides | Moloni::ownAssetsMovementGuides() |
| Customer Return Notes | Moloni::customerReturnNotes() |
| Global Guides | Moloni::globalGuides() |
| Supplier Invoices | Moloni::supplierInvoices() |
| Supplier Purchase Orders | Moloni::supplierPurchaseOrders() |
| Supplier Simplified Invoices | Moloni::supplierSimplifiedInvoices() |
| Supplier Credit Notes | Moloni::supplierCreditNotes() |
| Supplier Debit Notes | Moloni::supplierDebitNotes() |
| Supplier Return Notes | Moloni::supplierReturnNotes() |
| Supplier Receipts | Moloni::supplierReceipts() |
| Supplier Warranty Requests | Moloni::supplierWarrantyRequests() |
Settings
| Resource | Accessor Method |
|---|---|
| Taxes | Moloni::taxes() |
| Tax Exemptions | Moloni::taxExemptions() |
| Payment Methods | Moloni::paymentMethods() |
| Document Sets | Moloni::documentSets() |
| Warehouses | Moloni::warehouses() |
| Measurement Units | Moloni::measurementUnits() |
| Maturity Dates | Moloni::maturityDates() |
| Delivery Methods | Moloni::deliveryMethods() |
| Bank Accounts | Moloni::bankAccounts() |
| CAE Codes | Moloni::caeCodes() |
| Vehicles | Moloni::vehicles() |
| Deductions | Moloni::deductions() |
| Identification Templates | Moloni::identificationTemplates() |
Global Data
| Resource | Accessor Method |
|---|---|
| Countries | Moloni::countries() |
| Fiscal Zones | Moloni::fiscalZones() |
| Languages | Moloni::languages() |
| Currencies | Moloni::currencies() |
| Currency Exchange | Moloni::currencyExchange() |
| Document Models | Moloni::documentModels() |
| Multibanco Gateways | Moloni::multibancoGateways() |
Testing
Run the package test suite with Pest:
vendor/bin/pest
When testing your own application against the Moloni API, prefer Http::fake() so no real requests leave your machine. The host you fake should match MOLONI_BASE_URL exactly:
use Illuminate\Support\Facades\Http; Http::fake([ 'api.moloni.pt/sandbox/grant/*' => Http::response([ 'access_token' => 'fake-token', 'refresh_token' => 'fake-refresh', 'expires_in' => 3600, ]), 'api.moloni.pt/sandbox/customers/getAll/*' => Http::response([ ['customer_id' => 1, 'name' => 'Test Customer'], ]), ]); $customers = Moloni::customers()->getAll($companyId);
For an end-to-end example see tests/Unit/MoloniClientTest.php.
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
Repeated 401 after correct credentials |
The token in moloni_tokens was issued for a different MOLONI_BASE_URL. Truncate the table and re-authenticate. |
AuthenticationException with AuthError::InvalidGrant |
Wrong MOLONI_USERNAME / MOLONI_PASSWORD. Verify against the Moloni client area. |
AuthenticationException with AuthError::InvalidClient |
Wrong MOLONI_CLIENT_ID / MOLONI_CLIENT_SECRET. Re-check at the Developer Portal. |
RateLimitException |
Moloni returned HTTP 429. Back off and retry; consider batching requests. |
| Calls hit production while you expected sandbox | MOLONI_BASE_URL is not set or is cached. Run php artisan config:clear. |
Static Analysis
vendor/bin/phpstan analyse
Code Style
vendor/bin/pint
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
License
The MIT License (MIT). Please see License File for more information.
Credits
digitaldev-lx/laravel-moloni 适用场景与选型建议
digitaldev-lx/laravel-moloni 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 92 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 04 月 01 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「laravel」 「invoicing」 「portugal」 「Moloni」 「faturação」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 digitaldev-lx/laravel-moloni 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 digitaldev-lx/laravel-moloni 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 digitaldev-lx/laravel-moloni 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Space invoices PHP sdk
WeFact API client library for PHP.
Accounting module for atmosphere and laravel
Validações de documentos do Brasil e Portugal
Alfabank REST API integration
Official PHP SDK for the Vorgio API. Issue invoices, send them to clients, and verify webhooks.
统计信息
- 总下载量: 92
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 23
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-04-01