docs: initial import of CorePHP documentation

173 markdown files covering:
- Framework architecture (lifecycle events, module system, multi-tenancy)
- Package docs (admin, api, mcp, tenant, commerce, content, developer)
- CLI reference (dev, build, go, php, deploy commands)
- Patterns (actions, repositories, seeders, services, HLCRF)
- Deployment (Docker, PHP, LinuxKit, templates)
- Publishing (Homebrew, AUR, npm, Docker, Scoop, Chocolatey)

Source: core-php/docs (core.help content)

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-03 17:51:03 +00:00
commit 85bbb8e828
176 changed files with 49045 additions and 0 deletions

389
api/authentication.md Normal file
View file

@ -0,0 +1,389 @@
# API Authentication
Core PHP Framework provides multiple authentication methods for API access, including API keys, OAuth tokens, and session-based authentication.
## API Keys
API keys are the primary authentication method for external API access.
### Creating API Keys
```php
use Mod\Api\Models\ApiKey;
$apiKey = ApiKey::create([
'name' => 'Mobile App',
'workspace_id' => $workspace->id,
'scopes' => ['posts:read', 'posts:write', 'categories:read'],
'rate_limit_tier' => 'pro',
]);
// Get plaintext key (only shown once!)
$plaintext = $apiKey->plaintext_key; // sk_live_...
```
**Response:**
```json
{
"id": 123,
"name": "Mobile App",
"key": "sk_live_abc123...",
"scopes": ["posts:read", "posts:write"],
"rate_limit_tier": "pro",
"created_at": "2026-01-26T12:00:00Z"
}
```
::: warning
The plaintext API key is only shown once at creation. Store it securely!
:::
### Using API Keys
Include the API key in the `Authorization` header:
```bash
curl -H "Authorization: Bearer sk_live_abc123..." \
https://api.example.com/v1/posts
```
Or use basic authentication:
```bash
curl -u sk_live_abc123: \
https://api.example.com/v1/posts
```
### Key Format
API keys follow the format: `{prefix}_{environment}_{random}`
- **Prefix:** `sk` (secret key)
- **Environment:** `live` or `test`
- **Random:** 32 characters
**Examples:**
- `sk_live_`
- `sk_test_`
### Key Security
API keys are hashed with bcrypt before storage:
```php
// Creation
$hash = bcrypt($plaintext);
// Verification
if (Hash::check($providedKey, $apiKey->key_hash)) {
// Valid key
}
```
**Security Features:**
- Never stored in plaintext
- Bcrypt hashing (cost factor: 10)
- Secure comparison with `hash_equals()`
- Rate limiting per key
- Automatic expiry support
### Key Rotation
Rotate keys regularly for security:
```php
$newKey = $apiKey->rotate();
// Returns new key object with:
// - New plaintext key
// - Same scopes and settings
// - Old key marked for deletion after grace period
```
**Grace Period:**
- Default: 24 hours
- Both old and new keys work during this period
- Old key auto-deleted after grace period
### Key Permissions
Control what each key can access:
```php
$apiKey = ApiKey::create([
'name' => 'Read-Only Key',
'scopes' => [
'posts:read',
'categories:read',
'analytics:read',
],
]);
```
Available scopes documented in [Scopes & Permissions](#scopes--permissions).
## Sanctum Tokens
Laravel Sanctum provides token-based authentication for SPAs:
### Creating Tokens
```php
$user = User::find(1);
$token = $user->createToken('mobile-app', [
'posts:read',
'posts:write',
])->plainTextToken;
```
### Using Tokens
```bash
curl -H "Authorization: Bearer 1|abc123..." \
https://api.example.com/v1/posts
```
### Token Abilities
Check token abilities in controllers:
```php
if ($request->user()->tokenCan('posts:write')) {
// User has permission
}
```
## Session Authentication
For first-party applications, use session-based authentication:
```bash
# Login first
curl -X POST https://api.example.com/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secret"}' \
-c cookies.txt
# Use session cookie
curl https://api.example.com/v1/posts \
-b cookies.txt
```
## OAuth 2.0 (Optional)
If Laravel Passport is installed, OAuth 2.0 is available:
### Authorization Code Grant
```bash
# 1. Redirect user to authorization endpoint
https://api.example.com/oauth/authorize?
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
response_type=code&
scope=posts:read posts:write
# 2. Exchange code for token
curl -X POST https://api.example.com/oauth/token \
-d "grant_type=authorization_code" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "code=AUTH_CODE" \
-d "redirect_uri=CALLBACK_URL"
```
### Client Credentials Grant
For server-to-server:
```bash
curl -X POST https://api.example.com/oauth/token \
-d "grant_type=client_credentials" \
-d "client_id=CLIENT_ID" \
-d "client_secret=CLIENT_SECRET" \
-d "scope=posts:read"
```
## Scopes & Permissions
### Available Scopes
| Scope | Description |
|-------|-------------|
| `posts:read` | Read blog posts |
| `posts:write` | Create and update posts |
| `posts:delete` | Delete posts |
| `categories:read` | Read categories |
| `categories:write` | Create and update categories |
| `analytics:read` | Access analytics data |
| `webhooks:manage` | Manage webhook endpoints |
| `keys:manage` | Manage API keys |
| `admin:*` | Full admin access |
### Scope Enforcement
Protect routes with scope middleware:
```php
Route::middleware('scope:posts:write')
->post('/posts', [PostController::class, 'store']);
```
### Wildcard Scopes
Use wildcards for broad permissions:
- `posts:*` - All post permissions
- `*:read` - Read access to all resources
- `*` - Full access (use sparingly!)
## Authentication Errors
### 401 Unauthorized
Missing or invalid credentials:
```json
{
"message": "Unauthenticated."
}
```
**Causes:**
- No `Authorization` header
- Invalid API key
- Expired token
- Revoked credentials
### 403 Forbidden
Valid credentials but insufficient permissions:
```json
{
"message": "This action is unauthorized.",
"required_scope": "posts:write",
"provided_scopes": ["posts:read"]
}
```
**Causes:**
- Missing required scope
- Workspace suspended
- Resource access denied
## Best Practices
### 1. Use Minimum Required Scopes
```php
// ✅ Good - specific scopes
$apiKey->scopes = ['posts:read', 'categories:read'];
// ❌ Bad - excessive permissions
$apiKey->scopes = ['*'];
```
### 2. Rotate Keys Regularly
```php
// Rotate every 90 days
if ($apiKey->created_at->diffInDays() > 90) {
$apiKey->rotate();
}
```
### 3. Use Different Keys Per Client
```php
// ✅ Good - separate keys
ApiKey::create(['name' => 'Mobile App iOS']);
ApiKey::create(['name' => 'Mobile App Android']);
// ❌ Bad - shared key
ApiKey::create(['name' => 'All Mobile Apps']);
```
### 4. Monitor Key Usage
```php
$usage = ApiKey::find($id)->usage()
->whereBetween('created_at', [now()->subDays(7), now()])
->count();
```
### 5. Implement Key Expiry
```php
$apiKey = ApiKey::create([
'name' => 'Temporary Key',
'expires_at' => now()->addDays(30),
]);
```
## Rate Limiting
All authenticated requests are rate limited based on tier:
| Tier | Requests per Hour |
|------|------------------|
| Free | 1,000 |
| Pro | 10,000 |
| Enterprise | Unlimited |
Rate limit headers included in responses:
```
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9995
X-RateLimit-Reset: 1640995200
```
## Testing Authentication
### Test Mode Keys
Use test keys for development:
```php
$testKey = ApiKey::create([
'name' => 'Test Key',
'environment' => 'test',
]);
// Key prefix: sk_test_...
```
Test keys:
- Don't affect production data
- Higher rate limits
- Clearly marked in admin panel
- Can be deleted without confirmation
### cURL Examples
**API Key:**
```bash
curl -H "Authorization: Bearer sk_live_..." \
https://api.example.com/v1/posts
```
**Sanctum Token:**
```bash
curl -H "Authorization: Bearer 1|..." \
https://api.example.com/v1/posts
```
**Session:**
```bash
curl -H "Cookie: laravel_session=..." \
https://api.example.com/v1/posts
```
## Learn More
- [API Reference →](/api/endpoints)
- [Rate Limiting →](/api/endpoints#rate-limiting)
- [Error Handling →](/api/errors)
- [API Package →](/packages/api)

743
api/endpoints.md Normal file
View file

@ -0,0 +1,743 @@
# API Endpoints Reference
Core PHP Framework provides RESTful APIs for programmatic access to platform resources. All endpoints follow consistent patterns for authentication, pagination, filtering, and error handling.
## Base URL
```
https://your-domain.com/api/v1
```
## Common Parameters
### Pagination
All list endpoints support pagination:
```http
GET /api/v1/resources?page=2&per_page=50
```
**Parameters:**
- `page` (integer) - Page number (default: 1)
- `per_page` (integer) - Items per page (default: 15, max: 100)
**Response includes:**
```json
{
"data": [...],
"meta": {
"current_page": 2,
"per_page": 50,
"total": 250,
"last_page": 5
},
"links": {
"first": "https://api.example.com/resources?page=1",
"last": "https://api.example.com/resources?page=5",
"prev": "https://api.example.com/resources?page=1",
"next": "https://api.example.com/resources?page=3"
}
}
```
### Filtering
Filter list results using query parameters:
```http
GET /api/v1/resources?status=active&created_after=2024-01-01
```
Common filters:
- `status` - Filter by status (varies by resource)
- `created_after` - ISO 8601 date
- `created_before` - ISO 8601 date
- `updated_after` - ISO 8601 date
- `updated_before` - ISO 8601 date
- `search` - Full-text search (if supported)
### Sorting
Sort results using the `sort` parameter:
```http
GET /api/v1/resources?sort=-created_at,name
```
- Prefix with `-` for descending order
- Default is ascending order
- Comma-separate multiple sort fields
### Field Selection
Request specific fields only:
```http
GET /api/v1/resources?fields=id,name,created_at
```
Reduces payload size and improves performance.
### Includes
Eager-load related resources:
```http
GET /api/v1/resources?include=owner,tags,metadata
```
Reduces number of API calls needed.
## Rate Limiting
API requests are rate-limited based on your tier:
| Tier | Requests/Hour | Burst |
|------|--------------|-------|
| Free | 1,000 | 50 |
| Pro | 10,000 | 200 |
| Business | 50,000 | 500 |
| Enterprise | Custom | Custom |
Rate limit headers included in every response:
```http
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9847
X-RateLimit-Reset: 1640995200
```
When rate limit is exceeded, you'll receive a `429 Too Many Requests` response:
```json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Please retry after 3600 seconds.",
"retry_after": 3600
}
}
```
## Idempotency
POST, PATCH, PUT, and DELETE requests support idempotency keys to safely retry requests:
```http
POST /api/v1/resources
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
```
If the same idempotency key is used within 24 hours:
- Same status code and response body returned
- No duplicate resource created
- Safe to retry failed requests
## Versioning
The API version is included in the URL path:
```
/api/v1/resources
```
When breaking changes are introduced, a new version will be released (e.g., `/api/v2/`). Previous versions are supported for at least 12 months after deprecation notice.
## Workspaces & Namespaces
Multi-tenant resources require workspace and/or namespace context:
```http
GET /api/v1/resources
X-Workspace-ID: 123
X-Namespace-ID: 456
```
Alternatively, use query parameters:
```http
GET /api/v1/resources?workspace_id=123&namespace_id=456
```
See [Namespaces & Entitlements](/security/namespaces) for details on multi-tenancy.
## Webhook Events
Configure webhooks to receive real-time notifications:
```http
POST /api/v1/webhooks
{
"url": "https://your-app.com/webhooks",
"events": ["resource.created", "resource.updated"],
"secret": "whsec_abc123..."
}
```
**Common events:**
- `{resource}.created` - Resource created
- `{resource}.updated` - Resource updated
- `{resource}.deleted` - Resource deleted
**Webhook payload:**
```json
{
"id": "evt_1234567890",
"type": "resource.created",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"object": {
"id": "res_abc123",
"type": "resource",
"attributes": {...}
}
}
}
```
Webhook requests include HMAC-SHA256 signature in headers:
```http
X-Webhook-Signature: sha256=abc123...
X-Webhook-Timestamp: 1640995200
```
See [Webhook Security](/api/authentication#webhook-signatures) for signature verification.
## Error Handling
All errors follow a consistent format. See [Error Reference](/api/errors) for details.
**Example error response:**
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"email": ["The email field is required."]
},
"request_id": "req_abc123"
}
}
```
## Resource Endpoints
### Core Resources
The following resource types are available:
- **Workspaces** - Multi-tenant workspaces
- **Namespaces** - Service isolation contexts
- **Users** - User accounts
- **API Keys** - API authentication credentials
- **Webhooks** - Webhook endpoints
### Workspace Endpoints
#### List Workspaces
```http
GET /api/v1/workspaces
```
**Response:**
```json
{
"data": [
{
"id": "wks_abc123",
"name": "Acme Corporation",
"slug": "acme-corp",
"tier": "business",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
]
}
```
#### Get Workspace
```http
GET /api/v1/workspaces/{workspace_id}
```
**Response:**
```json
{
"data": {
"id": "wks_abc123",
"name": "Acme Corporation",
"slug": "acme-corp",
"tier": "business",
"settings": {
"timezone": "UTC",
"locale": "en_GB"
},
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
```
#### Create Workspace
```http
POST /api/v1/workspaces
```
**Request:**
```json
{
"name": "New Workspace",
"slug": "new-workspace",
"tier": "pro"
}
```
**Response:** `201 Created`
#### Update Workspace
```http
PATCH /api/v1/workspaces/{workspace_id}
```
**Request:**
```json
{
"name": "Updated Name",
"settings": {
"timezone": "Europe/London"
}
}
```
**Response:** `200 OK`
#### Delete Workspace
```http
DELETE /api/v1/workspaces/{workspace_id}
```
**Response:** `204 No Content`
### Namespace Endpoints
#### List Namespaces
```http
GET /api/v1/namespaces
```
**Query parameters:**
- `owner_type` - Filter by owner type (`User` or `Workspace`)
- `workspace_id` - Filter by workspace
- `is_active` - Filter by active status
**Response:**
```json
{
"data": [
{
"id": "ns_abc123",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Personal Namespace",
"slug": "personal",
"owner_type": "User",
"owner_id": 42,
"workspace_id": null,
"is_default": true,
"is_active": true,
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
#### Get Namespace
```http
GET /api/v1/namespaces/{namespace_id}
```
**Response:**
```json
{
"data": {
"id": "ns_abc123",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Client: Acme Corp",
"slug": "client-acme",
"owner_type": "Workspace",
"owner_id": 10,
"workspace_id": 10,
"packages": [
{
"id": "pkg_starter",
"name": "Starter Package",
"expires_at": null
}
],
"entitlements": {
"storage": {
"used": 1024000000,
"limit": 5368709120,
"unit": "bytes"
},
"api_calls": {
"used": 5430,
"limit": 10000,
"reset_at": "2024-02-01T00:00:00Z"
}
}
}
}
```
#### Check Entitlement
```http
POST /api/v1/namespaces/{namespace_id}/entitlements/check
```
**Request:**
```json
{
"feature": "storage",
"quantity": 1073741824
}
```
**Response:**
```json
{
"allowed": false,
"reason": "LIMIT_EXCEEDED",
"message": "Storage limit exceeded. Used: 1.00 GB, Available: 0.50 GB, Requested: 1.00 GB",
"current_usage": 1024000000,
"limit": 5368709120,
"available": 536870912
}
```
### User Endpoints
#### List Users
```http
GET /api/v1/users
X-Workspace-ID: 123
```
**Response:**
```json
{
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"tier": "pro",
"email_verified_at": "2024-01-01T12:00:00Z",
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
#### Get Current User
```http
GET /api/v1/user
```
Returns the authenticated user.
#### Update User
```http
PATCH /api/v1/users/{user_id}
```
**Request:**
```json
{
"name": "Jane Doe",
"email": "jane@example.com"
}
```
### API Key Endpoints
#### List API Keys
```http
GET /api/v1/api-keys
```
**Response:**
```json
{
"data": [
{
"id": "key_abc123",
"name": "Production API Key",
"prefix": "sk_live_",
"last_used_at": "2024-01-15T10:30:00Z",
"expires_at": null,
"scopes": ["read:all", "write:resources"],
"rate_limit_tier": "business",
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
#### Create API Key
```http
POST /api/v1/api-keys
```
**Request:**
```json
{
"name": "New API Key",
"scopes": ["read:all"],
"rate_limit_tier": "pro",
"expires_at": "2025-01-01T00:00:00Z"
}
```
**Response:**
```json
{
"data": {
"id": "key_abc123",
"name": "New API Key",
"key": "sk_live_abc123def456...",
"scopes": ["read:all"],
"created_at": "2024-01-15T10:30:00Z"
}
}
```
⚠️ **Important:** The `key` field is only returned once during creation. Store it securely.
#### Revoke API Key
```http
DELETE /api/v1/api-keys/{key_id}
```
**Response:** `204 No Content`
### Webhook Endpoints
#### List Webhooks
```http
GET /api/v1/webhooks
```
**Response:**
```json
{
"data": [
{
"id": "wh_abc123",
"url": "https://your-app.com/webhooks",
"events": ["resource.created", "resource.updated"],
"is_active": true,
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
#### Create Webhook
```http
POST /api/v1/webhooks
```
**Request:**
```json
{
"url": "https://your-app.com/webhooks",
"events": ["resource.created"],
"secret": "whsec_abc123..."
}
```
#### Test Webhook
```http
POST /api/v1/webhooks/{webhook_id}/test
```
Sends a test event to the webhook URL.
**Response:**
```json
{
"success": true,
"status_code": 200,
"response_time_ms": 145
}
```
#### Webhook Deliveries
```http
GET /api/v1/webhooks/{webhook_id}/deliveries
```
View delivery history and retry failed deliveries:
```json
{
"data": [
{
"id": "del_abc123",
"event_type": "resource.created",
"status": "success",
"status_code": 200,
"attempts": 1,
"delivered_at": "2024-01-15T10:30:00Z"
}
]
}
```
## Best Practices
### 1. Use Idempotency Keys
Always use idempotency keys for create/update operations:
```javascript
const response = await fetch('/api/v1/resources', {
method: 'POST',
headers: {
'Idempotency-Key': crypto.randomUUID(),
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(data)
});
```
### 2. Handle Rate Limits
Respect rate limit headers and implement exponential backoff:
```javascript
async function apiRequest(url, options) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('X-RateLimit-Reset');
await sleep(retryAfter * 1000);
return apiRequest(url, options); // Retry
}
return response;
}
```
### 3. Use Field Selection
Request only needed fields to reduce payload size:
```http
GET /api/v1/resources?fields=id,name,status
```
### 4. Batch Operations
When possible, use batch endpoints instead of multiple single requests:
```http
POST /api/v1/resources/batch
{
"operations": [
{"action": "create", "data": {...}},
{"action": "update", "id": "res_123", "data": {...}}
]
}
```
### 5. Verify Webhook Signatures
Always verify webhook signatures to ensure authenticity:
```javascript
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expected = 'sha256=' + hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
```
### 6. Store API Keys Securely
- Never commit API keys to version control
- Use environment variables or secrets management
- Rotate keys regularly
- Use separate keys for development/production
### 7. Monitor Usage
Track your API usage to avoid hitting rate limits:
```http
GET /api/v1/usage
```
Returns current usage statistics for your account.
## SDKs & Libraries
Official SDKs available:
- **PHP:** `composer require core-php/sdk`
- **JavaScript/Node.js:** `npm install @core-php/sdk`
- **Python:** `pip install core-php-sdk`
**Example (PHP):**
```php
use CorePhp\SDK\Client;
$client = new Client('sk_live_abc123...');
$workspace = $client->workspaces->create([
'name' => 'My Workspace',
'tier' => 'pro',
]);
$namespaces = $client->namespaces->list([
'workspace_id' => $workspace->id,
]);
```
## Further Reading
- [Authentication](/api/authentication) - API key management and authentication methods
- [Error Handling](/api/errors) - Error codes and debugging
- [Namespaces & Entitlements](/security/namespaces) - Multi-tenancy and feature access
- [Webhooks Guide](#webhook-events) - Setting up webhook endpoints
- [Rate Limiting](#rate-limiting) - Understanding rate limits and tiers

525
api/errors.md Normal file
View file

@ -0,0 +1,525 @@
# API Errors
Core PHP Framework uses conventional HTTP response codes and provides detailed error information to help you debug issues.
## HTTP Status Codes
### 2xx Success
| Code | Status | Description |
|------|--------|-------------|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 202 | Accepted | Request accepted for processing |
| 204 | No Content | Request succeeded, no content to return |
### 4xx Client Errors
| Code | Status | Description |
|------|--------|-------------|
| 400 | Bad Request | Invalid request format or parameters |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 405 | Method Not Allowed | HTTP method not supported for endpoint |
| 409 | Conflict | Request conflicts with current state |
| 422 | Unprocessable Entity | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
### 5xx Server Errors
| Code | Status | Description |
|------|--------|-------------|
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Invalid response from upstream server |
| 503 | Service Unavailable | Server temporarily unavailable |
| 504 | Gateway Timeout | Upstream server timeout |
## Error Response Format
All errors return JSON with consistent structure:
```json
{
"message": "Human-readable error message",
"error_code": "MACHINE_READABLE_CODE",
"errors": {
"field": ["Detailed validation errors"]
},
"meta": {
"timestamp": "2026-01-26T12:00:00Z",
"request_id": "req_abc123"
}
}
```
## Common Errors
### 400 Bad Request
**Missing Required Parameter:**
```json
{
"message": "Missing required parameter: title",
"error_code": "MISSING_PARAMETER",
"errors": {
"title": ["The title field is required."]
}
}
```
**Invalid Parameter Type:**
```json
{
"message": "Invalid parameter type",
"error_code": "INVALID_TYPE",
"errors": {
"published_at": ["The published at must be a valid date."]
}
}
```
### 401 Unauthorized
**Missing Authentication:**
```json
{
"message": "Unauthenticated.",
"error_code": "UNAUTHENTICATED"
}
```
**Invalid API Key:**
```json
{
"message": "Invalid API key",
"error_code": "INVALID_API_KEY"
}
```
**Expired Token:**
```json
{
"message": "Token has expired",
"error_code": "TOKEN_EXPIRED",
"meta": {
"expired_at": "2026-01-20T12:00:00Z"
}
}
```
### 403 Forbidden
**Insufficient Permissions:**
```json
{
"message": "This action is unauthorized.",
"error_code": "INSUFFICIENT_PERMISSIONS",
"required_scope": "posts:write",
"provided_scopes": ["posts:read"]
}
```
**Workspace Suspended:**
```json
{
"message": "Workspace is suspended",
"error_code": "WORKSPACE_SUSPENDED",
"meta": {
"suspended_at": "2026-01-25T12:00:00Z",
"reason": "Payment overdue"
}
}
```
**Namespace Access Denied:**
```json
{
"message": "You do not have access to this namespace",
"error_code": "NAMESPACE_ACCESS_DENIED"
}
```
### 404 Not Found
**Resource Not Found:**
```json
{
"message": "Post not found",
"error_code": "RESOURCE_NOT_FOUND",
"resource_type": "Post",
"resource_id": 999
}
```
**Endpoint Not Found:**
```json
{
"message": "Endpoint not found",
"error_code": "ENDPOINT_NOT_FOUND",
"requested_path": "/v1/nonexistent"
}
```
### 409 Conflict
**Duplicate Resource:**
```json
{
"message": "A post with this slug already exists",
"error_code": "DUPLICATE_RESOURCE",
"conflicting_field": "slug",
"existing_resource_id": 123
}
```
**State Conflict:**
```json
{
"message": "Post is already published",
"error_code": "STATE_CONFLICT",
"current_state": "published",
"requested_action": "publish"
}
```
### 422 Unprocessable Entity
**Validation Failed:**
```json
{
"message": "The given data was invalid.",
"error_code": "VALIDATION_FAILED",
"errors": {
"title": [
"The title field is required."
],
"content": [
"The content must be at least 10 characters."
],
"category_id": [
"The selected category is invalid."
]
}
}
```
### 429 Too Many Requests
**Rate Limit Exceeded:**
```json
{
"message": "Too many requests",
"error_code": "RATE_LIMIT_EXCEEDED",
"limit": 10000,
"remaining": 0,
"reset_at": "2026-01-26T13:00:00Z",
"retry_after": 3600
}
```
**Usage Quota Exceeded:**
```json
{
"message": "Monthly usage quota exceeded",
"error_code": "QUOTA_EXCEEDED",
"quota_type": "monthly",
"limit": 50000,
"used": 50000,
"reset_at": "2026-02-01T00:00:00Z"
}
```
### 500 Internal Server Error
**Unexpected Error:**
```json
{
"message": "An unexpected error occurred",
"error_code": "INTERNAL_ERROR",
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-01-26T12:00:00Z"
}
}
```
::: tip
In production, internal error messages are sanitized. Include the `request_id` when reporting issues for debugging.
:::
## Error Codes
### Authentication Errors
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `UNAUTHENTICATED` | 401 | No authentication provided |
| `INVALID_API_KEY` | 401 | API key is invalid or revoked |
| `TOKEN_EXPIRED` | 401 | Authentication token has expired |
| `INVALID_CREDENTIALS` | 401 | Username/password incorrect |
| `INSUFFICIENT_PERMISSIONS` | 403 | Missing required permissions/scopes |
### Resource Errors
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `RESOURCE_NOT_FOUND` | 404 | Requested resource doesn't exist |
| `DUPLICATE_RESOURCE` | 409 | Resource with identifier already exists |
| `RESOURCE_LOCKED` | 409 | Resource is locked by another process |
| `STATE_CONFLICT` | 409 | Action conflicts with current state |
### Validation Errors
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `VALIDATION_FAILED` | 422 | One or more fields failed validation |
| `INVALID_TYPE` | 400 | Parameter has wrong data type |
| `MISSING_PARAMETER` | 400 | Required parameter not provided |
| `INVALID_FORMAT` | 400 | Parameter format is invalid |
### Rate Limiting Errors
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests in time window |
| `QUOTA_EXCEEDED` | 429 | Usage quota exceeded |
| `CONCURRENT_LIMIT_EXCEEDED` | 429 | Too many concurrent requests |
### Business Logic Errors
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `ENTITLEMENT_DENIED` | 403 | Feature not included in plan |
| `WORKSPACE_SUSPENDED` | 403 | Workspace is suspended |
| `NAMESPACE_ACCESS_DENIED` | 403 | No access to namespace |
| `PAYMENT_REQUIRED` | 402 | Payment required to proceed |
### System Errors
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `INTERNAL_ERROR` | 500 | Unexpected server error |
| `SERVICE_UNAVAILABLE` | 503 | Service temporarily unavailable |
| `GATEWAY_TIMEOUT` | 504 | Upstream service timeout |
| `MAINTENANCE_MODE` | 503 | System under maintenance |
## Handling Errors
### JavaScript Example
```javascript
async function createPost(data) {
try {
const response = await fetch('/api/v1/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 401:
// Re-authenticate
redirectToLogin();
break;
case 403:
// Show permission error
showError('You do not have permission to create posts');
break;
case 422:
// Show validation errors
showValidationErrors(error.errors);
break;
case 429:
// Show rate limit message
showError(`Rate limited. Retry after ${error.retry_after} seconds`);
break;
default:
// Generic error
showError(error.message);
}
return null;
}
return await response.json();
} catch (err) {
// Network error
showError('Network error. Please check your connection.');
return null;
}
}
```
### PHP Example
```php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client(['base_uri' => 'https://api.example.com']);
try {
$response = $client->post('/v1/posts', [
'headers' => [
'Authorization' => "Bearer {$apiKey}",
'Content-Type' => 'application/json',
],
'json' => $data,
]);
$post = json_decode($response->getBody(), true);
} catch (RequestException $e) {
$statusCode = $e->getResponse()->getStatusCode();
$error = json_decode($e->getResponse()->getBody(), true);
switch ($statusCode) {
case 401:
throw new AuthenticationException($error['message']);
case 403:
throw new AuthorizationException($error['message']);
case 422:
throw new ValidationException($error['errors']);
case 429:
throw new RateLimitException($error['retry_after']);
default:
throw new ApiException($error['message']);
}
}
```
## Debugging
### Request ID
Every response includes a `request_id` for debugging:
```bash
curl -i https://api.example.com/v1/posts
```
Response headers:
```
X-Request-ID: req_abc123def456
```
Include this ID when reporting issues.
### Debug Mode
In development, enable debug mode for detailed errors:
```php
// .env
APP_DEBUG=true
```
Debug responses include:
- Full stack traces
- SQL queries
- Exception details
::: danger
Never enable debug mode in production! It exposes sensitive information.
:::
### Logging
All errors are logged with context:
```
[2026-01-26 12:00:00] production.ERROR: Post not found
{
"user_id": 123,
"workspace_id": 456,
"namespace_id": 789,
"post_id": 999,
"request_id": "req_abc123"
}
```
## Best Practices
### 1. Always Check Status Codes
```javascript
// ✅ Good
if (!response.ok) {
handleError(response);
}
// ❌ Bad - assumes success
const data = await response.json();
```
### 2. Handle All Error Types
```javascript
// ✅ Good - specific handling
switch (error.error_code) {
case 'RATE_LIMIT_EXCEEDED':
retryAfter(error.retry_after);
break;
case 'VALIDATION_FAILED':
showValidationErrors(error.errors);
break;
default:
showGenericError(error.message);
}
// ❌ Bad - generic handling
alert(error.message);
```
### 3. Implement Retry Logic
```javascript
async function fetchWithRetry(url, options, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
// Rate limited - wait and retry
const retryAfter = parseInt(response.headers.get('Retry-After'));
await sleep(retryAfter * 1000);
continue;
}
return response;
} catch (err) {
if (i === retries - 1) throw err;
await sleep(1000 * Math.pow(2, i)); // Exponential backoff
}
}
}
```
### 4. Log Error Context
```javascript
// ✅ Good - log context
console.error('API Error:', {
endpoint: '/v1/posts',
method: 'POST',
status: response.status,
error_code: error.error_code,
request_id: error.meta.request_id
});
// ❌ Bad - no context
console.error(error.message);
```
## Learn More
- [API Authentication →](/api/authentication)
- [Rate Limiting →](/api/endpoints#rate-limiting)
- [API Endpoints →](/api/endpoints)

BIN
build/.DS_Store vendored Normal file

Binary file not shown.

100
build/cli/ai/example.md Normal file
View file

@ -0,0 +1,100 @@
# AI Examples
## Workflow Example
Complete task management workflow:
```bash
# 1. List available tasks
core ai tasks --status pending
# 2. Auto-select and claim a task
core ai task --auto --claim
# 3. Work on the task...
# 4. Update progress
core ai task:update abc123 --progress 75
# 5. Commit with task reference
core ai task:commit abc123 -m 'implement feature'
# 6. Create PR
core ai task:pr abc123
# 7. Mark complete
core ai task:complete abc123 --output 'Feature implemented and PR created'
```
## Task Filtering
```bash
# By status
core ai tasks --status pending
core ai tasks --status in_progress
# By priority
core ai tasks --priority critical
core ai tasks --priority high
# By labels
core ai tasks --labels bug,urgent
# Combined filters
core ai tasks --status pending --priority high --labels bug
```
## Task Updates
```bash
# Change status
core ai task:update abc123 --status in_progress
core ai task:update abc123 --status blocked
# Update progress
core ai task:update abc123 --progress 25
core ai task:update abc123 --progress 50 --notes 'Halfway done'
core ai task:update abc123 --progress 100
```
## Git Integration
```bash
# Commit with task reference
core ai task:commit abc123 -m 'add authentication'
# With scope
core ai task:commit abc123 -m 'fix login' --scope auth
# Commit and push
core ai task:commit abc123 -m 'complete feature' --push
# Create PR
core ai task:pr abc123
# Draft PR
core ai task:pr abc123 --draft
# PR with labels
core ai task:pr abc123 --labels 'enhancement,ready-for-review'
# PR to different base
core ai task:pr abc123 --base develop
```
## Configuration
### Environment Variables
```env
AGENTIC_TOKEN=your-api-token
AGENTIC_BASE_URL=https://agentic.example.com
```
### ~/.core/agentic.yaml
```yaml
token: your-api-token
base_url: https://agentic.example.com
default_project: my-project
```

262
build/cli/ai/index.md Normal file
View file

@ -0,0 +1,262 @@
# core ai
AI agent task management and Claude Code integration.
## Task Management Commands
| Command | Description |
|---------|-------------|
| `tasks` | List available tasks from core-agentic |
| `task` | View task details or auto-select |
| `task:update` | Update task status or progress |
| `task:complete` | Mark task as completed or failed |
| `task:commit` | Create git commit with task reference |
| `task:pr` | Create GitHub PR linked to task |
## Claude Integration
| Command | Description |
|---------|-------------|
| `claude run` | Run Claude Code in current directory |
| `claude config` | Manage Claude configuration |
---
## Configuration
Task commands load configuration from:
1. Environment variables (`AGENTIC_TOKEN`, `AGENTIC_BASE_URL`)
2. `.env` file in current directory
3. `~/.core/agentic.yaml`
---
## ai tasks
List available tasks from core-agentic.
```bash
core ai tasks [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--status` | Filter by status (`pending`, `in_progress`, `completed`, `blocked`) |
| `--priority` | Filter by priority (`critical`, `high`, `medium`, `low`) |
| `--labels` | Filter by labels (comma-separated) |
| `--project` | Filter by project |
| `--limit` | Max number of tasks to return (default: 20) |
### Examples
```bash
# List all pending tasks
core ai tasks
# Filter by status and priority
core ai tasks --status pending --priority high
# Filter by labels
core ai tasks --labels bug,urgent
```
---
## ai task
View task details or auto-select a task.
```bash
core ai task [task-id] [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--auto` | Auto-select highest priority pending task |
| `--claim` | Claim the task after showing details |
| `--context` | Show gathered context for AI collaboration |
### Examples
```bash
# Show task details
core ai task abc123
# Show and claim
core ai task abc123 --claim
# Show with context
core ai task abc123 --context
# Auto-select highest priority pending task
core ai task --auto
```
---
## ai task:update
Update a task's status, progress, or notes.
```bash
core ai task:update <task-id> [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--status` | New status (`pending`, `in_progress`, `completed`, `blocked`) |
| `--progress` | Progress percentage (0-100) |
| `--notes` | Notes about the update |
### Examples
```bash
# Set task to in progress
core ai task:update abc123 --status in_progress
# Update progress with notes
core ai task:update abc123 --progress 50 --notes 'Halfway done'
```
---
## ai task:complete
Mark a task as completed with optional output and artifacts.
```bash
core ai task:complete <task-id> [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--output` | Summary of the completed work |
| `--failed` | Mark the task as failed |
| `--error` | Error message if failed |
### Examples
```bash
# Complete successfully
core ai task:complete abc123 --output 'Feature implemented'
# Mark as failed
core ai task:complete abc123 --failed --error 'Build failed'
```
---
## ai task:commit
Create a git commit with a task reference and co-author attribution.
```bash
core ai task:commit <task-id> [flags]
```
Commit message format:
```
feat(scope): description
Task: #123
Co-Authored-By: Claude <noreply@anthropic.com>
```
### Flags
| Flag | Description |
|------|-------------|
| `-m`, `--message` | Commit message (without task reference) |
| `--scope` | Scope for the commit type (e.g., `auth`, `api`, `ui`) |
| `--push` | Push changes after committing |
### Examples
```bash
# Commit with message
core ai task:commit abc123 --message 'add user authentication'
# With scope
core ai task:commit abc123 -m 'fix login bug' --scope auth
# Commit and push
core ai task:commit abc123 -m 'update docs' --push
```
---
## ai task:pr
Create a GitHub pull request linked to a task.
```bash
core ai task:pr <task-id> [flags]
```
Requires the GitHub CLI (`gh`) to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--title` | PR title (defaults to task title) |
| `--base` | Base branch (defaults to main) |
| `--draft` | Create as draft PR |
| `--labels` | Labels to add (comma-separated) |
### Examples
```bash
# Create PR with defaults
core ai task:pr abc123
# Custom title
core ai task:pr abc123 --title 'Add authentication feature'
# Draft PR with labels
core ai task:pr abc123 --draft --labels 'enhancement,needs-review'
# Target different base branch
core ai task:pr abc123 --base develop
```
---
## ai claude
Claude Code integration commands.
### ai claude run
Run Claude Code in the current directory.
```bash
core ai claude run
```
### ai claude config
Manage Claude configuration.
```bash
core ai claude config
```
---
## Workflow Example
See [Workflow Example](example.md#workflow-example) for a complete task management workflow.
## See Also
- [dev](../dev/) - Multi-repo workflow commands
- [Claude Code documentation](https://claude.ai/code)

View file

@ -0,0 +1,83 @@
# Build Examples
## Quick Start
```bash
# Auto-detect and build
core build
# Build for specific platforms
core build --targets linux/amd64,darwin/arm64
# CI mode
core build --ci
```
## Configuration
`.core/build.yaml`:
```yaml
version: 1
project:
name: myapp
binary: myapp
build:
main: ./cmd/myapp
ldflags:
- -s -w
- -X main.version={{.Version}}
targets:
- os: linux
arch: amd64
- os: linux
arch: arm64
- os: darwin
arch: arm64
```
## Cross-Platform Build
```bash
core build --targets linux/amd64,linux/arm64,darwin/arm64,windows/amd64
```
Output:
```
dist/
├── myapp-linux-amd64.tar.gz
├── myapp-linux-arm64.tar.gz
├── myapp-darwin-arm64.tar.gz
├── myapp-windows-amd64.zip
└── CHECKSUMS.txt
```
## Code Signing
```yaml
sign:
enabled: true
gpg:
key: $GPG_KEY_ID
macos:
identity: "Developer ID Application: Your Name (TEAM_ID)"
notarize: true
apple_id: $APPLE_ID
team_id: $APPLE_TEAM_ID
app_password: $APPLE_APP_PASSWORD
```
## Docker Build
```bash
core build --type docker --image ghcr.io/myorg/myapp
```
## Wails Desktop App
```bash
core build --type wails --targets darwin/arm64,windows/amd64
```

176
build/cli/build/index.md Normal file
View file

@ -0,0 +1,176 @@
# core build
Build Go, Wails, Docker, and LinuxKit projects with automatic project detection.
## Subcommands
| Command | Description |
|---------|-------------|
| [sdk](sdk/) | Generate API SDKs from OpenAPI |
| `from-path` | Build from a local directory |
| `pwa` | Build from a live PWA URL |
## Usage
```bash
core build [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--type` | Project type: `go`, `wails`, `docker`, `linuxkit`, `taskfile` (auto-detected) |
| `--targets` | Build targets: `linux/amd64,darwin/arm64,windows/amd64` |
| `--output` | Output directory (default: `dist`) |
| `--ci` | CI mode - minimal output with JSON artifact list at the end |
| `--image` | Docker image name (for docker builds) |
| `--config` | Config file path (for linuxkit: YAML config, for docker: Dockerfile) |
| `--format` | Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk) |
| `--push` | Push Docker image after build (default: false) |
| `--archive` | Create archives (tar.gz for linux/darwin, zip for windows) - default: true |
| `--checksum` | Generate SHA256 checksums and CHECKSUMS.txt - default: true |
| `--no-sign` | Skip all code signing |
| `--notarize` | Enable macOS notarization (requires Apple credentials) |
## Examples
### Go Project
```bash
# Auto-detect and build
core build
# Build for specific platforms
core build --targets linux/amd64,linux/arm64,darwin/arm64
# CI mode
core build --ci
```
### Wails Project
```bash
# Build Wails desktop app
core build --type wails
# Build for all desktop platforms
core build --type wails --targets darwin/amd64,darwin/arm64,windows/amd64,linux/amd64
```
### Docker Image
```bash
# Build Docker image
core build --type docker
# With custom image name
core build --type docker --image ghcr.io/myorg/myapp
# Build and push to registry
core build --type docker --image ghcr.io/myorg/myapp --push
```
### LinuxKit Image
```bash
# Build LinuxKit ISO
core build --type linuxkit
# Build with specific format
core build --type linuxkit --config linuxkit.yml --format qcow2-bios
```
## Project Detection
Core automatically detects project type based on files:
| Files | Type |
|-------|------|
| `wails.json` | Wails |
| `go.mod` | Go |
| `Dockerfile` | Docker |
| `Taskfile.yml` | Taskfile |
| `composer.json` | PHP |
| `package.json` | Node |
## Output
Build artifacts are placed in `dist/` by default:
```
dist/
├── myapp-linux-amd64.tar.gz
├── myapp-linux-arm64.tar.gz
├── myapp-darwin-amd64.tar.gz
├── myapp-darwin-arm64.tar.gz
├── myapp-windows-amd64.zip
└── CHECKSUMS.txt
```
## Configuration
Optional `.core/build.yaml` - see [Configuration](example.md#configuration) for examples.
## Code Signing
Core supports GPG signing for checksums and native code signing for macOS.
### GPG Signing
Signs `CHECKSUMS.txt` with a detached ASCII signature (`.asc`):
```bash
# Build with GPG signing (default if key configured)
core build
# Skip signing
core build --no-sign
```
Users can verify:
```bash
gpg --verify CHECKSUMS.txt.asc CHECKSUMS.txt
sha256sum -c CHECKSUMS.txt
```
### macOS Code Signing
Signs Darwin binaries with your Developer ID and optionally notarizes with Apple:
```bash
# Build with codesign (automatic if identity configured)
core build
# Build with notarization (takes 1-5 minutes)
core build --notarize
```
### Environment Variables
| Variable | Purpose |
|----------|---------|
| `GPG_KEY_ID` | GPG key ID or fingerprint |
| `CODESIGN_IDENTITY` | macOS Developer ID (fallback) |
| `APPLE_ID` | Apple account email |
| `APPLE_TEAM_ID` | Apple Developer Team ID |
| `APPLE_APP_PASSWORD` | App-specific password for notarization |
## Building from PWAs and Static Sites
### Build from Local Directory
Build a desktop app from static web application files:
```bash
core build from-path --path ./dist
```
### Build from Live PWA
Build a desktop app from a live Progressive Web App URL:
```bash
core build pwa --url https://example.com
```

View file

@ -0,0 +1,56 @@
# SDK Build Examples
## Generate All SDKs
```bash
core build sdk
```
## Specific Language
```bash
core build sdk --lang typescript
core build sdk --lang php
core build sdk --lang go
```
## Custom Spec
```bash
core build sdk --spec ./api/openapi.yaml
```
## With Version
```bash
core build sdk --version v2.0.0
```
## Preview
```bash
core build sdk --dry-run
```
## Configuration
`.core/sdk.yaml`:
```yaml
version: 1
spec: ./api/openapi.yaml
languages:
- name: typescript
output: sdk/typescript
package: "@myorg/api-client"
- name: php
output: sdk/php
namespace: MyOrg\ApiClient
- name: go
output: sdk/go
module: github.com/myorg/api-client-go
```

View file

@ -0,0 +1,27 @@
# core build sdk
Generate typed API clients from OpenAPI specifications. Supports TypeScript, Python, Go, and PHP.
## Usage
```bash
core build sdk [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--spec` | Path to OpenAPI spec file |
| `--lang` | Generate only this language (typescript, python, go, php) |
| `--version` | Version to embed in generated SDKs |
| `--dry-run` | Show what would be generated without writing files |
## Examples
```bash
core build sdk # Generate all
core build sdk --lang typescript # TypeScript only
core build sdk --spec ./api.yaml # Custom spec
core build sdk --dry-run # Preview
```

View file

@ -0,0 +1,36 @@
# CI Changelog Examples
```bash
core ci changelog
```
## Output
```markdown
## v1.2.0
### Features
- Add user authentication (#123)
- Support dark mode (#124)
### Bug Fixes
- Fix memory leak in worker (#125)
### Performance
- Optimize database queries (#126)
```
## Configuration
`.core/release.yaml`:
```yaml
changelog:
include:
- feat
- fix
- perf
exclude:
- chore
- docs
```

View file

@ -0,0 +1,28 @@
# core ci changelog
Generate changelog from conventional commits.
## Usage
```bash
core ci changelog
```
## Output
Generates markdown changelog from git commits since last tag:
```markdown
## v1.2.0
### Features
- Add user authentication (#123)
- Support dark mode (#124)
### Bug Fixes
- Fix memory leak in worker (#125)
```
## Configuration
See [configuration.md](../../../configuration.md) for changelog configuration options.

90
build/cli/ci/example.md Normal file
View file

@ -0,0 +1,90 @@
# CI Examples
## Quick Start
```bash
# Build first
core build
# Preview release
core ci
# Publish
core ci --we-are-go-for-launch
```
## Configuration
`.core/release.yaml`:
```yaml
version: 1
project:
name: myapp
repository: host-uk/myapp
publishers:
- type: github
```
## Publisher Examples
### GitHub + Docker
```yaml
publishers:
- type: github
- type: docker
registry: ghcr.io
image: host-uk/myapp
platforms:
- linux/amd64
- linux/arm64
tags:
- latest
- "{{.Version}}"
```
### Full Stack (GitHub + npm + Homebrew)
```yaml
publishers:
- type: github
- type: npm
package: "@host-uk/myapp"
access: public
- type: homebrew
tap: host-uk/homebrew-tap
```
### LinuxKit Image
```yaml
publishers:
- type: linuxkit
config: .core/linuxkit/server.yml
formats:
- iso
- qcow2
platforms:
- linux/amd64
- linux/arm64
```
## Changelog Configuration
```yaml
changelog:
include:
- feat
- fix
- perf
exclude:
- chore
- docs
- test
```

79
build/cli/ci/index.md Normal file
View file

@ -0,0 +1,79 @@
# core ci
Publish releases to GitHub, Docker, npm, Homebrew, and more.
**Safety:** Dry-run by default. Use `--we-are-go-for-launch` to actually publish.
## Subcommands
| Command | Description |
|---------|-------------|
| [init](init/) | Initialize release config |
| [changelog](changelog/) | Generate changelog |
| [version](version/) | Show determined version |
## Usage
```bash
core ci [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--we-are-go-for-launch` | Actually publish (default is dry-run) |
| `--version` | Override version |
| `--draft` | Create as draft release |
| `--prerelease` | Mark as prerelease |
## Examples
```bash
# Preview what would be published (safe)
core ci
# Actually publish
core ci --we-are-go-for-launch
# Publish as draft
core ci --we-are-go-for-launch --draft
# Publish as prerelease
core ci --we-are-go-for-launch --prerelease
```
## Workflow
Build and publish are **separated** to prevent accidents:
```bash
# Step 1: Build artifacts
core build
core build sdk
# Step 2: Preview (dry-run by default)
core ci
# Step 3: Publish (explicit flag required)
core ci --we-are-go-for-launch
```
## Publishers
See [Publisher Examples](example.md#publisher-examples) for configuration.
| Type | Target |
|------|--------|
| `github` | GitHub Releases |
| `docker` | Container registries |
| `linuxkit` | LinuxKit images |
| `npm` | npm registry |
| `homebrew` | Homebrew tap |
| `scoop` | Scoop bucket |
| `aur` | Arch User Repository |
| `chocolatey` | Chocolatey |
## Changelog
Auto-generated from conventional commits. See [Changelog Configuration](example.md#changelog-configuration).

View file

@ -0,0 +1,17 @@
# CI Init Examples
```bash
core ci init
```
Creates `.core/release.yaml`:
```yaml
version: 1
project:
name: myapp
publishers:
- type: github
```

View file

@ -0,0 +1,11 @@
# core ci init
Initialize release configuration.
## Usage
```bash
core ci init
```
Creates `.core/release.yaml` with default configuration. See [Configuration](../example.md#configuration) for output format.

View file

@ -0,0 +1,18 @@
# CI Version Examples
```bash
core ci version
```
## Output
```
v1.2.0
```
## Version Resolution
1. `--version` flag (if provided)
2. Git tag on HEAD
3. Latest git tag + increment
4. `v0.0.1` (no tags)

View file

@ -0,0 +1,21 @@
# core ci version
Show the determined release version.
## Usage
```bash
core ci version
```
## Output
```
v1.2.0
```
Version is determined from:
1. `--version` flag (if provided)
2. Git tag on HEAD
3. Latest git tag + increment
4. `v0.0.1` (if no tags exist)

61
build/cli/dev/ci/index.md Normal file
View file

@ -0,0 +1,61 @@
# core dev ci
Check CI status across all repositories.
Fetches GitHub Actions workflow status for all repos. Shows latest run status for each repo. Requires the `gh` CLI to be installed and authenticated.
## Usage
```bash
core dev ci [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--branch` | Filter by branch (default: main) |
| `--failed` | Show only failed runs |
## Examples
```bash
# Check CI status for all repos
core dev ci
# Check specific branch
core dev ci --branch develop
# Show only failures
core dev ci --failed
```
## Output
```
core-php ✓ passing 2m ago
core-tenant ✓ passing 5m ago
core-admin ✗ failed 12m ago
core-api ⏳ running now
core-bio ✓ passing 1h ago
```
## Status Icons
| Symbol | Meaning |
|--------|---------|
| `✓` | Passing |
| `✗` | Failed |
| `⏳` | Running |
| `-` | No runs |
## Requirements
- GitHub CLI (`gh`) must be installed
- Must be authenticated: `gh auth login`
## See Also
- [issues command](../issues/) - List open issues
- [reviews command](../reviews/) - List PRs needing review

View file

@ -0,0 +1,46 @@
# core dev commit
Claude-assisted commits across repositories.
Uses Claude to create commits for dirty repos. Shows uncommitted changes and invokes Claude to generate commit messages.
## Usage
```bash
core dev commit [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--all` | Commit all dirty repos without prompting |
## Examples
```bash
# Interactive commit (prompts for each repo)
core dev commit
# Commit all dirty repos automatically
core dev commit --all
# Use specific registry
core dev commit --registry ~/projects/repos.yaml
```
## How It Works
1. Scans all repositories for uncommitted changes
2. For each dirty repo:
- Shows the diff
- Invokes Claude to generate a commit message
- Creates the commit with `Co-Authored-By: Claude`
3. Reports success/failure for each repo
## See Also
- [health command](../health/) - Check repo status
- [push command](../push/) - Push commits after committing
- [work command](../work/) - Full workflow (status + commit + push)

203
build/cli/dev/example.md Normal file
View file

@ -0,0 +1,203 @@
# Dev Examples
## Multi-Repo Workflow
```bash
# Quick status
core dev health
# Detailed breakdown
core dev health --verbose
# Full workflow
core dev work
# Status only
core dev work --status
# Commit and push
core dev work --commit
# Commit dirty repos
core dev commit
# Commit all without prompting
core dev commit --all
# Push unpushed
core dev push
# Push without confirmation
core dev push --force
# Pull behind repos
core dev pull
# Pull all repos
core dev pull --all
```
## GitHub Integration
```bash
# Open issues
core dev issues
# Filter by assignee
core dev issues --assignee @me
# Limit results
core dev issues --limit 5
# PRs needing review
core dev reviews
# All PRs including drafts
core dev reviews --all
# Filter by author
core dev reviews --author username
# CI status
core dev ci
# Only failed runs
core dev ci --failed
# Specific branch
core dev ci --branch develop
```
## Dependency Analysis
```bash
# What depends on core-php?
core dev impact core-php
```
## Task Management
```bash
# List tasks
core ai tasks
# Filter by status and priority
core ai tasks --status pending --priority high
# Filter by labels
core ai tasks --labels bug,urgent
# Show task details
core ai task abc123
# Auto-select highest priority task
core ai task --auto
# Claim a task
core ai task abc123 --claim
# Update task status
core ai task:update abc123 --status in_progress
# Add progress notes
core ai task:update abc123 --progress 50 --notes 'Halfway done'
# Complete a task
core ai task:complete abc123 --output 'Feature implemented'
# Mark as failed
core ai task:complete abc123 --failed --error 'Build failed'
# Commit with task reference
core ai task:commit abc123 -m 'add user authentication'
# Commit with scope and push
core ai task:commit abc123 -m 'fix login bug' --scope auth --push
# Create PR for task
core ai task:pr abc123
# Create draft PR with labels
core ai task:pr abc123 --draft --labels 'enhancement,needs-review'
```
## Service API Management
```bash
# Synchronize public service APIs
core dev sync
# Or using the api command
core dev api sync
```
## Dev Environment
```bash
# First time setup
core dev install
core dev boot
# Open shell
core dev shell
# Mount and serve
core dev serve
# Run tests
core dev test
# Sandboxed Claude
core dev claude
```
## Configuration
### repos.yaml
```yaml
org: host-uk
repos:
core-php:
type: package
description: Foundation framework
core-tenant:
type: package
depends: [core-php]
```
### ~/.core/config.yaml
```yaml
version: 1
images:
source: auto # auto | github | registry | cdn
cdn:
url: https://images.example.com/core-devops
github:
repo: host-uk/core-images
registry:
image: ghcr.io/host-uk/core-devops
```
### .core/test.yaml
```yaml
version: 1
commands:
- name: unit
run: vendor/bin/pest --parallel
- name: types
run: vendor/bin/phpstan analyse
- name: lint
run: vendor/bin/pint --test
env:
APP_ENV: testing
DB_CONNECTION: sqlite
```

View file

@ -0,0 +1,52 @@
# core dev health
Quick health check across all repositories.
Shows a summary of repository health: total repos, dirty repos, unpushed commits, etc.
## Usage
```bash
core dev health [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--verbose` | Show detailed breakdown |
## Examples
```bash
# Quick health summary
core dev health
# Detailed breakdown
core dev health --verbose
# Use specific registry
core dev health --registry ~/projects/repos.yaml
```
## Output
```
18 repos │ 2 dirty │ 1 ahead │ all synced
```
With `--verbose`:
```
Repos: 18
Dirty: 2 (core-php, core-admin)
Ahead: 1 (core-tenant)
Behind: 0
Synced: ✓
```
## See Also
- [work command](../work/) - Full workflow (status + commit + push)
- [commit command](../commit/) - Claude-assisted commits

View file

@ -0,0 +1,65 @@
# core dev impact
Show impact of changing a repository.
Analyses the dependency graph to show which repos would be affected by changes to the specified repo.
## Usage
```bash
core dev impact <repo-name> [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
## Examples
```bash
# Show what depends on core-php
core dev impact core-php
# Show what depends on core-tenant
core dev impact core-tenant
```
## Output
```
Impact of changes to core-php:
Direct dependents (5):
core-tenant
core-admin
core-api
core-mcp
core-commerce
Indirect dependents (12):
core-bio (via core-tenant)
core-social (via core-tenant)
core-analytics (via core-tenant)
core-notify (via core-tenant)
core-trust (via core-tenant)
core-support (via core-tenant)
core-content (via core-tenant)
core-developer (via core-tenant)
core-agentic (via core-mcp)
...
Total: 17 repos affected
```
## Use Cases
- Before making breaking changes, see what needs updating
- Plan release order based on dependency graph
- Understand the ripple effect of changes
## See Also
- [health command](../health/) - Quick repo status
- [setup command](../../setup/) - Clone repos with dependencies

388
build/cli/dev/index.md Normal file
View file

@ -0,0 +1,388 @@
# core dev
Multi-repo workflow and portable development environment.
## Multi-Repo Commands
| Command | Description |
|---------|-------------|
| [work](work/) | Full workflow: status + commit + push |
| `health` | Quick health check across repos |
| `commit` | Claude-assisted commits |
| `push` | Push repos with unpushed commits |
| `pull` | Pull repos that are behind |
| `issues` | List open issues |
| `reviews` | List PRs needing review |
| `ci` | Check CI status |
| `impact` | Show dependency impact |
| `api` | Tools for managing service APIs |
| `sync` | Synchronize public service APIs |
## Task Management Commands
> **Note:** Task management commands have moved to [`core ai`](../ai/).
| Command | Description |
|---------|-------------|
| [`ai tasks`](../ai/) | List available tasks from core-agentic |
| [`ai task`](../ai/) | Show task details or auto-select a task |
| [`ai task:update`](../ai/) | Update task status or progress |
| [`ai task:complete`](../ai/) | Mark a task as completed |
| [`ai task:commit`](../ai/) | Auto-commit changes with task reference |
| [`ai task:pr`](../ai/) | Create a pull request for a task |
## Dev Environment Commands
| Command | Description |
|---------|-------------|
| `install` | Download the core-devops image |
| `boot` | Start the environment |
| `stop` | Stop the environment |
| `status` | Show status |
| `shell` | Open shell |
| `serve` | Start dev server |
| `test` | Run tests |
| `claude` | Sandboxed Claude |
| `update` | Update image |
---
## Dev Environment Overview
Core DevOps provides a sandboxed, immutable development environment based on LinuxKit with 100+ embedded tools.
## Quick Start
```bash
# First time setup
core dev install
core dev boot
# Open shell
core dev shell
# Or mount current project and serve
core dev serve
```
## dev install
Download the core-devops image for your platform.
```bash
core dev install
```
Downloads the platform-specific dev environment image including Go, PHP, Node.js, Python, Docker, and Claude CLI. Downloads are cached at `~/.core/images/`.
### Examples
```bash
# Download image (auto-detects platform)
core dev install
```
## dev boot
Start the development environment.
```bash
core dev boot [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--memory` | Memory allocation in MB (default: 4096) |
| `--cpus` | Number of CPUs (default: 2) |
| `--fresh` | Stop existing and start fresh |
### Examples
```bash
# Start with defaults
core dev boot
# More resources
core dev boot --memory 8192 --cpus 4
# Fresh start
core dev boot --fresh
```
## dev shell
Open a shell in the running environment.
```bash
core dev shell [flags] [-- command]
```
Uses SSH by default, or serial console with `--console`.
### Flags
| Flag | Description |
|------|-------------|
| `--console` | Use serial console instead of SSH |
### Examples
```bash
# SSH into environment
core dev shell
# Serial console (for debugging)
core dev shell --console
# Run a command
core dev shell -- ls -la
```
## dev serve
Mount current directory and start the appropriate dev server.
```bash
core dev serve [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--port` | Port to expose (default: 8000) |
| `--path` | Subdirectory to serve |
### Auto-Detection
| Project | Server Command |
|---------|---------------|
| Laravel (`artisan`) | `php artisan octane:start` |
| Node (`package.json` with `dev` script) | `npm run dev` |
| PHP (`composer.json`) | `frankenphp php-server` |
| Other | `python -m http.server` |
### Examples
```bash
# Auto-detect and serve
core dev serve
# Custom port
core dev serve --port 3000
```
## dev test
Run tests inside the environment.
```bash
core dev test [flags] [-- custom command]
```
### Flags
| Flag | Description |
|------|-------------|
| `--name` | Run named test command from `.core/test.yaml` |
### Test Detection
Core auto-detects the test framework or uses `.core/test.yaml`:
1. `.core/test.yaml` - Custom config
2. `composer.json``composer test`
3. `package.json``npm test`
4. `go.mod``go test ./...`
5. `pytest.ini``pytest`
6. `Taskfile.yaml``task test`
### Examples
```bash
# Auto-detect and run tests
core dev test
# Run named test from config
core dev test --name integration
# Custom command
core dev test -- go test -v ./pkg/...
```
### Test Configuration
Create `.core/test.yaml` for custom test setup - see [Configuration](example.md#configuration) for examples.
## dev claude
Start a sandboxed Claude session with your project mounted.
```bash
core dev claude [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--model` | Model to use (`opus`, `sonnet`) |
| `--no-auth` | Don't forward any auth credentials |
| `--auth` | Selective auth forwarding (`gh`, `anthropic`, `ssh`, `git`) |
### What Gets Forwarded
By default, these are forwarded to the sandbox:
- `~/.anthropic/` or `ANTHROPIC_API_KEY`
- `~/.config/gh/` (GitHub CLI auth)
- SSH agent
- Git config (name, email)
### Examples
```bash
# Full auth forwarding (default)
core dev claude
# Use Opus model
core dev claude --model opus
# Clean sandbox
core dev claude --no-auth
# Only GitHub and Anthropic auth
core dev claude --auth gh,anthropic
```
### Why Use This?
- **Immutable base** - Reset anytime with `core dev boot --fresh`
- **Safe experimentation** - Claude can install packages, make mistakes
- **Host system untouched** - All changes stay in the sandbox
- **Real credentials** - Can still push code, create PRs
- **Full tooling** - 100+ tools available in the image
## dev status
Show the current state of the development environment.
```bash
core dev status
```
Output includes:
- Running/stopped state
- Resource usage (CPU, memory)
- Exposed ports
- Mounted directories
## dev update
Check for and apply updates.
```bash
core dev update [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--apply` | Download and apply the update |
### Examples
```bash
# Check for updates
core dev update
# Apply available update
core dev update --apply
```
## Embedded Tools
The core-devops image includes 100+ tools:
| Category | Tools |
|----------|-------|
| **AI/LLM** | claude, gemini, aider, ollama, llm |
| **VCS** | git, gh, glab, lazygit, delta, git-lfs |
| **Runtimes** | frankenphp, node, bun, deno, go, python3, rustc |
| **Package Mgrs** | composer, npm, pnpm, yarn, pip, uv, cargo |
| **Build** | task, make, just, nx, turbo |
| **Linting** | pint, phpstan, prettier, eslint, biome, golangci-lint, ruff |
| **Testing** | phpunit, pest, vitest, playwright, k6 |
| **Infra** | docker, kubectl, k9s, helm, terraform, ansible |
| **Databases** | sqlite3, mysql, psql, redis-cli, mongosh, usql |
| **HTTP/Net** | curl, httpie, xh, websocat, grpcurl, mkcert, ngrok |
| **Data** | jq, yq, fx, gron, miller, dasel |
| **Security** | age, sops, cosign, trivy, trufflehog, vault |
| **Files** | fd, rg, fzf, bat, eza, tree, zoxide, broot |
| **Editors** | nvim, helix, micro |
## Configuration
Global config in `~/.core/config.yaml` - see [Configuration](example.md#configuration) for examples.
## Image Storage
Images are stored in `~/.core/images/`:
```
~/.core/
├── config.yaml
└── images/
├── core-devops-darwin-arm64.qcow2
├── core-devops-linux-amd64.qcow2
└── manifest.json
```
## Multi-Repo Commands
See the [work](work/) page for detailed documentation on multi-repo commands.
### dev ci
Check GitHub Actions workflow status across all repos.
```bash
core dev ci [flags]
```
#### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--branch` | Filter by branch (default: main) |
| `--failed` | Show only failed runs |
Requires the `gh` CLI to be installed and authenticated.
### dev api
Tools for managing service APIs.
```bash
core dev api sync
```
Synchronizes the public service APIs with their internal implementations.
### dev sync
Alias for `core dev api sync`. Synchronizes the public service APIs with their internal implementations.
```bash
core dev sync
```
This command scans the `pkg` directory for services and ensures that the top-level public API for each service is in sync with its internal implementation. It automatically generates the necessary Go files with type aliases.
## See Also
- [work](work/) - Multi-repo workflow commands (`core dev work`, `core dev health`, etc.)
- [ai](../ai/) - Task management commands (`core ai tasks`, `core ai task`, etc.)

View file

@ -0,0 +1,57 @@
# core dev issues
List open issues across all repositories.
Fetches open issues from GitHub for all repos in the registry. Requires the `gh` CLI to be installed and authenticated.
## Usage
```bash
core dev issues [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--assignee` | Filter by assignee (use `@me` for yourself) |
| `--limit` | Max issues per repo (default 10) |
## Examples
```bash
# List all open issues
core dev issues
# Show issues assigned to you
core dev issues --assignee @me
# Limit to 5 issues per repo
core dev issues --limit 5
# Filter by specific assignee
core dev issues --assignee username
```
## Output
```
core-php (3 issues)
#42 Add retry logic to HTTP client bug
#38 Update documentation for v2 API docs
#35 Support custom serializers enhancement
core-tenant (1 issue)
#12 Workspace isolation bug bug, critical
```
## Requirements
- GitHub CLI (`gh`) must be installed
- Must be authenticated: `gh auth login`
## See Also
- [reviews command](../reviews/) - List PRs needing review
- [ci command](../ci/) - Check CI status

View file

@ -0,0 +1,47 @@
# core dev pull
Pull updates across all repositories.
Pulls updates for all repos. By default only pulls repos that are behind. Use `--all` to pull all repos.
## Usage
```bash
core dev pull [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--all` | Pull all repos, not just those behind |
## Examples
```bash
# Pull only repos that are behind
core dev pull
# Pull all repos
core dev pull --all
# Use specific registry
core dev pull --registry ~/projects/repos.yaml
```
## Output
```
Pulling 2 repo(s) that are behind:
✓ core-php (3 commits)
✓ core-tenant (1 commit)
Done: 2 pulled
```
## See Also
- [push command](../push/) - Push local commits
- [health command](../health/) - Check sync status
- [work command](../work/) - Full workflow

View file

@ -0,0 +1,52 @@
# core dev push
Push commits across all repositories.
Pushes unpushed commits for all repos. Shows repos with commits to push and confirms before pushing.
## Usage
```bash
core dev push [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--force` | Skip confirmation prompt |
## Examples
```bash
# Push with confirmation
core dev push
# Push without confirmation
core dev push --force
# Use specific registry
core dev push --registry ~/projects/repos.yaml
```
## Output
```
3 repo(s) with unpushed commits:
core-php: 2 commit(s)
core-admin: 1 commit(s)
core-tenant: 1 commit(s)
Push all? [y/N] y
✓ core-php
✓ core-admin
✓ core-tenant
```
## See Also
- [commit command](../commit/) - Create commits before pushing
- [pull command](../pull/) - Pull updates from remote
- [work command](../work/) - Full workflow (status + commit + push)

View file

@ -0,0 +1,61 @@
# core dev reviews
List PRs needing review across all repositories.
Fetches open PRs from GitHub for all repos in the registry. Shows review status (approved, changes requested, pending). Requires the `gh` CLI to be installed and authenticated.
## Usage
```bash
core dev reviews [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--all` | Show all PRs including drafts |
| `--author` | Filter by PR author |
## Examples
```bash
# List PRs needing review
core dev reviews
# Include draft PRs
core dev reviews --all
# Filter by author
core dev reviews --author username
```
## Output
```
core-php (2 PRs)
#45 feat: Add caching layer ✓ approved @alice
#43 fix: Memory leak in worker ⏳ pending @bob
core-admin (1 PR)
#28 refactor: Extract components ✗ changes @charlie
```
## Review Status
| Symbol | Meaning |
|--------|---------|
| `✓` | Approved |
| `⏳` | Pending review |
| `✗` | Changes requested |
## Requirements
- GitHub CLI (`gh`) must be installed
- Must be authenticated: `gh auth login`
## See Also
- [issues command](../issues/) - List open issues
- [ci command](../ci/) - Check CI status

View file

@ -0,0 +1,33 @@
# Dev Work Examples
```bash
# Full workflow: status → commit → push
core dev work
# Status only
core dev work --status
```
## Output
```
┌─────────────┬────────┬──────────┬─────────┐
│ Repo │ Branch │ Status │ Behind │
├─────────────┼────────┼──────────┼─────────┤
│ core-php │ main │ clean │ 0 │
│ core-tenant │ main │ 2 files │ 0 │
│ core-admin │ dev │ clean │ 3 │
└─────────────┴────────┴──────────┴─────────┘
```
## Registry
```yaml
repos:
- name: core
path: ./core
url: https://github.com/host-uk/core
- name: core-php
path: ./core-php
url: https://github.com/host-uk/core-php
```

293
build/cli/dev/work/index.md Normal file
View file

@ -0,0 +1,293 @@
# core dev work
Multi-repo git operations for managing the host-uk organization.
## Overview
The `core dev work` command and related subcommands help manage multiple repositories in the host-uk ecosystem simultaneously.
## Commands
| Command | Description |
|---------|-------------|
| `core dev work` | Full workflow: status + commit + push |
| `core dev work --status` | Status table only |
| `core dev work --commit` | Use Claude to commit dirty repos |
| `core dev health` | Quick health check across all repos |
| `core dev commit` | Claude-assisted commits across repos |
| `core dev push` | Push commits across all repos |
| `core dev pull` | Pull updates across all repos |
| `core dev issues` | List open issues across all repos |
| `core dev reviews` | List PRs needing review |
| `core dev ci` | Check CI status across all repos |
| `core dev impact` | Show impact of changing a repo |
## core dev work
Manage git status, commits, and pushes across multiple repositories.
```bash
core dev work [flags]
```
Reads `repos.yaml` to discover repositories and their relationships. Shows status, optionally commits with Claude, and pushes changes.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--status` | Show status only, don't push |
| `--commit` | Use Claude to commit dirty repos before pushing |
### Examples
```bash
# Full workflow
core dev work
# Status only
core dev work --status
# Commit and push
core dev work --commit
```
## core dev health
Quick health check showing summary of repository health across all repos.
```bash
core dev health [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--verbose` | Show detailed breakdown |
Output shows:
- Total repos
- Dirty repos
- Unpushed commits
- Repos behind remote
### Examples
```bash
# Quick summary
core dev health
# Detailed breakdown
core dev health --verbose
```
## core dev issues
List open issues across all repositories.
```bash
core dev issues [flags]
```
Fetches open issues from GitHub for all repos in the registry. Requires the `gh` CLI to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--assignee` | Filter by assignee (use `@me` for yourself) |
| `--limit` | Max issues per repo (default: 10) |
### Examples
```bash
# List all open issues
core dev issues
# Filter by assignee
core dev issues --assignee @me
# Limit results
core dev issues --limit 5
```
## core dev reviews
List pull requests needing review across all repos.
```bash
core dev reviews [flags]
```
Fetches open PRs from GitHub for all repos in the registry. Shows review status (approved, changes requested, pending). Requires the `gh` CLI to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--all` | Show all PRs including drafts |
| `--author` | Filter by PR author |
### Examples
```bash
# List PRs needing review
core dev reviews
# Show all PRs including drafts
core dev reviews --all
# Filter by author
core dev reviews --author username
```
## core dev commit
Create commits across repos with Claude assistance.
```bash
core dev commit [flags]
```
Uses Claude to create commits for dirty repos. Shows uncommitted changes and invokes Claude to generate commit messages.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--all` | Commit all dirty repos without prompting |
### Examples
```bash
# Commit with prompts
core dev commit
# Commit all automatically
core dev commit --all
```
## core dev push
Push commits across all repos.
```bash
core dev push [flags]
```
Pushes unpushed commits for all repos. Shows repos with commits to push and confirms before pushing.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--force` | Skip confirmation prompt |
### Examples
```bash
# Push with confirmation
core dev push
# Skip confirmation
core dev push --force
```
## core dev pull
Pull updates across all repos.
```bash
core dev pull [flags]
```
Pulls updates for all repos. By default only pulls repos that are behind. Use `--all` to pull all repos.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--all` | Pull all repos, not just those behind |
### Examples
```bash
# Pull repos that are behind
core dev pull
# Pull all repos
core dev pull --all
```
## core dev ci
Check GitHub Actions workflow status across all repos.
```bash
core dev ci [flags]
```
Fetches GitHub Actions workflow status for all repos. Shows latest run status for each repo. Requires the `gh` CLI to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--branch` | Filter by branch (default: main) |
| `--failed` | Show only failed runs |
### Examples
```bash
# Show CI status for all repos
core dev ci
# Show only failed runs
core dev ci --failed
# Check specific branch
core dev ci --branch develop
```
## core dev impact
Show the impact of changing a repository.
```bash
core dev impact <repo> [flags]
```
Analyzes the dependency graph to show which repos would be affected by changes to the specified repo.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
### Examples
```bash
# Show impact of changing core-php
core dev impact core-php
```
## Registry
These commands use `repos.yaml` to know which repos to manage. See [repos.yaml](../../../configuration.md#reposyaml) for format.
Use `core setup` to clone all repos from the registry.
## See Also
- [setup command](../../setup/) - Clone repos from registry
- [search command](../../pkg/search/) - Find and install repos

14
build/cli/docs/example.md Normal file
View file

@ -0,0 +1,14 @@
# Docs Examples
## List
```bash
core docs list
```
## Sync
```bash
core docs sync
core docs sync --output ./docs
```

110
build/cli/docs/index.md Normal file
View file

@ -0,0 +1,110 @@
# core docs
Documentation management across repositories.
## Usage
```bash
core docs <command> [flags]
```
## Commands
| Command | Description |
|---------|-------------|
| `list` | List documentation across repos |
| `sync` | Sync documentation to output directory |
## docs list
Show documentation coverage across all repos.
```bash
core docs list [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml |
### Output
```
Repo README CLAUDE CHANGELOG docs/
──────────────────────────────────────────────────────────────────────
core ✓ ✓ — 12 files
core-php ✓ ✓ ✓ 8 files
core-images ✓ — — —
Coverage: 3 with docs, 0 without
```
## docs sync
Sync documentation from all repos to an output directory.
```bash
core docs sync [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml |
| `--output` | Output directory (default: ./docs-build) |
| `--dry-run` | Show what would be synced |
### Output Structure
```
docs-build/
└── packages/
├── core/
│ ├── index.md # from README.md
│ ├── claude.md # from CLAUDE.md
│ ├── changelog.md # from CHANGELOG.md
│ ├── build.md # from docs/build.md
│ └── ...
└── core-php/
├── index.md
└── ...
```
### Example
```bash
# Preview what will be synced
core docs sync --dry-run
# Sync to default output
core docs sync
# Sync to custom directory
core docs sync --output ./site/content
```
## What Gets Synced
For each repo, the following files are collected:
| Source | Destination |
|--------|-------------|
| `README.md` | `index.md` |
| `CLAUDE.md` | `claude.md` |
| `CHANGELOG.md` | `changelog.md` |
| `docs/*.md` | `*.md` |
## Integration with core.help
The synced docs are used to build https://core.help:
1. Run `core docs sync --output ../core-php/docs/packages`
2. VitePress builds the combined documentation
3. Deploy to core.help
## See Also
- [Configuration](../../configuration.md) - Project configuration

View file

@ -0,0 +1,20 @@
# Doctor Examples
```bash
core doctor
```
## Output
```
✓ go 1.25.0
✓ git 2.43.0
✓ gh 2.40.0
✓ docker 24.0.7
✓ task 3.30.0
✓ golangci-lint 1.55.0
✗ wails (not installed)
✓ php 8.3.0
✓ composer 2.6.0
✓ node 20.10.0
```

81
build/cli/doctor/index.md Normal file
View file

@ -0,0 +1,81 @@
# core doctor
Check your development environment for required tools and configuration.
## Usage
```bash
core doctor [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--verbose` | Show detailed version information |
## What It Checks
### Required Tools
| Tool | Purpose |
|------|---------|
| `git` | Version control |
| `go` | Go compiler |
| `gh` | GitHub CLI |
### Optional Tools
| Tool | Purpose |
|------|---------|
| `node` | Node.js runtime |
| `docker` | Container runtime |
| `wails` | Desktop app framework |
| `qemu` | VM runtime for LinuxKit |
| `gpg` | Code signing |
| `codesign` | macOS signing (macOS only) |
### Configuration
- Git user name and email
- GitHub CLI authentication
- Go workspace setup
## Output
```
Core Doctor
===========
Required:
[OK] git 2.43.0
[OK] go 1.23.0
[OK] gh 2.40.0
Optional:
[OK] node 20.10.0
[OK] docker 24.0.7
[--] wails (not installed)
[OK] qemu 8.2.0
[OK] gpg 2.4.3
[OK] codesign (available)
Configuration:
[OK] git user.name: Your Name
[OK] git user.email: you@example.com
[OK] gh auth status: Logged in
All checks passed!
```
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | All required checks passed |
| 1 | One or more required checks failed |
## See Also
- [setup command](../setup/) - Clone repos from registry
- [dev](../dev/) - Development environment

View file

@ -0,0 +1,18 @@
# Go Coverage Examples
```bash
# Summary
core go cov
# HTML report
core go cov --html
# Open in browser
core go cov --open
# Fail if below threshold
core go cov --threshold 80
# Specific package
core go cov --pkg ./pkg/release
```

28
build/cli/go/cov/index.md Normal file
View file

@ -0,0 +1,28 @@
# core go cov
Generate coverage report with thresholds.
## Usage
```bash
core go cov [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--pkg` | Package to test (default: `./...`) |
| `--html` | Generate HTML coverage report |
| `--open` | Generate and open HTML report in browser |
| `--threshold` | Minimum coverage percentage (exit 1 if below) |
## Examples
```bash
core go cov # Summary
core go cov --html # HTML report
core go cov --open # Open in browser
core go cov --threshold 80 # Fail if < 80%
core go cov --pkg ./pkg/release # Specific package
```

89
build/cli/go/example.md Normal file
View file

@ -0,0 +1,89 @@
# Go Examples
## Testing
```bash
# Run all tests
core go test
# Specific package
core go test --pkg ./pkg/core
# Specific test
core go test --run TestHash
# With coverage
core go test --coverage
# Race detection
core go test --race
```
## Coverage
```bash
# Summary
core go cov
# HTML report
core go cov --html
# Open in browser
core go cov --open
# Fail if below threshold
core go cov --threshold 80
```
## Formatting
```bash
# Check
core go fmt
# Fix
core go fmt --fix
# Show diff
core go fmt --diff
```
## Linting
```bash
# Check
core go lint
# Auto-fix
core go lint --fix
```
## Installing
```bash
# Auto-detect cmd/
core go install
# Specific path
core go install ./cmd/myapp
# Pure Go (no CGO)
core go install --no-cgo
```
## Module Management
```bash
core go mod tidy
core go mod download
core go mod verify
core go mod graph
```
## Workspace
```bash
core go work sync
core go work init
core go work use ./pkg/mymodule
```

View file

@ -0,0 +1,12 @@
# Go Format Examples
```bash
# Check only
core go fmt
# Apply fixes
core go fmt --fix
# Show diff
core go fmt --diff
```

25
build/cli/go/fmt/index.md Normal file
View file

@ -0,0 +1,25 @@
# core go fmt
Format Go code using goimports or gofmt.
## Usage
```bash
core go fmt [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--fix` | Fix formatting in place |
| `--diff` | Show diff of changes |
| `--check` | Check only, exit 1 if not formatted |
## Examples
```bash
core go fmt # Check formatting
core go fmt --fix # Fix formatting
core go fmt --diff # Show diff
```

15
build/cli/go/index.md Normal file
View file

@ -0,0 +1,15 @@
# core go
Go development tools with enhanced output and environment setup.
## Subcommands
| Command | Description |
|---------|-------------|
| [test](test/) | Run tests with coverage |
| [cov](cov/) | Run tests with coverage report |
| [fmt](fmt/) | Format Go code |
| [lint](lint/) | Run golangci-lint |
| [install](install/) | Install Go binary |
| [mod](mod/) | Module management |
| [work](work/) | Workspace management |

View file

@ -0,0 +1,15 @@
# Go Install Examples
```bash
# Auto-detect cmd/
core go install
# Specific path
core go install ./cmd/myapp
# Pure Go (no CGO)
core go install --no-cgo
# Verbose
core go install -v
```

View file

@ -0,0 +1,25 @@
# core go install
Install Go binary with auto-detection.
## Usage
```bash
core go install [path] [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--no-cgo` | Disable CGO |
| `-v` | Verbose |
## Examples
```bash
core go install # Install current module
core go install ./cmd/core # Install specific path
core go install --no-cgo # Pure Go (no C dependencies)
core go install -v # Verbose output
```

View file

@ -0,0 +1,22 @@
# Go Lint Examples
```bash
# Check
core go lint
# Auto-fix
core go lint --fix
```
## Configuration
`.golangci.yml`:
```yaml
linters:
enable:
- gofmt
- govet
- errcheck
- staticcheck
```

View file

@ -0,0 +1,22 @@
# core go lint
Run golangci-lint.
## Usage
```bash
core go lint [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--fix` | Fix issues automatically |
## Examples
```bash
core go lint # Check
core go lint --fix # Auto-fix
```

View file

@ -0,0 +1,29 @@
# core go mod download
Download modules to local cache.
Wrapper around `go mod download`. Downloads all dependencies to the module cache.
## Usage
```bash
core go mod download
```
## What It Does
- Downloads all modules in go.mod to `$GOPATH/pkg/mod`
- Useful for pre-populating cache for offline builds
- Validates checksums against go.sum
## Examples
```bash
# Download all dependencies
core go mod download
```
## See Also
- [tidy](../tidy/) - Clean up go.mod
- [verify](../verify/) - Verify checksums

View file

@ -0,0 +1,15 @@
# Go Module Examples
```bash
# Tidy
core go mod tidy
# Download
core go mod download
# Verify
core go mod verify
# Graph
core go mod graph
```

View file

@ -0,0 +1,44 @@
# core go mod graph
Print module dependency graph.
Wrapper around `go mod graph`. Outputs the module dependency graph in text form.
## Usage
```bash
core go mod graph
```
## What It Does
- Prints module dependencies as pairs
- Each line shows: `module@version dependency@version`
- Useful for understanding dependency relationships
## Examples
```bash
# Print dependency graph
core go mod graph
# Find who depends on a specific module
core go mod graph | grep "some/module"
# Visualise with graphviz
core go mod graph | dot -Tpng -o deps.png
```
## Output
```
github.com/host-uk/core github.com/stretchr/testify@v1.11.1
github.com/stretchr/testify@v1.11.1 github.com/davecgh/go-spew@v1.1.2
github.com/stretchr/testify@v1.11.1 github.com/pmezard/go-difflib@v1.0.1
...
```
## See Also
- [tidy](../tidy/) - Clean up go.mod
- [dev impact](../../../dev/impact/) - Show repo dependency impact

21
build/cli/go/mod/index.md Normal file
View file

@ -0,0 +1,21 @@
# core go mod
Module management.
## Subcommands
| Command | Description |
|---------|-------------|
| `tidy` | Add missing and remove unused modules |
| `download` | Download modules to local cache |
| `verify` | Verify dependencies |
| `graph` | Print module dependency graph |
## Examples
```bash
core go mod tidy
core go mod download
core go mod verify
core go mod graph
```

View file

@ -0,0 +1,29 @@
# core go mod tidy
Add missing and remove unused modules.
Wrapper around `go mod tidy`. Ensures go.mod and go.sum are in sync with the source code.
## Usage
```bash
core go mod tidy
```
## What It Does
- Adds missing module requirements
- Removes unused module requirements
- Updates go.sum with checksums
## Examples
```bash
# Tidy the current module
core go mod tidy
```
## See Also
- [download](../download/) - Download modules
- [verify](../verify/) - Verify dependencies

View file

@ -0,0 +1,41 @@
# core go mod verify
Verify dependencies have not been modified.
Wrapper around `go mod verify`. Checks that dependencies in the module cache match their checksums in go.sum.
## Usage
```bash
core go mod verify
```
## What It Does
- Verifies each module in the cache
- Compares against go.sum checksums
- Reports any tampering or corruption
## Examples
```bash
# Verify all dependencies
core go mod verify
```
## Output
```
all modules verified
```
Or if verification fails:
```
github.com/example/pkg v1.2.3: dir has been modified
```
## See Also
- [download](../download/) - Download modules
- [tidy](../tidy/) - Clean up go.mod

View file

@ -0,0 +1,27 @@
# Go Test Examples
```bash
# All tests
core go test
# Specific package
core go test --pkg ./pkg/core
# Specific test
core go test --run TestHash
# With coverage
core go test --coverage
# Race detection
core go test --race
# Short tests only
core go test --short
# Verbose
core go test -v
# JSON output (CI)
core go test --json
```

View file

@ -0,0 +1,31 @@
# core go test
Run Go tests with coverage and filtered output.
## Usage
```bash
core go test [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--pkg` | Package to test (default: `./...`) |
| `--run` | Run only tests matching regexp |
| `--short` | Run only short tests |
| `--race` | Enable race detector |
| `--coverage` | Show detailed per-package coverage |
| `--json` | Output JSON results |
| `-v` | Verbose output |
## Examples
```bash
core go test # All tests
core go test --pkg ./pkg/core # Specific package
core go test --run TestHash # Specific test
core go test --coverage # With coverage
core go test --race # Race detection
```

View file

@ -0,0 +1,19 @@
# core go work
Go workspace management commands.
## Subcommands
| Command | Description |
|---------|-------------|
| `sync` | Sync go.work with modules |
| `init` | Initialize go.work |
| `use` | Add module to workspace |
## Examples
```bash
core go work sync # Sync workspace
core go work init # Initialize workspace
core go work use ./pkg/mymodule # Add module to workspace
```

View file

@ -0,0 +1,40 @@
# core go work init
Initialize a Go workspace.
Wrapper around `go work init`. Creates a new go.work file in the current directory.
## Usage
```bash
core go work init
```
## What It Does
- Creates a go.work file
- Automatically adds current module if go.mod exists
- Enables multi-module development
## Examples
```bash
# Initialize workspace
core go work init
# Then add more modules
core go work use ./pkg/mymodule
```
## Generated File
```go
go 1.25
use .
```
## See Also
- [use](../use/) - Add module to workspace
- [sync](../sync/) - Sync workspace

View file

@ -0,0 +1,35 @@
# core go work sync
Sync go.work with modules.
Wrapper around `go work sync`. Synchronises the workspace's build list back to the workspace modules.
## Usage
```bash
core go work sync
```
## What It Does
- Updates each module's go.mod to match the workspace build list
- Ensures all modules use compatible dependency versions
- Run after adding new modules or updating dependencies
## Examples
```bash
# Sync workspace
core go work sync
```
## When To Use
- After running `go get` to update a dependency
- After adding a new module with `core go work use`
- When modules have conflicting dependency versions
## See Also
- [init](../init/) - Initialize workspace
- [use](../use/) - Add module to workspace

View file

@ -0,0 +1,46 @@
# core go work use
Add module to workspace.
Wrapper around `go work use`. Adds one or more modules to the go.work file.
## Usage
```bash
core go work use [paths...]
```
## What It Does
- Adds specified module paths to go.work
- Auto-discovers modules if no paths given
- Enables developing multiple modules together
## Examples
```bash
# Add a specific module
core go work use ./pkg/mymodule
# Add multiple modules
core go work use ./pkg/one ./pkg/two
# Auto-discover and add all modules
core go work use
```
## Auto-Discovery
When called without arguments, scans for go.mod files and adds all found modules:
```bash
core go work use
# Added ./pkg/build
# Added ./pkg/repos
# Added ./cmd/core
```
## See Also
- [init](../init/) - Initialize workspace
- [sync](../sync/) - Sync workspace

31
build/cli/index.md Normal file
View file

@ -0,0 +1,31 @@
# Core CLI
Unified interface for Go/PHP development, multi-repo management, and deployment.
## Commands
| Command | Description |
|---------|-------------|
| [ai](ai/) | AI agent task management and Claude integration |
| [go](go/) | Go development tools |
| [php](php/) | Laravel/PHP development tools |
| [build](build/) | Build projects |
| [ci](ci/) | Publish releases |
| [sdk](sdk/) | SDK validation and compatibility |
| [dev](dev/) | Multi-repo workflow + dev environment |
| [pkg](pkg/) | Package management |
| [vm](vm/) | LinuxKit VM management |
| [docs](docs/) | Documentation management |
| [setup](setup/) | Clone repos from registry |
| [doctor](doctor/) | Check environment |
| [test](test/) | Run Go tests with coverage |
## Installation
```bash
go install github.com/host-uk/core/cmd/core@latest
```
Verify: `core doctor`
See [Getting Started](/build/go/getting-started) for all installation options.

111
build/cli/php/example.md Normal file
View file

@ -0,0 +1,111 @@
# PHP Examples
## Development
```bash
# Start all services
core php dev
# With HTTPS
core php dev --https
# Skip services
core php dev --no-vite --no-horizon
```
## Testing
```bash
# Run all
core php test
# Parallel
core php test --parallel
# With coverage
core php test --coverage
# Filter
core php test --filter UserTest
```
## Code Quality
```bash
# Format
core php fmt --fix
# Static analysis
core php analyse --level 9
```
## Deployment
```bash
# Production
core php deploy
# Staging
core php deploy --staging
# Wait for completion
core php deploy --wait
# Check status
core php deploy:status
# Rollback
core php deploy:rollback
```
## Configuration
### .env
```env
COOLIFY_URL=https://coolify.example.com
COOLIFY_TOKEN=your-api-token
COOLIFY_APP_ID=production-app-id
COOLIFY_STAGING_APP_ID=staging-app-id
```
### .core/php.yaml
```yaml
version: 1
dev:
domain: myapp.test
ssl: true
services:
- frankenphp
- vite
- horizon
- reverb
- redis
deploy:
coolify:
server: https://coolify.example.com
project: my-project
```
## Package Linking
```bash
# Link local packages
core php packages link ../my-package
# Update linked
core php packages update
# Unlink
core php packages unlink my-package
```
## SSL Setup
```bash
core php ssl
core php ssl --domain myapp.test
```

413
build/cli/php/index.md Normal file
View file

@ -0,0 +1,413 @@
# core php
Laravel/PHP development tools with FrankenPHP.
## Commands
### Development
| Command | Description |
|---------|-------------|
| [`dev`](#php-dev) | Start development environment |
| [`logs`](#php-logs) | View service logs |
| [`stop`](#php-stop) | Stop all services |
| [`status`](#php-status) | Show service status |
| [`ssl`](#php-ssl) | Setup SSL certificates with mkcert |
### Build & Production
| Command | Description |
|---------|-------------|
| [`build`](#php-build) | Build Docker or LinuxKit image |
| [`serve`](#php-serve) | Run production container |
| [`shell`](#php-shell) | Open shell in running container |
### Code Quality
| Command | Description |
|---------|-------------|
| [`test`](#php-test) | Run PHP tests (PHPUnit/Pest) |
| [`fmt`](#php-fmt) | Format code with Laravel Pint |
| [`analyse`](#php-analyse) | Run PHPStan static analysis |
### Package Management
| Command | Description |
|---------|-------------|
| [`packages link`](#php-packages-link) | Link local packages by path |
| [`packages unlink`](#php-packages-unlink) | Unlink packages by name |
| [`packages update`](#php-packages-update) | Update linked packages |
| [`packages list`](#php-packages-list) | List linked packages |
### Deployment (Coolify)
| Command | Description |
|---------|-------------|
| [`deploy`](#php-deploy) | Deploy to Coolify |
| [`deploy:status`](#php-deploystatus) | Show deployment status |
| [`deploy:rollback`](#php-deployrollback) | Rollback to previous deployment |
| [`deploy:list`](#php-deploylist) | List recent deployments |
---
## php dev
Start the Laravel development environment with all detected services.
```bash
core php dev [flags]
```
### Services Orchestrated
- **FrankenPHP/Octane** - HTTP server (port 8000, HTTPS on 443)
- **Vite** - Frontend dev server (port 5173)
- **Laravel Horizon** - Queue workers
- **Laravel Reverb** - WebSocket server (port 8080)
- **Redis** - Cache and queue backend (port 6379)
### Flags
| Flag | Description |
|------|-------------|
| `--no-vite` | Skip Vite dev server |
| `--no-horizon` | Skip Laravel Horizon |
| `--no-reverb` | Skip Laravel Reverb |
| `--no-redis` | Skip Redis server |
| `--https` | Enable HTTPS with mkcert |
| `--domain` | Domain for SSL certificate (default: from APP_URL) |
| `--port` | FrankenPHP port (default: 8000) |
### Examples
```bash
# Start all detected services
core php dev
# With HTTPS
core php dev --https
# Skip optional services
core php dev --no-horizon --no-reverb
```
---
## php logs
Stream unified logs from all running services.
```bash
core php logs [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--follow` | Follow log output |
| `--service` | Specific service (frankenphp, vite, horizon, reverb, redis) |
---
## php stop
Stop all running Laravel services.
```bash
core php stop
```
---
## php status
Show the status of all Laravel services and project configuration.
```bash
core php status
```
---
## php ssl
Setup local SSL certificates using mkcert.
```bash
core php ssl [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--domain` | Domain for certificate (default: from APP_URL or localhost) |
---
## php build
Build a production-ready container image.
```bash
core php build [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--type` | Build type: `docker` (default) or `linuxkit` |
| `--name` | Image name (default: project directory name) |
| `--tag` | Image tag (default: latest) |
| `--platform` | Target platform (e.g., linux/amd64, linux/arm64) |
| `--dockerfile` | Path to custom Dockerfile |
| `--output` | Output path for LinuxKit image |
| `--format` | LinuxKit format: qcow2 (default), iso, raw, vmdk |
| `--template` | LinuxKit template name (default: server-php) |
| `--no-cache` | Build without cache |
### Examples
```bash
# Build Docker image
core php build
# With custom name and tag
core php build --name myapp --tag v1.0
# Build LinuxKit image
core php build --type linuxkit
```
---
## php serve
Run a production container.
```bash
core php serve [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--name` | Docker image name (required) |
| `--tag` | Image tag (default: latest) |
| `--container` | Container name |
| `--port` | HTTP port (default: 80) |
| `--https-port` | HTTPS port (default: 443) |
| `-d` | Run in detached mode |
| `--env-file` | Path to environment file |
### Examples
```bash
core php serve --name myapp
core php serve --name myapp -d
core php serve --name myapp --port 8080
```
---
## php shell
Open an interactive shell in a running container.
```bash
core php shell <container-id>
```
---
## php test
Run PHP tests using PHPUnit or Pest.
```bash
core php test [flags]
```
Auto-detects Pest if `tests/Pest.php` exists.
### Flags
| Flag | Description |
|------|-------------|
| `--parallel` | Run tests in parallel |
| `--coverage` | Generate code coverage |
| `--filter` | Filter tests by name pattern |
| `--group` | Run only tests in specified group |
### Examples
```bash
core php test
core php test --parallel --coverage
core php test --filter UserTest
```
---
## php fmt
Format PHP code using Laravel Pint.
```bash
core php fmt [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--fix` | Auto-fix formatting issues |
| `--diff` | Show diff of changes |
---
## php analyse
Run PHPStan or Larastan static analysis.
```bash
core php analyse [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--level` | PHPStan analysis level (0-9) |
| `--memory` | Memory limit (e.g., 2G) |
---
## php packages link
Link local PHP packages for development.
```bash
core php packages link <path> [<path>...]
```
Adds path repositories to composer.json with symlink enabled.
---
## php packages unlink
Remove linked packages from composer.json.
```bash
core php packages unlink <name> [<name>...]
```
---
## php packages update
Update linked packages via Composer.
```bash
core php packages update [<name>...]
```
---
## php packages list
List all locally linked packages.
```bash
core php packages list
```
---
## php deploy
Deploy the PHP application to Coolify.
```bash
core php deploy [flags]
```
### Configuration
Requires environment variables in `.env`:
```
COOLIFY_URL=https://coolify.example.com
COOLIFY_TOKEN=your-api-token
COOLIFY_APP_ID=production-app-id
COOLIFY_STAGING_APP_ID=staging-app-id
```
### Flags
| Flag | Description |
|------|-------------|
| `--staging` | Deploy to staging environment |
| `--force` | Force deployment even if no changes detected |
| `--wait` | Wait for deployment to complete |
---
## php deploy:status
Show the status of a deployment.
```bash
core php deploy:status [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--staging` | Check staging environment |
| `--id` | Specific deployment ID |
---
## php deploy:rollback
Rollback to a previous deployment.
```bash
core php deploy:rollback [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--staging` | Rollback staging environment |
| `--id` | Specific deployment ID to rollback to |
| `--wait` | Wait for rollback to complete |
---
## php deploy:list
List recent deployments.
```bash
core php deploy:list [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--staging` | List staging deployments |
| `--limit` | Number of deployments (default: 10) |
---
## Configuration
Optional `.core/php.yaml` - see [Configuration](example.md#configuration) for examples.

36
build/cli/pkg/example.md Normal file
View file

@ -0,0 +1,36 @@
# Package Examples
## Search
```bash
core pkg search core-
core pkg search api
core pkg search --org myorg
```
## Install
```bash
core pkg install core-api
core pkg install host-uk/core-api
```
## List
```bash
core pkg list
core pkg list --format json
```
## Update
```bash
core pkg update
core pkg update core-api
```
## Outdated
```bash
core pkg outdated
```

144
build/cli/pkg/index.md Normal file
View file

@ -0,0 +1,144 @@
# core pkg
Package management for host-uk repositories.
## Usage
```bash
core pkg <command> [flags]
```
## Commands
| Command | Description |
|---------|-------------|
| [`search`](#pkg-search) | Search GitHub for packages |
| [`install`](#pkg-install) | Clone a package from GitHub |
| [`list`](#pkg-list) | List installed packages |
| [`update`](#pkg-update) | Update installed packages |
| [`outdated`](#pkg-outdated) | Check for outdated packages |
---
## pkg search
Search GitHub for host-uk packages.
```bash
core pkg search [flags]
```
Results are cached for 1 hour in `.core/cache/`.
### Flags
| Flag | Description |
|------|-------------|
| `--org` | GitHub organisation (default: host-uk) |
| `--pattern` | Repo name pattern (* for wildcard) |
| `--type` | Filter by type in name (mod, services, plug, website) |
| `--limit` | Max results (default: 50) |
| `--refresh` | Bypass cache and fetch fresh data |
### Examples
```bash
# List all repos in org
core pkg search
# Search for core-* repos
core pkg search --pattern 'core-*'
# Search different org
core pkg search --org mycompany
# Bypass cache
core pkg search --refresh
```
---
## pkg install
Clone a package from GitHub.
```bash
core pkg install <org/repo> [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--dir` | Target directory (default: ./packages or current dir) |
| `--add` | Add to repos.yaml registry |
### Examples
```bash
# Clone to packages/
core pkg install host-uk/core-php
# Clone to custom directory
core pkg install host-uk/core-tenant --dir ./packages
# Clone and add to registry
core pkg install host-uk/core-admin --add
```
---
## pkg list
List installed packages from repos.yaml.
```bash
core pkg list
```
Shows installed status (✓) and description for each package.
---
## pkg update
Pull latest changes for installed packages.
```bash
core pkg update [<name>...] [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--all` | Update all packages |
### Examples
```bash
# Update specific package
core pkg update core-php
# Update all packages
core pkg update --all
```
---
## pkg outdated
Check which packages have unpulled commits.
```bash
core pkg outdated
```
Fetches from remote and shows packages that are behind.
---
## See Also
- [setup](../setup/) - Clone all repos from registry
- [dev work](../dev/work/) - Multi-repo workflow

View file

@ -0,0 +1,23 @@
# Package Search Examples
```bash
# Find all core-* packages
core pkg search core-
# Search term
core pkg search api
# Different org
core pkg search --org myorg query
```
## Output
```
┌──────────────┬─────────────────────────────┐
│ Package │ Description │
├──────────────┼─────────────────────────────┤
│ core-api │ REST API framework │
│ core-auth │ Authentication utilities │
└──────────────┴─────────────────────────────┘
```

View file

@ -0,0 +1,75 @@
# core pkg search
Search GitHub for repositories matching a pattern.
Uses `gh` CLI for authenticated search. Results are cached for 1 hour.
## Usage
```bash
core pkg search [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--pattern` | Repo name pattern (* for wildcard) |
| `--org` | GitHub organization (default: host-uk) |
| `--type` | Filter by type in name (mod, services, plug, website) |
| `--limit` | Max results (default: 50) |
| `--refresh` | Bypass cache and fetch fresh data |
## Examples
```bash
# List all host-uk repos
core pkg search
# Search for core-* repos
core pkg search --pattern "core-*"
# Search different org
core pkg search --org mycompany
# Filter by type
core pkg search --type services
# Bypass cache
core pkg search --refresh
# Combine filters
core pkg search --pattern "core-*" --type mod --limit 20
```
## Output
```
Found 5 repositories:
host-uk/core
Go CLI for the host-uk ecosystem
★ 42 Go Updated 2 hours ago
host-uk/core-php
PHP/Laravel packages for Core
★ 18 PHP Updated 1 day ago
host-uk/core-images
Docker and LinuxKit images
★ 8 Dockerfile Updated 3 days ago
```
## Authentication
Uses GitHub CLI (`gh`) authentication. Ensure you're logged in:
```bash
gh auth status
gh auth login # if not authenticated
```
## See Also
- [pkg install](../) - Clone a package from GitHub
- [setup command](../../setup/) - Clone all repos from registry

35
build/cli/sdk/example.md Normal file
View file

@ -0,0 +1,35 @@
# SDK Examples
## Validate
```bash
core sdk validate
core sdk validate --spec ./api.yaml
```
## Diff
```bash
# Compare with tag
core sdk diff --base v1.0.0
# Compare files
core sdk diff --base ./old-api.yaml --spec ./new-api.yaml
```
## Output
```
Breaking changes detected:
- DELETE /users/{id}/profile
Endpoint removed
- PATCH /users/{id}
Required field 'email' added
Non-breaking changes:
+ POST /users/{id}/avatar
New endpoint added
```

106
build/cli/sdk/index.md Normal file
View file

@ -0,0 +1,106 @@
# core sdk
SDK validation and API compatibility tools.
To generate SDKs, use: `core build sdk`
## Usage
```bash
core sdk <command> [flags]
```
## Commands
| Command | Description |
|---------|-------------|
| `diff` | Check for breaking API changes |
| `validate` | Validate OpenAPI spec |
## sdk validate
Validate an OpenAPI specification file.
```bash
core sdk validate [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--spec` | Path to OpenAPI spec file (auto-detected) |
### Examples
```bash
# Validate detected spec
core sdk validate
# Validate specific file
core sdk validate --spec api/openapi.yaml
```
## sdk diff
Check for breaking changes between API versions.
```bash
core sdk diff [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--base` | Base spec version (git tag or file path) |
| `--spec` | Current spec file (auto-detected) |
### Examples
```bash
# Compare against previous release
core sdk diff --base v1.0.0
# Compare two files
core sdk diff --base old-api.yaml --spec new-api.yaml
```
### Breaking Changes Detected
- Removed endpoints
- Changed parameter types
- Removed required fields
- Changed response types
## SDK Generation
SDK generation is handled by `core build sdk`, not this command.
```bash
# Generate SDKs
core build sdk
# Generate specific language
core build sdk --lang typescript
# Preview without writing
core build sdk --dry-run
```
See [build sdk](../build/sdk/) for generation details.
## Spec Auto-Detection
Core looks for OpenAPI specs in this order:
1. Path specified in config (`sdk.spec`)
2. `openapi.yaml` / `openapi.json`
3. `api/openapi.yaml` / `api/openapi.json`
4. `docs/openapi.yaml` / `docs/openapi.json`
5. Laravel Scramble endpoint (`/docs/api.json`)
## See Also
- [build sdk](../build/sdk/) - Generate SDKs from OpenAPI
- [ci command](../ci/) - Release workflow

293
build/cli/setup/example.md Normal file
View file

@ -0,0 +1,293 @@
# Setup Examples
## Clone from Registry
```bash
# Clone all repos defined in repos.yaml
core setup
# Preview what would be cloned
core setup --dry-run
# Only foundation packages
core setup --only foundation
# Multiple types
core setup --only foundation,module
# Use specific registry file
core setup --registry ~/projects/repos.yaml
```
## Bootstrap New Workspace
```bash
# In an empty directory - bootstraps in place
mkdir my-workspace && cd my-workspace
core setup
# Shows interactive wizard to select packages:
# ┌─────────────────────────────────────────────┐
# │ Select packages to clone │
# │ Use space to select, enter to confirm │
# │ │
# │ ── Foundation (core framework) ── │
# │ ☑ core-php Foundation framework │
# │ ☑ core-tenant Multi-tenancy module │
# │ │
# │ ── Products (applications) ── │
# │ ☐ core-bio Link-in-bio product │
# │ ☐ core-social Social scheduling │
# └─────────────────────────────────────────────┘
# Non-interactive: clone all packages
core setup --all
# Create workspace in subdirectory
cd ~/Code
core setup --name my-project
# CI mode: fully non-interactive
core setup --all --name ci-test
```
## Setup Single Repository
```bash
# In a git repo without .core/ configuration
cd ~/Code/my-go-project
core setup
# Shows choice dialog:
# ┌─────────────────────────────────────────────┐
# │ Setup options │
# │ You're in a git repository. What would you │
# │ like to do? │
# │ │
# │ ● Setup this repo (create .core/ config) │
# │ ○ Create a new workspace (clone repos) │
# └─────────────────────────────────────────────┘
# Preview generated configuration
core setup --dry-run
# Output:
# → Setting up repository configuration
#
# ✓ Detected project type: go
# → Also found: (none)
#
# → Would create:
# /Users/you/Code/my-go-project/.core/build.yaml
#
# Configuration preview:
# version: 1
# project:
# name: my-go-project
# description: Go application
# main: ./cmd/my-go-project
# binary: my-go-project
# ...
```
## Configuration Files
### repos.yaml (Workspace Registry)
```yaml
org: host-uk
base_path: .
defaults:
ci: github
license: EUPL-1.2
branch: main
repos:
core-php:
type: foundation
description: Foundation framework
core-tenant:
type: module
depends_on: [core-php]
description: Multi-tenancy module
core-admin:
type: module
depends_on: [core-php, core-tenant]
description: Admin panel
core-bio:
type: product
depends_on: [core-php, core-tenant]
description: Link-in-bio product
domain: bio.host.uk.com
core-devops:
type: foundation
clone: false # Already exists, skip cloning
```
### .core/build.yaml (Repository Config)
Generated for Go projects:
```yaml
version: 1
project:
name: my-project
description: Go application
main: ./cmd/my-project
binary: my-project
build:
cgo: false
flags:
- -trimpath
ldflags:
- -s
- -w
env: []
targets:
- os: linux
arch: amd64
- os: linux
arch: arm64
- os: darwin
arch: amd64
- os: darwin
arch: arm64
- os: windows
arch: amd64
sign:
enabled: false
```
Generated for Wails projects:
```yaml
version: 1
project:
name: my-app
description: Wails desktop application
main: .
binary: my-app
targets:
- os: darwin
arch: amd64
- os: darwin
arch: arm64
- os: windows
arch: amd64
- os: linux
arch: amd64
```
### .core/release.yaml (Release Config)
Generated for Go projects:
```yaml
version: 1
project:
name: my-project
repository: owner/my-project
changelog:
include:
- feat
- fix
- perf
- refactor
exclude:
- chore
- docs
- style
- test
publishers:
- type: github
draft: false
prerelease: false
```
### .core/test.yaml (Test Config)
Generated for Go projects:
```yaml
version: 1
commands:
- name: unit
run: go test ./...
- name: coverage
run: go test -coverprofile=coverage.out ./...
- name: race
run: go test -race ./...
env:
CGO_ENABLED: "0"
```
Generated for PHP projects:
```yaml
version: 1
commands:
- name: unit
run: vendor/bin/pest --parallel
- name: types
run: vendor/bin/phpstan analyse
- name: lint
run: vendor/bin/pint --test
env:
APP_ENV: testing
DB_CONNECTION: sqlite
```
Generated for Node.js projects:
```yaml
version: 1
commands:
- name: unit
run: npm test
- name: lint
run: npm run lint
- name: typecheck
run: npm run typecheck
env:
NODE_ENV: test
```
## Workflow Examples
### New Developer Setup
```bash
# Clone the workspace
mkdir host-uk && cd host-uk
core setup
# Select packages in wizard, then:
core health # Check all repos are healthy
core doctor # Verify environment
```
### CI Pipeline Setup
```bash
# Non-interactive full clone
core setup --all --name workspace
# Or with specific packages
core setup --only foundation,module --name workspace
```
### Adding Build Config to Existing Repo
```bash
cd my-existing-project
core setup # Choose "Setup this repo"
# Edit .core/build.yaml as needed
core build # Build the project
```

213
build/cli/setup/index.md Normal file
View file

@ -0,0 +1,213 @@
# core setup
Clone repositories from registry or bootstrap a new workspace.
## Overview
The `setup` command operates in three modes:
1. **Registry mode** - When `repos.yaml` exists nearby, clones repositories into packages/
2. **Bootstrap mode** - When no registry exists, clones `core-devops` first, then presents an interactive wizard to select packages
3. **Repo setup mode** - When run in a git repo root, offers to create `.core/build.yaml` configuration
## Usage
```bash
core setup [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--dry-run` | Show what would be cloned without cloning |
| `--only` | Only clone repos of these types (comma-separated: foundation,module,product) |
| `--all` | Skip wizard, clone all packages (non-interactive) |
| `--name` | Project directory name for bootstrap mode |
| `--build` | Run build after cloning |
---
## Registry Mode
When `repos.yaml` is found nearby (current directory or parents), setup clones all defined repositories:
```bash
# In a directory with repos.yaml
core setup
# Preview what would be cloned
core setup --dry-run
# Only clone foundation packages
core setup --only foundation
# Multiple types
core setup --only foundation,module
```
In registry mode with a TTY, an interactive wizard allows you to select which packages to clone. Use `--all` to skip the wizard and clone everything.
---
## Bootstrap Mode
When no `repos.yaml` exists, setup enters bootstrap mode:
```bash
# In an empty directory - bootstraps workspace in place
mkdir my-project && cd my-project
core setup
# In a non-empty directory - creates subdirectory
cd ~/Code
core setup --name my-workspace
# Non-interactive: clone all packages
core setup --all --name ci-test
```
Bootstrap mode:
1. Detects if current directory is empty
2. If not empty, prompts for project name (or uses `--name`)
3. Clones `core-devops` (contains `repos.yaml`)
4. Loads the registry from core-devops
5. Shows interactive package selection wizard (unless `--all`)
6. Clones selected packages
7. Optionally runs build (with `--build`)
---
## Repo Setup Mode
When run in a git repository root (without `repos.yaml`), setup offers two choices:
1. **Setup Working Directory** - Creates `.core/build.yaml` based on detected project type
2. **Create Package** - Creates a subdirectory and clones packages there
```bash
cd ~/Code/my-go-project
core setup
# Output:
# >> This directory is a git repository
# > Setup Working Directory
# Create Package (clone repos into subdirectory)
```
Choosing "Setup Working Directory" detects the project type and generates configuration:
| Detected File | Project Type |
|---------------|--------------|
| `wails.json` | Wails |
| `go.mod` | Go |
| `composer.json` | PHP |
| `package.json` | Node.js |
Creates three config files in `.core/`:
| File | Purpose |
|------|---------|
| `build.yaml` | Build targets, flags, output settings |
| `release.yaml` | Changelog format, GitHub release config |
| `test.yaml` | Test commands, environment variables |
Also auto-detects GitHub repo from git remote for release config.
See [Configuration Files](example.md#configuration-files) for generated config examples.
---
## Interactive Wizard
When running in a terminal (TTY), the setup command presents an interactive multi-select wizard:
- Packages are grouped by type (foundation, module, product, template)
- Use arrow keys to navigate
- Press space to select/deselect packages
- Type to filter the list
- Press enter to confirm selection
The wizard is skipped when:
- `--all` flag is specified
- Not running in a TTY (e.g., CI pipelines)
- `--dry-run` is specified
---
## Examples
### Clone from Registry
```bash
# Clone all repos (interactive wizard)
core setup
# Clone all repos (non-interactive)
core setup --all
# Preview without cloning
core setup --dry-run
# Only foundation packages
core setup --only foundation
```
### Bootstrap New Workspace
```bash
# Interactive bootstrap in empty directory
mkdir workspace && cd workspace
core setup
# Non-interactive with all packages
core setup --all --name my-project
# Bootstrap and run build
core setup --all --name my-project --build
```
---
## Registry Format
The registry file (`repos.yaml`) defines repositories. See [Configuration Files](example.md#configuration-files) for format.
---
## Finding Registry
Core looks for `repos.yaml` in:
1. Current directory
2. Parent directories (walking up to root)
3. `~/Code/host-uk/repos.yaml`
4. `~/.config/core/repos.yaml`
---
## After Setup
```bash
# Check workspace health
core dev health
# Full workflow (status + commit + push)
core dev work
# Build the project
core build
# Run tests
core go test # Go projects
core php test # PHP projects
```
---
## See Also
- [dev work](../dev/work/) - Multi-repo operations
- [build](../build/) - Build projects
- [doctor](../doctor/) - Check environment

View file

@ -0,0 +1,8 @@
# Test Examples
**Note:** Prefer `core go test` or `core php test` instead.
```bash
core test
core test --coverage
```

74
build/cli/test/index.md Normal file
View file

@ -0,0 +1,74 @@
# core test
Run Go tests with coverage reporting.
Sets `MACOSX_DEPLOYMENT_TARGET=26.0` to suppress linker warnings on macOS.
## Usage
```bash
core test [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--coverage` | Show detailed per-package coverage |
| `--json` | Output JSON for CI/agents |
| `--pkg` | Package pattern to test (default: ./...) |
| `--race` | Enable race detector |
| `--run` | Run only tests matching this regex |
| `--short` | Skip long-running tests |
| `--verbose` | Show test output as it runs |
## Examples
```bash
# Run all tests with coverage summary
core test
# Show test output as it runs
core test --verbose
# Detailed per-package coverage
core test --coverage
# Test specific packages
core test --pkg ./pkg/...
# Run specific test by name
core test --run TestName
# Run tests matching pattern
core test --run "Test.*Good"
# Skip long-running tests
core test --short
# Enable race detector
core test --race
# Output JSON for CI/agents
core test --json
```
## JSON Output
With `--json`, outputs structured results:
```json
{
"passed": 14,
"failed": 0,
"skipped": 0,
"coverage": 75.1,
"exit_code": 0,
"failed_packages": []
}
```
## See Also
- [go test](../go/test/) - Go-specific test options
- [go cov](../go/cov/) - Coverage reports

52
build/cli/vm/example.md Normal file
View file

@ -0,0 +1,52 @@
# VM Examples
## Running VMs
```bash
# Run image
core vm run server.iso
# Detached with resources
core vm run -d --memory 4096 --cpus 4 server.iso
# From template
core vm run --template core-dev --var SSH_KEY="ssh-rsa AAAA..."
```
## Management
```bash
# List running
core vm ps
# Include stopped
core vm ps -a
# Stop
core vm stop abc123
# View logs
core vm logs abc123
# Follow logs
core vm logs -f abc123
# Execute command
core vm exec abc123 ls -la
# Shell
core vm exec abc123 /bin/sh
```
## Templates
```bash
# List
core vm templates
# Show content
core vm templates show core-dev
# Show variables
core vm templates vars core-dev
```

163
build/cli/vm/index.md Normal file
View file

@ -0,0 +1,163 @@
# core vm
LinuxKit VM management.
LinuxKit VMs are lightweight, immutable VMs built from YAML templates.
They run using qemu or hyperkit depending on your system.
## Usage
```bash
core vm <command> [flags]
```
## Commands
| Command | Description |
|---------|-------------|
| [`run`](#vm-run) | Run a LinuxKit image or template |
| [`ps`](#vm-ps) | List running VMs |
| [`stop`](#vm-stop) | Stop a VM |
| [`logs`](#vm-logs) | View VM logs |
| [`exec`](#vm-exec) | Execute command in VM |
| [templates](templates/) | Manage LinuxKit templates |
---
## vm run
Run a LinuxKit image or build from a template.
```bash
core vm run <image> [flags]
core vm run --template <name> [flags]
```
Supported image formats: `.iso`, `.qcow2`, `.vmdk`, `.raw`
### Flags
| Flag | Description |
|------|-------------|
| `--template` | Run from a LinuxKit template (build + run) |
| `--var` | Template variable in KEY=VALUE format (repeatable) |
| `--name` | Name for the container |
| `--memory` | Memory in MB (default: 1024) |
| `--cpus` | CPU count (default: 1) |
| `--ssh-port` | SSH port for exec commands (default: 2222) |
| `-d` | Run in detached mode (background) |
### Examples
```bash
# Run from image file
core vm run image.iso
# Run detached with more resources
core vm run -d image.qcow2 --memory 2048 --cpus 4
# Run from template
core vm run --template core-dev --var SSH_KEY="ssh-rsa AAAA..."
# Multiple template variables
core vm run --template server-php --var SSH_KEY="..." --var DOMAIN=example.com
```
---
## vm ps
List running VMs.
```bash
core vm ps [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `-a` | Show all (including stopped) |
### Output
```
ID NAME IMAGE STATUS STARTED PID
abc12345 myvm ...core-dev.qcow2 running 5m 12345
```
---
## vm stop
Stop a running VM by ID or name.
```bash
core vm stop <id>
```
Supports partial ID matching.
### Examples
```bash
# Full ID
core vm stop abc12345678
# Partial ID
core vm stop abc1
```
---
## vm logs
View VM logs.
```bash
core vm logs <id> [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `-f` | Follow log output |
### Examples
```bash
# View logs
core vm logs abc12345
# Follow logs
core vm logs -f abc1
```
---
## vm exec
Execute a command in a running VM via SSH.
```bash
core vm exec <id> <command...>
```
### Examples
```bash
# List files
core vm exec abc12345 ls -la
# Open shell
core vm exec abc1 /bin/sh
```
---
## See Also
- [templates](templates/) - Manage LinuxKit templates
- [build](../build/) - Build LinuxKit images
- [dev](../dev/) - Dev environment management

View file

@ -0,0 +1,53 @@
# VM Templates Examples
## List
```bash
core vm templates
```
## Show
```bash
core vm templates show core-dev
```
## Variables
```bash
core vm templates vars core-dev
```
## Output
```
Variables for core-dev:
SSH_KEY (required) SSH public key
MEMORY (optional) Memory in MB (default: 4096)
CPUS (optional) CPU count (default: 4)
```
## Using Templates
```bash
core vm run --template core-dev --var SSH_KEY="ssh-rsa AAAA..."
```
## Template Format
`.core/linuxkit/myserver.yml`:
```yaml
kernel:
image: linuxkit/kernel:5.15
cmdline: "console=tty0"
init:
- linuxkit/init:v1.0.0
services:
- name: sshd
image: linuxkit/sshd:v1.0.0
- name: myapp
image: ghcr.io/myorg/myapp:latest
```

View file

@ -0,0 +1,124 @@
# core vm templates
Manage LinuxKit templates for container images.
## Usage
```bash
core vm templates [command]
```
## Commands
| Command | Description |
|---------|-------------|
| `list` | List available templates |
| `show` | Show template details |
| `vars` | Show template variables |
## templates list
List all available LinuxKit templates.
```bash
core vm templates list
```
### Output
```
Available Templates:
core-dev
Full development environment with 100+ tools
Platforms: linux/amd64, linux/arm64
server-php
FrankenPHP production server
Platforms: linux/amd64, linux/arm64
edge-node
Minimal edge deployment
Platforms: linux/amd64, linux/arm64
```
## templates show
Show details of a specific template.
```bash
core vm templates show <name>
```
### Example
```bash
core vm templates show core-dev
```
Output:
```
Template: core-dev
Description: Full development environment with 100+ tools
Platforms:
- linux/amd64
- linux/arm64
Formats:
- iso
- qcow2
Services:
- sshd
- docker
- frankenphp
Size: ~1.8GB
```
## templates vars
Show variables defined by a template.
```bash
core vm templates vars <name>
```
### Example
```bash
core vm templates vars core-dev
```
Output:
```
Variables for core-dev:
SSH_KEY (required) SSH public key
MEMORY (optional) Memory in MB (default: 4096)
CPUS (optional) CPU count (default: 4)
```
## Template Locations
Templates are searched in order:
1. `.core/linuxkit/` - Project-specific
2. `~/.core/templates/` - User templates
3. Built-in templates
## Creating Templates
Create a LinuxKit YAML in `.core/linuxkit/`. See [Template Format](example.md#template-format) for examples.
Run with:
```bash
core vm run --template myserver
```
## See Also
- [vm command](../) - Run LinuxKit images
- [build command](../../build/) - Build LinuxKit images

357
build/go/configuration.md Normal file
View file

@ -0,0 +1,357 @@
# Configuration
Core uses `.core/` directory for project configuration.
## Directory Structure
```
.core/
├── release.yaml # Release configuration
├── build.yaml # Build configuration (optional)
├── php.yaml # PHP configuration (optional)
└── linuxkit/ # LinuxKit templates
├── server.yml
└── dev.yml
```
## release.yaml
Full release configuration reference:
```yaml
version: 1
project:
name: myapp
repository: myorg/myapp
build:
targets:
- os: linux
arch: amd64
- os: linux
arch: arm64
- os: darwin
arch: amd64
- os: darwin
arch: arm64
- os: windows
arch: amd64
publishers:
# GitHub Releases (required - others reference these artifacts)
- type: github
prerelease: false
draft: false
# npm binary wrapper
- type: npm
package: "@myorg/myapp"
access: public # or "restricted"
# Homebrew formula
- type: homebrew
tap: myorg/homebrew-tap
formula: myapp
official:
enabled: false
output: dist/homebrew
# Scoop manifest (Windows)
- type: scoop
bucket: myorg/scoop-bucket
official:
enabled: false
output: dist/scoop
# AUR (Arch Linux)
- type: aur
maintainer: "Name <email>"
# Chocolatey (Windows)
- type: chocolatey
push: false # true to publish
# Docker multi-arch
- type: docker
registry: ghcr.io
image: myorg/myapp
dockerfile: Dockerfile
platforms:
- linux/amd64
- linux/arm64
tags:
- latest
- "{{.Version}}"
build_args:
VERSION: "{{.Version}}"
# LinuxKit images
- type: linuxkit
config: .core/linuxkit/server.yml
formats:
- iso
- qcow2
- docker
platforms:
- linux/amd64
- linux/arm64
changelog:
include:
- feat
- fix
- perf
- refactor
exclude:
- chore
- docs
- style
- test
- ci
```
## build.yaml
Optional build configuration:
```yaml
version: 1
project:
name: myapp
binary: myapp
build:
main: ./cmd/myapp
env:
CGO_ENABLED: "0"
flags:
- -trimpath
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
targets:
- os: linux
arch: amd64
- os: darwin
arch: arm64
```
## php.yaml
PHP/Laravel configuration:
```yaml
version: 1
dev:
domain: myapp.test
ssl: true
port: 8000
services:
- frankenphp
- vite
- horizon
- reverb
- redis
test:
parallel: true
coverage: false
deploy:
coolify:
server: https://coolify.example.com
project: my-project
environment: production
```
## LinuxKit Templates
LinuxKit YAML configuration:
```yaml
kernel:
image: linuxkit/kernel:6.6
cmdline: "console=tty0 console=ttyS0"
init:
- linuxkit/init:latest
- linuxkit/runc:latest
- linuxkit/containerd:latest
- linuxkit/ca-certificates:latest
onboot:
- name: sysctl
image: linuxkit/sysctl:latest
services:
- name: dhcpcd
image: linuxkit/dhcpcd:latest
- name: sshd
image: linuxkit/sshd:latest
- name: myapp
image: myorg/myapp:latest
capabilities:
- CAP_NET_BIND_SERVICE
files:
- path: /etc/myapp/config.yaml
contents: |
server:
port: 8080
```
## repos.yaml
Package registry for multi-repo workspaces:
```yaml
# Organisation name (used for GitHub URLs)
org: host-uk
# Base path for cloning (default: current directory)
base_path: .
# Default settings for all repos
defaults:
ci: github
license: EUPL-1.2
branch: main
# Repository definitions
repos:
# Foundation packages (no dependencies)
core-php:
type: foundation
description: Foundation framework
core-devops:
type: foundation
description: Development environment
clone: false # Skip during setup (already exists)
# Module packages (depend on foundation)
core-tenant:
type: module
depends_on: [core-php]
description: Multi-tenancy module
core-admin:
type: module
depends_on: [core-php, core-tenant]
description: Admin panel
core-api:
type: module
depends_on: [core-php]
description: REST API framework
# Product packages (user-facing applications)
core-bio:
type: product
depends_on: [core-php, core-tenant]
description: Link-in-bio product
domain: bio.host.uk.com
core-social:
type: product
depends_on: [core-php, core-tenant]
description: Social scheduling
domain: social.host.uk.com
# Templates
core-template:
type: template
description: Starter template for new projects
```
### repos.yaml Fields
| Field | Required | Description |
|-------|----------|-------------|
| `org` | Yes | GitHub organisation name |
| `base_path` | No | Directory for cloning (default: `.`) |
| `defaults` | No | Default settings applied to all repos |
| `repos` | Yes | Map of repository definitions |
### Repository Fields
| Field | Required | Description |
|-------|----------|-------------|
| `type` | Yes | `foundation`, `module`, `product`, or `template` |
| `description` | No | Human-readable description |
| `depends_on` | No | List of package dependencies |
| `clone` | No | Set `false` to skip during setup |
| `domain` | No | Production domain (for products) |
| `branch` | No | Override default branch |
### Package Types
| Type | Description | Dependencies |
|------|-------------|--------------|
| `foundation` | Core framework packages | None |
| `module` | Reusable modules | Foundation packages |
| `product` | User-facing applications | Foundation + modules |
| `template` | Starter templates | Any |
---
## Environment Variables
Complete reference of environment variables used by Core CLI.
### Authentication
| Variable | Used By | Description |
|----------|---------|-------------|
| `GITHUB_TOKEN` | `core ci`, `core dev` | GitHub API authentication |
| `ANTHROPIC_API_KEY` | `core ai`, `core dev claude` | Claude API key |
| `AGENTIC_TOKEN` | `core ai task*` | Agentic API authentication |
| `AGENTIC_BASE_URL` | `core ai task*` | Agentic API endpoint |
### Publishing
| Variable | Used By | Description |
|----------|---------|-------------|
| `NPM_TOKEN` | `core ci` (npm publisher) | npm registry auth token |
| `CHOCOLATEY_API_KEY` | `core ci` (chocolatey publisher) | Chocolatey API key |
| `DOCKER_USERNAME` | `core ci` (docker publisher) | Docker registry username |
| `DOCKER_PASSWORD` | `core ci` (docker publisher) | Docker registry password |
### Deployment
| Variable | Used By | Description |
|----------|---------|-------------|
| `COOLIFY_URL` | `core php deploy` | Coolify server URL |
| `COOLIFY_TOKEN` | `core php deploy` | Coolify API token |
| `COOLIFY_APP_ID` | `core php deploy` | Production application ID |
| `COOLIFY_STAGING_APP_ID` | `core php deploy --staging` | Staging application ID |
### Build
| Variable | Used By | Description |
|----------|---------|-------------|
| `CGO_ENABLED` | `core build`, `core go *` | Enable/disable CGO (default: 0) |
| `GOOS` | `core build` | Target operating system |
| `GOARCH` | `core build` | Target architecture |
### Configuration Paths
| Variable | Description |
|----------|-------------|
| `CORE_CONFIG` | Override config directory (default: `~/.core/`) |
| `CORE_REGISTRY` | Override repos.yaml path |
---
## Defaults
If no configuration exists, sensible defaults are used:
- **Targets**: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
- **Publishers**: GitHub only
- **Changelog**: feat, fix, perf, refactor included

191
build/go/getting-started.md Normal file
View file

@ -0,0 +1,191 @@
# Getting Started
This guide walks you through installing Core and running your first build.
## Prerequisites
Before installing Core, ensure you have:
| Tool | Minimum Version | Check Command |
|------|-----------------|---------------|
| Go | 1.23+ | `go version` |
| Git | 2.30+ | `git --version` |
Optional (for specific features):
| Tool | Required For | Install |
|------|--------------|---------|
| `gh` | GitHub integration (`core dev issues`, `core dev reviews`) | [cli.github.com](https://cli.github.com) |
| Docker | Container builds | [docker.com](https://docker.com) |
| `task` | Task automation | `go install github.com/go-task/task/v3/cmd/task@latest` |
## Installation
### Option 1: Go Install (Recommended)
```bash
# Install latest release
go install github.com/host-uk/core/cmd/core@latest
# Verify installation
core doctor
```
If `core: command not found`, add Go's bin directory to your PATH:
```bash
export PATH="$PATH:$(go env GOPATH)/bin"
```
### Option 2: Download Binary
Download pre-built binaries from [GitHub Releases](https://github.com/host-uk/core/releases):
```bash
# macOS (Apple Silicon)
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-darwin-arm64
chmod +x core
sudo mv core /usr/local/bin/
# macOS (Intel)
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-darwin-amd64
chmod +x core
sudo mv core /usr/local/bin/
# Linux (x86_64)
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-linux-amd64
chmod +x core
sudo mv core /usr/local/bin/
```
### Option 3: Build from Source
```bash
# Clone repository
git clone https://github.com/host-uk/core.git
cd core
# Build with Task (recommended)
task cli:build
# Binary at ./bin/core
# Or build with Go directly
CGO_ENABLED=0 go build -o core ./cmd/core/
sudo mv core /usr/local/bin/
```
## Your First Build
### 1. Navigate to a Go Project
```bash
cd ~/Code/my-go-project
```
### 2. Initialise Configuration
```bash
core setup
```
This detects your project type and creates configuration files in `.core/`:
- `build.yaml` - Build settings
- `release.yaml` - Release configuration
- `test.yaml` - Test commands
### 3. Build
```bash
core build
```
Output appears in `dist/`:
```
dist/
├── my-project-darwin-arm64.tar.gz
├── my-project-linux-amd64.tar.gz
└── CHECKSUMS.txt
```
### 4. Cross-Compile (Optional)
```bash
core build --targets linux/amd64,linux/arm64,darwin/arm64,windows/amd64
```
## Your First Release
Releases are **safe by default** - Core runs in dry-run mode unless you explicitly confirm.
### 1. Preview
```bash
core ci
```
This shows what would be published without actually publishing.
### 2. Publish
```bash
core ci --we-are-go-for-launch
```
This creates a GitHub release with your built artifacts.
## Multi-Repo Workflow
If you work with multiple repositories (like the host-uk ecosystem):
### 1. Clone All Repositories
```bash
mkdir host-uk && cd host-uk
core setup
```
Select packages in the interactive wizard.
### 2. Check Status
```bash
core dev health
# Output: "18 repos │ clean │ synced"
```
### 3. Work Across Repos
```bash
core dev work --status # See status table
core dev work # Commit and push all dirty repos
```
## Next Steps
| Task | Command | Documentation |
|------|---------|---------------|
| Run tests | `core go test` | [go/test](cmd/go/test/) |
| Format code | `core go fmt --fix` | [go/fmt](cmd/go/fmt/) |
| Lint code | `core go lint` | [go/lint](cmd/go/lint/) |
| PHP development | `core php dev` | [php](cmd/php/) |
| View all commands | `core --help` | [cmd](cmd/) |
## Getting Help
```bash
# Check environment
core doctor
# Command help
core <command> --help
# Full documentation
https://github.com/host-uk/core/tree/main/docs
```
## See Also
- [Configuration](configuration.md) - All config options
- [Workflows](workflows.md) - Common task sequences
- [Troubleshooting](troubleshooting.md) - When things go wrong

112
build/go/glossary.md Normal file
View file

@ -0,0 +1,112 @@
# Glossary
Definitions of terms used throughout Core CLI documentation.
## A
### Artifact
A file produced by a build, typically a binary, archive, or checksum file. Artifacts are stored in the `dist/` directory and published during releases.
## C
### CGO
Go's mechanism for calling C code. Core disables CGO by default (`CGO_ENABLED=0`) to produce statically-linked binaries that don't depend on system libraries.
### Changelog
Automatically generated list of changes between releases, created from conventional commit messages. Configure in `.core/release.yaml`.
### Conventional Commits
A commit message format: `type(scope): description`. Types include `feat`, `fix`, `docs`, `chore`. Core uses this to generate changelogs.
## D
### Dry-run
A mode where commands show what they would do without actually doing it. `core ci` runs in dry-run mode by default for safety.
## F
### Foundation Package
A core package with no dependencies on other packages. Examples: `core-php`, `core-devops`. These form the base of the dependency tree.
### FrankenPHP
A modern PHP application server used by `core php dev`. Combines PHP with Caddy for high-performance serving.
## G
### `gh`
The GitHub CLI tool. Required for commands that interact with GitHub: `core dev issues`, `core dev reviews`, `core dev ci`.
## L
### LinuxKit
A toolkit for building lightweight, immutable Linux distributions. Core can build LinuxKit images via `core build --type linuxkit`.
## M
### Module (Go)
A collection of Go packages with a `go.mod` file. Core's Go commands operate on modules.
### Module (Package)
A host-uk package that depends on foundation packages. Examples: `core-tenant`, `core-admin`. Compare with **Foundation Package** and **Product**.
## P
### Package
An individual repository in the host-uk ecosystem. Packages are defined in `repos.yaml` and managed with `core pkg` commands.
### Package Index
The `repos.yaml` file that lists all packages in a workspace. Contains metadata like dependencies, type, and description.
### Product
A user-facing application package. Examples: `core-bio`, `core-social`. Products depend on foundation and module packages.
### Publisher
A release target configured in `.core/release.yaml`. Types include `github`, `docker`, `npm`, `homebrew`, `linuxkit`.
## R
### Registry (Docker/npm)
A remote repository for container images or npm packages. Core can publish to registries during releases.
### `repos.yaml`
The package index file defining all repositories in a workspace. Used by multi-repo commands like `core dev work`.
## S
### SDK
Software Development Kit. Core can generate API client SDKs from OpenAPI specs via `core build sdk`.
## T
### Target
A build target specified as `os/arch`, e.g., `linux/amd64`, `darwin/arm64`. Use `--targets` flag to specify.
## W
### Wails
A framework for building desktop applications with Go backends and web frontends. Core detects Wails projects and uses appropriate build commands.
### Workspace (Go)
A Go 1.18+ feature for working with multiple modules simultaneously. Managed via `core go work` commands.
### Workspace (Multi-repo)
A directory containing multiple packages from `repos.yaml`. Created via `core setup` and managed with `core dev` commands.
## Symbols
### `.core/`
Directory containing project configuration files:
- `build.yaml` - Build settings
- `release.yaml` - Release targets
- `test.yaml` - Test configuration
- `linuxkit/` - LinuxKit templates
### `--we-are-go-for-launch`
Flag to disable dry-run mode and actually publish a release. Named as a deliberate friction to prevent accidental releases.
---
## See Also
- [Configuration](configuration.md) - Config file reference
- [Getting Started](getting-started.md) - First-time setup

98
build/go/index.md Normal file
View file

@ -0,0 +1,98 @@
# Core Go
Core is a Go framework for the host-uk ecosystem - build, release, and deploy Go, Wails, PHP, and container workloads.
## Installation
```bash
# Via Go (recommended)
go install github.com/host-uk/core/cmd/core@latest
# Or download binary from releases
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-$(go env GOOS)-$(go env GOARCH)
chmod +x core && sudo mv core /usr/local/bin/
# Verify
core doctor
```
See [Getting Started](getting-started.md) for all installation options including building from source.
## Command Reference
See [CLI](/build/cli/) for full command documentation.
| Command | Description |
|---------|-------------|
| [go](/build/cli/go/) | Go development (test, fmt, lint, cov) |
| [php](/build/cli/php/) | Laravel/PHP development |
| [build](/build/cli/build/) | Build Go, Wails, Docker, LinuxKit projects |
| [ci](/build/cli/ci/) | Publish releases (dry-run by default) |
| [sdk](/build/cli/sdk/) | SDK generation and validation |
| [dev](/build/cli/dev/) | Multi-repo workflow + dev environment |
| [pkg](/build/cli/pkg/) | Package search and install |
| [vm](/build/cli/vm/) | LinuxKit VM management |
| [docs](/build/cli/docs/) | Documentation management |
| [setup](/build/cli/setup/) | Clone repos from registry |
| [doctor](/build/cli/doctor/) | Check development environment |
## Quick Start
```bash
# Go development
core go test # Run tests
core go test --coverage # With coverage
core go fmt # Format code
core go lint # Lint code
# Build
core build # Auto-detect and build
core build --targets linux/amd64,darwin/arm64
# Release (dry-run by default)
core ci # Preview release
core ci --we-are-go-for-launch # Actually publish
# Multi-repo workflow
core dev work # Status + commit + push
core dev work --status # Just show status
# PHP development
core php dev # Start dev environment
core php test # Run tests
```
## Configuration
Core uses `.core/` directory for project configuration:
```
.core/
├── release.yaml # Release targets and settings
├── build.yaml # Build configuration (optional)
└── linuxkit/ # LinuxKit templates
```
And `repos.yaml` in workspace root for multi-repo management.
## Guides
- [Getting Started](getting-started.md) - Installation and first steps
- [Workflows](workflows.md) - Common task sequences
- [Troubleshooting](troubleshooting.md) - When things go wrong
- [Migration](migration.md) - Moving from legacy tools
## Reference
- [Configuration](configuration.md) - All config options
- [Glossary](glossary.md) - Term definitions
## Claude Code Skill
Install the skill to teach Claude Code how to use the Core CLI:
```bash
curl -fsSL https://raw.githubusercontent.com/host-uk/core/main/.claude/skills/core/install.sh | bash
```
See [skill/](skill/) for details.

233
build/go/migration.md Normal file
View file

@ -0,0 +1,233 @@
# Migration Guide
Migrating from legacy scripts and tools to Core CLI.
## From push-all.sh
The `push-all.sh` script has been replaced by `core dev` commands.
| Legacy | Core CLI | Notes |
|--------|----------|-------|
| `./push-all.sh --status` | `core dev work --status` | Status table |
| `./push-all.sh --commit` | `core dev commit` | Commit dirty repos |
| `./push-all.sh` | `core dev work` | Full workflow |
### Quick Migration
```bash
# Instead of
./push-all.sh --status
# Use
core dev work --status
```
### New Features
Core CLI adds features not available in the legacy script:
```bash
# Quick health summary
core dev health
# Output: "18 repos │ clean │ synced"
# Pull repos that are behind
core dev pull
# GitHub integration
core dev issues # List open issues
core dev reviews # List PRs needing review
core dev ci # Check CI status
# Dependency analysis
core dev impact core-php # What depends on core-php?
```
---
## From Raw Go Commands
Core wraps Go commands with enhanced defaults and output.
| Raw Command | Core CLI | Benefits |
|-------------|----------|----------|
| `go test ./...` | `core go test` | Filters warnings, sets CGO_ENABLED=0 |
| `go test -coverprofile=...` | `core go cov` | HTML reports, thresholds |
| `gofmt -w .` | `core go fmt --fix` | Uses goimports if available |
| `golangci-lint run` | `core go lint` | Consistent interface |
| `go build` | `core build` | Cross-compile, sign, archive |
### Why Use Core?
```bash
# Raw go test shows linker warnings on macOS
go test ./...
# ld: warning: -no_pie is deprecated...
# Core filters noise
core go test
# PASS (clean output)
```
### Environment Setup
Core automatically sets:
- `CGO_ENABLED=0` - Static binaries
- `MACOSX_DEPLOYMENT_TARGET=26.0` - Suppress macOS warnings
- Colour output for coverage reports
---
## From Raw PHP Commands
Core orchestrates Laravel development services.
| Raw Command | Core CLI | Benefits |
|-------------|----------|----------|
| `php artisan serve` | `core php dev` | Adds Vite, Horizon, Reverb, Redis |
| `./vendor/bin/pest` | `core php test` | Auto-detects test runner |
| `./vendor/bin/pint` | `core php fmt --fix` | Consistent interface |
| Manual Coolify deploy | `core php deploy` | Tracked, scriptable |
### Development Server Comparison
```bash
# Raw: Start each service manually
php artisan serve &
npm run dev &
php artisan horizon &
php artisan reverb:start &
# Core: One command
core php dev
# Starts all services, shows unified logs
```
---
## From goreleaser
Core's release system is simpler than goreleaser for host-uk projects.
| goreleaser | Core CLI |
|------------|----------|
| `.goreleaser.yaml` | `.core/release.yaml` |
| `goreleaser release --snapshot` | `core ci` (dry-run) |
| `goreleaser release` | `core ci --we-are-go-for-launch` |
### Configuration Migration
**goreleaser:**
```yaml
builds:
- main: ./cmd/app
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
archives:
- format: tar.gz
files: [LICENSE, README.md]
release:
github:
owner: host-uk
name: app
```
**Core:**
```yaml
version: 1
project:
name: app
repository: host-uk/app
targets:
- os: linux
arch: amd64
- os: darwin
arch: arm64
publishers:
- type: github
```
### Key Differences
1. **Separate build and release** - Core separates `core build` from `core ci`
2. **Safe by default** - `core ci` is dry-run unless `--we-are-go-for-launch`
3. **Simpler config** - Fewer options, sensible defaults
---
## From Manual Git Operations
Core automates multi-repo git workflows.
| Manual | Core CLI |
|--------|----------|
| `cd repo1 && git status && cd ../repo2 && ...` | `core dev work --status` |
| Check each repo for uncommitted changes | `core dev health` |
| Commit each repo individually | `core dev commit` |
| Push each repo individually | `core dev push` |
### Example: Committing Across Repos
**Manual:**
```bash
cd core-php
git add -A
git commit -m "feat: add feature"
cd ../core-tenant
git add -A
git commit -m "feat: use new feature"
# ... repeat for each repo
```
**Core:**
```bash
core dev commit
# Interactive: reviews changes, suggests messages
# Adds Co-Authored-By automatically
```
---
## Deprecated Commands
These commands have been removed or renamed:
| Deprecated | Replacement | Version |
|------------|-------------|---------|
| `core sdk generate` | `core build sdk` | v0.5.0 |
| `core dev task*` | `core ai task*` | v0.8.0 |
| `core release` | `core ci` | v0.6.0 |
---
## Version Compatibility
| Core Version | Go Version | Breaking Changes |
|--------------|------------|------------------|
| v1.0.0+ | 1.23+ | Stable API |
| v0.8.0 | 1.22+ | Task commands moved to `ai` |
| v0.6.0 | 1.22+ | Release command renamed to `ci` |
| v0.5.0 | 1.21+ | SDK generation moved to `build sdk` |
---
## Getting Help
If you encounter issues during migration:
1. Check [Troubleshooting](troubleshooting.md)
2. Run `core doctor` to verify setup
3. Use `--help` on any command: `core dev work --help`
---
## See Also
- [Getting Started](getting-started.md) - Fresh installation
- [Workflows](workflows.md) - Common task sequences
- [Configuration](configuration.md) - Config file reference

35
build/go/skill/index.md Normal file
View file

@ -0,0 +1,35 @@
# Claude Code Skill
The `core` skill teaches Claude Code how to use the Core CLI effectively.
## Installation
```bash
curl -fsSL https://raw.githubusercontent.com/host-uk/core/main/.claude/skills/core/install.sh | bash
```
Or if you have the repo cloned:
```bash
./.claude/skills/core/install.sh
```
## What it does
Once installed, Claude Code will:
- Auto-invoke when working in host-uk repositories
- Use `core` commands instead of raw `go`/`php`/`git` commands
- Follow the correct patterns for testing, building, and releasing
## Manual invocation
Type `/core` in Claude Code to invoke the skill manually.
## Updating
Re-run the install command to update to the latest version.
## Location
Skills are installed to `~/.claude/skills/core/SKILL.md`.

332
build/go/troubleshooting.md Normal file
View file

@ -0,0 +1,332 @@
# Troubleshooting
Common issues and how to resolve them.
## Installation Issues
### "command not found: core"
**Cause:** Go's bin directory is not in your PATH.
**Fix:**
```bash
# Add to ~/.bashrc or ~/.zshrc
export PATH="$PATH:$(go env GOPATH)/bin"
# Then reload
source ~/.bashrc # or ~/.zshrc
```
### "go: module github.com/host-uk/core: no matching versions"
**Cause:** Go module proxy hasn't cached the latest version yet.
**Fix:**
```bash
# Bypass proxy
GOPROXY=direct go install github.com/host-uk/core/cmd/core@latest
```
---
## Build Issues
### "no Go files in..."
**Cause:** Core couldn't find a main package to build.
**Fix:**
1. Check you're in the correct directory
2. Ensure `.core/build.yaml` has the correct `main` path:
```yaml
project:
main: ./cmd/myapp # Path to main package
```
### "CGO_ENABLED=1 but no C compiler"
**Cause:** Build requires CGO but no C compiler is available.
**Fix:**
```bash
# Option 1: Disable CGO (if not needed)
core build # Core disables CGO by default
# Option 2: Install a C compiler
# macOS
xcode-select --install
# Ubuntu/Debian
sudo apt install build-essential
# Windows
# Install MinGW or use WSL
```
### Build succeeds but binary doesn't run
**Cause:** Built for wrong architecture.
**Fix:**
```bash
# Check what you built
file dist/myapp-*
# Build for your current platform
core build --targets $(go env GOOS)/$(go env GOARCH)
```
---
## Release Issues
### "dry-run mode, use --we-are-go-for-launch to publish"
**This is expected behaviour.** Core runs in dry-run mode by default for safety.
**To actually publish:**
```bash
core ci --we-are-go-for-launch
```
### "failed to create release: 401 Unauthorized"
**Cause:** GitHub token missing or invalid.
**Fix:**
```bash
# Authenticate with GitHub CLI
gh auth login
# Or set token directly
export GITHUB_TOKEN=ghp_xxxxxxxxxxxx
```
### "no artifacts found in dist/"
**Cause:** You need to build before releasing.
**Fix:**
```bash
# Build first
core build
# Then release
core ci --we-are-go-for-launch
```
### "tag already exists"
**Cause:** Trying to release a version that's already been released.
**Fix:**
1. Update version in your code/config
2. Or delete the existing tag (if intentional):
```bash
git tag -d v1.0.0
git push origin :refs/tags/v1.0.0
```
---
## Multi-Repo Issues
### "repos.yaml not found"
**Cause:** Core can't find the package registry.
**Fix:**
Core looks for `repos.yaml` in:
1. Current directory
2. Parent directories (walking up to root)
3. `~/Code/host-uk/repos.yaml`
4. `~/.config/core/repos.yaml`
Either:
- Run commands from a directory with `repos.yaml`
- Use `--registry /path/to/repos.yaml`
- Run `core setup` to bootstrap a new workspace
### "failed to clone: Permission denied (publickey)"
**Cause:** SSH key not configured for GitHub.
**Fix:**
```bash
# Check SSH connection
ssh -T git@github.com
# If that fails, add your key
ssh-add ~/.ssh/id_ed25519
# Or configure SSH
# See: https://docs.github.com/en/authentication/connecting-to-github-with-ssh
```
### "repository not found" during setup
**Cause:** You don't have access to the repository, or it doesn't exist.
**Fix:**
1. Check you're authenticated: `gh auth status`
2. Verify the repo exists and you have access
3. For private repos, ensure your token has `repo` scope
---
## GitHub Integration Issues
### "gh: command not found"
**Cause:** GitHub CLI not installed.
**Fix:**
```bash
# macOS
brew install gh
# Ubuntu/Debian
sudo apt install gh
# Windows
winget install GitHub.cli
# Then authenticate
gh auth login
```
### "core dev issues" shows nothing
**Possible causes:**
1. No open issues exist
2. Not authenticated with GitHub
3. Not in a directory with `repos.yaml`
**Fix:**
```bash
# Check auth
gh auth status
# Check you're in a workspace
ls repos.yaml
# Show all issues including closed
core dev issues --all
```
---
## PHP Issues
### "frankenphp: command not found"
**Cause:** FrankenPHP not installed.
**Fix:**
```bash
# macOS
brew install frankenphp
# Or use Docker
core php dev --docker
```
### "core php dev" exits immediately
**Cause:** Usually a port conflict or missing dependency.
**Fix:**
```bash
# Check if port 8000 is in use
lsof -i :8000
# Try a different port
core php dev --port 9000
# Check logs for errors
core php logs
```
---
## Performance Issues
### Commands are slow
**Possible causes:**
1. Large number of repositories
2. Network latency to GitHub
3. Go module downloads
**Fix:**
```bash
# For multi-repo commands, use health for quick check
core dev health # Fast summary
# Instead of
core dev work --status # Full table (slower)
# Pre-download Go modules
go mod download
```
---
## Getting More Help
### Enable Verbose Output
Most commands support `-v` or `--verbose`:
```bash
core build -v
core go test -v
```
### Check Environment
```bash
core doctor
```
This verifies all required tools are installed and configured.
### Report Issues
If you've found a bug:
1. Check existing issues: https://github.com/host-uk/core/issues
2. Create a new issue with:
- Core version (`core --version`)
- OS and architecture (`go env GOOS GOARCH`)
- Command that failed
- Full error output
---
## See Also
- [Getting Started](getting-started.md) - Installation and first steps
- [Configuration](configuration.md) - Config file reference
- [doctor](cmd/doctor/) - Environment verification

334
build/go/workflows.md Normal file
View file

@ -0,0 +1,334 @@
# Workflows
Common end-to-end workflows for Core CLI.
## Go Project: Build and Release
Complete workflow from code to GitHub release.
```bash
# 1. Run tests
core go test
# 2. Check coverage
core go cov --threshold 80
# 3. Format and lint
core go fmt --fix
core go lint
# 4. Build for all platforms
core build --targets linux/amd64,linux/arm64,darwin/arm64,windows/amd64
# 5. Preview release (dry-run)
core ci
# 6. Publish
core ci --we-are-go-for-launch
```
**Output structure:**
```
dist/
├── myapp-darwin-arm64.tar.gz
├── myapp-linux-amd64.tar.gz
├── myapp-linux-arm64.tar.gz
├── myapp-windows-amd64.zip
└── CHECKSUMS.txt
```
---
## PHP Project: Development to Deployment
Local development through to production deployment.
```bash
# 1. Start development environment
core php dev
# 2. Run tests (in another terminal)
core php test --parallel
# 3. Check code quality
core php fmt --fix
core php analyse
# 4. Deploy to staging
core php deploy --staging --wait
# 5. Verify staging
# (manual testing)
# 6. Deploy to production
core php deploy --wait
# 7. Monitor
core php deploy:status
```
**Rollback if needed:**
```bash
core php deploy:rollback
```
---
## Multi-Repo: Daily Workflow
Working across the host-uk monorepo.
### Morning: Sync Everything
```bash
# Quick health check
core dev health
# Pull all repos that are behind
core dev pull --all
# Check for issues assigned to you
core dev issues --assignee @me
```
### During Development
```bash
# Work on code...
# Check status across all repos
core dev work --status
# Commit changes (Claude-assisted messages)
core dev commit
# Push when ready
core dev push
```
### End of Day
```bash
# Full workflow: status → commit → push
core dev work
# Check CI status
core dev ci
# Review any failed builds
core dev ci --failed
```
---
## New Developer: Environment Setup
First-time setup for a new team member.
```bash
# 1. Verify prerequisites
core doctor
# 2. Create workspace directory
mkdir ~/Code/host-uk && cd ~/Code/host-uk
# 3. Bootstrap workspace (interactive)
core setup
# 4. Select packages in wizard
# Use arrow keys, space to select, enter to confirm
# 5. Verify setup
core dev health
# 6. Start working
core dev work --status
```
---
## CI Pipeline: Automated Build
Example GitHub Actions workflow.
```yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Install Core
run: go install github.com/host-uk/core/cmd/core@latest
- name: Build
run: core build --ci
- name: Release
run: core ci --we-are-go-for-launch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
---
## SDK Generation: API Client Updates
Generate SDK clients when API changes.
```bash
# 1. Validate OpenAPI spec
core sdk validate
# 2. Check for breaking changes
core sdk diff --base v1.0.0
# 3. Generate SDKs
core build sdk
# 4. Review generated code
git diff
# 5. Commit if satisfied
git add -A && git commit -m "chore: regenerate SDK clients"
```
---
## Dependency Update: Cross-Repo Change
When updating a shared package like `core-php`.
```bash
# 1. Make changes in core-php
cd ~/Code/host-uk/core-php
# ... edit code ...
# 2. Run tests
core go test # or core php test
# 3. Check what depends on core-php
core dev impact core-php
# Output:
# core-tenant (direct)
# core-admin (via core-tenant)
# core-api (direct)
# ...
# 4. Commit core-php changes
core dev commit
# 5. Update dependent packages
cd ~/Code/host-uk
for pkg in core-tenant core-admin core-api; do
cd $pkg
composer update host-uk/core-php
core php test
cd ..
done
# 6. Commit all updates
core dev work
```
---
## Hotfix: Emergency Production Fix
Fast path for critical fixes.
```bash
# 1. Create hotfix branch
git checkout -b hotfix/critical-bug main
# 2. Make fix
# ... edit code ...
# 3. Test
core go test --run TestCriticalPath
# 4. Build
core build
# 5. Preview release
core ci --prerelease
# 6. Publish hotfix
core ci --we-are-go-for-launch --prerelease
# 7. Merge back to main
git checkout main
git merge hotfix/critical-bug
git push
```
---
## Documentation: Sync Across Repos
Keep documentation synchronised.
```bash
# 1. List all docs
core docs list
# 2. Sync to central location
core docs sync --output ./docs-site
# 3. Review changes
git diff docs-site/
# 4. Commit
git add docs-site/
git commit -m "docs: sync from packages"
```
---
## Troubleshooting: Failed Build
When a build fails.
```bash
# 1. Check environment
core doctor
# 2. Clean previous artifacts
rm -rf dist/
# 3. Verbose build
core build -v
# 4. If Go-specific issues
core go mod tidy
core go mod verify
# 5. Check for test failures
core go test -v
# 6. Review configuration
cat .core/build.yaml
```
---
## See Also
- [Getting Started](getting-started.md) - First-time setup
- [Troubleshooting](troubleshooting.md) - When things go wrong
- [Configuration](configuration.md) - Config file reference

BIN
build/php/.DS_Store vendored Normal file

Binary file not shown.

181
build/php/actions.md Normal file
View file

@ -0,0 +1,181 @@
# Actions Pattern
Actions are single-purpose, reusable classes that encapsulate business logic. They provide a clean, testable alternative to fat controllers and model methods.
## Basic Action
```php
<?php
namespace Mod\Blog\Actions;
use Core\Actions\Action;
use Mod\Blog\Models\Post;
class CreatePost
{
use Action;
public function handle(array $data): Post
{
$post = Post::create($data);
event(new PostCreated($post));
return $post;
}
}
// Usage
$post = CreatePost::run(['title' => 'My Post', 'content' => '...']);
```
## With Validation
```php
use Illuminate\Support\Facades\Validator;
class CreatePost
{
use Action;
public function handle(array $data): Post
{
$validated = Validator::make($data, [
'title' => 'required|max:255',
'content' => 'required',
'status' => 'required|in:draft,published',
])->validate();
return Post::create($validated);
}
}
```
## With Authorization
```php
class DeletePost
{
use Action;
public function handle(Post $post, User $user): bool
{
if (!$user->can('delete', $post)) {
throw new UnauthorizedException('Cannot delete this post');
}
$post->delete();
return true;
}
}
// Usage
DeletePost::run($post, auth()->user());
```
## With Events
```php
class PublishPost
{
use Action;
public function handle(Post $post): Post
{
$post->update([
'status' => 'published',
'published_at' => now(),
]);
event(new PostPublished($post));
return $post;
}
}
```
## As Job
```php
class CreatePost
{
use Action;
public function asJob(): bool
{
return true; // Run as queued job
}
public function handle(array $data): Post
{
// Heavy processing...
return Post::create($data);
}
}
// Automatically queued
CreatePost::run($data);
```
## Best Practices
### 1. Single Responsibility
```php
// ✅ Good - one action, one purpose
CreatePost::run($data);
UpdatePost::run($post, $data);
DeletePost::run($post);
// ❌ Bad - multiple responsibilities
ManagePost::run($action, $post, $data);
```
### 2. Type Hints
```php
// ✅ Good - clear types
public function handle(Post $post, User $user): bool
// ❌ Bad - no types
public function handle($post, $user)
```
### 3. Descriptive Names
```php
// ✅ Good
PublishScheduledPosts
SendWeeklyNewsletter
GenerateMonthlyReport
// ❌ Bad
ProcessPosts
DoWork
HandleIt
```
## Testing
```php
use Tests\TestCase;
use Mod\Blog\Actions\CreatePost;
class CreatePostTest extends TestCase
{
public function test_creates_post(): void
{
$post = CreatePost::run([
'title' => 'Test Post',
'content' => 'Content',
]);
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
]);
}
}
```
## Learn More
- [Lifecycle Events →](/core/events)
- [Module System →](/core/modules)

531
build/php/activity.md Normal file
View file

@ -0,0 +1,531 @@
# Activity Logging
Track user actions, model changes, and system events with GDPR-compliant activity logging.
## Basic Usage
### Enabling Activity Logging
Add the `LogsActivity` trait to your model:
```php
<?php
namespace Mod\Blog\Models;
use Illuminate\Database\Eloquent\Model;
use Core\Activity\Concerns\LogsActivity;
class Post extends Model
{
use LogsActivity;
protected $fillable = ['title', 'content', 'status'];
}
```
**Automatic Logging:**
- Created events
- Updated events (with changed attributes)
- Deleted events
- Restored events (soft deletes)
### Manual Logging
```php
use Core\Activity\Services\ActivityLogService;
$logger = app(ActivityLogService::class);
// Log custom activity
$logger->log(
subject: $post,
event: 'published',
description: 'Post published to homepage',
causer: auth()->user()
);
// Log with properties
$logger->log(
subject: $post,
event: 'viewed',
properties: [
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]
);
```
## Activity Model
### Retrieving Activity
```php
use Core\Activity\Models\Activity;
// Get all activity
$activities = Activity::latest()->get();
// Get activity for specific model
$postActivity = Activity::forSubject($post)->get();
// Get activity by user
$userActivity = Activity::causedBy($user)->get();
// Get activity by event
$published = Activity::where('event', 'published')->get();
```
### Activity Attributes
```php
$activity = Activity::latest()->first();
$activity->subject; // The model that was acted upon
$activity->causer; // The user who caused the activity
$activity->event; // Event name (created, updated, deleted, etc.)
$activity->description; // Human-readable description
$activity->properties; // Additional data (array)
$activity->created_at; // When it occurred
```
### Relationships
```php
// Subject (polymorphic)
$post = $activity->subject;
// Causer (polymorphic)
$user = $activity->causer;
// Workspace (if applicable)
$workspace = $activity->workspace;
```
## Activity Scopes
### Filtering Activity
```php
use Core\Activity\Models\Activity;
// By date range
$activities = Activity::query()
->whereBetween('created_at', [now()->subDays(7), now()])
->get();
// By event type
$activities = Activity::query()
->whereIn('event', ['created', 'updated'])
->get();
// By workspace
$activities = Activity::query()
->where('workspace_id', $workspace->id)
->get();
// Complex filters
$activities = Activity::query()
->forSubject($post)
->causedBy($user)
->where('event', 'updated')
->latest()
->paginate(20);
```
### Custom Scopes
```php
use Core\Activity\Scopes\ActivityScopes;
// Add to Activity model
class Activity extends Model
{
use ActivityScopes;
public function scopeForWorkspace($query, $workspaceId)
{
return $query->where('workspace_id', $workspaceId);
}
public function scopeWithinDays($query, $days)
{
return $query->where('created_at', '>=', now()->subDays($days));
}
}
// Usage
$recent = Activity::withinDays(7)
->forWorkspace($workspace->id)
->get();
```
## Customizing Logged Data
### Controlling What's Logged
```php
class Post extends Model
{
use LogsActivity;
// Only log these events
protected static $recordEvents = ['created', 'published'];
// Exclude these attributes from change tracking
protected static $ignoreChangedAttributes = ['views', 'updated_at'];
// Log only these attributes
protected static $logAttributes = ['title', 'status'];
}
```
### Custom Descriptions
```php
class Post extends Model
{
use LogsActivity;
public function getActivityDescription(string $event): string
{
return match($event) {
'created' => "Created post: {$this->title}",
'updated' => "Updated post: {$this->title}",
'published' => "Published post: {$this->title}",
default => "Post {$event}",
};
}
}
```
### Custom Properties
```php
class Post extends Model
{
use LogsActivity;
public function getActivityProperties(string $event): array
{
return [
'title' => $this->title,
'category' => $this->category->name,
'word_count' => str_word_count($this->content),
'published_at' => $this->published_at?->toIso8601String(),
];
}
}
```
## GDPR Compliance
### IP Address Hashing
IP addresses are automatically hashed for privacy:
```php
use Core\Crypt\LthnHash;
// Automatically applied
$activity = Activity::create([
'properties' => [
'ip_address' => request()->ip(), // Hashed before storage
],
]);
// Verify IP match without storing plaintext
if (LthnHash::check(request()->ip(), $activity->properties['ip_address'])) {
// IP matches
}
```
### Data Retention
```php
use Core\Activity\Console\ActivityPruneCommand;
// Prune old activity (default: 90 days)
php artisan activity:prune
// Custom retention
php artisan activity:prune --days=30
// Dry run
php artisan activity:prune --dry-run
```
**Scheduled Pruning:**
```php
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('activity:prune')
->daily()
->at('02:00');
}
```
### Right to Erasure
```php
// Delete all activity for a user
Activity::causedBy($user)->delete();
// Delete activity for specific subject
Activity::forSubject($post)->delete();
// Anonymize instead of delete
Activity::causedBy($user)->update([
'causer_id' => null,
'causer_type' => null,
]);
```
## Activity Feed
### Building Activity Feeds
```php
use Core\Activity\Models\Activity;
// User's personal feed
$feed = Activity::causedBy($user)
->with(['subject', 'causer'])
->latest()
->paginate(20);
// Workspace activity feed
$feed = Activity::query()
->where('workspace_id', $workspace->id)
->whereIn('event', ['created', 'updated', 'published'])
->with(['subject', 'causer'])
->latest()
->paginate(20);
```
### Rendering Activity
```blade
{{-- resources/views/activity/feed.blade.php --}}
@foreach($activities as $activity)
<div class="activity-item">
<div class="activity-icon">
@if($activity->event === 'created')
<svg>...</svg>
@elseif($activity->event === 'updated')
<svg>...</svg>
@endif
</div>
<div class="activity-content">
<p>
<strong>{{ $activity->causer?->name ?? 'System' }}</strong>
{{ $activity->description }}
</p>
<time>{{ $activity->created_at->diffForHumans() }}</time>
</div>
</div>
@endforeach
```
### Livewire Component
```php
<?php
namespace Core\Activity\View\Modal\Admin;
use Livewire\Component;
use Core\Activity\Models\Activity;
class ActivityFeed extends Component
{
public $workspaceId;
public $events = ['created', 'updated', 'deleted'];
public $days = 7;
public function render()
{
$activities = Activity::query()
->when($this->workspaceId, fn($q) => $q->where('workspace_id', $this->workspaceId))
->whereIn('event', $this->events)
->where('created_at', '>=', now()->subDays($this->days))
->with(['subject', 'causer'])
->latest()
->paginate(20);
return view('activity::admin.activity-feed', [
'activities' => $activities,
]);
}
}
```
## Performance Optimization
### Eager Loading
```php
// ✅ Good - eager load relationships
$activities = Activity::query()
->with(['subject', 'causer', 'workspace'])
->latest()
->get();
// ❌ Bad - N+1 queries
$activities = Activity::latest()->get();
foreach ($activities as $activity) {
echo $activity->causer->name; // Query per iteration
}
```
### Chunking Large Datasets
```php
// Process activity in chunks
Activity::query()
->where('created_at', '<', now()->subDays(90))
->chunk(1000, function ($activities) {
foreach ($activities as $activity) {
$activity->delete();
}
});
```
### Queuing Activity Logging
```php
// For high-traffic applications
use Illuminate\Bus\Queueable;
class Post extends Model
{
use LogsActivity;
protected static $logActivityQueue = true;
protected static $logActivityConnection = 'redis';
}
```
## Analytics
### Activity Statistics
```php
use Core\Activity\Services\ActivityLogService;
$analytics = app(ActivityLogService::class);
// Count by event type
$stats = Activity::query()
->where('workspace_id', $workspace->id)
->whereBetween('created_at', [now()->subDays(30), now()])
->groupBy('event')
->selectRaw('event, COUNT(*) as count')
->get();
// Most active users
$topUsers = Activity::query()
->selectRaw('causer_id, causer_type, COUNT(*) as activity_count')
->groupBy('causer_id', 'causer_type')
->orderByDesc('activity_count')
->limit(10)
->get();
```
### Audit Reports
```php
// Generate audit trail
$audit = Activity::query()
->forSubject($post)
->with('causer')
->oldest()
->get()
->map(fn($activity) => [
'timestamp' => $activity->created_at->toIso8601String(),
'user' => $activity->causer?->name ?? 'System',
'event' => $activity->event,
'changes' => $activity->properties,
]);
```
## Best Practices
### 1. Log Meaningful Events
```php
// ✅ Good - business-relevant events
$logger->log($post, 'published', 'Post went live');
$logger->log($order, 'payment_received', 'Customer paid');
// ❌ Bad - too granular
$logger->log($post, 'view_count_incremented', 'Views++');
```
### 2. Include Context
```php
// ✅ Good - rich context
$logger->log($post, 'published', properties: [
'category' => $post->category->name,
'scheduled' => $post->published_at->isPast(),
'author' => $post->author->name,
]);
// ❌ Bad - no context
$logger->log($post, 'published');
```
### 3. Respect Privacy
```php
// ✅ Good - hash sensitive data
$logger->log($user, 'login', properties: [
'ip_address' => LthnHash::make(request()->ip()),
]);
// ❌ Bad - plaintext IP
$logger->log($user, 'login', properties: [
'ip_address' => request()->ip(),
]);
```
## Testing
```php
use Tests\TestCase;
use Core\Activity\Models\Activity;
class ActivityTest extends TestCase
{
public function test_logs_model_creation(): void
{
$post = Post::create(['title' => 'Test']);
$this->assertDatabaseHas('activities', [
'subject_type' => Post::class,
'subject_id' => $post->id,
'event' => 'created',
]);
}
public function test_logs_changes(): void
{
$post = Post::factory()->create(['status' => 'draft']);
$post->update(['status' => 'published']);
$activity = Activity::latest()->first();
$this->assertEquals('published', $activity->properties['status']);
}
}
```
## Learn More
- [Multi-Tenancy →](/core/tenancy)
- [GDPR Compliance →](/security/overview)

View file

@ -0,0 +1,546 @@
# Creating Custom Events
Learn how to create custom lifecycle events for extensibility in your modules.
## Why Custom Events?
Custom lifecycle events allow you to:
- Create extension points in your modules
- Enable third-party integrations
- Decouple module components
- Follow the framework's event-driven pattern
## Basic Custom Event
### Step 1: Create Event Class
```php
<?php
namespace Mod\Shop\Events;
use Core\Events\LifecycleEvent;
use Core\Events\Concerns\HasEventVersion;
class PaymentGatewaysRegistering extends LifecycleEvent
{
use HasEventVersion;
protected array $gateways = [];
public function gateway(string $name, string $class): void
{
$this->gateways[$name] = $class;
}
public function getGateways(): array
{
return $this->gateways;
}
public function version(): string
{
return '1.0.0';
}
}
```
### Step 2: Fire Event
```php
<?php
namespace Mod\Shop;
use Core\Events\FrameworkBooted;
use Mod\Shop\Events\PaymentGatewaysRegistering;
class Boot
{
public static array $listens = [
FrameworkBooted::class => 'onFrameworkBooted',
];
public function onFrameworkBooted(FrameworkBooted $event): void
{
// Fire custom event
$gatewayEvent = new PaymentGatewaysRegistering();
event($gatewayEvent);
// Register all collected gateways
foreach ($gatewayEvent->getGateways() as $name => $class) {
app('payment.gateways')->register($name, $class);
}
}
}
```
### Step 3: Listen to Event
```php
<?php
namespace Mod\Stripe;
use Mod\Shop\Events\PaymentGatewaysRegistering;
class Boot
{
public static array $listens = [
PaymentGatewaysRegistering::class => 'onPaymentGateways',
];
public function onPaymentGateways(PaymentGatewaysRegistering $event): void
{
$event->gateway('stripe', StripeGateway::class);
}
}
```
## Event with Multiple Methods
Provide different registration methods:
```php
<?php
namespace Mod\Blog\Events;
use Core\Events\LifecycleEvent;
class ContentTypesRegistering extends LifecycleEvent
{
protected array $types = [];
protected array $renderers = [];
protected array $validators = [];
public function type(string $name, string $model): void
{
$this->types[$name] = $model;
}
public function renderer(string $type, string $class): void
{
$this->renderers[$type] = $class;
}
public function validator(string $type, array $rules): void
{
$this->validators[$type] = $rules;
}
public function getTypes(): array
{
return $this->types;
}
public function getRenderers(): array
{
return $this->renderers;
}
public function getValidators(): array
{
return $this->validators;
}
}
```
**Usage:**
```php
public function onContentTypes(ContentTypesRegistering $event): void
{
$event->type('video', Video::class);
$event->renderer('video', VideoRenderer::class);
$event->validator('video', [
'url' => 'required|url',
'duration' => 'required|integer',
]);
}
```
## Event with Configuration
Pass configuration to listeners:
```php
<?php
namespace Mod\Analytics\Events;
use Core\Events\LifecycleEvent;
class AnalyticsProvidersRegistering extends LifecycleEvent
{
protected array $providers = [];
public function __construct(
public readonly array $config
) {}
public function provider(string $name, string $class, array $config = []): void
{
$this->providers[$name] = [
'class' => $class,
'config' => array_merge($this->config[$name] ?? [], $config),
];
}
public function getProviders(): array
{
return $this->providers;
}
}
```
**Fire with Config:**
```php
$event = new AnalyticsProvidersRegistering(
config('analytics.providers')
);
event($event);
```
## Event Versioning
Track event versions for backward compatibility:
```php
<?php
namespace Mod\Api\Events;
use Core\Events\LifecycleEvent;
use Core\Events\Concerns\HasEventVersion;
class ApiEndpointsRegistering extends LifecycleEvent
{
use HasEventVersion;
public function version(): string
{
return '2.0.0';
}
// v2 method
public function endpoint(string $path, string $controller, array $options = []): void
{
$this->endpoints[] = compact('path', 'controller', 'options');
}
// v1 compatibility method (deprecated)
public function route(string $path, string $controller): void
{
$this->endpoint($path, $controller, ['deprecated' => true]);
}
}
```
**Check Version in Listener:**
```php
public function onApiEndpoints(ApiEndpointsRegistering $event): void
{
if (version_compare($event->version(), '2.0.0', '>=')) {
// Use v2 API
$event->endpoint('/posts', PostController::class, [
'middleware' => ['auth:sanctum'],
]);
} else {
// Use v1 API (deprecated)
$event->route('/posts', PostController::class);
}
}
```
## Event Priority
Control listener execution order:
```php
<?php
namespace Mod\Core\Events;
use Core\Events\LifecycleEvent;
class ThemesRegistering extends LifecycleEvent
{
protected array $themes = [];
public function theme(string $name, string $class, int $priority = 0): void
{
$this->themes[] = compact('name', 'class', 'priority');
}
public function getThemes(): array
{
// Sort by priority (higher first)
usort($this->themes, fn($a, $b) => $b['priority'] <=> $a['priority']);
return $this->themes;
}
}
```
**Usage:**
```php
public function onThemes(ThemesRegistering $event): void
{
$event->theme('default', DefaultTheme::class, priority: 0);
$event->theme('premium', PremiumTheme::class, priority: 100);
}
```
## Event Validation
Validate registrations:
```php
<?php
namespace Mod\Forms\Events;
use Core\Events\LifecycleEvent;
use InvalidArgumentException;
class FormFieldsRegistering extends LifecycleEvent
{
protected array $fields = [];
public function field(string $type, string $class): void
{
// Validate field class
if (!class_exists($class)) {
throw new InvalidArgumentException("Field class {$class} does not exist");
}
if (!is_subclass_of($class, FormField::class)) {
throw new InvalidArgumentException("Field class must extend FormField");
}
$this->fields[$type] = $class;
}
public function getFields(): array
{
return $this->fields;
}
}
```
## Event Documentation
Document your events with docblocks:
```php
<?php
namespace Mod\Media\Events;
use Core\Events\LifecycleEvent;
/**
* Fired when media processors are being registered.
*
* Allows modules to register custom image/video processors.
*
* @example
* ```php
* public function onMediaProcessors(MediaProcessorsRegistering $event): void
* {
* $event->processor('watermark', WatermarkProcessor::class);
* $event->processor('thumbnail', ThumbnailProcessor::class);
* }
* ```
*/
class MediaProcessorsRegistering extends LifecycleEvent
{
protected array $processors = [];
/**
* Register a media processor.
*
* @param string $name Processor name (e.g., 'watermark')
* @param string $class Processor class (must implement ProcessorInterface)
*/
public function processor(string $name, string $class): void
{
$this->processors[$name] = $class;
}
/**
* Get all registered processors.
*
* @return array<string, string>
*/
public function getProcessors(): array
{
return $this->processors;
}
}
```
## Testing Custom Events
```php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Mod\Shop\Events\PaymentGatewaysRegistering;
use Mod\Stripe\StripeGateway;
class PaymentGatewaysEventTest extends TestCase
{
public function test_fires_payment_gateways_event(): void
{
Event::fake([PaymentGatewaysRegistering::class]);
// Trigger module boot
$this->app->boot();
Event::assertDispatched(PaymentGatewaysRegistering::class);
}
public function test_registers_payment_gateway(): void
{
$event = new PaymentGatewaysRegistering();
$event->gateway('stripe', StripeGateway::class);
$this->assertEquals(
['stripe' => StripeGateway::class],
$event->getGateways()
);
}
public function test_stripe_module_registers_gateway(): void
{
$event = new PaymentGatewaysRegistering();
$boot = new \Mod\Stripe\Boot();
$boot->onPaymentGateways($event);
$this->assertArrayHasKey('stripe', $event->getGateways());
}
}
```
## Best Practices
### 1. Use Descriptive Names
```php
// ✅ Good
class PaymentGatewaysRegistering extends LifecycleEvent
// ❌ Bad
class RegisterGateways extends LifecycleEvent
```
### 2. Provide Fluent API
```php
// ✅ Good - chainable
public function gateway(string $name, string $class): self
{
$this->gateways[$name] = $class;
return $this;
}
// Usage:
$event->gateway('stripe', StripeGateway::class)
->gateway('paypal', PayPalGateway::class);
```
### 3. Validate Early
```php
// ✅ Good - validate on registration
public function gateway(string $name, string $class): void
{
if (!class_exists($class)) {
throw new InvalidArgumentException("Gateway class not found: {$class}");
}
$this->gateways[$name] = $class;
}
```
### 4. Version Your Events
```php
// ✅ Good - versioned
use HasEventVersion;
public function version(): string
{
return '1.0.0';
}
```
## Real-World Example
Complete example of a custom event system:
```php
// Event
class SearchProvidersRegistering extends LifecycleEvent
{
use HasEventVersion;
protected array $providers = [];
public function provider(
string $name,
string $class,
int $priority = 0,
array $config = []
): void {
$this->providers[$name] = compact('class', 'priority', 'config');
}
public function getProviders(): array
{
uasort($this->providers, fn($a, $b) => $b['priority'] <=> $a['priority']);
return $this->providers;
}
public function version(): string
{
return '1.0.0';
}
}
// Fire event
$event = new SearchProvidersRegistering();
event($event);
foreach ($event->getProviders() as $name => $config) {
app('search')->register($name, new $config['class']($config['config']));
}
// Listen to event
class Boot
{
public static array $listens = [
SearchProvidersRegistering::class => 'onSearchProviders',
];
public function onSearchProviders(SearchProvidersRegistering $event): void
{
$event->provider('posts', PostSearchProvider::class, priority: 100);
$event->provider('users', UserSearchProvider::class, priority: 50);
}
}
```
## Learn More
- [Lifecycle Events →](/packages/core/events)
- [Module System →](/packages/core/modules)

View file

@ -0,0 +1,535 @@
# Lazy Loading
Core PHP Framework uses lazy loading to defer module instantiation until absolutely necessary. This dramatically improves performance by only loading code relevant to the current request.
## How It Works
### Traditional Approach (Everything Loads)
```php
// Boot ALL modules on every request
$modules = [
new BlogModule(),
new CommerceModule(),
new AnalyticsModule(),
new AdminModule(),
new ApiModule(),
// ... dozens more
];
// Web request loads admin code it doesn't need
// API request loads web views it doesn't use
// Memory: ~50MB, Boot time: ~500ms
```
### Lazy Loading Approach (On-Demand)
```php
// Register listeners WITHOUT instantiating modules
Event::listen(WebRoutesRegistering::class, LazyModuleListener::for(BlogModule::class));
Event::listen(AdminPanelBooting::class, LazyModuleListener::for(AdminModule::class));
// Web request → Only BlogModule instantiated
// API request → Only ApiModule instantiated
// Memory: ~15MB, Boot time: ~150ms
```
## Architecture
### 1. Module Discovery
`ModuleScanner` finds modules and extracts their event interests:
```php
$modules = [
[
'class' => Mod\Blog\Boot::class,
'listens' => [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => 'onAdmin',
],
],
// ...
];
```
### 2. Lazy Listener Registration
`ModuleRegistry` creates lazy listeners for each event-module pair:
```php
foreach ($modules as $module) {
foreach ($module['listens'] as $event => $method) {
Event::listen($event, new LazyModuleListener(
$module['class'],
$method
));
}
}
```
### 3. Event-Driven Loading
When an event fires, `LazyModuleListener` instantiates the module:
```php
class LazyModuleListener
{
public function __construct(
private string $moduleClass,
private string $method,
) {}
public function handle($event): void
{
// Module instantiated HERE, not before
$module = new $this->moduleClass();
$module->{$this->method}($event);
}
}
```
## Request Types and Loading
### Web Request
```
Request: GET /blog
WebRoutesRegistering fired
Only modules listening to WebRoutesRegistering loaded:
- BlogModule
- MarketingModule
Admin/API modules never instantiated
```
### Admin Request
```
Request: GET /admin/posts
AdminPanelBooting fired
Only modules with admin routes loaded:
- BlogAdminModule
- CoreAdminModule
Public web modules never instantiated
```
### API Request
```
Request: GET /api/v1/posts
ApiRoutesRegistering fired
Only modules with API endpoints loaded:
- BlogApiModule
- AuthModule
Web/Admin views never loaded
```
### Console Command
```
Command: php artisan blog:publish
ConsoleBooting fired
Only modules with commands loaded:
- BlogModule (has blog:publish command)
Web/Admin/API routes never registered
```
## Performance Impact
### Memory Usage
| Request Type | Traditional | Lazy Loading | Savings |
|--------------|-------------|--------------|---------|
| Web | 50 MB | 15 MB | 70% |
| Admin | 50 MB | 18 MB | 64% |
| API | 50 MB | 12 MB | 76% |
| Console | 50 MB | 10 MB | 80% |
### Boot Time
| Request Type | Traditional | Lazy Loading | Savings |
|--------------|-------------|--------------|---------|
| Web | 500ms | 150ms | 70% |
| Admin | 500ms | 180ms | 64% |
| API | 500ms | 120ms | 76% |
| Console | 500ms | 100ms | 80% |
*Measurements from production application with 50+ modules*
## Selective Loading
### Only Listen to Needed Events
Don't register for events you don't need:
```php
// ✅ Good - API-only module
class Boot
{
public static array $listens = [
ApiRoutesRegistering::class => 'onApiRoutes',
];
}
// ❌ Bad - unnecessary listeners
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes', // Not needed
AdminPanelBooting::class => 'onAdmin', // Not needed
ApiRoutesRegistering::class => 'onApiRoutes',
];
}
```
### Conditional Loading
Load features conditionally within event handlers:
```php
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Only load blog if enabled
if (config('modules.blog.enabled')) {
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
}
```
## Deferred Service Providers
Combine with Laravel's deferred providers for maximum laziness:
```php
<?php
namespace Mod\Blog;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
class BlogServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function register(): void
{
$this->app->singleton(BlogService::class, function ($app) {
return new BlogService(
$app->make(PostRepository::class)
);
});
}
public function provides(): array
{
// Only load this provider when BlogService is requested
return [BlogService::class];
}
}
```
## Lazy Collections
Use lazy collections for memory-efficient data processing:
```php
// ✅ Good - lazy loading
Post::query()
->published()
->cursor() // Returns lazy collection
->each(function ($post) {
ProcessPost::dispatch($post);
});
// ❌ Bad - loads all into memory
Post::query()
->published()
->get() // Loads everything
->each(function ($post) {
ProcessPost::dispatch($post);
});
```
## Lazy Relationships
Defer relationship loading until needed:
```php
// ✅ Good - lazy eager loading
$posts = Post::all();
if ($needsComments) {
$posts->load('comments');
}
// ❌ Bad - always loads comments
$posts = Post::with('comments')->get();
```
## Route Lazy Loading
Laravel 11+ supports route file lazy loading:
```php
// routes/web.php
Route::middleware('web')->group(function () {
// Only load blog routes when /blog is accessed
Route::prefix('blog')->group(base_path('routes/blog.php'));
});
```
## Cache Warming
Warm caches during deployment, not during requests:
```bash
# Deploy script
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Modules discovered once, cached
php artisan core:cache-modules
```
## Monitoring Lazy Loading
### Track Module Loading
Log when modules are instantiated:
```php
class LazyModuleListener
{
public function handle($event): void
{
$start = microtime(true);
$module = new $this->moduleClass();
$module->{$this->method}($event);
$duration = (microtime(true) - $start) * 1000;
Log::debug("Module loaded", [
'module' => $this->moduleClass,
'event' => get_class($event),
'duration_ms' => round($duration, 2),
]);
}
}
```
### Analyze Module Usage
Track which modules load for different request types:
```bash
# Enable debug logging
APP_DEBUG=true LOG_LEVEL=debug
# Make requests and check logs
tail -f storage/logs/laravel.log | grep "Module loaded"
```
## Debugging Lazy Loading
### Force Load All Modules
Disable lazy loading for debugging:
```php
// config/core.php
'modules' => [
'lazy_loading' => env('MODULES_LAZY_LOADING', true),
],
// .env
MODULES_LAZY_LOADING=false
```
### Check Module Load Order
```php
Event::listen('*', function ($eventName, $data) {
if (str_starts_with($eventName, 'Core\\Events\\')) {
Log::debug("Event fired", ['event' => $eventName]);
}
});
```
### Verify Listeners Registered
```bash
php artisan event:list | grep "Core\\Events"
```
## Best Practices
### 1. Keep Boot.php Lightweight
Move heavy initialization to service providers:
```php
// ✅ Good - lightweight Boot.php
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
// ❌ Bad - heavy initialization in Boot.php
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Don't do this in event handlers!
$this->registerServices();
$this->loadViews();
$this->publishAssets();
$this->registerCommands();
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
```
### 2. Avoid Global State in Modules
Don't store state in module classes:
```php
// ✅ Good - stateless
class Boot
{
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
}
// ❌ Bad - stateful
class Boot
{
private array $config = [];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$this->config = config('blog'); // Don't store state
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
}
```
### 3. Use Dependency Injection
Let the container handle dependencies:
```php
// ✅ Good - DI in services
class BlogService
{
public function __construct(
private PostRepository $posts,
private CacheManager $cache,
) {}
}
// ❌ Bad - manual instantiation
class BlogService
{
public function __construct()
{
$this->posts = new PostRepository();
$this->cache = new CacheManager();
}
}
```
### 4. Defer Heavy Operations
Don't perform expensive operations during boot:
```php
// ✅ Good - defer to queue
public function onFrameworkBooted(FrameworkBooted $event): void
{
dispatch(new WarmBlogCache())->afterResponse();
}
// ❌ Bad - expensive operation during boot
public function onFrameworkBooted(FrameworkBooted $event): void
{
// Don't do this!
$posts = Post::with('comments', 'categories', 'tags')->get();
Cache::put('blog:all-posts', $posts, 3600);
}
```
## Advanced Patterns
### Lazy Singletons
Register services as lazy singletons:
```php
$this->app->singleton(BlogService::class, function ($app) {
return new BlogService(
$app->make(PostRepository::class)
);
});
```
Service only instantiated when first requested:
```php
// BlogService not instantiated yet
$posts = Post::all();
// BlogService instantiated HERE
app(BlogService::class)->getRecentPosts();
```
### Contextual Binding
Bind different implementations based on context:
```php
$this->app->when(ApiController::class)
->needs(PostRepository::class)
->give(CachedPostRepository::class);
$this->app->when(AdminController::class)
->needs(PostRepository::class)
->give(LivePostRepository::class);
```
### Module Proxies
Create proxies for optional modules:
```php
class AnalyticsProxy
{
public function track(string $event, array $data = []): void
{
// Only load analytics module if it exists
if (class_exists(Mod\Analytics\AnalyticsService::class)) {
app(AnalyticsService::class)->track($event, $data);
}
}
}
```
## Learn More
- [Module System](/architecture/module-system)
- [Lifecycle Events](/architecture/lifecycle-events)
- [Performance Optimization](/architecture/performance)

View file

@ -0,0 +1,610 @@
# Lifecycle Events
Core PHP Framework uses an event-driven architecture where modules declare interest in lifecycle events. This enables lazy loading and modular composition without tight coupling.
## Overview
The lifecycle event system provides extension points throughout the framework's boot process. Modules register listeners for specific events, and are only instantiated when those events fire.
```
Application Boot
LifecycleEventProvider fires events
LazyModuleListener intercepts events
Module instantiated on-demand
Event handler executes
Module collects requests (routes, menus, etc.)
LifecycleEventProvider processes requests
```
## Core Events
### WebRoutesRegistering
**Fired during:** Web route registration (early boot)
**Purpose:** Register public-facing web routes and views
**Use cases:**
- Marketing pages
- Public blog
- Documentation site
- Landing pages
**Example:**
```php
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Register view namespace
$event->views('marketing', __DIR__.'/Views');
// Register routes
$event->routes(function () {
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/pricing', [PricingController::class, 'index'])->name('pricing');
Route::get('/contact', [ContactController::class, 'index'])->name('contact');
});
// Register middleware
$event->middleware(['web', 'track-visitor']);
}
```
**Available Methods:**
- `views(string $namespace, string $path)` - Register view namespace
- `routes(Closure $callback)` - Register routes
- `middleware(array $middleware)` - Apply middleware to routes
---
### AdminPanelBooting
**Fired during:** Admin panel initialization
**Purpose:** Register admin routes, menus, and dashboard widgets
**Use cases:**
- Admin CRUD interfaces
- Dashboard widgets
- Settings pages
- Admin navigation
**Example:**
```php
public function onAdmin(AdminPanelBooting $event): void
{
// Register admin routes
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
// Register admin menu
$event->menu(new BlogMenuProvider());
// Register dashboard widget
$event->widget(new PostStatsWidget());
// Register settings page
$event->settings('blog', BlogSettingsPage::class);
}
```
**Available Methods:**
- `routes(Closure $callback)` - Register admin routes
- `menu(AdminMenuProvider $provider)` - Register menu items
- `widget(DashboardWidget $widget)` - Register dashboard widget
- `settings(string $key, string $class)` - Register settings page
---
### ApiRoutesRegistering
**Fired during:** API route registration
**Purpose:** Register REST API endpoints
**Use cases:**
- RESTful APIs
- Webhooks
- Third-party integrations
- Mobile app backends
**Example:**
```php
public function onApiRoutes(ApiRoutesRegistering $event): void
{
$event->routes(function () {
Route::prefix('v1')->group(function () {
Route::apiResource('posts', PostApiController::class);
Route::get('posts/{post}/analytics', [PostApiController::class, 'analytics']);
});
});
// API-specific middleware
$event->middleware(['api', 'auth:sanctum', 'scope:blog:read']);
}
```
**Available Methods:**
- `routes(Closure $callback)` - Register API routes
- `middleware(array $middleware)` - Apply middleware
- `version(string $version)` - Set API version prefix
---
### ClientRoutesRegistering
**Fired during:** Client route registration
**Purpose:** Register authenticated client/dashboard routes
**Use cases:**
- User dashboards
- Account settings
- Client portals
- Authenticated SPA routes
**Example:**
```php
public function onClientRoutes(ClientRoutesRegistering $event): void
{
$event->views('dashboard', __DIR__.'/Views/Client');
$event->routes(function () {
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/account', [AccountController::class, 'show'])->name('account');
Route::post('/account', [AccountController::class, 'update']);
});
});
}
```
**Available Methods:**
- `views(string $namespace, string $path)` - Register view namespace
- `routes(Closure $callback)` - Register routes
- `middleware(array $middleware)` - Apply middleware
---
### ConsoleBooting
**Fired during:** Console kernel initialization
**Purpose:** Register Artisan commands
**Use cases:**
- Custom commands
- Scheduled tasks
- Maintenance scripts
- Data migrations
**Example:**
```php
public function onConsole(ConsoleBooting $event): void
{
// Register commands
$event->commands([
PublishPostCommand::class,
ImportPostsCommand::class,
GenerateSitemapCommand::class,
]);
// Register scheduled tasks
$event->schedule(function (Schedule $schedule) {
$schedule->command(PublishScheduledPostsCommand::class)
->hourly()
->withoutOverlapping();
$schedule->command(GenerateSitemapCommand::class)
->daily()
->at('01:00');
});
}
```
**Available Methods:**
- `commands(array $commands)` - Register commands
- `schedule(Closure $callback)` - Define scheduled tasks
---
### McpToolsRegistering
**Fired during:** MCP server initialization
**Purpose:** Register MCP (Model Context Protocol) tools for AI integrations
**Use cases:**
- AI-powered features
- LLM tool integrations
- Automated workflows
- AI assistants
**Example:**
```php
public function onMcpTools(McpToolsRegistering $event): void
{
$event->tools([
GetPostTool::class,
CreatePostTool::class,
UpdatePostTool::class,
SearchPostsTool::class,
]);
// Register prompts
$event->prompts([
GenerateBlogPostPrompt::class,
]);
// Register resources
$event->resources([
BlogPostResource::class,
]);
}
```
**Available Methods:**
- `tools(array $tools)` - Register MCP tools
- `prompts(array $prompts)` - Register prompt templates
- `resources(array $resources)` - Register resources
---
### FrameworkBooted
**Fired after:** All other lifecycle events have completed
**Purpose:** Late-stage initialization and cross-module setup
**Use cases:**
- Service registration
- Event listeners
- Observer registration
- Cache warming
**Example:**
```php
public function onFrameworkBooted(FrameworkBooted $event): void
{
// Register event listeners
Event::listen(PostPublished::class, SendPostNotification::class);
Event::listen(PostViewed::class, IncrementViewCount::class);
// Register model observers
Post::observe(PostObserver::class);
// Register service
app()->singleton(BlogService::class, function ($app) {
return new BlogService(
$app->make(PostRepository::class),
$app->make(CategoryRepository::class)
);
});
// Register policies
Gate::policy(Post::class, PostPolicy::class);
}
```
**Available Methods:**
- `service(string $abstract, Closure $factory)` - Register service
- `singleton(string $abstract, Closure $factory)` - Register singleton
- `listener(string $event, string $listener)` - Register event listener
## Event Declaration
Modules declare event listeners via the `$listens` property in `Boot.php`:
```php
<?php
namespace Mod\Blog;
use Core\Events\WebRoutesRegistering;
use Core\Events\AdminPanelBooting;
use Core\Events\ApiRoutesRegistering;
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => 'onAdmin',
ApiRoutesRegistering::class => 'onApiRoutes',
];
public function onWebRoutes(WebRoutesRegistering $event): void { }
public function onAdmin(AdminPanelBooting $event): void { }
public function onApiRoutes(ApiRoutesRegistering $event): void { }
}
```
## Lazy Loading
Modules are **not** instantiated until an event they listen to is fired:
```php
// Web request → Only WebRoutesRegistering listeners loaded
// API request → Only ApiRoutesRegistering listeners loaded
// Admin request → Only AdminPanelBooting listeners loaded
// Console command → Only ConsoleBooting listeners loaded
```
This dramatically reduces bootstrap time and memory usage.
## Event Flow
### 1. Module Discovery
`ModuleScanner` scans configured paths for `Boot.php` files:
```php
$scanner = new ModuleScanner();
$modules = $scanner->scan([
app_path('Core'),
app_path('Mod'),
app_path('Plug'),
]);
```
### 2. Listener Registration
`ModuleRegistry` wires lazy listeners:
```php
$registry = new ModuleRegistry();
$registry->registerModules($modules);
// Creates LazyModuleListener for each event-module pair
Event::listen(WebRoutesRegistering::class, LazyModuleListener::class);
```
### 3. Event Firing
`LifecycleEventProvider` fires events at appropriate times:
```php
// During route registration
$event = new WebRoutesRegistering();
event($event);
```
### 4. Module Loading
`LazyModuleListener` instantiates module on-demand:
```php
public function handle($event): void
{
$module = new $this->moduleClass(); // Module instantiated HERE
$module->{$this->method}($event);
}
```
### 5. Request Collection
Modules collect requests during event handling:
```php
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Stored in $event->routeRequests
$event->routes(fn () => require __DIR__.'/Routes/web.php');
// Stored in $event->viewRequests
$event->views('blog', __DIR__.'/Views');
}
```
### 6. Request Processing
`LifecycleEventProvider` processes collected requests:
```php
foreach ($event->routeRequests as $request) {
Route::middleware($request['middleware'])
->group($request['callback']);
}
```
## Custom Lifecycle Events
You can create custom lifecycle events by extending `LifecycleEvent`:
```php
<?php
namespace Mod\Commerce\Events;
use Core\Events\LifecycleEvent;
class PaymentProvidersRegistering extends LifecycleEvent
{
protected array $providers = [];
public function provider(string $name, string $class): void
{
$this->providers[$name] = $class;
}
public function getProviders(): array
{
return $this->providers;
}
}
```
Fire the event in your service provider:
```php
$event = new PaymentProvidersRegistering();
event($event);
foreach ($event->getProviders() as $name => $class) {
PaymentGateway::register($name, $class);
}
```
Modules can listen to your custom event:
```php
public static array $listens = [
PaymentProvidersRegistering::class => 'onPaymentProviders',
];
public function onPaymentProviders(PaymentProvidersRegistering $event): void
{
$event->provider('stripe', StripeProvider::class);
}
```
## Event Priorities
Control event listener execution order:
```php
Event::listen(WebRoutesRegistering::class, FirstModule::class, 100);
Event::listen(WebRoutesRegistering::class, SecondModule::class, 50);
Event::listen(WebRoutesRegistering::class, ThirdModule::class, 10);
// Execution order: FirstModule → SecondModule → ThirdModule
```
## Testing Lifecycle Events
Test that modules respond to events correctly:
```php
<?php
namespace Tests\Feature\Mod\Blog;
use Tests\TestCase;
use Core\Events\WebRoutesRegistering;
use Mod\Blog\Boot;
class BlogBootTest extends TestCase
{
public function test_registers_web_routes(): void
{
$event = new WebRoutesRegistering();
$boot = new Boot();
$boot->onWebRoutes($event);
$this->assertNotEmpty($event->routeRequests);
$this->assertNotEmpty($event->viewRequests);
}
public function test_registers_admin_menu(): void
{
$event = new AdminPanelBooting();
$boot = new Boot();
$boot->onAdmin($event);
$this->assertNotEmpty($event->menuProviders);
}
}
```
## Best Practices
### 1. Keep Event Handlers Focused
Each event handler should only register resources related to that lifecycle phase:
```php
// ✅ Good
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('blog', __DIR__.'/Views');
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
// ❌ Bad - service registration belongs in FrameworkBooted
public function onWebRoutes(WebRoutesRegistering $event): void
{
app()->singleton(BlogService::class, ...);
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
```
### 2. Use Dependency Injection
Event handlers receive the event object - use it instead of facades:
```php
// ✅ Good
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(function () {
Route::get('/blog', ...);
});
}
// ❌ Bad - bypasses event system
public function onWebRoutes(WebRoutesRegistering $event): void
{
Route::get('/blog', ...);
}
```
### 3. Only Listen to Needed Events
Don't register listeners for events you don't need:
```php
// ✅ Good - API-only module
public static array $listens = [
ApiRoutesRegistering::class => 'onApiRoutes',
];
// ❌ Bad - unnecessary listeners
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => 'onAdmin',
ApiRoutesRegistering::class => 'onApiRoutes',
];
```
### 4. Keep Boot.php Lightweight
`Boot.php` should only coordinate - extract complex logic to dedicated classes:
```php
// ✅ Good
public function onAdmin(AdminPanelBooting $event): void
{
$event->menu(new BlogMenuProvider());
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
}
// ❌ Bad - too much inline logic
public function onAdmin(AdminPanelBooting $event): void
{
$event->menu([
'label' => 'Blog',
'icon' => 'newspaper',
'children' => [
// ... 50 lines of menu configuration
],
]);
}
```
## Learn More
- [Module System](/architecture/module-system)
- [Lazy Loading](/architecture/lazy-loading)
- [Creating Custom Events](/architecture/custom-events)

View file

@ -0,0 +1,615 @@
# Module System
Core PHP Framework uses a modular monolith architecture where features are organized into self-contained modules that communicate through events and contracts.
## What is a Module?
A module is a self-contained feature with its own:
- Routes (web, admin, API)
- Models and migrations
- Controllers and actions
- Views and assets
- Configuration
- Tests
Modules declare their lifecycle event interests and are only loaded when needed.
## Module Types
### Core Modules (`app/Core/`)
Foundation modules that provide framework functionality:
```
app/Core/
├── Events/ # Lifecycle events
├── Module/ # Module system
├── Actions/ # Actions pattern
├── Config/ # Configuration system
├── Media/ # Media handling
└── Storage/ # Cache and storage
```
**Namespace:** `Core\`
**Purpose:** Framework internals, shared utilities
### Feature Modules (`app/Mod/`)
Business domain modules:
```
app/Mod/
├── Tenant/ # Multi-tenancy
├── Commerce/ # E-commerce features
├── Blog/ # Blogging
└── Analytics/ # Analytics
```
**Namespace:** `Mod\`
**Purpose:** Application features
### Website Modules (`app/Website/`)
Site-specific implementations:
```
app/Website/
├── Marketing/ # Marketing site
├── Docs/ # Documentation site
└── Support/ # Support portal
```
**Namespace:** `Website\`
**Purpose:** Deployable websites/frontends
### Plugin Modules (`app/Plug/`)
Optional integrations:
```
app/Plug/
├── Stripe/ # Stripe integration
├── Mailchimp/ # Mailchimp integration
└── Analytics/ # Analytics integrations
```
**Namespace:** `Plug\`
**Purpose:** Third-party integrations, optional features
## Module Structure
Standard module structure created by `php artisan make:mod`:
```
app/Mod/Example/
├── Boot.php # Module entry point
├── config.php # Module configuration
├── Actions/ # Business logic
│ ├── CreateExample.php
│ └── UpdateExample.php
├── Controllers/ # HTTP controllers
│ ├── Admin/
│ │ └── ExampleController.php
│ └── ExampleController.php
├── Models/ # Eloquent models
│ └── Example.php
├── Migrations/ # Database migrations
│ └── 2026_01_01_create_examples_table.php
├── Database/
│ ├── Factories/ # Model factories
│ │ └── ExampleFactory.php
│ └── Seeders/ # Database seeders
│ └── ExampleSeeder.php
├── Routes/ # Route definitions
│ ├── web.php # Public routes
│ ├── admin.php # Admin routes
│ └── api.php # API routes
├── Views/ # Blade templates
│ ├── index.blade.php
│ └── show.blade.php
├── Requests/ # Form requests
│ ├── StoreExampleRequest.php
│ └── UpdateExampleRequest.php
├── Resources/ # API resources
│ └── ExampleResource.php
├── Policies/ # Authorization policies
│ └── ExamplePolicy.php
├── Events/ # Domain events
│ └── ExampleCreated.php
├── Listeners/ # Event listeners
│ └── SendExampleNotification.php
├── Jobs/ # Queued jobs
│ └── ProcessExample.php
├── Services/ # Domain services
│ └── ExampleService.php
├── Mcp/ # MCP tools
│ └── Tools/
│ └── GetExampleTool.php
└── Tests/ # Module tests
├── Feature/
│ └── ExampleTest.php
└── Unit/
└── ExampleServiceTest.php
```
## Creating Modules
### Using Artisan Commands
```bash
# Create a feature module
php artisan make:mod Blog
# Create a website module
php artisan make:website Marketing
# Create a plugin module
php artisan make:plug Stripe
```
### Manual Creation
1. Create directory structure
2. Create `Boot.php` with `$listens` array
3. Register lifecycle event handlers
```php
<?php
namespace Mod\Example;
use Core\Events\WebRoutesRegistering;
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('example', __DIR__.'/Views');
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
}
```
## Module Discovery
### Auto-Discovery
Modules are automatically discovered by scanning configured paths:
```php
// config/core.php
'module_paths' => [
app_path('Core'),
app_path('Mod'),
app_path('Plug'),
],
```
### Manual Registration
Disable auto-discovery and register modules explicitly:
```php
// config/core.php
'modules' => [
'auto_discover' => false,
],
// app/Providers/AppServiceProvider.php
use Core\Module\ModuleRegistry;
public function boot(): void
{
$registry = app(ModuleRegistry::class);
$registry->register(Mod\Blog\Boot::class);
$registry->register(Mod\Commerce\Boot::class);
}
```
## Module Configuration
### Module-Level Configuration
Each module can have a `config.php` file:
```php
<?php
// app/Mod/Blog/config.php
return [
'posts_per_page' => env('BLOG_POSTS_PER_PAGE', 12),
'enable_comments' => env('BLOG_COMMENTS_ENABLED', true),
'cache_duration' => env('BLOG_CACHE_DURATION', 3600),
];
```
Access configuration:
```php
$perPage = config('mod.blog.posts_per_page', 12);
```
### Publishing Configuration
Allow users to customize module configuration:
```php
// app/Mod/Blog/BlogServiceProvider.php
public function boot(): void
{
$this->publishes([
__DIR__.'/config.php' => config_path('mod/blog.php'),
], 'blog-config');
}
```
Users can then publish and customize:
```bash
php artisan vendor:publish --tag=blog-config
```
## Inter-Module Communication
### 1. Events (Recommended)
Modules communicate via domain events:
```php
// Mod/Blog/Events/PostPublished.php
class PostPublished
{
public function __construct(public Post $post) {}
}
// Mod/Blog/Actions/PublishPost.php
PostPublished::dispatch($post);
// Mod/Analytics/Listeners/TrackPostPublished.php
Event::listen(PostPublished::class, TrackPostPublished::class);
```
### 2. Service Contracts
Define contracts for shared functionality:
```php
// Core/Contracts/NotificationService.php
interface NotificationService
{
public function send(Notifiable $notifiable, Notification $notification): void;
}
// Mod/Email/EmailNotificationService.php
class EmailNotificationService implements NotificationService
{
public function send(Notifiable $notifiable, Notification $notification): void
{
// Implementation
}
}
// Register in service provider
app()->bind(NotificationService::class, EmailNotificationService::class);
// Use in other modules
app(NotificationService::class)->send($user, $notification);
```
### 3. Facades
Create facades for frequently used services:
```php
// Mod/Blog/Facades/Blog.php
class Blog extends Facade
{
protected static function getFacadeAccessor()
{
return BlogService::class;
}
}
// Usage
Blog::getRecentPosts(10);
Blog::findBySlug('example-post');
```
## Module Dependencies
### Declaring Dependencies
Use PHP attributes to declare module dependencies:
```php
<?php
namespace Mod\BlogComments;
use Core\Module\Attributes\RequiresModule;
#[RequiresModule(Mod\Blog\Boot::class)]
class Boot
{
// ...
}
```
### Checking Dependencies
Verify dependencies are met:
```php
use Core\Module\ModuleRegistry;
$registry = app(ModuleRegistry::class);
if ($registry->isLoaded(Mod\Blog\Boot::class)) {
// Blog module is available
}
```
## Module Isolation
### Database Isolation
Use workspace scoping for multi-tenant isolation:
```php
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
class Post extends Model
{
use BelongsToWorkspace;
}
// Queries automatically scoped to current workspace
Post::all(); // Only returns posts for current workspace
```
### Cache Isolation
Use workspace-scoped caching:
```php
use Core\Mod\Tenant\Concerns\HasWorkspaceCache;
class Post extends Model
{
use BelongsToWorkspace, HasWorkspaceCache;
}
// Cache isolated per workspace
Post::forWorkspaceCached($workspace, 600);
```
### Route Isolation
Separate route files by context:
```php
// Routes/web.php - Public routes
Route::get('/blog', [BlogController::class, 'index']);
// Routes/admin.php - Admin routes
Route::resource('posts', PostController::class);
// Routes/api.php - API routes
Route::apiResource('posts', PostApiController::class);
```
## Module Testing
### Feature Tests
Test module functionality end-to-end:
```php
<?php
namespace Tests\Feature\Mod\Blog;
use Tests\TestCase;
use Mod\Blog\Models\Post;
class PostTest extends TestCase
{
public function test_can_view_published_posts(): void
{
Post::factory()->published()->count(3)->create();
$response = $this->get('/blog');
$response->assertStatus(200);
$response->assertViewHas('posts');
}
}
```
### Unit Tests
Test module services and actions:
```php
<?php
namespace Tests\Unit\Mod\Blog;
use Tests\TestCase;
use Mod\Blog\Actions\PublishPost;
use Mod\Blog\Models\Post;
class PublishPostTest extends TestCase
{
public function test_publishes_post(): void
{
$post = Post::factory()->create(['published_at' => null]);
PublishPost::run($post);
$this->assertNotNull($post->fresh()->published_at);
}
}
```
### Module Isolation Tests
Test that module doesn't leak dependencies:
```php
public function test_module_works_without_optional_dependencies(): void
{
// Simulate missing optional module
app()->forgetInstance(Mod\Analytics\AnalyticsService::class);
$response = $this->get('/blog');
$response->assertStatus(200);
}
```
## Best Practices
### 1. Keep Modules Focused
Each module should have a single, well-defined responsibility:
```
✅ Good: Mod\Blog (blogging features)
✅ Good: Mod\Comments (commenting system)
❌ Bad: Mod\BlogAndCommentsAndTags (too broad)
```
### 2. Use Explicit Dependencies
Don't assume other modules exist:
```php
// ✅ Good
if (class_exists(Mod\Analytics\AnalyticsService::class)) {
app(AnalyticsService::class)->track($event);
}
// ❌ Bad
app(AnalyticsService::class)->track($event); // Crashes if not available
```
### 3. Avoid Circular Dependencies
```
✅ Good: Blog → Comments (one-way)
❌ Bad: Blog ⟷ Comments (circular)
```
### 4. Use Interfaces for Contracts
Define interfaces for inter-module communication:
```php
// Core/Contracts/SearchProvider.php
interface SearchProvider
{
public function search(string $query): Collection;
}
// Mod/Blog/BlogSearchProvider.php
class BlogSearchProvider implements SearchProvider
{
// Implementation
}
```
### 5. Version Your APIs
If modules expose APIs, version them:
```php
// Routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('posts', V1\PostController::class);
});
Route::prefix('v2')->group(function () {
Route::apiResource('posts', V2\PostController::class);
});
```
## Troubleshooting
### Module Not Loading
Check module is in configured path:
```bash
# Verify path exists
ls -la app/Mod/YourModule
# Check Boot.php exists
cat app/Mod/YourModule/Boot.php
# Verify $listens array
grep "listens" app/Mod/YourModule/Boot.php
```
### Routes Not Registered
Ensure event handler calls `$event->routes()`:
```php
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Don't forget this!
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
```
### Views Not Found
Register view namespace:
```php
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Register view namespace
$event->views('blog', __DIR__.'/Views');
}
```
Then use namespaced views:
```php
return view('blog::index'); // Not just 'index'
```
## Learn More
- [Lifecycle Events](/architecture/lifecycle-events)
- [Lazy Loading](/architecture/lazy-loading)
- [Multi-Tenancy](/patterns-guide/multi-tenancy)
- [Actions Pattern](/patterns-guide/actions)

View file

@ -0,0 +1,600 @@
# Multi-Tenancy Architecture
Core PHP Framework provides robust multi-tenant isolation using workspace-scoped data. All tenant data is automatically isolated without manual filtering.
## Overview
Multi-tenancy ensures that users in one workspace (tenant) cannot access data from another workspace. Core PHP implements this through:
- Automatic query scoping via global scopes
- Workspace context validation
- Workspace-scoped caching
- Request-level workspace resolution
## Workspace Model
The `Workspace` model represents a tenant:
```php
<?php
namespace Mod\Tenant\Models;
use Illuminate\Database\Eloquent\Model;
class Workspace extends Model
{
protected $fillable = [
'name',
'slug',
'domain',
'is_suspended',
'settings',
];
protected $casts = [
'is_suspended' => 'boolean',
'settings' => 'array',
];
public function users()
{
return $this->hasMany(User::class);
}
public function isSuspended(): bool
{
return $this->is_suspended;
}
}
```
## Making Models Workspace-Scoped
### Basic Usage
Add the `BelongsToWorkspace` trait to any model:
```php
<?php
namespace Mod\Blog\Models;
use Illuminate\Database\Eloquent\Model;
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
class Post extends Model
{
use BelongsToWorkspace;
protected $fillable = ['title', 'content'];
}
```
### What the Trait Provides
```php
// All queries automatically scoped to current workspace
$posts = Post::all(); // Only returns posts for current workspace
// Create automatically assigns workspace_id
$post = Post::create([
'title' => 'Example',
'content' => 'Content',
// workspace_id added automatically
]);
// Cannot access posts from other workspaces
$post = Post::find(999); // null if belongs to different workspace
```
### Migration
Add `workspace_id` foreign key to tables:
```php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('content');
$table->timestamps();
$table->index(['workspace_id', 'created_at']);
});
```
## Workspace Scope
The `WorkspaceScope` global scope enforces data isolation:
```php
<?php
namespace Mod\Tenant\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class WorkspaceScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if ($workspace = $this->getCurrentWorkspace()) {
$builder->where("{$model->getTable()}.workspace_id", $workspace->id);
} elseif ($this->isStrictMode()) {
throw new MissingWorkspaceContextException();
}
}
// ...
}
```
### Strict Mode
Strict mode throws exceptions if workspace context is missing:
```php
// config/core.php
'workspace' => [
'strict_mode' => env('WORKSPACE_STRICT_MODE', true),
],
```
**Development:** Set to `true` to catch missing context bugs early
**Production:** Keep at `true` for security
### Bypassing Workspace Scope
Sometimes you need to query across workspaces:
```php
// Query all workspaces (use with caution!)
Post::acrossWorkspaces()->get();
// Temporarily disable strict mode
WorkspaceScope::withoutStrictMode(function () {
return Post::all();
});
// Query specific workspace
Post::forWorkspace($otherWorkspace)->get();
```
## Workspace Context
### Setting Workspace Context
The current workspace is typically set via middleware:
```php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Mod\Tenant\Models\Workspace;
class SetWorkspaceContext
{
public function handle(Request $request, Closure $next)
{
// Resolve workspace from subdomain
$subdomain = $this->extractSubdomain($request);
$workspace = Workspace::where('slug', $subdomain)->firstOrFail();
// Set workspace context for this request
app()->instance('current.workspace', $workspace);
return $next($request);
}
}
```
### Retrieving Current Workspace
```php
// Via helper
$workspace = workspace();
// Via container
$workspace = app('current.workspace');
// Via auth user
$workspace = auth()->user()->workspace;
```
### Middleware
Apply workspace validation middleware to routes:
```php
// Ensure workspace context exists
Route::middleware(RequireWorkspaceContext::class)->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
```
## Workspace-Scoped Caching
### Overview
Workspace-scoped caching ensures cache isolation between tenants:
```php
// Cache key: workspace:123:posts:recent
// Different workspace = different cache key
$posts = Post::forWorkspaceCached($workspace, 600);
```
### HasWorkspaceCache Trait
Add workspace caching to models:
```php
<?php
namespace Mod\Blog\Models;
use Illuminate\Database\Eloquent\Model;
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
use Core\Mod\Tenant\Concerns\HasWorkspaceCache;
class Post extends Model
{
use BelongsToWorkspace, HasWorkspaceCache;
}
```
### Cache Methods
```php
// Cache for specific workspace
$posts = Post::forWorkspaceCached($workspace, 600);
// Cache for current workspace
$posts = Post::ownedByCurrentWorkspaceCached(600);
// Invalidate workspace cache
Post::invalidateWorkspaceCache($workspace);
// Invalidate all caches for a workspace
WorkspaceCacheManager::invalidateAll($workspace);
```
### Cache Configuration
```php
// config/core.php
'workspace_cache' => [
'enabled' => env('WORKSPACE_CACHE_ENABLED', true),
'ttl' => env('WORKSPACE_CACHE_TTL', 3600),
'use_tags' => env('WORKSPACE_CACHE_USE_TAGS', true),
'prefix' => 'workspace',
],
```
### Cache Tags (Recommended)
Use cache tags for granular invalidation:
```php
// Store with tags
Cache::tags(['workspace:'.$workspace->id, 'posts'])
->put('recent-posts', $posts, 600);
// Invalidate all posts caches for workspace
Cache::tags(['workspace:'.$workspace->id, 'posts'])->flush();
// Invalidate everything for workspace
Cache::tags(['workspace:'.$workspace->id])->flush();
```
## Database Isolation Strategies
### Shared Database (Recommended)
Single database with `workspace_id` column:
**Pros:**
- Simple deployment
- Easy backups
- Cross-workspace queries possible
- Cost-effective
**Cons:**
- Requires careful scoping
- One bad query can leak data
```php
// All tables have workspace_id
Schema::create('posts', function (Blueprint $table) {
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
// ...
});
```
### Separate Databases (Advanced)
Each workspace has its own database:
**Pros:**
- Complete isolation
- Better security
- Easier compliance
**Cons:**
- Complex migrations
- Higher operational cost
- No cross-workspace queries
```php
// Dynamically switch database connection
config([
'database.connections.workspace' => [
'database' => "workspace_{$workspace->id}",
// ...
],
]);
DB::connection('workspace')->table('posts')->get();
```
## Security Best Practices
### 1. Always Use WorkspaceScope
Never bypass workspace scoping in application code:
```php
// ✅ Good
$posts = Post::all();
// ❌ Bad - security vulnerability!
$posts = Post::withoutGlobalScope(WorkspaceScope::class)->get();
```
### 2. Validate Workspace Context
Always validate workspace exists and isn't suspended:
```php
public function handle(Request $request, Closure $next)
{
$workspace = workspace();
if (! $workspace) {
throw new MissingWorkspaceContextException();
}
if ($workspace->isSuspended()) {
abort(403, 'Workspace suspended');
}
return $next($request);
}
```
### 3. Use Policies for Authorization
Combine workspace scoping with Laravel policies:
```php
class PostPolicy
{
public function update(User $user, Post $post): bool
{
// Workspace scope ensures $post belongs to current workspace
// Policy checks user has permission within that workspace
return $user->can('edit-posts');
}
}
```
### 4. Audit Workspace Access
Log workspace access for security auditing:
```php
activity()
->causedBy($user)
->performedOn($workspace)
->withProperties(['action' => 'accessed'])
->log('Workspace accessed');
```
### 5. Test Cross-Workspace Isolation
Write tests to verify data isolation:
```php
public function test_cannot_access_other_workspace_data(): void
{
$workspace1 = Workspace::factory()->create();
$workspace2 = Workspace::factory()->create();
$post = Post::factory()->for($workspace1)->create();
// Set context to workspace2
app()->instance('current.workspace', $workspace2);
// Should not find post from workspace1
$this->assertNull(Post::find($post->id));
}
```
## Cross-Workspace Operations
### Admin Operations
Admins sometimes need cross-workspace access:
```php
// Check if user is super admin
if (auth()->user()->isSuperAdmin()) {
// Allow cross-workspace queries
$allPosts = Post::acrossWorkspaces()
->where('published_at', '>', now()->subDays(7))
->get();
}
```
### Reporting
Generate reports across workspaces:
```php
class GenerateSystemReportJob
{
public function handle(): void
{
$stats = WorkspaceScope::withoutStrictMode(function () {
return [
'total_posts' => Post::count(),
'total_users' => User::count(),
'by_workspace' => Workspace::withCount('posts')->get(),
];
});
// ...
}
}
```
### Migrations
Migrations run without workspace context:
```php
public function up(): void
{
WorkspaceScope::withoutStrictMode(function () {
// Migrate data across all workspaces
Post::chunk(100, function ($posts) {
foreach ($posts as $post) {
$post->update(['migrated' => true]);
}
});
});
}
```
## Performance Optimization
### Eager Loading
Include workspace relation when needed:
```php
// ✅ Good
$posts = Post::with('workspace')->get();
// ❌ Bad - N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
echo $post->workspace->name; // N+1
}
```
### Index Optimization
Add composite indexes for workspace queries:
```php
$table->index(['workspace_id', 'created_at']);
$table->index(['workspace_id', 'status']);
$table->index(['workspace_id', 'user_id']);
```
### Partition Tables (Advanced)
For very large datasets, partition by workspace_id:
```sql
CREATE TABLE posts (
id BIGINT,
workspace_id BIGINT NOT NULL,
-- ...
) PARTITION BY HASH(workspace_id) PARTITIONS 10;
```
## Monitoring
### Track Workspace Usage
Monitor workspace-level metrics:
```php
// Query count per workspace
DB::listen(function ($query) {
$workspace = workspace();
if ($workspace) {
Redis::zincrby('workspace:queries', 1, $workspace->id);
}
});
// Get top workspaces by query count
$top = Redis::zrevrange('workspace:queries', 0, 10, 'WITHSCORES');
```
### Cache Hit Rates
Track cache effectiveness per workspace:
```php
WorkspaceCacheManager::trackHit($workspace);
WorkspaceCacheManager::trackMiss($workspace);
$hitRate = WorkspaceCacheManager::getHitRate($workspace);
```
## Troubleshooting
### Missing Workspace Context
```
MissingWorkspaceContextException: Workspace context required but not set
```
**Solution:** Ensure middleware sets workspace context:
```php
Route::middleware(RequireWorkspaceContext::class)->group(/*...*/);
```
### Wrong Workspace Data
```
User sees data from different workspace
```
**Solution:** Check workspace is set correctly:
```php
dd(workspace()); // Verify correct workspace
```
### Cache Bleeding
```
Cached data appearing across workspaces
```
**Solution:** Ensure cache keys include workspace ID:
```php
// ✅ Good
$key = "workspace:{$workspace->id}:posts:recent";
// ❌ Bad
$key = "posts:recent"; // Same key for all workspaces!
```
## Learn More
- [Workspace Caching](/patterns-guide/workspace-caching)
- [Security Best Practices](/security/overview)
- [Testing Multi-Tenancy](/testing/multi-tenancy)

View file

@ -0,0 +1,513 @@
# Performance Optimization
Best practices and techniques for optimizing Core PHP Framework applications.
## Database Optimization
### Eager Loading
Prevent N+1 queries with eager loading:
```php
// ❌ Bad - N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Query per post
echo $post->category->name; // Another query per post
}
// ✅ Good - 3 queries total
$posts = Post::with(['author', 'category'])->get();
foreach ($posts as $post) {
echo $post->author->name;
echo $post->category->name;
}
```
### Query Optimization
```php
// ❌ Bad - fetches all columns
$posts = Post::all();
// ✅ Good - only needed columns
$posts = Post::select(['id', 'title', 'created_at'])->get();
// ✅ Good - count instead of loading all
$count = Post::count();
// ❌ Bad
$count = Post::all()->count();
// ✅ Good - exists check
$exists = Post::where('status', 'published')->exists();
// ❌ Bad
$exists = Post::where('status', 'published')->count() > 0;
```
### Chunking Large Datasets
```php
// ❌ Bad - loads everything into memory
$posts = Post::all();
foreach ($posts as $post) {
$this->process($post);
}
// ✅ Good - process in chunks
Post::chunk(1000, function ($posts) {
foreach ($posts as $post) {
$this->process($post);
}
});
// ✅ Better - lazy collection
Post::lazy()->each(function ($post) {
$this->process($post);
});
```
### Database Indexes
```php
// Migration
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique(); // Index for lookups
$table->string('status')->index(); // Index for filtering
$table->foreignId('workspace_id')->constrained(); // Foreign key index
// Composite index for common query
$table->index(['workspace_id', 'status', 'created_at']);
});
```
## Caching Strategies
### Model Caching
```php
use Illuminate\Support\Facades\Cache;
class Post extends Model
{
public static function findCached(int $id): ?self
{
return Cache::remember(
"posts.{$id}",
now()->addHour(),
fn () => self::find($id)
);
}
protected static function booted(): void
{
// Invalidate cache on update
static::updated(fn ($post) => Cache::forget("posts.{$post->id}"));
static::deleted(fn ($post) => Cache::forget("posts.{$post->id}"));
}
}
```
### Query Result Caching
```php
// ❌ Bad - no caching
public function getPopularPosts()
{
return Post::where('views', '>', 1000)
->orderByDesc('views')
->limit(10)
->get();
}
// ✅ Good - cached for 1 hour
public function getPopularPosts()
{
return Cache::remember('posts.popular', 3600, function () {
return Post::where('views', '>', 1000)
->orderByDesc('views')
->limit(10)
->get();
});
}
```
### Cache Tags
```php
// Tag cache for easy invalidation
Cache::tags(['posts', 'popular'])->put('popular-posts', $posts, 3600);
// Clear all posts cache
Cache::tags('posts')->flush();
```
### Redis Caching
```php
// config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
],
```
## Asset Optimization
### CDN Integration
```php
// Use CDN helper
<img src="{{ cdn('images/hero.jpg') }}" alt="Hero">
// With transformations
<img src="{{ cdn('images/hero.jpg', ['width' => 800, 'quality' => 85]) }}">
```
### Image Optimization
```php
use Core\Media\Image\ImageOptimizer;
$optimizer = app(ImageOptimizer::class);
// Automatic optimization
$optimizer->optimize($imagePath, [
'quality' => 85,
'max_width' => 1920,
'strip_exif' => true,
'convert_to_webp' => true,
]);
```
### Lazy Loading
```blade
{{-- Lazy load images --}}
<img src="{{ cdn($image) }}" loading="lazy" alt="...">
{{-- Lazy load thumbnails --}}
<img src="{{ lazy_thumbnail($image, 'medium') }}" loading="lazy" alt="...">
```
## Code Optimization
### Lazy Loading Modules
Modules only load when their events fire:
```php
// Module Boot.php
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
// Only loads when WebRoutesRegistering fires
// Saves memory and boot time
```
### Deferred Service Providers
```php
<?php
namespace Mod\Analytics;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function register(): void
{
$this->app->singleton(AnalyticsService::class);
}
public function provides(): array
{
return [AnalyticsService::class];
}
}
```
### Configuration Caching
```bash
# Cache configuration
php artisan config:cache
# Clear config cache
php artisan config:clear
```
### Route Caching
```bash
# Cache routes
php artisan route:cache
# Clear route cache
php artisan route:clear
```
## Queue Optimization
### Queue Heavy Operations
```php
// ❌ Bad - slow request
public function store(Request $request)
{
$post = Post::create($request->validated());
// Slow operations in request cycle
$this->generateThumbnails($post);
$this->generateOgImage($post);
$this->notifySubscribers($post);
return redirect()->route('posts.show', $post);
}
// ✅ Good - queued
public function store(Request $request)
{
$post = Post::create($request->validated());
// Queue heavy operations
GenerateThumbnails::dispatch($post);
GenerateOgImage::dispatch($post);
NotifySubscribers::dispatch($post);
return redirect()->route('posts.show', $post);
}
```
### Job Batching
```php
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
Bus::batch([
new ProcessPost($post1),
new ProcessPost($post2),
new ProcessPost($post3),
])->then(function (Batch $batch) {
// All jobs completed successfully
})->catch(function (Batch $batch, Throwable $e) {
// First batch job failure
})->finally(function (Batch $batch) {
// Batch finished
})->dispatch();
```
## Livewire Optimization
### Lazy Loading Components
```blade
{{-- Load component when visible --}}
<livewire:post-list lazy />
{{-- Load on interaction --}}
<livewire:comments lazy on="click" />
```
### Polling Optimization
```php
// ❌ Bad - polls every 1s
<div wire:poll.1s>
{{ $count }} users online
</div>
// ✅ Good - polls every 30s
<div wire:poll.30s>
{{ $count }} users online
</div>
// ✅ Better - poll only when visible
<div wire:poll.visible.30s>
{{ $count }} users online
</div>
```
### Debouncing
```blade
{{-- Debounce search input --}}
<input
type="search"
wire:model.live.debounce.500ms="search"
placeholder="Search..."
>
```
## Response Optimization
### HTTP Caching
```php
// Cache response for 1 hour
return response($content)
->header('Cache-Control', 'public, max-age=3600');
// ETag caching
$etag = md5($content);
if ($request->header('If-None-Match') === $etag) {
return response('', 304);
}
return response($content)
->header('ETag', $etag);
```
### Gzip Compression
```php
// config/app.php (handled by middleware)
'middleware' => [
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Http\Middleware\ValidatePostSize::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
],
```
### Response Streaming
```php
// Stream large files
return response()->streamDownload(function () {
$handle = fopen('large-file.csv', 'r');
while (!feof($handle)) {
echo fread($handle, 8192);
flush();
}
fclose($handle);
}, 'download.csv');
```
## Monitoring Performance
### Query Logging
```php
// Enable query log in development
if (app()->isLocal()) {
DB::enableQueryLog();
}
// View queries
dd(DB::getQueryLog());
```
### Telescope
```bash
# Install Laravel Telescope
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
```
### Clockwork
```bash
# Install Clockwork
composer require itsgoingd/clockwork --dev
```
### Application Performance
```php
// Measure execution time
$start = microtime(true);
// Your code here
$duration = (microtime(true) - $start) * 1000; // milliseconds
Log::info("Operation took {$duration}ms");
```
## Load Testing
### Using Apache Bench
```bash
# 1000 requests, 10 concurrent
ab -n 1000 -c 10 https://example.com/
```
### Using k6
```javascript
// load-test.js
import http from 'k6/http';
export let options = {
vus: 10, // 10 virtual users
duration: '30s',
};
export default function () {
http.get('https://example.com/api/posts');
}
```
```bash
k6 run load-test.js
```
## Best Practices Checklist
### Database
- [ ] Use eager loading to prevent N+1 queries
- [ ] Add indexes to frequently queried columns
- [ ] Use `select()` to limit columns
- [ ] Chunk large datasets
- [ ] Use `exists()` instead of `count() > 0`
### Caching
- [ ] Cache expensive query results
- [ ] Use Redis for session/cache storage
- [ ] Implement cache tags for easy invalidation
- [ ] Set appropriate cache TTLs
### Assets
- [ ] Optimize images before uploading
- [ ] Use CDN for static assets
- [ ] Enable lazy loading for images
- [ ] Generate responsive image sizes
### Code
- [ ] Queue heavy operations
- [ ] Use lazy loading for modules
- [ ] Cache configuration and routes
- [ ] Implement deferred service providers
### Frontend
- [ ] Minimize JavaScript bundle size
- [ ] Debounce user input
- [ ] Use lazy loading for Livewire components
- [ ] Optimize polling intervals
### Monitoring
- [ ] Use Telescope/Clockwork in development
- [ ] Log slow queries
- [ ] Monitor cache hit rates
- [ ] Track job queue performance
## Learn More
- [Configuration →](/packages/core/configuration)
- [CDN Integration →](/packages/core/cdn)
- [Media Processing →](/packages/core/media)

399
build/php/cdn.md Normal file
View file

@ -0,0 +1,399 @@
# CDN Integration
Core PHP provides unified CDN integration for BunnyCDN and Cloudflare with automatic asset offloading, URL generation, and cache management.
## Configuration
```php
// config/cdn.php
return [
'driver' => env('CDN_DRIVER', 'bunnycdn'),
'bunnycdn' => [
'api_key' => env('BUNNY_API_KEY'),
'storage_zone' => env('BUNNY_STORAGE_ZONE'),
'storage_password' => env('BUNNY_STORAGE_PASSWORD'),
'cdn_url' => env('BUNNY_CDN_URL'),
'pull_zone_id' => env('BUNNY_PULL_ZONE_ID'),
],
'cloudflare' => [
'zone_id' => env('CLOUDFLARE_ZONE_ID'),
'api_token' => env('CLOUDFLARE_API_TOKEN'),
'cdn_url' => env('CLOUDFLARE_CDN_URL'),
],
'offload' => [
'enabled' => env('CDN_OFFLOAD_ENABLED', false),
'paths' => ['public/images', 'public/media', 'storage/app/public'],
],
];
```
## Basic Usage
### Generating CDN URLs
```php
use Core\Cdn\Facades\Cdn;
// Generate CDN URL
$url = Cdn::url('images/photo.jpg');
// https://cdn.example.com/images/photo.jpg
// With transformation parameters
$url = Cdn::url('images/photo.jpg', [
'width' => 800,
'quality' => 85,
]);
```
### Helper Function
```php
// Global helper
$url = cdn_url('images/photo.jpg');
// In Blade templates
<img src="{{ cdn_url('images/photo.jpg') }}" alt="Photo">
```
### Storing Files
```php
// Upload file to CDN
$path = Cdn::store($uploadedFile, 'media');
// Store with custom filename
$path = Cdn::store($uploadedFile, 'media', 'custom-name.jpg');
// Store from contents
$path = Cdn::put('path/file.txt', $contents);
```
### Deleting Files
```php
// Delete single file
Cdn::delete('media/photo.jpg');
// Delete multiple files
Cdn::delete(['media/photo1.jpg', 'media/photo2.jpg']);
// Delete directory
Cdn::deleteDirectory('media/old');
```
## Cache Purging
### Purge Single File
```php
// Purge specific file from CDN cache
Cdn::purge('images/photo.jpg');
```
### Purge Multiple Files
```php
// Purge multiple files
Cdn::purge([
'images/photo1.jpg',
'images/photo2.jpg',
]);
```
### Purge by Pattern
```php
// Purge all images
Cdn::purge('images/*');
// Purge all JPEGs
Cdn::purge('**/*.jpg');
```
### Purge Everything
```php
// Purge entire CDN cache (use sparingly!)
Cdn::purgeAll();
```
## Asset Offloading
Automatically offload existing assets to CDN:
```bash
# Offload public disk
php artisan storage:offload --disk=public
# Offload specific path
php artisan storage:offload --path=public/images
# Dry run (preview without uploading)
php artisan storage:offload --dry-run
```
### Programmatic Offloading
```php
use Core\Cdn\Services\AssetPipeline;
$pipeline = app(AssetPipeline::class);
// Offload directory
$result = $pipeline->offload('public/images', [
'extensions' => ['jpg', 'png', 'gif', 'webp'],
'min_size' => 1024, // Only files > 1KB
]);
echo "Uploaded: {$result['uploaded']} files\n";
echo "Skipped: {$result['skipped']} files\n";
```
## URL Builder
Advanced URL construction with transformations:
```php
use Core\Cdn\Services\CdnUrlBuilder;
$builder = app(CdnUrlBuilder::class);
$url = $builder->build('images/photo.jpg', [
// Dimensions
'width' => 800,
'height' => 600,
'aspect_ratio' => '16:9',
// Quality
'quality' => 85,
'format' => 'webp',
// Effects
'blur' => 10,
'brightness' => 1.2,
'contrast' => 1.1,
// Cropping
'crop' => 'center',
'gravity' => 'face',
]);
```
## BunnyCDN Specific
### Pull Zone Management
```php
use Core\Cdn\Services\BunnyCdnService;
$bunny = app(BunnyCdnService::class);
// Get pull zone info
$pullZone = $bunny->getPullZone($pullZoneId);
// Add/remove hostnames
$bunny->addHostname($pullZoneId, 'cdn.example.com');
$bunny->removeHostname($pullZoneId, 'cdn.example.com');
// Enable/disable cache
$bunny->setCacheEnabled($pullZoneId, true);
```
### Storage Zone Operations
```php
use Core\Cdn\Services\BunnyStorageService;
$storage = app(BunnyStorageService::class);
// List files
$files = $storage->list('media/');
// Get file info
$info = $storage->getFileInfo('media/photo.jpg');
// Download file
$contents = $storage->download('media/photo.jpg');
```
## Cloudflare Specific
### Zone Management
```php
use Core\Cdn\Services\FluxCdnService;
$cloudflare = app(FluxCdnService::class);
// Purge cache by URLs
$cloudflare->purgePaths([
'https://example.com/images/photo.jpg',
'https://example.com/styles/app.css',
]);
// Purge by cache tags
$cloudflare->purgeTags(['images', 'media']);
// Purge everything
$cloudflare->purgeEverything();
```
## Testing
### Fake CDN
```php
use Core\Cdn\Facades\Cdn;
class UploadTest extends TestCase
{
public function test_uploads_file(): void
{
Cdn::fake();
$response = $this->post('/upload', [
'file' => UploadedFile::fake()->image('photo.jpg'),
]);
Cdn::assertStored('media/photo.jpg');
}
}
```
### Assert Operations
```php
// Assert file was stored
Cdn::assertStored('path/file.jpg');
// Assert file was deleted
Cdn::assertDeleted('path/file.jpg');
// Assert cache was purged
Cdn::assertPurged('path/file.jpg');
// Assert nothing was stored
Cdn::assertNothingStored();
```
## Performance
### URL Caching
CDN URLs are cached to avoid repeated lookups:
```php
// URLs cached for 1 hour
$url = Cdn::url('images/photo.jpg'); // Generates URL + caches
$url = Cdn::url('images/photo.jpg'); // Returns from cache
```
### Batch Operations
```php
// Batch delete (single API call)
Cdn::delete([
'media/photo1.jpg',
'media/photo2.jpg',
'media/photo3.jpg',
]);
// Batch purge (single API call)
Cdn::purge([
'images/*.jpg',
'styles/*.css',
]);
```
## Best Practices
### 1. Use Helper in Blade
```blade
{{-- ✅ Good --}}
<img src="{{ cdn_url('images/photo.jpg') }}" alt="Photo">
{{-- ❌ Bad - relative path --}}
<img src="/images/photo.jpg" alt="Photo">
```
### 2. Offload Static Assets
```php
// ✅ Good - offload after upload
public function store(Request $request)
{
$path = $request->file('image')->store('media');
// Offload to CDN immediately
Cdn::store($path);
return $path;
}
```
### 3. Purge After Updates
```php
// ✅ Good - purge on update
public function update(Request $request, Media $media)
{
$oldPath = $media->path;
$media->update($request->validated());
// Purge old file from cache
Cdn::purge($oldPath);
}
```
### 4. Use Transformations
```php
// ✅ Good - CDN transforms image
<img src="{{ cdn_url('photo.jpg', ['width' => 400, 'quality' => 85]) }}">
// ❌ Bad - transform server-side
<img src="{{ route('image.transform', ['path' => 'photo.jpg', 'width' => 400]) }}">
```
## Troubleshooting
### Files Not Appearing
```bash
# Verify CDN credentials
php artisan tinker
>>> Cdn::store(UploadedFile::fake()->image('test.jpg'), 'test')
# Check CDN dashboard for new files
```
### Purge Not Working
```bash
# Verify pull zone ID
php artisan tinker
>>> config('cdn.bunnycdn.pull_zone_id')
# Manual purge via dashboard
```
### URLs Not Resolving
```php
// Check CDN URL configuration
echo config('cdn.bunnycdn.cdn_url');
// Verify file exists on CDN
$exists = Cdn::exists('path/file.jpg');
```
## Learn More
- [Media Processing →](/core/media)
- [Storage Configuration →](/guide/configuration#storage)
- [Asset Pipeline →](/core/media#asset-pipeline)

474
build/php/configuration.md Normal file
View file

@ -0,0 +1,474 @@
# Configuration Management
Core PHP Framework provides a powerful multi-profile configuration system with versioning, rollback capabilities, and environment-specific overrides.
## Basic Usage
### Storing Configuration
```php
use Core\Config\ConfigService;
$config = app(ConfigService::class);
// Store simple value
$config->set('app.name', 'My Application');
// Store nested configuration
$config->set('mail.driver', 'smtp', [
'host' => 'smtp.mailtrap.io',
'port' => 2525,
'encryption' => 'tls',
]);
// Store with profile
$config->set('cache.driver', 'redis', [], 'production');
```
### Retrieving Configuration
```php
// Get simple value
$name = $config->get('app.name');
// Get with default
$driver = $config->get('cache.driver', 'file');
// Get nested value
$host = $config->get('mail.driver.host');
// Get from specific profile
$driver = $config->get('cache.driver', 'file', 'production');
```
## Profiles
Profiles enable environment-specific configuration:
### Creating Profiles
```php
use Core\Config\Models\ConfigProfile;
// Development profile
$dev = ConfigProfile::create([
'name' => 'development',
'description' => 'Development environment settings',
'is_active' => true,
]);
// Staging profile
$staging = ConfigProfile::create([
'name' => 'staging',
'description' => 'Staging environment',
'is_active' => false,
]);
// Production profile
$prod = ConfigProfile::create([
'name' => 'production',
'description' => 'Production environment',
'is_active' => false,
]);
```
### Activating Profiles
```php
// Activate production profile
$prod->activate();
// Deactivate all others
ConfigProfile::query()
->where('id', '!=', $prod->id)
->update(['is_active' => false]);
```
### Profile Inheritance
```php
// Set base value
$config->set('cache.ttl', 3600);
// Override in production
$config->set('cache.ttl', 86400, [], 'production');
// Override in development
$config->set('cache.ttl', 60, [], 'development');
// Retrieval uses active profile automatically
$ttl = $config->get('cache.ttl'); // Returns profile-specific value
```
## Configuration Keys
### Key Metadata
```php
use Core\Config\Models\ConfigKey;
$key = ConfigKey::create([
'key' => 'api.rate_limit',
'description' => 'API rate limit per hour',
'type' => 'integer',
'is_sensitive' => false,
'validation_rules' => ['required', 'integer', 'min:100'],
]);
```
### Sensitive Configuration
```php
// Mark as sensitive (encrypted at rest)
$key = ConfigKey::create([
'key' => 'payment.stripe.secret',
'is_sensitive' => true,
]);
// Set sensitive value (auto-encrypted)
$config->set('payment.stripe.secret', 'sk_live_...');
// Retrieve (auto-decrypted)
$secret = $config->get('payment.stripe.secret');
```
### Validation
```php
// Validation runs automatically
try {
$config->set('api.rate_limit', 'invalid'); // Throws ValidationException
} catch (ValidationException $e) {
// Handle validation error
}
// Valid value
$config->set('api.rate_limit', 1000); // ✅ Passes validation
```
## Versioning
Track configuration changes with automatic versioning:
### Creating Versions
```php
use Core\Config\ConfigVersioning;
$versioning = app(ConfigVersioning::class);
// Create snapshot
$version = $versioning->createVersion('production', [
'description' => 'Pre-deployment snapshot',
'created_by' => auth()->id(),
]);
```
### Viewing Versions
```php
use Core\Config\Models\ConfigVersion;
// List all versions
$versions = ConfigVersion::query()
->where('profile', 'production')
->orderByDesc('created_at')
->get();
// Get specific version
$version = ConfigVersion::find($id);
// View snapshot
$snapshot = $version->snapshot; // ['cache.driver' => 'redis', ...]
```
### Rolling Back
```php
// Rollback to previous version
$versioning->rollback($version->id);
// Rollback with confirmation
if ($version->created_at->isToday()) {
$versioning->rollback($version->id);
}
```
### Comparing Versions
```php
use Core\Config\VersionDiff;
$diff = app(VersionDiff::class);
// Compare two versions
$changes = $diff->compare($oldVersion, $newVersion);
// Output:
[
'added' => ['cache.prefix' => 'app_'],
'modified' => ['cache.ttl' => ['old' => 3600, 'new' => 7200]],
'removed' => ['cache.legacy_driver'],
]
```
## Import & Export
### Exporting Configuration
```php
use Core\Config\ConfigExporter;
$exporter = app(ConfigExporter::class);
// Export active profile
$json = $exporter->export();
// Export specific profile
$json = $exporter->export('production');
// Export with metadata
$json = $exporter->export('production', [
'include_sensitive' => false, // Exclude secrets
'include_metadata' => true, // Include descriptions
]);
```
**Export Format:**
```json
{
"profile": "production",
"exported_at": "2026-01-26T12:00:00Z",
"config": {
"cache.driver": {
"value": "redis",
"description": "Cache driver",
"type": "string"
},
"cache.ttl": {
"value": 86400,
"description": "Cache TTL in seconds",
"type": "integer"
}
}
}
```
### Importing Configuration
```php
use Core\Config\ConfigService;
$config = app(ConfigService::class);
// Import from JSON
$result = $config->import($json, 'production');
// Import with merge strategy
$result = $config->import($json, 'production', [
'merge' => true, // Merge with existing
'overwrite' => false, // Don't overwrite existing
'validate' => true, // Validate before import
]);
```
**Import Result:**
```php
use Core\Config\ImportResult;
$result->imported; // ['cache.driver', 'cache.ttl']
$result->skipped; // ['cache.legacy']
$result->failed; // ['cache.invalid' => 'Validation failed']
```
### Console Commands
```bash
# Export configuration
php artisan config:export production --output=config.json
# Import configuration
php artisan config:import config.json --profile=staging
# Create version snapshot
php artisan config:version production --message="Pre-deployment"
```
## Configuration Providers
Create reusable configuration providers:
```php
<?php
namespace Mod\Blog\Config;
use Core\Config\Contracts\ConfigProvider;
class BlogConfigProvider implements ConfigProvider
{
public function provide(): array
{
return [
'blog.posts_per_page' => [
'value' => 10,
'description' => 'Posts per page',
'type' => 'integer',
'validation' => ['required', 'integer', 'min:1'],
],
'blog.allow_comments' => [
'value' => true,
'description' => 'Enable comments',
'type' => 'boolean',
],
];
}
}
```
**Register Provider:**
```php
use Core\Events\FrameworkBooted;
public function onFrameworkBooted(FrameworkBooted $event): void
{
$config = app(ConfigService::class);
$config->register(new BlogConfigProvider());
}
```
## Caching
Configuration is cached for performance:
```php
// Clear config cache
$config->invalidate();
// Clear specific key cache
$config->invalidate('cache.driver');
// Rebuild cache
$config->rebuild();
```
**Cache Strategy:**
- Uses `remember()` with 1-hour TTL
- Invalidated on config changes
- Per-profile cache keys
- Tagged for easy clearing
## Events
Configuration changes fire events:
```php
use Core\Config\Events\ConfigChanged;
use Core\Config\Events\ConfigInvalidated;
// Listen for changes
Event::listen(ConfigChanged::class, function ($event) {
Log::info('Config changed', [
'key' => $event->key,
'old' => $event->oldValue,
'new' => $event->newValue,
]);
});
// Listen for cache invalidation
Event::listen(ConfigInvalidated::class, function ($event) {
// Rebuild dependent caches
});
```
## Best Practices
### 1. Use Profiles for Environments
```php
// ✅ Good - environment-specific
$config->set('cache.driver', 'redis', [], 'production');
$config->set('cache.driver', 'array', [], 'testing');
// ❌ Bad - single value for all environments
$config->set('cache.driver', 'redis');
```
### 2. Mark Sensitive Data
```php
// ✅ Good - encrypted at rest
ConfigKey::create([
'key' => 'payment.api_key',
'is_sensitive' => true,
]);
// ❌ Bad - plaintext secrets
$config->set('payment.api_key', 'secret123');
```
### 3. Version Before Changes
```php
// ✅ Good - create snapshot first
$versioning->createVersion('production', [
'description' => 'Pre-cache-driver-change',
]);
$config->set('cache.driver', 'redis', [], 'production');
// ❌ Bad - no rollback point
$config->set('cache.driver', 'redis', [], 'production');
```
### 4. Validate Configuration
```php
// ✅ Good - validation rules
ConfigKey::create([
'key' => 'api.rate_limit',
'validation_rules' => ['required', 'integer', 'min:100', 'max:10000'],
]);
// ❌ Bad - no validation
$config->set('api.rate_limit', 'unlimited'); // Invalid!
```
## Testing Configuration
```php
use Tests\TestCase;
use Core\Config\ConfigService;
class ConfigTest extends TestCase
{
public function test_stores_configuration(): void
{
$config = app(ConfigService::class);
$config->set('test.key', 'value');
$this->assertEquals('value', $config->get('test.key'));
}
public function test_profile_isolation(): void
{
$config = app(ConfigService::class);
$config->set('cache.driver', 'redis', [], 'production');
$config->set('cache.driver', 'array', [], 'testing');
// Activate testing profile
ConfigProfile::where('name', 'testing')->first()->activate();
$this->assertEquals('array', $config->get('cache.driver'));
}
}
```
## Learn More
- [Module System →](/core/modules)
- [Multi-Tenancy →](/core/tenancy)

420
build/php/events.md Normal file
View file

@ -0,0 +1,420 @@
# Lifecycle Events
Core PHP Framework uses lifecycle events to coordinate module loading and system initialization. This event-driven architecture enables lazy loading and keeps modules decoupled.
## Event Flow
```mermaid
graph TD
A[Application Boot] --> B[WebRoutesRegistering]
A --> C[ApiRoutesRegistering]
A --> D[AdminPanelBooting]
A --> E[ClientRoutesRegistering]
A --> F[ConsoleBooting]
A --> G[McpToolsRegistering]
B --> H[FrameworkBooted]
C --> H
D --> H
E --> H
F --> H
G --> H
```
## Core Events
### WebRoutesRegistering
Fired when public web routes are being registered.
```php
<?php
namespace Mod\Blog;
use Core\Events\WebRoutesRegistering;
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('blog', __DIR__.'/Views');
$event->translations('blog', __DIR__.'/Lang');
$event->routes(function () {
require __DIR__.'/Routes/web.php';
});
}
}
```
**Available Methods:**
- `views(string $namespace, string $path)` - Register view namespace
- `translations(string $namespace, string $path)` - Register translations
- `routes(Closure $callback)` - Register routes
- `middleware(array $middleware)` - Add global middleware
### ApiRoutesRegistering
Fired when API routes are being registered.
```php
public function onApiRoutes(ApiRoutesRegistering $event): void
{
$event->routes(function () {
Route::middleware(['auth:sanctum', 'scope:posts:read'])
->get('/posts', [PostApiController::class, 'index']);
});
}
```
**Available Methods:**
- `routes(Closure $callback)` - Register API routes
- `middleware(array $middleware)` - Add API middleware
- `prefix(string $prefix)` - Set route prefix
- `version(string $version)` - Set API version
### AdminPanelBooting
Fired when admin panel is initializing.
```php
use Core\Events\AdminPanelBooting;
use Core\Front\Admin\Contracts\AdminMenuProvider;
public function onAdminPanel(AdminPanelBooting $event): void
{
$event->menu(new BlogMenuProvider());
$event->views('blog-admin', __DIR__.'/Views/Admin');
$event->livewire('blog', __DIR__.'/Livewire');
}
```
**Available Methods:**
- `menu(AdminMenuProvider $provider)` - Register menu provider
- `views(string $namespace, string $path)` - Register admin views
- `livewire(string $namespace, string $path)` - Register Livewire components
- `assets(string $path)` - Register frontend assets
### ClientRoutesRegistering
Fired when authenticated client routes are being registered.
```php
use Core\Events\ClientRoutesRegistering;
public function onClientRoutes(ClientRoutesRegistering $event): void
{
$event->routes(function () {
Route::middleware(['auth', 'verified'])
->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
});
}
```
### ConsoleBooting
Fired when Artisan console is initializing.
```php
use Core\Events\ConsoleBooting;
public function onConsole(ConsoleBooting $event): void
{
$event->commands([
PublishPostsCommand::class,
GenerateSitemapCommand::class,
]);
$event->schedule(function ($schedule) {
$schedule->command('posts:publish')
->hourly()
->withoutOverlapping();
});
}
```
**Available Methods:**
- `commands(array $commands)` - Register Artisan commands
- `schedule(Closure $callback)` - Define scheduled tasks
### McpToolsRegistering
Fired when MCP (Model Context Protocol) tools are being registered.
```php
use Core\Events\McpToolsRegistering;
use Mod\Blog\Mcp\BlogTools;
public function onMcpTools(McpToolsRegistering $event): void
{
$event->tool(new BlogTools());
}
```
**Available Methods:**
- `tool(object $tool)` - Register MCP tool
- `resource(string $type, Closure $callback)` - Register resource provider
- `prompt(string $name, Closure $callback)` - Register prompt template
### FrameworkBooted
Fired after all modules have loaded. Use for late initialization.
```php
use Core\Events\FrameworkBooted;
public function onFrameworkBooted(FrameworkBooted $event): void
{
// Run after all modules loaded
$this->registerPolicies();
$this->publishAssets();
}
```
## Custom Events
Create custom lifecycle events by extending `LifecycleEvent`:
```php
<?php
namespace Mod\Shop\Events;
use Core\Events\LifecycleEvent;
use Core\Events\Concerns\HasEventVersion;
class PaymentGatewaysRegistering extends LifecycleEvent
{
use HasEventVersion;
protected array $gateways = [];
public function gateway(string $name, string $class): void
{
$this->gateways[$name] = $class;
}
public function getGateways(): array
{
return $this->gateways;
}
public function version(): string
{
return '1.0.0';
}
}
```
**Usage in Module:**
```php
use Mod\Shop\Events\PaymentGatewaysRegistering;
class Boot
{
public static array $listens = [
PaymentGatewaysRegistering::class => 'onPaymentGateways',
];
public function onPaymentGateways(PaymentGatewaysRegistering $event): void
{
$event->gateway('stripe', StripeGateway::class);
$event->gateway('paypal', PayPalGateway::class);
}
}
```
## Event Versioning
Events can declare versions for backward compatibility:
```php
use Core\Events\Concerns\HasEventVersion;
class MyEvent extends LifecycleEvent
{
use HasEventVersion;
public function version(): string
{
return '2.1.0';
}
}
```
**Version Checking:**
```php
if (version_compare($event->version(), '2.0.0', '>=')) {
// Use v2 features
} else {
// Fallback for v1
}
```
## Lazy Loading
Modules only instantiate when their events fire:
```php
// ModuleRegistry registers lazy listeners
Event::listen(WebRoutesRegistering::class, function ($event) {
// Module instantiated only when event fires
$module = new \Mod\Blog\Boot();
$module->onWebRoutes($event);
});
```
**Benefits:**
- Faster boot times
- Lower memory usage
- Load only what's needed
- No unused module overhead
## Event Profiling
Profile listener execution in development:
```php
use Core\Events\ListenerProfiler;
// config/app.php
'providers' => [
// ...
ListenerProfiler::class, // Only in development
],
```
**Output:**
```
Lifecycle Event Performance:
- WebRoutesRegistering: 45ms (12 listeners)
- ApiRoutesRegistering: 23ms (8 listeners)
- AdminPanelBooting: 67ms (15 listeners)
```
## Best Practices
### 1. Keep Listeners Fast
```php
// ✅ Good - quick registration
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
// ❌ Bad - heavy processing
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Don't do expensive operations here!
$this->generateSitemap();
$this->warmCache();
}
```
### 2. Use Appropriate Events
```php
// ✅ Good - right event for the job
WebRoutesRegistering::class => 'onWebRoutes',
ConsoleBooting::class => 'onConsole',
// ❌ Bad - wrong event
WebRoutesRegistering::class => 'registerCommands', // Use ConsoleBooting!
```
### 3. Defer Heavy Work
```php
public function onFrameworkBooted(FrameworkBooted $event): void
{
// ✅ Good - queue heavy work
dispatch(new BuildSearchIndex());
// ❌ Bad - blocking
$this->buildSearchIndex(); // Takes 5 seconds!
}
```
### 4. Handle Missing Dependencies
```php
public function onAdminPanel(AdminPanelBooting $event): void
{
if (!class_exists(Livewire::class)) {
Log::warning('Livewire not installed, skipping components');
return;
}
$event->livewire('blog', __DIR__.'/Livewire');
}
```
## Testing Events
```php
use Tests\TestCase;
use Core\Events\WebRoutesRegistering;
class BlogBootTest extends TestCase
{
public function test_registers_routes(): void
{
$event = new WebRoutesRegistering();
$boot = new \Mod\Blog\Boot();
$boot->onWebRoutes($event);
$this->assertTrue(Route::has('blog.index'));
}
public function test_registers_views(): void
{
$event = new WebRoutesRegistering();
$boot = new \Mod\Blog\Boot();
$boot->onWebRoutes($event);
$this->assertTrue(
View::getFinder()->getHints()['blog'] ?? false
);
}
}
```
## Debugging Events
Enable event logging:
```php
// config/logging.php
'channels' => [
'lifecycle' => [
'driver' => 'single',
'path' => storage_path('logs/lifecycle.log'),
'level' => 'debug',
],
],
```
**Log Output:**
```
[2026-01-26 12:00:00] Firing: WebRoutesRegistering
[2026-01-26 12:00:00] Listener: Mod\Blog\Boot@onWebRoutes (12ms)
[2026-01-26 12:00:00] Listener: Mod\Shop\Boot@onWebRoutes (8ms)
```
## Learn More
- [Module System →](/core/modules)
- [Actions Pattern →](/core/actions)
- [Multi-Tenancy →](/core/tenancy)

View file

@ -0,0 +1,150 @@
# Getting Started
Welcome to the Core PHP Framework! This guide will help you understand what the framework is, when to use it, and how to get started.
## What is Core PHP?
Core PHP is a **modular monolith framework** for Laravel that provides:
- **Event-driven architecture** - Modules communicate via lifecycle events
- **Lazy loading** - Only load what you need when you need it
- **Multi-tenant isolation** - Built-in workspace scoping
- **Action patterns** - Testable, reusable business logic
- **Activity logging** - Audit trails out of the box
## When to Use Core PHP
### ✅ Good Fit
- **Multi-tenant SaaS applications** - Built-in workspace isolation
- **Growing monoliths** - Need structure without microservices complexity
- **Modular applications** - Clear module boundaries with lazy loading
- **API-first applications** - Comprehensive API package with OpenAPI docs
### ❌ Not a Good Fit
- **Simple CRUD apps** - May be overkill for basic applications
- **Existing large codebases** - Migration would be significant effort
- **Need for polyglot services** - Better suited for monolithic PHP apps
## Architecture Overview
```
┌─────────────────────────────────────────────┐
│ Application Bootstrap │
├─────────────────────────────────────────────┤
│ LifecycleEventProvider │
│ (fires WebRoutesRegistering, etc.) │
└──────────────┬──────────────────────────────┘
┌───────▼────────┐
│ ModuleRegistry │
│ (lazy loading) │
└───────┬─────────┘
┌───────▼────────────────┐
│ Module Boot Classes │
│ • Mod/Commerce/Boot.php │
│ • Mod/Billing/Boot.php │
│ • Mod/Analytics/Boot.php│
└─────────────────────────┘
```
Modules declare which events they're interested in:
```php
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => 'onAdmin',
];
}
```
The framework only instantiates modules when their events fire.
## Core Concepts
### 1. Lifecycle Events
Events fired during application bootstrap:
- `WebRoutesRegistering` - Public web routes
- `AdminPanelBooting` - Admin panel
- `ApiRoutesRegistering` - REST API
- `ClientRoutesRegistering` - Authenticated client routes
- `ConsoleBooting` - Artisan commands
- `FrameworkBooted` - Late initialization
### 2. Module System
Modules are self-contained feature bundles:
```
app/Mod/Commerce/
├── Boot.php # Module entry point
├── Actions/ # Business logic
├── Models/ # Eloquent models
├── Routes/ # Route files
├── Views/ # Blade templates
├── Migrations/ # Database migrations
└── config.php # Module configuration
```
### 3. Workspace Scoping
All tenant data is automatically scoped:
```php
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
class Product extends Model
{
use BelongsToWorkspace;
}
// Automatically filtered to current workspace
$products = Product::all();
```
### 4. Actions Pattern
Single-purpose business logic:
```php
use Core\Actions\Action;
class CreateOrder
{
use Action;
public function handle(User $user, array $data): Order
{
// Business logic here
}
}
// Usage
$order = CreateOrder::run($user, $validated);
```
## Next Steps
- [Installation →](./installation)
- [Configuration →](./configuration)
- [Quick Start →](./quick-start)
## Requirements
- **PHP** 8.2 or higher
- **Laravel** 11 or 12
- **Database** MySQL 8.0+, PostgreSQL 13+, or SQLite 3.35+
- **Composer** 2.0+
## Support
- 📖 [Documentation](https://docs.example.com)
- 💬 [GitHub Discussions](https://github.com/host-uk/core-php/discussions)
- 🐛 [Issue Tracker](https://github.com/host-uk/core-php/issues)
- 📧 [Email Support](mailto:support@host.uk.com)

273
build/php/index.md Normal file
View file

@ -0,0 +1,273 @@
# PHP Framework
The PHP framework provides the foundation for Host UK applications including the module system, lifecycle events, multi-tenancy, and shared utilities.
## Installation
```bash
composer require host-uk/core
```
## Quick Start
```php
<?php
namespace Mod\Example;
use Core\Events\WebRoutesRegistering;
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('example', __DIR__.'/Views');
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
}
```
## Key Features
### Foundation
- **[Module System](/core/modules)** - Auto-discover and lazy-load modules based on lifecycle events
- **[Lifecycle Events](/core/events)** - Event-driven extension points throughout the framework
- **[Actions Pattern](/core/actions)** - Single-purpose business logic classes
- **[Service Discovery](/core/services)** - Automatic service registration and dependency management
### Multi-Tenancy
- **[Workspaces & Namespaces](/core/tenancy)** - Workspace and namespace scoping for data isolation
- **[Workspace Caching](/core/tenancy#workspace-caching)** - Isolated cache management per workspace
- **[Context Resolution](/core/tenancy#context-resolution)** - Automatic workspace/namespace detection
### Data & Storage
- **[Configuration Management](/core/configuration)** - Multi-profile configuration with versioning and export/import
- **[Activity Logging](/core/activity)** - Track changes to models with automatic workspace scoping
- **[Seeder Discovery](/core/seeders)** - Automatic seeder discovery with dependency ordering
- **[CDN Integration](/core/cdn)** - Unified CDN interface for BunnyCDN and Cloudflare
### Content & Media
- **[Media Processing](/core/media)** - Image optimization, responsive images, and thumbnails
- **[Search](/core/search)** - Unified search interface across modules with analytics
- **[SEO Tools](/core/seo)** - SEO metadata generation, sitemaps, and structured data
### Security
- **[Security Headers](/core/security)** - Configurable security headers with CSP support
- **[Email Shield](/core/email-shield)** - Disposable email detection and validation
- **[Action Gate](/core/action-gate)** - Permission-based action authorization
- **[Blocklist Service](/core/security#blocklist)** - IP blocklist and rate limiting
### Utilities
- **[Input Sanitization](/core/security#sanitization)** - XSS protection and input cleaning
- **[Encryption](/core/security#encryption)** - Additional encryption utilities (HadesEncrypt)
- **[Translation Memory](/core/i18n)** - Translation management with fuzzy matching and ICU support
## Architecture
The Core package follows a modular monolith architecture with:
1. **Event-Driven Loading** - Modules are lazy-loaded based on lifecycle events
2. **Dependency Injection** - All services are resolved through Laravel's container
3. **Trait-Based Features** - Common functionality provided via traits (e.g., `LogsActivity`, `BelongsToWorkspace`)
4. **Multi-Tenancy First** - Workspace scoping is built into the foundation
## Artisan Commands
```bash
# Module Management
php artisan make:mod Blog
php artisan make:website Marketing
php artisan make:plug Stripe
# Configuration
php artisan config:export production
php artisan config:import production.json
php artisan config:version
# Maintenance
php artisan activity:prune --days=90
php artisan email-shield:prune --days=30
php artisan cache:warm
# SEO
php artisan seo:generate-sitemap
php artisan seo:audit-canonical
php artisan seo:test-structured-data
# Storage
php artisan storage:offload --disk=public
```
## Configuration
```php
// config/core.php
return [
'module_paths' => [
app_path('Core'),
app_path('Mod'),
app_path('Plug'),
],
'modules' => [
'auto_discover' => true,
'cache_enabled' => true,
],
'seeders' => [
'auto_discover' => true,
'paths' => [
'Mod/*/Database/Seeders',
'Core/*/Database/Seeders',
],
],
'activity' => [
'enabled' => true,
'retention_days' => 90,
'log_ip_address' => false,
],
'workspace_cache' => [
'enabled' => true,
'ttl' => 3600,
'use_tags' => true,
],
];
```
[View full configuration options →](/guide/configuration#core-configuration)
## Events
Core package dispatches these lifecycle events:
- `Core\Events\WebRoutesRegistering` - Public web routes
- `Core\Events\AdminPanelBooting` - Admin panel initialization
- `Core\Events\ApiRoutesRegistering` - REST API routes
- `Core\Events\ClientRoutesRegistering` - Authenticated client routes
- `Core\Events\ConsoleBooting` - Artisan commands
- `Core\Events\McpToolsRegistering` - MCP tools
- `Core\Events\FrameworkBooted` - Late-stage initialization
[Learn more about Lifecycle Events →](/core/events)
## Middleware
- `Core\Mod\Tenant\Middleware\RequireWorkspaceContext` - Ensure workspace is set
- `Core\Headers\SecurityHeaders` - Apply security headers
- `Core\Bouncer\BlocklistService` - IP blocklist
- `Core\Bouncer\Gate\ActionGateMiddleware` - Action authorization
## Global Helpers
```php
// Get current workspace
$workspace = workspace();
// Create activity log
activity()
->performedOn($model)
->log('action');
// Generate CDN URL
$url = cdn_url('path/to/asset.jpg');
// Get CSP nonce
$nonce = csp_nonce();
```
## Best Practices
### 1. Use Actions for Business Logic
```php
// ✅ Good
$post = CreatePost::run($data);
// ❌ Bad
$post = Post::create($data);
event(new PostCreated($post));
Cache::forget('posts');
```
### 2. Log Activity for Audit Trail
```php
class Post extends Model
{
use LogsActivity;
protected array $activityLogAttributes = ['title', 'status', 'published_at'];
}
```
### 3. Use Workspace Scoping
```php
class Post extends Model
{
use BelongsToWorkspace;
}
```
### 4. Leverage Module System
```php
// Create focused modules with clear boundaries
Mod/Blog/
Mod/Commerce/
Mod/Analytics/
```
## Testing
```php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Mod\Blog\Actions\CreatePost;
class CreatePostTest extends TestCase
{
public function test_creates_post(): void
{
$post = CreatePost::run([
'title' => 'Test Post',
'content' => 'Test content',
]);
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
]);
}
}
```
## Changelog
See [CHANGELOG.md](https://github.com/host-uk/core-php/blob/main/packages/core-php/changelog/2026/jan/features.md)
## License
EUPL-1.2
## Learn More
- [Module System →](/core/modules)
- [Lifecycle Events →](/core/events)
- [Multi-Tenancy →](/core/tenancy)
- [Configuration →](/core/configuration)
- [Activity Logging →](/core/activity)

283
build/php/installation.md Normal file
View file

@ -0,0 +1,283 @@
# Installation
This guide covers installing the Core PHP Framework in a new or existing Laravel application.
## Quick Start (Recommended)
The fastest way to get started is using the `core:new` command from any existing Core PHP installation:
```bash
php artisan core:new my-project
cd my-project
php artisan serve
```
This scaffolds a complete project with all Core packages pre-configured.
### Command Options
```bash
# Custom template
php artisan core:new my-api --template=host-uk/core-api-template
# Specific version
php artisan core:new my-app --branch=v1.0.0
# Skip automatic installation
php artisan core:new my-app --no-install
# Development mode (--prefer-source)
php artisan core:new my-app --dev
# Overwrite existing directory
php artisan core:new my-app --force
```
## From GitHub Template
You can also use the GitHub template directly:
1. Visit [host-uk/core-template](https://github.com/host-uk/core-template)
2. Click "Use this template"
3. Clone your new repository
4. Run `composer install && php artisan core:install`
## Manual Installation
For adding Core PHP to an existing Laravel project:
```bash
# Install Core PHP
composer require host-uk/core
# Install optional packages
composer require host-uk/core-admin # Admin panel
composer require host-uk/core-api # REST API
composer require host-uk/core-mcp # MCP tools
```
## Existing Laravel Project
Add to an existing Laravel 11+ or 12 application:
```bash
composer require host-uk/core
```
The service provider will be auto-discovered.
## Package Installation
Install individual packages as needed:
### Core Package (Required)
```bash
composer require host-uk/core
```
Provides:
- Event-driven module system
- Actions pattern
- Multi-tenancy
- Activity logging
- Seeder auto-discovery
### Admin Package (Optional)
```bash
composer require host-uk/core-admin
```
Provides:
- Livewire admin panel
- Global search
- Service management UI
- Form components
**Additional requirements:**
```bash
composer require livewire/livewire:"^3.0|^4.0"
composer require livewire/flux:"^2.0"
```
### API Package (Optional)
```bash
composer require host-uk/core-api
```
Provides:
- OpenAPI/Swagger documentation
- Rate limiting
- Webhook signing
- Secure API keys
### MCP Package (Optional)
```bash
composer require host-uk/core-mcp
```
Provides:
- Model Context Protocol tools
- Tool analytics
- SQL query validation
- MCP playground UI
## Publishing Configuration
Publish configuration files:
```bash
# Publish core config
php artisan vendor:publish --tag=core-config
# Publish API config (if installed)
php artisan vendor:publish --tag=api-config
# Publish MCP config (if installed)
php artisan vendor:publish --tag=mcp-config
```
## Database Setup
Run migrations:
```bash
php artisan migrate
```
This creates tables for:
- Workspaces and users
- API keys (if core-api installed)
- MCP analytics (if core-mcp installed)
- Activity logs (if spatie/laravel-activitylog installed)
## Optional Dependencies
### Activity Logging
For activity logging features:
```bash
composer require spatie/laravel-activitylog:"^4.8"
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate
```
### Feature Flags
For feature flag support:
```bash
composer require laravel/pennant:"^1.0"
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
```
## Verify Installation
Check that everything is installed correctly:
```bash
# Check installed packages
composer show | grep host-uk
# List available artisan commands
php artisan list make
# Should see:
# make:mod Create a new module
# make:website Create a new website module
# make:plug Create a new plugin
```
## Environment Configuration
Add to your `.env`:
```env
# Core Configuration
CORE_MODULE_DISCOVERY=true
CORE_STRICT_WORKSPACE_MODE=true
# API Configuration (if using core-api)
API_DOCS_ENABLED=true
API_DOCS_REQUIRE_AUTH=false
API_RATE_LIMIT_DEFAULT=60
# MCP Configuration (if using core-mcp)
MCP_ANALYTICS_ENABLED=true
MCP_QUOTA_ENABLED=true
MCP_DATABASE_CONNECTION=readonly
```
## Directory Structure
After installation, your project structure will look like:
```
your-app/
├── app/
│ ├── Core/ # Core modules (framework-level)
│ ├── Mod/ # Feature modules (your code)
│ ├── Website/ # Website modules
│ └── Plug/ # Plugins
├── config/
│ ├── core.php # Core configuration
│ ├── api.php # API configuration (optional)
│ └── mcp.php # MCP configuration (optional)
├── packages/ # Local package development (optional)
└── vendor/
└── host-uk/ # Installed packages
```
## Next Steps
- [Configuration →](./configuration)
- [Quick Start →](./quick-start)
- [Create Your First Module →](./quick-start#creating-a-module)
## Troubleshooting
### Service Provider Not Discovered
If the service provider isn't auto-discovered:
```bash
composer dump-autoload
php artisan package:discover --ansi
```
### Migration Errors
If migrations fail:
```bash
# Check database connection
php artisan db:show
# Run migrations with verbose output
php artisan migrate --verbose
```
### Module Discovery Issues
If modules aren't being discovered:
```bash
# Clear application cache
php artisan optimize:clear
# Verify module paths in config/core.php
php artisan config:show core.module_paths
```
## Minimum Requirements
- PHP 8.2+
- Laravel 11.0+ or 12.0+
- MySQL 8.0+ / PostgreSQL 13+ / SQLite 3.35+
- Composer 2.0+
- 128MB PHP memory limit (256MB recommended)

506
build/php/media.md Normal file
View file

@ -0,0 +1,506 @@
# Media Processing
Powerful media processing with image optimization, responsive images, lazy thumbnails, and CDN integration.
## Image Optimization
### Automatic Optimization
Images are automatically optimized on upload:
```php
use Core\Media\Image\ImageOptimizer;
$optimizer = app(ImageOptimizer::class);
// Optimize image
$optimizer->optimize($path);
// Returns optimized path with reduced file size
```
**Optimization Features:**
- Strip EXIF data (privacy)
- Lossless compression
- Format conversion (WebP/AVIF support)
- Quality adjustment
- Dimension constraints
### Configuration
```php
// config/media.php
return [
'optimization' => [
'enabled' => true,
'quality' => 85,
'max_width' => 2560,
'max_height' => 2560,
'strip_exif' => true,
'convert_to_webp' => true,
],
];
```
### Manual Optimization
```php
use Core\Media\Image\ImageOptimization;
$optimization = app(ImageOptimization::class);
// Optimize with custom quality
$optimization->optimize($path, quality: 90);
// Optimize and resize
$optimization->optimize($path, maxWidth: 1920, maxHeight: 1080);
// Get optimization stats
$stats = $optimization->getStats($path);
// ['original_size' => 2500000, 'optimized_size' => 890000, 'savings' => 64]
```
## Responsive Images
### Generating Responsive Images
```php
use Core\Media\Support\ImageResizer;
$resizer = app(ImageResizer::class);
// Generate multiple sizes
$sizes = $resizer->resize($originalPath, [
'thumbnail' => [150, 150],
'small' => [320, 240],
'medium' => [768, 576],
'large' => [1920, 1440],
]);
// Returns:
[
'thumbnail' => '/storage/images/photo-150x150.jpg',
'small' => '/storage/images/photo-320x240.jpg',
'medium' => '/storage/images/photo-768x576.jpg',
'large' => '/storage/images/photo-1920x1440.jpg',
]
```
### Responsive Image Tag
```blade
<picture>
<source
srcset="{{ cdn($image->large) }} 1920w,
{{ cdn($image->medium) }} 768w,
{{ cdn($image->small) }} 320w"
sizes="(max-width: 768px) 100vw, 50vw"
>
<img
src="{{ cdn($image->medium) }}"
alt="{{ $image->alt }}"
loading="lazy"
>
</picture>
```
### Modern Format Support
```php
use Core\Media\Image\ModernFormatSupport;
$formats = app(ModernFormatSupport::class);
// Check browser support
if ($formats->supportsWebP(request())) {
return cdn($image->webp);
}
if ($formats->supportsAVIF(request())) {
return cdn($image->avif);
}
return cdn($image->jpg);
```
**Blade Component:**
```blade
<x-responsive-image
:image="$post->featured_image"
sizes="(max-width: 768px) 100vw, 50vw"
loading="lazy"
/>
```
## Lazy Thumbnails
Generate thumbnails on-demand:
### Configuration
```php
// config/media.php
return [
'lazy_thumbnails' => [
'enabled' => true,
'cache_ttl' => 86400, // 24 hours
'allowed_sizes' => [
'thumbnail' => [150, 150],
'small' => [320, 240],
'medium' => [768, 576],
'large' => [1920, 1440],
],
],
];
```
### Generating Thumbnails
```php
use Core\Media\Thumbnail\LazyThumbnail;
// Generate thumbnail URL (not created until requested)
$url = lazy_thumbnail($originalPath, 'medium');
// Returns: /thumbnail/abc123/medium/photo.jpg
// Generate with custom dimensions
$url = lazy_thumbnail($originalPath, [width: 500, height: 300]);
```
### Thumbnail Controller
Thumbnails are generated on first request:
```
GET /thumbnail/{hash}/{size}/{filename}
```
**Process:**
1. Check if thumbnail exists in cache
2. If not, generate from original
3. Store in cache/CDN
4. Serve to client
**Benefits:**
- No upfront processing
- Storage efficient
- CDN-friendly
- Automatic cleanup
## Media Conversions
Define custom media conversions:
```php
<?php
namespace Mod\Blog\Media;
use Core\Media\Abstracts\MediaConversion;
class PostThumbnailConversion extends MediaConversion
{
public function name(): string
{
return 'post-thumbnail';
}
public function apply(string $path): string
{
return $this->resize($path, 400, 300)
->optimize(quality: 85)
->sharpen()
->save();
}
}
```
**Register Conversion:**
```php
use Core\Events\FrameworkBooted;
use Core\Media\Conversions\MediaImageResizerConversion;
public function onFrameworkBooted(FrameworkBooted $event): void
{
MediaImageResizerConversion::register(
new PostThumbnailConversion()
);
}
```
**Apply Conversion:**
```php
use Core\Media\Jobs\ProcessMediaConversion;
// Queue conversion
ProcessMediaConversion::dispatch($media, 'post-thumbnail');
// Synchronous conversion
$converted = $media->convert('post-thumbnail');
```
## EXIF Data
### Stripping EXIF
Remove privacy-sensitive metadata:
```php
use Core\Media\Image\ExifStripper;
$stripper = app(ExifStripper::class);
// Strip all EXIF data
$stripper->strip($imagePath);
// Strip specific tags
$stripper->strip($imagePath, preserve: [
'orientation', // Keep orientation
'copyright', // Keep copyright
]);
```
**Auto-strip on Upload:**
```php
// config/media.php
return [
'optimization' => [
'strip_exif' => true, // Default: strip everything
'preserve_exif' => ['orientation'], // Keep these tags
],
];
```
### Reading EXIF
```php
use Intervention\Image\ImageManager;
$manager = app(ImageManager::class);
$image = $manager->read($path);
$exif = $image->exif();
$camera = $exif->get('Model'); // Camera model
$date = $exif->get('DateTimeOriginal'); // Photo date
$gps = $exif->get('GPSLatitude'); // GPS coordinates (privacy risk!)
```
## CDN Integration
### Uploading to CDN
```php
use Core\Cdn\Services\BunnyStorageService;
$cdn = app(BunnyStorageService::class);
// Upload file
$cdnPath = $cdn->upload($localPath, 'images/photo.jpg');
// Upload with public URL
$url = $cdn->uploadAndGetUrl($localPath, 'images/photo.jpg');
```
### CDN Helper
```blade
{{-- Blade template --}}
<img src="{{ cdn('images/photo.jpg') }}" alt="Photo">
{{-- With transformation --}}
<img src="{{ cdn('images/photo.jpg', ['width' => 800, 'quality' => 85]) }}" alt="Photo">
```
### Purging CDN Cache
```php
use Core\Cdn\Services\FluxCdnService;
$cdn = app(FluxCdnService::class);
// Purge single file
$cdn->purge('/images/photo.jpg');
// Purge multiple files
$cdn->purge([
'/images/photo.jpg',
'/images/thumbnail.jpg',
]);
// Purge entire directory
$cdn->purge('/images/*');
```
## Progress Tracking
Track conversion progress:
```php
use Core\Media\Events\ConversionProgress;
// Listen for progress
Event::listen(ConversionProgress::class, function ($event) {
echo "Processing: {$event->percentage}%\n";
echo "Step: {$event->currentStep}/{$event->totalSteps}\n";
});
```
**With Livewire:**
```php
class MediaUploader extends Component
{
public $progress = 0;
protected $listeners = ['conversionProgress' => 'updateProgress'];
public function updateProgress($percentage)
{
$this->progress = $percentage;
}
public function render()
{
return view('livewire.media-uploader');
}
}
```
```blade
<div>
@if($progress > 0)
<div class="progress-bar">
<div style="width: {{ $progress }}%"></div>
</div>
<p>Processing: {{ $progress }}%</p>
@endif
</div>
```
## Queued Processing
Process media in background:
```php
use Core\Media\Jobs\GenerateThumbnail;
use Core\Media\Jobs\ProcessMediaConversion;
// Queue thumbnail generation
GenerateThumbnail::dispatch($media, 'large');
// Queue conversion
ProcessMediaConversion::dispatch($media, 'optimized');
// Chain jobs
GenerateThumbnail::dispatch($media, 'large')
->chain([
new ProcessMediaConversion($media, 'watermark'),
new ProcessMediaConversion($media, 'optimize'),
]);
```
## Best Practices
### 1. Optimize on Upload
```php
// ✅ Good - optimize immediately
public function store(Request $request)
{
$path = $request->file('image')->store('images');
$optimizer = app(ImageOptimizer::class);
$optimizer->optimize(storage_path("app/{$path}"));
return $path;
}
// ❌ Bad - serve unoptimized images
public function store(Request $request)
{
return $request->file('image')->store('images');
}
```
### 2. Use Lazy Thumbnails
```php
// ✅ Good - generate on-demand
<img src="{{ lazy_thumbnail($image->path, 'medium') }}">
// ❌ Bad - generate all sizes upfront
$resizer->resize($path, [
'thumbnail' => [150, 150],
'small' => [320, 240],
'medium' => [768, 576],
'large' => [1920, 1440],
'xlarge' => [2560, 1920],
]); // Slow upload, wasted storage
```
### 3. Strip EXIF Data
```php
// ✅ Good - protect privacy
$stripper->strip($imagePath);
// ❌ Bad - leak GPS coordinates, camera info
// (no stripping)
```
### 4. Use CDN for Assets
```php
// ✅ Good - CDN delivery
<img src="{{ cdn($image->path) }}">
// ❌ Bad - serve from origin
<img src="{{ Storage::url($image->path) }}">
```
## Testing
```php
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Core\Media\Image\ImageOptimizer;
class MediaTest extends TestCase
{
public function test_optimizes_uploaded_image(): void
{
$file = UploadedFile::fake()->image('photo.jpg', 2000, 2000);
$path = $file->store('test');
$fullPath = storage_path("app/{$path}");
$originalSize = filesize($fullPath);
$optimizer = app(ImageOptimizer::class);
$optimizer->optimize($fullPath);
$optimizedSize = filesize($fullPath);
$this->assertLessThan($originalSize, $optimizedSize);
}
public function test_generates_lazy_thumbnail(): void
{
$path = UploadedFile::fake()->image('photo.jpg')->store('test');
$url = lazy_thumbnail($path, 'medium');
$this->assertStringContainsString('/thumbnail/', $url);
}
}
```
## Learn More
- [CDN Integration →](/core/cdn)
- [Configuration →](/core/configuration)

488
build/php/modules.md Normal file
View file

@ -0,0 +1,488 @@
# Module System
The module system provides automatic discovery and lazy loading of modules based on lifecycle events. Modules are self-contained units of functionality that can hook into the framework at specific points.
## Overview
Traditional Laravel applications use service providers which are all loaded on every request. The Core module system:
- **Auto-discovers** modules by scanning directories
- **Lazy-loads** modules only when their events fire
- **Caches** module registry for performance
- **Supports** multiple module types (Mod, Plug, Website)
## Creating a Module
### Using Artisan
```bash
# Create a standard module
php artisan make:mod Blog
# Create a website module
php artisan make:website Marketing
# Create a plugin module
php artisan make:plug Stripe
```
### Manual Creation
Create a `Boot.php` file in your module directory:
```php
<?php
namespace Mod\Blog;
use Core\Events\WebRoutesRegistering;
use Core\Events\AdminPanelBooting;
use Core\Events\ConsoleBooting;
class Boot
{
/**
* Events this module listens to
*/
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => 'onAdminPanel',
ConsoleBooting::class => 'onConsole',
];
/**
* Register public web routes
*/
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->views('blog', __DIR__.'/Views');
$event->routes(fn () => require __DIR__.'/Routes/web.php');
}
/**
* Register admin panel routes and menus
*/
public function onAdminPanel(AdminPanelBooting $event): void
{
$event->menu('blog', [
'label' => 'Blog',
'icon' => 'newspaper',
'route' => 'admin.blog.index',
'order' => 20,
]);
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
}
/**
* Register console commands
*/
public function onConsole(ConsoleBooting $event): void
{
$event->commands([
Commands\PublishPostsCommand::class,
Commands\ImportPostsCommand::class,
]);
}
}
```
## Directory Structure
```
Mod/Blog/
├── Boot.php # Module bootstrap
├── Actions/ # Business logic
│ ├── CreatePost.php
│ ├── UpdatePost.php
│ └── DeletePost.php
├── Controllers/
│ ├── Web/
│ │ └── PostController.php
│ └── Admin/
│ └── PostController.php
├── Models/
│ ├── Post.php
│ └── Category.php
├── Routes/
│ ├── web.php
│ ├── admin.php
│ └── api.php
├── Views/
│ ├── web/
│ └── admin/
├── Database/
│ ├── Migrations/
│ ├── Factories/
│ └── Seeders/
├── Tests/
│ ├── Feature/
│ └── Unit/
└── Lang/
└── en_GB/
```
## Lifecycle Events
Modules can hook into these lifecycle events:
### WebRoutesRegistering
Register public-facing web routes:
```php
public function onWebRoutes(WebRoutesRegistering $event): void
{
// Register views
$event->views('blog', __DIR__.'/Views');
// Register translations
$event->lang('blog', __DIR__.'/Lang');
// Register routes
$event->routes(function () {
Route::get('/blog', [PostController::class, 'index']);
Route::get('/blog/{slug}', [PostController::class, 'show']);
});
}
```
### AdminPanelBooting
Register admin panel routes, menus, and widgets:
```php
public function onAdminPanel(AdminPanelBooting $event): void
{
// Register admin menu
$event->menu('blog', [
'label' => 'Blog',
'icon' => 'newspaper',
'route' => 'admin.blog.index',
'order' => 20,
'children' => [
['label' => 'Posts', 'route' => 'admin.blog.posts'],
['label' => 'Categories', 'route' => 'admin.blog.categories'],
],
]);
// Register routes
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
}
```
### ApiRoutesRegistering
Register REST API endpoints:
```php
public function onApiRoutes(ApiRoutesRegistering $event): void
{
$event->routes(function () {
Route::get('/posts', [Api\PostController::class, 'index']);
Route::post('/posts', [Api\PostController::class, 'store']);
Route::get('/posts/{id}', [Api\PostController::class, 'show']);
});
}
```
### ClientRoutesRegistering
Register authenticated client routes:
```php
public function onClientRoutes(ClientRoutesRegistering $event): void
{
$event->routes(function () {
Route::get('/dashboard/posts', [Client\PostController::class, 'index']);
Route::post('/dashboard/posts', [Client\PostController::class, 'store']);
});
}
```
### ConsoleBooting
Register Artisan commands:
```php
public function onConsole(ConsoleBooting $event): void
{
$event->commands([
Commands\PublishPostsCommand::class,
Commands\GenerateSitemapCommand::class,
]);
$event->schedule(function (Schedule $schedule) {
$schedule->command('blog:publish-scheduled')
->everyFiveMinutes();
});
}
```
### McpToolsRegistering
Register MCP (Model Context Protocol) tools:
```php
public function onMcpTools(McpToolsRegistering $event): void
{
$event->tool('blog:create-post', Tools\CreatePostTool::class);
$event->tool('blog:list-posts', Tools\ListPostsTool::class);
}
```
### FrameworkBooted
Late-stage initialization after all modules loaded:
```php
public function onFrameworkBooted(FrameworkBooted $event): void
{
// Register macros, observers, policies, etc.
Post::observe(PostObserver::class);
Builder::macro('published', function () {
return $this->where('status', 'published')
->where('published_at', '<=', now());
});
}
```
## Module Discovery
The framework automatically scans these directories:
```php
// config/core.php
'module_paths' => [
app_path('Core'), // Core modules
app_path('Mod'), // Standard modules
app_path('Website'), // Website modules
app_path('Plug'), // Plugin modules
],
```
### Custom Namespaces
Map custom paths to namespaces:
```php
use Core\Module\ModuleScanner;
$scanner = app(ModuleScanner::class);
$scanner->setNamespaceMap([
'/Extensions' => 'Extensions\\',
'/Custom' => 'Custom\\Modules\\',
]);
```
## Lazy Loading
Modules are only instantiated when their events fire:
1. **Scan Phase** - `ModuleScanner` finds all `Boot.php` files
2. **Registry Phase** - `ModuleRegistry` wires lazy listeners
3. **Event Phase** - Event fires, `LazyModuleListener` instantiates module
4. **Execution Phase** - Module method is called
**Performance Benefits:**
- Modules not used in CLI don't load in CLI
- Admin modules don't load on public requests
- API modules don't load on web requests
## Module Registry
View registered modules and their listeners:
```php
use Core\Module\ModuleRegistry;
$registry = app(ModuleRegistry::class);
// Get all registered modules
$modules = $registry->all();
// Get modules for specific event
$webModules = $registry->forEvent(WebRoutesRegistering::class);
```
## Module Cache
Module discovery is cached for performance:
```bash
# Clear module cache
php artisan cache:clear
# Or specifically
php artisan optimize:clear
```
**Cache Location:** `bootstrap/cache/modules.php`
## Module Dependencies
Modules can declare dependencies using service discovery:
```php
use Core\Service\Contracts\ServiceDefinition;
use Core\Service\Contracts\ServiceDependency;
class Boot implements ServiceDefinition
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
public function getServiceName(): string
{
return 'blog';
}
public function getServiceVersion(): string
{
return '1.0.0';
}
public function getDependencies(): array
{
return [
new ServiceDependency('media', '>=1.0'),
new ServiceDependency('cdn', '>=2.0'),
];
}
}
```
## Testing Modules
### Feature Tests
```php
<?php
namespace Mod\Blog\Tests\Feature;
use Tests\TestCase;
use Mod\Blog\Actions\CreatePost;
class PostCreationTest extends TestCase
{
public function test_creates_post(): void
{
$post = CreatePost::run([
'title' => 'Test Post',
'content' => 'Content here',
]);
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
]);
$this->get("/blog/{$post->slug}")
->assertOk()
->assertSee('Test Post');
}
}
```
### Unit Tests
```php
<?php
namespace Mod\Blog\Tests\Unit;
use Tests\TestCase;
use Mod\Blog\Boot;
use Core\Events\WebRoutesRegistering;
class BootTest extends TestCase
{
public function test_registers_web_routes(): void
{
$event = new WebRoutesRegistering();
$boot = new Boot();
$boot->onWebRoutes($event);
$this->assertTrue($event->hasRoutes());
}
}
```
## Best Practices
### 1. Keep Modules Focused
```php
// ✅ Good - focused modules
Mod/Blog/
Mod/Comments/
Mod/Analytics/
// ❌ Bad - monolithic module
Mod/Everything/
```
### 2. Use Proper Namespacing
```php
// ✅ Good
namespace Mod\Blog\Controllers\Web;
// ❌ Bad
namespace App\Http\Controllers;
```
### 3. Register Dependencies
```php
// ✅ Good - declare dependencies
public function getDependencies(): array
{
return [
new ServiceDependency('media', '>=1.0'),
];
}
```
### 4. Only Hook Necessary Events
```php
// ✅ Good - only web routes
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
];
// ❌ Bad - hooks everything
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => 'onAdminPanel',
ApiRoutesRegistering::class => 'onApiRoutes',
// ... (when you don't need them all)
];
```
### 5. Use Actions for Business Logic
```php
// ✅ Good
$post = CreatePost::run($data);
// ❌ Bad - logic in controller
public function store(Request $request)
{
$post = Post::create($request->all());
event(new PostCreated($post));
Cache::forget('posts');
return redirect()->route('posts.show', $post);
}
```
## Learn More
- [Lifecycle Events →](/core/events)
- [Actions Pattern →](/core/actions)
- [Service Discovery →](/core/services)
- [Architecture Overview →](/architecture/module-system)

906
build/php/namespaces.md Normal file
View file

@ -0,0 +1,906 @@
# Namespaces & Entitlements
Core PHP Framework provides a sophisticated namespace and entitlements system for flexible multi-tenant SaaS applications. Namespaces provide universal tenant boundaries, while entitlements control feature access and usage limits.
## Overview
### The Problem
Traditional multi-tenant systems force a choice:
**Option A: User Ownership**
- Individual users own resources
- No team collaboration
- Billing per user
**Option B: Workspace Ownership**
- Teams own resources via workspaces
- Can't have personal resources
- Billing per workspace
Both approaches are too rigid for modern SaaS:
- **Agencies** need separate namespaces per client
- **Freelancers** want personal AND client resources
- **White-label operators** need brand isolation
- **Enterprise teams** need department-level isolation
### The Solution: Namespaces
Namespaces provide a **polymorphic ownership boundary** where resources belong to a namespace, and namespaces can be owned by either Users or Workspaces.
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ User ────┬──→ Namespace (Personal) ──→ Resources │
│ │ │
│ └──→ Workspace ──→ Namespace (Client A) ──→ Res │
│ └──→ Namespace (Client B) ──→ Res │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Benefits:**
- Users can have personal namespaces
- Workspaces can have multiple namespaces (one per client)
- Clean billing boundaries
- Complete resource isolation
- Flexible permission models
## Namespace Model
### Structure
```php
Namespace {
id: int
uuid: string // Public identifier
name: string // Display name
slug: string // URL-safe identifier
description: ?string
icon: ?string
color: ?string
owner_type: string // User::class or Workspace::class
owner_id: int
workspace_id: ?int // Billing context (optional)
settings: ?json
is_default: bool // User's default namespace
is_active: bool
sort_order: int
}
```
### Ownership Patterns
#### Personal Namespace (User-Owned)
Individual user owns namespace for personal resources:
```php
$namespace = Namespace_::create([
'name' => 'Personal',
'owner_type' => User::class,
'owner_id' => $user->id,
'workspace_id' => $user->defaultHostWorkspace()->id, // For billing
'is_default' => true,
]);
```
**Use Cases:**
- Personal projects
- Individual freelancer work
- Testing/development environments
#### Agency Namespace (Workspace-Owned)
Workspace owns namespace for client/project isolation:
```php
$namespace = Namespace_::create([
'name' => 'Client: Acme Corp',
'slug' => 'acme-corp',
'owner_type' => Workspace::class,
'owner_id' => $workspace->id,
'workspace_id' => $workspace->id, // Same workspace for billing
]);
```
**Use Cases:**
- Agency client projects
- White-label deployments
- Department/team isolation
#### White-Label Namespace
SaaS operator creates namespaces for customers:
```php
$namespace = Namespace_::create([
'name' => 'Customer Instance',
'owner_type' => User::class, // Customer user owns it
'owner_id' => $customerUser->id,
'workspace_id' => $operatorWorkspace->id, // Operator billed
]);
```
**Use Cases:**
- White-label SaaS
- Reseller programs
- Managed services
## Using Namespaces
### Model Setup
Add namespace scoping to models:
```php
<?php
namespace Mod\Blog\Models;
use Illuminate\Database\Eloquent\Model;
use Core\Mod\Tenant\Concerns\BelongsToNamespace;
class Page extends Model
{
use BelongsToNamespace;
protected $fillable = ['title', 'content', 'slug'];
}
```
**Migration:**
```php
Schema::create('pages', function (Blueprint $table) {
$table->id();
$table->foreignId('namespace_id')
->constrained('namespaces')
->cascadeOnDelete();
$table->string('title');
$table->text('content');
$table->string('slug');
$table->timestamps();
$table->index(['namespace_id', 'created_at']);
});
```
### Automatic Scoping
The `BelongsToNamespace` trait automatically handles scoping:
```php
// Queries automatically scoped to current namespace
$pages = Page::ownedByCurrentNamespace()->get();
// Create automatically assigns namespace_id
$page = Page::create([
'title' => 'Example Page',
'content' => 'Content...',
// namespace_id added automatically
]);
// Can't access pages from other namespaces
$page = Page::find(999); // null if belongs to different namespace
```
### Namespace Context
#### Middleware Resolution
```php
// routes/web.php
Route::middleware(['auth', 'namespace'])
->group(function () {
Route::get('/pages', [PageController::class, 'index']);
});
```
The `ResolveNamespace` middleware sets current namespace from:
1. Query parameter: `?namespace=uuid`
2. Request header: `X-Namespace: uuid`
3. Session: `current_namespace_uuid`
4. User's default namespace
#### Manual Context
```php
use Core\Mod\Tenant\Services\NamespaceService;
$namespaceService = app(NamespaceService::class);
// Get current namespace
$current = $namespaceService->current();
// Set current namespace
$namespaceService->setCurrent($namespace);
// Get all accessible namespaces
$namespaces = $namespaceService->accessibleByCurrentUser();
// Group by ownership
$grouped = $namespaceService->groupedForCurrentUser();
// [
// 'personal' => Collection, // User-owned
// 'workspaces' => [ // Workspace-owned
// ['workspace' => Workspace, 'namespaces' => Collection],
// ...
// ]
// ]
```
### Namespace Switcher UI
Provide namespace switching in your UI:
```blade
<div class="namespace-switcher">
<x-dropdown>
<x-slot:trigger>
{{ $currentNamespace->name }}
</x-slot>
@foreach($personalNamespaces as $ns)
<x-dropdown-item href="?namespace={{ $ns->uuid }}">
{{ $ns->name }}
</x-dropdown-item>
@endforeach
@foreach($workspaceNamespaces as $group)
<x-dropdown-header>{{ $group['workspace']->name }}</x-dropdown-header>
@foreach($group['namespaces'] as $ns)
<x-dropdown-item href="?namespace={{ $ns->uuid }}">
{{ $ns->name }}
</x-dropdown-item>
@endforeach
@endforeach
</x-dropdown>
</div>
```
### API Integration
Include namespace in API requests:
```bash
# Header-based
curl -H "X-Namespace: uuid-here" \
-H "Authorization: Bearer sk_live_..." \
https://api.example.com/v1/pages
# Query parameter
curl "https://api.example.com/v1/pages?namespace=uuid-here" \
-H "Authorization: Bearer sk_live_..."
```
## Entitlements System
Entitlements control **what users can do** within their namespaces. The system answers: *"Can this namespace perform this action?"*
### Core Concepts
#### Packages
Bundles of features with defined limits:
```php
Package {
id: int
code: string // 'social-creator', 'bio-pro'
name: string
is_base_package: bool // Only one base package per namespace
is_stackable: bool // Can have multiple addon packages
is_active: bool
is_public: bool // Shown in pricing page
}
```
**Types:**
- **Base Package**: Core subscription (e.g., "Pro Plan")
- **Add-on Package**: Stackable extras (e.g., "Extra Storage")
#### Features
Capabilities or limits that can be granted:
```php
Feature {
id: int
code: string // 'social.accounts', 'ai.credits'
name: string
type: enum // boolean, limit, unlimited
reset_type: enum // none, monthly, rolling
rolling_window_days: ?int
parent_feature_id: ?int // For hierarchical limits
category: string // 'social', 'ai', 'storage'
}
```
**Feature Types:**
| Type | Behavior | Example |
|------|----------|---------|
| **Boolean** | On/off access gate | `tier.apollo`, `host.social` |
| **Limit** | Numeric cap on usage | `social.accounts: 5`, `ai.credits: 100` |
| **Unlimited** | No cap | `social.posts: unlimited` |
**Reset Types:**
| Reset Type | Behavior | Example |
|------------|----------|---------|
| **None** | Usage accumulates forever | Account limits |
| **Monthly** | Resets at billing cycle start | API requests per month |
| **Rolling** | Rolling window (e.g., last 30 days) | Posts per day |
#### Hierarchical Features (Pools)
Child features share a parent's limit pool:
```
host.storage.total (1000 MB) ← Parent pool
├── host.cdn ← Draws from parent
├── bio.cdn ← Draws from parent
└── social.cdn ← Draws from parent
```
**Configuration:**
```php
Feature::create([
'code' => 'host.storage.total',
'name' => 'Total Storage',
'type' => 'limit',
'reset_type' => 'none',
]);
Feature::create([
'code' => 'bio.cdn',
'name' => 'Bio Link Storage',
'type' => 'limit',
'parent_feature_id' => $parentFeature->id, // Shares pool
]);
```
### Entitlement Checks
Use the entitlement service to check permissions:
```php
use Core\Mod\Tenant\Services\EntitlementService;
$entitlements = app(EntitlementService::class);
// Check if namespace can use feature
$result = $entitlements->can($namespace, 'social.accounts', quantity: 3);
if ($result->isDenied()) {
return back()->with('error', $result->getMessage());
}
// Proceed with action...
// Record usage
$entitlements->recordUsage($namespace, 'social.accounts', quantity: 1);
```
### Entitlement Result
The `EntitlementResult` object provides complete context:
```php
$result = $entitlements->can($namespace, 'ai.credits', quantity: 10);
// Status checks
$result->isAllowed(); // true/false
$result->isDenied(); // true/false
$result->isUnlimited(); // true if unlimited
// Limits
$result->limit; // 100
$result->used; // 75
$result->remaining; // 25
// Percentage
$result->getUsagePercentage(); // 75.0
$result->isNearLimit(); // true if > 80%
// Denial reason
$result->getMessage(); // "Exceeded limit for ai.credits"
```
### Usage Tracking
Record consumption after successful actions:
```php
$entitlements->recordUsage(
namespace: $namespace,
featureCode: 'ai.credits',
quantity: 10,
user: $user, // Optional: who triggered it
metadata: [ // Optional: context
'model' => 'claude-3',
'tokens' => 1500,
]
);
```
**Database Schema:**
```php
usage_records {
id: int
namespace_id: int
feature_id: int
workspace_id: ?int // For workspace-level aggregation
user_id: ?int
quantity: int
metadata: ?json
created_at: timestamp
}
```
### Boosts
Temporary or permanent additions to limits:
```php
Boost {
id: int
namespace_id: int
feature_id: int
boost_type: enum // add_limit, enable, unlimited
duration_type: enum // cycle_bound, duration, permanent
limit_value: ?int // Amount to add
consumed_quantity: int // How much used
expires_at: ?timestamp
status: enum // active, exhausted, expired
}
```
**Use Cases:**
- One-time credit top-ups
- Promotional extras
- Beta access grants
- Temporary unlimited access
**Example:**
```php
// Give 1000 bonus AI credits
Boost::create([
'namespace_id' => $namespace->id,
'feature_id' => $aiCreditsFeature->id,
'boost_type' => 'add_limit',
'duration_type' => 'cycle_bound', // Expires at billing cycle end
'limit_value' => 1000,
]);
```
### Package Assignment
Namespaces subscribe to packages:
```php
NamespacePackage {
id: int
namespace_id: int
package_id: int
status: enum // active, suspended, cancelled, expired
starts_at: timestamp
expires_at: ?timestamp
billing_cycle_anchor: timestamp
}
```
**Provision Package:**
```php
$entitlements->provisionPackage(
namespace: $namespace,
package: $package,
startsAt: now(),
expiresAt: now()->addMonth(),
);
```
**Package Features:**
Features are attached to packages with specific limits:
```php
// Package definition
$package = Package::find($packageId);
// Attach features with limits
$package->features()->attach($feature->id, [
'limit_value' => 5, // This package grants 5 accounts
]);
// Multiple features
$package->features()->sync([
$socialAccountsFeature->id => ['limit_value' => 5],
$aiCreditsFeature->id => ['limit_value' => 100],
$storageFeature->id => ['limit_value' => 1000], // MB
]);
```
## Usage Dashboard
Display usage stats to users:
```php
$summary = $entitlements->getUsageSummary($namespace);
// Returns array grouped by category:
[
'social' => [
[
'feature' => Feature,
'limit' => 5,
'used' => 3,
'remaining' => 2,
'percentage' => 60.0,
'is_unlimited' => false,
],
...
],
'ai' => [...],
]
```
**UI Example:**
```blade
@foreach($summary as $category => $features)
<div class="category">
<h3>{{ ucfirst($category) }}</h3>
@foreach($features as $item)
<div class="feature-usage">
<div class="feature-name">
{{ $item['feature']->name }}
</div>
@if($item['is_unlimited'])
<div class="badge">Unlimited</div>
@else
<div class="progress-bar">
<div class="progress-fill"
style="width: {{ $item['percentage'] }}%"
class="{{ $item['percentage'] > 80 ? 'text-red-600' : 'text-green-600' }}">
</div>
</div>
<div class="usage-text">
{{ $item['used'] }} / {{ $item['limit'] }}
({{ number_format($item['percentage'], 1) }}%)
</div>
@endif
</div>
@endforeach
</div>
@endforeach
```
## Billing Integration
### Billing Context
Namespaces use `workspace_id` for billing aggregation:
```php
// Get billing workspace
$billingWorkspace = $namespace->getBillingContext();
// User-owned namespace → User's default workspace
// Workspace-owned namespace → Owner workspace
// Explicit workspace_id → That workspace
```
### Commerce Integration
Link subscriptions to namespace packages:
```php
// When subscription created
event(new SubscriptionCreated($subscription));
// Listener provisions package
$entitlements->provisionPackage(
namespace: $subscription->namespace,
package: $subscription->package,
startsAt: $subscription->starts_at,
expiresAt: $subscription->expires_at,
);
// When subscription renewed
$namespacePackage->update([
'expires_at' => $subscription->next_billing_date,
'billing_cycle_anchor' => now(),
]);
// Expire cycle-bound boosts
Boost::where('namespace_id', $namespace->id)
->where('duration_type', 'cycle_bound')
->update(['status' => 'expired']);
```
### External Billing Systems
API endpoints for external billing (Blesta, Stripe, etc.):
```bash
# Provision package
POST /api/v1/entitlements
{
"namespace_uuid": "uuid",
"package_code": "social-creator",
"starts_at": "2026-01-01T00:00:00Z",
"expires_at": "2026-02-01T00:00:00Z"
}
# Suspend package
POST /api/v1/entitlements/{id}/suspend
# Cancel package
POST /api/v1/entitlements/{id}/cancel
# Renew package
POST /api/v1/entitlements/{id}/renew
{
"expires_at": "2026-03-01T00:00:00Z"
}
# Check entitlements
GET /api/v1/entitlements/check
?namespace=uuid
&feature=social.accounts
&quantity=1
```
## Audit Logging
All entitlement changes are logged:
```php
EntitlementLog {
id: int
namespace_id: int
workspace_id: ?int
action: enum // package_provisioned, boost_expired, etc.
source: enum // blesta, commerce, admin, system, api
user_id: ?int
data: json // Context about the change
created_at: timestamp
}
```
**Actions:**
- `package_provisioned`, `package_suspended`, `package_cancelled`
- `boost_provisioned`, `boost_exhausted`, `boost_expired`
- `usage_recorded`, `usage_denied`
**Retrieve logs:**
```php
$logs = EntitlementLog::where('namespace_id', $namespace->id)
->latest()
->paginate(20);
```
## Feature Seeder
Define features in seeders:
```php
<?php
namespace Mod\Tenant\Database\Seeders;
use Illuminate\Database\Seeder;
use Core\Mod\Tenant\Models\Feature;
class FeatureSeeder extends Seeder
{
public function run(): void
{
// Tier features (boolean gates)
Feature::create([
'code' => 'tier.apollo',
'name' => 'Apollo Tier',
'type' => 'boolean',
'category' => 'tier',
]);
// Social features
Feature::create([
'code' => 'social.accounts',
'name' => 'Social Accounts',
'type' => 'limit',
'reset_type' => 'none',
'category' => 'social',
]);
Feature::create([
'code' => 'social.posts.scheduled',
'name' => 'Scheduled Posts',
'type' => 'limit',
'reset_type' => 'monthly',
'category' => 'social',
]);
// AI features
Feature::create([
'code' => 'ai.credits',
'name' => 'AI Credits',
'type' => 'limit',
'reset_type' => 'monthly',
'category' => 'ai',
]);
// Storage pool
$storagePool = Feature::create([
'code' => 'host.storage.total',
'name' => 'Total Storage',
'type' => 'limit',
'reset_type' => 'none',
'category' => 'storage',
]);
// Child features share pool
Feature::create([
'code' => 'host.cdn',
'name' => 'CDN Storage',
'type' => 'limit',
'parent_feature_id' => $storagePool->id,
'category' => 'storage',
]);
}
}
```
## Testing
### Test Namespace Isolation
```php
public function test_cannot_access_other_namespace_resources(): void
{
$namespace1 = Namespace_::factory()->create();
$namespace2 = Namespace_::factory()->create();
$page = Page::factory()->for($namespace1, 'namespace')->create();
// Set context to namespace2
request()->attributes->set('current_namespace', $namespace2);
// Should not find page from namespace1
$this->assertNull(Page::ownedByCurrentNamespace()->find($page->id));
}
```
### Test Entitlements
```php
public function test_enforces_feature_limits(): void
{
$namespace = Namespace_::factory()->create();
$package = Package::factory()->create();
$feature = Feature::factory()->create([
'code' => 'social.accounts',
'type' => 'limit',
]);
$package->features()->attach($feature->id, ['limit_value' => 5]);
$entitlements = app(EntitlementService::class);
$entitlements->provisionPackage($namespace, $package);
// Can create up to limit
for ($i = 0; $i < 5; $i++) {
$result = $entitlements->can($namespace, 'social.accounts');
$this->assertTrue($result->isAllowed());
$entitlements->recordUsage($namespace, 'social.accounts');
}
// 6th attempt denied
$result = $entitlements->can($namespace, 'social.accounts');
$this->assertTrue($result->isDenied());
}
```
## Best Practices
### 1. Always Use Namespace Scoping
```php
// ✅ Good - scoped to namespace
class Page extends Model
{
use BelongsToNamespace;
}
// ❌ Bad - no isolation
class Page extends Model { }
```
### 2. Check Entitlements Before Actions
```php
// ✅ Good - check before creating
$result = $entitlements->can($namespace, 'social.accounts');
if ($result->isDenied()) {
return back()->with('error', $result->getMessage());
}
SocialAccount::create($data);
$entitlements->recordUsage($namespace, 'social.accounts');
// ❌ Bad - no entitlement check
SocialAccount::create($data);
```
### 3. Use Descriptive Feature Codes
```php
// ✅ Good - clear hierarchy
'social.accounts'
'social.posts.scheduled'
'ai.credits.claude'
// ❌ Bad - unclear
'accounts'
'posts'
'credits'
```
### 4. Provide Usage Visibility
Always show users their current usage and limits in the UI.
### 5. Log Entitlement Changes
All provisioning, suspension, and cancellation should be logged for audit purposes.
## Migration from Workspace-Only
If migrating from workspace-only system:
```php
// Create namespace for each workspace
foreach (Workspace::all() as $workspace) {
$namespace = Namespace_::create([
'name' => $workspace->name,
'owner_type' => Workspace::class,
'owner_id' => $workspace->id,
'workspace_id' => $workspace->id,
'is_default' => true,
]);
// Migrate existing resources
Resource::where('workspace_id', $workspace->id)
->update(['namespace_id' => $namespace->id]);
// Migrate packages
WorkspacePackage::where('workspace_id', $workspace->id)
->each(function ($wp) use ($namespace) {
NamespacePackage::create([
'namespace_id' => $namespace->id,
'package_id' => $wp->package_id,
'status' => $wp->status,
'starts_at' => $wp->starts_at,
'expires_at' => $wp->expires_at,
]);
});
}
```
## Learn More
- [Multi-Tenancy Architecture →](/architecture/multi-tenancy)
- [Entitlements RFC](https://github.com/host-uk/core-php/blob/main/docs/rfc/RFC-004-ENTITLEMENTS.md)
- [API Package →](/packages/api)
- [Security Overview →](/security/overview)

View file

@ -0,0 +1,776 @@
# Actions Pattern
Actions are single-purpose classes that encapsulate business logic. They provide a clean, testable, and reusable way to handle complex operations.
## Why Actions?
### Traditional Controller (Fat Controllers)
```php
class PostController extends Controller
{
public function store(Request $request)
{
// Validation
$validated = $request->validate([/*...*/]);
// Business logic mixed with controller concerns
$slug = Str::slug($validated['title']);
if (Post::where('slug', $slug)->exists()) {
$slug .= '-' . Str::random(5);
}
$post = Post::create([
'title' => $validated['title'],
'slug' => $slug,
'content' => $validated['content'],
'workspace_id' => auth()->user()->workspace_id,
]);
if ($request->has('tags')) {
$post->tags()->sync($validated['tags']);
}
event(new PostCreated($post));
Cache::tags(['posts'])->flush();
return redirect()->route('posts.show', $post);
}
}
```
**Problems:**
- Business logic tied to HTTP layer
- Hard to reuse from console, jobs, or tests
- Difficult to test in isolation
- Controller responsibilities bloat
### Actions Pattern (Clean Separation)
```php
class PostController extends Controller
{
public function store(StorePostRequest $request)
{
$post = CreatePost::run($request->validated());
return redirect()->route('posts.show', $post);
}
}
class CreatePost
{
use Action;
public function handle(array $data): Post
{
$slug = $this->generateUniqueSlug($data['title']);
$post = Post::create([
'title' => $data['title'],
'slug' => $slug,
'content' => $data['content'],
]);
if (isset($data['tags'])) {
$post->tags()->sync($data['tags']);
}
event(new PostCreated($post));
Cache::tags(['posts'])->flush();
return $post;
}
private function generateUniqueSlug(string $title): string
{
$slug = Str::slug($title);
if (Post::where('slug', $slug)->exists()) {
$slug .= '-' . Str::random(5);
}
return $slug;
}
}
```
**Benefits:**
- Business logic isolated from HTTP concerns
- Reusable from anywhere (controllers, jobs, commands, tests)
- Easy to test
- Single responsibility
- Dependency injection support
## Creating Actions
### Basic Action
```php
<?php
namespace Mod\Blog\Actions;
use Core\Actions\Action;
use Mod\Blog\Models\Post;
class PublishPost
{
use Action;
public function handle(Post $post): Post
{
$post->update([
'published_at' => now(),
'status' => 'published',
]);
return $post;
}
}
```
### Using Actions
```php
// Static call (recommended)
$post = PublishPost::run($post);
// Instance call
$action = new PublishPost();
$post = $action->handle($post);
// Via container (with DI)
$post = app(PublishPost::class)->handle($post);
```
## Dependency Injection
Actions support constructor dependency injection:
```php
<?php
namespace Mod\Blog\Actions;
use Core\Actions\Action;
use Mod\Blog\Models\Post;
use Mod\Blog\Repositories\PostRepository;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Cache\Repository as Cache;
class CreatePost
{
use Action;
public function __construct(
private PostRepository $posts,
private Dispatcher $events,
private Cache $cache,
) {}
public function handle(array $data): Post
{
$post = $this->posts->create($data);
$this->events->dispatch(new PostCreated($post));
$this->cache->tags(['posts'])->flush();
return $post;
}
}
```
## Action Return Types
### Returning Models
```php
class CreatePost
{
use Action;
public function handle(array $data): Post
{
return Post::create($data);
}
}
$post = CreatePost::run($data);
```
### Returning Collections
```php
class GetRecentPosts
{
use Action;
public function handle(int $limit = 10): Collection
{
return Post::published()
->latest('published_at')
->limit($limit)
->get();
}
}
$posts = GetRecentPosts::run(5);
```
### Returning Boolean
```php
class DeletePost
{
use Action;
public function handle(Post $post): bool
{
return $post->delete();
}
}
$deleted = DeletePost::run($post);
```
### Returning DTOs
```php
class AnalyzePost
{
use Action;
public function handle(Post $post): PostAnalytics
{
return new PostAnalytics(
views: $post->views()->count(),
averageReadTime: $this->calculateReadTime($post),
engagement: $this->calculateEngagement($post),
);
}
}
$analytics = AnalyzePost::run($post);
echo $analytics->views;
```
## Complex Actions
### Multi-Step Actions
```php
class ImportPostsFromWordPress
{
use Action;
public function __construct(
private WordPressClient $client,
private CreatePost $createPost,
private AttachCategories $attachCategories,
private ImportMedia $importMedia,
) {}
public function handle(string $siteUrl, array $options = []): ImportResult
{
$posts = $this->client->fetchPosts($siteUrl);
$imported = [];
$errors = [];
foreach ($posts as $wpPost) {
try {
DB::transaction(function () use ($wpPost, &$imported) {
// Create post
$post = $this->createPost->handle([
'title' => $wpPost['title'],
'content' => $wpPost['content'],
'published_at' => $wpPost['date'],
]);
// Import media
if ($wpPost['featured_image']) {
$this->importMedia->handle($post, $wpPost['featured_image']);
}
// Attach categories
$this->attachCategories->handle($post, $wpPost['categories']);
$imported[] = $post;
});
} catch (\Exception $e) {
$errors[] = [
'post' => $wpPost['title'],
'error' => $e->getMessage(),
];
}
}
return new ImportResult(
imported: collect($imported),
errors: collect($errors),
);
}
}
```
### Actions with Validation
```php
class UpdatePost
{
use Action;
public function __construct(
private ValidatePostData $validator,
) {}
public function handle(Post $post, array $data): Post
{
// Validate before processing
$validated = $this->validator->handle($data);
$post->update($validated);
return $post->fresh();
}
}
class ValidatePostData
{
use Action;
public function handle(array $data): array
{
return validator($data, [
'title' => 'required|max:255',
'content' => 'required',
'published_at' => 'nullable|date',
])->validate();
}
}
```
## Action Patterns
### Command Pattern
Actions are essentially the Command pattern:
```php
interface ActionInterface
{
public function handle(...$params);
}
// Each action is a command
class PublishPost implements ActionInterface { }
class UnpublishPost implements ActionInterface { }
class SchedulePost implements ActionInterface { }
```
### Pipeline Pattern
Chain multiple actions:
```php
class ProcessNewPost
{
use Action;
public function handle(array $data): Post
{
return Pipeline::send($data)
->through([
ValidatePostData::class,
SanitizeContent::class,
CreatePost::class,
GenerateExcerpt::class,
GenerateSocialImages::class,
NotifySubscribers::class,
])
->thenReturn();
}
}
```
### Strategy Pattern
Different strategies as actions:
```php
interface PublishStrategy
{
public function publish(Post $post): void;
}
class PublishImmediately implements PublishStrategy
{
public function publish(Post $post): void
{
$post->update(['published_at' => now()]);
}
}
class ScheduleForLater implements PublishStrategy
{
public function publish(Post $post): void
{
PublishPostJob::dispatch($post)
->delay($post->scheduled_at);
}
}
class PublishPost
{
use Action;
public function handle(Post $post, PublishStrategy $strategy): void
{
$strategy->publish($post);
}
}
```
## Testing Actions
### Unit Testing
Test actions in isolation:
```php
<?php
namespace Tests\Unit\Mod\Blog\Actions;
use Tests\TestCase;
use Mod\Blog\Actions\CreatePost;
use Mod\Blog\Models\Post;
class CreatePostTest extends TestCase
{
public function test_creates_post_with_valid_data(): void
{
$data = [
'title' => 'Test Post',
'content' => 'Test content',
];
$post = CreatePost::run($data);
$this->assertInstanceOf(Post::class, $post);
$this->assertEquals('Test Post', $post->title);
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
]);
}
public function test_generates_unique_slug(): void
{
Post::factory()->create(['slug' => 'test-post']);
$post = CreatePost::run([
'title' => 'Test Post',
'content' => 'Content',
]);
$this->assertNotEquals('test-post', $post->slug);
$this->assertStringStartsWith('test-post-', $post->slug);
}
}
```
### Mocking Dependencies
```php
public function test_dispatches_event_after_creation(): void
{
Event::fake();
$post = CreatePost::run([
'title' => 'Test Post',
'content' => 'Content',
]);
Event::assertDispatched(PostCreated::class, function ($event) use ($post) {
return $event->post->id === $post->id;
});
}
```
### Integration Testing
```php
public function test_import_creates_posts_from_wordpress(): void
{
Http::fake([
'wordpress.example.com/*' => Http::response([
[
'title' => 'WP Post 1',
'content' => 'Content 1',
'date' => '2026-01-01',
],
[
'title' => 'WP Post 2',
'content' => 'Content 2',
'date' => '2026-01-02',
],
]),
]);
$result = ImportPostsFromWordPress::run('wordpress.example.com');
$this->assertCount(2, $result->imported);
$this->assertCount(0, $result->errors);
$this->assertEquals(2, Post::count());
}
```
## Action Composition
### Composing Actions
Build complex operations from simple actions:
```php
class PublishBlogPost
{
use Action;
public function __construct(
private UpdatePost $updatePost,
private GenerateOgImage $generateImage,
private NotifySubscribers $notifySubscribers,
private PingSearchEngines $pingSearchEngines,
) {}
public function handle(Post $post): Post
{
// Update post status
$post = $this->updatePost->handle($post, [
'status' => 'published',
'published_at' => now(),
]);
// Generate social images
$this->generateImage->handle($post);
// Notify subscribers
dispatch(fn () => $this->notifySubscribers->handle($post))
->afterResponse();
// Ping search engines
dispatch(fn () => $this->pingSearchEngines->handle($post))
->afterResponse();
return $post;
}
}
```
### Conditional Execution
```php
class ProcessPost
{
use Action;
public function handle(Post $post, array $options = []): Post
{
if ($options['publish'] ?? false) {
PublishPost::run($post);
}
if ($options['notify'] ?? false) {
NotifySubscribers::run($post);
}
if ($options['generate_images'] ?? true) {
GenerateSocialImages::run($post);
}
return $post;
}
}
```
## Best Practices
### 1. Single Responsibility
Each action should do one thing:
```php
// ✅ Good - focused actions
class CreatePost { }
class PublishPost { }
class NotifySubscribers { }
// ❌ Bad - does too much
class CreateAndPublishPostAndNotifySubscribers { }
```
### 2. Meaningful Names
Use descriptive verb-noun names:
```php
// ✅ Good names
class CreatePost { }
class UpdatePost { }
class DeletePost { }
class PublishPost { }
class UnpublishPost { }
// ❌ Bad names
class PostAction { }
class HandlePost { }
class DoStuff { }
```
### 3. Return Values
Always return something useful:
```php
// ✅ Good - returns created model
public function handle(array $data): Post
{
return Post::create($data);
}
// ❌ Bad - returns nothing
public function handle(array $data): void
{
Post::create($data);
}
```
### 4. Idempotency
Make actions idempotent when possible:
```php
class PublishPost
{
use Action;
public function handle(Post $post): Post
{
// Idempotent - safe to call multiple times
if ($post->isPublished()) {
return $post;
}
$post->update(['published_at' => now()]);
return $post;
}
}
```
### 5. Type Hints
Always use type hints:
```php
// ✅ Good - clear types
public function handle(Post $post, array $data): Post
// ❌ Bad - no types
public function handle($post, $data)
```
## Common Use Cases
### CRUD Operations
```php
class CreatePost { }
class UpdatePost { }
class DeletePost { }
class RestorePost { }
```
### State Transitions
```php
class PublishPost { }
class UnpublishPost { }
class ArchivePost { }
class SchedulePost { }
```
### Data Processing
```php
class ImportPosts { }
class ExportPosts { }
class SyncPosts { }
class MigratePosts { }
```
### Calculations
```php
class CalculatePostStatistics { }
class GeneratePostSummary { }
class AnalyzePostPerformance { }
```
### External Integrations
```php
class SyncToWordPress { }
class PublishToMedium { }
class ShareOnSocial { }
```
## Action vs Service
### When to Use Actions
- Single, focused operations
- No state management needed
- Reusable across contexts
### When to Use Services
- Multiple related operations
- Stateful operations
- Facade for complex subsystem
```php
// Action - single operation
class CreatePost
{
use Action;
public function handle(array $data): Post
{
return Post::create($data);
}
}
// Service - multiple operations, state
class BlogService
{
private Collection $posts;
public function getRecentPosts(int $limit): Collection
{
return $this->posts ??= Post::latest()->limit($limit)->get();
}
public function getPopularPosts(int $limit): Collection { }
public function searchPosts(string $query): Collection { }
public function getPostsByCategory(Category $category): Collection { }
}
```
## Learn More
- [Service Layer](/patterns-guide/services)
- [Repository Pattern](/patterns-guide/repositories)
- [Testing Actions](/testing/actions)

Some files were not shown because too many files have changed in this diff Show more