Add documentation covering architecture, modules, getting started, and security considerations for the Core PHP Framework starter template. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.8 KiB
| title | description | updated |
|---|---|---|
| Modules | Creating and organising modules in Core PHP Framework applications | 2026-01-29 |
Modules
Modules are the building blocks of Core PHP Framework applications. Each module is a self-contained feature that registers itself via lifecycle events.
Module Structure
A typical module follows this structure:
app/Mod/Blog/
├── Boot.php # Entry point - event listeners
├── Models/
│ └── Post.php # Eloquent models
├── Routes/
│ ├── web.php # Public routes
│ └── api.php # API routes
├── Views/
│ ├── index.blade.php
│ └── posts/
│ └── show.blade.php
├── Livewire/
│ ├── PostListPage.php
│ └── PostShowPage.php
├── Actions/
│ └── CreatePost.php # Business logic
├── Services/
│ └── PostService.php
├── Migrations/
│ └── 2025_01_01_create_posts_table.php
└── Tests/
├── PostTest.php
└── CreatePostTest.php
The Boot Class
Every module requires a Boot.php file that declares its event listeners:
<?php
declare(strict_types=1);
namespace App\Mod\Blog;
use Core\Events\WebRoutesRegistering;
use Core\Events\ApiRoutesRegistering;
use Core\Events\AdminPanelBooting;
use Core\Events\ConsoleBooting;
class Boot
{
/**
* Static listeners array - module is only instantiated when these events fire
*/
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
ApiRoutesRegistering::class => 'onApiRoutes',
AdminPanelBooting::class => ['onAdminPanel', 10], // With priority
ConsoleBooting::class => 'onConsole',
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Register routes
$event->routes(fn() => require __DIR__.'/Routes/web.php');
// Register view namespace (accessed as 'blog::view.name')
$event->views('blog', __DIR__.'/Views');
}
public function onApiRoutes(ApiRoutesRegistering $event): void
{
$event->routes(fn() => require __DIR__.'/Routes/api.php');
}
public function onAdminPanel(AdminPanelBooting $event): void
{
// Register navigation item
$event->navigation('Blog', 'blog.admin.index', 'newspaper');
// Register admin resources
$event->resource('posts', PostResource::class);
}
public function onConsole(ConsoleBooting $event): void
{
// Register artisan commands
$event->commands([
ImportPostsCommand::class,
PublishScheduledPostsCommand::class,
]);
// Register scheduled tasks
$event->schedule(function ($schedule) {
$schedule->command('blog:publish-scheduled')->hourly();
});
}
}
Lifecycle Events
WebRoutesRegistering
Fired when web routes are being registered. Use for public-facing routes.
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Register route file
$event->routes(fn() => require __DIR__.'/Routes/web.php');
// Register view namespace
$event->views('blog', __DIR__.'/Views');
// Register Blade components
$event->components('blog', __DIR__.'/Views/Components');
// Register middleware
$event->middleware('blog.auth', BlogAuthMiddleware::class);
}
ApiRoutesRegistering
Fired when API routes are being registered. Routes are automatically prefixed with /api.
public function onApiRoutes(ApiRoutesRegistering $event): void
{
$event->routes(fn() => require __DIR__.'/Routes/api.php');
// Register API resources
$event->resource('posts', PostApiResource::class);
}
AdminPanelBooting
Fired when the admin panel is being set up (requires core-admin package).
public function onAdminPanel(AdminPanelBooting $event): void
{
// Navigation item with icon
$event->navigation('Blog', 'blog.admin.index', 'newspaper');
// Navigation group with sub-items
$event->navigationGroup('Blog', [
['Posts', 'blog.admin.posts', 'file-text'],
['Categories', 'blog.admin.categories', 'folder'],
['Tags', 'blog.admin.tags', 'tag'],
], 'newspaper');
// Register admin resource
$event->resource('posts', PostResource::class);
// Register widget for dashboard
$event->widget(RecentPostsWidget::class);
}
ClientRoutesRegistering
Fired for authenticated SaaS routes (dashboard, settings, etc.).
public function onClientRoutes(ClientRoutesRegistering $event): void
{
$event->routes(fn() => require __DIR__.'/Routes/client.php');
}
ConsoleBooting
Fired when Artisan is bootstrapping.
public function onConsole(ConsoleBooting $event): void
{
// Register commands
$event->commands([
ImportPostsCommand::class,
]);
// Register scheduled tasks
$event->schedule(function ($schedule) {
$schedule->command('blog:publish-scheduled')
->hourly()
->withoutOverlapping();
});
}
McpToolsRegistering
Fired when the MCP server is being set up (requires core-mcp package).
public function onMcpTools(McpToolsRegistering $event): void
{
$event->tool('create_post', CreatePostTool::class);
$event->tool('list_posts', ListPostsTool::class);
}
Event Priorities
You can specify a priority for event listeners. Higher numbers execute first:
public static array $listens = [
AdminPanelBooting::class => ['onAdminPanel', 100], // High priority
WebRoutesRegistering::class => ['onWebRoutes', 10], // Normal priority
];
Priorities are useful when:
- Your module needs to register before/after other modules
- You need to override routes from other modules
- You need to modify admin navigation order
View Namespacing
Views are namespaced by the identifier you provide:
$event->views('blog', __DIR__.'/Views');
Access views using the namespace prefix:
{{-- In controllers/components --}}
return view('blog::posts.index');
{{-- In Blade templates --}}
@include('blog::partials.sidebar')
@extends('blog::layouts.main')
Route Files
Web Routes (Routes/web.php)
<?php
use App\Mod\Blog\Livewire\PostListPage;
use App\Mod\Blog\Livewire\PostShowPage;
use Illuminate\Support\Facades\Route;
Route::prefix('blog')->name('blog.')->group(function () {
Route::get('/', PostListPage::class)->name('index');
Route::get('/{post:slug}', PostShowPage::class)->name('show');
});
// With middleware
Route::middleware(['auth'])->prefix('blog')->name('blog.')->group(function () {
Route::get('/my-posts', MyPostsPage::class)->name('my-posts');
});
API Routes (Routes/api.php)
<?php
use App\Mod\Blog\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
Route::prefix('blog')->name('api.blog.')->group(function () {
Route::apiResource('posts', PostController::class);
});
Actions Pattern
For complex business logic, use the Actions pattern:
<?php
declare(strict_types=1);
namespace App\Mod\Blog\Actions;
use App\Mod\Blog\Models\Post;
use Core\Action;
use Illuminate\Support\Str;
class CreatePost
{
use Action;
public function handle(array $data): Post
{
return Post::create([
'title' => $data['title'],
'slug' => Str::slug($data['title']),
'content' => $data['content'],
'published_at' => $data['publish_now'] ? now() : null,
]);
}
}
Usage:
$post = CreatePost::run([
'title' => 'My Post',
'content' => 'Content here...',
'publish_now' => true,
]);
Module Discovery
Modules are discovered automatically from paths configured in config/core.php:
'module_paths' => [
app_path('Core'), // Framework overrides
app_path('Mod'), // Application modules
app_path('Website'), // Website-specific modules
],
Caching
In production, module discovery is cached. Clear the cache when adding new modules:
php artisan cache:clear
Or disable caching during development:
CORE_CACHE_DISCOVERY=false
Creating Modules with Artisan
The make:mod command scaffolds a new module:
# Full module with all features
php artisan make:mod Blog --all
# Web routes only
php artisan make:mod Blog --web
# API routes only
php artisan make:mod Blog --api
# Admin panel integration
php artisan make:mod Blog --admin
# Combination
php artisan make:mod Blog --web --api --admin
Module Dependencies
If your module depends on another module, check for its presence:
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Check if core-tenant is available
if (!class_exists(\Core\Tenant\Models\Workspace::class)) {
return;
}
$event->routes(fn() => require __DIR__.'/Routes/web.php');
}
Best Practices
Keep Modules Focused
Each module should represent a single feature or domain:
Blog- Blog posts, categories, tagsShop- Products, orders, cartNewsletter- Subscribers, campaigns
Use Clear Naming
- Module name: PascalCase singular (
Blog, notBlogs) - Namespace:
App\Mod\{ModuleName} - View namespace: lowercase (
blog::,shop::)
Isolate Dependencies
Keep inter-module dependencies minimal. If modules need to communicate:
- Use events (preferred)
- Use interfaces and dependency injection
- Use shared services in
app/Services/
Test Modules in Isolation
Write tests that don't depend on other modules being present:
it('creates a blog post', function () {
$post = Post::create([
'title' => 'Test',
'slug' => 'test',
'content' => 'Content',
]);
expect($post)->toBeInstanceOf(Post::class);
expect($post->title)->toBe('Test');
});