docs: add comprehensive documentation for core-template
Some checks are pending
CI / PHP 8.2 (push) Waiting to run
CI / PHP 8.3 (push) Waiting to run
CI / PHP 8.4 (push) Waiting to run
CI / Assets (push) Waiting to run

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:
Snider 2026-01-29 21:21:11 +00:00
parent 9d6acb9121
commit ef4379a76f
4 changed files with 1408 additions and 0 deletions

361
docs/architecture.md Normal file
View 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
View 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
View 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
View 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/)