laravelmodular / modular
A powerful NestJS-inspired modular architecture for Laravel — write less, do more.
Requires
- php: ^8.2
- illuminate/cache: ^10.0|^11.0|^12.0
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/container: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/events: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/pipeline: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
This package is auto-updated.
Last update: 2026-05-04 22:54:50 UTC
README
NestJS-inspired modular architecture for Laravel. Write less, do more.
Installation
composer require ouchestech/laravel-modular
Then run setup:
php artisan modular:setup composer dump-autoload
Add to your composer.json autoload:
"autoload": { "psr-4": { "App\\Modules\\": "app/Modules/" } }
Creating a Module
php artisan module:make User
This creates:
app/Modules/User/
├── UserModuleProvider.php ← Module entrypoint (like NestJS @Module)
├── Controllers/
│ └── UserController.php
├── Services/
│ └── UserService.php
├── Repositories/
│ └── UserRepository.php
├── Models/
│ └── User.php
├── Actions/
│ ├── CreateUserAction.php
│ ├── UpdateUserAction.php
│ └── DeleteUserAction.php
├── DTOs/
│ ├── CreateUserDto.php
│ └── UpdateUserDto.php
├── Events/
│ ├── UserCreated.php
│ ├── UserUpdated.php
│ └── UserDeleted.php
├── Observers/
│ └── UserObserver.php ← NEW
├── Notifications/ ← NEW
├── Rules/ ← NEW
├── Contracts/ ← NEW (interfaces)
├── Policies/
│ └── UserPolicy.php
├── Resources/
│ └── UserResource.php
├── Requests/
│ ├── CreateUserRequest.php
│ └── UpdateUserRequest.php
├── Routes/
│ ├── api.php
│ └── web.php
├── Database/
│ └── migrations/
├── Config/
│ └── user.php
└── Tests/
└── UserTest.php
Inter-Module Communication
// Facade use LaravelModular\Facades\Module; $user = Module::call('User@UserService', 'findOrFail', [1]); // Helper function $user = module('User@UserService')->findOrFail(1); // Check existence before calling Module::whenEnabled('User', fn($m) => module('User@UserService')->findOrFail(1));
To allow access, export the service in the module provider:
protected array $exports = [ 'UserService', ];
Base Classes
AbstractController
class UserController extends AbstractController { public function index() { return $this->paginated($this->service->paginate()); // includes links + meta } public function store(CreateUserRequest $request) { return $this->created(new UserResource($item)); // HTTP 201 } public function destroy(int $id) { $this->service->delete($id); return $this->noContent(); // HTTP 204 } }
Full response methods:
| Method | Status |
|---|---|
$this->ok($data) |
200 |
$this->created($data) |
201 |
$this->accepted($data) |
202 |
$this->noContent() |
204 |
$this->badRequest($msg, $errors) |
400 |
$this->unauthorized($msg) |
401 |
$this->forbidden($msg) |
403 |
$this->notFound($msg) |
404 |
$this->conflict($msg) |
409 |
$this->unprocessable($errors) |
422 |
$this->tooManyRequests($msg) |
429 |
$this->serverError($msg) |
500 |
$this->paginated($paginator) |
200 + meta + links |
$this->collection($items) |
200 |
AbstractRepository
class UserRepository extends AbstractRepository { protected string $model = User::class; // Enable full-text search across these columns protected array $searchable = ['name', 'email']; }
Built-in methods:
// Standard CRUD $repo->all() $repo->find($id) $repo->findOrFail($id) $repo->findBy('email', $email) $repo->findWhere(['role' => 'admin', 'active' => true]) $repo->create($data) $repo->update($id, $data) $repo->delete($id) // Pagination $repo->paginate(15) $repo->paginateWhere(['role' => 'admin'], 15) // NEW: Filter, sort, and search in one call $repo->filter( filters: ['status' => 'active', 'amount' => ['between', [10, 100]]], search: 'john', sort: ['created_at' => 'desc'], perPage: 20 ); // NEW: Full-text search (against $searchable columns) $repo->search('john doe', perPage: 20); // Utilities $repo->count(['active' => true]) $repo->exists(['email' => $email]) $repo->firstOrCreate(['email' => $email], $data) $repo->updateOrCreate(['email' => $email], $data) $repo->with(['posts', 'roles']) $repo->withPaginated(['posts'], 15) $repo->latest(10) $repo->oldest(10) $repo->chunk(100, fn($users) => ...) $repo->insertBulk($rows) // bulk insert (no events) $repo->transaction(fn() => ...) // wrap in DB transaction // Soft deletes (if model uses SoftDeletes) $repo->withTrashed() $repo->onlyTrashed() $repo->restore($id) $repo->forceDelete($id) $repo->paginateTrashed(15)
AbstractDto
class CreateUserDto extends AbstractDto { public string $name = ''; public string $email = ''; public string $role = 'user'; // Optional: validation rules public function rules(): array { return [ 'name' => ['required', 'string'], 'email' => ['required', 'email'], ]; } } // Fill from various sources $dto = CreateUserDto::from($request->validated()); $dto = CreateUserDto::fromModel($user); $dto = CreateUserDto::fromRequest($request); // Validate and throw on failure $dto->validate(); // Override values $updated = $dto->with(['role' => 'admin']); // Extract $dto->only(['name', 'email']); $dto->except(['password']); $dto->toArray(); $dto->toJson(); // Collection $dtos = CreateUserDto::collection($request->all());
AbstractObserver (NEW)
Register observers in your module provider and define lifecycle methods:
// app/Modules/User/UserModuleProvider.php protected array $observers = [ User::class => Observers\UserObserver::class, ];
// app/Modules/User/Observers/UserObserver.php class UserObserver extends AbstractObserver { public function created(User $model): void { // e.g. create profile, send welcome email } public function deleted(User $model): void { // e.g. cleanup } }
AbstractRule (NEW)
class UniqueEmailRule extends AbstractRule { public function validate(string $attribute, mixed $value, Closure $fail): void { if (User::where('email', $value)->exists()) { $fail("The :attribute is already taken."); } } } // Usage in a Request public function rules(): array { return [ 'email' => ['required', 'email', new UniqueEmailRule], ]; }
AbstractAction
class CreateUserAction extends AbstractAction { public function execute(mixed ...$args): mixed { [$dto, $role] = $args; // ... } } // Three ways to invoke: app(CreateUserAction::class)->execute($dto, 'admin'); CreateUserAction::make()->execute($dto); action(CreateUserAction::class, $dto, 'admin'); // helper
AbstractPolicy
class PostPolicy extends AbstractPolicy { public function publish(User $user, Post $post): bool { return $user->role === 'editor' || $this->isOwner($user, $post); } }
Module Provider
class UserModuleProvider extends AbstractModule { protected array $exports = ['UserService']; protected array $bindings = [ UserRepositoryInterface::class => UserRepository::class, ]; protected array $singletons = [ UserService::class, ]; protected array $middleware = [ 'user.auth' => UserAuthMiddleware::class, ]; protected array $policies = [ User::class => UserPolicy::class, ]; protected array $observers = [ User::class => Observers\UserObserver::class, // NEW ]; protected array $listen = [ UserCreated::class => [ SendWelcomeEmail::class, ], ]; // NEW: override API version per module (optional) protected ?string $apiVersion = 'v2'; }
Traits
Injectable
UserService::make() // resolve from container UserService::inject() // alias for make()
EmitsEvents
$this->emit(UserCreated::class, $user); $this->emitIf($user->isNew(), UserCreated::class, $user);
HasCaching
$this->cached("user:{$id}", fn() => User::find($id), 3600); $this->cachedForever("settings", fn() => Settings::all()); $this->invalidateCache(["user:{$id}", "users:all"]); $this->cacheKey('user', $id, 'profile'); // → 'user:123:profile'
Keys are automatically prefixed with modular: (configurable).
HasPipeline
$result = $this->pipeline($dto, [ ValidateUserPipe::class, HashPasswordPipe::class, AssignRolePipe::class, ]); // With destination $result = $this->pipeThrough($dto, [ValidatePipe::class], fn($dto) => $this->repo->create($dto->toArray()));
HasHooks (NEW — on services)
Override before* / after* methods to add side effects:
class UserService extends AbstractService { public function create(array $data): User { return $this->withHooks('create', fn($d) => $this->repository->create($d), $data); } protected function beforeCreate(array $data): array { $data['slug'] = Str::slug($data['name']); return $data; } protected function afterCreate(User $user): void { $this->emit(UserCreated::class, $user); } }
HasQueryFilters (NEW — on repositories)
// Simple equality $this->applyFilters($query, ['status' => 'active']); // Operators: like | in | not_in | between | null | not_null | > | < | >= | <= $this->applyFilters($query, ['name' => ['like', '%john%']]); $this->applyFilters($query, ['role' => ['in', ['admin', 'editor']]]); // Sort $this->applySort($query, ['created_at' => 'desc', 'name' => 'asc']); // Search across columns $this->applySearch($query, 'john', ['name', 'email', 'username']);
Collection Macros
// Cast to DTOs collect($users)->toDto(UserDto::class); // In-memory pagination collect($items)->paginate(15); // Group by multiple keys collect($orders)->groupByMany(['status', 'region']); // Sum a nested key collect($orders)->sumNested('product.price'); // NEW: map chunk collect($data)->mapChunks(100, fn($chunk) => $chunk->map(fn($i) => process($i)));
Artisan Commands
# Setup php artisan modular:setup # Scaffolding php artisan module:make User php artisan module:make User --minimal # core files only php artisan module:make User --no-test # Add to existing module php artisan module:service User ExtraService php artisan module:action User SendWelcomeEmail php artisan module:dto User UpdateProfile php artisan module:event User ProfileUpdated php artisan module:listener User HandleProfileUpdate php artisan module:job User ProcessUserExport php artisan module:policy User Post php artisan module:middleware User ApiThrottle php artisan module:resource User UserProfile php artisan module:observer User User # NEW php artisan module:notification User WelcomeEmail # NEW php artisan module:rule User UniqueSlug # NEW php artisan module:contract User UserRepository # NEW # Module management php artisan module:list php artisan module:info User # NEW — shows structure + exports php artisan module:enable Analytics php artisan module:disable Analytics php artisan module:delete Analytics # NEW (with confirmation) # Migrations php artisan module:migrate User # NEW php artisan module:migrate User --rollback php artisan module:migrate User --fresh
API Versioning (NEW)
Enable in config/modular.php:
'versioning' => [ 'enabled' => true, 'default' => 'v1', ],
All module API routes will be prefixed with /api/v1/.... Override per module:
class UserModuleProvider extends AbstractModule { protected ?string $apiVersion = 'v2'; // → /api/v2/users }
Health Endpoint (NEW)
Enable in config/modular.php:
'health' => [ 'enabled' => true, 'route' => '/modular/health', 'middleware' => ['api'], ],
GET /modular/health returns:
{
"status": "ok",
"modules": [
{ "name": "User", "exports": ["UserService"], "status": "active" },
{ "name": "Order", "exports": ["OrderService"], "status": "active" }
],
"total": 2
}
Helper Functions
module('User@UserService')->findOrFail(1) // get module service module('User@UserService', 'findOrFail', [1]) // call method directly module_path('User', 'Services') // → app/Modules/User/Services module_config('user', 'pagination.per_page', 15)// read module config is_module_enabled('Analytics') // bool check dto(CreateUserDto::class, $data) // create DTO action(CreateUserAction::class, $dto) // execute action
Module Structure Convention
| File | Purpose |
|---|---|
*ModuleProvider.php |
Module entrypoint, bindings, exports, observers |
Services/ |
Business logic, injectable, event-aware, hooks |
Repositories/ |
Data access, full CRUD, filter/search built-in |
Actions/ |
Single-purpose operations |
DTOs/ |
Typed input/output objects with optional validation |
Controllers/ |
HTTP layer only, delegates to Service |
Events/ |
Domain events |
Listeners/ |
Event handlers |
Observers/ |
Model lifecycle hooks |
Notifications/ |
Mail/push/database notifications |
Rules/ |
Custom validation rules |
Contracts/ |
Interfaces for DI |
Policies/ |
Authorization gates |
Resources/ |
API response transformation |
Requirements
- PHP 8.2+
- Laravel 10 / 11 / 12
License
MIT — ouchestech