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.
|
||||||
248
README.md
248
README.md
|
|
@ -1,8 +1,16 @@
|
||||||
# Core PHP Framework
|
# 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
|
## Installation
|
||||||
|
|
||||||
|
|
@ -12,58 +20,15 @@ composer require host-uk/core
|
||||||
|
|
||||||
The service provider will be auto-discovered.
|
The service provider will be auto-discovered.
|
||||||
|
|
||||||
## Configuration
|
## Quick Start
|
||||||
|
|
||||||
Publish the config file:
|
### Creating a Module
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
This creates a module at `app/Mod/Commerce/` with a `Boot.php` entry point:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
|
|
@ -93,137 +58,154 @@ class Boot
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Events
|
### Lifecycle Events
|
||||||
|
|
||||||
| Event | Purpose |
|
| Event | Purpose |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
| `WebRoutesRegistering` | Public-facing web routes |
|
| `WebRoutesRegistering` | Public-facing web routes |
|
||||||
| `AdminPanelBooting` | Admin panel routes and navigation |
|
| `AdminPanelBooting` | Admin panel routes and navigation |
|
||||||
| `ApiRoutesRegistering` | REST API endpoints |
|
| `ApiRoutesRegistering` | REST API endpoints |
|
||||||
| `ClientRoutesRegistering` | Authenticated client/workspace routes |
|
| `ClientRoutesRegistering` | Authenticated client routes |
|
||||||
| `ConsoleBooting` | Artisan commands |
|
| `ConsoleBooting` | Artisan commands |
|
||||||
| `McpToolsRegistering` | MCP tool handlers |
|
| `McpToolsRegistering` | MCP tool handlers |
|
||||||
| `FrameworkBooted` | Late-stage initialisation |
|
| `FrameworkBooted` | Late-stage initialisation |
|
||||||
|
|
||||||
### Event Methods
|
## Core Patterns
|
||||||
|
|
||||||
Events collect requests from modules:
|
### Actions
|
||||||
|
|
||||||
|
Extract business logic into testable, reusable classes:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Register routes
|
use Core\Actions\Action;
|
||||||
$event->routes(fn () => require __DIR__.'/routes.php');
|
|
||||||
|
|
||||||
// Register view namespace
|
class CreateOrder
|
||||||
$event->views('namespace', __DIR__.'/Views');
|
{
|
||||||
|
use Action;
|
||||||
|
|
||||||
// Register Livewire component
|
public function handle(User $user, array $data): Order
|
||||||
$event->livewire('alias', ComponentClass::class);
|
{
|
||||||
|
// Business logic here
|
||||||
|
return Order::create($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register navigation item
|
// Usage
|
||||||
$event->navigation(['label' => 'Products', 'icon' => 'box']);
|
$order = CreateOrder::run($user, $validated);
|
||||||
|
|
||||||
// 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);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Firing Events
|
### Multi-Tenant Isolation
|
||||||
|
|
||||||
Create frontage service providers to fire events at appropriate times:
|
Automatic workspace scoping for models:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Core\LifecycleEventProvider;
|
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
|
||||||
|
|
||||||
class WebServiceProvider extends ServiceProvider
|
class Product extends Model
|
||||||
{
|
{
|
||||||
public function boot(): void
|
use BelongsToWorkspace;
|
||||||
{
|
}
|
||||||
LifecycleEventProvider::fireWebRoutes();
|
|
||||||
}
|
// 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.
|
Data-driven layouts with infinite nesting:
|
||||||
|
|
||||||
## Custom Namespace Mapping
|
|
||||||
|
|
||||||
For non-standard directory structures:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$scanner = app(ModuleScanner::class);
|
use Core\Front\Components\Layout;
|
||||||
$scanner->setNamespaceMap([
|
|
||||||
'CustomMod' => 'App\\CustomMod',
|
$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
|
```php
|
||||||
use Core\Front\Admin\Contracts\AdminMenuProvider;
|
return [
|
||||||
|
'module_paths' => [
|
||||||
class Boot implements AdminMenuProvider
|
app_path('Core'),
|
||||||
{
|
app_path('Mod'),
|
||||||
public function adminMenuItems(): array
|
],
|
||||||
{
|
];
|
||||||
return [
|
|
||||||
[
|
|
||||||
'group' => 'services',
|
|
||||||
'priority' => 20,
|
|
||||||
'item' => fn () => [
|
|
||||||
'label' => 'Products',
|
|
||||||
'icon' => 'box',
|
|
||||||
'href' => route('admin.products.index'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### ServiceDefinition
|
## Artisan Commands
|
||||||
|
|
||||||
For SaaS service registration:
|
```bash
|
||||||
|
php artisan make:mod Commerce # Create a module
|
||||||
```php
|
php artisan make:website Marketing # Create a website module
|
||||||
use Core\Service\Contracts\ServiceDefinition;
|
php artisan make:plug Stripe # Create a plugin
|
||||||
|
|
||||||
class Boot implements ServiceDefinition
|
|
||||||
{
|
|
||||||
public static function definition(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'code' => 'commerce',
|
|
||||||
'module' => 'Commerce',
|
|
||||||
'name' => 'Commerce',
|
|
||||||
'tagline' => 'E-commerce platform',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
## Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
composer test
|
composer test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- PHP 8.2+
|
||||||
|
- Laravel 11+
|
||||||
|
|
||||||
## License
|
## 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": {
|
"psr-4": {
|
||||||
"Tests\\": "tests/",
|
"Tests\\": "tests/",
|
||||||
"Core\\Tests\\": "packages/core-php/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/",
|
"Mod\\": "packages/core-php/tests/Fixtures/Mod/",
|
||||||
"Plug\\": "packages/core-php/tests/Fixtures/Plug/",
|
"Plug\\": "packages/core-php/tests/Fixtures/Plug/",
|
||||||
"Website\\": "packages/core-php/tests/Fixtures/Website/"
|
"Website\\": "packages/core-php/tests/Fixtures/Website/"
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,43 @@ return [
|
||||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
// '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