feat: add initial framework files including API, console, and web routes; set up testing structure
This commit is contained in:
parent
e498a1701e
commit
f1c4c8f46d
13 changed files with 2257 additions and 133 deletions
455
.claude/skills/core-patterns.md
Normal file
455
.claude/skills/core-patterns.md
Normal file
|
|
@ -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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\{Module}\Actions;
|
||||
|
||||
use Core\Actions\Action;
|
||||
|
||||
/**
|
||||
* {Description}
|
||||
*
|
||||
* Usage:
|
||||
* $action = app({ActionName}::class);
|
||||
* $result = $action->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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\{Module}\Models;
|
||||
|
||||
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class {ModelName} extends Model
|
||||
{
|
||||
use BelongsToWorkspace;
|
||||
|
||||
protected $fillable = [
|
||||
'workspace_id',
|
||||
// other fields...
|
||||
];
|
||||
|
||||
// Optional: Disable strict mode (not recommended)
|
||||
// protected bool $workspaceContextRequired = false;
|
||||
}
|
||||
```
|
||||
|
||||
### Key points to explain
|
||||
|
||||
- **Auto-assignment**: `workspace_id` is automatically set from the current workspace context on create
|
||||
- **Query scoping**: Use `Model::ownedByCurrentWorkspace()` to scope queries
|
||||
- **Caching**: Use `Model::ownedByCurrentWorkspaceCached()` for cached collections
|
||||
- **Security**: Throws `MissingWorkspaceContextException` if no workspace context and strict mode is enabled
|
||||
- **Relation**: Provides `workspace()` belongsTo relationship
|
||||
|
||||
### Usage examples
|
||||
|
||||
```php
|
||||
// Query scoped to current workspace
|
||||
$invoices = Invoice::ownedByCurrentWorkspace()->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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\{Module}\Models;
|
||||
|
||||
use Core\Activity\Concerns\LogsActivity;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class {ModelName} extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
// Optional configuration via properties:
|
||||
|
||||
// Log only specific attributes (default: all)
|
||||
// protected array $activityLogAttributes = ['status', 'amount'];
|
||||
|
||||
// Custom log name (default: from config)
|
||||
// protected string $activityLogName = 'invoices';
|
||||
|
||||
// Events to log (default: created, updated, deleted)
|
||||
// protected array $activityLogEvents = ['created', 'updated'];
|
||||
|
||||
// Include workspace_id in properties (default: true)
|
||||
// protected bool $activityLogWorkspace = true;
|
||||
|
||||
// Only log dirty attributes (default: true)
|
||||
// protected bool $activityLogOnlyDirty = true;
|
||||
}
|
||||
```
|
||||
|
||||
### Custom activity tap (optional)
|
||||
|
||||
```php
|
||||
/**
|
||||
* Customize activity before saving.
|
||||
*/
|
||||
protected function customizeActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName): void
|
||||
{
|
||||
$activity->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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\{ModuleName};
|
||||
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Events\ApiRoutesRegistering;
|
||||
use Core\Events\ConsoleBooting;
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
/**
|
||||
* {ModuleName} Module Boot.
|
||||
*
|
||||
* {Description of what this module handles}
|
||||
*/
|
||||
class Boot extends ServiceProvider
|
||||
{
|
||||
protected string $moduleName = '{module_slug}';
|
||||
|
||||
/**
|
||||
* Events this module listens to for lazy loading.
|
||||
*
|
||||
* @var array<class-string, string>
|
||||
*/
|
||||
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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\{Module}\Database\Seeders;
|
||||
|
||||
use Core\Database\Seeders\Attributes\SeederAfter;
|
||||
use Core\Database\Seeders\Attributes\SeederPriority;
|
||||
use Core\Mod\Tenant\Database\Seeders\FeatureSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Seeds {description}.
|
||||
*/
|
||||
#[SeederPriority(50)]
|
||||
#[SeederAfter(FeatureSeeder::class)]
|
||||
class {SeederName} extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Guard against missing tables
|
||||
if (! Schema::hasTable('your_table')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Seeding logic here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available attributes
|
||||
|
||||
```php
|
||||
// Set priority (lower runs first, default 50)
|
||||
#[SeederPriority(10)]
|
||||
|
||||
// Must run after these seeders
|
||||
#[SeederAfter(FeatureSeeder::class)]
|
||||
#[SeederAfter(FeatureSeeder::class, PackageSeeder::class)]
|
||||
|
||||
// Must run before these seeders
|
||||
#[SeederBefore(DemoDataSeeder::class)]
|
||||
```
|
||||
|
||||
### Priority guidelines
|
||||
|
||||
| Range | Use case |
|
||||
|-------|----------|
|
||||
| 0-20 | Foundation seeders (features, configuration) |
|
||||
| 20-40 | Core data (packages, workspaces) |
|
||||
| 40-60 | Default priority (general seeders) |
|
||||
| 60-80 | Content seeders (pages, posts) |
|
||||
| 80-100 | Demo/test data seeders |
|
||||
|
||||
### Key points to explain
|
||||
|
||||
- Always guard against missing tables with `Schema::hasTable()`
|
||||
- Use `updateOrCreate()` to make seeders idempotent
|
||||
- Seeders are auto-discovered from `Database/Seeders/` directories
|
||||
- The framework detects circular dependencies and throws `CircularDependencyException`
|
||||
|
||||
---
|
||||
|
||||
## After generating code
|
||||
|
||||
Always:
|
||||
1. Show the generated code with proper file paths
|
||||
2. Explain what was created and why
|
||||
3. Provide usage examples
|
||||
4. Mention any follow-up steps (migrations, route registration, etc.)
|
||||
5. Ask if they need any modifications or have questions
|
||||
|
||||
Remember: This is pair programming. Be helpful, explain decisions, and adapt to what the user needs.
|
||||
246
README.md
246
README.md
|
|
@ -1,8 +1,16 @@
|
|||
# Core PHP Framework
|
||||
|
||||
A modular monolith framework for Laravel with event-driven architecture, lazy module loading, and built-in multi-tenancy.
|
||||
|
||||
## Features
|
||||
|
||||
A modular monolith framework for Laravel with event-driven architecture and lazy module loading.
|
||||
- **Event-driven module system** - Modules declare interest in lifecycle events and are only loaded when needed
|
||||
- **Lazy loading** - Web requests don't load admin modules, API requests don't load web modules
|
||||
- **Multi-tenant isolation** - Workspace-scoped data with automatic query filtering
|
||||
- **Actions pattern** - Single-purpose business logic classes with dependency injection
|
||||
- **Activity logging** - Built-in audit trails for model changes
|
||||
- **Seeder auto-discovery** - Automatic ordering via priority and dependency attributes
|
||||
- **HLCRF Layout System** - Hierarchical composable layouts (Header, Left, Content, Right, Footer)
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -12,58 +20,15 @@ composer require host-uk/core
|
|||
|
||||
The service provider will be auto-discovered.
|
||||
|
||||
## Configuration
|
||||
## Quick Start
|
||||
|
||||
Publish the config file:
|
||||
### Creating a Module
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=core-config
|
||||
```
|
||||
|
||||
Configure your module paths in `config/core.php`:
|
||||
|
||||
```php
|
||||
return [
|
||||
'module_paths' => [
|
||||
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
|
||||
<?php
|
||||
|
|
@ -93,137 +58,154 @@ class Boot
|
|||
}
|
||||
```
|
||||
|
||||
### Available Events
|
||||
### Lifecycle Events
|
||||
|
||||
| Event | Purpose |
|
||||
|-------|---------|
|
||||
| `WebRoutesRegistering` | Public-facing web routes |
|
||||
| `AdminPanelBooting` | Admin panel routes and navigation |
|
||||
| `ApiRoutesRegistering` | REST API endpoints |
|
||||
| `ClientRoutesRegistering` | Authenticated client/workspace routes |
|
||||
| `ClientRoutesRegistering` | Authenticated client routes |
|
||||
| `ConsoleBooting` | Artisan commands |
|
||||
| `McpToolsRegistering` | MCP tool handlers |
|
||||
| `FrameworkBooted` | Late-stage initialisation |
|
||||
|
||||
### Event Methods
|
||||
## Core Patterns
|
||||
|
||||
Events collect requests from modules:
|
||||
### Actions
|
||||
|
||||
Extract business logic into testable, reusable classes:
|
||||
|
||||
```php
|
||||
// Register routes
|
||||
$event->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('<nav>Navigation</nav>')
|
||||
->c('<article>Main content</article>')
|
||||
->f('<footer>Footer</footer>');
|
||||
|
||||
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.
|
||||
|
|
|
|||
45
TODO.md
Normal file
45
TODO.md
Normal file
|
|
@ -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
|
||||
19
bootstrap/app.php
Normal file
19
bootstrap/app.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->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();
|
||||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
160
config/mcp.php
Normal file
160
config/mcp.php
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| MCP Database Security
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the MCP QueryDatabase tool security measures.
|
||||
|
|
||||
*/
|
||||
|
||||
'database' => [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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),
|
||||
],
|
||||
|
||||
];
|
||||
1336
docs/patterns.md
Normal file
1336
docs/patterns.md
Normal file
File diff suppressed because it is too large
Load diff
55
phpunit.xml
Normal file
55
phpunit.xml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
executionOrder="random"
|
||||
requireCoverageMetadata="false"
|
||||
beStrictAboutCoverageMetadata="false"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true">
|
||||
<testsuites>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
<directory>packages/core-php/tests/Feature</directory>
|
||||
<directory>packages/core-php/src/Core/**/Tests/Feature</directory>
|
||||
<directory>packages/core-php/src/Mod/**/Tests/Feature</directory>
|
||||
<directory>packages/core-admin/tests/Feature</directory>
|
||||
<directory>packages/core-api/tests/Feature</directory>
|
||||
<directory>packages/core-mcp/tests/Feature</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
<directory>packages/core-php/tests/Unit</directory>
|
||||
<directory>packages/core-php/src/Core/**/Tests/Unit</directory>
|
||||
<directory>packages/core-php/src/Mod/**/Tests/Unit</directory>
|
||||
<directory>packages/core-admin/tests/Unit</directory>
|
||||
<directory>packages/core-api/tests/Unit</directory>
|
||||
<directory>packages/core-mcp/tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
<directory>packages/core-php/src</directory>
|
||||
<directory>packages/core-admin/src</directory>
|
||||
<directory>packages/core-api/src</directory>
|
||||
<directory>packages/core-mcp/src</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_DEBUG" value="true"/>
|
||||
<env name="APP_KEY" value="base64:Kx0qLJZJAQcDSFE2gMpuOlwrJcC6kXHM0j0KJdMGqzQ="/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
3
routes/api.php
Normal file
3
routes/api.php
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
// API routes are registered via core-api package
|
||||
3
routes/console.php
Normal file
3
routes/console.php
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
// Console commands are registered via core-php module system
|
||||
7
routes/web.php
Normal file
7
routes/web.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return 'Core PHP Framework';
|
||||
});
|
||||
20
tests/TestCase.php
Normal file
20
tests/TestCase.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
/**
|
||||
* Automatically load migrations from packages.
|
||||
*/
|
||||
protected function defineDatabaseMigrations(): void
|
||||
{
|
||||
// Load core-php migrations
|
||||
$this->loadMigrationsFrom(__DIR__.'/../packages/core-php/src/Mod/Tenant/Migrations');
|
||||
$this->loadMigrationsFrom(__DIR__.'/../packages/core-php/src/Mod/Social/Migrations');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue