Adding it to a Laravel application
app/ is still your Laravel application. Each module lives in apps/. If you would rather start from a working application, use the starter.
config/distributable.php declares the modules: 'declared' => ['iam' => [], …]
apps/Iam/
├── app/ Apps\Iam\, laid out like a Laravel app
│ ├── Providers/IamServiceProvider.php the events it listens to, in $handlers
│ ├── Models/User.php
│ └── Events/UserRegistered.php
├── config/
│ └── database.php its connections: `modules:make` always writes it
├── database/migrations/
└── routes/api.php served under /iam/api/…
foundation/
├── FoundationServiceProvider.php what the foundation declares for every module
└── Iam/ Foundation\Iam\, what iam shares with the other modules
├── Rpc/FindUser.php a message iam answers
├── Services/IamClient.php how another process calls iam
├── Shadows/UserShadow.php
└── database/shadows/users/ the copy's migration, named by UserShadow's #[Shadow]The package loads the modules, not the foundation: map Foundation\ in composer.json and list Foundation\FoundationServiceProvider in bootstrap/providers.php.
Declare the modules
// config/distributable.php
'declared' => [
'iam' => [],
'analytics' => [],
],// apps/Iam/app/Providers/IamServiceProvider.php
final class IamServiceProvider extends \Distributable\Providers\ServiceProvider {}// apps/Iam/app/Models/User.php: plain Eloquent, it uses iam's database because it runs in iam
final class User extends \Illuminate\Database\Eloquent\Model {}Give a module its database
// apps/Iam/config/database.php, merged into config/database.php when iam boots
return [
// the connection the module's code runs on
'default' => 'iam',
// creates and alters tables; optional, `default` otherwise
'owner' => 'iam_owner',
'connections' => [
// reads and writes
'iam' => ['driver' => 'pgsql', 'database' => 'iam', 'username' => 'iam_app'],
'iam_owner' => ['driver' => 'pgsql', 'database' => 'iam', 'username' => 'iam_owner'],
],
];# migrates each module's database, see Migrations
php artisan migrateEmit an event from one module and handle it in another
final class UserRegistered extends \Distributable\Events\Event
{
public function __construct(private readonly int $id) {}
public function name(): string
{
return 'iam.user.registered';
}
public function payload(): array
{
return ['id' => $this->id];
}
// raised when the shape of the payload changes
public function version(): int
{
return 1;
}
}app(\Distributable\Contracts\Stream\Bus::class)->emit(new UserRegistered($user->id));// apps/Analytics/app/Handlers/RecordSignup.php
final class RecordSignup implements \Distributable\Contracts\Stream\Handler
{
public function handle(string $name, array $payload): void
{
// record the signup
}
}// apps/Analytics/app/Providers/AnalyticsServiceProvider.php
protected array $handlers = [
'iam.user.registered' => [
RecordSignup::class,
],
];php artisan stream:consumeThis event is version 1. When the shape of a payload changes, the event declares a new version and how to read the old ones, so events already in the stream stay readable: see Change the shape of a payload.
Check that every module could run apart
php artisan modules:doctorBoundary crossed: apps/Analytics/app/Models/Report.php: Apps\Iam\Models\User
[iam] runs elsewhere and serves RPC messages, but distributable.declared.iam.host is not set.It reports a module that imports another module's class or names its tables, an undeclared module, a missing connection, a missing host or RPC secret, and exits with a non-zero code if it finds anything. The full list is in Checking the application.