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>
This commit is contained in:
parent
94cb8cc3fa
commit
afc03418eb
3 changed files with 1269 additions and 0 deletions
308
changelog/2026/jan/CORE_PACKAGE_PLAN.md
Normal file
308
changelog/2026/jan/CORE_PACKAGE_PLAN.md
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
# Core Package Release Plan
|
||||||
|
|
||||||
|
**Package:** `host-uk/core` (GitHub: host-uk/core)
|
||||||
|
**Namespace:** `Core\` (not `Snide\` - that's *barf*)
|
||||||
|
**Usage:** `<core:button>`, `Core\Front\Components\Button::make()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Value Proposition
|
||||||
|
|
||||||
|
Core provides:
|
||||||
|
1. **Thin Flux Wrappers** - `<core:*>` components that pass through to `<flux:*>` with 100% parity
|
||||||
|
2. **HLCRF Layout System** - Compositor pattern for page layouts (Header, Left, Content, Right, Footer)
|
||||||
|
3. **FontAwesome Pro Integration** - Custom icon system with brand/jelly auto-detection
|
||||||
|
4. **PHP Builders** - Programmatic UI composition (`Button::make()->primary()`)
|
||||||
|
5. **Graceful Degradation** - Falls back to free versions of Flux/FontAwesome
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detection Strategy
|
||||||
|
|
||||||
|
### Flux Pro vs Free
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
|
||||||
|
class Core
|
||||||
|
{
|
||||||
|
public static function hasFluxPro(): bool
|
||||||
|
{
|
||||||
|
return InstalledVersions::isInstalled('livewire/flux-pro');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function proComponents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'calendar', 'date-picker', 'time-picker',
|
||||||
|
'editor', 'composer',
|
||||||
|
'chart', 'kanban',
|
||||||
|
'command', 'context',
|
||||||
|
'autocomplete', 'pillbox', 'slider',
|
||||||
|
'file-upload',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FontAwesome Pro vs Free
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Core
|
||||||
|
{
|
||||||
|
public static function hasFontAwesomePro(): bool
|
||||||
|
{
|
||||||
|
// Check for FA Pro kit or CDN link in config
|
||||||
|
return config('core.fontawesome.pro', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function faStyles(): array
|
||||||
|
{
|
||||||
|
// Pro: solid, regular, light, thin, duotone, brands, sharp, jelly
|
||||||
|
// Free: solid, regular, brands
|
||||||
|
return self::hasFontAwesomePro()
|
||||||
|
? ['solid', 'regular', 'light', 'thin', 'duotone', 'brands', 'sharp', 'jelly']
|
||||||
|
: ['solid', 'regular', 'brands'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Graceful Degradation
|
||||||
|
|
||||||
|
### Pro-Only Flux Components
|
||||||
|
|
||||||
|
When Flux Pro isn't installed, `<core:calendar>` etc. should:
|
||||||
|
|
||||||
|
**Option A: Helpful Error** (recommended for development)
|
||||||
|
```blade
|
||||||
|
{{-- calendar.blade.php --}}
|
||||||
|
@if(Core::hasFluxPro())
|
||||||
|
<flux:calendar {{ $attributes }} />
|
||||||
|
@else
|
||||||
|
<div class="p-4 border border-amber-300 bg-amber-50 rounded text-amber-800 text-sm">
|
||||||
|
<strong>Calendar requires Flux Pro.</strong>
|
||||||
|
<a href="https://fluxui.dev" class="underline">Learn more</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Silent Fallback** (for production)
|
||||||
|
```blade
|
||||||
|
{{-- calendar.blade.php --}}
|
||||||
|
@if(Core::hasFluxPro())
|
||||||
|
<flux:calendar {{ $attributes }} />
|
||||||
|
@else
|
||||||
|
{{-- Graceful degradation: render nothing or a basic HTML input --}}
|
||||||
|
<input type="date" {{ $attributes }} />
|
||||||
|
@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### FontAwesome Style Fallback
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In icon.blade.php
|
||||||
|
$availableStyles = Core::faStyles();
|
||||||
|
|
||||||
|
// Map pro-only styles to free equivalents
|
||||||
|
$styleFallback = [
|
||||||
|
'light' => 'regular', // FA Light → FA Regular
|
||||||
|
'thin' => 'regular', // FA Thin → FA Regular
|
||||||
|
'duotone' => 'solid', // FA Duotone → FA Solid
|
||||||
|
'sharp' => 'solid', // FA Sharp → FA Solid
|
||||||
|
'jelly' => 'solid', // Host UK Jelly → FA Solid
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!in_array($iconStyle, $availableStyles)) {
|
||||||
|
$iconStyle = $styleFallback[$iconStyle] ?? 'fa-solid';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Package Structure (Root Level)
|
||||||
|
|
||||||
|
```
|
||||||
|
host-uk/core/
|
||||||
|
├── composer.json
|
||||||
|
├── LICENSE
|
||||||
|
├── README.md
|
||||||
|
├── Core/
|
||||||
|
│ ├── Boot.php # ServiceProvider
|
||||||
|
│ ├── Core.php # Detection helpers + facade
|
||||||
|
│ ├── Front/
|
||||||
|
│ │ ├── Boot.php
|
||||||
|
│ │ └── Components/
|
||||||
|
│ │ ├── CoreTagCompiler.php # <core:*> syntax
|
||||||
|
│ │ ├── View/
|
||||||
|
│ │ │ └── Blade/ # 100+ components
|
||||||
|
│ │ │ ├── button.blade.php
|
||||||
|
│ │ │ ├── icon.blade.php
|
||||||
|
│ │ │ ├── layout.blade.php
|
||||||
|
│ │ │ └── layout/
|
||||||
|
│ │ ├── Button.php # PHP Builder
|
||||||
|
│ │ ├── Card.php
|
||||||
|
│ │ ├── Heading.php
|
||||||
|
│ │ ├── Layout.php # HLCRF compositor
|
||||||
|
│ │ ├── NavList.php
|
||||||
|
│ │ └── Text.php
|
||||||
|
│ └── config.php # Package config
|
||||||
|
├── tests/
|
||||||
|
│ └── Feature/
|
||||||
|
│ └── CoreComponentsTest.php
|
||||||
|
└── .github/
|
||||||
|
└── workflows/
|
||||||
|
└── tests.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** This mirrors Host Hub's current `app/Core/` structure exactly, just at root level. Minimal refactoring needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## composer.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "host-uk/core",
|
||||||
|
"description": "Core UI component library for Laravel - Flux Pro/Free compatible",
|
||||||
|
"keywords": ["laravel", "livewire", "flux", "components", "ui"],
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Snider",
|
||||||
|
"homepage": "https://host.uk.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"laravel/framework": "^11.0|^12.0",
|
||||||
|
"livewire/livewire": "^3.0",
|
||||||
|
"livewire/flux": "^2.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"livewire/flux-pro": "Required for Pro components (calendar, editor, chart, etc.)"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Core\\": "src/Core/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Core\\CoreServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/core.php
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| FontAwesome Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
'fontawesome' => [
|
||||||
|
'pro' => env('FONTAWESOME_PRO', false),
|
||||||
|
'kit' => env('FONTAWESOME_KIT'), // e.g., 'abc123def456'
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fallback Behaviour
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| How to handle Pro components when Pro isn't installed.
|
||||||
|
| Options: 'error', 'fallback', 'silent'
|
||||||
|
*/
|
||||||
|
'pro_fallback' => env('CORE_PRO_FALLBACK', 'error'),
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
### Step 1: Extract Core (Host Hub)
|
||||||
|
Move `app/Core/Front/Components/` to standalone package, update namespace `Core\` → `Core\`
|
||||||
|
|
||||||
|
### Step 2: Install Package Back
|
||||||
|
```bash
|
||||||
|
composer require host-uk/core
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Host Hub Uses Package
|
||||||
|
Replace `app/Core/Front/Components/` with import from package. Keep Host-specific stuff in `app/Core/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Stays in Host Hub
|
||||||
|
|
||||||
|
These are too app-specific for the package:
|
||||||
|
- `Core/Cdn/` - BunnyCDN integration
|
||||||
|
- `Core/Config/` - Multi-tenant config system
|
||||||
|
- `Core/Mail/` - EmailShield
|
||||||
|
- `Core/Seo/` - Schema, OG images
|
||||||
|
- `Core/Headers/` - Security headers (maybe extract later)
|
||||||
|
- `Core/Media/` - ImageOptimizer (maybe extract later)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Goes in Package
|
||||||
|
|
||||||
|
Universal value:
|
||||||
|
- `Core/Front/Components/` - All 100+ Blade components
|
||||||
|
- `Core/Front/Components/*.php` - PHP Builders
|
||||||
|
- `CoreTagCompiler.php` - `<core:*>` syntax
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions to Resolve
|
||||||
|
|
||||||
|
1. **Package name:** `host-uk/core`?
|
||||||
|
2. **FontAwesome:** Detect Kit from asset URL, or require config?
|
||||||
|
3. **Fallback mode:** Default to 'error' (dev-friendly) or 'fallback' (prod-safe)?
|
||||||
|
4. **Jelly icons:** Include your custom FA style in package, or keep Host UK specific?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Progress
|
||||||
|
|
||||||
|
### Done ✅
|
||||||
|
|
||||||
|
1. **Detection helpers** - `app/Core/Core.php`
|
||||||
|
- `Core::hasFluxPro()` - Uses Composer InstalledVersions
|
||||||
|
- `Core::hasFontAwesomePro()` - Uses config
|
||||||
|
- `Core::requiresFluxPro($component)` - Checks if component needs Pro
|
||||||
|
- `Core::fontAwesomeStyles()` - Returns available styles
|
||||||
|
- `Core::fontAwesomeFallback($style)` - Maps Pro→Free styles
|
||||||
|
|
||||||
|
2. **Config file** - `app/Core/config.php`
|
||||||
|
- `fontawesome.pro` - Enable FA Pro styles
|
||||||
|
- `fontawesome.kit` - FA Kit ID
|
||||||
|
- `pro_fallback` - How to handle Pro components (error/fallback/silent)
|
||||||
|
|
||||||
|
3. **Icon fallback** - `app/Core/Front/Components/View/Blade/icon.blade.php`
|
||||||
|
- Auto-detects FA Pro availability
|
||||||
|
- Falls back: jelly→solid, light→regular, thin→regular, duotone→solid
|
||||||
|
|
||||||
|
4. **Test coverage** - 49 tests, 79 assertions
|
||||||
|
- Detection helper tests
|
||||||
|
- Icon fallback tests (Pro/Free scenarios)
|
||||||
|
- Full Flux parity tests
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
|
||||||
|
1. Create pro-component wrappers with fallback (calendar, editor, chart, etc.)
|
||||||
|
2. Test with Flux Free only (remove flux-pro temporarily)
|
||||||
|
3. Extract to separate repo
|
||||||
|
4. Update namespace `Core\` → `Core\`
|
||||||
|
5. Create composer.json for package
|
||||||
|
6. Publish to Packagist
|
||||||
809
changelog/2026/jan/TASK-event-driven-module-loading.md
Normal file
809
changelog/2026/jan/TASK-event-driven-module-loading.md
Normal file
|
|
@ -0,0 +1,809 @@
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
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
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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
|
||||||
|
|
||||||
|
- [x] AC1: `Core\Events\` namespace exists with lifecycle event classes
|
||||||
|
- [x] AC2: Events defined for: `FrameworkBooted`, `AdminPanelBooting`, `ApiRoutesRegistering`, `WebRoutesRegistering`, `McpToolsRegistering`, `QueueWorkerBooting`, `ConsoleBooting`, `MediaRequested`, `SearchRequested`, `MailSending`
|
||||||
|
- [x] AC3: Each event class is a simple value object (no logic)
|
||||||
|
- [x] AC4: Events documented with PHPDoc describing when they fire
|
||||||
|
- [ ] AC5: Test verifies all event classes are instantiable
|
||||||
|
|
||||||
|
### Phase 2: Module Scanner
|
||||||
|
|
||||||
|
- [x] AC6: `Core\ModuleScanner` class exists
|
||||||
|
- [x] AC7: Scanner reads `Boot.php` files from configured paths without instantiation
|
||||||
|
- [x] AC8: Scanner extracts `public static array $listens` via reflection (not file parsing)
|
||||||
|
- [x] 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
|
||||||
|
|
||||||
|
- [x] 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`
|
||||||
|
- [x] AC17: Each module's `Boot.php` has `$listens` array declaring relevant events
|
||||||
|
- [x] AC18: Each module's routes register via `WebRoutesRegistering`, `ApiRoutesRegistering`, or `AdminPanelBooting` as appropriate
|
||||||
|
- [x] AC19: Each module's views/components register via appropriate events
|
||||||
|
- [x] AC20: Modules with commands register via `ConsoleBooting`
|
||||||
|
- [ ] AC21: Modules with queue jobs register via `QueueWorkerBooting`
|
||||||
|
- [x] 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
|
||||||
|
|
||||||
|
- [x] AC23: `Core\Boot::$providers` contains only infrastructure modules
|
||||||
|
- [x] AC24: No `Mod\*` classes appear in `Core\Boot` (modules load via events)
|
||||||
|
- [x] AC25: Unit test suite passes (503+ tests in ~5s), Feature tests require DB
|
||||||
|
- [ ] AC26: Benchmark shows reduced memory/bootstrap time for API-only request
|
||||||
|
- [x] AC27: Documentation updated in `doc/rfc/EVENT-DRIVEN-MODULES.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Phase 1: Event Definitions
|
||||||
|
|
||||||
|
- [x] File: `app/Core/Events/FrameworkBooted.php`
|
||||||
|
- [x] File: `app/Core/Events/AdminPanelBooting.php`
|
||||||
|
- [x] File: `app/Core/Events/ApiRoutesRegistering.php`
|
||||||
|
- [x] File: `app/Core/Events/WebRoutesRegistering.php`
|
||||||
|
- [x] File: `app/Core/Events/McpToolsRegistering.php`
|
||||||
|
- [x] File: `app/Core/Events/QueueWorkerBooting.php`
|
||||||
|
- [x] File: `app/Core/Events/ConsoleBooting.php`
|
||||||
|
- [x] File: `app/Core/Events/MediaRequested.php`
|
||||||
|
- [x] File: `app/Core/Events/SearchRequested.php`
|
||||||
|
- [x] File: `app/Core/Events/MailSending.php`
|
||||||
|
- [x] File: `app/Core/Front/Mcp/Contracts/McpToolHandler.php`
|
||||||
|
- [x] File: `app/Core/Front/Mcp/McpContext.php`
|
||||||
|
- [ ] Test: `app/Core/Tests/Unit/Events/LifecycleEventsTest.php`
|
||||||
|
|
||||||
|
### Phase 2: Module Scanner
|
||||||
|
|
||||||
|
- [x] File: `app/Core/ModuleScanner.php`
|
||||||
|
- [x] File: `app/Core/ModuleRegistry.php` (stores scanned mappings)
|
||||||
|
- [x] File: `app/Core/LazyModuleListener.php` (wraps module method as listener)
|
||||||
|
- [x] File: `app/Core/LifecycleEventProvider.php` (fires events, processes requests)
|
||||||
|
- [x] Update: `app/Core/Boot.php` — added LifecycleEventProvider
|
||||||
|
- [x] Update: `app/Core/Front/Web/Boot.php` — fires WebRoutesRegistering
|
||||||
|
- [x] Update: `app/Core/Front/Admin/Boot.php` — fires AdminPanelBooting
|
||||||
|
- [x] Update: `app/Core/Front/Api/Boot.php` — fires ApiRoutesRegistering
|
||||||
|
- [x] Test: `app/Core/Tests/Unit/ModuleScannerTest.php`
|
||||||
|
- [x] Test: `app/Core/Tests/Unit/LazyModuleListenerTest.php`
|
||||||
|
- [x] 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:
|
||||||
|
|
||||||
|
- [x] Update: `app/Mod/Agentic/Boot.php` ✓ (AdminPanelBooting, ConsoleBooting, McpToolsRegistering)
|
||||||
|
- [x] Update: `app/Mod/Analytics/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting)
|
||||||
|
- [x] Update: `app/Mod/Api/Boot.php` ✓ (ApiRoutesRegistering, ConsoleBooting)
|
||||||
|
- [x] Update: `app/Mod/Bio/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting)
|
||||||
|
- [x] Update: `app/Mod/Commerce/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering, ConsoleBooting)
|
||||||
|
- [x] Update: `app/Mod/Content/Boot.php` ✓ (WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting, McpToolsRegistering)
|
||||||
|
- [x] Update: `app/Mod/Developer/Boot.php` ✓ (AdminPanelBooting)
|
||||||
|
- [x] Update: `app/Mod/Hub/Boot.php` ✓ (AdminPanelBooting)
|
||||||
|
- [x] Update: `app/Mod/Mcp/Boot.php` ✓ (AdminPanelBooting, ConsoleBooting, McpToolsRegistering)
|
||||||
|
- [x] Update: `app/Mod/Notify/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering)
|
||||||
|
- [x] Update: `app/Mod/Social/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering, ConsoleBooting)
|
||||||
|
- [x] Update: `app/Mod/Support/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering)
|
||||||
|
- [x] Update: `app/Mod/Tenant/Boot.php` ✓ (WebRoutesRegistering, ConsoleBooting)
|
||||||
|
- [x] Update: `app/Mod/Tools/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering)
|
||||||
|
- [x] Update: `app/Mod/Trees/Boot.php` ✓ (WebRoutesRegistering, ConsoleBooting)
|
||||||
|
- [x] Update: `app/Mod/Trust/Boot.php` ✓ (AdminPanelBooting, WebRoutesRegistering, ApiRoutesRegistering)
|
||||||
|
- [x] Legacy patterns removed (no registerRoutes, registerViews, registerCommands methods)
|
||||||
|
- [ ] Test: `app/Mod/Tests/Feature/LazyModLoadingTest.php`
|
||||||
|
|
||||||
|
### Phase 5: Verification & Cleanup
|
||||||
|
|
||||||
|
- [x] Create: `doc/rfc/EVENT-DRIVEN-MODULES.md` — architecture reference (comprehensive)
|
||||||
|
- [x] Create: `app/Core/Tests/Unit/ModuleScannerTest.php` — unit tests for scanner
|
||||||
|
- [x] Create: `app/Core/Tests/Unit/LazyModuleListenerTest.php` — unit tests for lazy listener
|
||||||
|
- [x] Create: `app/Core/Tests/Feature/ModuleScannerIntegrationTest.php` — integration tests
|
||||||
|
- [x] 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.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```php
|
||||||
|
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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
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
|
||||||
|
|
||||||
|
```php
|
||||||
|
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
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
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):**
|
||||||
|
|
||||||
|
```php
|
||||||
|
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:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
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:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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`
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 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:**
|
||||||
|
```bash
|
||||||
|
# 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:**
|
||||||
|
```bash
|
||||||
|
# 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`
|
||||||
152
changelog/2026/jan/traffic-detection-inAppBrowser.md
Normal file
152
changelog/2026/jan/traffic-detection-inAppBrowser.md
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
# In-App Browser Detection
|
||||||
|
|
||||||
|
Detects when users visit from social media in-app browsers (Instagram, TikTok, etc.) rather than standard browsers.
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
Creators sharing links on social platforms need to know when traffic comes from in-app browsers because:
|
||||||
|
|
||||||
|
- **Content policies differ** - Some platforms deplatform users who link to adult content without warnings
|
||||||
|
- **User experience varies** - In-app browsers have limitations (no extensions, different cookie handling)
|
||||||
|
- **Traffic routing** - Creators may want to redirect certain platform traffic or show platform-specific messaging
|
||||||
|
|
||||||
|
## Location
|
||||||
|
|
||||||
|
```
|
||||||
|
app/Services/Shared/DeviceDetectionService.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Services\Shared\DeviceDetectionService;
|
||||||
|
|
||||||
|
$dd = app(DeviceDetectionService::class);
|
||||||
|
$ua = request()->userAgent();
|
||||||
|
|
||||||
|
// Check for specific platforms
|
||||||
|
$dd->isInstagram($ua) // true if Instagram in-app browser
|
||||||
|
$dd->isFacebook($ua) // true if Facebook in-app browser
|
||||||
|
$dd->isTikTok($ua) // true if TikTok in-app browser
|
||||||
|
$dd->isTwitter($ua) // true if Twitter/X in-app browser
|
||||||
|
$dd->isSnapchat($ua) // true if Snapchat in-app browser
|
||||||
|
$dd->isLinkedIn($ua) // true if LinkedIn in-app browser
|
||||||
|
$dd->isThreads($ua) // true if Threads in-app browser
|
||||||
|
$dd->isPinterest($ua) // true if Pinterest in-app browser
|
||||||
|
$dd->isReddit($ua) // true if Reddit in-app browser
|
||||||
|
|
||||||
|
// General checks
|
||||||
|
$dd->isInAppBrowser($ua) // true if ANY in-app browser
|
||||||
|
$dd->isMetaPlatform($ua) // true if Instagram, Facebook, or Threads
|
||||||
|
```
|
||||||
|
|
||||||
|
## Grouped platform checks
|
||||||
|
|
||||||
|
### Strict content platforms
|
||||||
|
|
||||||
|
Platforms known to enforce content policies that may result in account action:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$dd->isStrictContentPlatform($ua)
|
||||||
|
// Returns true for: Instagram, Facebook, Threads, TikTok, Twitter, Snapchat, LinkedIn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meta platforms
|
||||||
|
|
||||||
|
All Meta-owned apps (useful for consistent policy application):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$dd->isMetaPlatform($ua)
|
||||||
|
// Returns true for: Instagram, Facebook, Threads
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: BioHost 18+ warning
|
||||||
|
|
||||||
|
Show a content warning when adult content is accessed from strict platforms:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In PublicBioPageController or Livewire component
|
||||||
|
$deviceDetection = app(DeviceDetectionService::class);
|
||||||
|
|
||||||
|
$showAdultWarning = $biolink->is_adult_content
|
||||||
|
&& $deviceDetection->isStrictContentPlatform(request()->userAgent());
|
||||||
|
|
||||||
|
// Or target a specific platform
|
||||||
|
$showInstagramWarning = $biolink->is_adult_content
|
||||||
|
&& $deviceDetection->isInstagram(request()->userAgent());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full device info
|
||||||
|
|
||||||
|
The `parse()` method returns all detection data at once:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$dd->parse($ua);
|
||||||
|
|
||||||
|
// Returns:
|
||||||
|
[
|
||||||
|
'device_type' => 'mobile',
|
||||||
|
'os_name' => 'iOS',
|
||||||
|
'browser_name' => null, // In-app browsers often lack browser identification
|
||||||
|
'in_app_browser' => 'instagram',
|
||||||
|
'is_in_app' => true,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Display names
|
||||||
|
|
||||||
|
Get human-readable platform names for UI display:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$dd->getPlatformDisplayName($ua);
|
||||||
|
|
||||||
|
// Returns: "Instagram", "TikTok", "X (Twitter)", "LinkedIn", etc.
|
||||||
|
// Returns null if not an in-app browser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported platforms
|
||||||
|
|
||||||
|
| Platform | Method | In strict list |
|
||||||
|
|----------|--------|----------------|
|
||||||
|
| Instagram | `isInstagram()` | Yes |
|
||||||
|
| Facebook | `isFacebook()` | Yes |
|
||||||
|
| Threads | `isThreads()` | Yes |
|
||||||
|
| TikTok | `isTikTok()` | Yes |
|
||||||
|
| Twitter/X | `isTwitter()` | Yes |
|
||||||
|
| Snapchat | `isSnapchat()` | Yes |
|
||||||
|
| LinkedIn | `isLinkedIn()` | Yes |
|
||||||
|
| Pinterest | `isPinterest()` | No |
|
||||||
|
| Reddit | `isReddit()` | No |
|
||||||
|
| WeChat | via `detectInAppBrowser()` | No |
|
||||||
|
| LINE | via `detectInAppBrowser()` | No |
|
||||||
|
| Telegram | via `detectInAppBrowser()` | No |
|
||||||
|
| Discord | via `detectInAppBrowser()` | No |
|
||||||
|
| WhatsApp | via `detectInAppBrowser()` | No |
|
||||||
|
| Generic WebView | `isInAppBrowser()` | No |
|
||||||
|
|
||||||
|
## How detection works
|
||||||
|
|
||||||
|
Each platform adds identifiable strings to their in-app browser User-Agent:
|
||||||
|
|
||||||
|
```
|
||||||
|
Instagram: "Instagram" in UA
|
||||||
|
Facebook: "FBAN", "FBAV", "FB_IAB", "FBIOS", or "FBSS"
|
||||||
|
TikTok: "BytedanceWebview", "musical_ly", or "TikTok"
|
||||||
|
Twitter: "Twitter" in UA
|
||||||
|
LinkedIn: "LinkedInApp"
|
||||||
|
Snapchat: "Snapchat"
|
||||||
|
Threads: "Barcelona" (Meta's internal codename)
|
||||||
|
```
|
||||||
|
|
||||||
|
Generic WebView detection catches unknown in-app browsers via patterns like `wv` (Android WebView marker).
|
||||||
|
|
||||||
|
## Related services
|
||||||
|
|
||||||
|
This service is part of the shared services extracted for use across the platform:
|
||||||
|
|
||||||
|
- `DeviceDetectionService` - Device type, OS, browser, bot detection, in-app browser detection
|
||||||
|
- `GeoIpService` - IP geolocation from CDN headers or MaxMind
|
||||||
|
- `PrivacyHelper` - IP anonymisation and hashing
|
||||||
|
- `UtmHelper` - UTM parameter extraction
|
||||||
|
|
||||||
|
See also: `doc/dev-feat-docs/traffic-detections/` for other detection features.
|
||||||
Loading…
Add table
Reference in a new issue