docs: add comprehensive documentation for core-template
Add documentation covering architecture, modules, getting started, and security considerations for the Core PHP Framework starter template. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9d6acb9121
commit
ef4379a76f
4 changed files with 1408 additions and 0 deletions
361
docs/architecture.md
Normal file
361
docs/architecture.md
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
---
|
||||
title: Architecture
|
||||
description: Technical architecture of core-template - the starter template for Core PHP Framework applications
|
||||
updated: 2026-01-29
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
core-template is the official starter template for building applications with the Core PHP Framework. It provides a pre-configured Laravel 12 application with the modular monolith architecture, Livewire 3, and Flux UI integration.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
core-template/
|
||||
├── app/
|
||||
│ ├── Http/Controllers/ # Traditional controllers (rarely used)
|
||||
│ ├── Models/ # Application-wide Eloquent models
|
||||
│ ├── Mod/ # Feature modules (your code goes here)
|
||||
│ └── Providers/ # Service providers
|
||||
├── bootstrap/
|
||||
│ ├── app.php # Application bootstrap with Core providers
|
||||
│ └── providers.php # Additional providers
|
||||
├── config/
|
||||
│ └── core.php # Core framework configuration
|
||||
├── public/
|
||||
│ └── index.php # Web entry point
|
||||
├── resources/
|
||||
│ ├── css/app.css # Tailwind entry point
|
||||
│ ├── js/app.js # JavaScript entry point
|
||||
│ └── views/ # Global Blade views
|
||||
├── routes/
|
||||
│ ├── web.php # Fallback web routes
|
||||
│ ├── api.php # Fallback API routes
|
||||
│ └── console.php # Console command routes
|
||||
└── tests/
|
||||
├── Feature/ # HTTP/Livewire feature tests
|
||||
└── Unit/ # Unit tests
|
||||
```
|
||||
|
||||
## Bootstrap Process
|
||||
|
||||
The application bootstrap (`bootstrap/app.php`) registers Core PHP Framework providers:
|
||||
|
||||
```php
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withProviders([
|
||||
\Core\LifecycleEventProvider::class, // Event system
|
||||
\Core\Website\Boot::class, // Website components
|
||||
\Core\Front\Boot::class, // Frontend (Livewire, Flux)
|
||||
\Core\Mod\Boot::class, // Module discovery
|
||||
])
|
||||
->withRouting(...)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
\Core\Front\Boot::middleware($middleware);
|
||||
})
|
||||
->create();
|
||||
```
|
||||
|
||||
### Provider Loading Order
|
||||
|
||||
1. **LifecycleEventProvider** - Sets up the event-driven architecture
|
||||
2. **Website\Boot** - Registers website-level functionality
|
||||
3. **Front\Boot** - Configures Livewire and frontend middleware
|
||||
4. **Mod\Boot** - Discovers and loads modules from configured paths
|
||||
|
||||
## Module System
|
||||
|
||||
Modules are self-contained feature packages that register via lifecycle events. This is the core architectural pattern of the framework.
|
||||
|
||||
### Module Paths
|
||||
|
||||
Configured in `config/core.php`:
|
||||
|
||||
```php
|
||||
'module_paths' => [
|
||||
app_path('Core'), // Local framework overrides (EUPL-1.2)
|
||||
app_path('Mod'), // Your application modules
|
||||
app_path('Website'), // Website-specific code
|
||||
],
|
||||
```
|
||||
|
||||
### Module Structure
|
||||
|
||||
Each module lives in `app/Mod/{ModuleName}/` with a `Boot.php` entry point:
|
||||
|
||||
```
|
||||
app/Mod/Blog/
|
||||
├── Boot.php # Event listeners (required)
|
||||
├── Models/
|
||||
│ └── Post.php # Eloquent models
|
||||
├── Routes/
|
||||
│ ├── web.php # Web routes
|
||||
│ └── api.php # API routes
|
||||
├── Views/
|
||||
│ └── posts/
|
||||
│ └── index.blade.php
|
||||
├── Livewire/
|
||||
│ └── PostListPage.php # Livewire components
|
||||
├── Migrations/
|
||||
│ └── 2025_01_01_create_posts_table.php
|
||||
└── Tests/
|
||||
└── PostTest.php
|
||||
```
|
||||
|
||||
### Boot.php Pattern
|
||||
|
||||
The `Boot.php` class declares which lifecycle events it responds to:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mod\Blog;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
use Core\Events\ApiRoutesRegistering;
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Events\ConsoleBooting;
|
||||
|
||||
class Boot
|
||||
{
|
||||
/**
|
||||
* Event listeners - class is only instantiated when events fire
|
||||
*/
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
AdminPanelBooting::class => ['onAdminPanel', 10], // With priority
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Register routes
|
||||
$event->routes(fn() => require __DIR__.'/Routes/web.php');
|
||||
|
||||
// Register view namespace (accessed as 'blog::view.name')
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
}
|
||||
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn() => require __DIR__.'/Routes/api.php');
|
||||
}
|
||||
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
// Register admin navigation
|
||||
$event->navigation('Blog', 'blog.admin.index', 'newspaper');
|
||||
|
||||
// Register admin resources
|
||||
$event->resource('posts', PostResource::class);
|
||||
}
|
||||
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
// Register artisan commands
|
||||
$event->commands([
|
||||
ImportPostsCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lifecycle Events
|
||||
|
||||
| Event | When Fired | Common Uses |
|
||||
|-------|------------|-------------|
|
||||
| `WebRoutesRegistering` | Web routes loading | Public routes, views |
|
||||
| `ApiRoutesRegistering` | API routes loading | REST endpoints |
|
||||
| `AdminPanelBooting` | Admin panel setup | Navigation, resources |
|
||||
| `ClientRoutesRegistering` | Authenticated SaaS routes | Dashboard, settings |
|
||||
| `ConsoleBooting` | Artisan bootstrapping | Commands, schedules |
|
||||
| `McpToolsRegistering` | MCP server setup | AI agent tools |
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Modules are discovered at boot time, but their `Boot` classes are only instantiated when the events they listen to are fired. This means:
|
||||
|
||||
- Console commands don't load web routes
|
||||
- API requests don't load admin panel code
|
||||
- Unused modules have minimal overhead
|
||||
|
||||
## Dependency Packages
|
||||
|
||||
The template depends on four Core PHP Framework packages:
|
||||
|
||||
| Package | Namespace | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `host-uk/core` | `Core\` | Foundation: events, modules, lifecycle |
|
||||
| `host-uk/core-admin` | `Core\Admin\` | Admin panel, Livewire modals, Flux UI |
|
||||
| `host-uk/core-api` | `Core\Api\` | REST API, scopes, rate limiting, webhooks |
|
||||
| `host-uk/core-mcp` | `Core\Mcp\` | Model Context Protocol for AI agents |
|
||||
|
||||
These are loaded as Composer dependencies and provide the framework infrastructure.
|
||||
|
||||
## Frontend Stack
|
||||
|
||||
### Livewire 3
|
||||
|
||||
Livewire components live within modules:
|
||||
|
||||
```php
|
||||
// app/Mod/Blog/Livewire/PostListPage.php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mod\Blog\Livewire;
|
||||
|
||||
use App\Mod\Blog\Models\Post;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class PostListPage extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('blog::posts.index', [
|
||||
'posts' => Post::latest()->paginate(10),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Flux UI
|
||||
|
||||
Flux Pro components are the standard UI library. Example usage:
|
||||
|
||||
```blade
|
||||
<flux:modal name="edit-post">
|
||||
<flux:heading>Edit Post</flux:heading>
|
||||
<flux:input wire:model="title" label="Title" />
|
||||
<flux:textarea wire:model="content" label="Content" />
|
||||
<flux:button type="submit">Save</flux:button>
|
||||
</flux:modal>
|
||||
```
|
||||
|
||||
### Asset Pipeline
|
||||
|
||||
Vite handles asset compilation:
|
||||
|
||||
- `resources/css/app.css` - Tailwind CSS entry point
|
||||
- `resources/js/app.js` - JavaScript entry point
|
||||
- Module assets are not automatically included; import them in the main files or use `@vite` directive
|
||||
|
||||
## Configuration
|
||||
|
||||
### Core Framework (`config/core.php`)
|
||||
|
||||
```php
|
||||
return [
|
||||
// Paths to scan for modules
|
||||
'module_paths' => [
|
||||
app_path('Core'),
|
||||
app_path('Mod'),
|
||||
app_path('Website'),
|
||||
],
|
||||
|
||||
// Service configuration
|
||||
'services' => [
|
||||
'cache_discovery' => env('CORE_CACHE_DISCOVERY', true),
|
||||
],
|
||||
|
||||
// CDN configuration
|
||||
'cdn' => [
|
||||
'enabled' => env('CDN_ENABLED', false),
|
||||
'driver' => env('CDN_DRIVER', 'bunny'),
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Key Core-specific environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CORE_CACHE_DISCOVERY` | `true` | Cache module discovery for performance |
|
||||
| `CDN_ENABLED` | `false` | Enable CDN for static assets |
|
||||
| `CDN_DRIVER` | `bunny` | CDN provider (bunny, cloudflare, etc.) |
|
||||
| `FLUX_LICENSE_KEY` | - | Flux Pro license key (optional) |
|
||||
|
||||
## Testing
|
||||
|
||||
Tests use Pest PHP and follow Laravel conventions:
|
||||
|
||||
```php
|
||||
// tests/Feature/BlogTest.php
|
||||
<?php
|
||||
|
||||
use App\Mod\Blog\Models\Post;
|
||||
|
||||
it('displays posts on the index page', function () {
|
||||
$posts = Post::factory()->count(3)->create();
|
||||
|
||||
$this->get('/blog')
|
||||
->assertOk()
|
||||
->assertSee($posts->first()->title);
|
||||
});
|
||||
|
||||
it('requires authentication to create posts', function () {
|
||||
$this->post('/blog', ['title' => 'Test'])
|
||||
->assertRedirect('/login');
|
||||
});
|
||||
```
|
||||
|
||||
### Test Organisation
|
||||
|
||||
- **Feature tests** - HTTP requests, Livewire components, integration tests
|
||||
- **Unit tests** - Services, utilities, isolated logic
|
||||
- **Module tests** - Can live within the module directory (`app/Mod/Blog/Tests/`)
|
||||
|
||||
## Routing
|
||||
|
||||
### Route Registration
|
||||
|
||||
Routes are registered via module events, not the traditional `routes/` directory:
|
||||
|
||||
```php
|
||||
// app/Mod/Blog/Routes/web.php
|
||||
<?php
|
||||
|
||||
use App\Mod\Blog\Livewire\PostListPage;
|
||||
use App\Mod\Blog\Livewire\PostShowPage;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('blog')->name('blog.')->group(function () {
|
||||
Route::get('/', PostListPage::class)->name('index');
|
||||
Route::get('/{post:slug}', PostShowPage::class)->name('show');
|
||||
});
|
||||
```
|
||||
|
||||
The `routes/web.php` and `routes/api.php` files are fallbacks for routes that don't belong to any module.
|
||||
|
||||
### View Namespacing
|
||||
|
||||
Module views are namespaced:
|
||||
|
||||
```blade
|
||||
{{-- Accessing blog module views --}}
|
||||
@include('blog::partials.header')
|
||||
|
||||
{{-- In a Livewire component --}}
|
||||
return view('blog::posts.index', [...]);
|
||||
```
|
||||
|
||||
## Namespace Conventions
|
||||
|
||||
| Path | Namespace | License |
|
||||
|------|-----------|---------|
|
||||
| `app/Core/` | `Core\` (local) | EUPL-1.2 |
|
||||
| `app/Mod/` | `App\Mod\` | Your choice |
|
||||
| `app/Website/` | `App\Website\` | Your choice |
|
||||
| `vendor/host-uk/core/` | `Core\` | EUPL-1.2 |
|
||||
|
||||
The `app/Core/` directory is for local overrides of framework classes. Any class you place here will take precedence over the vendor package.
|
||||
354
docs/getting-started.md
Normal file
354
docs/getting-started.md
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
---
|
||||
title: Getting Started
|
||||
description: Quick start guide for creating a new Core PHP Framework application
|
||||
updated: 2026-01-29
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
This guide walks you through creating your first application with Core PHP Framework using the core-template.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PHP 8.2 or higher
|
||||
- Composer 2.x
|
||||
- Node.js 18+ and npm
|
||||
- SQLite (default) or MySQL/PostgreSQL
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Clone the Template
|
||||
|
||||
```bash
|
||||
git clone https://github.com/host-uk/core-template.git my-project
|
||||
cd my-project
|
||||
```
|
||||
|
||||
Or use Composer create-project (once published):
|
||||
|
||||
```bash
|
||||
composer create-project host-uk/core-template my-project
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
# PHP dependencies
|
||||
composer install
|
||||
|
||||
# JavaScript dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Configure Environment
|
||||
|
||||
```bash
|
||||
# Copy environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Generate application key
|
||||
php artisan key:generate
|
||||
|
||||
# Create SQLite database
|
||||
touch database/database.sqlite
|
||||
|
||||
# Run migrations
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### 4. Start Development Server
|
||||
|
||||
```bash
|
||||
# In one terminal - PHP server
|
||||
php artisan serve
|
||||
|
||||
# In another terminal - Vite dev server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit http://localhost:8000 to see your application.
|
||||
|
||||
## Creating Your First Module
|
||||
|
||||
The Core PHP Framework uses a modular architecture. Features are organised as self-contained modules.
|
||||
|
||||
### Using the Artisan Command
|
||||
|
||||
```bash
|
||||
# Create a full-featured module
|
||||
php artisan make:mod Blog --all
|
||||
|
||||
# Or select specific features
|
||||
php artisan make:mod Blog --web --api
|
||||
```
|
||||
|
||||
### Manual Creation
|
||||
|
||||
1. Create the module directory:
|
||||
|
||||
```bash
|
||||
mkdir -p app/Mod/Blog/{Models,Routes,Views,Livewire,Migrations,Tests}
|
||||
```
|
||||
|
||||
2. Create `app/Mod/Blog/Boot.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mod\Blog;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn() => require __DIR__.'/Routes/web.php');
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Create `app/Mod/Blog/Routes/web.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/blog', function () {
|
||||
return view('blog::index');
|
||||
})->name('blog.index');
|
||||
```
|
||||
|
||||
4. Create `app/Mod/Blog/Views/index.blade.php`:
|
||||
|
||||
```blade
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to the Blog</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Visit http://localhost:8000/blog to see your module in action.
|
||||
|
||||
## Adding a Model
|
||||
|
||||
Create `app/Mod/Blog/Models/Post.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mod\Blog\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'content',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create a migration in `app/Mod/Blog/Migrations/`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('content');
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('posts');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Run the migration:
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
## Adding a Livewire Component
|
||||
|
||||
Create `app/Mod/Blog/Livewire/PostListPage.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mod\Blog\Livewire;
|
||||
|
||||
use App\Mod\Blog\Models\Post;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class PostListPage extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('blog::posts.index', [
|
||||
'posts' => Post::latest()->paginate(10),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `app/Mod/Blog/Routes/web.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Mod\Blog\Livewire\PostListPage;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/blog', PostListPage::class)->name('blog.index');
|
||||
```
|
||||
|
||||
Create `app/Mod/Blog/Views/posts/index.blade.php`:
|
||||
|
||||
```blade
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold mb-4">Blog Posts</h1>
|
||||
|
||||
@forelse ($posts as $post)
|
||||
<article class="mb-4 p-4 border rounded">
|
||||
<h2 class="text-xl font-semibold">{{ $post->title }}</h2>
|
||||
<p class="text-gray-600">{{ Str::limit($post->content, 200) }}</p>
|
||||
</article>
|
||||
@empty
|
||||
<p>No posts yet.</p>
|
||||
@endforelse
|
||||
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Create `app/Mod/Blog/Tests/PostTest.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Mod\Blog\Models\Post;
|
||||
|
||||
it('displays the blog index', function () {
|
||||
$this->get('/blog')
|
||||
->assertOk()
|
||||
->assertSee('Blog Posts');
|
||||
});
|
||||
|
||||
it('shows posts on the index page', function () {
|
||||
$post = Post::create([
|
||||
'title' => 'Test Post',
|
||||
'slug' => 'test-post',
|
||||
'content' => 'This is a test post.',
|
||||
]);
|
||||
|
||||
$this->get('/blog')
|
||||
->assertOk()
|
||||
->assertSee('Test Post');
|
||||
});
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
vendor/bin/pest
|
||||
```
|
||||
|
||||
## Code Formatting
|
||||
|
||||
Before committing, run Laravel Pint:
|
||||
|
||||
```bash
|
||||
# Format changed files only
|
||||
vendor/bin/pint --dirty
|
||||
|
||||
# Format all files
|
||||
vendor/bin/pint
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [Architecture documentation](architecture.md) to understand the module system
|
||||
- Review [Security considerations](security.md) before deploying
|
||||
- Explore the [Core PHP Framework documentation](https://github.com/host-uk/core-php)
|
||||
- Add the Admin Panel with `host-uk/core-admin`
|
||||
- Build an API with `host-uk/core-api`
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
php artisan serve # Start PHP server
|
||||
npm run dev # Start Vite with HMR
|
||||
npm run build # Build for production
|
||||
|
||||
# Modules
|
||||
php artisan make:mod Name # Create a new module
|
||||
php artisan make:mod Name --all # With all features
|
||||
|
||||
# Database
|
||||
php artisan migrate # Run migrations
|
||||
php artisan migrate:fresh # Reset and re-run migrations
|
||||
php artisan db:seed # Run seeders
|
||||
|
||||
# Testing
|
||||
vendor/bin/pest # Run all tests
|
||||
vendor/bin/pest --filter=Name # Run specific test
|
||||
|
||||
# Code Quality
|
||||
vendor/bin/pint # Format code
|
||||
vendor/bin/pint --test # Check formatting without changes
|
||||
```
|
||||
427
docs/modules.md
Normal file
427
docs/modules.md
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
---
|
||||
title: Modules
|
||||
description: Creating and organising modules in Core PHP Framework applications
|
||||
updated: 2026-01-29
|
||||
---
|
||||
|
||||
# Modules
|
||||
|
||||
Modules are the building blocks of Core PHP Framework applications. Each module is a self-contained feature that registers itself via lifecycle events.
|
||||
|
||||
## Module Structure
|
||||
|
||||
A typical module follows this structure:
|
||||
|
||||
```
|
||||
app/Mod/Blog/
|
||||
├── Boot.php # Entry point - event listeners
|
||||
├── Models/
|
||||
│ └── Post.php # Eloquent models
|
||||
├── Routes/
|
||||
│ ├── web.php # Public routes
|
||||
│ └── api.php # API routes
|
||||
├── Views/
|
||||
│ ├── index.blade.php
|
||||
│ └── posts/
|
||||
│ └── show.blade.php
|
||||
├── Livewire/
|
||||
│ ├── PostListPage.php
|
||||
│ └── PostShowPage.php
|
||||
├── Actions/
|
||||
│ └── CreatePost.php # Business logic
|
||||
├── Services/
|
||||
│ └── PostService.php
|
||||
├── Migrations/
|
||||
│ └── 2025_01_01_create_posts_table.php
|
||||
└── Tests/
|
||||
├── PostTest.php
|
||||
└── CreatePostTest.php
|
||||
```
|
||||
|
||||
## The Boot Class
|
||||
|
||||
Every module requires a `Boot.php` file that declares its event listeners:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mod\Blog;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
use Core\Events\ApiRoutesRegistering;
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Events\ConsoleBooting;
|
||||
|
||||
class Boot
|
||||
{
|
||||
/**
|
||||
* Static listeners array - module is only instantiated when these events fire
|
||||
*/
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
AdminPanelBooting::class => ['onAdminPanel', 10], // With priority
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Register routes
|
||||
$event->routes(fn() => require __DIR__.'/Routes/web.php');
|
||||
|
||||
// Register view namespace (accessed as 'blog::view.name')
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
}
|
||||
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn() => require __DIR__.'/Routes/api.php');
|
||||
}
|
||||
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
// Register navigation item
|
||||
$event->navigation('Blog', 'blog.admin.index', 'newspaper');
|
||||
|
||||
// Register admin resources
|
||||
$event->resource('posts', PostResource::class);
|
||||
}
|
||||
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
// Register artisan commands
|
||||
$event->commands([
|
||||
ImportPostsCommand::class,
|
||||
PublishScheduledPostsCommand::class,
|
||||
]);
|
||||
|
||||
// Register scheduled tasks
|
||||
$event->schedule(function ($schedule) {
|
||||
$schedule->command('blog:publish-scheduled')->hourly();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Events
|
||||
|
||||
### WebRoutesRegistering
|
||||
|
||||
Fired when web routes are being registered. Use for public-facing routes.
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Register route file
|
||||
$event->routes(fn() => require __DIR__.'/Routes/web.php');
|
||||
|
||||
// Register view namespace
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
|
||||
// Register Blade components
|
||||
$event->components('blog', __DIR__.'/Views/Components');
|
||||
|
||||
// Register middleware
|
||||
$event->middleware('blog.auth', BlogAuthMiddleware::class);
|
||||
}
|
||||
```
|
||||
|
||||
### ApiRoutesRegistering
|
||||
|
||||
Fired when API routes are being registered. Routes are automatically prefixed with `/api`.
|
||||
|
||||
```php
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn() => require __DIR__.'/Routes/api.php');
|
||||
|
||||
// Register API resources
|
||||
$event->resource('posts', PostApiResource::class);
|
||||
}
|
||||
```
|
||||
|
||||
### AdminPanelBooting
|
||||
|
||||
Fired when the admin panel is being set up (requires `core-admin` package).
|
||||
|
||||
```php
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
// Navigation item with icon
|
||||
$event->navigation('Blog', 'blog.admin.index', 'newspaper');
|
||||
|
||||
// Navigation group with sub-items
|
||||
$event->navigationGroup('Blog', [
|
||||
['Posts', 'blog.admin.posts', 'file-text'],
|
||||
['Categories', 'blog.admin.categories', 'folder'],
|
||||
['Tags', 'blog.admin.tags', 'tag'],
|
||||
], 'newspaper');
|
||||
|
||||
// Register admin resource
|
||||
$event->resource('posts', PostResource::class);
|
||||
|
||||
// Register widget for dashboard
|
||||
$event->widget(RecentPostsWidget::class);
|
||||
}
|
||||
```
|
||||
|
||||
### ClientRoutesRegistering
|
||||
|
||||
Fired for authenticated SaaS routes (dashboard, settings, etc.).
|
||||
|
||||
```php
|
||||
public function onClientRoutes(ClientRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn() => require __DIR__.'/Routes/client.php');
|
||||
}
|
||||
```
|
||||
|
||||
### ConsoleBooting
|
||||
|
||||
Fired when Artisan is bootstrapping.
|
||||
|
||||
```php
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
// Register commands
|
||||
$event->commands([
|
||||
ImportPostsCommand::class,
|
||||
]);
|
||||
|
||||
// Register scheduled tasks
|
||||
$event->schedule(function ($schedule) {
|
||||
$schedule->command('blog:publish-scheduled')
|
||||
->hourly()
|
||||
->withoutOverlapping();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### McpToolsRegistering
|
||||
|
||||
Fired when the MCP server is being set up (requires `core-mcp` package).
|
||||
|
||||
```php
|
||||
public function onMcpTools(McpToolsRegistering $event): void
|
||||
{
|
||||
$event->tool('create_post', CreatePostTool::class);
|
||||
$event->tool('list_posts', ListPostsTool::class);
|
||||
}
|
||||
```
|
||||
|
||||
## Event Priorities
|
||||
|
||||
You can specify a priority for event listeners. Higher numbers execute first:
|
||||
|
||||
```php
|
||||
public static array $listens = [
|
||||
AdminPanelBooting::class => ['onAdminPanel', 100], // High priority
|
||||
WebRoutesRegistering::class => ['onWebRoutes', 10], // Normal priority
|
||||
];
|
||||
```
|
||||
|
||||
Priorities are useful when:
|
||||
- Your module needs to register before/after other modules
|
||||
- You need to override routes from other modules
|
||||
- You need to modify admin navigation order
|
||||
|
||||
## View Namespacing
|
||||
|
||||
Views are namespaced by the identifier you provide:
|
||||
|
||||
```php
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
```
|
||||
|
||||
Access views using the namespace prefix:
|
||||
|
||||
```blade
|
||||
{{-- In controllers/components --}}
|
||||
return view('blog::posts.index');
|
||||
|
||||
{{-- In Blade templates --}}
|
||||
@include('blog::partials.sidebar')
|
||||
@extends('blog::layouts.main')
|
||||
```
|
||||
|
||||
## Route Files
|
||||
|
||||
### Web Routes (`Routes/web.php`)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Mod\Blog\Livewire\PostListPage;
|
||||
use App\Mod\Blog\Livewire\PostShowPage;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('blog')->name('blog.')->group(function () {
|
||||
Route::get('/', PostListPage::class)->name('index');
|
||||
Route::get('/{post:slug}', PostShowPage::class)->name('show');
|
||||
});
|
||||
|
||||
// With middleware
|
||||
Route::middleware(['auth'])->prefix('blog')->name('blog.')->group(function () {
|
||||
Route::get('/my-posts', MyPostsPage::class)->name('my-posts');
|
||||
});
|
||||
```
|
||||
|
||||
### API Routes (`Routes/api.php`)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Mod\Blog\Http\Controllers\Api\PostController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('blog')->name('api.blog.')->group(function () {
|
||||
Route::apiResource('posts', PostController::class);
|
||||
});
|
||||
```
|
||||
|
||||
## Actions Pattern
|
||||
|
||||
For complex business logic, use the Actions pattern:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mod\Blog\Actions;
|
||||
|
||||
use App\Mod\Blog\Models\Post;
|
||||
use Core\Action;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
return Post::create([
|
||||
'title' => $data['title'],
|
||||
'slug' => Str::slug($data['title']),
|
||||
'content' => $data['content'],
|
||||
'published_at' => $data['publish_now'] ? now() : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```php
|
||||
$post = CreatePost::run([
|
||||
'title' => 'My Post',
|
||||
'content' => 'Content here...',
|
||||
'publish_now' => true,
|
||||
]);
|
||||
```
|
||||
|
||||
## Module Discovery
|
||||
|
||||
Modules are discovered automatically from paths configured in `config/core.php`:
|
||||
|
||||
```php
|
||||
'module_paths' => [
|
||||
app_path('Core'), // Framework overrides
|
||||
app_path('Mod'), // Application modules
|
||||
app_path('Website'), // Website-specific modules
|
||||
],
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
In production, module discovery is cached. Clear the cache when adding new modules:
|
||||
|
||||
```bash
|
||||
php artisan cache:clear
|
||||
```
|
||||
|
||||
Or disable caching during development:
|
||||
|
||||
```env
|
||||
CORE_CACHE_DISCOVERY=false
|
||||
```
|
||||
|
||||
## Creating Modules with Artisan
|
||||
|
||||
The `make:mod` command scaffolds a new module:
|
||||
|
||||
```bash
|
||||
# Full module with all features
|
||||
php artisan make:mod Blog --all
|
||||
|
||||
# Web routes only
|
||||
php artisan make:mod Blog --web
|
||||
|
||||
# API routes only
|
||||
php artisan make:mod Blog --api
|
||||
|
||||
# Admin panel integration
|
||||
php artisan make:mod Blog --admin
|
||||
|
||||
# Combination
|
||||
php artisan make:mod Blog --web --api --admin
|
||||
```
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
If your module depends on another module, check for its presence:
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Check if core-tenant is available
|
||||
if (!class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->routes(fn() => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Keep Modules Focused
|
||||
|
||||
Each module should represent a single feature or domain:
|
||||
|
||||
- `Blog` - Blog posts, categories, tags
|
||||
- `Shop` - Products, orders, cart
|
||||
- `Newsletter` - Subscribers, campaigns
|
||||
|
||||
### Use Clear Naming
|
||||
|
||||
- Module name: PascalCase singular (`Blog`, not `Blogs`)
|
||||
- Namespace: `App\Mod\{ModuleName}`
|
||||
- View namespace: lowercase (`blog::`, `shop::`)
|
||||
|
||||
### Isolate Dependencies
|
||||
|
||||
Keep inter-module dependencies minimal. If modules need to communicate:
|
||||
|
||||
1. Use events (preferred)
|
||||
2. Use interfaces and dependency injection
|
||||
3. Use shared services in `app/Services/`
|
||||
|
||||
### Test Modules in Isolation
|
||||
|
||||
Write tests that don't depend on other modules being present:
|
||||
|
||||
```php
|
||||
it('creates a blog post', function () {
|
||||
$post = Post::create([
|
||||
'title' => 'Test',
|
||||
'slug' => 'test',
|
||||
'content' => 'Content',
|
||||
]);
|
||||
|
||||
expect($post)->toBeInstanceOf(Post::class);
|
||||
expect($post->title)->toBe('Test');
|
||||
});
|
||||
```
|
||||
266
docs/security.md
Normal file
266
docs/security.md
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
---
|
||||
title: Security
|
||||
description: Security considerations and audit notes for core-template
|
||||
updated: 2026-01-29
|
||||
---
|
||||
|
||||
# Security
|
||||
|
||||
This document covers security considerations for applications built with core-template. It includes both framework-provided protections and recommendations for hardening your application.
|
||||
|
||||
## Built-in Protections
|
||||
|
||||
### CSRF Protection
|
||||
|
||||
Laravel's CSRF protection is enabled by default for all web routes. The template includes axios configuration that automatically attaches the CSRF token to AJAX requests:
|
||||
|
||||
```javascript
|
||||
// resources/js/bootstrap.js
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
```
|
||||
|
||||
For forms, use the `@csrf` Blade directive:
|
||||
|
||||
```blade
|
||||
<form method="POST" action="/posts">
|
||||
@csrf
|
||||
<!-- form fields -->
|
||||
</form>
|
||||
```
|
||||
|
||||
### XSS Protection
|
||||
|
||||
Blade's `{{ }}` syntax automatically escapes output. Use `{!! !!}` only for trusted HTML content.
|
||||
|
||||
### SQL Injection
|
||||
|
||||
Eloquent ORM and Query Builder use parameterised queries by default. Avoid raw queries where possible:
|
||||
|
||||
```php
|
||||
// Safe - parameterised
|
||||
User::where('email', $email)->first();
|
||||
|
||||
// Dangerous - raw SQL
|
||||
DB::select("SELECT * FROM users WHERE email = '$email'"); // Don't do this
|
||||
```
|
||||
|
||||
### Mass Assignment
|
||||
|
||||
Models should define `$fillable` or `$guarded` properties to prevent mass assignment vulnerabilities:
|
||||
|
||||
```php
|
||||
class Post extends Model
|
||||
{
|
||||
protected $fillable = ['title', 'content', 'slug'];
|
||||
}
|
||||
```
|
||||
|
||||
### Password Hashing
|
||||
|
||||
The template configures bcrypt with 12 rounds by default (`BCRYPT_ROUNDS=12` in `.env.example`). This is appropriate for production use.
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Security Headers
|
||||
|
||||
Add security headers via middleware. Create `app/Http/Middleware/SecurityHeaders.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SecurityHeaders
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
$response->headers->set('X-XSS-Protection', '1; mode=block');
|
||||
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// Content Security Policy (adjust as needed)
|
||||
$response->headers->set(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register in `bootstrap/app.php`:
|
||||
|
||||
```php
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
\Core\Front\Boot::middleware($middleware);
|
||||
$middleware->web(append: [
|
||||
\App\Http\Middleware\SecurityHeaders::class,
|
||||
]);
|
||||
})
|
||||
```
|
||||
|
||||
### Session Security
|
||||
|
||||
For production environments, update these settings in `.env`:
|
||||
|
||||
```env
|
||||
SESSION_SECURE_COOKIE=true # Only send cookies over HTTPS
|
||||
SESSION_ENCRYPT=true # Encrypt session data
|
||||
SESSION_HTTP_ONLY=true # Prevent JavaScript access to session cookie
|
||||
SESSION_SAME_SITE=strict # Strict same-site policy
|
||||
```
|
||||
|
||||
### HTTPS Enforcement
|
||||
|
||||
Force HTTPS in production by adding to `AppServiceProvider`:
|
||||
|
||||
```php
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->environment('production')) {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
The default welcome route has no rate limiting. For production, add throttle middleware:
|
||||
|
||||
```php
|
||||
Route::middleware('throttle:60,1')->group(function () {
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Configure custom rate limiters in `AppServiceProvider`:
|
||||
|
||||
```php
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->ip());
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### APP_KEY Management
|
||||
|
||||
The `APP_KEY` is critical for encryption. Never:
|
||||
|
||||
- Commit it to version control
|
||||
- Share it between environments
|
||||
- Use predictable values
|
||||
|
||||
Rotate the key only when necessary, understanding that:
|
||||
|
||||
- Existing encrypted data becomes unreadable
|
||||
- Active sessions are invalidated
|
||||
- Signed URLs become invalid
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Ensure `APP_DEBUG=false` in production. Debug mode exposes:
|
||||
|
||||
- Stack traces with file paths
|
||||
- Environment variables
|
||||
- Database queries
|
||||
|
||||
### Database Credentials
|
||||
|
||||
Never commit database credentials. Use environment variables:
|
||||
|
||||
```env
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=myapp
|
||||
DB_USERNAME=${DB_USERNAME}
|
||||
DB_PASSWORD=${DB_PASSWORD}
|
||||
```
|
||||
|
||||
## Audit Checklist
|
||||
|
||||
### Before Deployment
|
||||
|
||||
- [ ] `APP_DEBUG=false`
|
||||
- [ ] `APP_ENV=production`
|
||||
- [ ] `APP_KEY` is set and unique
|
||||
- [ ] Session cookies are secure (`SESSION_SECURE_COOKIE=true`)
|
||||
- [ ] HTTPS is enforced
|
||||
- [ ] Security headers are configured
|
||||
- [ ] Rate limiting is in place for sensitive endpoints
|
||||
- [ ] `.env` is not accessible via web (check with `curl https://yoursite.com/.env`)
|
||||
- [ ] Storage directory is not web-accessible
|
||||
- [ ] Error pages don't leak sensitive information
|
||||
- [ ] Database credentials are environment-specific
|
||||
- [ ] Third-party API keys are not exposed in client-side code
|
||||
|
||||
### Authentication (if using core-tenant)
|
||||
|
||||
- [ ] Password reset tokens expire appropriately
|
||||
- [ ] Login attempts are rate limited
|
||||
- [ ] Account lockout is configured after failed attempts
|
||||
- [ ] Two-factor authentication is available for sensitive accounts
|
||||
- [ ] Session regeneration on login
|
||||
- [ ] Session invalidation on logout
|
||||
|
||||
### API Security (if using core-api)
|
||||
|
||||
- [ ] API keys are properly scoped
|
||||
- [ ] Rate limiting per API key
|
||||
- [ ] Webhook signatures are verified
|
||||
- [ ] CORS is configured appropriately
|
||||
- [ ] Sensitive endpoints require authentication
|
||||
|
||||
## Dependencies
|
||||
|
||||
Keep dependencies updated to receive security patches:
|
||||
|
||||
```bash
|
||||
# Check for outdated packages
|
||||
composer outdated
|
||||
|
||||
# Update dependencies
|
||||
composer update
|
||||
|
||||
# Check for known vulnerabilities
|
||||
composer audit
|
||||
```
|
||||
|
||||
The template includes Dependabot configuration (`.github/dependabot.yml`) to automate security updates.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
If you discover a security vulnerability in the Core PHP Framework:
|
||||
|
||||
1. **Do not** create a public GitHub issue
|
||||
2. Email security concerns to the maintainers directly
|
||||
3. Include detailed steps to reproduce
|
||||
4. Allow reasonable time for a fix before public disclosure
|
||||
|
||||
## Resources
|
||||
|
||||
- [Laravel Security Documentation](https://laravel.com/docs/security)
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [PHP Security Best Practices](https://www.php.net/manual/en/security.php)
|
||||
- [Snyk Security Advisories](https://security.snyk.io/)
|
||||
Loading…
Add table
Reference in a new issue