php-template/docs/modules.md

428 lines
9.8 KiB
Markdown
Raw Normal View History

---
title: Modules
description: Creating and organising modules in Core PHP Framework applications
updated: 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
<?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.
```php
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`.
```php
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).
```php
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.).
```php
public function onClientRoutes(ClientRoutesRegistering $event): void
{
$event->routes(fn() => require __DIR__.'/Routes/client.php');
}
```
### ConsoleBooting
Fired when Artisan is bootstrapping.
```php
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).
```php
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:
```php
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:
```php
$event->views('blog', __DIR__.'/Views');
```
Access views using the namespace prefix:
```blade
{{-- 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
<?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
<?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
<?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:
```php
$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`:
```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:
```bash
php artisan cache:clear
```
Or disable caching during development:
```env
CORE_CACHE_DISCOVERY=false
```
## Creating Modules with Artisan
The `make:mod` command scaffolds a new module:
```bash
# 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:
```php
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, tags
- `Shop` - Products, orders, cart
- `Newsletter` - Subscribers, campaigns
### Use Clear Naming
- Module name: PascalCase singular (`Blog`, not `Blogs`)
- Namespace: `App\Mod\{ModuleName}`
- View namespace: lowercase (`blog::`, `shop::`)
### Isolate Dependencies
Keep inter-module dependencies minimal. If modules need to communicate:
1. Use events (preferred)
2. Use interfaces and dependency injection
3. Use shared services in `app/Services/`
### Test Modules in Isolation
Write tests that don't depend on other modules being present:
```php
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');
});
```