# Core PHP Framework Patterns This guide covers the key architectural patterns used throughout the Core PHP Framework. Each pattern includes guidance on when to use it, quick start examples, and common pitfalls to avoid. ## Table of Contents 1. [Actions Pattern](#1-actions-pattern) 2. [Multi-Tenant Data Isolation](#2-multi-tenant-data-isolation) 3. [Module System (Lifecycle Events)](#3-module-system-lifecycle-events) 4. [Activity Logging](#4-activity-logging) 5. [Seeder Auto-Discovery](#5-seeder-auto-discovery) 6. [Service Definition](#6-service-definition) --- ## 1. Actions Pattern **Location:** `packages/core-php/src/Core/Actions/` Actions are single-purpose classes that encapsulate business logic. They extract complex operations from controllers and Livewire components into testable, reusable units. ### When to Use - Business operations with multiple steps (validation, authorization, persistence, side effects) - Logic that should be reusable across controllers, commands, and API endpoints - Operations that benefit from dependency injection - Any operation complex enough to warrant unit testing in isolation ### When NOT to Use - Simple CRUD operations that don't need validation beyond form requests - One-line operations that don't benefit from abstraction ### Quick Start ```php posts->create([ 'user_id' => $user->id, 'title' => $data['title'], 'content' => $data['content'], ]); if (isset($data['image'])) { $this->images->attach($post, $data['image']); } return $post; } } ``` **Usage:** ```php // Via dependency injection (preferred) public function __construct(private CreatePost $createPost) {} $post = $this->createPost->handle($user, $validated); // Via static helper $post = CreatePost::run($user, $validated); // Via container $post = app(CreatePost::class)->handle($user, $validated); ``` ### Full Example Here's an Action that creates a project with entitlement checking: ```php defaultWorkspace(); // Check entitlements if ($workspace) { $this->checkEntitlements($workspace); } // Generate unique slug if not provided $data['slug'] = $data['slug'] ?? $this->generateUniqueSlug($data['name']); $data['user_id'] = $user->id; $data['workspace_id'] = $data['workspace_id'] ?? $workspace?->id; // Create the project $project = Project::create($data); // Record usage if ($workspace) { $this->entitlements->recordUsage( $workspace, 'projects.count', 1, $user, ['project_id' => $project->id] ); } // Log activity Activity::causedBy($user) ->performedOn($project) ->withProperties(['name' => $project->name]) ->log('created'); return $project; } public static function run(User $user, array $data): Project { return app(static::class)->handle($user, $data); } protected function checkEntitlements(Workspace $workspace): void { $result = $this->entitlements->can($workspace, 'projects.count'); if ($result->isDenied()) { throw new EntitlementException( "You have reached your project limit. Please upgrade your plan.", 'projects.count' ); } } protected function generateUniqueSlug(string $name): string { $slug = \Illuminate\Support\Str::slug($name); $original = $slug; $counter = 1; while (Project::where('slug', $slug)->exists()) { $slug = $original . '-' . $counter++; } return $slug; } } ``` ### Directory Structure ``` Mod/Example/Actions/ ├── CreateThing.php ├── UpdateThing.php ├── DeleteThing.php └── Thing/ ├── PublishThing.php └── ArchiveThing.php ``` ### Configuration Actions use Laravel's service container for dependency injection. No additional configuration is required. ### Common Pitfalls | Pitfall | Solution | |---------|----------| | Too many responsibilities | Keep actions focused on one operation. Split into multiple actions if needed. | | Returning void | Always return something useful (the created/updated model, a result DTO). | | Not using dependency injection | Inject dependencies via constructor, not `app()` calls inside methods. | | Catching all exceptions | Let exceptions bubble up for proper error handling. | | Direct database queries | Use repositories or model methods for testability. | --- ## 2. Multi-Tenant Data Isolation **Location:** `packages/core-php/src/Mod/Tenant/` The multi-tenant system ensures data isolation between workspaces using global scopes and traits. This is a **security-critical** pattern that prevents cross-tenant data leakage. ### When to Use - Any model that "belongs" to a workspace and should be isolated - Data that must never be visible across tenant boundaries - Resources that should be automatically scoped to the current workspace context ### Key Components | Component | Purpose | |-----------|---------| | `BelongsToWorkspace` trait | Add to models that belong to a workspace | | `WorkspaceScope` | Global scope that filters queries | | `MissingWorkspaceContextException` | Security exception when context is missing | ### Quick Start ```php where('is_active', true); } } ``` **Using the model:** ```php // Automatically scoped to current workspace $products = Product::all(); $product = Product::where('sku', 'ABC123')->first(); $activeProducts = Product::active()->get(); // Creating - workspace_id is auto-assigned $product = Product::create(['name' => 'Widget', 'sku' => 'W001']); // Cached collection for current workspace $products = Product::ownedByCurrentWorkspaceCached(); // Query a specific workspace (admin use) $products = Product::forWorkspace($workspace)->get(); // Query across all workspaces (admin use - be careful!) $allProducts = Product::acrossWorkspaces()->get(); ``` ### Strict Mode vs Permissive Mode **Strict mode (default):** Throws `MissingWorkspaceContextException` when workspace context is unavailable. This is the secure default. ```php // In strict mode, this throws an exception if no workspace context $products = Product::all(); // MissingWorkspaceContextException ``` **Permissive mode:** Returns empty results instead of throwing. Use sparingly. ```php // Disable strict mode globally (not recommended) WorkspaceScope::disableStrictMode(); // Disable for a specific callback WorkspaceScope::withoutStrictMode(function () { // Operations here return empty results if no context $products = Product::all(); // Returns empty collection }); // Disable for a specific model class LegacyProduct extends Model { use BelongsToWorkspace; protected bool $workspaceScopeStrict = false; // Not recommended } ``` ### When to Use `forWorkspace()` vs `acrossWorkspaces()` | Method | Use Case | |--------|----------| | `forWorkspace($workspace)` | Admin viewing a specific workspace's data | | `acrossWorkspaces()` | Global reports, admin dashboards, CLI commands | | Neither (default) | Normal application code - uses current context | **Always prefer the default scoping** unless you have a specific reason to query across workspaces. ### Security Considerations 1. **Never disable strict mode globally** in production 2. **Audit uses of `acrossWorkspaces()`** - each use should be justified 3. **CLI commands** automatically bypass strict mode (they have no request context) 4. **Test data isolation** - write tests that verify cross-tenant queries fail 5. **The `workspace_id` column must exist** on all tenant-scoped tables ### Configuration ```php // In migrations - always add workspace_id Schema::create('products', function (Blueprint $table) { $table->id(); $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); $table->string('name'); // ... $table->index('workspace_id'); // Important for performance }); ``` ### Common Pitfalls | Pitfall | Solution | |---------|----------| | Forgetting `workspace_id` in migrations | Always add the foreign key and index | | Using `acrossWorkspaces()` casually | Audit every usage, prefer default scoping | | Suppressing the exception | Don't catch `MissingWorkspaceContextException` - fix the context | | Direct DB queries | Use Eloquent models so scopes apply | | Joining across tenant boundaries | Ensure joins respect workspace_id | --- ## 3. Module System (Lifecycle Events) **Location:** `packages/core-php/src/Core/Events/`, `packages/core-php/src/Core/Module/` The module system uses lifecycle events for lazy-loading modules. Modules declare what events they listen to, and are only instantiated when those events fire. ### When to Use - Creating a new feature module - Registering routes, views, Livewire components - Adding admin panel navigation - Registering console commands ### How It Works ``` Application Start │ ▼ ModuleScanner scans for Boot.php files with $listens arrays │ ▼ ModuleRegistry registers lazy listeners for each event-module pair │ ▼ Request comes in → Appropriate lifecycle event fires │ ▼ LazyModuleListener instantiates module and calls handler │ ▼ Module registers its resources (routes, views, etc.) ``` ### Quick Start Create a `Boot.php` file in your module directory: ```php 'onWebRoutes', ]; public function onWebRoutes(WebRoutesRegistering $event): void { $event->views('example', __DIR__.'/Views'); $event->routes(fn () => require __DIR__.'/Routes/web.php'); } } ``` ### Available Lifecycle Events | Event | Context | When It Fires | |-------|---------|---------------| | `WebRoutesRegistering` | Web requests | Public frontend routes | | `AdminPanelBooting` | Admin requests | Admin panel setup | | `ApiRoutesRegistering` | API requests | REST API routes | | `ClientRoutesRegistering` | Client dashboard | Authenticated client routes | | `ConsoleBooting` | CLI commands | Artisan booting | | `QueueWorkerBooting` | Queue workers | Queue worker starting | | `McpToolsRegistering` | MCP server | MCP tool registration | | `FrameworkBooted` | All contexts | After all context-specific events | ### Full Example A complete module Boot class: ```php 'onAdminPanel', ApiRoutesRegistering::class => 'onApiRoutes', WebRoutesRegistering::class => 'onWebRoutes', ConsoleBooting::class => 'onConsole', ]; public function register(): void { // Register service bindings $this->app->singleton( Services\InventoryService::class, Services\InventoryService::class ); } public function boot(): void { // Load migrations $this->loadMigrationsFrom(__DIR__.'/Migrations'); $this->loadTranslationsFrom(__DIR__.'/Lang/en', 'inventory'); } public function onAdminPanel(AdminPanelBooting $event): void { $event->views($this->moduleName, __DIR__.'/View/Blade'); // Navigation $event->navigation([ 'label' => 'Inventory', 'icon' => 'box', 'route' => 'admin.inventory.index', 'sort_order' => 50, ]); // Livewire components $event->livewire('inventory-list', View\Livewire\InventoryList::class); $event->livewire('product-form', View\Livewire\ProductForm::class); } public function onApiRoutes(ApiRoutesRegistering $event): void { $event->routes(fn () => Route::middleware('api') ->prefix('v1/inventory') ->group(__DIR__.'/Routes/api.php')); } public function onWebRoutes(WebRoutesRegistering $event): void { $event->views($this->moduleName, __DIR__.'/View/Blade'); $event->routes(fn () => Route::middleware('web') ->group(__DIR__.'/Routes/web.php')); } public function onConsole(ConsoleBooting $event): void { $event->command(Console\SyncInventoryCommand::class); $event->command(Console\PruneStaleStockCommand::class); } } ``` ### Available Request Methods Events provide these methods for registering resources: | Method | Purpose | |--------|---------| | `routes(callable)` | Register route files/callbacks | | `views(namespace, path)` | Register view namespaces | | `livewire(alias, class)` | Register Livewire components | | `middleware(alias, class)` | Register middleware aliases | | `command(class)` | Register Artisan commands | | `translations(namespace, path)` | Register translation namespaces | | `bladeComponentPath(path, namespace)` | Register anonymous Blade components | | `policy(model, policy)` | Register model policies | | `navigation(item)` | Register navigation items | ### The Request/Collect Pattern Events use a "request/collect" pattern: 1. **Modules request** resources via methods like `routes()`, `views()` 2. **Requests are collected** in arrays during event dispatch 3. **LifecycleEventProvider processes** all requests with validation This ensures modules don't directly mutate infrastructure and allows central validation. ### Module Directory Structure ``` Mod/Inventory/ ├── Boot.php # Module entry point ├── Routes/ │ ├── web.php │ └── api.php ├── View/ │ ├── Blade/ # Blade templates │ │ └── admin/ │ └── Livewire/ # Livewire components ├── Models/ ├── Actions/ ├── Services/ ├── Console/ ├── Migrations/ └── Lang/ ``` ### Configuration Namespace detection is automatic based on path: - `/Core` paths → `Core\` namespace - `/Mod` paths → `Mod\` namespace - `/Website` paths → `Website\` namespace - `/Plug` paths → `Plug\` namespace ### Common Pitfalls | Pitfall | Solution | |---------|----------| | Registering routes outside events | Always use `$event->routes()` in handlers | | Heavy work in `$listens` handlers | Keep handlers lightweight; defer work | | Forgetting to add `$listens` | Module won't load without static `$listens` | | Using wrong event | Match event to request context (web, api, admin) | | Instantiating in `$listens` | The array is read statically; don't call methods | --- ## 4. Activity Logging **Location:** `packages/core-php/src/Core/Activity/` Activity logging tracks model changes and user actions. Built on spatie/laravel-activitylog with workspace-aware enhancements. ### When to Use - Audit trails for compliance - User activity history - Model change tracking - Debugging and support ### Quick Start Add the trait to any model: ```php properties = $activity->properties->merge([ 'contract_number' => $this->contract_number, 'client_id' => $this->client_id, ]); } } ``` **Querying activity logs:** ```php use Core\Activity\Services\ActivityLogService; $service = app(ActivityLogService::class); // Get activities for a specific model $activities = $service->logFor($contract)->recent(20); // Get activities by a user in a workspace $activities = $service ->logBy($user) ->forWorkspace($workspace) ->lastDays(7) ->paginate(25); // Get all "updated" events for a model type $activities = $service ->forSubjectType(Contract::class) ->ofType('updated') ->between('2024-01-01', '2024-12-31') ->get(); // Search activities $results = $service->search('approved contract'); // Get statistics $stats = $service->statistics($workspace); // Returns: ['total' => 150, 'by_event' => [...], 'by_subject' => [...], 'by_user' => [...]] ``` **Using the Activity model directly:** ```php use Core\Activity\Models\Activity; // Get activities for a workspace $activities = Activity::forWorkspace($workspace)->newest()->get(); // Filter by event type $created = Activity::createdEvents()->lastDays(30)->get(); $deleted = Activity::deletedEvents()->withDeletedSubject()->get(); // Get activities with changes $withChanges = Activity::withChanges()->get(); // Access change details foreach ($activities as $activity) { echo $activity->description; echo $activity->causer_name; // "John Doe" or "System" echo $activity->subject_name; // "Contract #123" // Get specific changes foreach ($activity->changes as $field => $values) { echo "{$field}: {$values['old']} -> {$values['new']}"; } } ``` ### Configuration Configure via model properties: | Property | Type | Default | Description | |----------|------|---------|-------------| | `$activityLogAttributes` | array | null (all) | Attributes to log | | `$activityLogName` | string | 'default' | Log name for grouping | | `$activityLogEvents` | array | ['created', 'updated', 'deleted'] | Events to log | | `$activityLogWorkspace` | bool | true | Include workspace_id | | `$activityLogOnlyDirty` | bool | true | Only log changed attributes | **Global configuration (config/core.php):** ```php 'activity' => [ 'enabled' => env('ACTIVITY_LOG_ENABLED', true), 'log_name' => 'default', 'include_workspace' => true, 'default_events' => ['created', 'updated', 'deleted'], 'retention_days' => 90, ], ``` ### Temporarily Disabling Logging ```php // Disable for a callback Document::withoutActivityLogging(function () { $document->update(['status' => 'processed']); }); // Check if logging is enabled if (Document::activityLoggingEnabled()) { // ... } ``` ### Pruning Old Logs ```bash # Via artisan command php artisan activity:prune --days=90 # Via service $service = app(ActivityLogService::class); $deleted = $service->prune(90); // Delete logs older than 90 days ``` ### Common Pitfalls | Pitfall | Solution | |---------|----------| | Logging sensitive data | Exclude sensitive fields via `$activityLogAttributes` | | Excessive logging | Log only meaningful changes, not every field | | Performance issues | Use `withoutActivityLogging()` for bulk operations | | Missing workspace context | Ensure workspace is set before logging | | Large properties | Limit logged data; don't log file contents | --- ## 5. Seeder Auto-Discovery **Location:** `packages/core-php/src/Core/Database/Seeders/` The seeder auto-discovery system scans module directories for seeders and executes them in the correct order based on priority and dependencies. ### When to Use - Creating seeders for a module - Seeding reference data (features, packages, configuration) - Establishing dependencies between seeders - Demo/test data setup ### Quick Start Create a seeder in your module's `Database/Seeders/` directory: ```php 'example.feature'], ['name' => 'Example Feature', 'type' => 'boolean'] ); } } ``` Seeders are discovered automatically - no manual registration required. ### Priority and Dependencies **Using priority (simple ordering):** ```php // Using class property class FeatureSeeder extends Seeder { public int $priority = 10; // Runs early } // Using attribute use Core\Database\Seeders\Attributes\SeederPriority; #[SeederPriority(10)] class FeatureSeeder extends Seeder { public function run(): void { /* ... */ } } ``` **Priority guidelines:** | Range | Use Case | |-------|----------| | 0-20 | Foundation (features, config) | | 20-40 | Core data (packages, workspaces) | | 40-60 | Default (general seeders) | | 60-80 | Content (pages, posts) | | 80-100 | Demo/test data | **Using dependencies (explicit ordering):** ```php use Core\Database\Seeders\Attributes\SeederAfter; use Core\Database\Seeders\Attributes\SeederBefore; use Core\Mod\Tenant\Database\Seeders\FeatureSeeder; // This seeder runs AFTER FeatureSeeder completes #[SeederAfter(FeatureSeeder::class)] class PackageSeeder extends Seeder { public function run(): void { // Can safely reference features created by FeatureSeeder } } // This seeder runs BEFORE PackageSeeder #[SeederBefore(PackageSeeder::class)] class FeatureSeeder extends Seeder { public function run(): void { // Features are created first } } ``` **Multiple dependencies:** ```php #[SeederAfter(FeatureSeeder::class, PackageSeeder::class)] class WorkspaceSeeder extends Seeder { // Runs after both FeatureSeeder and PackageSeeder } ``` ### Full Example ```php 'free', 'name' => 'Free', 'price_monthly' => 0, 'features' => ['basic.access'], 'sort_order' => 1, ], [ 'code' => 'pro', 'name' => 'Professional', 'price_monthly' => 29, 'features' => ['basic.access', 'advanced.features', 'support.priority'], 'sort_order' => 2, ], [ 'code' => 'enterprise', 'name' => 'Enterprise', 'price_monthly' => 99, 'features' => ['basic.access', 'advanced.features', 'support.priority', 'api.access'], 'sort_order' => 3, ], ]; foreach ($plans as $planData) { Plan::updateOrCreate( ['code' => $planData['code']], $planData ); } $this->command->info('Billing plans seeded successfully.'); } } ``` ### How Discovery Works 1. `SeederDiscovery` scans configured paths for `*Seeder.php` files 2. Files are read to extract namespace and class name 3. Priority and dependency attributes/properties are parsed 4. Seeders are topologically sorted (dependencies first, then by priority) 5. `CoreDatabaseSeeder` executes them in order ### Configuration The discovery scans these paths by default: - `app/Core/*/Database/Seeders/` - `app/Mod/*/Database/Seeders/` - `packages/core-php/src/Core/*/Database/Seeders/` - `packages/core-php/src/Mod/*/Database/Seeders/` ### Common Pitfalls | Pitfall | Solution | |---------|----------| | Circular dependencies | Review dependencies; use priority instead if possible | | Missing table errors | Check `Schema::hasTable()` before seeding | | Non-idempotent seeders | Use `updateOrCreate()` or `firstOrCreate()` | | Hardcoded IDs | Use codes/slugs for lookups, not numeric IDs | | Dependencies on non-existent seeders | Ensure dependent seeders are discoverable | | Forgetting to output progress | Use `$this->command->info()` for visibility | --- ## 6. Service Definition **Location:** `packages/core-php/src/Core/Service/` Services are the product layer of the framework. They define how modules are presented as SaaS products with versioning, health checks, and dependency management. ### When to Use - Defining a new SaaS product/service - Adding health monitoring to a service - Declaring service dependencies - Managing service lifecycle (deprecation, sunset) ### Key Components | Component | Purpose | |-----------|---------| | `ServiceDefinition` | Interface for service definitions | | `ServiceVersion` | Semantic versioning with deprecation | | `ServiceDependency` | Declare dependencies on other services | | `HealthCheckable` | Interface for health monitoring | | `HealthCheckResult` | Health check response object | ### Quick Start ```php 'billing', 'module' => 'Mod\\Billing', 'name' => 'Billing', 'tagline' => 'Subscription management', 'icon' => 'credit-card', 'color' => '#10B981', 'entitlement_code' => 'core.srv.billing', 'sort_order' => 20, ]; } public static function version(): ServiceVersion { return new ServiceVersion(1, 0, 0); } public static function dependencies(): array { return []; } // From AdminMenuProvider interface public function menuItems(): array { return [ [ 'label' => 'Billing', 'icon' => 'credit-card', 'route' => 'admin.billing.index', ], ]; } } ``` ### Full Example A complete service with health checks and dependencies: ```php 'analytics', 'module' => 'Mod\\Analytics', 'name' => 'AnalyticsHost', 'tagline' => 'Privacy-first website analytics', 'description' => 'Lightweight, GDPR-compliant analytics without cookies.', 'icon' => 'chart-line', 'color' => '#F59E0B', 'entitlement_code' => 'core.srv.analytics', 'sort_order' => 30, ]; } public static function version(): ServiceVersion { return new ServiceVersion(2, 1, 0); } public static function dependencies(): array { return [ ServiceDependency::required('auth', '>=1.0.0'), ServiceDependency::optional('billing'), ]; } public function healthCheck(): HealthCheckResult { try { $start = microtime(true); // Check ClickHouse connection $this->clickhouse->select('SELECT 1'); // Check Redis connection $this->redis->ping(); $responseTime = (microtime(true) - $start) * 1000; if ($responseTime > 1000) { return HealthCheckResult::degraded( 'Database responding slowly', ['response_time_ms' => $responseTime] ); } return HealthCheckResult::healthy( 'All systems operational', [], $responseTime ); } catch (\Exception $e) { return HealthCheckResult::fromException($e); } } public function menuItems(): array { return [ [ 'label' => 'Analytics', 'icon' => 'chart-line', 'route' => 'admin.analytics.dashboard', 'children' => [ ['label' => 'Dashboard', 'route' => 'admin.analytics.dashboard'], ['label' => 'Sites', 'route' => 'admin.analytics.sites'], ['label' => 'Reports', 'route' => 'admin.analytics.reports'], ], ], ]; } } ``` ### Service Versioning ```php use Core\Service\ServiceVersion; // Create a version $version = new ServiceVersion(2, 1, 0); echo $version; // "2.1.0" // Parse from string $version = ServiceVersion::fromString('v2.1.0'); // Check compatibility $minimum = new ServiceVersion(1, 5, 0); $current = new ServiceVersion(1, 8, 2); $current->isCompatibleWith($minimum); // true // Mark as deprecated $version = (new ServiceVersion(1, 0, 0)) ->deprecate( 'Use v2.x instead. See docs/migration.md', new \DateTimeImmutable('2025-06-01') ); // Check deprecation status if ($version->deprecated) { echo $version->deprecationMessage; } if ($version->isPastSunset()) { throw new ServiceSunsetException('Service no longer available'); } ``` ### Service Dependencies ```php use Core\Service\Contracts\ServiceDependency; public static function dependencies(): array { return [ // Required with minimum version ServiceDependency::required('auth', '>=1.0.0'), // Required with version range ServiceDependency::required('billing', '>=2.0.0', '<3.0.0'), // Optional dependency ServiceDependency::optional('analytics'), ]; } ``` ### Health Checks ```php use Core\Service\Contracts\HealthCheckable; use Core\Service\HealthCheckResult; class MyService implements ServiceDefinition, HealthCheckable { public function healthCheck(): HealthCheckResult { // Healthy return HealthCheckResult::healthy('All systems operational'); // Healthy with response time return HealthCheckResult::healthy( 'Service operational', ['connections' => 5], responseTimeMs: 45.2 ); // Degraded (works but with issues) return HealthCheckResult::degraded( 'High latency detected', ['latency_ms' => 1500] ); // Unhealthy return HealthCheckResult::unhealthy( 'Database connection failed', ['last_error' => 'Connection refused'] ); // From exception try { $this->checkCriticalSystem(); } catch (\Exception $e) { return HealthCheckResult::fromException($e); } } } ``` ### Health Check Guidelines Health checks should be: | Guideline | Recommendation | |-----------|----------------| | Fast | Complete within 5 seconds (< 1 second preferred) | | Non-destructive | Read-only operations only | | Representative | Test actual critical dependencies | | Safe | Handle all exceptions, return HealthCheckResult | ### Configuration Service definitions populate the `platform_services` table: ```php // definition() return array [ 'code' => 'billing', // Unique identifier (required) 'module' => 'Mod\\Billing', // Module namespace (required) 'name' => 'Billing', // Display name (required) 'tagline' => 'Subscription management', // Short description 'description' => '...', // Full description 'icon' => 'link', // FontAwesome icon 'color' => '#3B82F6', // Brand color 'entitlement_code' => '...', // Access control code 'sort_order' => 10, // Menu ordering ] ``` ### Common Pitfalls | Pitfall | Solution | |---------|----------| | Slow health checks | Keep under 1 second; test critical paths only | | Circular dependencies | Review service architecture; refactor if needed | | Missing version() | Always implement; return `ServiceVersion::initial()` at minimum | | Health check exceptions | Catch all exceptions; return `fromException()` | | Forgetting dependencies | Document all service interdependencies | --- ## Summary These patterns form the backbone of the Core PHP Framework: | Pattern | Purpose | Key Files | |---------|---------|-----------| | Actions | Encapsulate business logic | `Core\Actions\Action` | | Multi-Tenant | Data isolation between workspaces | `BelongsToWorkspace`, `WorkspaceScope` | | Module System | Lazy-loading via lifecycle events | `Boot.php`, `$listens` | | Activity Logging | Audit trail and change tracking | `LogsActivity`, `ActivityLogService` | | Seeder Discovery | Auto-discovered, ordered seeding | `#[SeederPriority]`, `#[SeederAfter]` | | Service Definition | SaaS product layer | `ServiceDefinition`, `HealthCheckable` | For more details, explore the source files in their respective locations or check the inline documentation.