- Remove non-existent src/Core/Service/ from CLAUDE.md L1 packages list - Fix LifecycleEventsTest: remove dependency on McpToolHandler interface (lives in core-mcp, not needed since McpToolsRegistering stores class name strings) - Run Laravel Pint to fix PSR-12 violations across all source and test files - Add missing declare(strict_types=1) to 18 PHP files (tests, seeders, Layout.php, GenerateServiceOgImages.php) Co-Authored-By: Virgil <virgil@lethean.io>
6.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
composer test # Run all tests (PHPUnit)
composer test -- --filter=Name # Run single test by name
composer test -- --testsuite=Unit # Run specific test suite
composer pint # Format code with Laravel Pint
./vendor/bin/pint --dirty # Format only changed files
vendor/bin/phpstan analyse --memory-limit=2G # Static analysis (level 1)
vendor/bin/psalm --show-info=false # Type checking (level 8)
vendor/bin/rector process --dry-run # Code modernisation preview
composer audit # Security vulnerability check
CI Matrix
Tests run against PHP 8.2/8.3/8.4 × Laravel 11/12 (excluding PHP 8.2 + Laravel 12). CI also runs Pint (--test), PHPStan, Psalm, and composer audit.
Coding Standards
- UK English: colour, organisation, centre (never American spellings)
- Strict types:
declare(strict_types=1);in every PHP file - Type hints: All parameters and return types required
- Testing: PHPUnit with Orchestra Testbench
- License: EUPL-1.2
Architecture
Event-Driven Module Loading
Modules declare interest in lifecycle events via static $listens arrays and are only instantiated when those events fire:
LifecycleEventProvider::register()
└── ModuleScanner::scan() # Finds Boot.php files with $listens
└── ModuleRegistry::register() # Wires LazyModuleListener for each event
Key benefit: Web requests don't load admin modules; API requests don't load web modules.
Frontages
Frontages are ServiceProviders in src/Core/Front/ that fire context-specific lifecycle events:
| Frontage | Event | Middleware | Fires When |
|---|---|---|---|
| Web | WebRoutesRegistering |
web |
Public routes |
| Admin | AdminPanelBooting |
admin |
Admin panel |
| Api | ApiRoutesRegistering |
api |
REST endpoints |
| Client | ClientRoutesRegistering |
client |
Authenticated SaaS |
| Cli | ConsoleBooting |
- | Artisan commands |
| Mcp | McpToolsRegistering |
- | MCP tool handlers |
| - | FrameworkBooted |
- | Late-stage initialisation |
L1 Packages
Subdirectories under src/Core/ are self-contained "L1 packages" with their own Boot.php, migrations, tests, and views:
src/Core/Actions/ # Action pattern + scheduled action scanning
src/Core/Activity/ # Activity logging (wraps spatie/laravel-activitylog)
src/Core/Bouncer/ # Security blocking/redirects + honeypot + action gate
src/Core/Cdn/ # CDN integration
src/Core/Config/ # Dynamic configuration with two-tier caching
src/Core/Front/ # Frontage system (Web, Admin, Api, Client, Cli, Mcp)
src/Core/Lang/ # Translation system with ICU + locale fallback chains
src/Core/Media/ # Media handling with thumbnail helpers
src/Core/Search/ # Search functionality
src/Core/Seo/ # SEO utilities
src/Core/Storage/ # Storage with Redis circuit breaker + fallback
src/Core/Webhook/ # Webhook system + CronTrigger scheduled action
Module Pattern
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => ['onAdmin', 10], // With priority (higher = runs first)
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('example', __DIR__.'/Views');
$event->routes(fn () => require __DIR__.'/Routes/web.php');
$event->livewire('example.widget', ExampleWidget::class);
}
}
Scaffold new modules with artisan: make:mod, make:website, make:plug.
Namespace Mapping
| Path | Namespace |
|---|---|
src/Core/ |
Core\ |
src/Mod/ |
Core\Mod\ |
src/Plug/ |
Core\Plug\ |
src/Website/ |
Core\Website\ |
app/Mod/ |
Mod\ |
Actions Pattern
Single-purpose business logic classes with static run() helper:
use Core\Actions\Action;
class CreateOrder
{
use Action;
public function __construct(private OrderService $orders) {}
public function handle(User $user, array $data): Order
{
return $this->orders->create($user, $data);
}
}
// Usage: CreateOrder::run($user, $validated);
Scheduled Actions
Actions can be marked for scheduled execution with the #[Scheduled] attribute. ScheduledActionScanner discovers these by scanning for the attribute.
use Core\Actions\Scheduled;
#[Scheduled(frequency: 'dailyAt:09:00', timezone: 'Europe/London')]
class PublishDigest
{
use Action;
public function handle(): void { /* ... */ }
}
Frequency strings map to Laravel Schedule methods: everyMinute, dailyAt:09:00, weeklyOn:1,09:00, etc.
Multi-Tenant Isolation
Models using Core\Mod\Tenant\Concerns\BelongsToWorkspace are automatically scoped to the current workspace. The workspace_id is set on create and queries are filtered transparently.
Seeder Ordering
Seeders use PHP attributes for dependency ordering:
#[SeederPriority(50)] // Lower runs first (default 50)
#[SeederAfter(FeatureSeeder::class)]
class PackageSeeder extends Seeder { }
HLCRF Layout System
Data-driven layouts with five regions (Header, Left, Content, Right, Footer):
$page = Layout::make('HCF')->h(view('header'))->c($content)->f(view('footer'));
Variant strings: C (content only), HCF (standard page), HLCF (with sidebar), HLCRF (full dashboard).
Go Bridge
pkg/php/ contains Go code for a native desktop HTTP bridge (PHP-to-native calls), container/deployment utilities (Coolify), and Dockerfile generation.
Testing
Uses Orchestra Testbench with in-memory SQLite. Tests can live:
tests/Feature/andtests/Unit/- main test suitessrc/Core/{Package}/Tests/- L1 package co-located testssrc/Mod/{Module}/Tests/- module co-located tests
Test fixtures are in tests/Fixtures/. Base test class provides:
$this->getFixturePath('Mod') // Returns tests/Fixtures/Mod path