Commands must be in `.claude-plugin/commands/` for Claude Code auto-discovery.
Fixed plugin.json to use `{name, description, file}` array format.
Co-Authored-By: Virgil <virgil@lethean.io>
187 lines
6.2 KiB
Markdown
187 lines
6.2 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
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/Service/ # Service discovery and dependency resolution
|
||
src/Core/Storage/ # Storage with Redis circuit breaker + fallback
|
||
src/Core/Webhook/ # Webhook system + CronTrigger scheduled action
|
||
```
|
||
|
||
### Module Pattern
|
||
|
||
```php
|
||
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:
|
||
|
||
```php
|
||
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.
|
||
|
||
```php
|
||
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:
|
||
|
||
```php
|
||
#[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):
|
||
|
||
```php
|
||
$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/` and `tests/Unit/` - main test suites
|
||
- `src/Core/{Package}/Tests/` - L1 package co-located tests
|
||
- `src/Mod/{Module}/Tests/` - module co-located tests
|
||
|
||
Test fixtures are in `tests/Fixtures/`. Base test class provides:
|
||
```php
|
||
$this->getFixturePath('Mod') // Returns tests/Fixtures/Mod path
|
||
```
|