From f1c4c8f46d02d7dfa65a104e17ae9d629ea50f36 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 26 Jan 2026 14:25:55 +0000 Subject: [PATCH] feat: add initial framework files including API, console, and web routes; set up testing structure --- .claude/skills/core-patterns.md | 455 +++++++++++ README.md | 248 +++--- TODO.md | 45 ++ bootstrap/app.php | 19 + composer.json | 2 + config/database.php | 37 + config/mcp.php | 160 ++++ docs/patterns.md | 1336 +++++++++++++++++++++++++++++++ phpunit.xml | 55 ++ routes/api.php | 3 + routes/console.php | 3 + routes/web.php | 7 + tests/TestCase.php | 20 + 13 files changed, 2257 insertions(+), 133 deletions(-) create mode 100644 .claude/skills/core-patterns.md create mode 100644 TODO.md create mode 100644 bootstrap/app.php create mode 100644 config/mcp.php create mode 100644 docs/patterns.md create mode 100644 phpunit.xml create mode 100644 routes/api.php create mode 100644 routes/console.php create mode 100644 routes/web.php create mode 100644 tests/TestCase.php diff --git a/.claude/skills/core-patterns.md b/.claude/skills/core-patterns.md new file mode 100644 index 0000000..05dbb94 --- /dev/null +++ b/.claude/skills/core-patterns.md @@ -0,0 +1,455 @@ +--- +name: core-patterns +description: Scaffold Core PHP Framework patterns (Actions, Multi-tenant, Activity Logging, Modules, Seeders) +--- + +# Core Patterns Scaffolding + +You are helping the user scaffold common Core PHP Framework patterns. This is an interactive skill - gather information through conversation before generating code. + +## Start by asking what the user wants to create + +Present these options: + +1. **Action class** - Single-purpose business logic class +2. **Multi-tenant model** - Add workspace isolation to a model +3. **Activity logging** - Add change tracking to a model +4. **Module** - Create a new module with Boot class +5. **Seeder** - Create a seeder with dependency ordering + +Ask: "What would you like to scaffold? (1-5 or describe what you need)" + +--- + +## Option 1: Action Class + +Actions are small, focused classes that do one thing well. They extract complex logic from controllers and Livewire components. + +### Gather information + +Ask the user for: +- **Action name** (e.g., `CreateInvoice`, `PublishPost`, `SendNotification`) +- **Module** (e.g., `Billing`, `Content`, `Notification`) +- **What it does** (brief description to understand parameters needed) + +### Generate the Action + +Location: `packages/core-php/src/Mod/{Module}/Actions/{ActionName}.php` + +```php +handle($param1, $param2); + * + * // Or via static helper: + * $result = {ActionName}::run($param1, $param2); + */ +class {ActionName} +{ + use Action; + + public function __construct( + // Inject dependencies here + ) {} + + /** + * Execute the action. + */ + public function handle(/* parameters */): mixed + { + // Implementation + } +} +``` + +### Key points to explain + +- Actions use the `Core\Actions\Action` trait for the static `run()` helper +- Dependencies are constructor-injected +- The `handle()` method contains the business logic +- Can optionally implement `Core\Actions\Actionable` for type-hinting +- Naming convention: verb + noun (CreateThing, UpdateThing, DeleteThing) + +--- + +## Option 2: Multi-tenant Model + +The `BelongsToWorkspace` trait enforces workspace isolation with automatic scoping and caching. + +### Gather information + +Ask the user for: +- **Model name** (e.g., `Invoice`, `Project`) +- **Whether workspace context is always required** (default: yes) + +### Migration requirement + +Ensure the model's table has a `workspace_id` column: + +```php +$table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); +``` + +### Add the trait + +```php +where('status', 'paid')->get(); + +// Cached collection for current workspace +$invoices = Invoice::ownedByCurrentWorkspaceCached(); + +// Query for specific workspace +$invoices = Invoice::forWorkspace($workspace)->get(); + +// Check ownership +if ($invoice->belongsToCurrentWorkspace()) { + // safe to display +} +``` + +--- + +## Option 3: Activity Logging + +The `LogsActivity` trait wraps spatie/laravel-activitylog with framework defaults and workspace tagging. + +### Gather information + +Ask the user for: +- **Model name** to add logging to +- **Which attributes to log** (all, or specific ones) +- **Which events to log** (created, updated, deleted - default: all) + +### Add the trait + +```php +properties = $activity->properties->merge([ + 'custom_field' => $this->some_field, + ]); +} +``` + +### Key points to explain + +- Automatically includes `workspace_id` in activity properties +- Empty logs are not submitted +- Uses sensible defaults that can be overridden via model properties +- Can temporarily disable logging with `Model::withoutActivityLogging(fn() => ...)` + +--- + +## Option 4: Module + +Modules are the core organizational unit. Each module has a Boot class that declares which lifecycle events it listens to. + +### Gather information + +Ask the user for: +- **Module name** (e.g., `Billing`, `Notifications`) +- **What the module provides** (web routes, admin panel, API, console commands) + +### Create the directory structure + +``` +packages/core-php/src/Mod/{ModuleName}/ +├── Boot.php # Module entry point +├── Models/ # Eloquent models +├── Actions/ # Business logic +├── Routes/ +│ ├── web.php # Web routes +│ └── api.php # API routes +├── View/ +│ └── Blade/ # Blade views +├── Console/ # Artisan commands +├── Database/ +│ ├── Migrations/ # Database migrations +│ └── Seeders/ # Database seeders +└── Lang/ + └── en_GB/ # Translations +``` + +### Generate Boot.php + +```php + + */ + public static array $listens = [ + WebRoutesRegistering::class => 'onWebRoutes', + ApiRoutesRegistering::class => 'onApiRoutes', + AdminPanelBooting::class => 'onAdminPanel', + ConsoleBooting::class => 'onConsole', + ]; + + public function register(): void + { + // Register singletons and bindings + } + + public function boot(): void + { + $this->loadMigrationsFrom(__DIR__.'/Database/Migrations'); + $this->loadTranslationsFrom(__DIR__.'/Lang/en_GB', $this->moduleName); + } + + // ------------------------------------------------------------------------- + // Event-driven handlers + // ------------------------------------------------------------------------- + + public function onWebRoutes(WebRoutesRegistering $event): void + { + $event->views($this->moduleName, __DIR__.'/View/Blade'); + + if (file_exists(__DIR__.'/Routes/web.php')) { + $event->routes(fn () => Route::middleware('web')->group(__DIR__.'/Routes/web.php')); + } + + // Register Livewire components + // $event->livewire('{module}.component-name', View\Components\ComponentName::class); + } + + public function onApiRoutes(ApiRoutesRegistering $event): void + { + if (file_exists(__DIR__.'/Routes/api.php')) { + $event->routes(fn () => Route::middleware('api')->group(__DIR__.'/Routes/api.php')); + } + } + + public function onAdminPanel(AdminPanelBooting $event): void + { + $event->views($this->moduleName, __DIR__.'/View/Blade'); + } + + public function onConsole(ConsoleBooting $event): void + { + // Register commands + // $event->command(Console\MyCommand::class); + } +} +``` + +### Available lifecycle events + +| Event | Purpose | Handler receives | +|-------|---------|------------------| +| `WebRoutesRegistering` | Public web routes | views, routes, livewire | +| `AdminPanelBooting` | Admin panel setup | views, routes | +| `ApiRoutesRegistering` | REST API routes | routes | +| `ClientRoutesRegistering` | Authenticated client routes | routes | +| `ConsoleBooting` | Artisan commands | command, middleware | +| `McpToolsRegistering` | MCP tools | tools | +| `FrameworkBooted` | Late initialization | - | + +### Key points to explain + +- The `$listens` array declares which events trigger which methods +- Modules are lazy-loaded - only instantiated when their events fire +- Keep Boot classes thin - delegate to services and actions +- Use the `$moduleName` for consistent view namespace and translations + +--- + +## Option 5: Seeder with Dependencies + +Seeders can declare ordering via attributes for dependencies between seeders. + +### Gather information + +Ask the user for: +- **Seeder name** (e.g., `PackageSeeder`, `DemoDataSeeder`) +- **Module** it belongs to +- **Dependencies** - which seeders must run before this one +- **Priority** (optional) - lower numbers run first (default: 50) + +### Generate the Seeder + +Location: `packages/core-php/src/Mod/{Module}/Database/Seeders/{SeederName}.php` + +```php + [ - app_path('Core'), - app_path('Mod'), - ], -]; -``` - -## Creating Modules - -Use the artisan commands to scaffold modules: - -```bash -# Create a full module php artisan make:mod Commerce - -# Create a website module (domain-scoped) -php artisan make:website Marketing - -# Create a plugin -php artisan make:plug Stripe ``` -## Module Structure - -Modules are organised with a `Boot.php` entry point: - -``` -app/Mod/Commerce/ -├── Boot.php -├── Routes/ -│ ├── web.php -│ ├── admin.php -│ └── api.php -├── Views/ -└── config.php -``` - -## Lifecycle Events - -Modules declare interest in lifecycle events via a static `$listens` array: +This creates a module at `app/Mod/Commerce/` with a `Boot.php` entry point: ```php routes(fn () => require __DIR__.'/routes.php'); +use Core\Actions\Action; -// Register view namespace -$event->views('namespace', __DIR__.'/Views'); +class CreateOrder +{ + use Action; -// Register Livewire component -$event->livewire('alias', ComponentClass::class); + public function handle(User $user, array $data): Order + { + // Business logic here + return Order::create($data); + } +} -// Register navigation item -$event->navigation(['label' => 'Products', 'icon' => 'box']); - -// Register Artisan command (ConsoleBooting) -$event->command(MyCommand::class); - -// Register middleware alias -$event->middleware('alias', MiddlewareClass::class); - -// Register translations -$event->translations('namespace', __DIR__.'/lang'); - -// Register Blade component path -$event->bladeComponentPath(__DIR__.'/components', 'prefix'); - -// Register policy -$event->policy(Model::class, Policy::class); +// Usage +$order = CreateOrder::run($user, $validated); ``` -## Firing Events +### Multi-Tenant Isolation -Create frontage service providers to fire events at appropriate times: +Automatic workspace scoping for models: ```php -use Core\LifecycleEventProvider; +use Core\Mod\Tenant\Concerns\BelongsToWorkspace; -class WebServiceProvider extends ServiceProvider +class Product extends Model { - public function boot(): void - { - LifecycleEventProvider::fireWebRoutes(); - } + use BelongsToWorkspace; +} + +// Queries are automatically scoped to the current workspace +$products = Product::all(); + +// workspace_id is auto-assigned on create +$product = Product::create(['name' => 'Widget']); +``` + +### Activity Logging + +Track model changes with minimal setup: + +```php +use Core\Activity\Concerns\LogsActivity; + +class Order extends Model +{ + use LogsActivity; + + protected array $activityLogAttributes = ['status', 'total']; } ``` -## Lazy Loading +### HLCRF Layout System -Modules are only instantiated when their subscribed events fire. A web request doesn't load admin-only modules. An API request doesn't load web modules. This keeps your application fast. - -## Custom Namespace Mapping - -For non-standard directory structures: +Data-driven layouts with infinite nesting: ```php -$scanner = app(ModuleScanner::class); -$scanner->setNamespaceMap([ - 'CustomMod' => 'App\\CustomMod', -]); +use Core\Front\Components\Layout; + +$page = Layout::make('HCF') + ->h('') + ->c('
Main content
') + ->f(''); + +echo $page; ``` -## Contracts +Variant strings define structure: `HCF` (Header-Content-Footer), `HLCRF` (all five regions), `H[LC]CF` (nested layouts). -### AdminMenuProvider +See [HLCRF.md](packages/core-php/src/Core/Front/HLCRF.md) for full documentation. -Implement for admin navigation: +## Configuration + +Publish the config file: + +```bash +php artisan vendor:publish --tag=core-config +``` + +Configure module paths in `config/core.php`: ```php -use Core\Front\Admin\Contracts\AdminMenuProvider; - -class Boot implements AdminMenuProvider -{ - public function adminMenuItems(): array - { - return [ - [ - 'group' => 'services', - 'priority' => 20, - 'item' => fn () => [ - 'label' => 'Products', - 'icon' => 'box', - 'href' => route('admin.products.index'), - ], - ], - ]; - } -} +return [ + 'module_paths' => [ + app_path('Core'), + app_path('Mod'), + ], +]; ``` -### ServiceDefinition +## Artisan Commands -For SaaS service registration: - -```php -use Core\Service\Contracts\ServiceDefinition; - -class Boot implements ServiceDefinition -{ - public static function definition(): array - { - return [ - 'code' => 'commerce', - 'module' => 'Commerce', - 'name' => 'Commerce', - 'tagline' => 'E-commerce platform', - ]; - } -} +```bash +php artisan make:mod Commerce # Create a module +php artisan make:website Marketing # Create a website module +php artisan make:plug Stripe # Create a plugin ``` +## Module Structure + +``` +app/Mod/Commerce/ +├── Boot.php # Module entry point +├── Actions/ # Business logic +├── Models/ # Eloquent models +├── Routes/ +│ ├── web.php +│ ├── admin.php +│ └── api.php +├── Views/ +├── Migrations/ +└── config.php +``` + +## Documentation + +- [Patterns Guide](docs/patterns.md) - Detailed documentation for all framework patterns +- [HLCRF Layout System](packages/core-php/src/Core/Front/HLCRF.md) - Composable layout documentation + ## Testing ```bash composer test ``` +## Requirements + +- PHP 8.2+ +- Laravel 11+ + ## License -EUPL-1.2. See [LICENSE](LICENSE) for details. +EUPL-1.2 - See [LICENSE](LICENSE) for details. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6dda50f --- /dev/null +++ b/TODO.md @@ -0,0 +1,45 @@ +# Core PHP Framework - TODO + +## Code Cleanup + +- [ ] **ApiExplorer** - Update biolinks endpoint examples + +--- + +## Completed (January 2026) + +### Security Fixes + +- [x] **MCP: Database Connection Fallback** - Fixed to throw exception instead of silently falling back to default connection + - See: `packages/core-mcp/changelog/2026/jan/security.md` + +- [x] **MCP: SQL Validator Regex** - Strengthened WHERE clause patterns to prevent SQL injection vectors + - See: `packages/core-mcp/changelog/2026/jan/security.md` + +### Features + +- [x] **MCP: EXPLAIN Plan** - Added query optimization analysis with human-readable performance insights + - See: `packages/core-mcp/changelog/2026/jan/features.md` + +- [x] **CDN: Integration Tests** - Comprehensive test suite for CDN operations and asset pipeline + - See: `packages/core-php/changelog/2026/jan/features.md` + +### Documentation & Code Quality + +- [x] **API docs** - Genericized vendor-specific content (removed Host UK branding, lt.hn references) + - See: `packages/core-api/changelog/2026/jan/features.md` + +- [x] **Admin: Route Audit** - Verified admin routes use Livewire modals instead of traditional controllers; #[Action] attributes not applicable + +- [x] **ServicesAdmin** - Reviewed stubbed bio service methods; intentionally stubbed pending module extraction (documented with TODO comments) + +--- + +## Package Changelogs + +For complete feature lists and implementation details: +- `packages/core-php/changelog/2026/jan/features.md` +- `packages/core-admin/changelog/2026/jan/features.md` +- `packages/core-api/changelog/2026/jan/features.md` +- `packages/core-mcp/changelog/2026/jan/features.md` +- `packages/core-mcp/changelog/2026/jan/security.md` ⚠️ Security fixes diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..d654276 --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,19 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + // + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/composer.json b/composer.json index d8b3e9f..b0b7486 100644 --- a/composer.json +++ b/composer.json @@ -39,6 +39,8 @@ "psr-4": { "Tests\\": "tests/", "Core\\Tests\\": "packages/core-php/tests/", + "Core\\Mod\\Mcp\\Tests\\": "packages/core-mcp/tests/", + "Core\\Mod\\Tenant\\Tests\\": "packages/core-php/src/Mod/Tenant/Tests/", "Mod\\": "packages/core-php/tests/Fixtures/Mod/", "Plug\\": "packages/core-php/tests/Fixtures/Plug/", "Website\\": "packages/core-php/tests/Fixtures/Website/" diff --git a/config/database.php b/config/database.php index df933e7..033b966 100644 --- a/config/database.php +++ b/config/database.php @@ -113,6 +113,43 @@ return [ // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), ], + /* + |-------------------------------------------------------------------------- + | MCP Read-Only Connection + |-------------------------------------------------------------------------- + | + | This connection is used by the MCP QueryDatabase tool. It should be + | configured with a database user that has SELECT-only permissions. + | + | For MySQL, create a read-only user: + | CREATE USER 'mcp_readonly'@'localhost' IDENTIFIED BY 'password'; + | GRANT SELECT ON your_database.* TO 'mcp_readonly'@'localhost'; + | FLUSH PRIVILEGES; + | + | If MCP_DB_CONNECTION is not set, this falls back to the default connection. + | In production, always configure a dedicated read-only user. + | + */ + 'mcp_readonly' => [ + 'driver' => env('MCP_DB_DRIVER', env('DB_CONNECTION', 'mysql')), + 'url' => env('MCP_DB_URL'), + 'host' => env('MCP_DB_HOST', env('DB_HOST', '127.0.0.1')), + 'port' => env('MCP_DB_PORT', env('DB_PORT', '3306')), + 'database' => env('MCP_DB_DATABASE', env('DB_DATABASE', 'laravel')), + 'username' => env('MCP_DB_USERNAME', env('DB_USERNAME', 'root')), + 'password' => env('MCP_DB_PASSWORD', env('DB_PASSWORD', '')), + 'unix_socket' => env('MCP_DB_SOCKET', env('DB_SOCKET', '')), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ], /* diff --git a/config/mcp.php b/config/mcp.php new file mode 100644 index 0000000..e271434 --- /dev/null +++ b/config/mcp.php @@ -0,0 +1,160 @@ + [ + /* + |-------------------------------------------------------------------------- + | Read-Only Connection + |-------------------------------------------------------------------------- + | + | The database connection to use for MCP query execution. This should + | be configured with a read-only database user for defence in depth. + | + | Set to null to use the default connection (not recommended for production). + | + */ + 'connection' => env('MCP_DB_CONNECTION', 'mcp_readonly'), + + /* + |-------------------------------------------------------------------------- + | Query Whitelist + |-------------------------------------------------------------------------- + | + | Enable or disable whitelist-based query validation. When enabled, + | queries must match at least one pattern in the whitelist to execute. + | + */ + 'use_whitelist' => env('MCP_DB_USE_WHITELIST', true), + + /* + |-------------------------------------------------------------------------- + | Custom Whitelist Patterns + |-------------------------------------------------------------------------- + | + | Additional regex patterns to allow. The default whitelist allows basic + | SELECT queries. Add patterns here for application-specific queries. + | + | Example: + | '/^\s*SELECT\s+.*\s+FROM\s+`?users`?\s+WHERE\s+id\s*=\s*\d+;?\s*$/i' + | + */ + 'whitelist_patterns' => [ + // Add custom patterns here + ], + + /* + |-------------------------------------------------------------------------- + | Blocked Tables + |-------------------------------------------------------------------------- + | + | Tables that cannot be queried even with valid SELECT queries. + | Use this to protect sensitive tables from MCP access. + | + */ + 'blocked_tables' => [ + 'users', + 'password_reset_tokens', + 'sessions', + 'personal_access_tokens', + 'failed_jobs', + ], + + /* + |-------------------------------------------------------------------------- + | Row Limit + |-------------------------------------------------------------------------- + | + | Maximum number of rows that can be returned from a query. + | This prevents accidentally returning huge result sets. + | + */ + 'max_rows' => env('MCP_DB_MAX_ROWS', 1000), + ], + + /* + |-------------------------------------------------------------------------- + | Tool Usage Analytics + |-------------------------------------------------------------------------- + | + | Configuration for MCP tool usage analytics and metrics tracking. + | + */ + + 'analytics' => [ + /* + |-------------------------------------------------------------------------- + | Enable Analytics + |-------------------------------------------------------------------------- + | + | Enable or disable tool usage analytics. When disabled, no metrics + | will be recorded for tool executions. + | + */ + 'enabled' => env('MCP_ANALYTICS_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Data Retention + |-------------------------------------------------------------------------- + | + | Number of days to retain analytics data before pruning. + | Use the mcp:prune-metrics command to clean up old data. + | + */ + 'retention_days' => env('MCP_ANALYTICS_RETENTION_DAYS', 90), + + /* + |-------------------------------------------------------------------------- + | Batch Size + |-------------------------------------------------------------------------- + | + | Number of metrics to accumulate before flushing to the database. + | Higher values improve write performance but may lose data on crashes. + | + */ + 'batch_size' => env('MCP_ANALYTICS_BATCH_SIZE', 100), + ], + + /* + |-------------------------------------------------------------------------- + | Log Retention + |-------------------------------------------------------------------------- + | + | Configuration for MCP log retention and cleanup. + | + */ + + 'log_retention' => [ + /* + |-------------------------------------------------------------------------- + | Detailed Logs Retention + |-------------------------------------------------------------------------- + | + | Number of days to retain detailed tool call logs. + | + */ + 'days' => env('MCP_LOG_RETENTION_DAYS', 90), + + /* + |-------------------------------------------------------------------------- + | Statistics Retention + |-------------------------------------------------------------------------- + | + | Number of days to retain aggregated statistics. + | Should typically be longer than detailed logs. + | + */ + 'stats_days' => env('MCP_LOG_RETENTION_STATS_DAYS', 365), + ], + +]; diff --git a/docs/patterns.md b/docs/patterns.md new file mode 100644 index 0000000..de2d66e --- /dev/null +++ b/docs/patterns.md @@ -0,0 +1,1336 @@ +# Core PHP Framework Patterns + +This guide covers the key architectural patterns used throughout the Core PHP Framework. Each pattern includes guidance on when to use it, quick start examples, and common pitfalls to avoid. + +## Table of Contents + +1. [Actions Pattern](#1-actions-pattern) +2. [Multi-Tenant Data Isolation](#2-multi-tenant-data-isolation) +3. [Module System (Lifecycle Events)](#3-module-system-lifecycle-events) +4. [Activity Logging](#4-activity-logging) +5. [Seeder Auto-Discovery](#5-seeder-auto-discovery) +6. [Service Definition](#6-service-definition) + +--- + +## 1. Actions Pattern + +**Location:** `packages/core-php/src/Core/Actions/` + +Actions are single-purpose classes that encapsulate business logic. They extract complex operations from controllers and Livewire components into testable, reusable units. + +### When to Use + +- Business operations with multiple steps (validation, authorization, persistence, side effects) +- Logic that should be reusable across controllers, commands, and API endpoints +- Operations that benefit from dependency injection +- Any operation complex enough to warrant unit testing in isolation + +### When NOT to Use + +- Simple CRUD operations that don't need validation beyond form requests +- One-line operations that don't benefit from abstraction + +### Quick Start + +```php +posts->create([ + 'user_id' => $user->id, + 'title' => $data['title'], + 'content' => $data['content'], + ]); + + if (isset($data['image'])) { + $this->images->attach($post, $data['image']); + } + + return $post; + } +} +``` + +**Usage:** + +```php +// Via dependency injection (preferred) +public function __construct(private CreatePost $createPost) {} + +$post = $this->createPost->handle($user, $validated); + +// Via static helper +$post = CreatePost::run($user, $validated); + +// Via container +$post = app(CreatePost::class)->handle($user, $validated); +``` + +### Full Example + +Here's an Action that creates a project with entitlement checking: + +```php +defaultWorkspace(); + + // Check entitlements + if ($workspace) { + $this->checkEntitlements($workspace); + } + + // Generate unique slug if not provided + $data['slug'] = $data['slug'] ?? $this->generateUniqueSlug($data['name']); + $data['user_id'] = $user->id; + $data['workspace_id'] = $data['workspace_id'] ?? $workspace?->id; + + // Create the project + $project = Project::create($data); + + // Record usage + if ($workspace) { + $this->entitlements->recordUsage( + $workspace, + 'projects.count', + 1, + $user, + ['project_id' => $project->id] + ); + } + + // Log activity + Activity::causedBy($user) + ->performedOn($project) + ->withProperties(['name' => $project->name]) + ->log('created'); + + return $project; + } + + public static function run(User $user, array $data): Project + { + return app(static::class)->handle($user, $data); + } + + protected function checkEntitlements(Workspace $workspace): void + { + $result = $this->entitlements->can($workspace, 'projects.count'); + + if ($result->isDenied()) { + throw new EntitlementException( + "You have reached your project limit. Please upgrade your plan.", + 'projects.count' + ); + } + } + + protected function generateUniqueSlug(string $name): string + { + $slug = \Illuminate\Support\Str::slug($name); + $original = $slug; + $counter = 1; + + while (Project::where('slug', $slug)->exists()) { + $slug = $original . '-' . $counter++; + } + + return $slug; + } +} +``` + +### Directory Structure + +``` +Mod/Example/Actions/ +├── CreateThing.php +├── UpdateThing.php +├── DeleteThing.php +└── Thing/ + ├── PublishThing.php + └── ArchiveThing.php +``` + +### Configuration + +Actions use Laravel's service container for dependency injection. No additional configuration is required. + +### Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Too many responsibilities | Keep actions focused on one operation. Split into multiple actions if needed. | +| Returning void | Always return something useful (the created/updated model, a result DTO). | +| Not using dependency injection | Inject dependencies via constructor, not `app()` calls inside methods. | +| Catching all exceptions | Let exceptions bubble up for proper error handling. | +| Direct database queries | Use repositories or model methods for testability. | + +--- + +## 2. Multi-Tenant Data Isolation + +**Location:** `packages/core-php/src/Mod/Tenant/` + +The multi-tenant system ensures data isolation between workspaces using global scopes and traits. This is a **security-critical** pattern that prevents cross-tenant data leakage. + +### When to Use + +- Any model that "belongs" to a workspace and should be isolated +- Data that must never be visible across tenant boundaries +- Resources that should be automatically scoped to the current workspace context + +### Key Components + +| Component | Purpose | +|-----------|---------| +| `BelongsToWorkspace` trait | Add to models that belong to a workspace | +| `WorkspaceScope` | Global scope that filters queries | +| `MissingWorkspaceContextException` | Security exception when context is missing | + +### Quick Start + +```php +where('is_active', true); + } +} +``` + +**Using the model:** + +```php +// Automatically scoped to current workspace +$products = Product::all(); +$product = Product::where('sku', 'ABC123')->first(); +$activeProducts = Product::active()->get(); + +// Creating - workspace_id is auto-assigned +$product = Product::create(['name' => 'Widget', 'sku' => 'W001']); + +// Cached collection for current workspace +$products = Product::ownedByCurrentWorkspaceCached(); + +// Query a specific workspace (admin use) +$products = Product::forWorkspace($workspace)->get(); + +// Query across all workspaces (admin use - be careful!) +$allProducts = Product::acrossWorkspaces()->get(); +``` + +### Strict Mode vs Permissive Mode + +**Strict mode (default):** Throws `MissingWorkspaceContextException` when workspace context is unavailable. This is the secure default. + +```php +// In strict mode, this throws an exception if no workspace context +$products = Product::all(); // MissingWorkspaceContextException +``` + +**Permissive mode:** Returns empty results instead of throwing. Use sparingly. + +```php +// Disable strict mode globally (not recommended) +WorkspaceScope::disableStrictMode(); + +// Disable for a specific callback +WorkspaceScope::withoutStrictMode(function () { + // Operations here return empty results if no context + $products = Product::all(); // Returns empty collection +}); + +// Disable for a specific model +class LegacyProduct extends Model +{ + use BelongsToWorkspace; + + protected bool $workspaceScopeStrict = false; // Not recommended +} +``` + +### When to Use `forWorkspace()` vs `acrossWorkspaces()` + +| Method | Use Case | +|--------|----------| +| `forWorkspace($workspace)` | Admin viewing a specific workspace's data | +| `acrossWorkspaces()` | Global reports, admin dashboards, CLI commands | +| Neither (default) | Normal application code - uses current context | + +**Always prefer the default scoping** unless you have a specific reason to query across workspaces. + +### Security Considerations + +1. **Never disable strict mode globally** in production +2. **Audit uses of `acrossWorkspaces()`** - each use should be justified +3. **CLI commands** automatically bypass strict mode (they have no request context) +4. **Test data isolation** - write tests that verify cross-tenant queries fail +5. **The `workspace_id` column must exist** on all tenant-scoped tables + +### Configuration + +```php +// In migrations - always add workspace_id +Schema::create('products', function (Blueprint $table) { + $table->id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + // ... + $table->index('workspace_id'); // Important for performance +}); +``` + +### Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Forgetting `workspace_id` in migrations | Always add the foreign key and index | +| Using `acrossWorkspaces()` casually | Audit every usage, prefer default scoping | +| Suppressing the exception | Don't catch `MissingWorkspaceContextException` - fix the context | +| Direct DB queries | Use Eloquent models so scopes apply | +| Joining across tenant boundaries | Ensure joins respect workspace_id | + +--- + +## 3. Module System (Lifecycle Events) + +**Location:** `packages/core-php/src/Core/Events/`, `packages/core-php/src/Core/Module/` + +The module system uses lifecycle events for lazy-loading modules. Modules declare what events they listen to, and are only instantiated when those events fire. + +### When to Use + +- Creating a new feature module +- Registering routes, views, Livewire components +- Adding admin panel navigation +- Registering console commands + +### How It Works + +``` +Application Start + │ + ▼ +ModuleScanner scans for Boot.php files with $listens arrays + │ + ▼ +ModuleRegistry registers lazy listeners for each event-module pair + │ + ▼ +Request comes in → Appropriate lifecycle event fires + │ + ▼ +LazyModuleListener instantiates module and calls handler + │ + ▼ +Module registers its resources (routes, views, etc.) +``` + +### Quick Start + +Create a `Boot.php` file in your module directory: + +```php + 'onWebRoutes', + ]; + + public function onWebRoutes(WebRoutesRegistering $event): void + { + $event->views('example', __DIR__.'/Views'); + $event->routes(fn () => require __DIR__.'/Routes/web.php'); + } +} +``` + +### Available Lifecycle Events + +| Event | Context | When It Fires | +|-------|---------|---------------| +| `WebRoutesRegistering` | Web requests | Public frontend routes | +| `AdminPanelBooting` | Admin requests | Admin panel setup | +| `ApiRoutesRegistering` | API requests | REST API routes | +| `ClientRoutesRegistering` | Client dashboard | Authenticated client routes | +| `ConsoleBooting` | CLI commands | Artisan booting | +| `QueueWorkerBooting` | Queue workers | Queue worker starting | +| `McpToolsRegistering` | MCP server | MCP tool registration | +| `FrameworkBooted` | All contexts | After all context-specific events | + +### Full Example + +A complete module Boot class: + +```php + 'onAdminPanel', + ApiRoutesRegistering::class => 'onApiRoutes', + WebRoutesRegistering::class => 'onWebRoutes', + ConsoleBooting::class => 'onConsole', + ]; + + public function register(): void + { + // Register service bindings + $this->app->singleton( + Services\InventoryService::class, + Services\InventoryService::class + ); + } + + public function boot(): void + { + // Load migrations + $this->loadMigrationsFrom(__DIR__.'/Migrations'); + $this->loadTranslationsFrom(__DIR__.'/Lang/en', 'inventory'); + } + + public function onAdminPanel(AdminPanelBooting $event): void + { + $event->views($this->moduleName, __DIR__.'/View/Blade'); + + // Navigation + $event->navigation([ + 'label' => 'Inventory', + 'icon' => 'box', + 'route' => 'admin.inventory.index', + 'sort_order' => 50, + ]); + + // Livewire components + $event->livewire('inventory-list', View\Livewire\InventoryList::class); + $event->livewire('product-form', View\Livewire\ProductForm::class); + } + + public function onApiRoutes(ApiRoutesRegistering $event): void + { + $event->routes(fn () => Route::middleware('api') + ->prefix('v1/inventory') + ->group(__DIR__.'/Routes/api.php')); + } + + public function onWebRoutes(WebRoutesRegistering $event): void + { + $event->views($this->moduleName, __DIR__.'/View/Blade'); + $event->routes(fn () => Route::middleware('web') + ->group(__DIR__.'/Routes/web.php')); + } + + public function onConsole(ConsoleBooting $event): void + { + $event->command(Console\SyncInventoryCommand::class); + $event->command(Console\PruneStaleStockCommand::class); + } +} +``` + +### Available Request Methods + +Events provide these methods for registering resources: + +| Method | Purpose | +|--------|---------| +| `routes(callable)` | Register route files/callbacks | +| `views(namespace, path)` | Register view namespaces | +| `livewire(alias, class)` | Register Livewire components | +| `middleware(alias, class)` | Register middleware aliases | +| `command(class)` | Register Artisan commands | +| `translations(namespace, path)` | Register translation namespaces | +| `bladeComponentPath(path, namespace)` | Register anonymous Blade components | +| `policy(model, policy)` | Register model policies | +| `navigation(item)` | Register navigation items | + +### The Request/Collect Pattern + +Events use a "request/collect" pattern: + +1. **Modules request** resources via methods like `routes()`, `views()` +2. **Requests are collected** in arrays during event dispatch +3. **LifecycleEventProvider processes** all requests with validation + +This ensures modules don't directly mutate infrastructure and allows central validation. + +### Module Directory Structure + +``` +Mod/Inventory/ +├── Boot.php # Module entry point +├── Routes/ +│ ├── web.php +│ └── api.php +├── View/ +│ ├── Blade/ # Blade templates +│ │ └── admin/ +│ └── Livewire/ # Livewire components +├── Models/ +├── Actions/ +├── Services/ +├── Console/ +├── Migrations/ +└── Lang/ +``` + +### Configuration + +Namespace detection is automatic based on path: +- `/Core` paths → `Core\` namespace +- `/Mod` paths → `Mod\` namespace +- `/Website` paths → `Website\` namespace +- `/Plug` paths → `Plug\` namespace + +### Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Registering routes outside events | Always use `$event->routes()` in handlers | +| Heavy work in `$listens` handlers | Keep handlers lightweight; defer work | +| Forgetting to add `$listens` | Module won't load without static `$listens` | +| Using wrong event | Match event to request context (web, api, admin) | +| Instantiating in `$listens` | The array is read statically; don't call methods | + +--- + +## 4. Activity Logging + +**Location:** `packages/core-php/src/Core/Activity/` + +Activity logging tracks model changes and user actions. Built on spatie/laravel-activitylog with workspace-aware enhancements. + +### When to Use + +- Audit trails for compliance +- User activity history +- Model change tracking +- Debugging and support + +### Quick Start + +Add the trait to any model: + +```php +properties = $activity->properties->merge([ + 'contract_number' => $this->contract_number, + 'client_id' => $this->client_id, + ]); + } +} +``` + +**Querying activity logs:** + +```php +use Core\Activity\Services\ActivityLogService; + +$service = app(ActivityLogService::class); + +// Get activities for a specific model +$activities = $service->logFor($contract)->recent(20); + +// Get activities by a user in a workspace +$activities = $service + ->logBy($user) + ->forWorkspace($workspace) + ->lastDays(7) + ->paginate(25); + +// Get all "updated" events for a model type +$activities = $service + ->forSubjectType(Contract::class) + ->ofType('updated') + ->between('2024-01-01', '2024-12-31') + ->get(); + +// Search activities +$results = $service->search('approved contract'); + +// Get statistics +$stats = $service->statistics($workspace); +// Returns: ['total' => 150, 'by_event' => [...], 'by_subject' => [...], 'by_user' => [...]] +``` + +**Using the Activity model directly:** + +```php +use Core\Activity\Models\Activity; + +// Get activities for a workspace +$activities = Activity::forWorkspace($workspace)->newest()->get(); + +// Filter by event type +$created = Activity::createdEvents()->lastDays(30)->get(); +$deleted = Activity::deletedEvents()->withDeletedSubject()->get(); + +// Get activities with changes +$withChanges = Activity::withChanges()->get(); + +// Access change details +foreach ($activities as $activity) { + echo $activity->description; + echo $activity->causer_name; // "John Doe" or "System" + echo $activity->subject_name; // "Contract #123" + + // Get specific changes + foreach ($activity->changes as $field => $values) { + echo "{$field}: {$values['old']} -> {$values['new']}"; + } +} +``` + +### Configuration + +Configure via model properties: + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `$activityLogAttributes` | array | null (all) | Attributes to log | +| `$activityLogName` | string | 'default' | Log name for grouping | +| `$activityLogEvents` | array | ['created', 'updated', 'deleted'] | Events to log | +| `$activityLogWorkspace` | bool | true | Include workspace_id | +| `$activityLogOnlyDirty` | bool | true | Only log changed attributes | + +**Global configuration (config/core.php):** + +```php +'activity' => [ + 'enabled' => env('ACTIVITY_LOG_ENABLED', true), + 'log_name' => 'default', + 'include_workspace' => true, + 'default_events' => ['created', 'updated', 'deleted'], + 'retention_days' => 90, +], +``` + +### Temporarily Disabling Logging + +```php +// Disable for a callback +Document::withoutActivityLogging(function () { + $document->update(['status' => 'processed']); +}); + +// Check if logging is enabled +if (Document::activityLoggingEnabled()) { + // ... +} +``` + +### Pruning Old Logs + +```bash +# Via artisan command +php artisan activity:prune --days=90 + +# Via service +$service = app(ActivityLogService::class); +$deleted = $service->prune(90); // Delete logs older than 90 days +``` + +### Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Logging sensitive data | Exclude sensitive fields via `$activityLogAttributes` | +| Excessive logging | Log only meaningful changes, not every field | +| Performance issues | Use `withoutActivityLogging()` for bulk operations | +| Missing workspace context | Ensure workspace is set before logging | +| Large properties | Limit logged data; don't log file contents | + +--- + +## 5. Seeder Auto-Discovery + +**Location:** `packages/core-php/src/Core/Database/Seeders/` + +The seeder auto-discovery system scans module directories for seeders and executes them in the correct order based on priority and dependencies. + +### When to Use + +- Creating seeders for a module +- Seeding reference data (features, packages, configuration) +- Establishing dependencies between seeders +- Demo/test data setup + +### Quick Start + +Create a seeder in your module's `Database/Seeders/` directory: + +```php + 'example.feature'], + ['name' => 'Example Feature', 'type' => 'boolean'] + ); + } +} +``` + +Seeders are discovered automatically - no manual registration required. + +### Priority and Dependencies + +**Using priority (simple ordering):** + +```php +// Using class property +class FeatureSeeder extends Seeder +{ + public int $priority = 10; // Runs early +} + +// Using attribute +use Core\Database\Seeders\Attributes\SeederPriority; + +#[SeederPriority(10)] +class FeatureSeeder extends Seeder +{ + public function run(): void { /* ... */ } +} +``` + +**Priority guidelines:** + +| Range | Use Case | +|-------|----------| +| 0-20 | Foundation (features, config) | +| 20-40 | Core data (packages, workspaces) | +| 40-60 | Default (general seeders) | +| 60-80 | Content (pages, posts) | +| 80-100 | Demo/test data | + +**Using dependencies (explicit ordering):** + +```php +use Core\Database\Seeders\Attributes\SeederAfter; +use Core\Database\Seeders\Attributes\SeederBefore; +use Core\Mod\Tenant\Database\Seeders\FeatureSeeder; + +// This seeder runs AFTER FeatureSeeder completes +#[SeederAfter(FeatureSeeder::class)] +class PackageSeeder extends Seeder +{ + public function run(): void + { + // Can safely reference features created by FeatureSeeder + } +} + +// This seeder runs BEFORE PackageSeeder +#[SeederBefore(PackageSeeder::class)] +class FeatureSeeder extends Seeder +{ + public function run(): void + { + // Features are created first + } +} +``` + +**Multiple dependencies:** + +```php +#[SeederAfter(FeatureSeeder::class, PackageSeeder::class)] +class WorkspaceSeeder extends Seeder +{ + // Runs after both FeatureSeeder and PackageSeeder +} +``` + +### Full Example + +```php + 'free', + 'name' => 'Free', + 'price_monthly' => 0, + 'features' => ['basic.access'], + 'sort_order' => 1, + ], + [ + 'code' => 'pro', + 'name' => 'Professional', + 'price_monthly' => 29, + 'features' => ['basic.access', 'advanced.features', 'support.priority'], + 'sort_order' => 2, + ], + [ + 'code' => 'enterprise', + 'name' => 'Enterprise', + 'price_monthly' => 99, + 'features' => ['basic.access', 'advanced.features', 'support.priority', 'api.access'], + 'sort_order' => 3, + ], + ]; + + foreach ($plans as $planData) { + Plan::updateOrCreate( + ['code' => $planData['code']], + $planData + ); + } + + $this->command->info('Billing plans seeded successfully.'); + } +} +``` + +### How Discovery Works + +1. `SeederDiscovery` scans configured paths for `*Seeder.php` files +2. Files are read to extract namespace and class name +3. Priority and dependency attributes/properties are parsed +4. Seeders are topologically sorted (dependencies first, then by priority) +5. `CoreDatabaseSeeder` executes them in order + +### Configuration + +The discovery scans these paths by default: +- `app/Core/*/Database/Seeders/` +- `app/Mod/*/Database/Seeders/` +- `packages/core-php/src/Core/*/Database/Seeders/` +- `packages/core-php/src/Mod/*/Database/Seeders/` + +### Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Circular dependencies | Review dependencies; use priority instead if possible | +| Missing table errors | Check `Schema::hasTable()` before seeding | +| Non-idempotent seeders | Use `updateOrCreate()` or `firstOrCreate()` | +| Hardcoded IDs | Use codes/slugs for lookups, not numeric IDs | +| Dependencies on non-existent seeders | Ensure dependent seeders are discoverable | +| Forgetting to output progress | Use `$this->command->info()` for visibility | + +--- + +## 6. Service Definition + +**Location:** `packages/core-php/src/Core/Service/` + +Services are the product layer of the framework. They define how modules are presented as SaaS products with versioning, health checks, and dependency management. + +### When to Use + +- Defining a new SaaS product/service +- Adding health monitoring to a service +- Declaring service dependencies +- Managing service lifecycle (deprecation, sunset) + +### Key Components + +| Component | Purpose | +|-----------|---------| +| `ServiceDefinition` | Interface for service definitions | +| `ServiceVersion` | Semantic versioning with deprecation | +| `ServiceDependency` | Declare dependencies on other services | +| `HealthCheckable` | Interface for health monitoring | +| `HealthCheckResult` | Health check response object | + +### Quick Start + +```php + 'billing', + 'module' => 'Mod\\Billing', + 'name' => 'Billing', + 'tagline' => 'Subscription management', + 'icon' => 'credit-card', + 'color' => '#10B981', + 'entitlement_code' => 'core.srv.billing', + 'sort_order' => 20, + ]; + } + + public static function version(): ServiceVersion + { + return new ServiceVersion(1, 0, 0); + } + + public static function dependencies(): array + { + return []; + } + + // From AdminMenuProvider interface + public function menuItems(): array + { + return [ + [ + 'label' => 'Billing', + 'icon' => 'credit-card', + 'route' => 'admin.billing.index', + ], + ]; + } +} +``` + +### Full Example + +A complete service with health checks and dependencies: + +```php + 'analytics', + 'module' => 'Mod\\Analytics', + 'name' => 'AnalyticsHost', + 'tagline' => 'Privacy-first website analytics', + 'description' => 'Lightweight, GDPR-compliant analytics without cookies.', + 'icon' => 'chart-line', + 'color' => '#F59E0B', + 'entitlement_code' => 'core.srv.analytics', + 'sort_order' => 30, + ]; + } + + public static function version(): ServiceVersion + { + return new ServiceVersion(2, 1, 0); + } + + public static function dependencies(): array + { + return [ + ServiceDependency::required('auth', '>=1.0.0'), + ServiceDependency::optional('billing'), + ]; + } + + public function healthCheck(): HealthCheckResult + { + try { + $start = microtime(true); + + // Check ClickHouse connection + $this->clickhouse->select('SELECT 1'); + + // Check Redis connection + $this->redis->ping(); + + $responseTime = (microtime(true) - $start) * 1000; + + if ($responseTime > 1000) { + return HealthCheckResult::degraded( + 'Database responding slowly', + ['response_time_ms' => $responseTime] + ); + } + + return HealthCheckResult::healthy( + 'All systems operational', + [], + $responseTime + ); + } catch (\Exception $e) { + return HealthCheckResult::fromException($e); + } + } + + public function menuItems(): array + { + return [ + [ + 'label' => 'Analytics', + 'icon' => 'chart-line', + 'route' => 'admin.analytics.dashboard', + 'children' => [ + ['label' => 'Dashboard', 'route' => 'admin.analytics.dashboard'], + ['label' => 'Sites', 'route' => 'admin.analytics.sites'], + ['label' => 'Reports', 'route' => 'admin.analytics.reports'], + ], + ], + ]; + } +} +``` + +### Service Versioning + +```php +use Core\Service\ServiceVersion; + +// Create a version +$version = new ServiceVersion(2, 1, 0); +echo $version; // "2.1.0" + +// Parse from string +$version = ServiceVersion::fromString('v2.1.0'); + +// Check compatibility +$minimum = new ServiceVersion(1, 5, 0); +$current = new ServiceVersion(1, 8, 2); +$current->isCompatibleWith($minimum); // true + +// Mark as deprecated +$version = (new ServiceVersion(1, 0, 0)) + ->deprecate( + 'Use v2.x instead. See docs/migration.md', + new \DateTimeImmutable('2025-06-01') + ); + +// Check deprecation status +if ($version->deprecated) { + echo $version->deprecationMessage; +} + +if ($version->isPastSunset()) { + throw new ServiceSunsetException('Service no longer available'); +} +``` + +### Service Dependencies + +```php +use Core\Service\Contracts\ServiceDependency; + +public static function dependencies(): array +{ + return [ + // Required with minimum version + ServiceDependency::required('auth', '>=1.0.0'), + + // Required with version range + ServiceDependency::required('billing', '>=2.0.0', '<3.0.0'), + + // Optional dependency + ServiceDependency::optional('analytics'), + ]; +} +``` + +### Health Checks + +```php +use Core\Service\Contracts\HealthCheckable; +use Core\Service\HealthCheckResult; + +class MyService implements ServiceDefinition, HealthCheckable +{ + public function healthCheck(): HealthCheckResult + { + // Healthy + return HealthCheckResult::healthy('All systems operational'); + + // Healthy with response time + return HealthCheckResult::healthy( + 'Service operational', + ['connections' => 5], + responseTimeMs: 45.2 + ); + + // Degraded (works but with issues) + return HealthCheckResult::degraded( + 'High latency detected', + ['latency_ms' => 1500] + ); + + // Unhealthy + return HealthCheckResult::unhealthy( + 'Database connection failed', + ['last_error' => 'Connection refused'] + ); + + // From exception + try { + $this->checkCriticalSystem(); + } catch (\Exception $e) { + return HealthCheckResult::fromException($e); + } + } +} +``` + +### Health Check Guidelines + +Health checks should be: + +| Guideline | Recommendation | +|-----------|----------------| +| Fast | Complete within 5 seconds (< 1 second preferred) | +| Non-destructive | Read-only operations only | +| Representative | Test actual critical dependencies | +| Safe | Handle all exceptions, return HealthCheckResult | + +### Configuration + +Service definitions populate the `platform_services` table: + +```php +// definition() return array +[ + 'code' => 'billing', // Unique identifier (required) + 'module' => 'Mod\\Billing', // Module namespace (required) + 'name' => 'Billing', // Display name (required) + 'tagline' => 'Subscription management', // Short description + 'description' => '...', // Full description + 'icon' => 'link', // FontAwesome icon + 'color' => '#3B82F6', // Brand color + 'entitlement_code' => '...', // Access control code + 'sort_order' => 10, // Menu ordering +] +``` + +### Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Slow health checks | Keep under 1 second; test critical paths only | +| Circular dependencies | Review service architecture; refactor if needed | +| Missing version() | Always implement; return `ServiceVersion::initial()` at minimum | +| Health check exceptions | Catch all exceptions; return `fromException()` | +| Forgetting dependencies | Document all service interdependencies | + +--- + +## Summary + +These patterns form the backbone of the Core PHP Framework: + +| Pattern | Purpose | Key Files | +|---------|---------|-----------| +| Actions | Encapsulate business logic | `Core\Actions\Action` | +| Multi-Tenant | Data isolation between workspaces | `BelongsToWorkspace`, `WorkspaceScope` | +| Module System | Lazy-loading via lifecycle events | `Boot.php`, `$listens` | +| Activity Logging | Audit trail and change tracking | `LogsActivity`, `ActivityLogService` | +| Seeder Discovery | Auto-discovered, ordered seeding | `#[SeederPriority]`, `#[SeederAfter]` | +| Service Definition | SaaS product layer | `ServiceDefinition`, `HealthCheckable` | + +For more details, explore the source files in their respective locations or check the inline documentation. diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9f39f23 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,55 @@ + + + + + tests/Feature + packages/core-php/tests/Feature + packages/core-php/src/Core/**/Tests/Feature + packages/core-php/src/Mod/**/Tests/Feature + packages/core-admin/tests/Feature + packages/core-api/tests/Feature + packages/core-mcp/tests/Feature + + + tests/Unit + packages/core-php/tests/Unit + packages/core-php/src/Core/**/Tests/Unit + packages/core-php/src/Mod/**/Tests/Unit + packages/core-admin/tests/Unit + packages/core-api/tests/Unit + packages/core-mcp/tests/Unit + + + + + app + packages/core-php/src + packages/core-admin/src + packages/core-api/src + packages/core-mcp/src + + + + + + + + + + + + + + + + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..6dcf813 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,3 @@ +loadMigrationsFrom(__DIR__.'/../packages/core-php/src/Mod/Tenant/Migrations'); + $this->loadMigrationsFrom(__DIR__.'/../packages/core-php/src/Mod/Social/Migrations'); + } +}