From 13670ebb34e3a74d5bcab1e5c9065b8258a3de5e Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 22 Jan 2026 19:24:39 +0000 Subject: [PATCH] Add dependency guards and PHPDoc documentation Dependency guards for optional modules: - Remove hard use statements for Core\Mod\*, Core\Plug\* classes - Add class_exists() guards before using optional dependencies - Change type hints to ?object with docblock annotations - Add fallback behavior when optional modules not installed Files with dependency guards added: - Cdn/Console/CdnPurge.php (Workspace, Purge) - Cdn/Console/PushAssetsToCdn.php (VBucket) - Cdn/Boot.php (CdnManager, StorageManager) - Cdn/Jobs/PushAssetToCdn.php (StorageManager) - Front/Admin/AdminMenuRegistry.php (User, Workspace, EntitlementService) - Front/Admin/Contracts/*.php (User, Workspace) - Front/Admin/View/Components/Sidemenu.php (User, WorkspaceService) - Front/Mcp/McpContext.php (AgentPlan) Comprehensive PHPDoc documentation: - ModuleScanner.php - Scanner mechanics and priority system - ModuleRegistry.php - Registration flow and querying - LazyModuleListener.php - Lazy loading mechanism - LifecycleEventProvider.php - Lifecycle phases - All Event classes - When fired, context, examples - Service contracts - HealthCheckable, ServiceDefinition - Admin contracts - AdminMenuProvider, DynamicMenuProvider Co-Authored-By: Claude Opus 4.5 --- packages/core-php/src/Core/Cdn/Boot.php | 12 +- .../src/Core/Cdn/Console/CdnPurge.php | 42 ++++- .../src/Core/Cdn/Console/PushAssetsToCdn.php | 14 +- .../src/Core/Cdn/Jobs/PushAssetToCdn.php | 16 +- .../src/Core/Cdn/Services/AssetPipeline.php | 24 +-- .../src/Core/Cdn/Services/BunnyCdnService.php | 5 +- packages/core-php/src/Core/Config/Config.php | 26 ++- .../src/Core/Config/ConfigResolver.php | 35 ++-- .../src/Core/Config/ConfigService.php | 59 ++++--- .../Core/Config/Console/ConfigListCommand.php | 9 +- .../Config/Console/ConfigPrimeCommand.php | 19 ++- .../src/Core/Config/Models/Channel.php | 10 +- .../src/Core/Config/Models/ConfigResolved.php | 10 +- .../Config/View/Modal/Admin/ConfigPanel.php | 21 ++- .../View/Modal/Admin/WorkspaceConfig.php | 24 ++- .../src/Core/Events/AdminPanelBooting.php | 43 ++++- .../src/Core/Events/ApiRoutesRegistering.php | 37 ++++- .../Core/Events/ClientRoutesRegistering.php | 45 +++++- .../src/Core/Events/ConsoleBooting.php | 30 +++- .../src/Core/Events/DomainResolving.php | 61 ++++++- .../src/Core/Events/EventAuditLog.php | 52 ++++-- .../src/Core/Events/FrameworkBooted.php | 45 +++++- .../src/Core/Events/LifecycleEvent.php | 153 +++++++++++++++++- .../core-php/src/Core/Events/MailSending.php | 36 ++++- .../src/Core/Events/McpToolsRegistering.php | 47 +++++- .../src/Core/Events/MediaRequested.php | 40 ++++- .../src/Core/Events/QueueWorkerBooting.php | 42 ++++- .../src/Core/Events/SearchRequested.php | 35 +++- .../src/Core/Events/WebRoutesRegistering.php | 43 ++++- .../Core/Front/Admin/AdminMenuRegistry.php | 94 +++++++---- .../Admin/Concerns/HasMenuPermissions.php | 15 +- .../Admin/Contracts/AdminMenuProvider.php | 41 ++++- .../Admin/Contracts/DynamicMenuProvider.php | 51 ++++-- .../Front/Admin/View/Components/Sidemenu.php | 14 +- .../src/Core/Front/Mcp/McpContext.php | 14 +- .../Front/Web/Middleware/FindDomainRecord.php | 16 +- .../src/Core/Helpers/ServiceCollection.php | 9 +- .../core-php/src/Core/LazyModuleListener.php | 76 +++++++-- .../src/Core/LifecycleEventProvider.php | 141 +++++++++++++--- packages/core-php/src/Core/ModuleRegistry.php | 72 ++++++++- packages/core-php/src/Core/ModuleScanner.php | 67 ++++++-- packages/core-php/src/Core/Search/Unified.php | 20 +-- .../src/Core/Seo/Jobs/GenerateOgImageJob.php | 22 ++- packages/core-php/src/Core/Seo/Schema.php | 41 +++-- .../Seo/Services/SchemaBuilderService.php | 28 ++-- .../Service/Contracts/HealthCheckable.php | 31 +++- .../Service/Contracts/ServiceDefinition.php | 54 ++++--- 47 files changed, 1491 insertions(+), 350 deletions(-) diff --git a/packages/core-php/src/Core/Cdn/Boot.php b/packages/core-php/src/Core/Cdn/Boot.php index a117b15..516e5b9 100644 --- a/packages/core-php/src/Core/Cdn/Boot.php +++ b/packages/core-php/src/Core/Cdn/Boot.php @@ -20,8 +20,6 @@ use Core\Cdn\Services\BunnyStorageService; use Core\Cdn\Services\FluxCdnService; use Core\Cdn\Services\StorageOffload; use Core\Cdn\Services\StorageUrlResolver; -use Core\Plug\Cdn\CdnManager; -use Core\Plug\Storage\StorageManager; use Illuminate\Support\ServiceProvider; /** @@ -45,9 +43,13 @@ class Boot extends ServiceProvider $this->mergeConfigFrom(__DIR__.'/config.php', 'cdn'); $this->mergeConfigFrom(__DIR__.'/offload.php', 'offload'); - // Register Plug managers as singletons - $this->app->singleton(CdnManager::class); - $this->app->singleton(StorageManager::class); + // Register Plug managers as singletons (when available) + if (class_exists(\Core\Plug\Cdn\CdnManager::class)) { + $this->app->singleton(\Core\Plug\Cdn\CdnManager::class); + } + if (class_exists(\Core\Plug\Storage\StorageManager::class)) { + $this->app->singleton(\Core\Plug\Storage\StorageManager::class); + } // Register legacy services as singletons (for backward compatibility) $this->app->singleton(BunnyCdnService::class); diff --git a/packages/core-php/src/Core/Cdn/Console/CdnPurge.php b/packages/core-php/src/Core/Cdn/Console/CdnPurge.php index 050c684..2d474ba 100644 --- a/packages/core-php/src/Core/Cdn/Console/CdnPurge.php +++ b/packages/core-php/src/Core/Cdn/Console/CdnPurge.php @@ -10,8 +10,6 @@ declare(strict_types=1); namespace Core\Cdn\Console; -use Core\Mod\Tenant\Models\Workspace; -use Core\Plug\Cdn\Bunny\Purge; use Illuminate\Console\Command; class CdnPurge extends Command @@ -35,12 +33,18 @@ class CdnPurge extends Command */ protected $description = 'Purge content from CDN edge cache'; - protected Purge $purger; + /** + * Purger instance (Core\Plug\Cdn\Bunny\Purge when available). + */ + protected ?object $purger = null; public function __construct() { parent::__construct(); - $this->purger = new Purge; + + if (class_exists(\Core\Plug\Cdn\Bunny\Purge::class)) { + $this->purger = new \Core\Plug\Cdn\Bunny\Purge; + } } /** @@ -48,6 +52,12 @@ class CdnPurge extends Command */ public function handle(): int { + if ($this->purger === null) { + $this->error('CDN Purge requires Core\Plug\Cdn\Bunny\Purge class. Plug module not installed.'); + + return self::FAILURE; + } + $workspaceArg = $this->argument('workspace'); $urls = $this->option('url'); $tag = $this->option('tag'); @@ -84,9 +94,13 @@ class CdnPurge extends Command // Purge by workspace if (empty($workspaceArg)) { + $workspaceOptions = ['all', 'Select specific URLs']; + if (class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + $workspaceOptions = array_merge($workspaceOptions, \Core\Mod\Tenant\Models\Workspace::pluck('slug')->toArray()); + } $workspaceArg = $this->choice( 'What would you like to purge?', - array_merge(['all', 'Select specific URLs'], Workspace::pluck('slug')->toArray()), + $workspaceOptions, 0 ); @@ -203,7 +217,13 @@ class CdnPurge extends Command protected function purgeAllWorkspaces(bool $dryRun): int { - $workspaces = Workspace::all(); + if (! class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + $this->error('Workspace purge requires Tenant module to be installed.'); + + return self::FAILURE; + } + + $workspaces = \Core\Mod\Tenant\Models\Workspace::all(); if ($workspaces->isEmpty()) { $this->error('No workspaces found'); @@ -255,13 +275,19 @@ class CdnPurge extends Command protected function purgeWorkspace(string $slug, bool $dryRun): int { - $workspace = Workspace::where('slug', $slug)->first(); + if (! class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + $this->error('Workspace purge requires Tenant module to be installed.'); + + return self::FAILURE; + } + + $workspace = \Core\Mod\Tenant\Models\Workspace::where('slug', $slug)->first(); if (! $workspace) { $this->error("Workspace not found: {$slug}"); $this->newLine(); $this->info('Available workspaces:'); - Workspace::pluck('slug')->each(fn ($s) => $this->line(" - {$s}")); + \Core\Mod\Tenant\Models\Workspace::pluck('slug')->each(fn ($s) => $this->line(" - {$s}")); return self::FAILURE; } diff --git a/packages/core-php/src/Core/Cdn/Console/PushAssetsToCdn.php b/packages/core-php/src/Core/Cdn/Console/PushAssetsToCdn.php index af0fbab..5e44302 100644 --- a/packages/core-php/src/Core/Cdn/Console/PushAssetsToCdn.php +++ b/packages/core-php/src/Core/Cdn/Console/PushAssetsToCdn.php @@ -12,7 +12,6 @@ namespace Core\Cdn\Console; use Core\Cdn\Services\FluxCdnService; use Core\Cdn\Services\StorageUrlResolver; -use Core\Plug\Storage\Bunny\VBucket; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; @@ -30,7 +29,10 @@ class PushAssetsToCdn extends Command protected StorageUrlResolver $cdn; - protected VBucket $vbucket; + /** + * VBucket instance (Core\Plug\Storage\Bunny\VBucket when available). + */ + protected ?object $vbucket = null; protected bool $dryRun = false; @@ -40,12 +42,18 @@ class PushAssetsToCdn extends Command public function handle(FluxCdnService $flux, StorageUrlResolver $cdn): int { + if (! class_exists(\Core\Plug\Storage\Bunny\VBucket::class)) { + $this->error('Push assets to CDN requires Core\Plug\Storage\Bunny\VBucket class. Plug module not installed.'); + + return self::FAILURE; + } + $this->cdn = $cdn; $this->dryRun = $this->option('dry-run'); // Create vBucket for workspace isolation $domain = $this->option('domain'); - $this->vbucket = VBucket::public($domain); + $this->vbucket = \Core\Plug\Storage\Bunny\VBucket::public($domain); $pushFlux = $this->option('flux'); $pushFontawesome = $this->option('fontawesome'); diff --git a/packages/core-php/src/Core/Cdn/Jobs/PushAssetToCdn.php b/packages/core-php/src/Core/Cdn/Jobs/PushAssetToCdn.php index f631b95..4a5e9e2 100644 --- a/packages/core-php/src/Core/Cdn/Jobs/PushAssetToCdn.php +++ b/packages/core-php/src/Core/Cdn/Jobs/PushAssetToCdn.php @@ -10,7 +10,6 @@ declare(strict_types=1); namespace Core\Cdn\Jobs; -use Core\Plug\Storage\StorageManager; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -55,9 +54,22 @@ class PushAssetToCdn implements ShouldQueue /** * Execute the job. + * + * @param object|null $storage StorageManager instance when Plug module available */ - public function handle(StorageManager $storage): void + public function handle(?object $storage = null): void { + if (! class_exists(\Core\Plug\Storage\StorageManager::class)) { + Log::warning('PushAssetToCdn: StorageManager not available, Plug module not installed'); + + return; + } + + // Resolve from container if not injected + if ($storage === null) { + $storage = app(\Core\Plug\Storage\StorageManager::class); + } + if (! config('cdn.bunny.push_enabled', false)) { Log::debug('PushAssetToCdn: Push disabled, skipping', [ 'disk' => $this->disk, diff --git a/packages/core-php/src/Core/Cdn/Services/AssetPipeline.php b/packages/core-php/src/Core/Cdn/Services/AssetPipeline.php index 19559b0..9be7020 100644 --- a/packages/core-php/src/Core/Cdn/Services/AssetPipeline.php +++ b/packages/core-php/src/Core/Cdn/Services/AssetPipeline.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Core\Cdn\Services; use Core\Cdn\Jobs\PushAssetToCdn; -use Core\Plug\Storage\StorageManager; use Illuminate\Http\UploadedFile; use Illuminate\Support\Str; @@ -37,9 +36,12 @@ class AssetPipeline { protected StorageUrlResolver $urlResolver; - protected StorageManager $storage; + /** + * Storage manager instance (Core\Plug\Storage\StorageManager when available). + */ + protected ?object $storage = null; - public function __construct(StorageUrlResolver $urlResolver, StorageManager $storage) + public function __construct(StorageUrlResolver $urlResolver, ?object $storage = null) { $this->urlResolver = $urlResolver; $this->storage = $storage; @@ -220,8 +222,10 @@ class AssetPipeline $results[$path] = $disk->delete($path); } - // Bulk delete from CDN storage - $this->storage->zone($bucket)->delete()->paths($paths); + // Bulk delete from CDN storage (requires StorageManager from Plug module) + if ($this->storage !== null) { + $this->storage->zone($bucket)->delete()->paths($paths); + } // Purge from CDN cache if enabled if (config('cdn.pipeline.auto_purge', true)) { @@ -299,11 +303,11 @@ class AssetPipeline if ($queue) { PushAssetToCdn::dispatch($disk, $path, $zone); - } else { - // Synchronous push if no queue configured - $disk = \Illuminate\Support\Facades\Storage::disk($disk); - if ($disk->exists($path)) { - $contents = $disk->get($path); + } elseif ($this->storage !== null) { + // Synchronous push if no queue configured (requires StorageManager from Plug module) + $diskInstance = \Illuminate\Support\Facades\Storage::disk($disk); + if ($diskInstance->exists($path)) { + $contents = $diskInstance->get($path); $this->storage->zone($zone)->upload()->contents($path, $contents); } } diff --git a/packages/core-php/src/Core/Cdn/Services/BunnyCdnService.php b/packages/core-php/src/Core/Cdn/Services/BunnyCdnService.php index c83d253..defc877 100644 --- a/packages/core-php/src/Core/Cdn/Services/BunnyCdnService.php +++ b/packages/core-php/src/Core/Cdn/Services/BunnyCdnService.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Core\Cdn\Services; use Core\Config\ConfigService; -use Core\Mod\Tenant\Models\Workspace; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -167,8 +166,10 @@ class BunnyCdnService /** * Purge all cached content for a workspace. + * + * @param object $workspace Workspace model instance (requires uuid property) */ - public function purgeWorkspace(Workspace $workspace): bool + public function purgeWorkspace(object $workspace): bool { return $this->purgeByTag("workspace-{$workspace->uuid}"); } diff --git a/packages/core-php/src/Core/Config/Config.php b/packages/core-php/src/Core/Config/Config.php index a3b5095..0615d82 100644 --- a/packages/core-php/src/Core/Config/Config.php +++ b/packages/core-php/src/Core/Config/Config.php @@ -10,8 +10,6 @@ declare(strict_types=1); namespace Core\Config; -use Core\Mod\Social\Contracts\Config as ConfigContract; -use Core\Mod\Social\Models\Config as ConfigModel; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; @@ -21,8 +19,11 @@ use Illuminate\Support\Facades\Cache; * * Provides a standardised interface for managing configuration settings * with validation, caching, and database persistence. + * + * Note: This class requires Core\Mod\Social module to be installed for + * database persistence functionality. Implements ConfigContract when available. */ -abstract class Config implements ConfigContract +abstract class Config { /** * Create a new config instance. @@ -58,12 +59,18 @@ abstract class Config implements ConfigContract /** * Insert or update configuration in database. * + * Requires Core\Mod\Social module to be installed. + * * @param string $name Configuration field name * @param mixed $payload Value to store */ public function insert(string $name, mixed $payload): void { - ConfigModel::updateOrCreate( + if (! class_exists(\Core\Mod\Social\Models\Config::class)) { + return; + } + + \Core\Mod\Social\Models\Config::updateOrCreate( ['name' => $name, 'group' => $this->group()], ['payload' => $payload] ); @@ -73,15 +80,22 @@ abstract class Config implements ConfigContract * Get a configuration value. * * Checks cache first, then database, finally falls back to default from form(). + * Requires Core\Mod\Social module for database lookup. * * @param string $name Configuration field name */ public function get(string $name): mixed { return $this->getCache($name, function () use ($name) { - $payload = ConfigModel::get( + $default = Arr::get($this->form(), $name); + + if (! class_exists(\Core\Mod\Social\Models\Config::class)) { + return $default; + } + + $payload = \Core\Mod\Social\Models\Config::get( property: "{$this->group()}.{$name}", - default: Arr::get($this->form(), $name) + default: $default ); $this->putCache($name, $payload); diff --git a/packages/core-php/src/Core/Config/ConfigResolver.php b/packages/core-php/src/Core/Config/ConfigResolver.php index 5bddeae..293a04d 100644 --- a/packages/core-php/src/Core/Config/ConfigResolver.php +++ b/packages/core-php/src/Core/Config/ConfigResolver.php @@ -15,7 +15,6 @@ use Core\Config\Models\Channel; use Core\Config\Models\ConfigKey; use Core\Config\Models\ConfigProfile; use Core\Config\Models\ConfigValue; -use Core\Mod\Tenant\Models\Workspace; use Illuminate\Support\Collection; /** @@ -150,11 +149,12 @@ class ConfigResolver * NOTE: This is the expensive path - only called when lazy-priming. * Normal reads hit the hash directly via ConfigService. * + * @param object|null $workspace Workspace model instance or null for system scope * @param string|Channel|null $channel Channel code or object */ public function resolve( string $keyCode, - ?Workspace $workspace = null, + ?object $workspace = null, string|Channel|null $channel = null, ): ConfigResult { // Get key definition (DB query - only during resolve, not normal reads) @@ -229,9 +229,12 @@ class ConfigResolver /** * Try to resolve a JSON sub-key (e.g., "website.title" from "website" JSON). */ + /** + * @param object|null $workspace Workspace model instance or null for system scope + */ protected function resolveJsonSubKey( string $keyCode, - ?Workspace $workspace, + ?object $workspace, string|Channel|null $channel, ): ConfigResult { // Guard against stack overflow from deep nesting @@ -277,11 +280,12 @@ class ConfigResolver /** * Build the channel inheritance chain. * + * @param object|null $workspace Workspace model instance or null for system scope * @return Collection */ public function buildChannelChain( string|Channel|null $channel, - ?Workspace $workspace = null, + ?object $workspace = null, ): Collection { $chain = new Collection; @@ -407,7 +411,7 @@ class ConfigResolver * Providers supply values from module data without database storage. * * @param string $pattern Key pattern (supports * wildcard) - * @param callable $provider fn(string $key, ?Workspace $workspace, ?Channel $channel): mixed + * @param callable $provider fn(string $key, ?object $workspace, ?Channel $channel): mixed */ public function registerProvider(string $pattern, callable $provider): void { @@ -416,10 +420,12 @@ class ConfigResolver /** * Resolve value from virtual providers. + * + * @param object|null $workspace Workspace model instance or null for system scope */ public function resolveFromProviders( string $keyCode, - ?Workspace $workspace, + ?object $workspace, string|Channel|null $channel, ): mixed { foreach ($this->providers as $pattern => $provider) { @@ -455,9 +461,10 @@ class ConfigResolver * * NOTE: Only called during prime, not normal reads. * + * @param object|null $workspace Workspace model instance or null for system scope * @return array */ - public function resolveAll(?Workspace $workspace = null, string|Channel|null $channel = null): array + public function resolveAll(?object $workspace = null, string|Channel|null $channel = null): array { $results = []; @@ -474,11 +481,12 @@ class ConfigResolver * * NOTE: Only called during prime, not normal reads. * + * @param object|null $workspace Workspace model instance or null for system scope * @return array */ public function resolveCategory( string $category, - ?Workspace $workspace = null, + ?object $workspace = null, string|Channel|null $channel = null, ): array { $results = []; @@ -497,9 +505,10 @@ class ConfigResolver * Returns profiles ordered from most specific (workspace) to least (system). * Chain: workspace → org → system * + * @param object|null $workspace Workspace model instance or null for system scope * @return Collection */ - public function buildProfileChain(?Workspace $workspace = null): Collection + public function buildProfileChain(?object $workspace = null): Collection { $chain = new Collection; @@ -531,8 +540,10 @@ class ConfigResolver * * Stub for now - will connect to Tenant module when org model exists. * Organisation = multi-workspace grouping (agency accounts, teams). + * + * @param object|null $workspace Workspace model instance or null */ - protected function resolveOrgId(?Workspace $workspace): ?int + protected function resolveOrgId(?object $workspace): ?int { if ($workspace === null) { return null; @@ -590,10 +601,12 @@ class ConfigResolver * Check if a key prefix is configured. * * Optimised to use EXISTS query instead of resolving each key. + * + * @param object|null $workspace Workspace model instance or null for system scope */ public function isPrefixConfigured( string $prefix, - ?Workspace $workspace = null, + ?object $workspace = null, string|Channel|null $channel = null, ): bool { // Get profile IDs for this workspace diff --git a/packages/core-php/src/Core/Config/ConfigService.php b/packages/core-php/src/Core/Config/ConfigService.php index 9b9092b..8c23533 100644 --- a/packages/core-php/src/Core/Config/ConfigService.php +++ b/packages/core-php/src/Core/Config/ConfigService.php @@ -19,7 +19,6 @@ use Core\Config\Models\ConfigKey; use Core\Config\Models\ConfigProfile; use Core\Config\Models\ConfigResolved; use Core\Config\Models\ConfigValue; -use Core\Mod\Tenant\Models\Workspace; /** * Configuration service - main API. @@ -37,7 +36,10 @@ use Core\Mod\Tenant\Models\Workspace; */ class ConfigService { - protected ?Workspace $workspace = null; + /** + * Current workspace context (Workspace model instance or null for system scope). + */ + protected ?object $workspace = null; protected ?Channel $channel = null; @@ -47,8 +49,10 @@ class ConfigService /** * Set the current context (called by middleware). + * + * @param object|null $workspace Workspace model instance or null for system scope */ - public function setContext(?Workspace $workspace, ?Channel $channel = null): void + public function setContext(?object $workspace, ?Channel $channel = null): void { $this->workspace = $workspace; $this->channel = $channel; @@ -56,8 +60,10 @@ class ConfigService /** * Get current workspace context. + * + * @return object|null Workspace model instance or null */ - public function getWorkspace(): ?Workspace + public function getWorkspace(): ?object { return $this->workspace; } @@ -79,8 +85,10 @@ class ConfigService * Get config for a specific workspace (admin use only). * * Use this when you need another workspace's settings - requires explicit intent. + * + * @param object $workspace Workspace model instance */ - public function getForWorkspace(string $key, Workspace $workspace, mixed $default = null): mixed + public function getForWorkspace(string $key, object $workspace, mixed $default = null): mixed { $result = $this->resolve($key, $workspace, null); @@ -96,11 +104,12 @@ class ConfigService * 3. Hash lookup again * 4. Compute via resolver if still not found (lazy prime) * + * @param object|null $workspace Workspace model instance or null for system scope * @param string|Channel|null $channel Channel code or object */ public function resolve( string $key, - ?Workspace $workspace = null, + ?object $workspace = null, string|Channel|null $channel = null, ): ConfigResult { $workspaceId = $workspace?->id; @@ -207,10 +216,12 @@ class ConfigService /** * Try to resolve a JSON sub-key (e.g., "website.title" from "website" JSON). + * + * @param object|null $workspace Workspace model instance or null for system scope */ protected function resolveJsonSubKey( string $keyCode, - ?Workspace $workspace, + ?object $workspace, string|Channel|null $channel, ): ConfigResult { $parts = explode('.', $keyCode); @@ -394,9 +405,10 @@ class ConfigService /** * Get all config values for a workspace. * + * @param object|null $workspace Workspace model instance or null for system scope * @return array */ - public function all(?Workspace $workspace = null, string|Channel|null $channel = null): array + public function all(?object $workspace = null, string|Channel|null $channel = null): array { $workspaceId = $workspace?->id; $channelId = $this->resolveChannelId($channel, $workspace); @@ -414,11 +426,12 @@ class ConfigService /** * Get all config values for a category. * + * @param object|null $workspace Workspace model instance or null for system scope * @return array */ public function category( string $category, - ?Workspace $workspace = null, + ?object $workspace = null, string|Channel|null $channel = null, ): array { $workspaceId = $workspace?->id; @@ -447,8 +460,10 @@ class ConfigService * Call after workspace creation, config changes, or on schedule. * * Populates both hash (process-scoped) and database (persistent). + * + * @param object|null $workspace Workspace model instance or null for system scope */ - public function prime(?Workspace $workspace = null, string|Channel|null $channel = null): void + public function prime(?object $workspace = null, string|Channel|null $channel = null): void { $workspaceId = $workspace?->id; $channelId = $this->resolveChannelId($channel, $workspace); @@ -500,7 +515,10 @@ class ConfigService ->delete(); // Re-compute this key for the affected scope - $workspace = $workspaceId !== null ? Workspace::find($workspaceId) : null; + $workspace = null; + if ($workspaceId !== null && class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + $workspace = \Core\Mod\Tenant\Models\Workspace::find($workspaceId); + } $channel = $channelId ? Channel::find($channelId) : null; $result = $this->resolver->resolve($keyCode, $workspace, $channel); @@ -526,18 +544,21 @@ class ConfigService * Prime cache for all workspaces. * * Run this from a scheduled command or queue job. + * Requires Core\Mod\Tenant module to prime workspace-level config. */ public function primeAll(): void { // Prime system config $this->prime(null); - // Prime each workspace - Workspace::chunk(100, function ($workspaces) { - foreach ($workspaces as $workspace) { - $this->prime($workspace); - } - }); + // Prime each workspace (requires Tenant module) + if (class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + \Core\Mod\Tenant\Models\Workspace::chunk(100, function ($workspaces) { + foreach ($workspaces as $workspace) { + $this->prime($workspace); + } + }); + } } /** @@ -545,8 +566,10 @@ class ConfigService * * Clears both hash and database. Next read will lazy-prime. * Fires ConfigInvalidated event. + * + * @param object|null $workspace Workspace model instance or null for system scope */ - public function invalidateWorkspace(?Workspace $workspace = null): void + public function invalidateWorkspace(?object $workspace = null): void { $workspaceId = $workspace?->id; diff --git a/packages/core-php/src/Core/Config/Console/ConfigListCommand.php b/packages/core-php/src/Core/Config/Console/ConfigListCommand.php index 27dd04f..b4331d2 100644 --- a/packages/core-php/src/Core/Config/Console/ConfigListCommand.php +++ b/packages/core-php/src/Core/Config/Console/ConfigListCommand.php @@ -12,7 +12,6 @@ namespace Core\Config\Console; use Core\Config\ConfigService; use Core\Config\Models\ConfigKey; -use Core\Mod\Tenant\Models\Workspace; use Illuminate\Console\Command; class ConfigListCommand extends Command @@ -33,7 +32,13 @@ class ConfigListCommand extends Command $workspace = null; if ($workspaceSlug) { - $workspace = Workspace::where('slug', $workspaceSlug)->first(); + if (! class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + $this->error('Tenant module not installed. Cannot filter by workspace.'); + + return self::FAILURE; + } + + $workspace = \Core\Mod\Tenant\Models\Workspace::where('slug', $workspaceSlug)->first(); if (! $workspace) { $this->error("Workspace not found: {$workspaceSlug}"); diff --git a/packages/core-php/src/Core/Config/Console/ConfigPrimeCommand.php b/packages/core-php/src/Core/Config/Console/ConfigPrimeCommand.php index 9a67003..9fa8c89 100644 --- a/packages/core-php/src/Core/Config/Console/ConfigPrimeCommand.php +++ b/packages/core-php/src/Core/Config/Console/ConfigPrimeCommand.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Core\Config\Console; use Core\Config\ConfigService; -use Core\Mod\Tenant\Models\Workspace; use Illuminate\Console\Command; class ConfigPrimeCommand extends Command @@ -36,7 +35,13 @@ class ConfigPrimeCommand extends Command } if ($workspaceSlug) { - $workspace = Workspace::where('slug', $workspaceSlug)->first(); + if (! class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + $this->error('Tenant module not installed. Cannot prime workspace config.'); + + return self::FAILURE; + } + + $workspace = \Core\Mod\Tenant\Models\Workspace::where('slug', $workspaceSlug)->first(); if (! $workspace) { $this->error("Workspace not found: {$workspaceSlug}"); @@ -53,7 +58,15 @@ class ConfigPrimeCommand extends Command $this->info('Priming config cache for all workspaces...'); - $this->withProgressBar(Workspace::all(), function ($workspace) use ($config) { + if (! class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + $this->warn('Tenant module not installed. Only priming system config.'); + $config->prime(null); + $this->info('System config cached.'); + + return self::SUCCESS; + } + + $this->withProgressBar(\Core\Mod\Tenant\Models\Workspace::all(), function ($workspace) use ($config) { $config->prime($workspace); }); diff --git a/packages/core-php/src/Core/Config/Models/Channel.php b/packages/core-php/src/Core/Config/Models/Channel.php index c2030be..5076871 100644 --- a/packages/core-php/src/Core/Config/Models/Channel.php +++ b/packages/core-php/src/Core/Config/Models/Channel.php @@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Core\Mod\Tenant\Models\Workspace; /** * Configuration channel (voice/context substrate). @@ -71,10 +70,17 @@ class Channel extends Model /** * Workspace this channel belongs to (null = system channel). + * + * Requires Core\Mod\Tenant module to be installed. */ public function workspace(): BelongsTo { - return $this->belongsTo(Workspace::class); + if (class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + return $this->belongsTo(\Core\Mod\Tenant\Models\Workspace::class); + } + + // Return a null relationship when Tenant module is not installed + return $this->belongsTo(self::class, 'workspace_id')->whereRaw('1 = 0'); } /** diff --git a/packages/core-php/src/Core/Config/Models/ConfigResolved.php b/packages/core-php/src/Core/Config/Models/ConfigResolved.php index 6a6007a..312e76c 100644 --- a/packages/core-php/src/Core/Config/Models/ConfigResolved.php +++ b/packages/core-php/src/Core/Config/Models/ConfigResolved.php @@ -12,7 +12,6 @@ namespace Core\Config\Models; use Core\Config\ConfigResult; use Core\Config\Enums\ConfigType; -use Core\Mod\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -66,10 +65,17 @@ class ConfigResolved extends Model /** * Workspace this resolution is for (null = system). + * + * Requires Core\Mod\Tenant module to be installed. */ public function workspace(): BelongsTo { - return $this->belongsTo(Workspace::class); + if (class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + return $this->belongsTo(\Core\Mod\Tenant\Models\Workspace::class); + } + + // Return a null relationship when Tenant module is not installed + return $this->belongsTo(self::class, 'workspace_id')->whereRaw('1 = 0'); } /** diff --git a/packages/core-php/src/Core/Config/View/Modal/Admin/ConfigPanel.php b/packages/core-php/src/Core/Config/View/Modal/Admin/ConfigPanel.php index cb12c90..d88757a 100644 --- a/packages/core-php/src/Core/Config/View/Modal/Admin/ConfigPanel.php +++ b/packages/core-php/src/Core/Config/View/Modal/Admin/ConfigPanel.php @@ -14,7 +14,6 @@ use Core\Config\ConfigService; use Core\Config\Models\ConfigKey; use Core\Config\Models\ConfigProfile; use Core\Config\Models\ConfigValue; -use Core\Mod\Tenant\Models\Workspace; use Livewire\Attributes\Computed; use Livewire\Attributes\Url; use Livewire\Component; @@ -68,10 +67,17 @@ class ConfigPanel extends Component ->toArray(); } + /** + * Get all workspaces (requires Tenant module). + */ #[Computed] public function workspaces(): \Illuminate\Database\Eloquent\Collection { - return Workspace::orderBy('name')->get(); + if (! class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + return new \Illuminate\Database\Eloquent\Collection; + } + + return \Core\Mod\Tenant\Models\Workspace::orderBy('name')->get(); } #[Computed] @@ -97,11 +103,16 @@ class ConfigPanel extends Component return ConfigProfile::ensureSystem(); } + /** + * Get selected workspace (requires Tenant module). + * + * @return object|null Workspace model instance or null + */ #[Computed] - public function selectedWorkspace(): ?Workspace + public function selectedWorkspace(): ?object { - if ($this->workspaceId) { - return Workspace::find($this->workspaceId); + if ($this->workspaceId && class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + return \Core\Mod\Tenant\Models\Workspace::find($this->workspaceId); } return null; diff --git a/packages/core-php/src/Core/Config/View/Modal/Admin/WorkspaceConfig.php b/packages/core-php/src/Core/Config/View/Modal/Admin/WorkspaceConfig.php index 1bbb88e..85e94c5 100644 --- a/packages/core-php/src/Core/Config/View/Modal/Admin/WorkspaceConfig.php +++ b/packages/core-php/src/Core/Config/View/Modal/Admin/WorkspaceConfig.php @@ -14,8 +14,6 @@ use Core\Config\ConfigService; use Core\Config\Models\ConfigKey; use Core\Config\Models\ConfigProfile; use Core\Config\Models\ConfigValue; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Services\WorkspaceService; use Livewire\Attributes\Computed; use Livewire\Attributes\On; use Livewire\Component; @@ -26,12 +24,19 @@ class WorkspaceConfig extends Component protected ConfigService $config; - protected WorkspaceService $workspaceService; + /** + * Workspace service instance (from Tenant module when available). + */ + protected ?object $workspaceService = null; - public function boot(ConfigService $config, WorkspaceService $workspaceService): void + public function boot(ConfigService $config): void { $this->config = $config; - $this->workspaceService = $workspaceService; + + // Try to resolve WorkspaceService if Tenant module is installed + if (class_exists(\Core\Mod\Tenant\Services\WorkspaceService::class)) { + $this->workspaceService = app(\Core\Mod\Tenant\Services\WorkspaceService::class); + } } public function mount(?string $path = null): void @@ -55,10 +60,15 @@ class WorkspaceConfig extends Component unset($this->currentKeys); } + /** + * Get current workspace (requires Tenant module). + * + * @return object|null Workspace model instance or null + */ #[Computed] - public function workspace(): ?Workspace + public function workspace(): ?object { - return $this->workspaceService->currentModel(); + return $this->workspaceService?->currentModel(); } #[Computed] diff --git a/packages/core-php/src/Core/Events/AdminPanelBooting.php b/packages/core-php/src/Core/Events/AdminPanelBooting.php index a8568ce..7a76dfe 100644 --- a/packages/core-php/src/Core/Events/AdminPanelBooting.php +++ b/packages/core-php/src/Core/Events/AdminPanelBooting.php @@ -13,13 +13,44 @@ namespace Core\Events; /** * Fired when the admin panel is being bootstrapped. * - * Modules listen to this event to register: - * - Admin navigation items - * - Admin routes (wrapped with admin middleware) - * - Admin view namespaces - * - Admin Livewire components + * Modules listen to this event to register admin-specific resources including + * routes, views, Livewire components, and translations for the admin dashboard. * - * Only fired for requests to admin routes, not public pages or API calls. + * ## When This Event Fires + * + * Fired by `LifecycleEventProvider::fireAdminBooting()` only for requests + * to admin routes. Not fired for public pages, API calls, or client dashboard. + * + * ## Middleware + * + * Routes registered through this event are automatically wrapped with the 'admin' + * middleware group, which typically includes authentication, admin authorization, etc. + * + * ## Navigation Items + * + * For admin navigation, consider implementing `AdminMenuProvider` interface + * for more control over menu items including permissions, entitlements, and groups. + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * AdminPanelBooting::class => 'onAdmin', + * ]; + * + * public function onAdmin(AdminPanelBooting $event): void + * { + * $event->views('commerce', __DIR__.'/Views/Admin'); + * $event->translations('commerce', __DIR__.'/Lang'); + * $event->livewire('commerce-dashboard', DashboardComponent::class); + * $event->routes(fn () => require __DIR__.'/Routes/admin.php'); + * } + * ``` + * + * @package Core\Events + * + * @see AdminMenuProvider For navigation registration + * @see WebRoutesRegistering For public web routes */ class AdminPanelBooting extends LifecycleEvent { diff --git a/packages/core-php/src/Core/Events/ApiRoutesRegistering.php b/packages/core-php/src/Core/Events/ApiRoutesRegistering.php index e9f373a..b858fef 100644 --- a/packages/core-php/src/Core/Events/ApiRoutesRegistering.php +++ b/packages/core-php/src/Core/Events/ApiRoutesRegistering.php @@ -11,12 +11,41 @@ declare(strict_types=1); namespace Core\Events; /** - * Fired when API routes are being registered. + * Fired when REST API routes are being registered. * - * Modules listen to this event to register their REST API endpoints. - * Routes are automatically wrapped with the 'api' middleware group. + * Modules listen to this event to register their REST API endpoints for + * programmatic access by external applications, mobile apps, or SPAs. * - * Only fired for API requests, not web or admin requests. + * ## When This Event Fires + * + * Fired by `LifecycleEventProvider::fireApiRoutes()` when the API frontage + * initializes, typically for requests to `/api/*` routes. + * + * ## Middleware and Prefix + * + * Routes registered through this event are automatically: + * - Wrapped with the 'api' middleware group (typically stateless, rate limiting) + * - Prefixed with `/api` + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * ApiRoutesRegistering::class => 'onApi', + * ]; + * + * public function onApi(ApiRoutesRegistering $event): void + * { + * $event->routes(fn () => require __DIR__.'/Routes/api.php'); + * } + * ``` + * + * Note: API routes typically don't need views or Livewire components, but + * all LifecycleEvent methods are available if needed. + * + * @package Core\Events + * + * @see WebRoutesRegistering For web routes with session state */ class ApiRoutesRegistering extends LifecycleEvent { diff --git a/packages/core-php/src/Core/Events/ClientRoutesRegistering.php b/packages/core-php/src/Core/Events/ClientRoutesRegistering.php index 7123039..c7a1d01 100644 --- a/packages/core-php/src/Core/Events/ClientRoutesRegistering.php +++ b/packages/core-php/src/Core/Events/ClientRoutesRegistering.php @@ -11,17 +11,48 @@ declare(strict_types=1); namespace Core\Events; /** - * Fired when client routes are being registered. + * Fired when client dashboard routes are being registered. * - * Modules listen to this event to register routes for namespace owners - * (authenticated SaaS customers managing their space). + * Modules listen to this event to register routes for namespace owners - + * authenticated SaaS customers who manage their own space within the platform. * - * Routes are automatically wrapped with the 'client' middleware group. + * ## When This Event Fires * - * Use this for authenticated namespace management pages: - * - Bio/link editors - * - Settings pages + * Fired by `LifecycleEventProvider::fireClientRoutes()` when the client + * frontage initializes, typically for requests to client dashboard routes. + * + * ## Middleware + * + * Routes registered through this event are automatically wrapped with the 'client' + * middleware group, which typically includes authentication and workspace context. + * + * ## Typical Use Cases + * + * - Bio/link page editors + * - User settings and preferences * - Analytics dashboards + * - Content management + * - Billing and subscription management + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * ClientRoutesRegistering::class => 'onClient', + * ]; + * + * public function onClient(ClientRoutesRegistering $event): void + * { + * $event->views('bio', __DIR__.'/Views/Client'); + * $event->livewire('bio-editor', BioEditorComponent::class); + * $event->routes(fn () => require __DIR__.'/Routes/client.php'); + * } + * ``` + * + * @package Core\Events + * + * @see AdminPanelBooting For admin/staff routes + * @see WebRoutesRegistering For public-facing routes */ class ClientRoutesRegistering extends LifecycleEvent { diff --git a/packages/core-php/src/Core/Events/ConsoleBooting.php b/packages/core-php/src/Core/Events/ConsoleBooting.php index 0895747..9e1c90d 100644 --- a/packages/core-php/src/Core/Events/ConsoleBooting.php +++ b/packages/core-php/src/Core/Events/ConsoleBooting.php @@ -11,13 +11,33 @@ declare(strict_types=1); namespace Core\Events; /** - * Fired when running in console/CLI context. + * Fired when the application runs in console/CLI context. * - * Modules listen to this event to register Artisan commands. - * Commands are registered via the command() method inherited - * from LifecycleEvent. + * Modules listen to this event to register Artisan commands for CLI operations + * such as maintenance tasks, data processing, or administrative functions. * - * Only fired when running artisan commands, not web requests. + * ## When This Event Fires + * + * Fired when the application is invoked via `php artisan`, not during web + * requests. This allows modules to only register commands when actually needed. + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * ConsoleBooting::class => 'onConsole', + * ]; + * + * public function onConsole(ConsoleBooting $event): void + * { + * $event->command(ProcessOrdersCommand::class); + * $event->command(SyncInventoryCommand::class); + * } + * ``` + * + * @package Core\Events + * + * @see QueueWorkerBooting For queue worker specific initialization */ class ConsoleBooting extends LifecycleEvent { diff --git a/packages/core-php/src/Core/Events/DomainResolving.php b/packages/core-php/src/Core/Events/DomainResolving.php index 2dab3ca..02615f2 100644 --- a/packages/core-php/src/Core/Events/DomainResolving.php +++ b/packages/core-php/src/Core/Events/DomainResolving.php @@ -13,19 +13,63 @@ namespace Core\Events; /** * Fired when resolving a domain to a website provider. * - * Mod Boot classes listen for this event and register themselves - * if their domain pattern matches the incoming host. + * This event enables multi-tenancy by domain, allowing different modules to + * handle requests based on the incoming hostname. Website modules listen + * to this event and register themselves if their domain pattern matches. + * + * ## When This Event Fires + * + * Fired early in the request lifecycle when the framework needs to determine + * which website provider should handle the current request. Only the first + * provider to register wins. + * + * ## Domain Pattern Matching + * + * Use regex patterns to match domains: + * - `/^example\.com$/` - Exact domain match + * - `/\.example\.com$/` - Subdomain wildcard + * - `/^(www\.)?example\.com$/` - Optional www prefix + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * DomainResolving::class => 'onDomain', + * ]; + * + * public function onDomain(DomainResolving $event): void + * { + * if ($event->matches('/^(www\.)?mysite\.com$/')) { + * $event->register(MySiteProvider::class); + * } + * } + * ``` + * + * @package Core\Events */ class DomainResolving { + /** + * The matched provider class, if any. + */ protected ?string $matchedProvider = null; + /** + * Create a new DomainResolving event. + * + * @param string $host The incoming request hostname + */ public function __construct( public readonly string $host ) {} /** - * Check if host matches a domain pattern. + * Check if the incoming host matches a regex pattern. + * + * The host is normalized to lowercase before matching. + * + * @param string $pattern Regex pattern to match against (e.g., '/^example\.com$/') + * @return bool True if the pattern matches the host */ public function matches(string $pattern): bool { @@ -35,7 +79,12 @@ class DomainResolving } /** - * Register as the matching provider. + * Register as the matching provider for this domain. + * + * Only the first provider to register wins. Subsequent registrations + * are ignored. + * + * @param string $providerClass Fully qualified provider class name */ public function register(string $providerClass): void { @@ -43,7 +92,9 @@ class DomainResolving } /** - * Get the matched provider (if any). + * Get the matched provider class name. + * + * @return string|null Provider class name, or null if no match */ public function matchedProvider(): ?string { diff --git a/packages/core-php/src/Core/Events/EventAuditLog.php b/packages/core-php/src/Core/Events/EventAuditLog.php index c2f1a62..cb793f7 100644 --- a/packages/core-php/src/Core/Events/EventAuditLog.php +++ b/packages/core-php/src/Core/Events/EventAuditLog.php @@ -15,16 +15,50 @@ use Illuminate\Support\Facades\Log; /** * Tracks lifecycle event execution for debugging and monitoring. * - * Records when events fire and which handlers respond. This is useful for: - * - Debugging module loading issues - * - Performance monitoring - * - Understanding application bootstrap flow + * EventAuditLog records when lifecycle events fire and which handlers respond, + * including timing information and success/failure status. This is invaluable for: * - * Usage: - * EventAuditLog::enable(); // Enable logging - * EventAuditLog::enableLog(); // Also write to Laravel log - * // ... events fire ... - * $entries = EventAuditLog::entries(); // Get recorded entries + * - **Debugging** - Understanding why modules aren't loading + * - **Performance** - Identifying slow event handlers + * - **Monitoring** - Tracking application bootstrap flow + * - **Diagnostics** - Finding failed handlers in production + * + * ## Enabling Audit Logging + * + * Logging is disabled by default for performance. Enable it when needed: + * + * ```php + * EventAuditLog::enable(); // Enable in-memory logging + * EventAuditLog::enableLog(); // Also write to Laravel log channel + * ``` + * + * ## Retrieving Entries + * + * ```php + * $entries = EventAuditLog::entries(); // All entries + * $failures = EventAuditLog::failures(); // Only failed handlers + * $webEntries = EventAuditLog::entriesFor(WebRoutesRegistering::class); + * $summary = EventAuditLog::summary(); // Statistics + * ``` + * + * ## Entry Structure + * + * Each entry contains: + * - `event` - Event class name + * - `handler` - Handler module class name + * - `duration_ms` - Execution time in milliseconds + * - `failed` - Whether the handler threw an exception + * - `error` - Error message (if failed) + * - `timestamp` - Unix timestamp with microseconds + * + * ## Integration with LazyModuleListener + * + * The `LazyModuleListener` automatically records to EventAuditLog when + * enabled, so you don't need to manually instrument event handlers. + * + * @package Core\Events + * + * @see LazyModuleListener For automatic audit logging integration */ class EventAuditLog { diff --git a/packages/core-php/src/Core/Events/FrameworkBooted.php b/packages/core-php/src/Core/Events/FrameworkBooted.php index db4b737..e9f4e38 100644 --- a/packages/core-php/src/Core/Events/FrameworkBooted.php +++ b/packages/core-php/src/Core/Events/FrameworkBooted.php @@ -11,12 +11,47 @@ declare(strict_types=1); namespace Core\Events; /** - * Fired after the framework has fully booted. + * Fired after all service providers have booted. * - * Use this for late-stage initialisation that needs the full - * application context available. Most modules should use more - * specific events (AdminPanelBooting, ApiRoutesRegistering, etc.) - * rather than this general event. + * This event fires via Laravel's `$app->booted()` callback, after all service + * providers have completed their `boot()` methods. Use this for late-stage + * initialization that requires the full application context. + * + * ## When This Event Fires + * + * Fires after all service providers have booted, regardless of request type + * (web, API, console, queue). This is one of the last events in the bootstrap + * sequence. + * + * ## When to Use This Event + * + * Use FrameworkBooted sparingly. Most modules should prefer context-specific + * events that only fire when relevant: + * + * - **WebRoutesRegistering** - Web routes only + * - **AdminPanelBooting** - Admin requests only + * - **ApiRoutesRegistering** - API requests only + * - **ConsoleBooting** - CLI only + * + * Good use cases for FrameworkBooted: + * - Cross-cutting concerns that apply to all contexts + * - Initialization that depends on other modules being registered + * - Late-binding configuration that needs full container state + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * FrameworkBooted::class => 'onBooted', + * ]; + * + * public function onBooted(FrameworkBooted $event): void + * { + * // Late-stage initialization + * } + * ``` + * + * @package Core\Events */ class FrameworkBooted extends LifecycleEvent { diff --git a/packages/core-php/src/Core/Events/LifecycleEvent.php b/packages/core-php/src/Core/Events/LifecycleEvent.php index b660d73..cdb9769 100644 --- a/packages/core-php/src/Core/Events/LifecycleEvent.php +++ b/packages/core-php/src/Core/Events/LifecycleEvent.php @@ -13,35 +13,86 @@ namespace Core\Events; /** * Base class for lifecycle events. * - * Lifecycle events are fired at key points during application bootstrap. - * Modules listen to these events via static $listens arrays and register - * their resources (routes, views, navigation, etc.) through request methods. + * Lifecycle events are fired at key points during application bootstrap. Modules + * listen to these events via static `$listens` arrays in their Boot class and + * register their resources through the request methods provided here. * - * Core collects all requests and processes them with validation, ensuring - * modules cannot directly mutate infrastructure. + * ## Request/Collect Pattern + * + * This class implements a "request/collect" pattern rather than direct mutation: + * + * 1. **Modules request** resources via methods like `routes()`, `views()`, etc. + * 2. **Requests are collected** in arrays during event dispatch + * 3. **LifecycleEventProvider processes** collected requests with validation + * + * This pattern ensures modules cannot directly mutate infrastructure and allows + * the framework to validate, sort, and process requests centrally. + * + * ## Available Request Methods + * + * | Method | Purpose | + * |--------|---------| + * | `routes()` | Register route files/callbacks | + * | `views()` | Register view namespaces | + * | `livewire()` | Register Livewire components | + * | `middleware()` | Register middleware aliases | + * | `command()` | Register Artisan commands | + * | `translations()` | Register translation namespaces | + * | `bladeComponentPath()` | Register anonymous Blade component paths | + * | `policy()` | Register model policies | + * | `navigation()` | Register navigation items | + * + * ## Usage Example + * + * ```php + * public function onWebRoutes(WebRoutesRegistering $event): void + * { + * $event->views('mymodule', __DIR__.'/Views'); + * $event->livewire('my-component', MyComponent::class); + * $event->routes(fn () => require __DIR__.'/Routes/web.php'); + * } + * ``` + * + * @package Core\Events + * + * @see LifecycleEventProvider For event processing */ abstract class LifecycleEvent { + /** @var array> Collected navigation item requests */ protected array $navigationRequests = []; + /** @var array Collected route registration callbacks */ protected array $routeRequests = []; + /** @var array Collected view namespace requests [namespace, path] */ protected array $viewRequests = []; + /** @var array Collected middleware alias requests [alias, class] */ protected array $middlewareRequests = []; + /** @var array Collected Livewire component requests [alias, class] */ protected array $livewireRequests = []; + /** @var array Collected Artisan command class names */ protected array $commandRequests = []; + /** @var array Collected translation namespace requests [namespace, path] */ protected array $translationRequests = []; + /** @var array Collected Blade component path requests [path, namespace] */ protected array $bladeComponentRequests = []; + /** @var array Collected policy requests [model, policy] */ protected array $policyRequests = []; /** * Request a navigation item be added. + * + * Navigation items are collected and processed by the admin menu system. + * Consider implementing AdminMenuProvider for more control over menu items. + * + * @param array $item Navigation item configuration */ public function navigation(array $item): void { @@ -50,6 +101,19 @@ abstract class LifecycleEvent /** * Request routes be registered. + * + * The callback is invoked within the appropriate middleware group + * (web, admin, api, client) depending on which event fired. + * + * ```php + * $event->routes(fn () => require __DIR__.'/Routes/web.php'); + * // or + * $event->routes(function () { + * Route::get('/example', ExampleController::class); + * }); + * ``` + * + * @param callable $callback Route registration callback */ public function routes(callable $callback): void { @@ -58,6 +122,16 @@ abstract class LifecycleEvent /** * Request a view namespace be registered. + * + * After registration, views can be referenced as `namespace::view.name`. + * + * ```php + * $event->views('commerce', __DIR__.'/Views'); + * // Later: view('commerce::products.index') + * ``` + * + * @param string $namespace The view namespace (e.g., 'commerce') + * @param string $path Absolute path to the views directory */ public function views(string $namespace, string $path): void { @@ -66,6 +140,9 @@ abstract class LifecycleEvent /** * Request a middleware alias be registered. + * + * @param string $alias The middleware alias (e.g., 'commerce.auth') + * @param string $class Fully qualified middleware class name */ public function middleware(string $alias, string $class): void { @@ -74,6 +151,14 @@ abstract class LifecycleEvent /** * Request a Livewire component be registered. + * + * ```php + * $event->livewire('commerce-cart', CartComponent::class); + * // Later: + * ``` + * + * @param string $alias The component alias used in Blade templates + * @param string $class Fully qualified Livewire component class name */ public function livewire(string $alias, string $class): void { @@ -82,6 +167,10 @@ abstract class LifecycleEvent /** * Request an Artisan command be registered. + * + * Only processed during ConsoleBooting event. + * + * @param string $class Fully qualified command class name */ public function command(string $class): void { @@ -90,6 +179,16 @@ abstract class LifecycleEvent /** * Request translations be loaded for a namespace. + * + * After registration, translations can be accessed as `namespace::key`. + * + * ```php + * $event->translations('commerce', __DIR__.'/Lang'); + * // Later: __('commerce::products.title') + * ``` + * + * @param string $namespace The translation namespace + * @param string $path Absolute path to the lang directory */ public function translations(string $namespace, string $path): void { @@ -98,6 +197,11 @@ abstract class LifecycleEvent /** * Request an anonymous Blade component path be registered. + * + * Anonymous components in this path can be used in templates. + * + * @param string $path Absolute path to the components directory + * @param string|null $namespace Optional prefix for component names */ public function bladeComponentPath(string $path, ?string $namespace = null): void { @@ -106,6 +210,9 @@ abstract class LifecycleEvent /** * Request a policy be registered for a model. + * + * @param string $model Fully qualified model class name + * @param string $policy Fully qualified policy class name */ public function policy(string $model, string $policy): void { @@ -114,6 +221,10 @@ abstract class LifecycleEvent /** * Get all navigation requests for processing. + * + * @return array> + * + * @internal Used by LifecycleEventProvider */ public function navigationRequests(): array { @@ -122,6 +233,10 @@ abstract class LifecycleEvent /** * Get all route requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function routeRequests(): array { @@ -130,6 +245,10 @@ abstract class LifecycleEvent /** * Get all view namespace requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function viewRequests(): array { @@ -138,6 +257,10 @@ abstract class LifecycleEvent /** * Get all middleware alias requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function middlewareRequests(): array { @@ -146,6 +269,10 @@ abstract class LifecycleEvent /** * Get all Livewire component requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function livewireRequests(): array { @@ -154,6 +281,10 @@ abstract class LifecycleEvent /** * Get all command requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function commandRequests(): array { @@ -162,6 +293,10 @@ abstract class LifecycleEvent /** * Get all translation requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function translationRequests(): array { @@ -170,6 +305,10 @@ abstract class LifecycleEvent /** * Get all Blade component path requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function bladeComponentRequests(): array { @@ -178,6 +317,10 @@ abstract class LifecycleEvent /** * Get all policy requests for processing. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function policyRequests(): array { diff --git a/packages/core-php/src/Core/Events/MailSending.php b/packages/core-php/src/Core/Events/MailSending.php index 31ef8a3..aa8950c 100644 --- a/packages/core-php/src/Core/Events/MailSending.php +++ b/packages/core-php/src/Core/Events/MailSending.php @@ -13,18 +13,40 @@ namespace Core\Events; /** * Fired when mail functionality is needed. * - * Modules listen to this event to register mail templates, - * custom mailers, or mail-related services. + * Modules listen to this event to register mail templates, custom mailers, + * or mail-related services. This enables lazy loading of mail dependencies + * until email actually needs to be sent. * - * Allows lazy loading of mail dependencies until email - * actually needs to be sent. + * ## When This Event Fires + * + * Fired when the mail system initializes, typically just before sending + * the first email in a request. + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * MailSending::class => 'onMail', + * ]; + * + * public function onMail(MailSending $event): void + * { + * $event->mailable(OrderConfirmationMail::class); + * $event->mailable(WelcomeEmail::class); + * } + * ``` + * + * @package Core\Events */ class MailSending extends LifecycleEvent { + /** @var array Collected mailable class names */ protected array $mailableRequests = []; /** * Register a mailable class. + * + * @param string $class Fully qualified mailable class name */ public function mailable(string $class): void { @@ -32,7 +54,11 @@ class MailSending extends LifecycleEvent } /** - * Get all registered mailable classes. + * Get all registered mailable class names. + * + * @return array + * + * @internal Used by mail system */ public function mailableRequests(): array { diff --git a/packages/core-php/src/Core/Events/McpToolsRegistering.php b/packages/core-php/src/Core/Events/McpToolsRegistering.php index b1f76a4..a6255e9 100644 --- a/packages/core-php/src/Core/Events/McpToolsRegistering.php +++ b/packages/core-php/src/Core/Events/McpToolsRegistering.php @@ -13,20 +13,51 @@ namespace Core\Events; use Core\Front\Mcp\Contracts\McpToolHandler; /** - * Fired when MCP tools are being registered. + * Fired when MCP (Model Context Protocol) tools are being registered. * - * Modules listen to this event to register their MCP tool handlers. - * Each handler class must implement McpToolHandler interface. + * Modules listen to this event to register their MCP tool handlers, which + * expose functionality to AI assistants and LLM-powered applications. * - * Fired at MCP server startup (stdio transport) or when MCP routes - * are accessed (HTTP transport). + * ## When This Event Fires + * + * Fired by `LifecycleEventProvider::fireMcpTools()` when: + * - MCP server starts up (stdio transport for CLI usage) + * - MCP routes are accessed (HTTP transport for web-based integration) + * + * ## Handler Requirements + * + * Each handler class must implement `McpToolHandler` interface. Handlers + * define the tools, their input schemas, and execution logic. + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * McpToolsRegistering::class => 'onMcp', + * ]; + * + * public function onMcp(McpToolsRegistering $event): void + * { + * $event->handler(ProductSearchHandler::class); + * $event->handler(InventoryQueryHandler::class); + * } + * ``` + * + * @package Core\Events + * + * @see \Core\Front\Mcp\Contracts\McpToolHandler */ class McpToolsRegistering extends LifecycleEvent { + /** @var array Collected MCP tool handler class names */ protected array $handlers = []; /** * Register an MCP tool handler class. + * + * @param string $handlerClass Fully qualified class name implementing McpToolHandler + * + * @throws \InvalidArgumentException If class doesn't implement McpToolHandler */ public function handler(string $handlerClass): void { @@ -37,7 +68,11 @@ class McpToolsRegistering extends LifecycleEvent } /** - * Get all registered handler classes. + * Get all registered handler class names. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function handlers(): array { diff --git a/packages/core-php/src/Core/Events/MediaRequested.php b/packages/core-php/src/Core/Events/MediaRequested.php index db4110c..e3e8f79 100644 --- a/packages/core-php/src/Core/Events/MediaRequested.php +++ b/packages/core-php/src/Core/Events/MediaRequested.php @@ -15,15 +15,47 @@ namespace Core\Events; * * Modules listen to this event to provide media handling capabilities * such as image processing, video transcoding, CDN integration, etc. + * This enables lazy loading of heavy media processing dependencies. * - * Allows lazy loading of heavy media processing dependencies. + * ## When This Event Fires + * + * Fired when the media system initializes, typically when media + * upload or processing is triggered. + * + * ## Processor Types + * + * Register processors by type to handle different media formats: + * - `image` - Image processing (resize, crop, optimize) + * - `video` - Video transcoding and thumbnail generation + * - `audio` - Audio processing and format conversion + * - `document` - Document preview and text extraction + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * MediaRequested::class => 'onMedia', + * ]; + * + * public function onMedia(MediaRequested $event): void + * { + * $event->processor('image', ImageProcessor::class); + * $event->processor('video', VideoProcessor::class); + * } + * ``` + * + * @package Core\Events */ class MediaRequested extends LifecycleEvent { + /** @var array Collected processor registrations [type => class] */ protected array $processorRequests = []; /** - * Register a media processor. + * Register a media processor for a specific type. + * + * @param string $type Media type (e.g., 'image', 'video', 'audio') + * @param string $class Fully qualified processor class name */ public function processor(string $type, string $class): void { @@ -32,6 +64,10 @@ class MediaRequested extends LifecycleEvent /** * Get all registered processors. + * + * @return array [type => class] + * + * @internal Used by media system */ public function processorRequests(): array { diff --git a/packages/core-php/src/Core/Events/QueueWorkerBooting.php b/packages/core-php/src/Core/Events/QueueWorkerBooting.php index 24133ff..66bf3b3 100644 --- a/packages/core-php/src/Core/Events/QueueWorkerBooting.php +++ b/packages/core-php/src/Core/Events/QueueWorkerBooting.php @@ -13,17 +13,47 @@ namespace Core\Events; /** * Fired when a queue worker is starting up. * - * Modules listen to this event to register job classes or - * perform queue-specific initialisation. + * Modules listen to this event to perform queue-specific initialization or + * register job classes that need explicit registration. * - * Only fired in queue worker context, not web requests. + * ## When This Event Fires + * + * Fired by `LifecycleEventProvider::fireQueueWorkerBooting()` when the + * application detects it's running in queue worker context (i.e., when + * `queue.worker` is bound in the container). + * + * Not fired during web requests, API calls, or console commands. + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * QueueWorkerBooting::class => 'onQueueWorker', + * ]; + * + * public function onQueueWorker(QueueWorkerBooting $event): void + * { + * $event->job(ProcessOrderJob::class); + * $event->job(SendNotificationJob::class); + * } + * ``` + * + * Note: Most Laravel jobs don't need explicit registration. This event + * is primarily for queue-specific initialization or custom job handling. + * + * @package Core\Events + * + * @see ConsoleBooting For CLI-specific initialization */ class QueueWorkerBooting extends LifecycleEvent { + /** @var array Collected job class names */ protected array $jobRequests = []; /** * Register a job class. + * + * @param string $class Fully qualified job class name */ public function job(string $class): void { @@ -31,7 +61,11 @@ class QueueWorkerBooting extends LifecycleEvent } /** - * Get all registered job classes. + * Get all registered job class names. + * + * @return array + * + * @internal Used by LifecycleEventProvider */ public function jobRequests(): array { diff --git a/packages/core-php/src/Core/Events/SearchRequested.php b/packages/core-php/src/Core/Events/SearchRequested.php index f943364..d97e108 100644 --- a/packages/core-php/src/Core/Events/SearchRequested.php +++ b/packages/core-php/src/Core/Events/SearchRequested.php @@ -13,17 +13,40 @@ namespace Core\Events; /** * Fired when search functionality is requested. * - * Modules listen to this event to register searchable models - * or search providers. + * Modules listen to this event to register searchable models or search + * providers. This enables lazy loading of search indexing dependencies + * until search is actually needed. * - * Allows lazy loading of search indexing dependencies. + * ## When This Event Fires + * + * Fired when the search system initializes, typically when a search + * query is performed or search indexing is triggered. + * + * ## Usage Example + * + * ```php + * public static array $listens = [ + * SearchRequested::class => 'onSearch', + * ]; + * + * public function onSearch(SearchRequested $event): void + * { + * $event->searchable(Product::class); + * $event->searchable(Article::class); + * } + * ``` + * + * @package Core\Events */ class SearchRequested extends LifecycleEvent { + /** @var array Collected searchable model class names */ protected array $searchableRequests = []; /** * Register a searchable model. + * + * @param string $model Fully qualified model class name */ public function searchable(string $model): void { @@ -31,7 +54,11 @@ class SearchRequested extends LifecycleEvent } /** - * Get all registered searchable models. + * Get all registered searchable model class names. + * + * @return array + * + * @internal Used by search system */ public function searchableRequests(): array { diff --git a/packages/core-php/src/Core/Events/WebRoutesRegistering.php b/packages/core-php/src/Core/Events/WebRoutesRegistering.php index 6c7adf3..d6a7086 100644 --- a/packages/core-php/src/Core/Events/WebRoutesRegistering.php +++ b/packages/core-php/src/Core/Events/WebRoutesRegistering.php @@ -11,13 +11,46 @@ declare(strict_types=1); namespace Core\Events; /** - * Fired when web routes are being registered. + * Fired when public web routes are being registered. * - * Modules listen to this event to register public-facing web routes. - * Routes are automatically wrapped with the 'web' middleware group. + * Modules listen to this event to register public-facing web routes such as + * marketing pages, product listings, or any routes accessible without authentication. * - * Use this for marketing pages, public product pages, etc. - * For authenticated dashboard routes, use AdminPanelBooting instead. + * ## When This Event Fires + * + * Fired by `LifecycleEventProvider::fireWebRoutes()` when the web frontage + * initializes, typically early in the request lifecycle for web requests. + * + * ## Middleware + * + * Routes registered through this event are automatically wrapped with the 'web' + * middleware group, which typically includes session handling, CSRF protection, etc. + * + * ## Usage Example + * + * ```php + * // In your module's Boot class: + * public static array $listens = [ + * WebRoutesRegistering::class => 'onWebRoutes', + * ]; + * + * public function onWebRoutes(WebRoutesRegistering $event): void + * { + * $event->views('marketing', __DIR__.'/Views'); + * $event->routes(fn () => require __DIR__.'/Routes/web.php'); + * } + * ``` + * + * ## When to Use Other Events + * + * - **AdminPanelBooting** - For admin dashboard routes + * - **ClientRoutesRegistering** - For authenticated customer/namespace routes + * - **ApiRoutesRegistering** - For REST API endpoints + * + * @package Core\Events + * + * @see AdminPanelBooting For admin routes + * @see ClientRoutesRegistering For client dashboard routes */ class WebRoutesRegistering extends LifecycleEvent { diff --git a/packages/core-php/src/Core/Front/Admin/AdminMenuRegistry.php b/packages/core-php/src/Core/Front/Admin/AdminMenuRegistry.php index 1c83689..fa0987a 100644 --- a/packages/core-php/src/Core/Front/Admin/AdminMenuRegistry.php +++ b/packages/core-php/src/Core/Front/Admin/AdminMenuRegistry.php @@ -12,9 +12,6 @@ namespace Core\Front\Admin; use Core\Front\Admin\Contracts\AdminMenuProvider; use Core\Front\Admin\Contracts\DynamicMenuProvider; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Services\EntitlementService; use Illuminate\Support\Facades\Cache; /** @@ -92,9 +89,19 @@ class AdminMenuRegistry */ protected int $cacheTtl; - public function __construct( - protected EntitlementService $entitlements, - ) { + /** + * EntitlementService instance (Core\Mod\Tenant\Services\EntitlementService when available). + */ + protected ?object $entitlements = null; + + public function __construct(?object $entitlements = null) + { + if ($entitlements === null && class_exists(\Core\Mod\Tenant\Services\EntitlementService::class)) { + $this->entitlements = app(\Core\Mod\Tenant\Services\EntitlementService::class); + } else { + $this->entitlements = $entitlements; + } + $this->cacheTtl = (int) config('core.admin_menu.cache_ttl', self::DEFAULT_CACHE_TTL); $this->cachingEnabled = (bool) config('core.admin_menu.cache_enabled', true); } @@ -139,12 +146,12 @@ class AdminMenuRegistry /** * Build the complete menu structure. * - * @param Workspace|null $workspace Current workspace for entitlement checks + * @param object|null $workspace Current workspace for entitlement checks (Workspace model instance) * @param bool $isAdmin Whether user is admin (Hades) - * @param User|null $user The authenticated user for permission checks + * @param object|null $user The authenticated user for permission checks (User model instance) * @return array */ - public function build(?Workspace $workspace, bool $isAdmin = false, ?User $user = null): array + public function build(?object $workspace, bool $isAdmin = false, ?object $user = null): array { // Get static items (potentially cached) $staticItems = $this->getStaticItems($workspace, $isAdmin, $user); @@ -162,9 +169,12 @@ class AdminMenuRegistry /** * Get static menu items, using cache if enabled. * + * @param object|null $workspace Workspace model instance + * @param bool $isAdmin + * @param object|null $user User model instance * @return array> */ - protected function getStaticItems(?Workspace $workspace, bool $isAdmin, ?User $user): array + protected function getStaticItems(?object $workspace, bool $isAdmin, ?object $user): array { if (! $this->cachingEnabled) { return $this->collectItems($workspace, $isAdmin, $user); @@ -180,9 +190,12 @@ class AdminMenuRegistry /** * Get dynamic menu items from dynamic providers. * + * @param object|null $workspace Workspace model instance + * @param bool $isAdmin + * @param object|null $user User model instance * @return array> */ - protected function getDynamicItems(?Workspace $workspace, bool $isAdmin, ?User $user): array + protected function getDynamicItems(?object $workspace, bool $isAdmin, ?object $user): array { $grouped = []; @@ -201,7 +214,7 @@ class AdminMenuRegistry } // Skip if entitlement check fails - if ($entitlement && $workspace) { + if ($entitlement && $workspace && $this->entitlements !== null) { if ($this->entitlements->can($workspace, $entitlement)->isDenied()) { continue; } @@ -249,8 +262,13 @@ class AdminMenuRegistry /** * Build the final menu structure from collected items. + * + * @param array $allItems + * @param object|null $workspace Workspace model instance + * @param bool $isAdmin + * @return array */ - protected function buildMenuStructure(array $allItems, ?Workspace $workspace, bool $isAdmin): array + protected function buildMenuStructure(array $allItems, ?object $workspace, bool $isAdmin): array { // Build flat structure with dividers $menu = []; @@ -343,8 +361,13 @@ class AdminMenuRegistry /** * Build the cache key for menu items. + * + * @param object|null $workspace Workspace model instance + * @param bool $isAdmin + * @param object|null $user User model instance + * @return string */ - protected function buildCacheKey(?Workspace $workspace, bool $isAdmin, ?User $user): string + protected function buildCacheKey(?object $workspace, bool $isAdmin, ?object $user): string { $parts = [ self::CACHE_PREFIX, @@ -367,9 +390,12 @@ class AdminMenuRegistry /** * Collect items from all providers, filtering by entitlements and permissions. * + * @param object|null $workspace Workspace model instance + * @param bool $isAdmin + * @param object|null $user User model instance * @return array> */ - protected function collectItems(?Workspace $workspace, bool $isAdmin, ?User $user): array + protected function collectItems(?object $workspace, bool $isAdmin, ?object $user): array { $grouped = []; @@ -391,7 +417,7 @@ class AdminMenuRegistry } // Skip if entitlement check fails - if ($entitlement && $workspace) { + if ($entitlement && $workspace && $this->entitlements !== null) { if ($this->entitlements->can($workspace, $entitlement)->isDenied()) { continue; } @@ -420,12 +446,12 @@ class AdminMenuRegistry /** * Check if a user has all required permissions. * - * @param User|null $user + * @param object|null $user User model instance * @param array $permissions - * @param Workspace|null $workspace + * @param object|null $workspace Workspace model instance * @return bool */ - protected function checkPermissions(?User $user, array $permissions, ?Workspace $workspace): bool + protected function checkPermissions(?object $user, array $permissions, ?object $workspace): bool { if (empty($permissions)) { return true; @@ -448,10 +474,10 @@ class AdminMenuRegistry /** * Invalidate cached menu for a specific context. * - * @param Workspace|null $workspace - * @param User|null $user + * @param object|null $workspace Workspace model instance + * @param object|null $user User model instance */ - public function invalidateCache(?Workspace $workspace = null, ?User $user = null): void + public function invalidateCache(?object $workspace = null, ?object $user = null): void { if ($workspace !== null && $user !== null) { // Invalidate specific cache keys @@ -469,8 +495,10 @@ class AdminMenuRegistry /** * Invalidate all cached menus for a workspace. + * + * @param object $workspace Workspace model instance */ - public function invalidateWorkspaceCache(Workspace $workspace): void + public function invalidateWorkspaceCache(object $workspace): void { // We can't easily clear pattern-based cache keys with all drivers, // so we rely on TTL expiration for non-tagged caches @@ -481,8 +509,10 @@ class AdminMenuRegistry /** * Invalidate all cached menus for a user. + * + * @param object $user User model instance */ - public function invalidateUserCache(User $user): void + public function invalidateUserCache(object $user): void { if (method_exists(Cache::getStore(), 'tags')) { Cache::tags([self::CACHE_PREFIX, 'user:' . $user->id])->flush(); @@ -512,12 +542,12 @@ class AdminMenuRegistry /** * Get all service menu items indexed by service key. * - * @param Workspace|null $workspace Current workspace for entitlement checks + * @param object|null $workspace Current workspace for entitlement checks (Workspace model instance) * @param bool $isAdmin Whether user is admin (Hades) - * @param User|null $user The authenticated user for permission checks + * @param object|null $user The authenticated user for permission checks (User model instance) * @return array Service items indexed by service key */ - public function getAllServiceItems(?Workspace $workspace, bool $isAdmin = false, ?User $user = null): array + public function getAllServiceItems(?object $workspace, bool $isAdmin = false, ?object $user = null): array { $services = []; @@ -547,7 +577,7 @@ class AdminMenuRegistry } // Skip if entitlement check fails - if ($entitlement && $workspace) { + if ($entitlement && $workspace && $this->entitlements !== null) { if ($this->entitlements->can($workspace, $entitlement)->isDenied()) { continue; } @@ -578,12 +608,12 @@ class AdminMenuRegistry * Get a specific service's menu item including its children (tabs). * * @param string $serviceKey The service identifier (e.g., 'commerce', 'support') - * @param Workspace|null $workspace Current workspace for entitlement checks + * @param object|null $workspace Current workspace for entitlement checks (Workspace model instance) * @param bool $isAdmin Whether user is admin (Hades) - * @param User|null $user The authenticated user for permission checks + * @param object|null $user The authenticated user for permission checks (User model instance) * @return array|null The service menu item with children, or null if not found */ - public function getServiceItem(string $serviceKey, ?Workspace $workspace, bool $isAdmin = false, ?User $user = null): ?array + public function getServiceItem(string $serviceKey, ?object $workspace, bool $isAdmin = false, ?object $user = null): ?array { foreach ($this->providers as $provider) { // Check provider-level permissions @@ -611,7 +641,7 @@ class AdminMenuRegistry } // Skip if entitlement check fails - if ($entitlement && $workspace) { + if ($entitlement && $workspace && $this->entitlements !== null) { if ($this->entitlements->can($workspace, $entitlement)->isDenied()) { continue; } diff --git a/packages/core-php/src/Core/Front/Admin/Concerns/HasMenuPermissions.php b/packages/core-php/src/Core/Front/Admin/Concerns/HasMenuPermissions.php index ac9074f..27e9068 100644 --- a/packages/core-php/src/Core/Front/Admin/Concerns/HasMenuPermissions.php +++ b/packages/core-php/src/Core/Front/Admin/Concerns/HasMenuPermissions.php @@ -10,9 +10,6 @@ declare(strict_types=1); namespace Core\Front\Admin\Concerns; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\Workspace; - /** * Provides default permission handling for AdminMenuProvider implementations. * @@ -40,11 +37,11 @@ trait HasMenuPermissions * By default, checks that the user has all permissions returned by * menuPermissions(). Override for custom logic. * - * @param User|null $user The authenticated user - * @param Workspace|null $workspace The current workspace context + * @param object|null $user The authenticated user (User model instance) + * @param object|null $workspace The current workspace context (Workspace model instance) * @return bool */ - public function canViewMenu(?User $user, ?Workspace $workspace): bool + public function canViewMenu(?object $user, ?object $workspace): bool { // No user means no permission (unless we have no requirements) $permissions = $this->menuPermissions(); @@ -73,12 +70,12 @@ trait HasMenuPermissions * Override this method to customise how permission checks are performed. * By default, uses Laravel's Gate/Authorization system. * - * @param User $user + * @param object $user User model instance * @param string $permission - * @param Workspace|null $workspace + * @param object|null $workspace Workspace model instance * @return bool */ - protected function userHasPermission(User $user, string $permission, ?Workspace $workspace): bool + protected function userHasPermission(object $user, string $permission, ?object $workspace): bool { // Check using Laravel's authorization if (method_exists($user, 'can')) { diff --git a/packages/core-php/src/Core/Front/Admin/Contracts/AdminMenuProvider.php b/packages/core-php/src/Core/Front/Admin/Contracts/AdminMenuProvider.php index c1d87f7..763b9ed 100644 --- a/packages/core-php/src/Core/Front/Admin/Contracts/AdminMenuProvider.php +++ b/packages/core-php/src/Core/Front/Admin/Contracts/AdminMenuProvider.php @@ -10,14 +10,39 @@ declare(strict_types=1); namespace Core\Front\Admin\Contracts; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\Workspace; - /** * Interface for modules that provide admin menu items. * - * Modules implement this interface and register themselves with AdminMenuRegistry - * during boot. The registry collects all items and builds the final menu structure. + * Modules implement this interface to contribute navigation items to the admin + * panel sidebar. The `AdminMenuRegistry` collects items from all registered + * providers and builds the final menu structure with proper ordering, grouping, + * and permission filtering. + * + * ## Menu Item Structure + * + * Each item returned by `adminMenuItems()` specifies: + * + * - **group** - Where in the menu hierarchy (`dashboard`, `webhost`, `services`, `settings`, `admin`) + * - **priority** - Order within group (lower = earlier) + * - **entitlement** - Optional feature code for workspace-level access + * - **permissions** - Optional array of required user permissions + * - **admin** - Whether item requires Hades/admin user + * - **item** - Closure returning the actual menu item data (lazy-evaluated) + * + * ## Lazy Evaluation + * + * The `item` closure is only called when the menu is rendered, after permission + * checks pass. This avoids unnecessary work for filtered items and allows + * route-dependent data (like `active` state) to be computed at render time. + * + * ## Registration + * + * Providers are typically registered via `AdminMenuRegistry::register()` during + * the AdminPanelBooting event or in a service provider's boot method. + * + * @package Core\Front\Admin\Contracts + * + * @see DynamicMenuProvider For uncached, real-time menu items */ interface AdminMenuProvider { @@ -81,9 +106,9 @@ interface AdminMenuProvider * Override this method to implement custom permission logic beyond * simple permission key checks. * - * @param User|null $user The authenticated user - * @param Workspace|null $workspace The current workspace context + * @param object|null $user The authenticated user (User model instance) + * @param object|null $workspace The current workspace context (Workspace model instance) * @return bool */ - public function canViewMenu(?User $user, ?Workspace $workspace): bool; + public function canViewMenu(?object $user, ?object $workspace): bool; } diff --git a/packages/core-php/src/Core/Front/Admin/Contracts/DynamicMenuProvider.php b/packages/core-php/src/Core/Front/Admin/Contracts/DynamicMenuProvider.php index 364d368..a17f246 100644 --- a/packages/core-php/src/Core/Front/Admin/Contracts/DynamicMenuProvider.php +++ b/packages/core-php/src/Core/Front/Admin/Contracts/DynamicMenuProvider.php @@ -10,19 +10,38 @@ declare(strict_types=1); namespace Core\Front\Admin\Contracts; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\Workspace; - /** - * Interface for providers that supply dynamic menu items. + * Interface for providers that supply dynamic (uncached) menu items. * - * Dynamic menu items are computed at runtime based on context (user, workspace, - * database state, etc.) and are never cached. Use this interface when menu items - * need to reflect real-time data such as notification counts, recent items, or - * user-specific content. + * Dynamic menu items are computed at runtime based on context and are never + * cached. Use this interface when menu items need to reflect real-time data + * that changes frequently or per-request. * - * Classes implementing this interface are processed separately from static - * AdminMenuProvider items - their results are merged after cache retrieval. + * ## When to Use DynamicMenuProvider + * + * - **Notification counts** - Unread messages, pending approvals + * - **Recent items** - Recently accessed documents, pages + * - **User-specific content** - Personalized shortcuts, favorites + * - **Real-time status** - Online users, active sessions + * + * ## Performance Considerations + * + * Dynamic items are computed on every request, so keep the `dynamicMenuItems()` + * method efficient: + * + * - Use eager loading for database queries + * - Cache intermediate results if possible + * - Limit the number of items returned + * + * ## Cache Integration + * + * Static menu items from `AdminMenuProvider` are cached. Dynamic items are + * merged in after cache retrieval. The `dynamicCacheKey()` method can be used + * to invalidate the static cache when dynamic state changes significantly. + * + * @package Core\Front\Admin\Contracts + * + * @see AdminMenuProvider For static (cached) menu items */ interface DynamicMenuProvider { @@ -35,8 +54,8 @@ interface DynamicMenuProvider * Each item should include the same structure as AdminMenuProvider::adminMenuItems() * plus an optional 'dynamic' key set to true for identification. * - * @param User|null $user The authenticated user - * @param Workspace|null $workspace The current workspace context + * @param object|null $user The authenticated user (User model instance) + * @param object|null $workspace The current workspace context (Workspace model instance) * @param bool $isAdmin Whether the user is an admin * @return array */ - public function dynamicMenuItems(?User $user, ?Workspace $workspace, bool $isAdmin): array; + public function dynamicMenuItems(?object $user, ?object $workspace, bool $isAdmin): array; /** * Get the cache key modifier for dynamic items. @@ -57,9 +76,9 @@ interface DynamicMenuProvider * this key changes. Return null if dynamic items should never affect * cache invalidation. * - * @param User|null $user - * @param Workspace|null $workspace + * @param object|null $user User model instance + * @param object|null $workspace Workspace model instance * @return string|null */ - public function dynamicCacheKey(?User $user, ?Workspace $workspace): ?string; + public function dynamicCacheKey(?object $user, ?object $workspace): ?string; } diff --git a/packages/core-php/src/Core/Front/Admin/View/Components/Sidemenu.php b/packages/core-php/src/Core/Front/Admin/View/Components/Sidemenu.php index d0b0465..4a332ff 100644 --- a/packages/core-php/src/Core/Front/Admin/View/Components/Sidemenu.php +++ b/packages/core-php/src/Core/Front/Admin/View/Components/Sidemenu.php @@ -13,8 +13,6 @@ namespace Core\Front\Admin\View\Components; use Core\Front\Admin\AdminMenuRegistry; use Illuminate\Support\Facades\Auth; use Illuminate\View\Component; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Services\WorkspaceService; class Sidemenu extends Component { @@ -37,9 +35,17 @@ class Sidemenu extends Component protected function buildFromRegistry(): array { $user = Auth::user(); + // Use current workspace from session, not default - $workspace = app(WorkspaceService::class)->currentModel(); - $isAdmin = $user instanceof User && $user->isHades(); + $workspace = null; + if (class_exists(\Core\Mod\Tenant\Services\WorkspaceService::class)) { + $workspace = app(\Core\Mod\Tenant\Services\WorkspaceService::class)->currentModel(); + } + + $isAdmin = false; + if (class_exists(\Core\Mod\Tenant\Models\User::class) && $user instanceof \Core\Mod\Tenant\Models\User) { + $isAdmin = $user->isHades(); + } return app(AdminMenuRegistry::class)->build($workspace, $isAdmin); } diff --git a/packages/core-php/src/Core/Front/Mcp/McpContext.php b/packages/core-php/src/Core/Front/Mcp/McpContext.php index 7a9464e..4439151 100644 --- a/packages/core-php/src/Core/Front/Mcp/McpContext.php +++ b/packages/core-php/src/Core/Front/Mcp/McpContext.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Core\Front\Mcp; use Closure; -use Core\Mod\Agentic\Models\AgentPlan; /** * Context object passed to MCP tool handlers. @@ -27,9 +26,12 @@ use Core\Mod\Agentic\Models\AgentPlan; */ class McpContext { + /** + * @param object|null $currentPlan AgentPlan model instance when Agentic module installed + */ public function __construct( private ?string $sessionId = null, - private ?AgentPlan $currentPlan = null, + private ?object $currentPlan = null, private ?Closure $notificationCallback = null, private ?Closure $logCallback = null, ) {} @@ -52,16 +54,20 @@ class McpContext /** * Get the current plan if one is active. + * + * @return object|null AgentPlan model instance when Agentic module installed */ - public function getCurrentPlan(): ?AgentPlan + public function getCurrentPlan(): ?object { return $this->currentPlan; } /** * Set the current plan. + * + * @param object|null $plan AgentPlan model instance */ - public function setCurrentPlan(?AgentPlan $plan): void + public function setCurrentPlan(?object $plan): void { $this->currentPlan = $plan; } diff --git a/packages/core-php/src/Core/Front/Web/Middleware/FindDomainRecord.php b/packages/core-php/src/Core/Front/Web/Middleware/FindDomainRecord.php index 06c0f08..e6ec223 100644 --- a/packages/core-php/src/Core/Front/Web/Middleware/FindDomainRecord.php +++ b/packages/core-php/src/Core/Front/Web/Middleware/FindDomainRecord.php @@ -12,7 +12,6 @@ namespace Core\Front\Web\Middleware; use Closure; use Illuminate\Http\Request; -use Core\Mod\Tenant\Models\Workspace; use Symfony\Component\HttpFoundation\Response; /** @@ -76,11 +75,20 @@ class FindDomainRecord /** * Resolve workspace from the domain. + * + * Requires Core\Mod\Tenant module to be installed. + * + * @return object|null Workspace model instance or null */ - protected function resolveWorkspaceFromDomain(string $host): ?Workspace + protected function resolveWorkspaceFromDomain(string $host): ?object { + // Check if Tenant module is installed + if (! class_exists(\Core\Mod\Tenant\Models\Workspace::class)) { + return null; + } + // Check for custom domain first - $workspace = Workspace::where('domain', $host)->first(); + $workspace = \Core\Mod\Tenant\Models\Workspace::where('domain', $host)->first(); if ($workspace) { return $workspace; } @@ -95,7 +103,7 @@ class FindDomainRecord if (count($parts) >= 1) { $workspaceSlug = $parts[0]; - return Workspace::where('slug', $workspaceSlug) + return \Core\Mod\Tenant\Models\Workspace::where('slug', $workspaceSlug) ->where('is_active', true) ->first(); } diff --git a/packages/core-php/src/Core/Helpers/ServiceCollection.php b/packages/core-php/src/Core/Helpers/ServiceCollection.php index 0cf1cac..d59de0d 100644 --- a/packages/core-php/src/Core/Helpers/ServiceCollection.php +++ b/packages/core-php/src/Core/Helpers/ServiceCollection.php @@ -10,7 +10,6 @@ declare(strict_types=1); namespace Core\Helpers; -use Core\Mod\Social\Enums\ServiceGroup; use Illuminate\Support\Arr; /** @@ -33,10 +32,12 @@ class ServiceCollection /** * Filter services by group (social, AI, media, miscellaneous). * - * @param ServiceGroup|array|null $group Service group(s) to filter by + * Requires Core\Mod\Social module to be installed for ServiceGroup enum. + * + * @param object|array|null $group Service group(s) to filter by (ServiceGroup enum) * @return static New collection containing only services in the specified group(s) */ - public function group(ServiceGroup|array|null $group = null): static + public function group(object|array|null $group = null): static { return new static( array_values( @@ -75,7 +76,7 @@ class ServiceCollection * - group: The service group enum (social, AI, media, miscellaneous) * - form: The form configuration array for the service * - * @return array + * @return array */ public function getCollection(): array { diff --git a/packages/core-php/src/Core/LazyModuleListener.php b/packages/core-php/src/Core/LazyModuleListener.php index 61433c6..9f7f027 100644 --- a/packages/core-php/src/Core/LazyModuleListener.php +++ b/packages/core-php/src/Core/LazyModuleListener.php @@ -14,24 +14,69 @@ use Core\Events\EventAuditLog; use Illuminate\Support\ServiceProvider; /** - * Wraps a module method as an event listener. + * Wraps a module method as a lazy-loading event listener. * - * The module is only instantiated when the event fires, - * enabling lazy loading of modules based on actual usage. + * LazyModuleListener is the key to the framework's lazy loading strategy. Instead of + * instantiating all modules at boot time, modules are only created when their + * registered events actually fire. This significantly reduces memory usage and + * speeds up application bootstrap for requests that don't use all modules. * - * Handles both plain classes and ServiceProviders correctly. - * Integrates with EventAuditLog for debugging and monitoring. + * ## How Lazy Loading Works * - * Usage: - * Event::listen( - * AdminPanelBooting::class, - * new LazyModuleListener(Commerce\Boot::class, 'registerAdmin') - * ); + * 1. During registration, a LazyModuleListener wraps each module class name and method + * 2. The listener is registered with Laravel's event system + * 3. When an event fires, `__invoke()` is called + * 4. The module is instantiated via Laravel's container (first time only) + * 5. The specified method is called with the event object + * + * ## ServiceProvider Support + * + * If the module class extends `ServiceProvider`, it's instantiated using + * `$app->resolveProvider()` to ensure proper `$app` injection. Plain classes + * use standard container resolution via `$app->make()`. + * + * ## Instance Caching + * + * Once instantiated, the module instance is cached for the lifetime of the + * LazyModuleListener. This means if the same module listens to multiple events, + * it will be instantiated once per event type. + * + * ## Audit Logging + * + * All event handling is tracked via EventAuditLog when enabled. This records: + * - Event class name + * - Handler module class name + * - Execution duration + * - Success/failure status + * + * ## Usage Example + * + * ```php + * // Typically used by ModuleRegistry, but can be used directly: + * Event::listen( + * AdminPanelBooting::class, + * new LazyModuleListener(Commerce\Boot::class, 'registerAdmin') + * ); + * ``` + * + * @package Core + * + * @see ModuleRegistry For the automatic registration system + * @see EventAuditLog For execution monitoring */ class LazyModuleListener { + /** + * Cached module instance (created on first event). + */ private ?object $instance = null; + /** + * Create a new lazy module listener. + * + * @param string $moduleClass Fully qualified class name of the module Boot class + * @param string $method Method name to call when the event fires + */ public function __construct( private string $moduleClass, private string $method @@ -40,8 +85,15 @@ class LazyModuleListener /** * Handle the event by instantiating the module and calling its method. * - * This is the callable interface for Laravel's event dispatcher. - * Records execution to EventAuditLog when enabled. + * This is the callable interface for Laravel's event dispatcher. The module + * is instantiated on first call and cached for subsequent events. + * + * Records execution timing and success/failure to EventAuditLog when enabled. + * Any exceptions thrown by the handler are re-thrown after logging. + * + * @param object $event The lifecycle event instance + * + * @throws \Throwable Re-throws any exception from the module handler */ public function __invoke(object $event): void { diff --git a/packages/core-php/src/Core/LifecycleEventProvider.php b/packages/core-php/src/Core/LifecycleEventProvider.php index bd903d4..7d36f39 100644 --- a/packages/core-php/src/Core/LifecycleEventProvider.php +++ b/packages/core-php/src/Core/LifecycleEventProvider.php @@ -23,29 +23,83 @@ use Illuminate\Support\ServiceProvider; use Livewire\Livewire; /** - * Manages lifecycle events for lazy module loading. + * Orchestrates lifecycle events for lazy module loading. * - * This provider: - * 1. Scans modules for $listens declarations during register() - * 2. Wires up lazy listeners for each event-module pair - * 3. Fires lifecycle events at appropriate times during boot() + * The LifecycleEventProvider is the entry point for the event-driven module system. + * It coordinates module discovery, listener registration, and event firing at + * appropriate points during the application lifecycle. * - * Modules declare interest via static $listens arrays: + * ## Lifecycle Phases * + * **Registration Phase (register())** + * - Registers ModuleScanner and ModuleRegistry as singletons + * - Scans configured paths for Boot classes with `$listens` declarations + * - Wires lazy listeners for each event-module pair + * + * **Boot Phase (boot())** + * - Fires queue worker event if in queue context + * - Schedules FrameworkBooted event via `$app->booted()` + * + * **Event Firing (static fire* methods)** + * - Called by frontage modules (Web, Admin, Api, etc.) at appropriate times + * - Fire events, collect requests, and process them with appropriate middleware + * + * ## Module Declaration + * + * Modules declare interest in events via static `$listens` arrays in their Boot class: + * + * ```php + * class Boot + * { * public static array $listens = [ * WebRoutesRegistering::class => 'onWebRoutes', * AdminPanelBooting::class => 'onAdmin', + * ConsoleBooting::class => ['onConsole', 10], // With priority * ]; * - * The module is only instantiated when its events fire. + * public function onWebRoutes(WebRoutesRegistering $event): void + * { + * $event->routes(fn () => require __DIR__.'/Routes/web.php'); + * $event->views('mymodule', __DIR__.'/Views'); + * } + * } + * ``` + * + * The module is only instantiated when its registered events actually fire, + * enabling efficient lazy loading based on request context. + * + * ## Default Scan Paths + * + * By default, scans these directories under `app_path()`: + * - `Core` - Core system modules + * - `Mod` - Feature modules + * - `Website` - Website/domain-specific modules + * + * @package Core + * + * @see ModuleScanner For module discovery + * @see ModuleRegistry For listener registration + * @see LazyModuleListener For lazy instantiation */ class LifecycleEventProvider extends ServiceProvider { /** * Directories to scan for modules with $listens declarations. + * + * @var array */ protected array $scanPaths = []; + /** + * Register module infrastructure and wire lazy listeners. + * + * This method: + * 1. Registers ModuleScanner and ModuleRegistry as singletons + * 2. Configures default scan paths (Core, Mod, Website) + * 3. Triggers module scanning and listener registration + * + * Runs early in the application lifecycle before boot(). + */ public function register(): void { // Register infrastructure @@ -66,6 +120,15 @@ class LifecycleEventProvider extends ServiceProvider $registry->register($this->scanPaths); } + /** + * Boot the provider and schedule late-stage events. + * + * Fires queue worker event if running in queue context, and schedules + * the FrameworkBooted event to fire after all providers have booted. + * + * Note: Most lifecycle events (Web, Admin, API, etc.) are fired by their + * respective frontage modules, not here. + */ public function boot(): void { // Console event now fired by Core\Front\Cli\Boot @@ -82,9 +145,18 @@ class LifecycleEventProvider extends ServiceProvider } /** - * Fire WebRoutesRegistering and process requests. + * Fire WebRoutesRegistering and process collected requests. * - * Called by Front/Web/Boot when web middleware is being set up. + * Called by Front/Web/Boot when web middleware is being set up. This method: + * + * 1. Fires the WebRoutesRegistering event to all listeners + * 2. Processes view namespace requests (adds them to the view finder) + * 3. Processes Livewire component requests (registers with Livewire) + * 4. Processes route requests (wraps with 'web' middleware) + * 5. Refreshes route name and action lookups + * + * Routes registered through this event are automatically wrapped with + * the 'web' middleware group for session, CSRF, etc. */ public static function fireWebRoutes(): void { @@ -116,9 +188,20 @@ class LifecycleEventProvider extends ServiceProvider } /** - * Fire AdminPanelBooting and process requests. + * Fire AdminPanelBooting and process collected requests. * - * Called by Front/Admin/Boot when admin routes are being set up. + * Called by Front/Admin/Boot when admin routes are being set up. This method: + * + * 1. Fires the AdminPanelBooting event to all listeners + * 2. Processes view namespace requests + * 3. Processes translation namespace requests + * 4. Processes Livewire component requests + * 5. Processes route requests (wraps with 'admin' middleware) + * + * Routes registered through this event are automatically wrapped with + * the 'admin' middleware group for authentication, authorization, etc. + * + * Navigation items are handled separately via AdminMenuProvider interface. */ public static function fireAdminBooting(): void { @@ -159,9 +242,14 @@ class LifecycleEventProvider extends ServiceProvider } /** - * Fire ClientRoutesRegistering and process requests. + * Fire ClientRoutesRegistering and process collected requests. * - * Called by Front/Client/Boot when client routes are being set up. + * Called by Front/Client/Boot when client dashboard routes are being set up. + * This is for authenticated SaaS customers managing their namespace (bio pages, + * settings, analytics, etc.). + * + * Routes registered through this event are automatically wrapped with + * the 'client' middleware group. */ public static function fireClientRoutes(): void { @@ -193,9 +281,13 @@ class LifecycleEventProvider extends ServiceProvider } /** - * Fire ApiRoutesRegistering and process requests. + * Fire ApiRoutesRegistering and process collected requests. * - * Called by Front/Api/Boot when API routes are being set up. + * Called by Front/Api/Boot when REST API routes are being set up. + * + * Routes registered through this event are automatically: + * - Wrapped with the 'api' middleware group + * - Prefixed with '/api' */ public static function fireApiRoutes(): void { @@ -209,11 +301,14 @@ class LifecycleEventProvider extends ServiceProvider } /** - * Fire McpToolsRegistering and return collected handlers. + * Fire McpToolsRegistering and return collected handler classes. * - * Called by MCP server command when loading tools. + * Called by the MCP (Model Context Protocol) server command when loading tools. + * Modules register their MCP tool handlers through this event. * - * @return array Handler class names + * @return array Fully qualified class names of McpToolHandler implementations + * + * @see \Core\Front\Mcp\Contracts\McpToolHandler */ public static function fireMcpTools(): array { @@ -224,7 +319,10 @@ class LifecycleEventProvider extends ServiceProvider } /** - * Fire ConsoleBooting and process requests. + * Fire ConsoleBooting and register collected Artisan commands. + * + * Called when running in CLI context. Modules register their Artisan + * commands through the event's `command()` method. */ protected function fireConsoleBooting(): void { @@ -238,7 +336,10 @@ class LifecycleEventProvider extends ServiceProvider } /** - * Fire QueueWorkerBooting and process requests. + * Fire QueueWorkerBooting for queue worker context. + * + * Called when the application is running as a queue worker. Modules can + * use this event for queue-specific initialization. */ protected function fireQueueWorkerBooting(): void { diff --git a/packages/core-php/src/Core/ModuleRegistry.php b/packages/core-php/src/Core/ModuleRegistry.php index f62a8ee..1f059fe 100644 --- a/packages/core-php/src/Core/ModuleRegistry.php +++ b/packages/core-php/src/Core/ModuleRegistry.php @@ -13,23 +13,81 @@ namespace Core; use Illuminate\Support\Facades\Event; /** - * Manages lazy module registration via events. + * Manages lazy module registration via Laravel's event system. * - * Scans module directories, extracts $listens declarations, - * and wires up lazy listeners for each event-module pair. + * The ModuleRegistry is the central coordinator for the event-driven module loading + * system. It uses ModuleScanner to discover modules, then wires up LazyModuleListener + * instances for each event-module pair. * - * Listeners are registered in priority order (higher priority runs first). + * ## Registration Flow * - * Usage: - * $registry = new ModuleRegistry(new ModuleScanner()); - * $registry->register([app_path('Core'), app_path('Mod')]); + * 1. `register()` is called with paths to scan (typically in a ServiceProvider) + * 2. ModuleScanner discovers all Boot classes with `$listens` declarations + * 3. For each event-listener pair, a LazyModuleListener is registered + * 4. Listeners are sorted by priority (highest first) before registration + * 5. When events fire, LazyModuleListener instantiates modules on-demand + * + * ## Priority System + * + * Listeners are sorted by priority before registration with Laravel's event system. + * Higher priority values run first: + * + * - Priority 100: Runs first + * - Priority 0: Default + * - Priority -100: Runs last + * + * ## Usage Example + * + * ```php + * // In a ServiceProvider's register() method: + * $registry = new ModuleRegistry(new ModuleScanner()); + * $registry->register([ + * app_path('Core'), + * app_path('Mod'), + * app_path('Website'), + * ]); + * + * // Query registered modules: + * $events = $registry->getEvents(); + * $modules = $registry->getModules(); + * $listeners = $registry->getListenersFor(WebRoutesRegistering::class); + * ``` + * + * ## Adding Paths After Initial Registration + * + * Use `addPaths()` to register additional module directories after the initial + * registration (e.g., for dynamically loaded plugins): + * + * ```php + * $registry->addPaths([base_path('plugins/custom-module')]); + * ``` + * + * @package Core + * + * @see ModuleScanner For the discovery mechanism + * @see LazyModuleListener For the lazy-loading wrapper */ class ModuleRegistry { + /** + * Event-to-module mappings discovered by the scanner. + * + * Structure: [EventClass => [ModuleClass => ['method' => string, 'priority' => int]]] + * + * @var array> + */ private array $mappings = []; + /** + * Whether initial registration has been performed. + */ private bool $registered = false; + /** + * Create a new ModuleRegistry instance. + * + * @param ModuleScanner $scanner The scanner used to discover module listeners + */ public function __construct( private ModuleScanner $scanner ) {} diff --git a/packages/core-php/src/Core/ModuleScanner.php b/packages/core-php/src/Core/ModuleScanner.php index eb3701b..f12d140 100644 --- a/packages/core-php/src/Core/ModuleScanner.php +++ b/packages/core-php/src/Core/ModuleScanner.php @@ -15,19 +15,54 @@ use ReflectionClass; /** * Scans module Boot.php files for event listener declarations. * - * Reads the static $listens property from Boot classes without - * instantiating them, enabling lazy loading of modules. + * The ModuleScanner is responsible for discovering modules that wish to participate + * in the lifecycle event system. It reads the static `$listens` property from Boot + * classes without instantiating them, enabling lazy loading of modules. * - * Supports priority via array syntax: + * ## How It Works + * + * The scanner looks for `Boot.php` files in immediate subdirectories of the given paths. + * Each Boot class can declare a `$listens` array mapping events to handler methods: + * + * ```php + * class Boot + * { * public static array $listens = [ - * WebRoutesRegistering::class => 'onWebRoutes', // Default priority 0 - * AdminPanelBooting::class => ['onAdmin', 10], // Priority 10 (higher = runs first) + * WebRoutesRegistering::class => 'onWebRoutes', + * AdminPanelBooting::class => ['onAdmin', 10], // With priority * ]; + * } + * ``` * - * Usage: - * $scanner = new ModuleScanner(); - * $mappings = $scanner->scan([app_path('Core'), app_path('Mod')]); - * // Returns: [EventClass => [ModuleClass => ['method' => 'name', 'priority' => 0]]] + * ## Priority System + * + * Listeners can optionally specify a priority (default: 0). Higher priority values + * run first. Use array syntax to specify priority: + * + * - `'methodName'` - Default priority 0 + * - `['methodName', 10]` - Priority 10 (runs before priority 0) + * - `['methodName', -5]` - Priority -5 (runs after priority 0) + * + * ## Namespace Detection + * + * The scanner automatically determines namespaces based on path: + * - `/Core` paths map to `Core\` namespace + * - `/Mod` paths map to `Mod\` namespace + * - `/Website` paths map to `Website\` namespace + * - `/Plug` paths map to `Plug\` namespace + * + * ## Usage Example + * + * ```php + * $scanner = new ModuleScanner(); + * $mappings = $scanner->scan([app_path('Core'), app_path('Mod')]); + * // Returns: [EventClass => [ModuleClass => ['method' => 'name', 'priority' => 0]]] + * ``` + * + * @package Core + * + * @see ModuleRegistry For registering discovered listeners with Laravel's event system + * @see LazyModuleListener For the lazy-loading listener wrapper */ class ModuleScanner { @@ -133,10 +168,18 @@ class ModuleScanner } /** - * Derive class name from file path. + * Derive fully qualified class name from file path. * - * Converts: app/Mod/Commerce/Boot.php → Mod\Commerce\Boot - * Converts: app/Core/Cdn/Boot.php → Core\Cdn\Boot + * Maps file paths to PSR-4 namespaces based on directory structure: + * + * - `app/Mod/Commerce/Boot.php` becomes `Mod\Commerce\Boot` + * - `app/Core/Cdn/Boot.php` becomes `Core\Cdn\Boot` + * - `app/Website/Acme/Boot.php` becomes `Website\Acme\Boot` + * - `app/Plug/Analytics/Boot.php` becomes `Plug\Analytics\Boot` + * + * @param string $file Absolute path to the Boot.php file + * @param string $basePath Base directory path (e.g., app_path('Mod')) + * @return string|null Fully qualified class name, or null if path doesn't match expected structure */ private function classFromFile(string $file, string $basePath): ?string { diff --git a/packages/core-php/src/Core/Search/Unified.php b/packages/core-php/src/Core/Search/Unified.php index a77c981..24cd9c9 100644 --- a/packages/core-php/src/Core/Search/Unified.php +++ b/packages/core-php/src/Core/Search/Unified.php @@ -10,10 +10,6 @@ declare(strict_types=1); namespace Core\Search; -use Core\Mod\Agentic\Models\AgentPlan; -use Core\Mod\Uptelligence\Models\Asset; -use Core\Mod\Uptelligence\Models\Pattern; -use Core\Mod\Uptelligence\Models\UpstreamTodo; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; @@ -253,14 +249,14 @@ class Unified */ protected function searchPatterns(string $query): Collection { - if (! class_exists(Pattern::class)) { + if (! class_exists(\Core\Mod\Uptelligence\Models\Pattern::class)) { return collect(); } $escaped = $this->escapeLikeQuery($query); try { - return Pattern::where('name', 'like', "%{$escaped}%") + return \Core\Mod\Uptelligence\Models\Pattern::where('name', 'like', "%{$escaped}%") ->orWhere('description', 'like', "%{$escaped}%") ->orWhere('category', 'like', "%{$escaped}%") ->limit(20) @@ -291,14 +287,14 @@ class Unified */ protected function searchAssets(string $query): Collection { - if (! class_exists(Asset::class)) { + if (! class_exists(\Core\Mod\Uptelligence\Models\Asset::class)) { return collect(); } $escaped = $this->escapeLikeQuery($query); try { - return Asset::where('name', 'like', "%{$escaped}%") + return \Core\Mod\Uptelligence\Models\Asset::where('name', 'like', "%{$escaped}%") ->orWhere('slug', 'like', "%{$escaped}%") ->orWhere('description', 'like', "%{$escaped}%") ->limit(20) @@ -330,14 +326,14 @@ class Unified */ protected function searchTodos(string $query): Collection { - if (! class_exists(UpstreamTodo::class)) { + if (! class_exists(\Core\Mod\Uptelligence\Models\UpstreamTodo::class)) { return collect(); } $escaped = $this->escapeLikeQuery($query); try { - return UpstreamTodo::where('title', 'like', "%{$escaped}%") + return \Core\Mod\Uptelligence\Models\UpstreamTodo::where('title', 'like', "%{$escaped}%") ->orWhere('description', 'like', "%{$escaped}%") ->limit(20) ->get() @@ -368,14 +364,14 @@ class Unified */ protected function searchPlans(string $query): Collection { - if (! class_exists(AgentPlan::class)) { + if (! class_exists(\Core\Mod\Agentic\Models\AgentPlan::class)) { return collect(); } $escaped = $this->escapeLikeQuery($query); try { - return AgentPlan::where('title', 'like', "%{$escaped}%") + return \Core\Mod\Agentic\Models\AgentPlan::where('title', 'like', "%{$escaped}%") ->orWhere('slug', 'like', "%{$escaped}%") ->orWhere('description', 'like', "%{$escaped}%") ->limit(20) diff --git a/packages/core-php/src/Core/Seo/Jobs/GenerateOgImageJob.php b/packages/core-php/src/Core/Seo/Jobs/GenerateOgImageJob.php index 5254829..b992a85 100644 --- a/packages/core-php/src/Core/Seo/Jobs/GenerateOgImageJob.php +++ b/packages/core-php/src/Core/Seo/Jobs/GenerateOgImageJob.php @@ -10,8 +10,6 @@ declare(strict_types=1); namespace Core\Seo\Jobs; -use Core\Mod\Web\Models\Page; -use Core\Mod\Web\Services\DynamicOgImageService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -59,10 +57,26 @@ class GenerateOgImageJob implements ShouldQueue /** * Execute the job. + * + * Requires Core\Mod\Web module to be installed for full functionality. */ - public function handle(DynamicOgImageService $ogService): void + public function handle(): void { - $biolink = Page::find($this->biolinkId); + // Check if required Web module classes exist + if (! class_exists(\Core\Mod\Web\Models\Page::class)) { + Log::warning('OG image generation skipped: Web module not installed'); + + return; + } + + if (! class_exists(\Core\Mod\Web\Services\DynamicOgImageService::class)) { + Log::warning('OG image generation skipped: DynamicOgImageService not available'); + + return; + } + + $ogService = app(\Core\Mod\Web\Services\DynamicOgImageService::class); + $biolink = \Core\Mod\Web\Models\Page::find($this->biolinkId); if (! $biolink) { Log::warning("OG image generation skipped: biolink {$this->biolinkId} not found"); diff --git a/packages/core-php/src/Core/Seo/Schema.php b/packages/core-php/src/Core/Seo/Schema.php index 15ba920..69ac67b 100644 --- a/packages/core-php/src/Core/Seo/Schema.php +++ b/packages/core-php/src/Core/Seo/Schema.php @@ -10,7 +10,6 @@ declare(strict_types=1); namespace Core\Seo; -use Core\Mod\Content\Models\ContentItem; use Core\Seo\Validation\SchemaValidator; /** @@ -62,8 +61,10 @@ class Schema /** * Generate complete JSON-LD schema for a content item. + * + * @param object $item Content item model instance (expects ContentItem-like interface) */ - public function generateSchema(ContentItem $item, array $options = []): array + public function generateSchema(object $item, array $options = []): array { $graph = []; @@ -94,8 +95,10 @@ class Schema /** * Generate article schema. + * + * @param object $item Content item model instance (expects ContentItem-like interface) */ - public function articleSchema(ContentItem $item, array $options = []): array + public function articleSchema(object $item, array $options = []): array { $type = $options['type'] ?? 'TechArticle'; $wordCount = str_word_count(strip_tags($item->display_content ?? '')); @@ -161,8 +164,10 @@ class Schema /** * Generate HowTo schema from article content. + * + * @param object $item Content item model instance (expects ContentItem-like interface) */ - public function howToSchema(ContentItem $item): array + public function howToSchema(object $item): array { $steps = $this->extractSteps($item); @@ -214,8 +219,10 @@ class Schema /** * Generate breadcrumb schema. + * + * @param object $item Content item model instance (expects ContentItem-like interface) */ - public function breadcrumbSchema(ContentItem $item): array + public function breadcrumbSchema(object $item): array { $workspace = $item->workspace; $domain = $workspace?->domain ?? $this->baseDomain(); @@ -295,8 +302,10 @@ class Schema /** * Check if article content contains numbered steps. + * + * @param object $item Content item model instance */ - protected function hasSteps(ContentItem $item): bool + protected function hasSteps(object $item): bool { $content = $item->display_content ?? ''; @@ -306,8 +315,10 @@ class Schema /** * Extract steps from article content. + * + * @param object $item Content item model instance */ - protected function extractSteps(ContentItem $item): array + protected function extractSteps(object $item): array { $content = $item->display_content ?? ''; $steps = []; @@ -340,8 +351,10 @@ class Schema /** * Extract FAQ from article content. + * + * @param object $item Content item model instance */ - protected function extractFaq(ContentItem $item): ?array + protected function extractFaq(object $item): ?array { $content = $item->display_content ?? ''; $faqs = []; @@ -367,8 +380,10 @@ class Schema /** * Get the full URL for an article. + * + * @param object $item Content item model instance */ - protected function getArticleUrl(ContentItem $item): string + protected function getArticleUrl(object $item): string { $workspace = $item->workspace; $domain = $workspace?->domain ?? $this->baseDomain(); @@ -382,8 +397,10 @@ class Schema /** * Generate an excerpt from content. + * + * @param object $item Content item model instance */ - protected function generateExcerpt(ContentItem $item, int $length = 155): string + protected function generateExcerpt(object $item, int $length = 155): string { $content = strip_tags($item->display_content ?? ''); $content = preg_replace('/\s+/', ' ', $content); @@ -421,9 +438,11 @@ class Schema /** * Generate schema with validation. * + * @param object $item Content item model instance (expects ContentItem-like interface) + * * @throws \InvalidArgumentException if schema validation fails */ - public function generateValidatedSchema(ContentItem $item, array $options = []): array + public function generateValidatedSchema(object $item, array $options = []): array { $schema = $this->generateSchema($item, $options); $result = $this->validate($schema); diff --git a/packages/core-php/src/Core/Seo/Services/SchemaBuilderService.php b/packages/core-php/src/Core/Seo/Services/SchemaBuilderService.php index 0e6682b..098c468 100644 --- a/packages/core-php/src/Core/Seo/Services/SchemaBuilderService.php +++ b/packages/core-php/src/Core/Seo/Services/SchemaBuilderService.php @@ -10,8 +10,6 @@ declare(strict_types=1); namespace Core\Seo\Services; -use Core\Mod\Content\Models\ContentItem; -use Core\Mod\Tenant\Models\Workspace; use Core\Seo\Validation\SchemaValidator; /** @@ -31,8 +29,10 @@ class SchemaBuilderService { /** * Build Article schema for a content item. + * + * @param object $item Content item model instance (expects ContentItem-like interface) */ - public function buildArticleSchema(ContentItem $item): array + public function buildArticleSchema(object $item): array { return [ '@context' => 'https://schema.org', @@ -55,8 +55,10 @@ class SchemaBuilderService /** * Build BlogPosting schema (more specific than Article). + * + * @param object $item Content item model instance (expects ContentItem-like interface) */ - public function buildBlogPostingSchema(ContentItem $item): array + public function buildBlogPostingSchema(object $item): array { $schema = $this->buildArticleSchema($item); $schema['@type'] = 'BlogPosting'; @@ -73,8 +75,10 @@ class SchemaBuilderService /** * Build HowTo schema for instructional content. + * + * @param object $item Content item model instance (expects ContentItem-like interface) */ - public function buildHowToSchema(ContentItem $item, array $steps): array + public function buildHowToSchema(object $item, array $steps): array { return [ '@context' => 'https://schema.org', @@ -130,13 +134,15 @@ class SchemaBuilderService /** * Build Organization schema. + * + * @param object|null $workspace Workspace model instance (expects name and domain properties) */ - public function getOrganizationSchema(?Workspace $workspace = null): array + public function getOrganizationSchema(?object $workspace = null): array { return [ '@type' => 'Organization', 'name' => $workspace?->name ?? 'Host UK', - 'url' => $workspace ? "https://{$workspace->domain}" : 'https://host.uk.com', + 'url' => $workspace !== null ? "https://{$workspace->domain}" : 'https://host.uk.com', 'logo' => [ '@type' => 'ImageObject', 'url' => 'https://host.uk.com/images/logo.png', @@ -146,8 +152,10 @@ class SchemaBuilderService /** * Build WebSite schema. + * + * @param object $workspace Workspace model instance (expects name and domain properties) */ - public function buildWebsiteSchema(Workspace $workspace): array + public function buildWebsiteSchema(object $workspace): array { return [ '@context' => 'https://schema.org', @@ -243,8 +251,10 @@ class SchemaBuilderService /** * Get the canonical URL for a content item. + * + * @param object $item Content item model instance */ - private function getContentUrl(ContentItem $item): string + private function getContentUrl(object $item): string { $domain = $item->workspace?->domain ?? 'host.uk.com'; diff --git a/packages/core-php/src/Core/Service/Contracts/HealthCheckable.php b/packages/core-php/src/Core/Service/Contracts/HealthCheckable.php index 17900b3..c3d23ad 100644 --- a/packages/core-php/src/Core/Service/Contracts/HealthCheckable.php +++ b/packages/core-php/src/Core/Service/Contracts/HealthCheckable.php @@ -15,15 +15,30 @@ use Core\Service\HealthCheckResult; /** * Contract for services that provide health checks. * - * Services implementing this interface can report their operational - * status for monitoring, load balancing, and alerting purposes. + * Services implementing this interface can report their operational status + * for monitoring, load balancing, and alerting purposes. Health endpoints + * can aggregate results from all registered HealthCheckable services. + * + * ## Health Check Guidelines * * Health checks should be: - * - Fast (< 5 seconds timeout recommended) - * - Non-destructive (read-only operations) - * - Representative of actual service health * - * Example implementation: + * - **Fast** - Complete within 5 seconds (preferably < 1 second) + * - **Non-destructive** - Perform read-only operations only + * - **Representative** - Actually test the critical dependencies + * - **Safe** - Handle all exceptions and return HealthCheckResult + * + * ## Result States + * + * Use `HealthCheckResult` factory methods: + * + * - `healthy()` - Service is fully operational + * - `degraded()` - Service works but with reduced performance/capability + * - `unhealthy()` - Service is not operational + * - `fromException()` - Convert exception to unhealthy result + * + * ## Example Implementation + * * ```php * public function healthCheck(): HealthCheckResult * { @@ -48,6 +63,10 @@ use Core\Service\HealthCheckResult; * } * } * ``` + * + * @package Core\Service\Contracts + * + * @see HealthCheckResult For result factory methods */ interface HealthCheckable { diff --git a/packages/core-php/src/Core/Service/Contracts/ServiceDefinition.php b/packages/core-php/src/Core/Service/Contracts/ServiceDefinition.php index f569c49..e28ad94 100644 --- a/packages/core-php/src/Core/Service/Contracts/ServiceDefinition.php +++ b/packages/core-php/src/Core/Service/Contracts/ServiceDefinition.php @@ -14,41 +14,59 @@ use Core\Front\Admin\Contracts\AdminMenuProvider; use Core\Service\ServiceVersion; /** - * Contract for service definitions. + * Contract for SaaS service definitions. * - * Services are the product layer - they define how modules are presented - * to users as SaaS products. Each service has a definition used to populate - * the platform_services table and admin menu registration. + * Services are the product layer of the framework - they define how modules are + * presented to users as SaaS products. Each service has a definition that: * - * Extends AdminMenuProvider to integrate with the admin menu system. + * - Populates the `platform_services` table for entitlement management + * - Integrates with the admin menu system via `AdminMenuProvider` + * - Provides versioning for API compatibility and deprecation tracking + * + * ## Service Definition Array + * + * The `definition()` method returns an array with service metadata: + * + * ```php + * public static function definition(): array + * { + * return [ + * 'code' => 'bio', // Unique service code + * 'module' => 'Mod\\Bio', // Module namespace + * 'name' => 'BioHost', // Display name + * 'tagline' => 'Link in bio pages', // Short description + * 'description' => 'Create beautiful...', // Full description + * 'icon' => 'link', // FontAwesome icon + * 'color' => '#3B82F6', // Brand color + * 'entitlement_code' => 'core.srv.bio', // Access control code + * 'sort_order' => 10, // Menu ordering + * ]; + * } + * ``` * * ## Versioning * - * Services should implement the version() method to declare their contract - * version. This enables: - * - Tracking breaking changes in service contracts - * - Deprecation warnings before removing features - * - Sunset date enforcement for deprecated versions + * Services should implement `version()` to declare their contract version. + * This enables tracking breaking changes and deprecation: * - * Example: * ```php * public static function version(): ServiceVersion * { * return new ServiceVersion(2, 1, 0); * } - * ``` * - * For deprecated services: - * ```php + * // For deprecated services: * public static function version(): ServiceVersion * { * return (new ServiceVersion(1, 0, 0)) - * ->deprecate( - * 'Use ServiceV2 instead', - * new \DateTimeImmutable('2025-06-01') - * ); + * ->deprecate('Use ServiceV2 instead', new \DateTimeImmutable('2025-06-01')); * } * ``` + * + * @package Core\Service\Contracts + * + * @see AdminMenuProvider For menu integration + * @see ServiceVersion For versioning */ interface ServiceDefinition extends AdminMenuProvider {