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 <noreply@anthropic.com>
This commit is contained in:
parent
c6993dbfca
commit
13670ebb34
47 changed files with 1491 additions and 350 deletions
|
|
@ -20,8 +20,6 @@ use Core\Cdn\Services\BunnyStorageService;
|
||||||
use Core\Cdn\Services\FluxCdnService;
|
use Core\Cdn\Services\FluxCdnService;
|
||||||
use Core\Cdn\Services\StorageOffload;
|
use Core\Cdn\Services\StorageOffload;
|
||||||
use Core\Cdn\Services\StorageUrlResolver;
|
use Core\Cdn\Services\StorageUrlResolver;
|
||||||
use Core\Plug\Cdn\CdnManager;
|
|
||||||
use Core\Plug\Storage\StorageManager;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,9 +43,13 @@ class Boot extends ServiceProvider
|
||||||
$this->mergeConfigFrom(__DIR__.'/config.php', 'cdn');
|
$this->mergeConfigFrom(__DIR__.'/config.php', 'cdn');
|
||||||
$this->mergeConfigFrom(__DIR__.'/offload.php', 'offload');
|
$this->mergeConfigFrom(__DIR__.'/offload.php', 'offload');
|
||||||
|
|
||||||
// Register Plug managers as singletons
|
// Register Plug managers as singletons (when available)
|
||||||
$this->app->singleton(CdnManager::class);
|
if (class_exists(\Core\Plug\Cdn\CdnManager::class)) {
|
||||||
$this->app->singleton(StorageManager::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)
|
// Register legacy services as singletons (for backward compatibility)
|
||||||
$this->app->singleton(BunnyCdnService::class);
|
$this->app->singleton(BunnyCdnService::class);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Cdn\Console;
|
namespace Core\Cdn\Console;
|
||||||
|
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Core\Plug\Cdn\Bunny\Purge;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class CdnPurge extends Command
|
class CdnPurge extends Command
|
||||||
|
|
@ -35,12 +33,18 @@ class CdnPurge extends Command
|
||||||
*/
|
*/
|
||||||
protected $description = 'Purge content from CDN edge cache';
|
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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__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
|
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');
|
$workspaceArg = $this->argument('workspace');
|
||||||
$urls = $this->option('url');
|
$urls = $this->option('url');
|
||||||
$tag = $this->option('tag');
|
$tag = $this->option('tag');
|
||||||
|
|
@ -84,9 +94,13 @@ class CdnPurge extends Command
|
||||||
|
|
||||||
// Purge by workspace
|
// Purge by workspace
|
||||||
if (empty($workspaceArg)) {
|
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(
|
$workspaceArg = $this->choice(
|
||||||
'What would you like to purge?',
|
'What would you like to purge?',
|
||||||
array_merge(['all', 'Select specific URLs'], Workspace::pluck('slug')->toArray()),
|
$workspaceOptions,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -203,7 +217,13 @@ class CdnPurge extends Command
|
||||||
|
|
||||||
protected function purgeAllWorkspaces(bool $dryRun): int
|
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()) {
|
if ($workspaces->isEmpty()) {
|
||||||
$this->error('No workspaces found');
|
$this->error('No workspaces found');
|
||||||
|
|
@ -255,13 +275,19 @@ class CdnPurge extends Command
|
||||||
|
|
||||||
protected function purgeWorkspace(string $slug, bool $dryRun): int
|
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) {
|
if (! $workspace) {
|
||||||
$this->error("Workspace not found: {$slug}");
|
$this->error("Workspace not found: {$slug}");
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
$this->info('Available workspaces:');
|
$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;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ namespace Core\Cdn\Console;
|
||||||
|
|
||||||
use Core\Cdn\Services\FluxCdnService;
|
use Core\Cdn\Services\FluxCdnService;
|
||||||
use Core\Cdn\Services\StorageUrlResolver;
|
use Core\Cdn\Services\StorageUrlResolver;
|
||||||
use Core\Plug\Storage\Bunny\VBucket;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
|
|
@ -30,7 +29,10 @@ class PushAssetsToCdn extends Command
|
||||||
|
|
||||||
protected StorageUrlResolver $cdn;
|
protected StorageUrlResolver $cdn;
|
||||||
|
|
||||||
protected VBucket $vbucket;
|
/**
|
||||||
|
* VBucket instance (Core\Plug\Storage\Bunny\VBucket when available).
|
||||||
|
*/
|
||||||
|
protected ?object $vbucket = null;
|
||||||
|
|
||||||
protected bool $dryRun = false;
|
protected bool $dryRun = false;
|
||||||
|
|
||||||
|
|
@ -40,12 +42,18 @@ class PushAssetsToCdn extends Command
|
||||||
|
|
||||||
public function handle(FluxCdnService $flux, StorageUrlResolver $cdn): int
|
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->cdn = $cdn;
|
||||||
$this->dryRun = $this->option('dry-run');
|
$this->dryRun = $this->option('dry-run');
|
||||||
|
|
||||||
// Create vBucket for workspace isolation
|
// Create vBucket for workspace isolation
|
||||||
$domain = $this->option('domain');
|
$domain = $this->option('domain');
|
||||||
$this->vbucket = VBucket::public($domain);
|
$this->vbucket = \Core\Plug\Storage\Bunny\VBucket::public($domain);
|
||||||
|
|
||||||
$pushFlux = $this->option('flux');
|
$pushFlux = $this->option('flux');
|
||||||
$pushFontawesome = $this->option('fontawesome');
|
$pushFontawesome = $this->option('fontawesome');
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Cdn\Jobs;
|
namespace Core\Cdn\Jobs;
|
||||||
|
|
||||||
use Core\Plug\Storage\StorageManager;
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
|
@ -55,9 +54,22 @@ class PushAssetToCdn implements ShouldQueue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* 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)) {
|
if (! config('cdn.bunny.push_enabled', false)) {
|
||||||
Log::debug('PushAssetToCdn: Push disabled, skipping', [
|
Log::debug('PushAssetToCdn: Push disabled, skipping', [
|
||||||
'disk' => $this->disk,
|
'disk' => $this->disk,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||||
namespace Core\Cdn\Services;
|
namespace Core\Cdn\Services;
|
||||||
|
|
||||||
use Core\Cdn\Jobs\PushAssetToCdn;
|
use Core\Cdn\Jobs\PushAssetToCdn;
|
||||||
use Core\Plug\Storage\StorageManager;
|
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
|
@ -37,9 +36,12 @@ class AssetPipeline
|
||||||
{
|
{
|
||||||
protected StorageUrlResolver $urlResolver;
|
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->urlResolver = $urlResolver;
|
||||||
$this->storage = $storage;
|
$this->storage = $storage;
|
||||||
|
|
@ -220,8 +222,10 @@ class AssetPipeline
|
||||||
$results[$path] = $disk->delete($path);
|
$results[$path] = $disk->delete($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk delete from CDN storage
|
// Bulk delete from CDN storage (requires StorageManager from Plug module)
|
||||||
$this->storage->zone($bucket)->delete()->paths($paths);
|
if ($this->storage !== null) {
|
||||||
|
$this->storage->zone($bucket)->delete()->paths($paths);
|
||||||
|
}
|
||||||
|
|
||||||
// Purge from CDN cache if enabled
|
// Purge from CDN cache if enabled
|
||||||
if (config('cdn.pipeline.auto_purge', true)) {
|
if (config('cdn.pipeline.auto_purge', true)) {
|
||||||
|
|
@ -299,11 +303,11 @@ class AssetPipeline
|
||||||
|
|
||||||
if ($queue) {
|
if ($queue) {
|
||||||
PushAssetToCdn::dispatch($disk, $path, $zone);
|
PushAssetToCdn::dispatch($disk, $path, $zone);
|
||||||
} else {
|
} elseif ($this->storage !== null) {
|
||||||
// Synchronous push if no queue configured
|
// Synchronous push if no queue configured (requires StorageManager from Plug module)
|
||||||
$disk = \Illuminate\Support\Facades\Storage::disk($disk);
|
$diskInstance = \Illuminate\Support\Facades\Storage::disk($disk);
|
||||||
if ($disk->exists($path)) {
|
if ($diskInstance->exists($path)) {
|
||||||
$contents = $disk->get($path);
|
$contents = $diskInstance->get($path);
|
||||||
$this->storage->zone($zone)->upload()->contents($path, $contents);
|
$this->storage->zone($zone)->upload()->contents($path, $contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||||
namespace Core\Cdn\Services;
|
namespace Core\Cdn\Services;
|
||||||
|
|
||||||
use Core\Config\ConfigService;
|
use Core\Config\ConfigService;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
|
@ -167,8 +166,10 @@ class BunnyCdnService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purge all cached content for a workspace.
|
* 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}");
|
return $this->purgeByTag("workspace-{$workspace->uuid}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Config;
|
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\Http\Request;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
@ -21,8 +19,11 @@ use Illuminate\Support\Facades\Cache;
|
||||||
*
|
*
|
||||||
* Provides a standardised interface for managing configuration settings
|
* Provides a standardised interface for managing configuration settings
|
||||||
* with validation, caching, and database persistence.
|
* 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.
|
* Create a new config instance.
|
||||||
|
|
@ -58,12 +59,18 @@ abstract class Config implements ConfigContract
|
||||||
/**
|
/**
|
||||||
* Insert or update configuration in database.
|
* Insert or update configuration in database.
|
||||||
*
|
*
|
||||||
|
* Requires Core\Mod\Social module to be installed.
|
||||||
|
*
|
||||||
* @param string $name Configuration field name
|
* @param string $name Configuration field name
|
||||||
* @param mixed $payload Value to store
|
* @param mixed $payload Value to store
|
||||||
*/
|
*/
|
||||||
public function insert(string $name, mixed $payload): void
|
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()],
|
['name' => $name, 'group' => $this->group()],
|
||||||
['payload' => $payload]
|
['payload' => $payload]
|
||||||
);
|
);
|
||||||
|
|
@ -73,15 +80,22 @@ abstract class Config implements ConfigContract
|
||||||
* Get a configuration value.
|
* Get a configuration value.
|
||||||
*
|
*
|
||||||
* Checks cache first, then database, finally falls back to default from form().
|
* 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
|
* @param string $name Configuration field name
|
||||||
*/
|
*/
|
||||||
public function get(string $name): mixed
|
public function get(string $name): mixed
|
||||||
{
|
{
|
||||||
return $this->getCache($name, function () use ($name) {
|
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}",
|
property: "{$this->group()}.{$name}",
|
||||||
default: Arr::get($this->form(), $name)
|
default: $default
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->putCache($name, $payload);
|
$this->putCache($name, $payload);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ use Core\Config\Models\Channel;
|
||||||
use Core\Config\Models\ConfigKey;
|
use Core\Config\Models\ConfigKey;
|
||||||
use Core\Config\Models\ConfigProfile;
|
use Core\Config\Models\ConfigProfile;
|
||||||
use Core\Config\Models\ConfigValue;
|
use Core\Config\Models\ConfigValue;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -150,11 +149,12 @@ class ConfigResolver
|
||||||
* NOTE: This is the expensive path - only called when lazy-priming.
|
* NOTE: This is the expensive path - only called when lazy-priming.
|
||||||
* Normal reads hit the hash directly via ConfigService.
|
* 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
|
* @param string|Channel|null $channel Channel code or object
|
||||||
*/
|
*/
|
||||||
public function resolve(
|
public function resolve(
|
||||||
string $keyCode,
|
string $keyCode,
|
||||||
?Workspace $workspace = null,
|
?object $workspace = null,
|
||||||
string|Channel|null $channel = null,
|
string|Channel|null $channel = null,
|
||||||
): ConfigResult {
|
): ConfigResult {
|
||||||
// Get key definition (DB query - only during resolve, not normal reads)
|
// 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).
|
* 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(
|
protected function resolveJsonSubKey(
|
||||||
string $keyCode,
|
string $keyCode,
|
||||||
?Workspace $workspace,
|
?object $workspace,
|
||||||
string|Channel|null $channel,
|
string|Channel|null $channel,
|
||||||
): ConfigResult {
|
): ConfigResult {
|
||||||
// Guard against stack overflow from deep nesting
|
// Guard against stack overflow from deep nesting
|
||||||
|
|
@ -277,11 +280,12 @@ class ConfigResolver
|
||||||
/**
|
/**
|
||||||
* Build the channel inheritance chain.
|
* Build the channel inheritance chain.
|
||||||
*
|
*
|
||||||
|
* @param object|null $workspace Workspace model instance or null for system scope
|
||||||
* @return Collection<int, Channel|null>
|
* @return Collection<int, Channel|null>
|
||||||
*/
|
*/
|
||||||
public function buildChannelChain(
|
public function buildChannelChain(
|
||||||
string|Channel|null $channel,
|
string|Channel|null $channel,
|
||||||
?Workspace $workspace = null,
|
?object $workspace = null,
|
||||||
): Collection {
|
): Collection {
|
||||||
$chain = new Collection;
|
$chain = new Collection;
|
||||||
|
|
||||||
|
|
@ -407,7 +411,7 @@ class ConfigResolver
|
||||||
* Providers supply values from module data without database storage.
|
* Providers supply values from module data without database storage.
|
||||||
*
|
*
|
||||||
* @param string $pattern Key pattern (supports * wildcard)
|
* @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
|
public function registerProvider(string $pattern, callable $provider): void
|
||||||
{
|
{
|
||||||
|
|
@ -416,10 +420,12 @@ class ConfigResolver
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve value from virtual providers.
|
* Resolve value from virtual providers.
|
||||||
|
*
|
||||||
|
* @param object|null $workspace Workspace model instance or null for system scope
|
||||||
*/
|
*/
|
||||||
public function resolveFromProviders(
|
public function resolveFromProviders(
|
||||||
string $keyCode,
|
string $keyCode,
|
||||||
?Workspace $workspace,
|
?object $workspace,
|
||||||
string|Channel|null $channel,
|
string|Channel|null $channel,
|
||||||
): mixed {
|
): mixed {
|
||||||
foreach ($this->providers as $pattern => $provider) {
|
foreach ($this->providers as $pattern => $provider) {
|
||||||
|
|
@ -455,9 +461,10 @@ class ConfigResolver
|
||||||
*
|
*
|
||||||
* NOTE: Only called during prime, not normal reads.
|
* NOTE: Only called during prime, not normal reads.
|
||||||
*
|
*
|
||||||
|
* @param object|null $workspace Workspace model instance or null for system scope
|
||||||
* @return array<string, ConfigResult>
|
* @return array<string, ConfigResult>
|
||||||
*/
|
*/
|
||||||
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 = [];
|
$results = [];
|
||||||
|
|
||||||
|
|
@ -474,11 +481,12 @@ class ConfigResolver
|
||||||
*
|
*
|
||||||
* NOTE: Only called during prime, not normal reads.
|
* NOTE: Only called during prime, not normal reads.
|
||||||
*
|
*
|
||||||
|
* @param object|null $workspace Workspace model instance or null for system scope
|
||||||
* @return array<string, ConfigResult>
|
* @return array<string, ConfigResult>
|
||||||
*/
|
*/
|
||||||
public function resolveCategory(
|
public function resolveCategory(
|
||||||
string $category,
|
string $category,
|
||||||
?Workspace $workspace = null,
|
?object $workspace = null,
|
||||||
string|Channel|null $channel = null,
|
string|Channel|null $channel = null,
|
||||||
): array {
|
): array {
|
||||||
$results = [];
|
$results = [];
|
||||||
|
|
@ -497,9 +505,10 @@ class ConfigResolver
|
||||||
* Returns profiles ordered from most specific (workspace) to least (system).
|
* Returns profiles ordered from most specific (workspace) to least (system).
|
||||||
* Chain: workspace → org → system
|
* Chain: workspace → org → system
|
||||||
*
|
*
|
||||||
|
* @param object|null $workspace Workspace model instance or null for system scope
|
||||||
* @return Collection<int, ConfigProfile>
|
* @return Collection<int, ConfigProfile>
|
||||||
*/
|
*/
|
||||||
public function buildProfileChain(?Workspace $workspace = null): Collection
|
public function buildProfileChain(?object $workspace = null): Collection
|
||||||
{
|
{
|
||||||
$chain = new Collection;
|
$chain = new Collection;
|
||||||
|
|
||||||
|
|
@ -531,8 +540,10 @@ class ConfigResolver
|
||||||
*
|
*
|
||||||
* Stub for now - will connect to Tenant module when org model exists.
|
* Stub for now - will connect to Tenant module when org model exists.
|
||||||
* Organisation = multi-workspace grouping (agency accounts, teams).
|
* 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) {
|
if ($workspace === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -590,10 +601,12 @@ class ConfigResolver
|
||||||
* Check if a key prefix is configured.
|
* Check if a key prefix is configured.
|
||||||
*
|
*
|
||||||
* Optimised to use EXISTS query instead of resolving each key.
|
* 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(
|
public function isPrefixConfigured(
|
||||||
string $prefix,
|
string $prefix,
|
||||||
?Workspace $workspace = null,
|
?object $workspace = null,
|
||||||
string|Channel|null $channel = null,
|
string|Channel|null $channel = null,
|
||||||
): bool {
|
): bool {
|
||||||
// Get profile IDs for this workspace
|
// Get profile IDs for this workspace
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ use Core\Config\Models\ConfigKey;
|
||||||
use Core\Config\Models\ConfigProfile;
|
use Core\Config\Models\ConfigProfile;
|
||||||
use Core\Config\Models\ConfigResolved;
|
use Core\Config\Models\ConfigResolved;
|
||||||
use Core\Config\Models\ConfigValue;
|
use Core\Config\Models\ConfigValue;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration service - main API.
|
* Configuration service - main API.
|
||||||
|
|
@ -37,7 +36,10 @@ use Core\Mod\Tenant\Models\Workspace;
|
||||||
*/
|
*/
|
||||||
class ConfigService
|
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;
|
protected ?Channel $channel = null;
|
||||||
|
|
||||||
|
|
@ -47,8 +49,10 @@ class ConfigService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current context (called by middleware).
|
* 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->workspace = $workspace;
|
||||||
$this->channel = $channel;
|
$this->channel = $channel;
|
||||||
|
|
@ -56,8 +60,10 @@ class ConfigService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current workspace context.
|
* Get current workspace context.
|
||||||
|
*
|
||||||
|
* @return object|null Workspace model instance or null
|
||||||
*/
|
*/
|
||||||
public function getWorkspace(): ?Workspace
|
public function getWorkspace(): ?object
|
||||||
{
|
{
|
||||||
return $this->workspace;
|
return $this->workspace;
|
||||||
}
|
}
|
||||||
|
|
@ -79,8 +85,10 @@ class ConfigService
|
||||||
* Get config for a specific workspace (admin use only).
|
* Get config for a specific workspace (admin use only).
|
||||||
*
|
*
|
||||||
* Use this when you need another workspace's settings - requires explicit intent.
|
* 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);
|
$result = $this->resolve($key, $workspace, null);
|
||||||
|
|
||||||
|
|
@ -96,11 +104,12 @@ class ConfigService
|
||||||
* 3. Hash lookup again
|
* 3. Hash lookup again
|
||||||
* 4. Compute via resolver if still not found (lazy prime)
|
* 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
|
* @param string|Channel|null $channel Channel code or object
|
||||||
*/
|
*/
|
||||||
public function resolve(
|
public function resolve(
|
||||||
string $key,
|
string $key,
|
||||||
?Workspace $workspace = null,
|
?object $workspace = null,
|
||||||
string|Channel|null $channel = null,
|
string|Channel|null $channel = null,
|
||||||
): ConfigResult {
|
): ConfigResult {
|
||||||
$workspaceId = $workspace?->id;
|
$workspaceId = $workspace?->id;
|
||||||
|
|
@ -207,10 +216,12 @@ class ConfigService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to resolve a JSON sub-key (e.g., "website.title" from "website" JSON).
|
* 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(
|
protected function resolveJsonSubKey(
|
||||||
string $keyCode,
|
string $keyCode,
|
||||||
?Workspace $workspace,
|
?object $workspace,
|
||||||
string|Channel|null $channel,
|
string|Channel|null $channel,
|
||||||
): ConfigResult {
|
): ConfigResult {
|
||||||
$parts = explode('.', $keyCode);
|
$parts = explode('.', $keyCode);
|
||||||
|
|
@ -394,9 +405,10 @@ class ConfigService
|
||||||
/**
|
/**
|
||||||
* Get all config values for a workspace.
|
* Get all config values for a workspace.
|
||||||
*
|
*
|
||||||
|
* @param object|null $workspace Workspace model instance or null for system scope
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
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;
|
$workspaceId = $workspace?->id;
|
||||||
$channelId = $this->resolveChannelId($channel, $workspace);
|
$channelId = $this->resolveChannelId($channel, $workspace);
|
||||||
|
|
@ -414,11 +426,12 @@ class ConfigService
|
||||||
/**
|
/**
|
||||||
* Get all config values for a category.
|
* Get all config values for a category.
|
||||||
*
|
*
|
||||||
|
* @param object|null $workspace Workspace model instance or null for system scope
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public function category(
|
public function category(
|
||||||
string $category,
|
string $category,
|
||||||
?Workspace $workspace = null,
|
?object $workspace = null,
|
||||||
string|Channel|null $channel = null,
|
string|Channel|null $channel = null,
|
||||||
): array {
|
): array {
|
||||||
$workspaceId = $workspace?->id;
|
$workspaceId = $workspace?->id;
|
||||||
|
|
@ -447,8 +460,10 @@ class ConfigService
|
||||||
* Call after workspace creation, config changes, or on schedule.
|
* Call after workspace creation, config changes, or on schedule.
|
||||||
*
|
*
|
||||||
* Populates both hash (process-scoped) and database (persistent).
|
* 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;
|
$workspaceId = $workspace?->id;
|
||||||
$channelId = $this->resolveChannelId($channel, $workspace);
|
$channelId = $this->resolveChannelId($channel, $workspace);
|
||||||
|
|
@ -500,7 +515,10 @@ class ConfigService
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
// Re-compute this key for the affected scope
|
// 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;
|
$channel = $channelId ? Channel::find($channelId) : null;
|
||||||
|
|
||||||
$result = $this->resolver->resolve($keyCode, $workspace, $channel);
|
$result = $this->resolver->resolve($keyCode, $workspace, $channel);
|
||||||
|
|
@ -526,18 +544,21 @@ class ConfigService
|
||||||
* Prime cache for all workspaces.
|
* Prime cache for all workspaces.
|
||||||
*
|
*
|
||||||
* Run this from a scheduled command or queue job.
|
* Run this from a scheduled command or queue job.
|
||||||
|
* Requires Core\Mod\Tenant module to prime workspace-level config.
|
||||||
*/
|
*/
|
||||||
public function primeAll(): void
|
public function primeAll(): void
|
||||||
{
|
{
|
||||||
// Prime system config
|
// Prime system config
|
||||||
$this->prime(null);
|
$this->prime(null);
|
||||||
|
|
||||||
// Prime each workspace
|
// Prime each workspace (requires Tenant module)
|
||||||
Workspace::chunk(100, function ($workspaces) {
|
if (class_exists(\Core\Mod\Tenant\Models\Workspace::class)) {
|
||||||
foreach ($workspaces as $workspace) {
|
\Core\Mod\Tenant\Models\Workspace::chunk(100, function ($workspaces) {
|
||||||
$this->prime($workspace);
|
foreach ($workspaces as $workspace) {
|
||||||
}
|
$this->prime($workspace);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -545,8 +566,10 @@ class ConfigService
|
||||||
*
|
*
|
||||||
* Clears both hash and database. Next read will lazy-prime.
|
* Clears both hash and database. Next read will lazy-prime.
|
||||||
* Fires ConfigInvalidated event.
|
* 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;
|
$workspaceId = $workspace?->id;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ namespace Core\Config\Console;
|
||||||
|
|
||||||
use Core\Config\ConfigService;
|
use Core\Config\ConfigService;
|
||||||
use Core\Config\Models\ConfigKey;
|
use Core\Config\Models\ConfigKey;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ConfigListCommand extends Command
|
class ConfigListCommand extends Command
|
||||||
|
|
@ -33,7 +32,13 @@ class ConfigListCommand extends Command
|
||||||
$workspace = null;
|
$workspace = null;
|
||||||
|
|
||||||
if ($workspaceSlug) {
|
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) {
|
if (! $workspace) {
|
||||||
$this->error("Workspace not found: {$workspaceSlug}");
|
$this->error("Workspace not found: {$workspaceSlug}");
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||||
namespace Core\Config\Console;
|
namespace Core\Config\Console;
|
||||||
|
|
||||||
use Core\Config\ConfigService;
|
use Core\Config\ConfigService;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ConfigPrimeCommand extends Command
|
class ConfigPrimeCommand extends Command
|
||||||
|
|
@ -36,7 +35,13 @@ class ConfigPrimeCommand extends Command
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($workspaceSlug) {
|
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) {
|
if (! $workspace) {
|
||||||
$this->error("Workspace not found: {$workspaceSlug}");
|
$this->error("Workspace not found: {$workspaceSlug}");
|
||||||
|
|
@ -53,7 +58,15 @@ class ConfigPrimeCommand extends Command
|
||||||
|
|
||||||
$this->info('Priming config cache for all workspaces...');
|
$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);
|
$config->prime($workspace);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration channel (voice/context substrate).
|
* Configuration channel (voice/context substrate).
|
||||||
|
|
@ -71,10 +70,17 @@ class Channel extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workspace this channel belongs to (null = system channel).
|
* Workspace this channel belongs to (null = system channel).
|
||||||
|
*
|
||||||
|
* Requires Core\Mod\Tenant module to be installed.
|
||||||
*/
|
*/
|
||||||
public function workspace(): BelongsTo
|
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ namespace Core\Config\Models;
|
||||||
|
|
||||||
use Core\Config\ConfigResult;
|
use Core\Config\ConfigResult;
|
||||||
use Core\Config\Enums\ConfigType;
|
use Core\Config\Enums\ConfigType;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
|
@ -66,10 +65,17 @@ class ConfigResolved extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workspace this resolution is for (null = system).
|
* Workspace this resolution is for (null = system).
|
||||||
|
*
|
||||||
|
* Requires Core\Mod\Tenant module to be installed.
|
||||||
*/
|
*/
|
||||||
public function workspace(): BelongsTo
|
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ use Core\Config\ConfigService;
|
||||||
use Core\Config\Models\ConfigKey;
|
use Core\Config\Models\ConfigKey;
|
||||||
use Core\Config\Models\ConfigProfile;
|
use Core\Config\Models\ConfigProfile;
|
||||||
use Core\Config\Models\ConfigValue;
|
use Core\Config\Models\ConfigValue;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\Computed;
|
||||||
use Livewire\Attributes\Url;
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -68,10 +67,17 @@ class ConfigPanel extends Component
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all workspaces (requires Tenant module).
|
||||||
|
*/
|
||||||
#[Computed]
|
#[Computed]
|
||||||
public function workspaces(): \Illuminate\Database\Eloquent\Collection
|
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]
|
#[Computed]
|
||||||
|
|
@ -97,11 +103,16 @@ class ConfigPanel extends Component
|
||||||
return ConfigProfile::ensureSystem();
|
return ConfigProfile::ensureSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selected workspace (requires Tenant module).
|
||||||
|
*
|
||||||
|
* @return object|null Workspace model instance or null
|
||||||
|
*/
|
||||||
#[Computed]
|
#[Computed]
|
||||||
public function selectedWorkspace(): ?Workspace
|
public function selectedWorkspace(): ?object
|
||||||
{
|
{
|
||||||
if ($this->workspaceId) {
|
if ($this->workspaceId && class_exists(\Core\Mod\Tenant\Models\Workspace::class)) {
|
||||||
return Workspace::find($this->workspaceId);
|
return \Core\Mod\Tenant\Models\Workspace::find($this->workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ use Core\Config\ConfigService;
|
||||||
use Core\Config\Models\ConfigKey;
|
use Core\Config\Models\ConfigKey;
|
||||||
use Core\Config\Models\ConfigProfile;
|
use Core\Config\Models\ConfigProfile;
|
||||||
use Core\Config\Models\ConfigValue;
|
use Core\Config\Models\ConfigValue;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Core\Mod\Tenant\Services\WorkspaceService;
|
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\Computed;
|
||||||
use Livewire\Attributes\On;
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -26,12 +24,19 @@ class WorkspaceConfig extends Component
|
||||||
|
|
||||||
protected ConfigService $config;
|
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->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
|
public function mount(?string $path = null): void
|
||||||
|
|
@ -55,10 +60,15 @@ class WorkspaceConfig extends Component
|
||||||
unset($this->currentKeys);
|
unset($this->currentKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current workspace (requires Tenant module).
|
||||||
|
*
|
||||||
|
* @return object|null Workspace model instance or null
|
||||||
|
*/
|
||||||
#[Computed]
|
#[Computed]
|
||||||
public function workspace(): ?Workspace
|
public function workspace(): ?object
|
||||||
{
|
{
|
||||||
return $this->workspaceService->currentModel();
|
return $this->workspaceService?->currentModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Computed]
|
#[Computed]
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,44 @@ namespace Core\Events;
|
||||||
/**
|
/**
|
||||||
* Fired when the admin panel is being bootstrapped.
|
* Fired when the admin panel is being bootstrapped.
|
||||||
*
|
*
|
||||||
* Modules listen to this event to register:
|
* Modules listen to this event to register admin-specific resources including
|
||||||
* - Admin navigation items
|
* routes, views, Livewire components, and translations for the admin dashboard.
|
||||||
* - Admin routes (wrapped with admin middleware)
|
|
||||||
* - Admin view namespaces
|
|
||||||
* - Admin Livewire components
|
|
||||||
*
|
*
|
||||||
* 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
|
class AdminPanelBooting extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,41 @@ declare(strict_types=1);
|
||||||
namespace Core\Events;
|
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.
|
* Modules listen to this event to register their REST API endpoints for
|
||||||
* Routes are automatically wrapped with the 'api' middleware group.
|
* 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
|
class ApiRoutesRegistering extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,48 @@ declare(strict_types=1);
|
||||||
namespace Core\Events;
|
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
|
* Modules listen to this event to register routes for namespace owners -
|
||||||
* (authenticated SaaS customers managing their space).
|
* 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:
|
* Fired by `LifecycleEventProvider::fireClientRoutes()` when the client
|
||||||
* - Bio/link editors
|
* frontage initializes, typically for requests to client dashboard routes.
|
||||||
* - Settings pages
|
*
|
||||||
|
* ## 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
|
* - 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
|
class ClientRoutesRegistering extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,33 @@ declare(strict_types=1);
|
||||||
namespace Core\Events;
|
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.
|
* Modules listen to this event to register Artisan commands for CLI operations
|
||||||
* Commands are registered via the command() method inherited
|
* such as maintenance tasks, data processing, or administrative functions.
|
||||||
* from LifecycleEvent.
|
|
||||||
*
|
*
|
||||||
* 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
|
class ConsoleBooting extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,63 @@ namespace Core\Events;
|
||||||
/**
|
/**
|
||||||
* Fired when resolving a domain to a website provider.
|
* Fired when resolving a domain to a website provider.
|
||||||
*
|
*
|
||||||
* Mod Boot classes listen for this event and register themselves
|
* This event enables multi-tenancy by domain, allowing different modules to
|
||||||
* if their domain pattern matches the incoming host.
|
* 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
|
class DomainResolving
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The matched provider class, if any.
|
||||||
|
*/
|
||||||
protected ?string $matchedProvider = null;
|
protected ?string $matchedProvider = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DomainResolving event.
|
||||||
|
*
|
||||||
|
* @param string $host The incoming request hostname
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $host
|
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
|
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
|
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
|
public function matchedProvider(): ?string
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,50 @@ use Illuminate\Support\Facades\Log;
|
||||||
/**
|
/**
|
||||||
* Tracks lifecycle event execution for debugging and monitoring.
|
* Tracks lifecycle event execution for debugging and monitoring.
|
||||||
*
|
*
|
||||||
* Records when events fire and which handlers respond. This is useful for:
|
* EventAuditLog records when lifecycle events fire and which handlers respond,
|
||||||
* - Debugging module loading issues
|
* including timing information and success/failure status. This is invaluable for:
|
||||||
* - Performance monitoring
|
|
||||||
* - Understanding application bootstrap flow
|
|
||||||
*
|
*
|
||||||
* Usage:
|
* - **Debugging** - Understanding why modules aren't loading
|
||||||
* EventAuditLog::enable(); // Enable logging
|
* - **Performance** - Identifying slow event handlers
|
||||||
* EventAuditLog::enableLog(); // Also write to Laravel log
|
* - **Monitoring** - Tracking application bootstrap flow
|
||||||
* // ... events fire ...
|
* - **Diagnostics** - Finding failed handlers in production
|
||||||
* $entries = EventAuditLog::entries(); // Get recorded entries
|
*
|
||||||
|
* ## 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
|
class EventAuditLog
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,47 @@ declare(strict_types=1);
|
||||||
namespace Core\Events;
|
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
|
* This event fires via Laravel's `$app->booted()` callback, after all service
|
||||||
* application context available. Most modules should use more
|
* providers have completed their `boot()` methods. Use this for late-stage
|
||||||
* specific events (AdminPanelBooting, ApiRoutesRegistering, etc.)
|
* initialization that requires the full application context.
|
||||||
* rather than this general event.
|
*
|
||||||
|
* ## 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
|
class FrameworkBooted extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,35 +13,86 @@ namespace Core\Events;
|
||||||
/**
|
/**
|
||||||
* Base class for lifecycle events.
|
* Base class for lifecycle events.
|
||||||
*
|
*
|
||||||
* Lifecycle events are fired at key points during application bootstrap.
|
* Lifecycle events are fired at key points during application bootstrap. Modules
|
||||||
* Modules listen to these events via static $listens arrays and register
|
* listen to these events via static `$listens` arrays in their Boot class and
|
||||||
* their resources (routes, views, navigation, etc.) through request methods.
|
* register their resources through the request methods provided here.
|
||||||
*
|
*
|
||||||
* Core collects all requests and processes them with validation, ensuring
|
* ## Request/Collect Pattern
|
||||||
* modules cannot directly mutate infrastructure.
|
*
|
||||||
|
* 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
|
abstract class LifecycleEvent
|
||||||
{
|
{
|
||||||
|
/** @var array<int, array<string, mixed>> Collected navigation item requests */
|
||||||
protected array $navigationRequests = [];
|
protected array $navigationRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, callable> Collected route registration callbacks */
|
||||||
protected array $routeRequests = [];
|
protected array $routeRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, array{0: string, 1: string}> Collected view namespace requests [namespace, path] */
|
||||||
protected array $viewRequests = [];
|
protected array $viewRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, array{0: string, 1: string}> Collected middleware alias requests [alias, class] */
|
||||||
protected array $middlewareRequests = [];
|
protected array $middlewareRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, array{0: string, 1: string}> Collected Livewire component requests [alias, class] */
|
||||||
protected array $livewireRequests = [];
|
protected array $livewireRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, string> Collected Artisan command class names */
|
||||||
protected array $commandRequests = [];
|
protected array $commandRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, array{0: string, 1: string}> Collected translation namespace requests [namespace, path] */
|
||||||
protected array $translationRequests = [];
|
protected array $translationRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, array{0: string, 1: string|null}> Collected Blade component path requests [path, namespace] */
|
||||||
protected array $bladeComponentRequests = [];
|
protected array $bladeComponentRequests = [];
|
||||||
|
|
||||||
|
/** @var array<int, array{0: string, 1: string}> Collected policy requests [model, policy] */
|
||||||
protected array $policyRequests = [];
|
protected array $policyRequests = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a navigation item be added.
|
* 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<string, mixed> $item Navigation item configuration
|
||||||
*/
|
*/
|
||||||
public function navigation(array $item): void
|
public function navigation(array $item): void
|
||||||
{
|
{
|
||||||
|
|
@ -50,6 +101,19 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request routes be registered.
|
* 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
|
public function routes(callable $callback): void
|
||||||
{
|
{
|
||||||
|
|
@ -58,6 +122,16 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a view namespace be registered.
|
* 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
|
public function views(string $namespace, string $path): void
|
||||||
{
|
{
|
||||||
|
|
@ -66,6 +140,9 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a middleware alias be registered.
|
* 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
|
public function middleware(string $alias, string $class): void
|
||||||
{
|
{
|
||||||
|
|
@ -74,6 +151,14 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a Livewire component be registered.
|
* Request a Livewire component be registered.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $event->livewire('commerce-cart', CartComponent::class);
|
||||||
|
* // Later: <livewire:commerce-cart />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @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
|
public function livewire(string $alias, string $class): void
|
||||||
{
|
{
|
||||||
|
|
@ -82,6 +167,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request an Artisan command be registered.
|
* 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
|
public function command(string $class): void
|
||||||
{
|
{
|
||||||
|
|
@ -90,6 +179,16 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request translations be loaded for a namespace.
|
* 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
|
public function translations(string $namespace, string $path): void
|
||||||
{
|
{
|
||||||
|
|
@ -98,6 +197,11 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request an anonymous Blade component path be registered.
|
* 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
|
public function bladeComponentPath(string $path, ?string $namespace = null): void
|
||||||
{
|
{
|
||||||
|
|
@ -106,6 +210,9 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a policy be registered for a model.
|
* 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
|
public function policy(string $model, string $policy): void
|
||||||
{
|
{
|
||||||
|
|
@ -114,6 +221,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all navigation requests for processing.
|
* Get all navigation requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function navigationRequests(): array
|
public function navigationRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -122,6 +233,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all route requests for processing.
|
* Get all route requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, callable>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function routeRequests(): array
|
public function routeRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -130,6 +245,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all view namespace requests for processing.
|
* Get all view namespace requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, array{0: string, 1: string}>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function viewRequests(): array
|
public function viewRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -138,6 +257,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all middleware alias requests for processing.
|
* Get all middleware alias requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, array{0: string, 1: string}>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function middlewareRequests(): array
|
public function middlewareRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -146,6 +269,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all Livewire component requests for processing.
|
* Get all Livewire component requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, array{0: string, 1: string}>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function livewireRequests(): array
|
public function livewireRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -154,6 +281,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all command requests for processing.
|
* Get all command requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function commandRequests(): array
|
public function commandRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -162,6 +293,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all translation requests for processing.
|
* Get all translation requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, array{0: string, 1: string}>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function translationRequests(): array
|
public function translationRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -170,6 +305,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all Blade component path requests for processing.
|
* Get all Blade component path requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, array{0: string, 1: string|null}>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function bladeComponentRequests(): array
|
public function bladeComponentRequests(): array
|
||||||
{
|
{
|
||||||
|
|
@ -178,6 +317,10 @@ abstract class LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all policy requests for processing.
|
* Get all policy requests for processing.
|
||||||
|
*
|
||||||
|
* @return array<int, array{0: string, 1: string}>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function policyRequests(): array
|
public function policyRequests(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,40 @@ namespace Core\Events;
|
||||||
/**
|
/**
|
||||||
* Fired when mail functionality is needed.
|
* Fired when mail functionality is needed.
|
||||||
*
|
*
|
||||||
* Modules listen to this event to register mail templates,
|
* Modules listen to this event to register mail templates, custom mailers,
|
||||||
* custom mailers, or mail-related services.
|
* 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
|
* ## When This Event Fires
|
||||||
* actually needs to be sent.
|
*
|
||||||
|
* 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
|
class MailSending extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
/** @var array<int, string> Collected mailable class names */
|
||||||
protected array $mailableRequests = [];
|
protected array $mailableRequests = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a mailable class.
|
* Register a mailable class.
|
||||||
|
*
|
||||||
|
* @param string $class Fully qualified mailable class name
|
||||||
*/
|
*/
|
||||||
public function mailable(string $class): void
|
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<int, string>
|
||||||
|
*
|
||||||
|
* @internal Used by mail system
|
||||||
*/
|
*/
|
||||||
public function mailableRequests(): array
|
public function mailableRequests(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,51 @@ namespace Core\Events;
|
||||||
use Core\Front\Mcp\Contracts\McpToolHandler;
|
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.
|
* Modules listen to this event to register their MCP tool handlers, which
|
||||||
* Each handler class must implement McpToolHandler interface.
|
* expose functionality to AI assistants and LLM-powered applications.
|
||||||
*
|
*
|
||||||
* Fired at MCP server startup (stdio transport) or when MCP routes
|
* ## When This Event Fires
|
||||||
* are accessed (HTTP transport).
|
*
|
||||||
|
* 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
|
class McpToolsRegistering extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
/** @var array<int, string> Collected MCP tool handler class names */
|
||||||
protected array $handlers = [];
|
protected array $handlers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an MCP tool handler class.
|
* 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
|
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<int, string>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function handlers(): array
|
public function handlers(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,47 @@ namespace Core\Events;
|
||||||
*
|
*
|
||||||
* Modules listen to this event to provide media handling capabilities
|
* Modules listen to this event to provide media handling capabilities
|
||||||
* such as image processing, video transcoding, CDN integration, etc.
|
* 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
|
class MediaRequested extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
/** @var array<string, string> Collected processor registrations [type => class] */
|
||||||
protected array $processorRequests = [];
|
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
|
public function processor(string $type, string $class): void
|
||||||
{
|
{
|
||||||
|
|
@ -32,6 +64,10 @@ class MediaRequested extends LifecycleEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all registered processors.
|
* Get all registered processors.
|
||||||
|
*
|
||||||
|
* @return array<string, string> [type => class]
|
||||||
|
*
|
||||||
|
* @internal Used by media system
|
||||||
*/
|
*/
|
||||||
public function processorRequests(): array
|
public function processorRequests(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,47 @@ namespace Core\Events;
|
||||||
/**
|
/**
|
||||||
* Fired when a queue worker is starting up.
|
* Fired when a queue worker is starting up.
|
||||||
*
|
*
|
||||||
* Modules listen to this event to register job classes or
|
* Modules listen to this event to perform queue-specific initialization or
|
||||||
* perform queue-specific initialisation.
|
* 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
|
class QueueWorkerBooting extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
/** @var array<int, string> Collected job class names */
|
||||||
protected array $jobRequests = [];
|
protected array $jobRequests = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a job class.
|
* Register a job class.
|
||||||
|
*
|
||||||
|
* @param string $class Fully qualified job class name
|
||||||
*/
|
*/
|
||||||
public function job(string $class): void
|
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<int, string>
|
||||||
|
*
|
||||||
|
* @internal Used by LifecycleEventProvider
|
||||||
*/
|
*/
|
||||||
public function jobRequests(): array
|
public function jobRequests(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,40 @@ namespace Core\Events;
|
||||||
/**
|
/**
|
||||||
* Fired when search functionality is requested.
|
* Fired when search functionality is requested.
|
||||||
*
|
*
|
||||||
* Modules listen to this event to register searchable models
|
* Modules listen to this event to register searchable models or search
|
||||||
* or search providers.
|
* 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
|
class SearchRequested extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
/** @var array<int, string> Collected searchable model class names */
|
||||||
protected array $searchableRequests = [];
|
protected array $searchableRequests = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a searchable model.
|
* Register a searchable model.
|
||||||
|
*
|
||||||
|
* @param string $model Fully qualified model class name
|
||||||
*/
|
*/
|
||||||
public function searchable(string $model): void
|
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<int, string>
|
||||||
|
*
|
||||||
|
* @internal Used by search system
|
||||||
*/
|
*/
|
||||||
public function searchableRequests(): array
|
public function searchableRequests(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,46 @@ declare(strict_types=1);
|
||||||
namespace Core\Events;
|
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.
|
* Modules listen to this event to register public-facing web routes such as
|
||||||
* Routes are automatically wrapped with the 'web' middleware group.
|
* marketing pages, product listings, or any routes accessible without authentication.
|
||||||
*
|
*
|
||||||
* Use this for marketing pages, public product pages, etc.
|
* ## When This Event Fires
|
||||||
* For authenticated dashboard routes, use AdminPanelBooting instead.
|
*
|
||||||
|
* 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
|
class WebRoutesRegistering extends LifecycleEvent
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,6 @@ namespace Core\Front\Admin;
|
||||||
|
|
||||||
use Core\Front\Admin\Contracts\AdminMenuProvider;
|
use Core\Front\Admin\Contracts\AdminMenuProvider;
|
||||||
use Core\Front\Admin\Contracts\DynamicMenuProvider;
|
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;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,9 +89,19 @@ class AdminMenuRegistry
|
||||||
*/
|
*/
|
||||||
protected int $cacheTtl;
|
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->cacheTtl = (int) config('core.admin_menu.cache_ttl', self::DEFAULT_CACHE_TTL);
|
||||||
$this->cachingEnabled = (bool) config('core.admin_menu.cache_enabled', true);
|
$this->cachingEnabled = (bool) config('core.admin_menu.cache_enabled', true);
|
||||||
}
|
}
|
||||||
|
|
@ -139,12 +146,12 @@ class AdminMenuRegistry
|
||||||
/**
|
/**
|
||||||
* Build the complete menu structure.
|
* 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 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<int, array>
|
* @return array<int, 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)
|
// Get static items (potentially cached)
|
||||||
$staticItems = $this->getStaticItems($workspace, $isAdmin, $user);
|
$staticItems = $this->getStaticItems($workspace, $isAdmin, $user);
|
||||||
|
|
@ -162,9 +169,12 @@ class AdminMenuRegistry
|
||||||
/**
|
/**
|
||||||
* Get static menu items, using cache if enabled.
|
* 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<string, array<int, array{priority: int, item: \Closure}>>
|
* @return array<string, array<int, array{priority: int, item: \Closure}>>
|
||||||
*/
|
*/
|
||||||
protected function getStaticItems(?Workspace $workspace, bool $isAdmin, ?User $user): array
|
protected function getStaticItems(?object $workspace, bool $isAdmin, ?object $user): array
|
||||||
{
|
{
|
||||||
if (! $this->cachingEnabled) {
|
if (! $this->cachingEnabled) {
|
||||||
return $this->collectItems($workspace, $isAdmin, $user);
|
return $this->collectItems($workspace, $isAdmin, $user);
|
||||||
|
|
@ -180,9 +190,12 @@ class AdminMenuRegistry
|
||||||
/**
|
/**
|
||||||
* Get dynamic menu items from dynamic providers.
|
* 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<string, array<int, array{priority: int, item: \Closure}>>
|
* @return array<string, array<int, array{priority: int, item: \Closure}>>
|
||||||
*/
|
*/
|
||||||
protected function getDynamicItems(?Workspace $workspace, bool $isAdmin, ?User $user): array
|
protected function getDynamicItems(?object $workspace, bool $isAdmin, ?object $user): array
|
||||||
{
|
{
|
||||||
$grouped = [];
|
$grouped = [];
|
||||||
|
|
||||||
|
|
@ -201,7 +214,7 @@ class AdminMenuRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if entitlement check fails
|
// Skip if entitlement check fails
|
||||||
if ($entitlement && $workspace) {
|
if ($entitlement && $workspace && $this->entitlements !== null) {
|
||||||
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -249,8 +262,13 @@ class AdminMenuRegistry
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the final menu structure from collected items.
|
* 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
|
// Build flat structure with dividers
|
||||||
$menu = [];
|
$menu = [];
|
||||||
|
|
@ -343,8 +361,13 @@ class AdminMenuRegistry
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the cache key for menu items.
|
* 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 = [
|
$parts = [
|
||||||
self::CACHE_PREFIX,
|
self::CACHE_PREFIX,
|
||||||
|
|
@ -367,9 +390,12 @@ class AdminMenuRegistry
|
||||||
/**
|
/**
|
||||||
* Collect items from all providers, filtering by entitlements and permissions.
|
* 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<string, array<int, array{priority: int, item: \Closure}>>
|
* @return array<string, array<int, array{priority: int, item: \Closure}>>
|
||||||
*/
|
*/
|
||||||
protected function collectItems(?Workspace $workspace, bool $isAdmin, ?User $user): array
|
protected function collectItems(?object $workspace, bool $isAdmin, ?object $user): array
|
||||||
{
|
{
|
||||||
$grouped = [];
|
$grouped = [];
|
||||||
|
|
||||||
|
|
@ -391,7 +417,7 @@ class AdminMenuRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if entitlement check fails
|
// Skip if entitlement check fails
|
||||||
if ($entitlement && $workspace) {
|
if ($entitlement && $workspace && $this->entitlements !== null) {
|
||||||
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -420,12 +446,12 @@ class AdminMenuRegistry
|
||||||
/**
|
/**
|
||||||
* Check if a user has all required permissions.
|
* Check if a user has all required permissions.
|
||||||
*
|
*
|
||||||
* @param User|null $user
|
* @param object|null $user User model instance
|
||||||
* @param array<string> $permissions
|
* @param array<string> $permissions
|
||||||
* @param Workspace|null $workspace
|
* @param object|null $workspace Workspace model instance
|
||||||
* @return bool
|
* @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)) {
|
if (empty($permissions)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -448,10 +474,10 @@ class AdminMenuRegistry
|
||||||
/**
|
/**
|
||||||
* Invalidate cached menu for a specific context.
|
* Invalidate cached menu for a specific context.
|
||||||
*
|
*
|
||||||
* @param Workspace|null $workspace
|
* @param object|null $workspace Workspace model instance
|
||||||
* @param User|null $user
|
* @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) {
|
if ($workspace !== null && $user !== null) {
|
||||||
// Invalidate specific cache keys
|
// Invalidate specific cache keys
|
||||||
|
|
@ -469,8 +495,10 @@ class AdminMenuRegistry
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate all cached menus for a workspace.
|
* 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,
|
// We can't easily clear pattern-based cache keys with all drivers,
|
||||||
// so we rely on TTL expiration for non-tagged caches
|
// so we rely on TTL expiration for non-tagged caches
|
||||||
|
|
@ -481,8 +509,10 @@ class AdminMenuRegistry
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate all cached menus for a user.
|
* 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')) {
|
if (method_exists(Cache::getStore(), 'tags')) {
|
||||||
Cache::tags([self::CACHE_PREFIX, 'user:' . $user->id])->flush();
|
Cache::tags([self::CACHE_PREFIX, 'user:' . $user->id])->flush();
|
||||||
|
|
@ -512,12 +542,12 @@ class AdminMenuRegistry
|
||||||
/**
|
/**
|
||||||
* Get all service menu items indexed by service key.
|
* 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 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<string, array> Service items indexed by service key
|
* @return array<string, 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 = [];
|
$services = [];
|
||||||
|
|
||||||
|
|
@ -547,7 +577,7 @@ class AdminMenuRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if entitlement check fails
|
// Skip if entitlement check fails
|
||||||
if ($entitlement && $workspace) {
|
if ($entitlement && $workspace && $this->entitlements !== null) {
|
||||||
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -578,12 +608,12 @@ class AdminMenuRegistry
|
||||||
* Get a specific service's menu item including its children (tabs).
|
* Get a specific service's menu item including its children (tabs).
|
||||||
*
|
*
|
||||||
* @param string $serviceKey The service identifier (e.g., 'commerce', 'support')
|
* @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 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
|
* @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) {
|
foreach ($this->providers as $provider) {
|
||||||
// Check provider-level permissions
|
// Check provider-level permissions
|
||||||
|
|
@ -611,7 +641,7 @@ class AdminMenuRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if entitlement check fails
|
// Skip if entitlement check fails
|
||||||
if ($entitlement && $workspace) {
|
if ($entitlement && $workspace && $this->entitlements !== null) {
|
||||||
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
if ($this->entitlements->can($workspace, $entitlement)->isDenied()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Front\Admin\Concerns;
|
namespace Core\Front\Admin\Concerns;
|
||||||
|
|
||||||
use Core\Mod\Tenant\Models\User;
|
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides default permission handling for AdminMenuProvider implementations.
|
* Provides default permission handling for AdminMenuProvider implementations.
|
||||||
*
|
*
|
||||||
|
|
@ -40,11 +37,11 @@ trait HasMenuPermissions
|
||||||
* By default, checks that the user has all permissions returned by
|
* By default, checks that the user has all permissions returned by
|
||||||
* menuPermissions(). Override for custom logic.
|
* menuPermissions(). Override for custom logic.
|
||||||
*
|
*
|
||||||
* @param User|null $user The authenticated user
|
* @param object|null $user The authenticated user (User model instance)
|
||||||
* @param Workspace|null $workspace The current workspace context
|
* @param object|null $workspace The current workspace context (Workspace model instance)
|
||||||
* @return bool
|
* @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)
|
// No user means no permission (unless we have no requirements)
|
||||||
$permissions = $this->menuPermissions();
|
$permissions = $this->menuPermissions();
|
||||||
|
|
@ -73,12 +70,12 @@ trait HasMenuPermissions
|
||||||
* Override this method to customise how permission checks are performed.
|
* Override this method to customise how permission checks are performed.
|
||||||
* By default, uses Laravel's Gate/Authorization system.
|
* By default, uses Laravel's Gate/Authorization system.
|
||||||
*
|
*
|
||||||
* @param User $user
|
* @param object $user User model instance
|
||||||
* @param string $permission
|
* @param string $permission
|
||||||
* @param Workspace|null $workspace
|
* @param object|null $workspace Workspace model instance
|
||||||
* @return bool
|
* @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
|
// Check using Laravel's authorization
|
||||||
if (method_exists($user, 'can')) {
|
if (method_exists($user, 'can')) {
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,39 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Front\Admin\Contracts;
|
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.
|
* Interface for modules that provide admin menu items.
|
||||||
*
|
*
|
||||||
* Modules implement this interface and register themselves with AdminMenuRegistry
|
* Modules implement this interface to contribute navigation items to the admin
|
||||||
* during boot. The registry collects all items and builds the final menu structure.
|
* 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
|
interface AdminMenuProvider
|
||||||
{
|
{
|
||||||
|
|
@ -81,9 +106,9 @@ interface AdminMenuProvider
|
||||||
* Override this method to implement custom permission logic beyond
|
* Override this method to implement custom permission logic beyond
|
||||||
* simple permission key checks.
|
* simple permission key checks.
|
||||||
*
|
*
|
||||||
* @param User|null $user The authenticated user
|
* @param object|null $user The authenticated user (User model instance)
|
||||||
* @param Workspace|null $workspace The current workspace context
|
* @param object|null $workspace The current workspace context (Workspace model instance)
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function canViewMenu(?User $user, ?Workspace $workspace): bool;
|
public function canViewMenu(?object $user, ?object $workspace): bool;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,38 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Front\Admin\Contracts;
|
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,
|
* Dynamic menu items are computed at runtime based on context and are never
|
||||||
* database state, etc.) and are never cached. Use this interface when menu items
|
* cached. Use this interface when menu items need to reflect real-time data
|
||||||
* need to reflect real-time data such as notification counts, recent items, or
|
* that changes frequently or per-request.
|
||||||
* user-specific content.
|
|
||||||
*
|
*
|
||||||
* Classes implementing this interface are processed separately from static
|
* ## When to Use DynamicMenuProvider
|
||||||
* AdminMenuProvider items - their results are merged after cache retrieval.
|
*
|
||||||
|
* - **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
|
interface DynamicMenuProvider
|
||||||
{
|
{
|
||||||
|
|
@ -35,8 +54,8 @@ interface DynamicMenuProvider
|
||||||
* Each item should include the same structure as AdminMenuProvider::adminMenuItems()
|
* Each item should include the same structure as AdminMenuProvider::adminMenuItems()
|
||||||
* plus an optional 'dynamic' key set to true for identification.
|
* plus an optional 'dynamic' key set to true for identification.
|
||||||
*
|
*
|
||||||
* @param User|null $user The authenticated user
|
* @param object|null $user The authenticated user (User model instance)
|
||||||
* @param Workspace|null $workspace The current workspace context
|
* @param object|null $workspace The current workspace context (Workspace model instance)
|
||||||
* @param bool $isAdmin Whether the user is an admin
|
* @param bool $isAdmin Whether the user is an admin
|
||||||
* @return array<int, array{
|
* @return array<int, array{
|
||||||
* group: string,
|
* group: string,
|
||||||
|
|
@ -48,7 +67,7 @@ interface DynamicMenuProvider
|
||||||
* item: \Closure
|
* item: \Closure
|
||||||
* }>
|
* }>
|
||||||
*/
|
*/
|
||||||
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.
|
* 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
|
* this key changes. Return null if dynamic items should never affect
|
||||||
* cache invalidation.
|
* cache invalidation.
|
||||||
*
|
*
|
||||||
* @param User|null $user
|
* @param object|null $user User model instance
|
||||||
* @param Workspace|null $workspace
|
* @param object|null $workspace Workspace model instance
|
||||||
* @return string|null
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
public function dynamicCacheKey(?User $user, ?Workspace $workspace): ?string;
|
public function dynamicCacheKey(?object $user, ?object $workspace): ?string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ namespace Core\Front\Admin\View\Components;
|
||||||
use Core\Front\Admin\AdminMenuRegistry;
|
use Core\Front\Admin\AdminMenuRegistry;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\View\Component;
|
use Illuminate\View\Component;
|
||||||
use Core\Mod\Tenant\Models\User;
|
|
||||||
use Core\Mod\Tenant\Services\WorkspaceService;
|
|
||||||
|
|
||||||
class Sidemenu extends Component
|
class Sidemenu extends Component
|
||||||
{
|
{
|
||||||
|
|
@ -37,9 +35,17 @@ class Sidemenu extends Component
|
||||||
protected function buildFromRegistry(): array
|
protected function buildFromRegistry(): array
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Use current workspace from session, not default
|
// Use current workspace from session, not default
|
||||||
$workspace = app(WorkspaceService::class)->currentModel();
|
$workspace = null;
|
||||||
$isAdmin = $user instanceof User && $user->isHades();
|
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);
|
return app(AdminMenuRegistry::class)->build($workspace, $isAdmin);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||||
namespace Core\Front\Mcp;
|
namespace Core\Front\Mcp;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Core\Mod\Agentic\Models\AgentPlan;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context object passed to MCP tool handlers.
|
* Context object passed to MCP tool handlers.
|
||||||
|
|
@ -27,9 +26,12 @@ use Core\Mod\Agentic\Models\AgentPlan;
|
||||||
*/
|
*/
|
||||||
class McpContext
|
class McpContext
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param object|null $currentPlan AgentPlan model instance when Agentic module installed
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ?string $sessionId = null,
|
private ?string $sessionId = null,
|
||||||
private ?AgentPlan $currentPlan = null,
|
private ?object $currentPlan = null,
|
||||||
private ?Closure $notificationCallback = null,
|
private ?Closure $notificationCallback = null,
|
||||||
private ?Closure $logCallback = null,
|
private ?Closure $logCallback = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -52,16 +54,20 @@ class McpContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current plan if one is active.
|
* 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;
|
return $this->currentPlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current plan.
|
* 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;
|
$this->currentPlan = $plan;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ namespace Core\Front\Web\Middleware;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,11 +75,20 @@ class FindDomainRecord
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve workspace from the domain.
|
* 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
|
// Check for custom domain first
|
||||||
$workspace = Workspace::where('domain', $host)->first();
|
$workspace = \Core\Mod\Tenant\Models\Workspace::where('domain', $host)->first();
|
||||||
if ($workspace) {
|
if ($workspace) {
|
||||||
return $workspace;
|
return $workspace;
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +103,7 @@ class FindDomainRecord
|
||||||
if (count($parts) >= 1) {
|
if (count($parts) >= 1) {
|
||||||
$workspaceSlug = $parts[0];
|
$workspaceSlug = $parts[0];
|
||||||
|
|
||||||
return Workspace::where('slug', $workspaceSlug)
|
return \Core\Mod\Tenant\Models\Workspace::where('slug', $workspaceSlug)
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Helpers;
|
namespace Core\Helpers;
|
||||||
|
|
||||||
use Core\Mod\Social\Enums\ServiceGroup;
|
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,10 +32,12 @@ class ServiceCollection
|
||||||
/**
|
/**
|
||||||
* Filter services by group (social, AI, media, miscellaneous).
|
* Filter services by group (social, AI, media, miscellaneous).
|
||||||
*
|
*
|
||||||
* @param ServiceGroup|array<int, ServiceGroup>|null $group Service group(s) to filter by
|
* Requires Core\Mod\Social module to be installed for ServiceGroup enum.
|
||||||
|
*
|
||||||
|
* @param object|array<int, object>|null $group Service group(s) to filter by (ServiceGroup enum)
|
||||||
* @return static New collection containing only services in the specified group(s)
|
* @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(
|
return new static(
|
||||||
array_values(
|
array_values(
|
||||||
|
|
@ -75,7 +76,7 @@ class ServiceCollection
|
||||||
* - group: The service group enum (social, AI, media, miscellaneous)
|
* - group: The service group enum (social, AI, media, miscellaneous)
|
||||||
* - form: The form configuration array for the service
|
* - form: The form configuration array for the service
|
||||||
*
|
*
|
||||||
* @return array<int, array{name: string, group: ServiceGroup, form: array}>
|
* @return array<int, array{name: string, group: object, form: array}>
|
||||||
*/
|
*/
|
||||||
public function getCollection(): array
|
public function getCollection(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,69 @@ use Core\Events\EventAuditLog;
|
||||||
use Illuminate\Support\ServiceProvider;
|
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,
|
* LazyModuleListener is the key to the framework's lazy loading strategy. Instead of
|
||||||
* enabling lazy loading of modules based on actual usage.
|
* 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.
|
* ## How Lazy Loading Works
|
||||||
* Integrates with EventAuditLog for debugging and monitoring.
|
|
||||||
*
|
*
|
||||||
* Usage:
|
* 1. During registration, a LazyModuleListener wraps each module class name and method
|
||||||
* Event::listen(
|
* 2. The listener is registered with Laravel's event system
|
||||||
* AdminPanelBooting::class,
|
* 3. When an event fires, `__invoke()` is called
|
||||||
* new LazyModuleListener(Commerce\Boot::class, 'registerAdmin')
|
* 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
|
class LazyModuleListener
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Cached module instance (created on first event).
|
||||||
|
*/
|
||||||
private ?object $instance = null;
|
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(
|
public function __construct(
|
||||||
private string $moduleClass,
|
private string $moduleClass,
|
||||||
private string $method
|
private string $method
|
||||||
|
|
@ -40,8 +85,15 @@ class LazyModuleListener
|
||||||
/**
|
/**
|
||||||
* Handle the event by instantiating the module and calling its method.
|
* Handle the event by instantiating the module and calling its method.
|
||||||
*
|
*
|
||||||
* This is the callable interface for Laravel's event dispatcher.
|
* This is the callable interface for Laravel's event dispatcher. The module
|
||||||
* Records execution to EventAuditLog when enabled.
|
* 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
|
public function __invoke(object $event): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,29 +23,83 @@ use Illuminate\Support\ServiceProvider;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages lifecycle events for lazy module loading.
|
* Orchestrates lifecycle events for lazy module loading.
|
||||||
*
|
*
|
||||||
* This provider:
|
* The LifecycleEventProvider is the entry point for the event-driven module system.
|
||||||
* 1. Scans modules for $listens declarations during register()
|
* It coordinates module discovery, listener registration, and event firing at
|
||||||
* 2. Wires up lazy listeners for each event-module pair
|
* appropriate points during the application lifecycle.
|
||||||
* 3. Fires lifecycle events at appropriate times during boot()
|
|
||||||
*
|
*
|
||||||
* 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 = [
|
* public static array $listens = [
|
||||||
* WebRoutesRegistering::class => 'onWebRoutes',
|
* WebRoutesRegistering::class => 'onWebRoutes',
|
||||||
* AdminPanelBooting::class => 'onAdmin',
|
* 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
|
class LifecycleEventProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Directories to scan for modules with $listens declarations.
|
* Directories to scan for modules with $listens declarations.
|
||||||
|
*
|
||||||
|
* @var array<string>
|
||||||
*/
|
*/
|
||||||
protected array $scanPaths = [];
|
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
|
public function register(): void
|
||||||
{
|
{
|
||||||
// Register infrastructure
|
// Register infrastructure
|
||||||
|
|
@ -66,6 +120,15 @@ class LifecycleEventProvider extends ServiceProvider
|
||||||
$registry->register($this->scanPaths);
|
$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
|
public function boot(): void
|
||||||
{
|
{
|
||||||
// Console event now fired by Core\Front\Cli\Boot
|
// 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
|
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
|
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
|
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
|
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<string> Handler class names
|
* @return array<string> Fully qualified class names of McpToolHandler implementations
|
||||||
|
*
|
||||||
|
* @see \Core\Front\Mcp\Contracts\McpToolHandler
|
||||||
*/
|
*/
|
||||||
public static function fireMcpTools(): array
|
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
|
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
|
protected function fireQueueWorkerBooting(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,23 +13,81 @@ namespace Core;
|
||||||
use Illuminate\Support\Facades\Event;
|
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,
|
* The ModuleRegistry is the central coordinator for the event-driven module loading
|
||||||
* and wires up lazy listeners for each event-module pair.
|
* 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:
|
* 1. `register()` is called with paths to scan (typically in a ServiceProvider)
|
||||||
* $registry = new ModuleRegistry(new ModuleScanner());
|
* 2. ModuleScanner discovers all Boot classes with `$listens` declarations
|
||||||
* $registry->register([app_path('Core'), app_path('Mod')]);
|
* 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
|
class ModuleRegistry
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Event-to-module mappings discovered by the scanner.
|
||||||
|
*
|
||||||
|
* Structure: [EventClass => [ModuleClass => ['method' => string, 'priority' => int]]]
|
||||||
|
*
|
||||||
|
* @var array<string, array<string, array{method: string, priority: int}>>
|
||||||
|
*/
|
||||||
private array $mappings = [];
|
private array $mappings = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether initial registration has been performed.
|
||||||
|
*/
|
||||||
private bool $registered = false;
|
private bool $registered = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ModuleRegistry instance.
|
||||||
|
*
|
||||||
|
* @param ModuleScanner $scanner The scanner used to discover module listeners
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ModuleScanner $scanner
|
private ModuleScanner $scanner
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,54 @@ use ReflectionClass;
|
||||||
/**
|
/**
|
||||||
* Scans module Boot.php files for event listener declarations.
|
* Scans module Boot.php files for event listener declarations.
|
||||||
*
|
*
|
||||||
* Reads the static $listens property from Boot classes without
|
* The ModuleScanner is responsible for discovering modules that wish to participate
|
||||||
* instantiating them, enabling lazy loading of modules.
|
* 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 = [
|
* public static array $listens = [
|
||||||
* WebRoutesRegistering::class => 'onWebRoutes', // Default priority 0
|
* WebRoutesRegistering::class => 'onWebRoutes',
|
||||||
* AdminPanelBooting::class => ['onAdmin', 10], // Priority 10 (higher = runs first)
|
* AdminPanelBooting::class => ['onAdmin', 10], // With priority
|
||||||
* ];
|
* ];
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* Usage:
|
* ## Priority System
|
||||||
* $scanner = new ModuleScanner();
|
*
|
||||||
* $mappings = $scanner->scan([app_path('Core'), app_path('Mod')]);
|
* Listeners can optionally specify a priority (default: 0). Higher priority values
|
||||||
* // Returns: [EventClass => [ModuleClass => ['method' => 'name', 'priority' => 0]]]
|
* 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
|
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
|
* Maps file paths to PSR-4 namespaces based on directory structure:
|
||||||
* Converts: app/Core/Cdn/Boot.php → Core\Cdn\Boot
|
*
|
||||||
|
* - `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
|
private function classFromFile(string $file, string $basePath): ?string
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Search;
|
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\Collection;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -253,14 +249,14 @@ class Unified
|
||||||
*/
|
*/
|
||||||
protected function searchPatterns(string $query): Collection
|
protected function searchPatterns(string $query): Collection
|
||||||
{
|
{
|
||||||
if (! class_exists(Pattern::class)) {
|
if (! class_exists(\Core\Mod\Uptelligence\Models\Pattern::class)) {
|
||||||
return collect();
|
return collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
$escaped = $this->escapeLikeQuery($query);
|
$escaped = $this->escapeLikeQuery($query);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Pattern::where('name', 'like', "%{$escaped}%")
|
return \Core\Mod\Uptelligence\Models\Pattern::where('name', 'like', "%{$escaped}%")
|
||||||
->orWhere('description', 'like', "%{$escaped}%")
|
->orWhere('description', 'like', "%{$escaped}%")
|
||||||
->orWhere('category', 'like', "%{$escaped}%")
|
->orWhere('category', 'like', "%{$escaped}%")
|
||||||
->limit(20)
|
->limit(20)
|
||||||
|
|
@ -291,14 +287,14 @@ class Unified
|
||||||
*/
|
*/
|
||||||
protected function searchAssets(string $query): Collection
|
protected function searchAssets(string $query): Collection
|
||||||
{
|
{
|
||||||
if (! class_exists(Asset::class)) {
|
if (! class_exists(\Core\Mod\Uptelligence\Models\Asset::class)) {
|
||||||
return collect();
|
return collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
$escaped = $this->escapeLikeQuery($query);
|
$escaped = $this->escapeLikeQuery($query);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Asset::where('name', 'like', "%{$escaped}%")
|
return \Core\Mod\Uptelligence\Models\Asset::where('name', 'like', "%{$escaped}%")
|
||||||
->orWhere('slug', 'like', "%{$escaped}%")
|
->orWhere('slug', 'like', "%{$escaped}%")
|
||||||
->orWhere('description', 'like', "%{$escaped}%")
|
->orWhere('description', 'like', "%{$escaped}%")
|
||||||
->limit(20)
|
->limit(20)
|
||||||
|
|
@ -330,14 +326,14 @@ class Unified
|
||||||
*/
|
*/
|
||||||
protected function searchTodos(string $query): Collection
|
protected function searchTodos(string $query): Collection
|
||||||
{
|
{
|
||||||
if (! class_exists(UpstreamTodo::class)) {
|
if (! class_exists(\Core\Mod\Uptelligence\Models\UpstreamTodo::class)) {
|
||||||
return collect();
|
return collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
$escaped = $this->escapeLikeQuery($query);
|
$escaped = $this->escapeLikeQuery($query);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return UpstreamTodo::where('title', 'like', "%{$escaped}%")
|
return \Core\Mod\Uptelligence\Models\UpstreamTodo::where('title', 'like', "%{$escaped}%")
|
||||||
->orWhere('description', 'like', "%{$escaped}%")
|
->orWhere('description', 'like', "%{$escaped}%")
|
||||||
->limit(20)
|
->limit(20)
|
||||||
->get()
|
->get()
|
||||||
|
|
@ -368,14 +364,14 @@ class Unified
|
||||||
*/
|
*/
|
||||||
protected function searchPlans(string $query): Collection
|
protected function searchPlans(string $query): Collection
|
||||||
{
|
{
|
||||||
if (! class_exists(AgentPlan::class)) {
|
if (! class_exists(\Core\Mod\Agentic\Models\AgentPlan::class)) {
|
||||||
return collect();
|
return collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
$escaped = $this->escapeLikeQuery($query);
|
$escaped = $this->escapeLikeQuery($query);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return AgentPlan::where('title', 'like', "%{$escaped}%")
|
return \Core\Mod\Agentic\Models\AgentPlan::where('title', 'like', "%{$escaped}%")
|
||||||
->orWhere('slug', 'like', "%{$escaped}%")
|
->orWhere('slug', 'like', "%{$escaped}%")
|
||||||
->orWhere('description', 'like', "%{$escaped}%")
|
->orWhere('description', 'like', "%{$escaped}%")
|
||||||
->limit(20)
|
->limit(20)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Seo\Jobs;
|
namespace Core\Seo\Jobs;
|
||||||
|
|
||||||
use Core\Mod\Web\Models\Page;
|
|
||||||
use Core\Mod\Web\Services\DynamicOgImageService;
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
|
@ -59,10 +57,26 @@ class GenerateOgImageJob implements ShouldQueue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* 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) {
|
if (! $biolink) {
|
||||||
Log::warning("OG image generation skipped: biolink {$this->biolinkId} not found");
|
Log::warning("OG image generation skipped: biolink {$this->biolinkId} not found");
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Seo;
|
namespace Core\Seo;
|
||||||
|
|
||||||
use Core\Mod\Content\Models\ContentItem;
|
|
||||||
use Core\Seo\Validation\SchemaValidator;
|
use Core\Seo\Validation\SchemaValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,8 +61,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate complete JSON-LD schema for a content item.
|
* 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 = [];
|
$graph = [];
|
||||||
|
|
||||||
|
|
@ -94,8 +95,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate article 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';
|
$type = $options['type'] ?? 'TechArticle';
|
||||||
$wordCount = str_word_count(strip_tags($item->display_content ?? ''));
|
$wordCount = str_word_count(strip_tags($item->display_content ?? ''));
|
||||||
|
|
@ -161,8 +164,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate HowTo schema from article content.
|
* 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);
|
$steps = $this->extractSteps($item);
|
||||||
|
|
||||||
|
|
@ -214,8 +219,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate breadcrumb 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;
|
$workspace = $item->workspace;
|
||||||
$domain = $workspace?->domain ?? $this->baseDomain();
|
$domain = $workspace?->domain ?? $this->baseDomain();
|
||||||
|
|
@ -295,8 +302,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if article content contains numbered steps.
|
* 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 ?? '';
|
$content = $item->display_content ?? '';
|
||||||
|
|
||||||
|
|
@ -306,8 +315,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract steps from article content.
|
* 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 ?? '';
|
$content = $item->display_content ?? '';
|
||||||
$steps = [];
|
$steps = [];
|
||||||
|
|
@ -340,8 +351,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract FAQ from article content.
|
* 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 ?? '';
|
$content = $item->display_content ?? '';
|
||||||
$faqs = [];
|
$faqs = [];
|
||||||
|
|
@ -367,8 +380,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full URL for an article.
|
* 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;
|
$workspace = $item->workspace;
|
||||||
$domain = $workspace?->domain ?? $this->baseDomain();
|
$domain = $workspace?->domain ?? $this->baseDomain();
|
||||||
|
|
@ -382,8 +397,10 @@ class Schema
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an excerpt from content.
|
* 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 = strip_tags($item->display_content ?? '');
|
||||||
$content = preg_replace('/\s+/', ' ', $content);
|
$content = preg_replace('/\s+/', ' ', $content);
|
||||||
|
|
@ -421,9 +438,11 @@ class Schema
|
||||||
/**
|
/**
|
||||||
* Generate schema with validation.
|
* Generate schema with validation.
|
||||||
*
|
*
|
||||||
|
* @param object $item Content item model instance (expects ContentItem-like interface)
|
||||||
|
*
|
||||||
* @throws \InvalidArgumentException if schema validation fails
|
* @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);
|
$schema = $this->generateSchema($item, $options);
|
||||||
$result = $this->validate($schema);
|
$result = $this->validate($schema);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Core\Seo\Services;
|
namespace Core\Seo\Services;
|
||||||
|
|
||||||
use Core\Mod\Content\Models\ContentItem;
|
|
||||||
use Core\Mod\Tenant\Models\Workspace;
|
|
||||||
use Core\Seo\Validation\SchemaValidator;
|
use Core\Seo\Validation\SchemaValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,8 +29,10 @@ class SchemaBuilderService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Build Article schema for a content item.
|
* 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 [
|
return [
|
||||||
'@context' => 'https://schema.org',
|
'@context' => 'https://schema.org',
|
||||||
|
|
@ -55,8 +55,10 @@ class SchemaBuilderService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build BlogPosting schema (more specific than Article).
|
* 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 = $this->buildArticleSchema($item);
|
||||||
$schema['@type'] = 'BlogPosting';
|
$schema['@type'] = 'BlogPosting';
|
||||||
|
|
@ -73,8 +75,10 @@ class SchemaBuilderService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build HowTo schema for instructional content.
|
* 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 [
|
return [
|
||||||
'@context' => 'https://schema.org',
|
'@context' => 'https://schema.org',
|
||||||
|
|
@ -130,13 +134,15 @@ class SchemaBuilderService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build Organization schema.
|
* 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 [
|
return [
|
||||||
'@type' => 'Organization',
|
'@type' => 'Organization',
|
||||||
'name' => $workspace?->name ?? 'Host UK',
|
'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' => [
|
'logo' => [
|
||||||
'@type' => 'ImageObject',
|
'@type' => 'ImageObject',
|
||||||
'url' => 'https://host.uk.com/images/logo.png',
|
'url' => 'https://host.uk.com/images/logo.png',
|
||||||
|
|
@ -146,8 +152,10 @@ class SchemaBuilderService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build WebSite schema.
|
* 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 [
|
return [
|
||||||
'@context' => 'https://schema.org',
|
'@context' => 'https://schema.org',
|
||||||
|
|
@ -243,8 +251,10 @@ class SchemaBuilderService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the canonical URL for a content item.
|
* 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';
|
$domain = $item->workspace?->domain ?? 'host.uk.com';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,30 @@ use Core\Service\HealthCheckResult;
|
||||||
/**
|
/**
|
||||||
* Contract for services that provide health checks.
|
* Contract for services that provide health checks.
|
||||||
*
|
*
|
||||||
* Services implementing this interface can report their operational
|
* Services implementing this interface can report their operational status
|
||||||
* status for monitoring, load balancing, and alerting purposes.
|
* for monitoring, load balancing, and alerting purposes. Health endpoints
|
||||||
|
* can aggregate results from all registered HealthCheckable services.
|
||||||
|
*
|
||||||
|
* ## Health Check Guidelines
|
||||||
*
|
*
|
||||||
* Health checks should be:
|
* 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
|
* ```php
|
||||||
* public function healthCheck(): HealthCheckResult
|
* public function healthCheck(): HealthCheckResult
|
||||||
* {
|
* {
|
||||||
|
|
@ -48,6 +63,10 @@ use Core\Service\HealthCheckResult;
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* @package Core\Service\Contracts
|
||||||
|
*
|
||||||
|
* @see HealthCheckResult For result factory methods
|
||||||
*/
|
*/
|
||||||
interface HealthCheckable
|
interface HealthCheckable
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,41 +14,59 @@ use Core\Front\Admin\Contracts\AdminMenuProvider;
|
||||||
use Core\Service\ServiceVersion;
|
use Core\Service\ServiceVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contract for service definitions.
|
* Contract for SaaS service definitions.
|
||||||
*
|
*
|
||||||
* Services are the product layer - they define how modules are presented
|
* Services are the product layer of the framework - they define how modules are
|
||||||
* to users as SaaS products. Each service has a definition used to populate
|
* presented to users as SaaS products. Each service has a definition that:
|
||||||
* the platform_services table and admin menu registration.
|
|
||||||
*
|
*
|
||||||
* 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
|
* ## Versioning
|
||||||
*
|
*
|
||||||
* Services should implement the version() method to declare their contract
|
* Services should implement `version()` to declare their contract version.
|
||||||
* version. This enables:
|
* This enables tracking breaking changes and deprecation:
|
||||||
* - Tracking breaking changes in service contracts
|
|
||||||
* - Deprecation warnings before removing features
|
|
||||||
* - Sunset date enforcement for deprecated versions
|
|
||||||
*
|
*
|
||||||
* Example:
|
|
||||||
* ```php
|
* ```php
|
||||||
* public static function version(): ServiceVersion
|
* public static function version(): ServiceVersion
|
||||||
* {
|
* {
|
||||||
* return new ServiceVersion(2, 1, 0);
|
* return new ServiceVersion(2, 1, 0);
|
||||||
* }
|
* }
|
||||||
* ```
|
|
||||||
*
|
*
|
||||||
* For deprecated services:
|
* // For deprecated services:
|
||||||
* ```php
|
|
||||||
* public static function version(): ServiceVersion
|
* public static function version(): ServiceVersion
|
||||||
* {
|
* {
|
||||||
* return (new ServiceVersion(1, 0, 0))
|
* return (new ServiceVersion(1, 0, 0))
|
||||||
* ->deprecate(
|
* ->deprecate('Use ServiceV2 instead', new \DateTimeImmutable('2025-06-01'));
|
||||||
* '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
|
interface ServiceDefinition extends AdminMenuProvider
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue