php/CLAUDE.md
Snider e27f8b1088
Some checks failed
CI / PHP 8.4 (push) Failing after 2m3s
CI / PHP 8.3 (push) Failing after 2m11s
fix(plugin): add commands/ auto-discover directory, fix plugin.json schema
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>
2026-03-13 10:18:41 +00:00

6.2 KiB
Raw Permalink Blame History

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/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

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/ 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:

$this->getFixturePath('Mod')  // Returns tests/Fixtures/Mod path