andrebhas / laravel-brick
Build modular monolith Laravel applications with a solid foundation. Each module is a 'brick' that can be stacked wisely. Includes an optional internal bridge with async support, circuit breaker, and Pest testing.
Requires
- php: ^8.1
- illuminate/cache: ^9.0|^10.0|^11.0|^12.0
- illuminate/console: ^9.0|^10.0|^11.0|^12.0
- illuminate/database: ^9.0|^10.0|^11.0|^12.0
- illuminate/filesystem: ^9.0|^10.0|^11.0|^12.0
- illuminate/log: ^9.0|^10.0|^11.0|^12.0
- illuminate/queue: ^9.0|^10.0|^11.0|^12.0
- illuminate/routing: ^9.0|^10.0|^11.0|^12.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- laravel/framework: ^9.0|^10.0|^11.0|^12.0
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
README
Build modular Laravel applications with a solid foundation. Each module is an independent, self-contained unit (a "brick") that can be composed into robust, scalable applications using Domain-Driven Design principles.
"Each module is a brick. Stack them wisely."
โจ Features
| Feature | Description |
|---|---|
| ๐ Modular Core | Create isolated modules with a standard DDD structure |
| ๐ Internal Bridge | Optional typed, validated cross-module communication |
| โก Async Calls | Dispatch bridge calls to queues, poll results by job ID |
| ๐ก Circuit Breaker | Protect against cascading failures (closed/open/half-open) |
| ๐ Middleware Pipeline | Logging, circuit breaker, caching per bridge call |
| ๐งช Pest Testing | Auto-generate test scaffolding with every module |
| ๐ฆ Module Dependencies | Modules declare dependencies; enable/disable with checks |
| โ๏ธ Per-Module Config | Each module has its own Config/config.php |
| ๐จ Asset Publishing | Copy or symlink module assets to public/bricks/ |
| ๐ Octane Compatible | No static state; fully safe for Laravel Octane |
๐ Requirements
- PHP 8.1 or higher
- Laravel 9.x, 10.x, or 11.x
๐ฆ Installation
composer require andrebhas/laravel-brick
The package uses Laravel's auto-discovery. The service provider is registered automatically.
Publish Configuration
php artisan vendor:publish --tag=brick-config
Publish Stubs (optional, to customise scaffolding)
php artisan vendor:publish --tag=brick-stubs
๐ Module Structure
Layer-Driven Architecture (Default)
After running php artisan brick:make, each module follows this structure:
bricks/
โโโ Hotel/
โโโ module.json # Module manifest
โโโ Actions/
โ โโโ CreateHotelAction.php
โโโ Models/
โ โโโ Hotel.php
โโโ Repositories/
โ โโโ HotelRepositoryInterface.php
โ โโโ EloquentHotelRepository.php
โโโ Services/
โ โโโ HotelService.php
โโโ Bridge/ # Optional: inter-module communication
โ โโโ HotelBridge.php
โโโ Http/
โ โโโ Controllers/
โ โโโ Requests/
โโโ Database/
โ โโโ Migrations/
โ โโโ Seeders/
โโโ Routes/
โ โโโ api.php
โ โโโ web.php
โโโ Resources/
โ โโโ views/
โ โโโ lang/
โ โโโ assets/
โโโ Config/
โ โโโ config.php
โโโ Providers/
โ โโโ HotelServiceProvider.php
โโโ tests/
โ โโโ Pest.php
โ โโโ Feature/
โ โโโ Unit/
โโโ README.md
Feature-Driven Architecture
If you prefer to organizer your module by business features rather than technical layers, you can use the --features flag (or answer yes during the interactive prompt). This replaces the Actions directory with a Features directory:
bricks/
โโโ Hotel/
โโโ Features/
โ โโโ CreateHotel/ # A cohesive, isolated feature
โ โโโ CreateHotelAction.php # Core logic
โ โโโ CreateHotelRequest.php # Specific validation
โ โโโ CreateHotelResponse.php # Formatting Output
โ โโโ CreateHotelFeatureTest.php # Feature tests lives here
โโโ Models/
โโโ Http/
โโโ ... (other standard directories)
๐ Quick Start
1. Create a Module
php artisan brick:make
Interactive flow:
Nama modul:
> Hotel
Deskripsi modul (opsional):
> Modul manajemen hotel
Versi modul [1.0.0]:
>
Buat jembatan (bridge)? (yes/no) [no]:
> yes
Buat rute API? (yes/no) [no]:
> yes
Buat rute web? (yes/no) [no]:
> no
Buat tes (Pest)? (yes/no) [yes]:
> yes
Gunakan struktur Feature-Driven? (yes/no) [no]:
> yes
Membuat modul...
โ Module [Hotel] created successfully
Or non-interactively:
php artisan brick:make Hotel --with-bridge --with-api --with-tests --features
2. Register the Module's Namespace
Add to composer.json:
{
"autoload": {
"psr-4": {
"Bricks\\Hotel\\": "bricks/Hotel/"
}
}
}
Then regenerate autoload:
composer dump-autoload
3. Enable the Module
php artisan brick:enable Hotel
4. Run Migrations
php artisan brick:migrate Hotel
๐ Module Manifest (module.json)
{
"name": "Hotel",
"description": "Modul manajemen hotel",
"version": "1.0.0",
"namespace": "Bricks\\Hotel",
"active": true,
"order": 0,
"dependencies": ["Auth"],
"bridge": {
"class": "Bricks\\Hotel\\Bridge\\HotelBridge"
},
"author": {
"name": "Andre Bhaskoro"
}
}
๐ Bridge: Cross-Module Communication
1. Implement the Bridge
namespace Bricks\Hotel\Bridge; use AndreBhas\Brick\Brick\Bridge\Contracts\Bridgeable; class HotelBridge implements Bridgeable { public static function getBridgeName(): string { return 'Hotel'; } public static function getExposedMethods(): array { return ['getById', 'checkAvailability']; } public function getById(int $id): array { // Fetch hotel data... return ['id' => $id, 'name' => 'Grand Hotel']; } public function checkAvailability(int $hotelId, string $date): bool { return true; } }
2. Synchronous Call (from another module)
use AndreBhas\Brick\Brick\Bridge\InternalGateway; $gateway = app(InternalGateway::class); // Goes through middleware pipeline (logging, circuit breaker, etc.) $hotel = $gateway->call('Hotel', 'getById', 42);
3. Asynchronous Call
// Dispatch to queue, get a job ID $jobId = $gateway->callAsync('Hotel', 'getById', 42); // Later, poll for the result $result = $gateway->getAsyncResult($jobId); // ['status' => 'success', 'result' => [...], 'completed_at' => '...'] // or // ['status' => 'pending', 'queued_at' => '...'] // or // ['status' => 'failed', 'error' => '...']
4. Generate Bridge for Existing Module
php artisan brick:make-bridge Hotel
๐ก Circuit Breaker
The circuit breaker prevents cascading failures when a module's bridge is unreliable.
States
- Closed ๐ข โ Normal operation; calls pass through
- Open ๐ด โ Failures exceeded threshold; calls rejected immediately
- Half-Open ๐ก โ Testing recovery with limited trial calls
Configuration (config/brick.php)
'circuit_breaker' => [ 'default' => [ 'threshold' => 5, // failures before opening 'timeout' => 30, // seconds before half-open 'half_open_max_attempts' => 3, ], ],
Status Command
php artisan brick:bridge:status
+--------+------------+---------------+
| Bridge | State | Failure Count |
+--------+------------+---------------+
| Hotel | โ CLOSED | 0 |
| Auth | โ OPEN | 7 |
+--------+------------+---------------+
Manual Reset
php artisan brick:bridge:status --reset=Hotel
๐งช Testing
Package Tests
composer test
Module Tests (Pest)
Each generated module includes its own tests directory with Pest setup:
# Run a specific module's tests
./vendor/bin/pest bricks/Hotel/tests/
Testing Helpers
BridgeFake โ Mock bridge calls in tests
use AndreBhas\Brick\Brick\Testing\BridgeFake; use AndreBhas\Brick\Brick\Bridge\InternalGateway; $fake = new BridgeFake( container: app(), cache: app('cache')->store(), queue: app('queue'), ); // Register a fake response $fake->shouldReceive('Hotel', 'getById', fn ($id) => ['id' => $id, 'name' => 'Test Hotel']); // Swap in the container app()->instance(InternalGateway::class, $fake); // ... run your code ... // Assert calls $fake->assertCalled('Hotel', 'getById', 1); $fake->assertNotCalled('Hotel', 'delete'); $fake->assertNothingCalled(); // if no calls expected
CircuitBreakerFake โ Simulate circuit states
use AndreBhas\Brick\Brick\Testing\CircuitBreakerFake; use AndreBhas\Brick\Brick\Bridge\CircuitBreaker; $fake = new CircuitBreakerFake(); $fake->forceOpen('Hotel'); // simulate open circuit $fake->forceHalfOpen('Payment'); $fake->forceClose('Auth'); app()->instance(CircuitBreaker::class, $fake);
Generate Test File
# Feature test php artisan brick:make-test Hotel HotelAvailability --type=feature # Unit test php artisan brick:make-test Hotel HotelPricing --type=unit
๐ฎ Artisan Commands Reference
| Command | Description |
|---|---|
brick:make |
Create a new module interactively |
brick:make-feature {module} {feature} |
Scaffold a full feature (Action, Request, etc) |
brick:make-action {module} {name} |
Generate an Action class |
brick:make-request {module} {name} |
Generate a FormRequest class |
brick:make-response {module} {name} |
Generate a Response/Resource class |
brick:make-job {module} {name} |
Generate a specific Job |
brick:make-event {module} {name} |
Generate an Event |
brick:make-listener {module} {name} |
Generate a Listener |
brick:make-bridge {module} |
Generate bridge for a module |
brick:make-test {module} {name} |
Generate a Pest test file |
brick:bridge:status |
Show circuit breaker status for all bridges |
brick:enable {module} |
Enable a module (checks dependencies) |
brick:disable {module} |
Disable a module (checks dependents) |
brick:publish-assets {module?} |
Publish module assets to public/ |
brick:migrate {module} |
Run module migrations |
brick:rollback {module} |
Rollback module migrations |
brick:refresh {module} |
Refresh (rollback + re-run) module migrations |
brick:seed {module} |
Run module seeders |
Command Options
# Enable with --force to skip dependency check php artisan brick:enable Hotel --force # Disable with --force to skip dependents check php artisan brick:disable Hotel --force # Publish assets as symlinks php artisan brick:publish-assets Hotel --symlink # Run specific number of rollback steps php artisan brick:rollback Hotel --step=2 # Refresh and seed php artisan brick:refresh Hotel --seed # Run specific seeder class php artisan brick:seed Hotel --class="Bricks\\Hotel\\Database\\Seeders\\HotelDemoSeeder"
โ๏ธ Configuration Reference
// config/brick.php return [ // Root directory for all modules 'modules_path' => base_path('bricks'), 'bridge' => [ 'enabled' => env('BRICK_BRIDGE_ENABLED', true), // Middleware applied to every synchronous bridge call 'middlewares' => [ \AndreBhas\Brick\Brick\Bridge\Middleware\LoggingMiddleware::class, \AndreBhas\Brick\Brick\Bridge\Middleware\CircuitBreakerMiddleware::class, // \AndreBhas\Brick\Brick\Bridge\Middleware\CacheMiddleware::class, ], 'circuit_breaker' => [ 'default' => [ 'threshold' => 5, 'timeout' => 30, 'half_open_max_attempts' => 3, ], ], 'queue' => env('BRICK_BRIDGE_QUEUE', 'default'), 'cache_store' => env('BRICK_BRIDGE_CACHE', null), 'cache_ttl' => 3600, ], 'assets' => [ 'publish_method' => env('BRICK_ASSETS_METHOD', 'copy'), // 'copy' or 'symlink' 'public_path' => public_path('bricks'), ], 'dependencies' => [ 'auto_enable' => env('BRICK_AUTO_ENABLE_DEPENDENCIES', false), ], ];
Environment Variables
| Variable | Default | Description |
|---|---|---|
BRICK_BRIDGE_ENABLED |
true |
Enable/disable the bridge globally |
BRICK_BRIDGE_QUEUE |
default |
Queue for async bridge calls |
BRICK_BRIDGE_CACHE |
null |
Cache store for circuit breaker & async results |
BRICK_ASSETS_METHOD |
copy |
Asset publishing method (copy or symlink) |
BRICK_AUTO_ENABLE_DEPENDENCIES |
false |
Auto-enable missing dependencies |
๐ง Adding Custom Middleware
Create a middleware class:
namespace App\Brick\Middleware; use Closure; class RateLimitMiddleware { public function handle(Closure $next, string $bridge, string $method, array $args): mixed { // Rate limiting logic... return $next($bridge, $method, $args); } }
Register in config/brick.php:
'middlewares' => [ \AndreBhas\Brick\Brick\Bridge\Middleware\LoggingMiddleware::class, \AndreBhas\Brick\Brick\Bridge\Middleware\CircuitBreakerMiddleware::class, \App\Brick\Middleware\RateLimitMiddleware::class, // <-- add here ],
๐งฉ Module Dependencies
Declare dependencies in module.json:
{
"name": "Booking",
"dependencies": ["Hotel", "Auth"]
}
When enabling:
php artisan brick:enable Booking # Error: Cannot enable [Booking]. Missing dependencies: Hotel (inactive). # Use --force to override. php artisan brick:enable Hotel php artisan brick:enable Booking # Now works
โ Compatibility Notes
| Package | Notes |
|---|---|
| Spatie Media Library | Use HasMedia trait directly in module models |
| Laravel Socialite | Place OAuth controllers in Auth module's Http/Controllers/ |
| Laravel Passport | Register Passport in Auth module's service provider |
| Laravel Telescope | Automatically captures all bridge calls via logging middleware |
| Laravel Debugbar | No conflicts; bridge calls appear in the timeline |
| Laravel Octane | Fully safe โ no static state anywhere in the package |
๐ Package Structure
laravel-brick/
โโโ src/
โ โโโ BrickServiceProvider.php # Auto-discovery entry point
โ โโโ Brick/
โ โโโ Bridge/
โ โ โโโ Contracts/Bridgeable.php # Interface for bridge classes
โ โ โโโ CircuitBreaker.php # State machine (closed/open/half-open)
โ โ โโโ InternalGateway.php # Main call dispatcher
โ โ โโโ Jobs/BridgeJob.php # Queued async bridge job
โ โ โโโ Middleware/
โ โ โโโ LoggingMiddleware.php
โ โ โโโ CircuitBreakerMiddleware.php
โ โ โโโ CacheMiddleware.php
โ โโโ Commands/ # 11 Artisan commands
โ โโโ Providers/
โ โ โโโ BrickServiceProvider.php
โ โ โโโ BridgeServiceProvider.php
โ โโโ Support/
โ โ โโโ Module.php # Module value object
โ โ โโโ ModuleManager.php # Module discovery & state management
โ โโโ Stubs/ # Templates used by generators
โ โ โโโ module/
โ โ โโโ bridge/
โ โโโ Testing/
โ โโโ BridgeFake.php # Test double for InternalGateway
โ โโโ CircuitBreakerFake.php # Test double for CircuitBreaker
โโโ config/brick.php
โโโ tests/
โโโ composer.json
โโโ phpunit.xml
โโโ README.md
๐ค Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Write tests for your changes
- Run the test suite:
composer test - Submit a pull request
๐ License
The MIT License (MIT). See LICENSE for details.
"Each module is a brick. Stack them wisely." โ andrebhas/laravel-brick