--- name: core-patterns description: Scaffold Core PHP Framework patterns (Actions, Multi-tenant, Activity Logging, Modules, Seeders) --- # Core Patterns Scaffolding You are helping the user scaffold common Core PHP Framework patterns. This is an interactive skill - gather information through conversation before generating code. ## Start by asking what the user wants to create Present these options: 1. **Action class** - Single-purpose business logic class 2. **Multi-tenant model** - Add workspace isolation to a model 3. **Activity logging** - Add change tracking to a model 4. **Module** - Create a new module with Boot class 5. **Seeder** - Create a seeder with dependency ordering Ask: "What would you like to scaffold? (1-5 or describe what you need)" --- ## Option 1: Action Class Actions are small, focused classes that do one thing well. They extract complex logic from controllers and Livewire components. ### Gather information Ask the user for: - **Action name** (e.g., `CreateInvoice`, `PublishPost`, `SendNotification`) - **Module** (e.g., `Billing`, `Content`, `Notification`) - **What it does** (brief description to understand parameters needed) ### Generate the Action Location: `packages/core-php/src/Mod/{Module}/Actions/{ActionName}.php` ```php handle($param1, $param2); * * // Or via static helper: * $result = {ActionName}::run($param1, $param2); */ class {ActionName} { use Action; public function __construct( // Inject dependencies here ) {} /** * Execute the action. */ public function handle(/* parameters */): mixed { // Implementation } } ``` ### Key points to explain - Actions use the `Core\Actions\Action` trait for the static `run()` helper - Dependencies are constructor-injected - The `handle()` method contains the business logic - Can optionally implement `Core\Actions\Actionable` for type-hinting - Naming convention: verb + noun (CreateThing, UpdateThing, DeleteThing) --- ## Option 2: Multi-tenant Model The `BelongsToWorkspace` trait enforces workspace isolation with automatic scoping and caching. ### Gather information Ask the user for: - **Model name** (e.g., `Invoice`, `Project`) - **Whether workspace context is always required** (default: yes) ### Migration requirement Ensure the model's table has a `workspace_id` column: ```php $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); ``` ### Add the trait ```php where('status', 'paid')->get(); // Cached collection for current workspace $invoices = Invoice::ownedByCurrentWorkspaceCached(); // Query for specific workspace $invoices = Invoice::forWorkspace($workspace)->get(); // Check ownership if ($invoice->belongsToCurrentWorkspace()) { // safe to display } ``` --- ## Option 3: Activity Logging The `LogsActivity` trait wraps spatie/laravel-activitylog with framework defaults and workspace tagging. ### Gather information Ask the user for: - **Model name** to add logging to - **Which attributes to log** (all, or specific ones) - **Which events to log** (created, updated, deleted - default: all) ### Add the trait ```php properties = $activity->properties->merge([ 'custom_field' => $this->some_field, ]); } ``` ### Key points to explain - Automatically includes `workspace_id` in activity properties - Empty logs are not submitted - Uses sensible defaults that can be overridden via model properties - Can temporarily disable logging with `Model::withoutActivityLogging(fn() => ...)` --- ## Option 4: Module Modules are the core organizational unit. Each module has a Boot class that declares which lifecycle events it listens to. ### Gather information Ask the user for: - **Module name** (e.g., `Billing`, `Notifications`) - **What the module provides** (web routes, admin panel, API, console commands) ### Create the directory structure ``` packages/core-php/src/Mod/{ModuleName}/ ├── Boot.php # Module entry point ├── Models/ # Eloquent models ├── Actions/ # Business logic ├── Routes/ │ ├── web.php # Web routes │ └── api.php # API routes ├── View/ │ └── Blade/ # Blade views ├── Console/ # Artisan commands ├── Database/ │ ├── Migrations/ # Database migrations │ └── Seeders/ # Database seeders └── Lang/ └── en_GB/ # Translations ``` ### Generate Boot.php ```php */ public static array $listens = [ WebRoutesRegistering::class => 'onWebRoutes', ApiRoutesRegistering::class => 'onApiRoutes', AdminPanelBooting::class => 'onAdminPanel', ConsoleBooting::class => 'onConsole', ]; public function register(): void { // Register singletons and bindings } public function boot(): void { $this->loadMigrationsFrom(__DIR__.'/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/Lang/en_GB', $this->moduleName); } // ------------------------------------------------------------------------- // Event-driven handlers // ------------------------------------------------------------------------- public function onWebRoutes(WebRoutesRegistering $event): void { $event->views($this->moduleName, __DIR__.'/View/Blade'); if (file_exists(__DIR__.'/Routes/web.php')) { $event->routes(fn () => Route::middleware('web')->group(__DIR__.'/Routes/web.php')); } // Register Livewire components // $event->livewire('{module}.component-name', View\Components\ComponentName::class); } public function onApiRoutes(ApiRoutesRegistering $event): void { if (file_exists(__DIR__.'/Routes/api.php')) { $event->routes(fn () => Route::middleware('api')->group(__DIR__.'/Routes/api.php')); } } public function onAdminPanel(AdminPanelBooting $event): void { $event->views($this->moduleName, __DIR__.'/View/Blade'); } public function onConsole(ConsoleBooting $event): void { // Register commands // $event->command(Console\MyCommand::class); } } ``` ### Available lifecycle events | Event | Purpose | Handler receives | |-------|---------|------------------| | `WebRoutesRegistering` | Public web routes | views, routes, livewire | | `AdminPanelBooting` | Admin panel setup | views, routes | | `ApiRoutesRegistering` | REST API routes | routes | | `ClientRoutesRegistering` | Authenticated client routes | routes | | `ConsoleBooting` | Artisan commands | command, middleware | | `McpToolsRegistering` | MCP tools | tools | | `FrameworkBooted` | Late initialization | - | ### Key points to explain - The `$listens` array declares which events trigger which methods - Modules are lazy-loaded - only instantiated when their events fire - Keep Boot classes thin - delegate to services and actions - Use the `$moduleName` for consistent view namespace and translations --- ## Option 5: Seeder with Dependencies Seeders can declare ordering via attributes for dependencies between seeders. ### Gather information Ask the user for: - **Seeder name** (e.g., `PackageSeeder`, `DemoDataSeeder`) - **Module** it belongs to - **Dependencies** - which seeders must run before this one - **Priority** (optional) - lower numbers run first (default: 50) ### Generate the Seeder Location: `packages/core-php/src/Mod/{Module}/Database/Seeders/{SeederName}.php` ```php