Recipes
Practical, copy-pasteable solutions for common tasks. Every example assumes a module generated with php artisan make:module <Name>.
Bind an interface to an implementation
Declare it on the provider — repeatable, with singleton: true for a shared instance.
use Dem1Off\LaravelModular\Module\Attributes\Bind;
use Dem1Off\LaravelModular\Module\ModuleServiceProvider;
#[Bind(Clock::class, SystemClock::class)]
#[Bind(InvoiceNumberGenerator::class, SequentialGenerator::class, singleton: true)]
final class BillingServiceProvider extends ModuleServiceProvider {}Anything type-hinting Clock now receives SystemClock from the container.
Auto-bind from the implementation (#[Provides])
Skip the provider entry — let the class declare what it provides. The module scans for it and binds automatically.
use Dem1Off\LaravelModular\Module\Attributes\Provides;
#[Provides(PaymentGateway::class, singleton: true)]
final class StripeGateway implements PaymentGateway {}Omit the abstract to infer it from a single implemented interface. Compiled by module:cache, so it's free in production.
Add a config file
Create config/<module>.php — it is merged automatically under the kebab-cased module name. No declaration needed.
// Modules/Billing/config/billing.php
return [
'currency' => 'EUR',
'retries' => 3,
];config('billing.currency'); // 'EUR'Don't want auto-merge? #[Module(config: false)].
Add migrations, factories and seeders
database/migrations loads automatically. Generate a migration:
php artisan module:make-migration Billing create_invoices_table --table=invoicesFactories and seeders are PSR-4 (Modules\Billing\Database\Factories\, Modules\Billing\Database\Seeders\):
// Modules/Billing/database/factories/InvoiceFactory.php
namespace Modules\Billing\Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Modules\Billing\Infrastructure\Persistence\Models\Invoice;
final class InvoiceFactory extends Factory
{
protected $model = Invoice::class;
public function definition(): array
{
return ['total' => $this->faker->numberBetween(100, 9999)];
}
}Add routes and a controller
routes/web.php and routes/api.php load automatically (the API file is given the api prefix). Generate a controller:
php artisan module:make-controller Billing ShowInvoice// Modules/Billing/routes/web.php
use Illuminate\Support\Facades\Route;
use Modules\Billing\Infrastructure\Http\ShowInvoiceController;
Route::get('/invoices/{invoice}', ShowInvoiceController::class)->name('billing.invoices.show');Don't want a routes file loaded? #[Module(routes: false)].
Add views (and Livewire)
resources/views loads under the lowercase module namespace:
return view('billing::invoice', ['invoice' => $invoice]); // resources/views/invoice.blade.phpFor Livewire, register components in boot() (call the parent first):
public function boot(): void
{
parent::boot();
Livewire::component('billing.invoice-list', InvoiceList::class);
}Register a console command
#[Module(commands: [GenerateMonthlyInvoices::class])]
final class BillingServiceProvider extends ModuleServiceProvider {}To schedule it, override boot():
public function boot(): void
{
parent::boot();
$this->app->booted(function () {
$this->app->make(Schedule::class)->command('billing:generate')->monthly();
});
}Listen to events (including cross-module)
#[Listen(InvoicePaid::class, SendReceipt::class)]
#[Listen(InvoicePaid::class, UpdateLedger::class)]
final class BillingServiceProvider extends ModuleServiceProvider {}Let two modules talk without coupling
Use a contract module: the consumer depends on an interface, the provider binds its implementation. Full walkthrough in Contract modules.
// Reporting consumes the port
final class RevenueReport
{
public function __construct(private InvoiceQuery $invoices) {}
}
// Billing provides it
#[Bind(InvoiceQuery::class, EloquentInvoiceQuery::class)]
final class BillingServiceProvider extends ModuleServiceProvider {}Disable a module
php artisan module:disable BillingOr edit modules_statuses.json. A disabled module's provider no-ops — its bindings, routes and migrations are not registered.
Swap an implementation in a test
Because everything is a container binding, override it in a test:
it('charges with a fake gateway', function () {
$this->app->bind(PaymentGateway::class, FakeGateway::class);
// ... assert against the fake
});Override what convention loads
#[Module(views: false, routes: false, migrations: false)]
final class ApiOnlyServiceProvider extends ModuleServiceProvider {}Add custom boot logic
A module provider is a normal Laravel provider — call the parent, then add anything:
public function boot(): void
{
parent::boot();
Gate::policy(Invoice::class, InvoicePolicy::class);
View::composer('billing::invoice', InvoiceComposer::class);
}Extract a module into a service later
Swap the binding from a local implementation to a remote adapter — callers that depend on the interface don't change:
// before — in-process
#[Bind(InvoiceQuery::class, EloquentInvoiceQuery::class)]
// after — the module now lives behind an HTTP API
#[Bind(InvoiceQuery::class, HttpInvoiceServiceClient::class)]See How it works and Promote to a package.
Make it fast in production
php artisan module:cache # or: php artisan optimizeCompiles discovery and attributes into one file — zero reflection, zero filesystem scanning per request. See Performance.