php-framework/changelog/2026/jan/TASK-event-driven-module-loading.md
Snider afc03418eb docs: add changelog entries for Jan 2026
- Core package extraction plan (Flux Pro/Free, FontAwesome fallbacks)
- Event-driven module loading task doc (complete)
- In-app browser detection documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:27:42 +00:00

26 KiB

TASK: Event-Driven Module Loading

Status: complete Created: 2026-01-15 Last Updated: 2026-01-15 by Claude (Phase 5 complete) Complexity: medium (5 phases) Estimated Phases: 5 Completed Phases: 5/5


Objective

Replace the static provider list in Core\Boot with an event-driven module loading system. Modules declare interest in lifecycle events via static $listens arrays in their Boot.php files. The framework fires events; modules self-register only when relevant. Result: most modules never load for most requests.


Background

Current State

Core\Boot::$providers hardcodes all providers:

public static array $providers = [
    \Core\Bouncer\Boot::class,
    \Core\Config\Boot::class,
    // ... 30+ more
    \Mod\Commerce\Boot::class,
    \Mod\Social\Boot::class,
];

Every request loads every module. A webhook request loads the entire admin UI. A public page loads payment processing.

Target State

// Mod/Commerce/Boot.php
class Boot
{
    public static array $listens = [
        PaymentRequested::class => 'bootPayments',
        AdminPanelBooting::class => 'registerAdmin',
        ApiRoutesRegistering::class => 'registerApi',
    ];

    public function bootPayments(): void { /* load payment stuff */ }
    public function registerAdmin(): void { /* load admin routes/views */ }
}

Framework scans $listens without instantiation. Wires lazy listeners. Events fire naturally during request. Only relevant modules boot.

Design Principles

  1. Framework announces, modules decide — Core fires events, doesn't call modules directly
  2. Static declaration, lazy instantiation — Read $listens without creating objects
  3. Infrastructure vs features — Some Core modules always load (Bouncer), others lazy
  4. Convention over configuration — Scan Mod/*/Boot.php, no manifest file

Scope

  • Files modified: ~15
  • Files created: ~8
  • Events defined: ~10-15 lifecycle events
  • Tests: 40-60 target

Module Classification

Always-On Infrastructure (loaded via traditional providers)

Module Reason
Core\Bouncer Security — must run first, blocks bad requests
Core\Input WAF — runs pre-Laravel in Init::handle()
Core\Front Frontage routing — fires the events others listen to
Core\Headers Security headers — every response needs them
Core\Config Config system — everything depends on it

Lazy Core (event-driven)

Module Loads When
Core\Cdn Media upload/serve events
Core\Media Media processing events
Core\Seo Public page rendering
Core\Search Search queries
Core\Mail Email sending events
Core\Helpers May stay always-on (utility)
Core\Storage Storage operations

Lazy Mod (event-driven)

All modules in Mod/* become event-driven.


Phase Overview

Phase Name Status ACs Dependencies
1 Event Definitions Complete AC1-5 None
2 Module Scanner Complete AC6-10 Phase 1
3 Core Migration Skipped AC11-15 Phase 2
4 Mod Migration Complete AC16-22 Phase 2
5 Verification & Cleanup Complete AC23-27 Phases 3, 4

Acceptance Criteria

Phase 1: Event Definitions

  • AC1: Core\Events\ namespace exists with lifecycle event classes
  • AC2: Events defined for: FrameworkBooted, AdminPanelBooting, ApiRoutesRegistering, WebRoutesRegistering, McpToolsRegistering, QueueWorkerBooting, ConsoleBooting, MediaRequested, SearchRequested, MailSending
  • AC3: Each event class is a simple value object (no logic)
  • AC4: Events documented with PHPDoc describing when they fire
  • AC5: Test verifies all event classes are instantiable

Phase 2: Module Scanner

  • AC6: Core\ModuleScanner class exists
  • AC7: Scanner reads Boot.php files from configured paths without instantiation
  • AC8: Scanner extracts public static array $listens via reflection (not file parsing)
  • AC9: Scanner returns array of [event => [module => method]] mappings
  • AC10: Test verifies scanner correctly reads a mock Boot class with $listens

Phase 3: Core Module Migration

  • AC11: Core\Boot::$providers split into $infrastructure (always-on) and removed lazy modules
  • AC12: Core\Cdn\Boot converted to $listens pattern
  • AC13: Core\Media\Boot converted to $listens pattern
  • AC14: Core\Seo\Boot converted to $listens pattern
  • AC15: Tests verify lazy Core modules only instantiate when their events fire

Phase 4: Mod Module Migration

  • AC16: All 16 modules converted to $listens pattern:
    • Mod\Agentic, Mod\Analytics, Mod\Api, Mod\Web, Mod\Commerce, Mod\Content
    • Mod\Developer, Mod\Hub, Mod\Mcp, Mod\Notify, Mod\Social, Mod\Support
    • Mod\Tenant, Mod\Tools, Mod\Trees, Mod\Trust
  • AC17: Each module's Boot.php has $listens array declaring relevant events
  • AC18: Each module's routes register via WebRoutesRegistering, ApiRoutesRegistering, or AdminPanelBooting as appropriate
  • AC19: Each module's views/components register via appropriate events
  • AC20: Modules with commands register via ConsoleBooting
  • AC21: Modules with queue jobs register via QueueWorkerBooting
  • AC21.5: Modules with MCP tools register via McpToolsRegistering using handler classes
  • AC22: Tests verify at least 3 modules only load when their events fire

Phase 5: Verification & Cleanup

  • AC23: Core\Boot::$providers contains only infrastructure modules
  • AC24: No Mod\* classes appear in Core\Boot (modules load via events)
  • AC25: Unit test suite passes (503+ tests in ~5s), Feature tests require DB
  • AC26: Benchmark shows reduced memory/bootstrap time for API-only request
  • AC27: Documentation updated in doc/rfc/EVENT-DRIVEN-MODULES.md

Implementation Checklist

Phase 1: Event Definitions

  • File: app/Core/Events/FrameworkBooted.php
  • File: app/Core/Events/AdminPanelBooting.php
  • File: app/Core/Events/ApiRoutesRegistering.php
  • File: app/Core/Events/WebRoutesRegistering.php
  • File: app/Core/Events/McpToolsRegistering.php
  • File: app/Core/Events/QueueWorkerBooting.php
  • File: app/Core/Events/ConsoleBooting.php
  • File: app/Core/Events/MediaRequested.php
  • File: app/Core/Events/SearchRequested.php
  • File: app/Core/Events/MailSending.php
  • File: app/Core/Front/Mcp/Contracts/McpToolHandler.php
  • File: app/Core/Front/Mcp/McpContext.php
  • Test: app/Core/Tests/Unit/Events/LifecycleEventsTest.php

Phase 2: Module Scanner

  • File: app/Core/ModuleScanner.php
  • File: app/Core/ModuleRegistry.php (stores scanned mappings)
  • File: app/Core/LazyModuleListener.php (wraps module method as listener)
  • File: app/Core/LifecycleEventProvider.php (fires events, processes requests)
  • Update: app/Core/Boot.php — added LifecycleEventProvider
  • Update: app/Core/Front/Web/Boot.php — fires WebRoutesRegistering
  • Update: app/Core/Front/Admin/Boot.php — fires AdminPanelBooting
  • Update: app/Core/Front/Api/Boot.php — fires ApiRoutesRegistering
  • Test: app/Core/Tests/Unit/ModuleScannerTest.php
  • Test: app/Core/Tests/Unit/LazyModuleListenerTest.php
  • Test: app/Core/Tests/Feature/ModuleScannerIntegrationTest.php

Phase 3: Core Module Migration

  • Update: app/Core/Boot.php — split $providers
  • Update: app/Core/Cdn/Boot.php — add $listens, remove ServiceProvider pattern
  • Update: app/Core/Media/Boot.php — add $listens
  • Update: app/Core/Seo/Boot.php — add $listens
  • Update: app/Core/Search/Boot.php — add $listens
  • Update: app/Core/Mail/Boot.php — add $listens
  • Test: app/Core/Tests/Feature/LazyCoreModulesTest.php

Phase 4: Mod Module Migration

All 16 Mod modules converted to $listens pattern:

  • Update: app/Mod/Agentic/Boot.php ✓ (AdminPanelBooting, ConsoleBooting, McpToolsRegistering)
  • Update: app/Mod/Analytics/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting)
  • Update: app/Mod/Api/Boot.php ✓ (ApiRoutesRegistering, ConsoleBooting)
  • Update: app/Mod/Bio/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting)
  • Update: app/Mod/Commerce/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering, ConsoleBooting)
  • Update: app/Mod/Content/Boot.php ✓ (WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting, McpToolsRegistering)
  • Update: app/Mod/Developer/Boot.php ✓ (AdminPanelBooting)
  • Update: app/Mod/Hub/Boot.php ✓ (AdminPanelBooting)
  • Update: app/Mod/Mcp/Boot.php ✓ (AdminPanelBooting, ConsoleBooting, McpToolsRegistering)
  • Update: app/Mod/Notify/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering)
  • Update: app/Mod/Social/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting)
  • Update: app/Mod/Support/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering)
  • Update: app/Mod/Tenant/Boot.php ✓ (WebRoutesRegistering, ConsoleBooting)
  • Update: app/Mod/Tools/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering)
  • Update: app/Mod/Trees/Boot.php ✓ (WebRoutesRegistering, ConsoleBooting)
  • Update: app/Mod/Trust/Boot.php ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering)
  • Legacy patterns removed (no registerRoutes, registerViews, registerCommands methods)
  • Test: app/Mod/Tests/Feature/LazyModLoadingTest.php

Phase 5: Verification & Cleanup

  • Create: doc/rfc/EVENT-DRIVEN-MODULES.md — architecture reference (comprehensive)
  • Create: app/Core/Tests/Unit/ModuleScannerTest.php — unit tests for scanner
  • Create: app/Core/Tests/Unit/LazyModuleListenerTest.php — unit tests for lazy listener
  • Create: app/Core/Tests/Feature/ModuleScannerIntegrationTest.php — integration tests
  • Run: Unit test suite (75 Core tests pass, 503+ total Unit tests)

Technical Design

Security Model

Lazy loading isn't just optimisation — it's a security boundary.

Defence in depth:

  1. Bouncer — blocks bad requests before anything loads
  2. Lazy loading — modules only exist when relevant events fire
  3. Capability requests — modules request resources, Core grants/denies
  4. Validation — Core sanitises everything modules ask for

A misbehaving module can't:

  • Register routes it wasn't asked about (Core controls route registration)
  • Add nav items to sections it doesn't own (Core validates structure)
  • Access services it didn't declare (not loaded, not in memory)
  • Corrupt other modules' state (they don't exist yet)

Event as Capability Request

Events are request forms, not direct access to infrastructure. Modules declare what they want; Core decides what to grant.

// BAD: Module directly modifies infrastructure (Option A from discussion)
public function registerAdmin(AdminPanelBooting $event): void
{
    $event->navigation->add('commerce', ...);  // Direct mutation — dangerous
}

// GOOD: Module requests, Core processes (Option C)
public function registerAdmin(AdminPanelBooting $event): void
{
    $event->navigation([                       // Request form — safe
        'key' => 'commerce',
        'label' => 'Commerce',
        'icon' => 'credit-card',
        'route' => 'admin.commerce.index',
    ]);

    $event->routes(function () {
        // Route definitions — Core will register them
    });

    $event->views('commerce', __DIR__.'/View/Blade');
}

Core collects all requests, then processes them:

// In Core, after event fires:
$event = new AdminPanelBooting();
event($event);

// Core processes requests with full control
foreach ($event->navigationRequests() as $request) {
    if ($this->validateNavRequest($request)) {
        $this->navigation->add($request);
    }
}

foreach ($event->routeRequests() as $callback) {
    Route::middleware('admin')->group($callback);
}

foreach ($event->viewRequests() as [$namespace, $path]) {
    if ($this->validateViewPath($path)) {
        view()->addNamespace($namespace, $path);
    }
}

ModuleScanner Implementation

namespace Core;

class ModuleScanner
{
    public function scan(array $paths): array
    {
        $mappings = [];

        foreach ($paths as $path) {
            foreach (glob("{$path}/*/Boot.php") as $file) {
                $class = $this->classFromFile($file);

                if (!class_exists($class)) {
                    continue;
                }

                $reflection = new \ReflectionClass($class);

                if (!$reflection->hasProperty('listens')) {
                    continue;
                }

                $prop = $reflection->getProperty('listens');
                if (!$prop->isStatic() || !$prop->isPublic()) {
                    continue;
                }

                $listens = $prop->getValue();

                foreach ($listens as $event => $method) {
                    $mappings[$event][$class] = $method;
                }
            }
        }

        return $mappings;
    }

    private function classFromFile(string $file): string
    {
        // Extract namespace\class from file path
        // e.g., app/Mod/Commerce/Boot.php → Mod\Commerce\Boot
    }
}

Base Event Class

All lifecycle events extend a base that provides the request collection API:

namespace Core\Events;

abstract class LifecycleEvent
{
    protected array $navigationRequests = [];
    protected array $routeRequests = [];
    protected array $viewRequests = [];
    protected array $middlewareRequests = [];

    public function navigation(array $item): void
    {
        $this->navigationRequests[] = $item;
    }

    public function routes(callable $callback): void
    {
        $this->routeRequests[] = $callback;
    }

    public function views(string $namespace, string $path): void
    {
        $this->viewRequests[] = [$namespace, $path];
    }

    public function middleware(string $alias, string $class): void
    {
        $this->middlewareRequests[] = [$alias, $class];
    }

    // Getters for Core to process
    public function navigationRequests(): array { return $this->navigationRequests; }
    public function routeRequests(): array { return $this->routeRequests; }
    public function viewRequests(): array { return $this->viewRequests; }
    public function middlewareRequests(): array { return $this->middlewareRequests; }
}

LazyModuleListener Implementation

namespace Core;

class LazyModuleListener
{
    public function __construct(
        private string $moduleClass,
        private string $method
    ) {}

    public function handle(object $event): void
    {
        // Module only instantiated NOW, when event fires
        $module = app()->make($this->moduleClass);
        $module->{$this->method}($event);
    }
}

Boot.php Integration Point

// In Boot::app(), after withProviders():
->withEvents(function () {
    $scanner = new ModuleScanner();
    $mappings = $scanner->scan([
        app_path('Core'),
        app_path('Mod'),
    ]);

    foreach ($mappings as $event => $listeners) {
        foreach ($listeners as $class => $method) {
            Event::listen($event, new LazyModuleListener($class, $method));
        }
    }
})

Example Converted Module

// app/Mod/Commerce/Boot.php
namespace Mod\Commerce;

use Core\Events\AdminPanelBooting;
use Core\Events\ApiRoutesRegistering;
use Core\Events\WebRoutesRegistering;
use Core\Events\QueueWorkerBooting;

class Boot
{
    public static array $listens = [
        AdminPanelBooting::class => 'registerAdmin',
        ApiRoutesRegistering::class => 'registerApiRoutes',
        WebRoutesRegistering::class => 'registerWebRoutes',
        QueueWorkerBooting::class => 'registerJobs',
    ];

    public function registerAdmin(AdminPanelBooting $event): void
    {
        // Request navigation — Core will validate and add
        $event->navigation([
            'key' => 'commerce',
            'label' => 'Commerce',
            'icon' => 'credit-card',
            'route' => 'admin.commerce.index',
            'children' => [
                ['key' => 'products', 'label' => 'Products', 'route' => 'admin.commerce.products'],
                ['key' => 'orders', 'label' => 'Orders', 'route' => 'admin.commerce.orders'],
                ['key' => 'subscriptions', 'label' => 'Subscriptions', 'route' => 'admin.commerce.subscriptions'],
            ],
        ]);

        // Request routes — Core will wrap with middleware
        $event->routes(fn () => require __DIR__.'/Routes/admin.php');

        // Request view namespace — Core will validate path
        $event->views('commerce', __DIR__.'/View/Blade');
    }

    public function registerApiRoutes(ApiRoutesRegistering $event): void
    {
        $event->routes(fn () => require __DIR__.'/Routes/api.php');
    }

    public function registerWebRoutes(WebRoutesRegistering $event): void
    {
        $event->routes(fn () => require __DIR__.'/Routes/web.php');
    }

    public function registerJobs(QueueWorkerBooting $event): void
    {
        // Request job registration if needed
    }
}

MCP Tool Registration

MCP tools use handler classes instead of closures for better testability and separation.

McpToolHandler interface:

namespace Core\Front\Mcp\Contracts;

interface McpToolHandler
{
    /**
     * JSON schema describing the tool for Claude.
     */
    public static function schema(): array;

    /**
     * Handle tool invocation.
     */
    public function handle(array $args, McpContext $context): array;
}

McpContext abstracts transport (stdio vs HTTP):

namespace Core\Front\Mcp;

class McpContext
{
    public function __construct(
        private ?string $sessionId = null,
        private ?AgentPlan $currentPlan = null,
        private ?Closure $notificationCallback = null,
    ) {}

    public function logToSession(string $message): void { /* ... */ }
    public function sendNotification(string $method, array $params): void { /* ... */ }
    public function getSessionId(): ?string { return $this->sessionId; }
    public function getCurrentPlan(): ?AgentPlan { return $this->currentPlan; }
}

McpToolsRegistering event:

namespace Core\Events;

class McpToolsRegistering extends LifecycleEvent
{
    protected array $handlers = [];

    public function handler(string $handlerClass): void
    {
        if (!is_a($handlerClass, McpToolHandler::class, true)) {
            throw new \InvalidArgumentException("{$handlerClass} must implement McpToolHandler");
        }
        $this->handlers[] = $handlerClass;
    }

    public function handlers(): array
    {
        return $this->handlers;
    }
}

Example tool handler:

// Mod/Content/Mcp/ContentStatusHandler.php
namespace Mod\Content\Mcp;

use Core\Front\Mcp\Contracts\McpToolHandler;
use Core\Front\Mcp\McpContext;

class ContentStatusHandler implements McpToolHandler
{
    public static function schema(): array
    {
        return [
            'name' => 'content_status',
            'description' => 'Get content generation pipeline status',
            'inputSchema' => [
                'type' => 'object',
                'properties' => [],
                'required' => [],
            ],
        ];
    }

    public function handle(array $args, McpContext $context): array
    {
        $context->logToSession('Checking content pipeline status...');

        // ... implementation

        return ['status' => 'ok', 'providers' => [...]];
    }
}

Module registration:

// Mod/Content/Boot.php
public static array $listens = [
    McpToolsRegistering::class => 'registerMcpTools',
];

public function registerMcpTools(McpToolsRegistering $event): void
{
    $event->handler(\Mod\Content\Mcp\ContentStatusHandler::class);
    $event->handler(\Mod\Content\Mcp\ContentBriefCreateHandler::class);
    $event->handler(\Mod\Content\Mcp\ContentBriefListHandler::class);
    // ... etc
}

Frontage integration (Stdio):

The McpAgentServerCommand becomes a thin shell that:

  1. Fires McpToolsRegistering event at startup
  2. Collects all handler classes
  3. Builds tool list from ::schema() methods
  4. Routes tool calls to handler instances with McpContext
// In McpAgentServerCommand::handle()
$event = new McpToolsRegistering();
event($event);

$context = new McpContext(
    sessionId: $this->sessionId,
    currentPlan: $this->currentPlan,
    notificationCallback: fn($m, $p) => $this->sendNotification($m, $p),
);

foreach ($event->handlers() as $handlerClass) {
    $schema = $handlerClass::schema();
    $this->tools[$schema['name']] = [
        'schema' => $schema,
        'handler' => fn($args) => app($handlerClass)->handle($args, $context),
    ];
}

Sync Protocol

Keeping This Document Current

This document may drift from implementation as code changes. To re-sync:

  1. After implementation changes:

    # Agent prompt:
    "Review tasks/TASK-event-driven-module-loading.md against current implementation.
    Update acceptance criteria status, note any deviations in Notes section."
    
  2. Before resuming work:

    # Agent prompt:
    "Read tasks/TASK-event-driven-module-loading.md.
    Check which phases are complete by examining the actual files.
    Update Phase Overview table with current status."
    
  3. Automated sync points:

    • After each phase completion, update Phase Overview
    • After test runs, update test counts in Phase Completion Log
    • After any design changes, update Technical Design section

Code Locations to Check

When syncing, verify these key files:

Check File What to Verify
Events exist app/Core/Events/*.php All AC2 events defined
Scanner works app/Core/ModuleScanner.php Class exists, has scan()
Boot updated app/Core/Boot.php Uses scanner, has $infrastructure
Mods converted app/Mod/*/Boot.php Has $listens array

Deviation Log

Record any implementation decisions that differ from this plan:

Date Section Change Reason
- - - -

Verification Results

To be filled by verification agent after implementation


Phase Completion Log

Phase 1: Event Definitions (2026-01-15)

Created all lifecycle event classes:

  • Core/Events/LifecycleEvent.php - Base class with request collection API
  • Core/Events/FrameworkBooted.php
  • Core/Events/AdminPanelBooting.php
  • Core/Events/ApiRoutesRegistering.php
  • Core/Events/WebRoutesRegistering.php
  • Core/Events/McpToolsRegistering.php - With handler registration for MCP tools
  • Core/Events/QueueWorkerBooting.php
  • Core/Events/ConsoleBooting.php
  • Core/Events/MediaRequested.php
  • Core/Events/SearchRequested.php
  • Core/Events/MailSending.php

Also created MCP infrastructure:

  • Core/Front/Mcp/Contracts/McpToolHandler.php - Interface for MCP tool handlers
  • Core/Front/Mcp/McpContext.php - Context object for transport abstraction

Phase 2: Module Scanner (2026-01-15)

Created scanning and lazy loading infrastructure:

  • Core/ModuleScanner.php - Scans Boot.php files for $listens via reflection
  • Core/LazyModuleListener.php - Wraps module methods as event listeners
  • Core/ModuleRegistry.php - Manages lazy module registration
  • Core/LifecycleEventProvider.php - Wires everything together

Integrated into application:

  • Added LifecycleEventProvider to Core/Boot::$providers
  • Updated Core/Front/Web/Boot to fire WebRoutesRegistering
  • Updated Core/Front/Admin/Boot to fire AdminPanelBooting
  • Updated Core/Front/Api/Boot to fire ApiRoutesRegistering

Proof of concept modules converted:

  • Mod/Content/Boot.php - listens to WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting, McpToolsRegistering
  • Mod/Agentic/Boot.php - listens to AdminPanelBooting, ConsoleBooting, McpToolsRegistering

Phase 4: Mod Module Migration (2026-01-15)

All 16 Mod modules converted to event-driven $listens pattern:

Modules converted:

  • Agentic, Analytics, Api, Bio, Commerce, Content, Developer, Hub, Mcp, Notify, Social, Support, Tenant, Tools, Trees, Trust

Legacy patterns removed:

  • No modules use registerRoutes(), registerViews(), registerCommands(), or registerLivewireComponents()
  • All route/view/component registration moved to event handlers

CLI Frontage created:

  • Core/Front/Cli/Boot.php - fires ConsoleBooting event and processes:
    • Artisan commands
    • Translations
    • Middleware aliases
    • Policies
    • Blade component paths

Phase 5: Verification & Cleanup (2026-01-15)

Tests created:

  • Core/Tests/Unit/ModuleScannerTest.php - Unit tests for extractListens() reflection
  • Core/Tests/Unit/LazyModuleListenerTest.php - Unit tests for lazy module instantiation
  • Core/Tests/Feature/ModuleScannerIntegrationTest.php - Integration tests with real modules

Documentation created:

  • doc/rfc/EVENT-DRIVEN-MODULES.md - Comprehensive RFC documenting:
    • Architecture overview with diagrams
    • Core components (ModuleScanner, ModuleRegistry, LazyModuleListener)
    • Available lifecycle events
    • Module implementation guide
    • Migration guide from legacy pattern
    • Testing examples
    • Performance considerations

Test results:

  • Unit tests: 75 Core tests pass in 1.44s
  • Total Unit tests: 503+ tests pass in ~5s
  • Feature tests require database (not run in quick verification)

Notes

Open Questions

  1. Event payload: Should events carry context (e.g., AdminPanelBooting carries the navigation builder), or should modules pull from container?

  2. Load order: If Module A needs Module B's routes registered first, how do we handle? Priority property on $listens?

  3. Proprietary modules: Bio, Analytics, Social, Trust, Notify, Front — these won't be in the open-source release. How do they integrate? Same pattern, just not shipped?

  4. Plug integration: Does Plug\Boot become event-driven too, or stay always-on since it's a pure library?

Decisions Made

  • Infrastructure modules stay as traditional ServiceProviders (simpler, no benefit to lazy loading security/config)
  • Modules don't extend ServiceProvider anymore — they're plain classes with $listens
  • Scanner uses reflection, not file parsing (more reliable, handles inheritance)

References

  • Current Core\Boot: app/Core/Boot.php:17-61
  • Current Init: app/Core/Init.php
  • Module README: app/Core/README.md