Module System
Core PHP Framework uses a modular monolith architecture where features are organized into self-contained modules that communicate through events and contracts.
What is a Module?
A module is a self-contained feature with its own:
- Routes (web, admin, API)
- Models and migrations
- Controllers and actions
- Views and assets
- Configuration
- Tests
Modules declare their lifecycle event interests and are only loaded when needed.
Module Types
Core Modules (app/Core/)
Foundation modules that provide framework functionality:
app/Core/
├── Events/ # Lifecycle events
├── Module/ # Module system
├── Actions/ # Actions pattern
├── Config/ # Configuration system
├── Media/ # Media handling
└── Storage/ # Cache and storageNamespace: Core\
Purpose: Framework internals, shared utilities
Feature Modules (app/Mod/)
Business domain modules:
app/Mod/
├── Tenant/ # Multi-tenancy
├── Commerce/ # E-commerce features
├── Blog/ # Blogging
└── Analytics/ # AnalyticsNamespace: Mod\
Purpose: Application features
Website Modules (app/Website/)
Site-specific implementations:
app/Website/
├── Marketing/ # Marketing site
├── Docs/ # Documentation site
└── Support/ # Support portalNamespace: Website\
Purpose: Deployable websites/frontends
Plugin Modules (app/Plug/)
Optional integrations:
app/Plug/
├── Stripe/ # Stripe integration
├── Mailchimp/ # Mailchimp integration
└── Analytics/ # Analytics integrationsNamespace: Plug\
Purpose: Third-party integrations, optional features
Module Structure
Standard module structure created by php artisan make:mod:
app/Mod/Example/
├── Boot.php # Module entry point
├── config.php # Module configuration
│
├── Actions/ # Business logic
│ ├── CreateExample.php
│ └── UpdateExample.php
│
├── Controllers/ # HTTP controllers
│ ├── Admin/
│ │ └── ExampleController.php
│ └── ExampleController.php
│
├── Models/ # Eloquent models
│ └── Example.php
│
├── Migrations/ # Database migrations
│ └── 2026_01_01_create_examples_table.php
│
├── Database/
│ ├── Factories/ # Model factories
│ │ └── ExampleFactory.php
│ └── Seeders/ # Database seeders
│ └── ExampleSeeder.php
│
├── Routes/ # Route definitions
│ ├── web.php # Public routes
│ ├── admin.php # Admin routes
│ └── api.php # API routes
│
├── Views/ # Blade templates
│ ├── index.blade.php
│ └── show.blade.php
│
├── Requests/ # Form requests
│ ├── StoreExampleRequest.php
│ └── UpdateExampleRequest.php
│
├── Resources/ # API resources
│ └── ExampleResource.php
│
├── Policies/ # Authorization policies
│ └── ExamplePolicy.php
│
├── Events/ # Domain events
│ └── ExampleCreated.php
│
├── Listeners/ # Event listeners
│ └── SendExampleNotification.php
│
├── Jobs/ # Queued jobs
│ └── ProcessExample.php
│
├── Services/ # Domain services
│ └── ExampleService.php
│
├── Mcp/ # MCP tools
│ └── Tools/
│ └── GetExampleTool.php
│
└── Tests/ # Module tests
├── Feature/
│ └── ExampleTest.php
└── Unit/
└── ExampleServiceTest.phpCreating Modules
Using Artisan Commands
# Create a feature module
php artisan make:mod Blog
# Create a website module
php artisan make:website Marketing
# Create a plugin module
php artisan make:plug StripeManual Creation
- Create directory structure
- Create
Boot.phpwith$listensarray - Register lifecycle event handlers
<?php
namespace Mod\Example;
use Core\Events\WebRoutesRegistering;
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('example', __DIR__.'/Views');
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
}Module Discovery
Auto-Discovery
Modules are automatically discovered by scanning configured paths:
// config/core.php
'module_paths' => [
app_path('Core'),
app_path('Mod'),
app_path('Plug'),
],Manual Registration
Disable auto-discovery and register modules explicitly:
// config/core.php
'modules' => [
'auto_discover' => false,
],
// app/Providers/AppServiceProvider.php
use Core\Module\ModuleRegistry;
public function boot(): void
{
$registry = app(ModuleRegistry::class);
$registry->register(Mod\Blog\Boot::class);
$registry->register(Mod\Commerce\Boot::class);
}Module Configuration
Module-Level Configuration
Each module can have a config.php file:
<?php
// app/Mod/Blog/config.php
return [
'posts_per_page' => env('BLOG_POSTS_PER_PAGE', 12),
'enable_comments' => env('BLOG_COMMENTS_ENABLED', true),
'cache_duration' => env('BLOG_CACHE_DURATION', 3600),
];Access configuration:
$perPage = config('mod.blog.posts_per_page', 12);Publishing Configuration
Allow users to customize module configuration:
// app/Mod/Blog/BlogServiceProvider.php
public function boot(): void
{
$this->publishes([
__DIR__.'/config.php' => config_path('mod/blog.php'),
], 'blog-config');
}Users can then publish and customize:
php artisan vendor:publish --tag=blog-configInter-Module Communication
1. Events (Recommended)
Modules communicate via domain events:
// Mod/Blog/Events/PostPublished.php
class PostPublished
{
public function __construct(public Post $post) {}
}
// Mod/Blog/Actions/PublishPost.php
PostPublished::dispatch($post);
// Mod/Analytics/Listeners/TrackPostPublished.php
Event::listen(PostPublished::class, TrackPostPublished::class);2. Service Contracts
Define contracts for shared functionality:
// Core/Contracts/NotificationService.php
interface NotificationService
{
public function send(Notifiable $notifiable, Notification $notification): void;
}
// Mod/Email/EmailNotificationService.php
class EmailNotificationService implements NotificationService
{
public function send(Notifiable $notifiable, Notification $notification): void
{
// Implementation
}
}
// Register in service provider
app()->bind(NotificationService::class, EmailNotificationService::class);
// Use in other modules
app(NotificationService::class)->send($user, $notification);3. Facades
Create facades for frequently used services:
// Mod/Blog/Facades/Blog.php
class Blog extends Facade
{
protected static function getFacadeAccessor()
{
return BlogService::class;
}
}
// Usage
Blog::getRecentPosts(10);
Blog::findBySlug('example-post');Module Dependencies
Declaring Dependencies
Use PHP attributes to declare module dependencies:
<?php
namespace Mod\BlogComments;
use Core\Module\Attributes\RequiresModule;
#[RequiresModule(Mod\Blog\Boot::class)]
class Boot
{
// ...
}Checking Dependencies
Verify dependencies are met:
use Core\Module\ModuleRegistry;
$registry = app(ModuleRegistry::class);
if ($registry->isLoaded(Mod\Blog\Boot::class)) {
// Blog module is available
}Module Isolation
Database Isolation
Use workspace scoping for multi-tenant isolation:
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
class Post extends Model
{
use BelongsToWorkspace;
}
// Queries automatically scoped to current workspace
Post::all(); // Only returns posts for current workspaceCache Isolation
Use workspace-scoped caching:
use Core\Mod\Tenant\Concerns\HasWorkspaceCache;
class Post extends Model
{
use BelongsToWorkspace, HasWorkspaceCache;
}
// Cache isolated per workspace
Post::forWorkspaceCached($workspace, 600);Route Isolation
Separate route files by context:
// Routes/web.php - Public routes
Route::get('/blog', [BlogController::class, 'index']);
// Routes/admin.php - Admin routes
Route::resource('posts', PostController::class);
// Routes/api.php - API routes
Route::apiResource('posts', PostApiController::class);Module Testing
Feature Tests
Test module functionality end-to-end:
<?php
namespace Tests\Feature\Mod\Blog;
use Tests\TestCase;
use Mod\Blog\Models\Post;
class PostTest extends TestCase
{
public function test_can_view_published_posts(): void
{
Post::factory()->published()->count(3)->create();
$response = $this->get('/blog');
$response->assertStatus(200);
$response->assertViewHas('posts');
}
}Unit Tests
Test module services and actions:
<?php
namespace Tests\Unit\Mod\Blog;
use Tests\TestCase;
use Mod\Blog\Actions\PublishPost;
use Mod\Blog\Models\Post;
class PublishPostTest extends TestCase
{
public function test_publishes_post(): void
{
$post = Post::factory()->create(['published_at' => null]);
PublishPost::run($post);
$this->assertNotNull($post->fresh()->published_at);
}
}Module Isolation Tests
Test that module doesn't leak dependencies:
public function test_module_works_without_optional_dependencies(): void
{
// Simulate missing optional module
app()->forgetInstance(Mod\Analytics\AnalyticsService::class);
$response = $this->get('/blog');
$response->assertStatus(200);
}Best Practices
1. Keep Modules Focused
Each module should have a single, well-defined responsibility:
✅ Good: Mod\Blog (blogging features)
✅ Good: Mod\Comments (commenting system)
❌ Bad: Mod\BlogAndCommentsAndTags (too broad)2. Use Explicit Dependencies
Don't assume other modules exist:
// ✅ Good
if (class_exists(Mod\Analytics\AnalyticsService::class)) {
app(AnalyticsService::class)->track($event);
}
// ❌ Bad
app(AnalyticsService::class)->track($event); // Crashes if not available3. Avoid Circular Dependencies
✅ Good: Blog → Comments (one-way)
❌ Bad: Blog ⟷ Comments (circular)4. Use Interfaces for Contracts
Define interfaces for inter-module communication:
// Core/Contracts/SearchProvider.php
interface SearchProvider
{
public function search(string $query): Collection;
}
// Mod/Blog/BlogSearchProvider.php
class BlogSearchProvider implements SearchProvider
{
// Implementation
}5. Version Your APIs
If modules expose APIs, version them:
// Routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('posts', V1\PostController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('posts', V2\PostController::class);
});Troubleshooting
Module Not Loading
Check module is in configured path:
# Verify path exists
ls -la app/Mod/YourModule
# Check Boot.php exists
cat app/Mod/YourModule/Boot.php
# Verify $listens array
grep "listens" app/Mod/YourModule/Boot.phpRoutes Not Registered
Ensure event handler calls $event->routes():
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Don't forget this!
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}Views Not Found
Register view namespace:
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Register view namespace
$event->views('blog', __DIR__.'/Views');
}Then use namespaced views:
return view('blog::index'); // Not just 'index'