commit 85bbb8e828118999a23f1eb819cf819cf8dfc917 Author: Snider Date: Tue Mar 3 17:51:03 2026 +0000 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 diff --git a/api/authentication.md b/api/authentication.md new file mode 100644 index 0000000..0687546 --- /dev/null +++ b/api/authentication.md @@ -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) diff --git a/api/endpoints.md b/api/endpoints.md new file mode 100644 index 0000000..5f6af1d --- /dev/null +++ b/api/endpoints.md @@ -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 diff --git a/api/errors.md b/api/errors.md new file mode 100644 index 0000000..56f5e04 --- /dev/null +++ b/api/errors.md @@ -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) diff --git a/build/.DS_Store b/build/.DS_Store new file mode 100644 index 0000000..0a58a5a Binary files /dev/null and b/build/.DS_Store differ diff --git a/build/cli/ai/example.md b/build/cli/ai/example.md new file mode 100644 index 0000000..b115b09 --- /dev/null +++ b/build/cli/ai/example.md @@ -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 +``` diff --git a/build/cli/ai/index.md b/build/cli/ai/index.md new file mode 100644 index 0000000..f6c49be --- /dev/null +++ b/build/cli/ai/index.md @@ -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 [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 [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 [flags] +``` + +Commit message format: +``` +feat(scope): description + +Task: #123 +Co-Authored-By: Claude +``` + +### 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 [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) diff --git a/build/cli/build/example.md b/build/cli/build/example.md new file mode 100644 index 0000000..da2f3b4 --- /dev/null +++ b/build/cli/build/example.md @@ -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 +``` diff --git a/build/cli/build/index.md b/build/cli/build/index.md new file mode 100644 index 0000000..6956e65 --- /dev/null +++ b/build/cli/build/index.md @@ -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 +``` diff --git a/build/cli/build/sdk/example.md b/build/cli/build/sdk/example.md new file mode 100644 index 0000000..e832308 --- /dev/null +++ b/build/cli/build/sdk/example.md @@ -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 +``` diff --git a/build/cli/build/sdk/index.md b/build/cli/build/sdk/index.md new file mode 100644 index 0000000..084c5ef --- /dev/null +++ b/build/cli/build/sdk/index.md @@ -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 +``` diff --git a/build/cli/ci/changelog/example.md b/build/cli/ci/changelog/example.md new file mode 100644 index 0000000..101cad7 --- /dev/null +++ b/build/cli/ci/changelog/example.md @@ -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 +``` diff --git a/build/cli/ci/changelog/index.md b/build/cli/ci/changelog/index.md new file mode 100644 index 0000000..ffc0712 --- /dev/null +++ b/build/cli/ci/changelog/index.md @@ -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. diff --git a/build/cli/ci/example.md b/build/cli/ci/example.md new file mode 100644 index 0000000..faf4720 --- /dev/null +++ b/build/cli/ci/example.md @@ -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 +``` diff --git a/build/cli/ci/index.md b/build/cli/ci/index.md new file mode 100644 index 0000000..ee2c759 --- /dev/null +++ b/build/cli/ci/index.md @@ -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). diff --git a/build/cli/ci/init/example.md b/build/cli/ci/init/example.md new file mode 100644 index 0000000..8f76ab9 --- /dev/null +++ b/build/cli/ci/init/example.md @@ -0,0 +1,17 @@ +# CI Init Examples + +```bash +core ci init +``` + +Creates `.core/release.yaml`: + +```yaml +version: 1 + +project: + name: myapp + +publishers: + - type: github +``` diff --git a/build/cli/ci/init/index.md b/build/cli/ci/init/index.md new file mode 100644 index 0000000..23ba068 --- /dev/null +++ b/build/cli/ci/init/index.md @@ -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. diff --git a/build/cli/ci/version/example.md b/build/cli/ci/version/example.md new file mode 100644 index 0000000..e669d65 --- /dev/null +++ b/build/cli/ci/version/example.md @@ -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) diff --git a/build/cli/ci/version/index.md b/build/cli/ci/version/index.md new file mode 100644 index 0000000..7014a34 --- /dev/null +++ b/build/cli/ci/version/index.md @@ -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) diff --git a/build/cli/dev/ci/index.md b/build/cli/dev/ci/index.md new file mode 100644 index 0000000..0cf8442 --- /dev/null +++ b/build/cli/dev/ci/index.md @@ -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 diff --git a/build/cli/dev/commit/index.md b/build/cli/dev/commit/index.md new file mode 100644 index 0000000..4258fb1 --- /dev/null +++ b/build/cli/dev/commit/index.md @@ -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) diff --git a/build/cli/dev/example.md b/build/cli/dev/example.md new file mode 100644 index 0000000..da75b5e --- /dev/null +++ b/build/cli/dev/example.md @@ -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 +``` diff --git a/build/cli/dev/health/index.md b/build/cli/dev/health/index.md new file mode 100644 index 0000000..d104689 --- /dev/null +++ b/build/cli/dev/health/index.md @@ -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 diff --git a/build/cli/dev/impact/index.md b/build/cli/dev/impact/index.md new file mode 100644 index 0000000..ac96e04 --- /dev/null +++ b/build/cli/dev/impact/index.md @@ -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 [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 diff --git a/build/cli/dev/index.md b/build/cli/dev/index.md new file mode 100644 index 0000000..56a5090 --- /dev/null +++ b/build/cli/dev/index.md @@ -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.) diff --git a/build/cli/dev/issues/index.md b/build/cli/dev/issues/index.md new file mode 100644 index 0000000..36091eb --- /dev/null +++ b/build/cli/dev/issues/index.md @@ -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 diff --git a/build/cli/dev/pull/index.md b/build/cli/dev/pull/index.md new file mode 100644 index 0000000..1f6f3df --- /dev/null +++ b/build/cli/dev/pull/index.md @@ -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 diff --git a/build/cli/dev/push/index.md b/build/cli/dev/push/index.md new file mode 100644 index 0000000..0c11195 --- /dev/null +++ b/build/cli/dev/push/index.md @@ -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) diff --git a/build/cli/dev/reviews/index.md b/build/cli/dev/reviews/index.md new file mode 100644 index 0000000..44c09ad --- /dev/null +++ b/build/cli/dev/reviews/index.md @@ -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 diff --git a/build/cli/dev/work/example.md b/build/cli/dev/work/example.md new file mode 100644 index 0000000..74db3fb --- /dev/null +++ b/build/cli/dev/work/example.md @@ -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 +``` diff --git a/build/cli/dev/work/index.md b/build/cli/dev/work/index.md new file mode 100644 index 0000000..454fe22 --- /dev/null +++ b/build/cli/dev/work/index.md @@ -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 [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 diff --git a/build/cli/docs/example.md b/build/cli/docs/example.md new file mode 100644 index 0000000..7729970 --- /dev/null +++ b/build/cli/docs/example.md @@ -0,0 +1,14 @@ +# Docs Examples + +## List + +```bash +core docs list +``` + +## Sync + +```bash +core docs sync +core docs sync --output ./docs +``` diff --git a/build/cli/docs/index.md b/build/cli/docs/index.md new file mode 100644 index 0000000..d73ebf0 --- /dev/null +++ b/build/cli/docs/index.md @@ -0,0 +1,110 @@ +# core docs + +Documentation management across repositories. + +## Usage + +```bash +core docs [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 diff --git a/build/cli/doctor/example.md b/build/cli/doctor/example.md new file mode 100644 index 0000000..ba94d71 --- /dev/null +++ b/build/cli/doctor/example.md @@ -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 +``` diff --git a/build/cli/doctor/index.md b/build/cli/doctor/index.md new file mode 100644 index 0000000..02cc44d --- /dev/null +++ b/build/cli/doctor/index.md @@ -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 diff --git a/build/cli/go/cov/example.md b/build/cli/go/cov/example.md new file mode 100644 index 0000000..4fdc6c2 --- /dev/null +++ b/build/cli/go/cov/example.md @@ -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 +``` diff --git a/build/cli/go/cov/index.md b/build/cli/go/cov/index.md new file mode 100644 index 0000000..3adeca3 --- /dev/null +++ b/build/cli/go/cov/index.md @@ -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 +``` diff --git a/build/cli/go/example.md b/build/cli/go/example.md new file mode 100644 index 0000000..51ad71a --- /dev/null +++ b/build/cli/go/example.md @@ -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 +``` diff --git a/build/cli/go/fmt/example.md b/build/cli/go/fmt/example.md new file mode 100644 index 0000000..40233e0 --- /dev/null +++ b/build/cli/go/fmt/example.md @@ -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 +``` diff --git a/build/cli/go/fmt/index.md b/build/cli/go/fmt/index.md new file mode 100644 index 0000000..fe6113e --- /dev/null +++ b/build/cli/go/fmt/index.md @@ -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 +``` diff --git a/build/cli/go/index.md b/build/cli/go/index.md new file mode 100644 index 0000000..981953c --- /dev/null +++ b/build/cli/go/index.md @@ -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 | diff --git a/build/cli/go/install/example.md b/build/cli/go/install/example.md new file mode 100644 index 0000000..bba88cd --- /dev/null +++ b/build/cli/go/install/example.md @@ -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 +``` diff --git a/build/cli/go/install/index.md b/build/cli/go/install/index.md new file mode 100644 index 0000000..e7bd109 --- /dev/null +++ b/build/cli/go/install/index.md @@ -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 +``` diff --git a/build/cli/go/lint/example.md b/build/cli/go/lint/example.md new file mode 100644 index 0000000..56b46d4 --- /dev/null +++ b/build/cli/go/lint/example.md @@ -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 +``` diff --git a/build/cli/go/lint/index.md b/build/cli/go/lint/index.md new file mode 100644 index 0000000..5f9e804 --- /dev/null +++ b/build/cli/go/lint/index.md @@ -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 +``` diff --git a/build/cli/go/mod/download/index.md b/build/cli/go/mod/download/index.md new file mode 100644 index 0000000..240ef6d --- /dev/null +++ b/build/cli/go/mod/download/index.md @@ -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 diff --git a/build/cli/go/mod/example.md b/build/cli/go/mod/example.md new file mode 100644 index 0000000..57d2e66 --- /dev/null +++ b/build/cli/go/mod/example.md @@ -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 +``` diff --git a/build/cli/go/mod/graph/index.md b/build/cli/go/mod/graph/index.md new file mode 100644 index 0000000..2aa2619 --- /dev/null +++ b/build/cli/go/mod/graph/index.md @@ -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 diff --git a/build/cli/go/mod/index.md b/build/cli/go/mod/index.md new file mode 100644 index 0000000..ee8e46e --- /dev/null +++ b/build/cli/go/mod/index.md @@ -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 +``` diff --git a/build/cli/go/mod/tidy/index.md b/build/cli/go/mod/tidy/index.md new file mode 100644 index 0000000..684b07e --- /dev/null +++ b/build/cli/go/mod/tidy/index.md @@ -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 diff --git a/build/cli/go/mod/verify/index.md b/build/cli/go/mod/verify/index.md new file mode 100644 index 0000000..e01dc2a --- /dev/null +++ b/build/cli/go/mod/verify/index.md @@ -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 diff --git a/build/cli/go/test/example.md b/build/cli/go/test/example.md new file mode 100644 index 0000000..85ff1b5 --- /dev/null +++ b/build/cli/go/test/example.md @@ -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 +``` diff --git a/build/cli/go/test/index.md b/build/cli/go/test/index.md new file mode 100644 index 0000000..8b54524 --- /dev/null +++ b/build/cli/go/test/index.md @@ -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 +``` diff --git a/build/cli/go/work/index.md b/build/cli/go/work/index.md new file mode 100644 index 0000000..4022507 --- /dev/null +++ b/build/cli/go/work/index.md @@ -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 +``` diff --git a/build/cli/go/work/init/index.md b/build/cli/go/work/init/index.md new file mode 100644 index 0000000..6527324 --- /dev/null +++ b/build/cli/go/work/init/index.md @@ -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 diff --git a/build/cli/go/work/sync/index.md b/build/cli/go/work/sync/index.md new file mode 100644 index 0000000..38caed1 --- /dev/null +++ b/build/cli/go/work/sync/index.md @@ -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 diff --git a/build/cli/go/work/use/index.md b/build/cli/go/work/use/index.md new file mode 100644 index 0000000..25e0cab --- /dev/null +++ b/build/cli/go/work/use/index.md @@ -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 diff --git a/build/cli/index.md b/build/cli/index.md new file mode 100644 index 0000000..9aeee02 --- /dev/null +++ b/build/cli/index.md @@ -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. diff --git a/build/cli/php/example.md b/build/cli/php/example.md new file mode 100644 index 0000000..96e1600 --- /dev/null +++ b/build/cli/php/example.md @@ -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 +``` diff --git a/build/cli/php/index.md b/build/cli/php/index.md new file mode 100644 index 0000000..83ad596 --- /dev/null +++ b/build/cli/php/index.md @@ -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 +``` + +--- + +## 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 [...] +``` + +Adds path repositories to composer.json with symlink enabled. + +--- + +## php packages unlink + +Remove linked packages from composer.json. + +```bash +core php packages unlink [...] +``` + +--- + +## php packages update + +Update linked packages via Composer. + +```bash +core php packages update [...] +``` + +--- + +## 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. diff --git a/build/cli/pkg/example.md b/build/cli/pkg/example.md new file mode 100644 index 0000000..7904aae --- /dev/null +++ b/build/cli/pkg/example.md @@ -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 +``` diff --git a/build/cli/pkg/index.md b/build/cli/pkg/index.md new file mode 100644 index 0000000..fcc218b --- /dev/null +++ b/build/cli/pkg/index.md @@ -0,0 +1,144 @@ +# core pkg + +Package management for host-uk repositories. + +## Usage + +```bash +core pkg [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 [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 [...] [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 diff --git a/build/cli/pkg/search/example.md b/build/cli/pkg/search/example.md new file mode 100644 index 0000000..fbcaa6f --- /dev/null +++ b/build/cli/pkg/search/example.md @@ -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 │ +└──────────────┴─────────────────────────────┘ +``` diff --git a/build/cli/pkg/search/index.md b/build/cli/pkg/search/index.md new file mode 100644 index 0000000..57fea91 --- /dev/null +++ b/build/cli/pkg/search/index.md @@ -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 diff --git a/build/cli/sdk/example.md b/build/cli/sdk/example.md new file mode 100644 index 0000000..2fada8c --- /dev/null +++ b/build/cli/sdk/example.md @@ -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 +``` diff --git a/build/cli/sdk/index.md b/build/cli/sdk/index.md new file mode 100644 index 0000000..bd6828c --- /dev/null +++ b/build/cli/sdk/index.md @@ -0,0 +1,106 @@ +# core sdk + +SDK validation and API compatibility tools. + +To generate SDKs, use: `core build sdk` + +## Usage + +```bash +core sdk [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 diff --git a/build/cli/setup/example.md b/build/cli/setup/example.md new file mode 100644 index 0000000..23f2410 --- /dev/null +++ b/build/cli/setup/example.md @@ -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 +``` diff --git a/build/cli/setup/index.md b/build/cli/setup/index.md new file mode 100644 index 0000000..d07121f --- /dev/null +++ b/build/cli/setup/index.md @@ -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 diff --git a/build/cli/test/example.md b/build/cli/test/example.md new file mode 100644 index 0000000..9e2a4a7 --- /dev/null +++ b/build/cli/test/example.md @@ -0,0 +1,8 @@ +# Test Examples + +**Note:** Prefer `core go test` or `core php test` instead. + +```bash +core test +core test --coverage +``` diff --git a/build/cli/test/index.md b/build/cli/test/index.md new file mode 100644 index 0000000..920baea --- /dev/null +++ b/build/cli/test/index.md @@ -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 diff --git a/build/cli/vm/example.md b/build/cli/vm/example.md new file mode 100644 index 0000000..f31f97e --- /dev/null +++ b/build/cli/vm/example.md @@ -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 +``` diff --git a/build/cli/vm/index.md b/build/cli/vm/index.md new file mode 100644 index 0000000..ec0be0f --- /dev/null +++ b/build/cli/vm/index.md @@ -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 [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 [flags] +core vm run --template [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 +``` + +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 [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 +``` + +### 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 diff --git a/build/cli/vm/templates/example.md b/build/cli/vm/templates/example.md new file mode 100644 index 0000000..c1f8b35 --- /dev/null +++ b/build/cli/vm/templates/example.md @@ -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 +``` diff --git a/build/cli/vm/templates/index.md b/build/cli/vm/templates/index.md new file mode 100644 index 0000000..7ca3700 --- /dev/null +++ b/build/cli/vm/templates/index.md @@ -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 +``` + +### 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 +``` + +### 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 diff --git a/build/go/configuration.md b/build/go/configuration.md new file mode 100644 index 0000000..deabb68 --- /dev/null +++ b/build/go/configuration.md @@ -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 " + + # 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 diff --git a/build/go/getting-started.md b/build/go/getting-started.md new file mode 100644 index 0000000..ad374ab --- /dev/null +++ b/build/go/getting-started.md @@ -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 --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 diff --git a/build/go/glossary.md b/build/go/glossary.md new file mode 100644 index 0000000..ea9d280 --- /dev/null +++ b/build/go/glossary.md @@ -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 diff --git a/build/go/index.md b/build/go/index.md new file mode 100644 index 0000000..155a047 --- /dev/null +++ b/build/go/index.md @@ -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. diff --git a/build/go/migration.md b/build/go/migration.md new file mode 100644 index 0000000..e5c4606 --- /dev/null +++ b/build/go/migration.md @@ -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 diff --git a/build/go/skill/index.md b/build/go/skill/index.md new file mode 100644 index 0000000..40ae3ad --- /dev/null +++ b/build/go/skill/index.md @@ -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`. diff --git a/build/go/troubleshooting.md b/build/go/troubleshooting.md new file mode 100644 index 0000000..c075f3a --- /dev/null +++ b/build/go/troubleshooting.md @@ -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 diff --git a/build/go/workflows.md b/build/go/workflows.md new file mode 100644 index 0000000..96b0c9f --- /dev/null +++ b/build/go/workflows.md @@ -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 diff --git a/build/php/.DS_Store b/build/php/.DS_Store new file mode 100644 index 0000000..1a8ee60 Binary files /dev/null and b/build/php/.DS_Store differ diff --git a/build/php/actions.md b/build/php/actions.md new file mode 100644 index 0000000..e7e1a9b --- /dev/null +++ b/build/php/actions.md @@ -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 + '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) diff --git a/build/php/activity.md b/build/php/activity.md new file mode 100644 index 0000000..4fd6632 --- /dev/null +++ b/build/php/activity.md @@ -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 +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) +
+
+ @if($activity->event === 'created') + ... + @elseif($activity->event === 'updated') + ... + @endif +
+ +
+

+ {{ $activity->causer?->name ?? 'System' }} + {{ $activity->description }} +

+ +
+
+@endforeach +``` + +### Livewire Component + +```php +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) diff --git a/build/php/architecture/custom-events.md b/build/php/architecture/custom-events.md new file mode 100644 index 0000000..0af9145 --- /dev/null +++ b/build/php/architecture/custom-events.md @@ -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 +gateways[$name] = $class; + } + + public function getGateways(): array + { + return $this->gateways; + } + + public function version(): string + { + return '1.0.0'; + } +} +``` + +### Step 2: Fire Event + +```php + '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 + 'onPaymentGateways', + ]; + + public function onPaymentGateways(PaymentGatewaysRegistering $event): void + { + $event->gateway('stripe', StripeGateway::class); + } +} +``` + +## Event with Multiple Methods + +Provide different registration methods: + +```php +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 +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 +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 +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 +fields[$type] = $class; + } + + public function getFields(): array + { + return $this->fields; + } +} +``` + +## Event Documentation + +Document your events with docblocks: + +```php +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 + */ + public function getProcessors(): array + { + return $this->processors; + } +} +``` + +## Testing Custom Events + +```php +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) diff --git a/build/php/architecture/lazy-loading.md b/build/php/architecture/lazy-loading.md new file mode 100644 index 0000000..284d8bd --- /dev/null +++ b/build/php/architecture/lazy-loading.md @@ -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 +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) diff --git a/build/php/architecture/lifecycle-events.md b/build/php/architecture/lifecycle-events.md new file mode 100644 index 0000000..5114cb2 --- /dev/null +++ b/build/php/architecture/lifecycle-events.md @@ -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 + '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 +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 +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) diff --git a/build/php/architecture/module-system.md b/build/php/architecture/module-system.md new file mode 100644 index 0000000..f139386 --- /dev/null +++ b/build/php/architecture/module-system.md @@ -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 + '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 + 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 +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 +published()->count(3)->create(); + + $response = $this->get('/blog'); + + $response->assertStatus(200); + $response->assertViewHas('posts'); + } +} +``` + +### Unit Tests + +Test module services and actions: + +```php +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) diff --git a/build/php/architecture/multi-tenancy.md b/build/php/architecture/multi-tenancy.md new file mode 100644 index 0000000..cccc412 --- /dev/null +++ b/build/php/architecture/multi-tenancy.md @@ -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 + '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 + '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 +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 +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 + [ + '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) diff --git a/build/php/architecture/performance.md b/build/php/architecture/performance.md new file mode 100644 index 0000000..171c357 --- /dev/null +++ b/build/php/architecture/performance.md @@ -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 +Hero + +// With transformations + +``` + +### 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 --}} +... + +{{-- Lazy load thumbnails --}} +... +``` + +## 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 +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 --}} + + +{{-- Load on interaction --}} + +``` + +### Polling Optimization + +```php +// ❌ Bad - polls every 1s +
+ {{ $count }} users online +
+ +// ✅ Good - polls every 30s +
+ {{ $count }} users online +
+ +// ✅ Better - poll only when visible +
+ {{ $count }} users online +
+``` + +### Debouncing + +```blade +{{-- Debounce search input --}} + +``` + +## 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) diff --git a/build/php/cdn.md b/build/php/cdn.md new file mode 100644 index 0000000..d3bc78f --- /dev/null +++ b/build/php/cdn.md @@ -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 +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 --}} +Photo + +{{-- ❌ Bad - relative path --}} +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 + + +// ❌ Bad - transform server-side + +``` + +## 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) diff --git a/build/php/configuration.md b/build/php/configuration.md new file mode 100644 index 0000000..3074caf --- /dev/null +++ b/build/php/configuration.md @@ -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 + [ + '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) diff --git a/build/php/events.md b/build/php/events.md new file mode 100644 index 0000000..1cc6798 --- /dev/null +++ b/build/php/events.md @@ -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 + '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 +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) diff --git a/build/php/getting-started.md b/build/php/getting-started.md new file mode 100644 index 0000000..48b00ea --- /dev/null +++ b/build/php/getting-started.md @@ -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) diff --git a/build/php/index.md b/build/php/index.md new file mode 100644 index 0000000..bb325d7 --- /dev/null +++ b/build/php/index.md @@ -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 + '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 + '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) diff --git a/build/php/installation.md b/build/php/installation.md new file mode 100644 index 0000000..26f7c3b --- /dev/null +++ b/build/php/installation.md @@ -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) diff --git a/build/php/media.md b/build/php/media.md new file mode 100644 index 0000000..05b9afe --- /dev/null +++ b/build/php/media.md @@ -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 + + + {{ $image->alt }} + +``` + +### 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 + +``` + +## 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 +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 --}} +Photo + +{{-- With transformation --}} +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 +
+ @if($progress > 0) +
+
+
+

Processing: {{ $progress }}%

+ @endif +
+``` + +## 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 + + +// ❌ 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 + + +// ❌ Bad - serve from origin + +``` + +## 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) diff --git a/build/php/modules.md b/build/php/modules.md new file mode 100644 index 0000000..5c27c3d --- /dev/null +++ b/build/php/modules.md @@ -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 + '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 + 'Test Post', + 'content' => 'Content here', + ]); + + $this->assertDatabaseHas('posts', [ + 'title' => 'Test Post', + ]); + + $this->get("/blog/{$post->slug}") + ->assertOk() + ->assertSee('Test Post'); + } +} +``` + +### Unit Tests + +```php +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) diff --git a/build/php/namespaces.md b/build/php/namespaces.md new file mode 100644 index 0000000..435baef --- /dev/null +++ b/build/php/namespaces.md @@ -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 +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 +
+ + + {{ $currentNamespace->name }} + + + @foreach($personalNamespaces as $ns) + + {{ $ns->name }} + + @endforeach + + @foreach($workspaceNamespaces as $group) + {{ $group['workspace']->name }} + @foreach($group['namespaces'] as $ns) + + {{ $ns->name }} + + @endforeach + @endforeach + +
+``` + +### 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) +
+

{{ ucfirst($category) }}

+ + @foreach($features as $item) +
+
+ {{ $item['feature']->name }} +
+ + @if($item['is_unlimited']) +
Unlimited
+@else +
+
+
+
+ +
+ {{ $item['used'] }} / {{ $item['limit'] }} + ({{ number_format($item['percentage'], 1) }}%) +
+ @endif +
+ @endforeach +
+@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 + '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) diff --git a/build/php/patterns/actions.md b/build/php/patterns/actions.md new file mode 100644 index 0000000..3808dda --- /dev/null +++ b/build/php/patterns/actions.md @@ -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 +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 +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 + '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) diff --git a/build/php/patterns/activity-logging.md b/build/php/patterns/activity-logging.md new file mode 100644 index 0000000..742ac9c --- /dev/null +++ b/build/php/patterns/activity-logging.md @@ -0,0 +1,678 @@ +# Activity Logging + +Core PHP Framework provides comprehensive activity logging to track changes to your models and user actions. Built on Spatie's `laravel-activitylog`, it adds workspace-scoped logging and automatic cleanup. + +## Overview + +Activity logging helps you: + +- Track who changed what and when +- Maintain audit trails for compliance +- Debug issues by reviewing historical changes +- Display activity feeds to users +- Revert changes when needed + +## Setup + +### Installation + +The activity log package is included in Core PHP: + +```bash +composer require spatie/laravel-activitylog +``` + +### Migration + +Run migrations to create the `activity_log` table: + +```bash +php artisan migrate +``` + +### Configuration + +Publish and customize the configuration: + +```bash +php artisan vendor:publish --tag=activitylog +``` + +Core PHP extends the default configuration: + +```php +// config/core.php +'activity' => [ + 'enabled' => env('ACTIVITY_LOG_ENABLED', true), + 'retention_days' => env('ACTIVITY_RETENTION_DAYS', 90), + 'cleanup_enabled' => true, + 'log_ip_address' => false, // GDPR compliance +], +``` + +## Basic Usage + +### Adding Logging to Models + +Use the `LogsActivity` trait: + +```php + 'My First Post', + 'content' => 'Hello world!', +]); +// Activity logged: "created" event + +$post->update(['title' => 'Updated Title']); +// Activity logged: "updated" event with changes + +$post->delete(); +// Activity logged: "deleted" event +``` + +### Manual Logging + +Log custom activities: + +```php +activity() + ->performedOn($post) + ->causedBy(auth()->user()) + ->withProperties(['custom' => 'data']) + ->log('published'); + +// Or use the helper on the model +$post->logActivity('published', ['published_at' => now()]); +``` + +## Configuration Options + +### Log Attributes + +Specify which attributes to track: + +```php +class Post extends Model +{ + use LogsActivity; + + // Log specific attributes + protected array $activityLogAttributes = ['title', 'content', 'status']; + + // Log all fillable attributes + protected static $logFillable = true; + + // Log all attributes + protected static $logAttributes = ['*']; + + // Log only dirty (changed) attributes + protected static $logOnlyDirty = true; + + // Don't log these attributes + protected static $logAttributesToIgnore = ['updated_at', 'view_count']; +} +``` + +### Log Events + +Control which events trigger logging: + +```php +class Post extends Model +{ + use LogsActivity; + + // Log only these events (default: all) + protected static $recordEvents = ['created', 'updated', 'deleted']; + + // Don't log these events + protected static $ignoreEvents = ['retrieved']; +} +``` + +### Custom Log Names + +Organize activities by type: + +```php +class Post extends Model +{ + use LogsActivity; + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['title', 'content']) + ->logOnlyDirty() + ->setDescriptionForEvent(fn(string $eventName) => "Post {$eventName}") + ->useLogName('blog'); + } +} +``` + +## Retrieving Activity + +### Get All Activity + +```php +// All activity in the system +$activities = Activity::all(); + +// Recent activity +$recent = Activity::latest()->limit(10)->get(); + +// Activity for specific model +$postActivity = Activity::forSubject($post)->get(); + +// Activity by specific user +$userActivity = Activity::causedBy($user)->get(); +``` + +### Filtering Activity + +```php +// By log name +$blogActivity = Activity::inLog('blog')->get(); + +// By description +$publishedPosts = Activity::where('description', 'published')->get(); + +// By date range +$recentActivity = Activity::whereBetween('created_at', [ + now()->subDays(7), + now(), +])->get(); + +// By properties +$activity = Activity::whereJsonContains('properties->status', 'published')->get(); +``` + +### Activity Scopes + +Core PHP adds workspace scoping: + +```php +use Core\Activity\Scopes\ActivityScopes; + +// Activity for current workspace +$workspaceActivity = Activity::forCurrentWorkspace()->get(); + +// Activity for specific workspace +$activity = Activity::forWorkspace($workspace)->get(); + +// Activity for specific subject type +$postActivity = Activity::forSubjectType(Post::class)->get(); +``` + +## Activity Properties + +### Storing Extra Data + +```php +activity() + ->performedOn($post) + ->withProperties([ + 'old_status' => 'draft', + 'new_status' => 'published', + 'scheduled_at' => $post->published_at, + 'notified_subscribers' => true, + ]) + ->log('published'); +``` + +### Retrieving Properties + +```php +$activity = Activity::latest()->first(); + +$properties = $activity->properties; +$oldStatus = $activity->properties['old_status'] ?? null; + +// Access as object +$newStatus = $activity->properties->new_status; +``` + +### Changes Tracking + +View before/after values: + +```php +$post->update(['title' => 'New Title']); + +$activity = Activity::forSubject($post)->latest()->first(); + +$changes = $activity->changes(); +// [ +// 'attributes' => ['title' => 'New Title'], +// 'old' => ['title' => 'Old Title'] +// ] +``` + +## Activity Presentation + +### Display Activity Feed + +```php +// Controller +public function activityFeed() +{ + $activities = Activity::with(['causer', 'subject']) + ->forCurrentWorkspace() + ->latest() + ->paginate(20); + + return view('activity-feed', compact('activities')); +} +``` + +```blade + +@foreach($activities as $activity) +
+
+ @if($activity->description === 'created') + + + @elseif($activity->description === 'deleted') + × + @else + + @endif +
+ +
+

+ {{ $activity->causer->name ?? 'System' }} + {{ $activity->description }} + {{ class_basename($activity->subject_type) }} + @if($activity->subject) + + {{ $activity->subject->title }} + + @endif +

+ +
+
+@endforeach +``` + +### Custom Descriptions + +Make descriptions more readable: + +```php +class Post extends Model +{ + use LogsActivity; + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->setDescriptionForEvent(function(string $eventName) { + return match($eventName) { + 'created' => 'created post "' . $this->title . '"', + 'updated' => 'updated post "' . $this->title . '"', + 'deleted' => 'deleted post "' . $this->title . '"', + 'published' => 'published post "' . $this->title . '"', + default => $eventName . ' post', + }; + }); + } +} +``` + +## Workspace Isolation + +### Automatic Scoping + +Activity is automatically scoped to workspaces: + +```php +// Only returns activity for current workspace +$activity = Activity::forCurrentWorkspace()->get(); + +// Explicitly query another workspace (admin only) +if (auth()->user()->isSuperAdmin()) { + $activity = Activity::forWorkspace($otherWorkspace)->get(); +} +``` + +### Cross-Workspace Activity + +```php +// Admin reports across all workspaces +$systemActivity = Activity::withoutGlobalScopes()->get(); + +// Activity counts by workspace +$stats = Activity::withoutGlobalScopes() + ->select('workspace_id', DB::raw('count(*) as count')) + ->groupBy('workspace_id') + ->get(); +``` + +## Activity Cleanup + +### Automatic Pruning + +Configure automatic cleanup of old activity: + +```php +// config/core.php +'activity' => [ + 'retention_days' => 90, + 'cleanup_enabled' => true, +], +``` + +Schedule the cleanup command: + +```php +// app/Console/Kernel.php +protected function schedule(Schedule $schedule) +{ + $schedule->command('activity:prune') + ->daily() + ->at('02:00'); +} +``` + +### Manual Pruning + +```bash +# Delete activity older than configured retention period +php artisan activity:prune + +# Delete activity older than specific number of days +php artisan activity:prune --days=30 + +# Dry run (see what would be deleted) +php artisan activity:prune --dry-run +``` + +### Selective Deletion + +```php +// Delete activity for specific model +Activity::forSubject($post)->delete(); + +// Delete activity by log name +Activity::inLog('temporary')->delete(); + +// Delete activity older than date +Activity::where('created_at', '<', now()->subMonths(6))->delete(); +``` + +## Advanced Usage + +### Batch Logging + +Log multiple changes as a single activity: + +```php +activity()->enableLogging(); + +// Disable automatic logging temporarily +activity()->disableLogging(); + +Post::create([/*...*/]); // Not logged +Post::create([/*...*/]); // Not logged +Post::create([/*...*/]); // Not logged + +// Re-enable and log batch operation +activity()->enableLogging(); + +activity() + ->performedOn($workspace) + ->log('imported 100 posts'); +``` + +### Custom Activity Models + +Extend the activity model: + +```php +where('properties->public', true); + } + + public function wasSuccessful(): bool + { + return $this->properties['success'] ?? true; + } +} +``` + +Update config: + +```php +// config/activitylog.php +'activity_model' => App\Models\Activity::class, +``` + +### Queued Logging + +Log activity in the background for performance: + +```php +// In a job or listener +dispatch(function () use ($post, $user) { + activity() + ->performedOn($post) + ->causedBy($user) + ->log('processed'); +})->afterResponse(); +``` + +## GDPR Compliance + +### Anonymize User Data + +Don't log personally identifiable information: + +```php +// config/core.php +'activity' => [ + 'log_ip_address' => false, + 'anonymize_after_days' => 30, +], +``` + +### Anonymization + +```php +class AnonymizeOldActivity +{ + public function handle(): void + { + Activity::where('created_at', '<', now()->subDays(30)) + ->whereNotNull('causer_id') + ->update([ + 'causer_id' => null, + 'causer_type' => null, + 'properties->ip_address' => null, + ]); + } +} +``` + +### User Data Deletion + +Delete user's activity when account is deleted: + +```php +class User extends Model +{ + protected static function booted() + { + static::deleting(function ($user) { + // Delete or anonymize activity + Activity::causedBy($user)->delete(); + }); + } +} +``` + +## Performance Optimization + +### Eager Loading + +Prevent N+1 queries: + +```php +$activities = Activity::with(['causer', 'subject']) + ->latest() + ->paginate(20); +``` + +### Selective Logging + +Only log important changes: + +```php +class Post extends Model +{ + use LogsActivity; + + // Only log changes to these critical fields + protected array $activityLogAttributes = ['title', 'published_at', 'status']; + + // Only log when attributes actually change + protected static $logOnlyDirty = true; +} +``` + +### Disable Logging Temporarily + +```php +// Disable for bulk operations +activity()->disableLogging(); + +Post::query()->update(['migrated' => true]); + +activity()->enableLogging(); +``` + +## Testing + +### Testing Activity Logging + +```php + 'Test Post', + 'content' => 'Test content', + ]); + + $activity = Activity::forSubject($post)->first(); + + $this->assertEquals('created', $activity->description); + $this->assertEquals(auth()->id(), $activity->causer_id); + } + + public function test_logs_attribute_changes(): void + { + $post = Post::factory()->create(['title' => 'Original']); + + $post->update(['title' => 'Updated']); + + $activity = Activity::forSubject($post)->latest()->first(); + + $this->assertEquals('updated', $activity->description); + $this->assertEquals('Original', $activity->changes()['old']['title']); + $this->assertEquals('Updated', $activity->changes()['attributes']['title']); + } +} +``` + +## Best Practices + +### 1. Log Business Events + +```php +// ✅ Good - meaningful business events +$post->logActivity('published', ['published_at' => now()]); +$post->logActivity('featured', ['featured_until' => $date]); + +// ❌ Bad - technical implementation details +$post->logActivity('database_updated'); +``` + +### 2. Include Context + +```php +// ✅ Good - rich context +activity() + ->performedOn($post) + ->withProperties([ + 'published_at' => $post->published_at, + 'notification_sent' => true, + 'subscribers_count' => $subscribersCount, + ]) + ->log('published'); + +// ❌ Bad - minimal context +activity()->performedOn($post)->log('published'); +``` + +### 3. Use Descriptive Log Names + +```php +// ✅ Good - organized by domain +activity()->useLog('blog')->log('post published'); +activity()->useLog('commerce')->log('order placed'); + +// ❌ Bad - generic log name +activity()->useLog('default')->log('thing happened'); +``` + +## Learn More + +- [Activity Feed UI](/packages/admin#activity-feed) +- [GDPR Compliance](/security/gdpr) +- [Testing Activity](/testing/activity-logging) diff --git a/build/php/patterns/hlcrf.md b/build/php/patterns/hlcrf.md new file mode 100644 index 0000000..64725b2 --- /dev/null +++ b/build/php/patterns/hlcrf.md @@ -0,0 +1,872 @@ +# HLCRF Layout System + +HLCRF (Header-Left-Content-Right-Footer) is a hierarchical, composable layout system for building complex layouts with infinite nesting. It provides flexible region-based layouts without restricting HTML structure. + +## Overview + +Traditional Blade layouts force rigid inheritance hierarchies. HLCRF allows components to declare which layout regions they contribute to, enabling composition without structural constraints. + +**Use Cases:** +- Admin panels and dashboards +- Content management interfaces +- Marketing landing pages +- E-commerce product pages +- Documentation sites +- Any complex multi-region layout + +### Traditional Blade Layouts + +```blade +{{-- layouts/admin.blade.php --}} + + +
@yield('header')
+ +
@yield('content')
+ + + +{{-- pages/dashboard.blade.php --}} +@extends('layouts.admin') + +@section('header') + Dashboard Header +@endsection + +@section('content') + Dashboard Content +@endsection +``` + +**Problems:** +- Rigid structure +- Deep nesting +- Hard to compose sections +- Components can't contribute to multiple regions + +### HLCRF Approach + +```blade +{{-- pages/dashboard.blade.php --}} + + + Dashboard Header + + + + Navigation Menu + + + + Dashboard Content + + + + Sidebar Widgets + + +``` + +**Benefits:** +- Declarative region definition +- Easy composition +- Components contribute to any region +- No structural constraints + +## Layout Regions + +HLCRF defines five semantic regions: + +``` +┌────────────────────────────────────┐ +│ Header (H) │ +├──────┬─────────────────┬───────────┤ +│ │ │ │ +│ Left │ Content (C) │ Right │ +│ (L) │ │ (R) │ +│ │ │ │ +├──────┴─────────────────┴───────────┤ +│ Footer (F) │ +└────────────────────────────────────┘ +``` + +### Self-Documenting IDs + +Every HLCRF element receives a unique ID that describes its position in the DOM tree. This makes debugging, styling, and testing trivial: + +**ID Format:** `{Region}-{Index}-{NestedRegion}-{NestedIndex}...` + +**Examples:** +- `H-0` = First header element +- `L-1` = Second left sidebar element (0-indexed) +- `C-R-2` = Content region → Right sidebar → Third element +- `C-L-0-R-1` = Content → Left → First element → Right → Second element + +**Region Letters:** +- `H` = Header +- `L` = Left +- `C` = Content +- `R` = Right +- `F` = Footer + +**Benefits:** +1. **Instant debugging** - See element position from DevTools +2. **Precise CSS targeting** - No class soup needed +3. **Test selectors** - Stable IDs for E2E tests +4. **Documentation** - DOM structure is self-explanatory + +```html + +
+ +
+ +
+
+ + +
+ +
+ +
Main content
+
+ +
+ + +
+
+``` + +**CSS Examples:** + +```css +/* Target specific nested elements */ +#C-R-2 { width: 300px; } + +/* Target all right sidebars at any depth */ +[id$="-R-0"] { background: #f9f9f9; } + +/* Target deeply nested content regions */ +[id*="-C-"][id*="-C-"] { padding: 2rem; } + +/* Target second header element anywhere */ +[id^="H-1"], [id*="-H-1"] { font-weight: bold; } +``` + +### Header Region + +Top section for navigation, branding, global actions: + +```blade + + + +``` + +### Left Region + +Sidebar navigation, filters, secondary navigation: + +```blade + + + +``` + +### Content Region + +Main content area: + +```blade + +
+

Dashboard

+ +
+ + + +
+ +
+ +
+
+
+``` + +### Right Region + +Contextual help, related actions, widgets: + +```blade + + + +``` + +### Footer Region + +Copyright, links, status information: + +```blade + +
+ © 2026 Your Company. All rights reserved. + | + Privacy + | + Terms +
+
+``` + +## Component Composition + +### Multiple Components Contributing + +Components can contribute to multiple regions: + +```blade + + {{-- Page header --}} + + + + + {{-- Filters sidebar --}} + + + + + {{-- Main content --}} + + + + + {{-- Help sidebar --}} + + + + + +``` + +### Nested Layouts + +HLCRF layouts can be nested infinitely. Each element receives a unique, self-documenting ID that describes its position in the DOM tree: + +```blade +{{-- components/post-editor.blade.php --}} +
+ {{-- Nested HLCRF layout inside a parent layout --}} + + {{-- Editor toolbar goes to header --}} + + + + + {{-- Content editor --}} + + + + + {{-- Metadata sidebar --}} + + + + +
+``` + +**Generated IDs:** +```html +
+
+
+
+
+``` + +The ID format follows the pattern: +- Single letter = region type (`H`=Header, `L`=Left, `C`=Content, `R`=Right, `F`=Footer) +- Number = index within that region (0-based) +- Dash separates nesting levels + +This makes the DOM structure self-documenting and enables precise CSS targeting: + +```css +/* Target all right sidebars at any nesting level */ +[id$="-R-0"] { /* ... */ } + +/* Target deeply nested content areas */ +[id^="C-"][id*="-C-"] { /* ... */ } + +/* Target second element in any header */ +[id^="H-1"] { /* ... */ } +``` + +## Layout Variants + +### Two-Column Layout + +```blade + + + Navigation + + + + Main Content + + +``` + +### Three-Column Layout + +```blade + + + Left Sidebar + + + + Main Content + + + + Right Sidebar + + +``` + +### Full-Width Layout + +```blade + + + Header + + + + Full-Width Content + + +``` + +### Modal Layout + +```blade + + +

Edit Post

+
+ + +
...
+
+ + + Save + Cancel + +
+``` + +## Responsive Behavior + +HLCRF layouts adapt to screen size: + +```blade + + Sidebar + Content + Widgets + +``` + +**Result:** +- **Mobile:** Left → Content → Right (stacked vertically) +- **Tablet:** Left | Content (side-by-side) +- **Desktop:** Left | Content | Right (three columns) + +## Region Options + +### Collapsible Regions + +```blade + + Navigation Menu + +``` + +### Fixed Regions + +```blade + + Sticky Header + +``` + +### Scrollable Regions + +```blade + + Long Content + +``` + +### Region Width + +```blade + + Fixed width sidebar + + + + Percentage width sidebar + +``` + +## Conditional Regions + +### Show/Hide Based on Conditions + +```blade + + @auth + + + + @endauth + + + Main Content + + + @can('view-admin-sidebar') + + + + @endcan + +``` + +### Feature Flags + +```blade + + + Content + + + @feature('advanced-analytics') + + + + @endfeature + +``` + +## Styling + +### Custom Classes + +```blade + + + Header + + + + Content + + +``` + +### Slot Attributes + +```blade + + Dark Sidebar + +``` + +## Real-World Examples + +### Marketing Landing Page + +```blade + + {{-- Sticky header with CTA --}} + + + + + {{-- Hero section with sidebar --}} + + + + + + + + + + + + + + {{-- Footer with newsletter --}} + + + + + + + + + + + + +``` + +### E-Commerce Product Page + +```blade + + + + + + + + {{-- Product images --}} + + + + + {{-- Product details and buy box --}} + + + + + + + + {{-- Reviews and recommendations --}} + + + + + + + + + + + +``` + +### Blog with Ads + +```blade + + + + + + + + {{-- Sidebar navigation --}} + + + + + + {{-- Article content --}} + +
+

{{ $post->title }}

+ + {!! $post->content !!} + +
+ + +
+ + {{-- Widgets and ads --}} + + + + + + +
+
+ + + + +
+``` + +## Advanced Patterns + +### Dynamic Region Loading + +```blade + + + Main Content + + + + {{-- Load widgets based on page --}} + @foreach($widgets as $widget) + @include("widgets.{$widget}") + @endforeach + + +``` + +### Livewire Integration + +```blade + + + @livewire('global-search') + + + + @livewire('post-list') + + + + @livewire('post-filters') + + +``` + +### Portal Teleportation + +Send content to regions from anywhere: + +```blade +{{-- Page content --}} + +

My Page

+ + {{-- Component that teleports to header --}} + + Action 1 + Action 2 + +
+ +{{-- page-actions.blade.php component --}} + + {{ $slot }} + +``` + +## Implementation + +### Layout Component + +```php + + @if($header ?? false) +
+ {{ $header }} +
+ @endif + +
+ @if($left ?? false) +
+ {{ $left }} +
+ @endif + +
+ {{ $content ?? $slot }} +
+ + @if($right ?? false) +
+ {{ $right }} +
+ @endif +
+ + @if($footer ?? false) + + @endif + +``` + +## Testing + +### Component Testing + +```php +blade( + ' + Left + Content + Right + ' + ); + + $view->assertSee('Left'); + $view->assertSee('Content'); + $view->assertSee('Right'); + } + + public function test_optional_regions(): void + { + $view = $this->blade( + ' + Content Only + ' + ); + + $view->assertSee('Content Only'); + $view->assertDontSee('hlcrf-left'); + $view->assertDontSee('hlcrf-right'); + } +} +``` + +## Best Practices + +### 1. Use Semantic Regions + +```blade +{{-- ✅ Good - semantic use --}} +Global Navigation +Page Navigation +Main Content +Contextual Help + +{{-- ❌ Bad - misuse of regions --}} +Sidebar Content +Footer Content +``` + +### 2. Keep Regions Optional + +```blade +{{-- ✅ Good - gracefully handles missing regions --}} + + + Content works without sidebars + + +``` + +### 3. Consistent Widths + +```blade +{{-- ✅ Good - consistent sidebar widths --}} +Nav +Widgets +``` + +### 4. Mobile-First + +```blade +{{-- ✅ Good - stack on mobile --}} + +``` + +## Learn More + +- [Admin Components](/packages/admin#components) +- [Livewire Integration](/packages/admin#livewire) +- [Responsive Design](/patterns-guide/responsive-design) diff --git a/build/php/patterns/repositories.md b/build/php/patterns/repositories.md new file mode 100644 index 0000000..423d2d4 --- /dev/null +++ b/build/php/patterns/repositories.md @@ -0,0 +1,327 @@ +# Repository Pattern + +Repositories abstract data access logic and provide a consistent interface for querying data. + +## When to Use Repositories + +Use repositories for: +- Complex query logic +- Multiple data sources +- Abstracting Eloquent/Query Builder +- Testing with fake data + +**Don't use repositories for:** +- Simple Eloquent queries (use models directly) +- Single-use queries +- Over-engineering simple applications + +## Basic Repository + +```php +orderByDesc('published_at') + ->paginate($perPage); + } + + public function findBySlug(string $slug): ?Post + { + return Post::where('slug', $slug) + ->where('status', 'published') + ->first(); + } + + public function findPopular(int $limit = 10): Collection + { + return Post::where('status', 'published') + ->where('views', '>', 1000) + ->orderByDesc('views') + ->limit($limit) + ->get(); + } + + public function findRecent(int $days = 7, int $limit = 10): Collection + { + return Post::where('status', 'published') + ->where('published_at', '>=', now()->subDays($days)) + ->orderByDesc('published_at') + ->limit($limit) + ->get(); + } +} +``` + +**Usage:** + +```php +$repository = app(PostRepository::class); +$posts = $repository->findPublished(); +$post = $repository->findBySlug('laravel-tutorial'); +``` + +## Repository with Interface + +```php +orderByDesc('published_at') + ->paginate($perPage); + } + + // ... other methods +} +``` + +**Binding:** + +```php +// Service Provider +$this->app->bind( + PostRepositoryInterface::class, + EloquentPostRepository::class +); +``` + +## Repository with Criteria + +```php +query = Post::query(); + } + + public function published(): self + { + $this->query->where('status', 'published'); + return $this; + } + + public function byAuthor(int $authorId): self + { + $this->query->where('author_id', $authorId); + return $this; + } + + public function inCategory(int $categoryId): self + { + $this->query->where('category_id', $categoryId); + return $this; + } + + public function recent(int $days = 7): self + { + $this->query->where('created_at', '>=', now()->subDays($days)); + return $this; + } + + public function get(): Collection + { + return $this->query->get(); + } + + public function paginate(int $perPage = 20) + { + return $this->query->paginate($perPage); + } +} +``` + +**Usage:** + +```php +$repository = app(PostRepository::class); + +// Chain criteria +$posts = $repository + ->published() + ->byAuthor($authorId) + ->recent(30) + ->paginate(); +``` + +## Repository with Caching + +```php +repository->findPublished($perPage); + }); + } + + public function findBySlug(string $slug): ?Post + { + return Cache::remember("posts.slug.{$slug}", 3600, function () use ($slug) { + return $this->repository->findBySlug($slug); + }); + } + + public function findPopular(int $limit = 10): Collection + { + return Cache::remember("posts.popular.{$limit}", 600, function () use ($limit) { + return $this->repository->findPopular($limit); + }); + } +} +``` + +## Testing with Repositories + +```php +create(['status' => 'published']); + Post::factory()->create(['status' => 'draft']); + + $posts = $repository->findPublished(); + + $this->assertCount(1, $posts); + $this->assertEquals('published', $posts->first()->status); + } + + public function test_finds_post_by_slug(): void + { + $repository = app(PostRepository::class); + + $post = Post::factory()->create([ + 'slug' => 'laravel-tutorial', + 'status' => 'published', + ]); + + $found = $repository->findBySlug('laravel-tutorial'); + + $this->assertEquals($post->id, $found->id); + } +} +``` + +## Best Practices + +### 1. Keep Methods Focused + +```php +// ✅ Good - specific method +public function findPublishedInCategory(int $categoryId): Collection +{ + return Post::where('status', 'published') + ->where('category_id', $categoryId) + ->get(); +} + +// ❌ Bad - too generic +public function find(array $criteria): Collection +{ + $query = Post::query(); + + foreach ($criteria as $key => $value) { + $query->where($key, $value); + } + + return $query->get(); +} +``` + +### 2. Return Collections or Models + +```php +// ✅ Good - returns typed result +public function findBySlug(string $slug): ?Post +{ + return Post::where('slug', $slug)->first(); +} + +// ❌ Bad - returns array +public function findBySlug(string $slug): ?array +{ + return Post::where('slug', $slug)->first()?->toArray(); +} +``` + +### 3. Use Constructor Injection + +```php +// ✅ Good - injected +public function __construct( + protected PostRepositoryInterface $posts +) {} + +// ❌ Bad - instantiated +public function __construct() +{ + $this->posts = new PostRepository(); +} +``` + +## Learn More + +- [Service Pattern →](/patterns-guide/services) +- [Actions Pattern →](/patterns-guide/actions) diff --git a/build/php/patterns/seeders.md b/build/php/patterns/seeders.md new file mode 100644 index 0000000..b72ec49 --- /dev/null +++ b/build/php/patterns/seeders.md @@ -0,0 +1,656 @@ +# Seeder Discovery & Ordering + +Core PHP Framework provides automatic seeder discovery with dependency-based ordering. Define seeder dependencies using PHP attributes and let the framework handle execution order. + +## Overview + +Traditional Laravel seeders require manual ordering in `DatabaseSeeder`. Core PHP automatically discovers seeders across modules and orders them based on declared dependencies. + +### Traditional Approach + +```php +// database/seeders/DatabaseSeeder.php +class DatabaseSeeder extends Seeder +{ + public function run(): void + { + // Manual ordering - easy to get wrong + $this->call([ + WorkspaceSeeder::class, + UserSeeder::class, + CategorySeeder::class, + PostSeeder::class, + CommentSeeder::class, + ]); + } +} +``` + +**Problems:** +- Manual dependency management +- Order mistakes cause failures +- Scattered across modules but centrally managed +- Hard to maintain as modules grow + +### Discovery Approach + +```php +// Mod/Tenant/Database/Seeders/WorkspaceSeeder.php +#[SeederPriority(100)] +class WorkspaceSeeder extends Seeder +{ + public function run(): void { /* ... */ } +} + +// Mod/Blog/Database/Seeders/CategorySeeder.php +#[SeederPriority(50)] +#[SeederAfter(WorkspaceSeeder::class)] +class CategorySeeder extends Seeder +{ + public function run(): void { /* ... */ } +} + +// Mod/Blog/Database/Seeders/PostSeeder.php +#[SeederAfter(CategorySeeder::class)] +class PostSeeder extends Seeder +{ + public function run(): void { /* ... */ } +} +``` + +**Benefits:** +- Automatic discovery across modules +- Explicit dependency declarations +- Topological sorting handles execution order +- Circular dependency detection +- Each module manages its own seeders + +## Configuration + +### Enable Auto-Discovery + +```php +// config/core.php +'seeders' => [ + 'auto_discover' => env('SEEDERS_AUTO_DISCOVER', true), + 'paths' => [ + 'Mod/*/Database/Seeders', + 'Core/*/Database/Seeders', + 'Plug/*/Database/Seeders', + ], + 'exclude' => [ + 'DatabaseSeeder', + 'CoreDatabaseSeeder', + ], +], +``` + +### Create Core Seeder + +Create a root seeder that uses discovery: + +```php +getOrderedSeeders(); + + $this->call($seeders); + } +} +``` + +## Seeder Attributes + +### SeederPriority + +Set execution priority (higher = runs earlier): + +```php +count(3)->create(); + } +} +``` + +**Priority Ranges:** +- `100+` - Foundation data (workspaces, system records) +- `50-99` - Core domain data (users, categories) +- `1-49` - Feature data (posts, comments) +- `0` - Default priority +- `<0` - Post-processing (analytics, cache warming) + +### SeederAfter + +Run after specific seeders: + +```php +count(5)->create(); + } +} +``` + +### SeederBefore + +Run before specific seeders: + +```php +count(5)->create(); + } +} +``` + +### Combining Attributes + +Use multiple attributes for complex dependencies: + +```php +#[SeederPriority(50)] +#[SeederAfter(WorkspaceSeeder::class, UserSeeder::class)] +#[SeederBefore(CommentSeeder::class)] +class PostSeeder extends Seeder +{ + public function run(): void + { + Post::factory()->count(20)->create(); + } +} +``` + +## Execution Order + +### Topological Sorting + +The framework automatically orders seeders using topological sorting: + +``` +Given seeders: + - WorkspaceSeeder (priority: 100) + - UserSeeder (priority: 90, after: WorkspaceSeeder) + - CategorySeeder (priority: 50, after: WorkspaceSeeder) + - PostSeeder (priority: 40, after: CategorySeeder, UserSeeder) + - CommentSeeder (priority: 30, after: PostSeeder, UserSeeder) + +Execution order: + 1. WorkspaceSeeder (priority 100) + 2. UserSeeder (priority 90, depends on Workspace) + 3. CategorySeeder (priority 50, depends on Workspace) + 4. PostSeeder (priority 40, depends on Category & User) + 5. CommentSeeder (priority 30, depends on Post & User) +``` + +### Resolution Algorithm + +1. Group seeders by priority (high to low) +2. Within each priority group, perform topological sort +3. Detect circular dependencies +4. Execute in resolved order + +## Circular Dependency Detection + +The framework detects and prevents circular dependencies: + +```php +// ❌ This will throw CircularDependencyException + +#[SeederAfter(SeederB::class)] +class SeederA extends Seeder { } + +#[SeederAfter(SeederC::class)] +class SeederB extends Seeder { } + +#[SeederAfter(SeederA::class)] +class SeederC extends Seeder { } + +// Error: Circular dependency detected: SeederA → SeederB → SeederC → SeederA +``` + +## Module Seeders + +### Typical Module Structure + +``` +Mod/Blog/Database/Seeders/ +├── BlogSeeder.php # Optional: calls other seeders +├── CategorySeeder.php # Creates categories +├── PostSeeder.php # Creates posts +└── DemoContentSeeder.php # Creates demo data +``` + +### Module Seeder Example + +```php +call([ + CategorySeeder::class, + PostSeeder::class, + ]); + } +} +``` + +### Environment-Specific Seeding + +```php +#[SeederPriority(10)] +class DemoContentSeeder extends Seeder +{ + public function run(): void + { + // Only seed demo data in non-production + if (app()->environment('production')) { + return; + } + + Post::factory() + ->count(50) + ->published() + ->create(); + } +} +``` + +## Conditional Seeding + +### Feature Flags + +```php +class AnalyticsSeeder extends Seeder +{ + public function run(): void + { + if (! Feature::active('analytics')) { + $this->command->info('Skipping analytics seeder (feature disabled)'); + return; + } + + // Seed analytics data + } +} +``` + +### Configuration + +```php +class EmailSeeder extends Seeder +{ + public function run(): void + { + if (! config('modules.email.enabled')) { + return; + } + + EmailTemplate::factory()->count(10)->create(); + } +} +``` + +### Database Check + +```php +class MigrationSeeder extends Seeder +{ + public function run(): void + { + if (! Schema::hasTable('legacy_posts')) { + return; + } + + // Migrate legacy data + } +} +``` + +## Factory Integration + +Seeders commonly use factories: + +```php +count(5)->create(); + + // Create posts for each category + $categories->each(function ($category) { + Post::factory() + ->count(10) + ->for($category) + ->published() + ->create(); + }); + + // Create unpublished drafts + Post::factory() + ->count(5) + ->draft() + ->create(); + } +} +``` + +## Testing Seeders + +### Unit Testing + +```php +seed(PostSeeder::class); + + $this->assertDatabaseCount('posts', 20); + } + + public function test_posts_have_categories(): void + { + $this->seed(PostSeeder::class); + + $posts = Post::all(); + + $posts->each(function ($post) { + $this->assertNotNull($post->category_id); + }); + } +} +``` + +### Integration Testing + +```php +public function test_seeder_execution_order(): void +{ + $registry = app(SeederRegistry::class); + + $seeders = $registry->getOrderedSeeders(); + + $workspaceIndex = array_search(WorkspaceSeeder::class, $seeders); + $userIndex = array_search(UserSeeder::class, $seeders); + $postIndex = array_search(PostSeeder::class, $seeders); + + $this->assertLessThan($userIndex, $workspaceIndex); + $this->assertLessThan($postIndex, $userIndex); +} +``` + +### Circular Dependency Testing + +```php +public function test_detects_circular_dependencies(): void +{ + $this->expectException(CircularDependencyException::class); + + // Force circular dependency + $registry = app(SeederRegistry::class); + $registry->register([ + CircularA::class, + CircularB::class, + CircularC::class, + ]); + + $registry->getOrderedSeeders(); +} +``` + +## Performance + +### Chunking + +Seed large datasets in chunks: + +```php +public function run(): void +{ + $faker = Faker\Factory::create(); + + // Seed in chunks for better memory usage + for ($i = 0; $i < 10; $i++) { + Post::factory() + ->count(100) + ->create(); + + $this->command->info("Seeded batch " . ($i + 1) . "/10"); + } +} +``` + +### Database Transactions + +Wrap seeders in transactions for performance: + +```php +public function run(): void +{ + DB::transaction(function () { + Post::factory()->count(1000)->create(); + }); +} +``` + +### Disable Event Listeners + +Skip event listeners during seeding: + +```php +public function run(): void +{ + // Disable events for performance + Post::withoutEvents(function () { + Post::factory()->count(1000)->create(); + }); +} +``` + +## Debugging + +### Verbose Output + +```bash +# Show seeder execution details +php artisan db:seed --verbose + +# Show discovered seeders +php artisan db:seed --show-seeders +``` + +### Dry Run + +```bash +# Preview seeder order without executing +php artisan db:seed --dry-run +``` + +### Seeder Registry Inspection + +```php +$registry = app(SeederRegistry::class); + +// Get all discovered seeders +$seeders = $registry->getAllSeeders(); + +// Get execution order +$ordered = $registry->getOrderedSeeders(); + +// Get seeder metadata +$metadata = $registry->getMetadata(PostSeeder::class); +``` + +## Best Practices + +### 1. Use Priorities for Groups + +```php +// ✅ Good - clear priority groups +#[SeederPriority(100)] // Foundation +class WorkspaceSeeder { } + +#[SeederPriority(50)] // Core domain +class CategorySeeder { } + +#[SeederPriority(10)] // Feature data +class PostSeeder { } +``` + +### 2. Explicit Dependencies + +```php +// ✅ Good - explicit dependencies +#[SeederAfter(WorkspaceSeeder::class, CategorySeeder::class)] +class PostSeeder { } + +// ❌ Bad - implicit dependencies via priority alone +#[SeederPriority(40)] +class PostSeeder { } +``` + +### 3. Idempotent Seeders + +```php +// ✅ Good - safe to run multiple times +public function run(): void +{ + if (Category::exists()) { + return; + } + + Category::factory()->count(5)->create(); +} + +// ❌ Bad - creates duplicates +public function run(): void +{ + Category::factory()->count(5)->create(); +} +``` + +### 4. Environment Awareness + +```php +// ✅ Good - respects environment +public function run(): void +{ + $count = app()->environment('production') ? 10 : 100; + + Post::factory()->count($count)->create(); +} +``` + +### 5. Meaningful Names + +```php +// ✅ Good names +class WorkspaceSeeder { } +class BlogDemoContentSeeder { } +class LegacyPostMigrationSeeder { } + +// ❌ Bad names +class Seeder1 { } +class TestSeeder { } +class DataSeeder { } +``` + +## Running Seeders + +```bash +# Run all seeders +php artisan db:seed + +# Run specific seeder +php artisan db:seed --class=PostSeeder + +# Fresh database with seeding +php artisan migrate:fresh --seed + +# Seed specific modules +php artisan db:seed --module=Blog + +# Seed with environment +php artisan db:seed --env=staging +``` + +## Learn More + +- [Database Factories](/patterns-guide/factories) +- [Module System](/architecture/module-system) +- [Testing Seeders](/testing/seeders) diff --git a/build/php/patterns/services.md b/build/php/patterns/services.md new file mode 100644 index 0000000..a3b2f77 --- /dev/null +++ b/build/php/patterns/services.md @@ -0,0 +1,445 @@ +# Service Pattern + +Services encapsulate business logic and coordinate between multiple models or external systems. + +## When to Use Services + +Use services for: +- Complex business logic involving multiple models +- External API integrations +- Operations requiring multiple steps +- Reusable functionality across controllers + +**Don't use services for:** +- Simple CRUD operations (use Actions) +- Single-model operations +- View logic (use View Models) + +## Basic Service + +```php +validateReadyForPublish($post); + + // Update post + $post->update([ + 'status' => 'published', + 'published_at' => now(), + 'published_by' => $user->id, + ]); + + // Generate SEO metadata + $this->generateSeoMetadata($post); + + // Notify subscribers + $this->notifySubscribers($post); + + // Update search index + $post->searchable(); + + return $post->fresh(); + } + + protected function validateReadyForPublish(Post $post): void + { + if (empty($post->title)) { + throw new ValidationException('Post must have a title'); + } + + if (empty($post->content)) { + throw new ValidationException('Post must have content'); + } + + if (!$post->featured_image) { + throw new ValidationException('Post must have a featured image'); + } + } + + protected function generateSeoMetadata(Post $post): void + { + if (empty($post->meta_description)) { + $post->meta_description = str($post->content) + ->stripTags() + ->limit(160); + } + + if (empty($post->og_image)) { + GenerateOgImageJob::dispatch($post); + } + + $post->save(); + } + + protected function notifySubscribers(Post $post): void + { + NotifySubscribersJob::dispatch($post); + } +} +``` + +**Usage:** + +```php +$service = app(PostPublishingService::class); +$publishedPost = $service->publish($post, auth()->user()); +``` + +## Service with Constructor Injection + +```php +apiUrl}/events", [ + 'api_key' => $this->apiKey, + 'event' => 'pageview', + 'url' => $url, + 'meta' => $meta, + ]); + } + + public function getPageViews(string $url, int $days = 30): int + { + return Cache::remember( + "analytics.pageviews.{$url}.{$days}", + now()->addHour(), + fn () => Http::get("{$this->apiUrl}/stats", [ + 'api_key' => $this->apiKey, + 'url' => $url, + 'days' => $days, + ])->json('views') + ); + } +} +``` + +**Service Provider:** + +```php +$this->app->singleton(AnalyticsService::class, function () { + return new AnalyticsService( + apiKey: config('analytics.api_key'), + apiUrl: config('analytics.api_url') + ); +}); +``` + +## Service Contracts + +Define interfaces for flexibility: + +```php +client->paymentIntents->create([ + 'amount' => $amount, + 'currency' => $currency, + 'metadata' => $meta, + ]); + + return new PaymentResult( + success: $intent->status === 'succeeded', + transactionId: $intent->id, + amount: $intent->amount, + currency: $intent->currency + ); + } + + // ... other methods +} +``` + +## Service with Dependencies + +```php +inventory->available($order->items)) { + return ProcessingResult::failed('Insufficient inventory'); + } + + // Reserve inventory + $this->inventory->reserve($order->items); + + try { + // Charge payment + $payment = $this->payment->charge( + amount: $order->total, + currency: $order->currency, + meta: ['order_id' => $order->id] + ); + + if (!$payment->success) { + $this->inventory->release($order->items); + return ProcessingResult::failed('Payment failed'); + } + + // Update order + $order->update([ + 'status' => 'paid', + 'transaction_id' => $payment->transactionId, + 'paid_at' => now(), + ]); + + // Send confirmation + $this->email->send( + to: $order->customer->email, + template: 'order-confirmation', + data: compact('order', 'payment') + ); + + return ProcessingResult::success($order); + + } catch (\Exception $e) { + $this->inventory->release($order->items); + throw $e; + } + } +} +``` + +## Service with Events + +```php +update([ + 'status' => 'scheduled', + 'publish_at' => $publishAt, + ]); + + // Dispatch event + event(new PostScheduled($post, $publishAt)); + + // Queue job to publish + PublishScheduledPostJob::dispatch($post) + ->delay($publishAt); + } + + public function publishScheduledPost(Post $post): void + { + if ($post->status !== 'scheduled') { + throw new InvalidStateException('Post is not scheduled'); + } + + $post->update([ + 'status' => 'published', + 'published_at' => now(), + ]); + + event(new PostPublished($post)); + } +} +``` + +## Testing Services + +```php +create(); + $post = Post::factory()->create(['status' => 'draft']); + + $result = $service->publish($post, $user); + + $this->assertEquals('published', $result->status); + $this->assertNotNull($result->published_at); + $this->assertEquals($user->id, $result->published_by); + } + + public function test_validates_post_before_publishing(): void + { + $service = app(PostPublishingService::class); + $user = User::factory()->create(); + $post = Post::factory()->create([ + 'title' => '', + 'status' => 'draft', + ]); + + $this->expectException(ValidationException::class); + + $service->publish($post, $user); + } + + public function test_generates_seo_metadata(): void + { + $service = app(PostPublishingService::class); + $user = User::factory()->create(); + $post = Post::factory()->create([ + 'content' => 'Long content here...', + 'meta_description' => null, + ]); + + $result = $service->publish($post, $user); + + $this->assertNotNull($result->meta_description); + } +} +``` + +## Best Practices + +### 1. Single Responsibility + +```php +// ✅ Good - focused service +class EmailVerificationService +{ + public function sendVerificationEmail(User $user): void {} + public function verify(string $token): bool {} + public function resend(User $user): void {} +} + +// ❌ Bad - too broad +class UserService +{ + public function create() {} + public function sendEmail() {} + public function processPayment() {} + public function generateReport() {} +} +``` + +### 2. Dependency Injection + +```php +// ✅ Good - injected dependencies +public function __construct( + protected EmailService $email, + protected PaymentGateway $payment +) {} + +// ❌ Bad - hard-coded dependencies +public function __construct() +{ + $this->email = new EmailService(); + $this->payment = new StripeGateway(); +} +``` + +### 3. Return Types + +```php +// ✅ Good - explicit return type +public function process(Order $order): ProcessingResult +{ + return new ProcessingResult(...); +} + +// ❌ Bad - no return type +public function process(Order $order) +{ + return [...]; +} +``` + +### 4. Error Handling + +```php +// ✅ Good - handle errors gracefully +public function process(Order $order): ProcessingResult +{ + try { + $result = $this->payment->charge($order->total); + + return ProcessingResult::success($result); + } catch (PaymentException $e) { + Log::error('Payment failed', ['order' => $order->id, 'error' => $e->getMessage()]); + + return ProcessingResult::failed($e->getMessage()); + } +} +``` + +## Learn More + +- [Actions Pattern →](/patterns-guide/actions) +- [Repository Pattern →](/patterns-guide/repositories) diff --git a/build/php/quick-start.md b/build/php/quick-start.md new file mode 100644 index 0000000..f7aca84 --- /dev/null +++ b/build/php/quick-start.md @@ -0,0 +1,639 @@ +# Quick Start + +This tutorial walks you through creating your first module with Core PHP Framework. We'll build a simple blog module with posts, categories, and a public-facing website. + +## Prerequisites + +- Core PHP Framework installed ([Installation Guide](/guide/installation)) +- Database configured +- Basic Laravel knowledge + +## Step 1: Create the Module + +Use the Artisan command to scaffold a new module: + +```bash +php artisan make:mod Blog +``` + +This creates the following structure: + +``` +app/Mod/Blog/ +├── Boot.php # Module entry point +├── Actions/ # Business logic +├── Models/ # Eloquent models +├── Routes/ +│ ├── web.php # Public routes +│ ├── admin.php # Admin routes +│ └── api.php # API routes +├── Views/ # Blade templates +├── Migrations/ # Database migrations +├── Database/ +│ ├── Factories/ # Model factories +│ └── Seeders/ # Database seeders +└── config.php # Module configuration +``` + +## Step 2: Define Lifecycle Events + +Open `app/Mod/Blog/Boot.php` and declare which events your module listens to: + +```php + 'onWebRoutes', + AdminPanelBooting::class => 'onAdmin', + ApiRoutesRegistering::class => 'onApiRoutes', + ]; + + public function onWebRoutes(WebRoutesRegistering $event): void + { + $event->views('blog', __DIR__.'/Views'); + $event->routes(fn () => require __DIR__.'/Routes/web.php'); + } + + public function onAdmin(AdminPanelBooting $event): void + { + $event->routes(fn () => require __DIR__.'/Routes/admin.php'); + $event->menu(new BlogMenuProvider()); + } + + public function onApiRoutes(ApiRoutesRegistering $event): void + { + $event->routes(fn () => require __DIR__.'/Routes/api.php'); + } +} +``` + +## Step 3: Create Models + +Create a `Post` model at `app/Mod/Blog/Models/Post.php`: + +```php + 'datetime', + ]; + + // Activity log configuration + protected array $activityLogAttributes = ['title', 'published_at']; + + public function category() + { + return $this->belongsTo(Category::class); + } + + public function scopePublished($query) + { + return $query->whereNotNull('published_at') + ->where('published_at', '<=', now()); + } +} +``` + +## Step 4: Create Migration + +Create a migration at `app/Mod/Blog/Migrations/2026_01_01_000001_create_blog_tables.php`: + +```php +id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + + Schema::create('blog_posts', function (Blueprint $table) { + $table->id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->foreignId('category_id')->nullable()->constrained('blog_categories')->nullOnDelete(); + $table->string('title'); + $table->string('slug')->unique(); + $table->text('excerpt')->nullable(); + $table->longText('content'); + $table->timestamp('published_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + + $table->index(['workspace_id', 'published_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('blog_posts'); + Schema::dropIfExists('blog_categories'); + } +}; +``` + +Run the migration: + +```bash +php artisan migrate +``` + +## Step 5: Create Actions + +Create a `CreatePost` action at `app/Mod/Blog/Actions/CreatePost.php`: + +```php +update($data); + + return $post->fresh(); + } +} +``` + +## Step 6: Create Routes + +Define web routes in `app/Mod/Blog/Routes/web.php`: + +```php +group(function () { + Route::get('/blog', [BlogController::class, 'index'])->name('index'); + Route::get('/blog/{slug}', [BlogController::class, 'show'])->name('show'); + Route::get('/blog/category/{slug}', [BlogController::class, 'category'])->name('category'); +}); +``` + +Define admin routes in `app/Mod/Blog/Routes/admin.php`: + +```php +name('admin.blog.')->group(function () { + Route::resource('posts', PostController::class); + Route::resource('categories', CategoryController::class); + + Route::post('posts/{post}/publish', [PostController::class, 'publish']) + ->name('posts.publish'); +}); +``` + +## Step 7: Create Controllers + +Create a web controller at `app/Mod/Blog/Controllers/BlogController.php`: + +```php +published() + ->latest('published_at') + ->paginate(12); + + return view('blog::index', compact('posts')); + } + + public function show(string $slug) + { + $post = Post::with('category') + ->where('slug', $slug) + ->published() + ->firstOrFail(); + + return view('blog::show', compact('post')); + } + + public function category(string $slug) + { + $category = Category::where('slug', $slug)->firstOrFail(); + + $posts = Post::with('category') + ->where('category_id', $category->id) + ->published() + ->latest('published_at') + ->paginate(12); + + return view('blog::category', compact('category', 'posts')); + } +} +``` + +Create an admin controller at `app/Mod/Blog/Controllers/Admin/PostController.php`: + +```php +validated()); + + return redirect() + ->route('admin.blog.posts.edit', $post) + ->with('success', 'Post created successfully'); + } + + public function edit(Post $post) + { + return view('blog::admin.posts.edit', compact('post')); + } + + public function update(UpdatePostRequest $request, Post $post) + { + UpdatePost::run($post, $request->validated()); + + return back()->with('success', 'Post updated successfully'); + } + + public function destroy(Post $post) + { + $post->delete(); + + return redirect() + ->route('admin.blog.posts.index') + ->with('success', 'Post deleted successfully'); + } + + public function publish(Post $post) + { + UpdatePost::run($post, [ + 'published_at' => now(), + ]); + + return back()->with('success', 'Post published successfully'); + } +} +``` + +## Step 8: Create Admin Menu + +Create a menu provider at `app/Mod/Blog/BlogMenuProvider.php`: + +```php +icon('newspaper') + ->priority(30) + ->children([ + MenuItemBuilder::make('Posts') + ->route('admin.blog.posts.index') + ->icon('document-text'), + + MenuItemBuilder::make('Categories') + ->route('admin.blog.categories.index') + ->icon('folder'), + + MenuItemBuilder::make('New Post') + ->route('admin.blog.posts.create') + ->icon('plus-circle'), + ]) + ->build(), + ]; + } +} +``` + +## Step 9: Create Views + +Create a blog index view at `app/Mod/Blog/Views/index.blade.php`: + +```blade +@extends('layouts.app') + +@section('content') +
+

Blog

+ +
+ @foreach($posts as $post) + + @endforeach +
+ +
+ {{ $posts->links() }} +
+
+@endsection +``` + +## Step 10: Create Seeder (Optional) + +Create a seeder at `app/Mod/Blog/Database/Seeders/BlogSeeder.php`: + +```php + 'Technology', + 'slug' => 'technology', + 'description' => 'Technology news and articles', + ]); + + $design = Category::create([ + 'name' => 'Design', + 'slug' => 'design', + 'description' => 'Design tips and inspiration', + ]); + + // Create posts + Post::create([ + 'category_id' => $tech->id, + 'title' => 'Getting Started with Core PHP', + 'slug' => 'getting-started-with-core-php', + 'excerpt' => 'Learn how to build modular Laravel applications.', + 'content' => '

Full article content here...

', + 'published_at' => now()->subDays(7), + ]); + + Post::create([ + 'category_id' => $design->id, + 'title' => 'Modern UI Design Patterns', + 'slug' => 'modern-ui-design-patterns', + 'excerpt' => 'Explore contemporary design patterns for web applications.', + 'content' => '

Full article content here...

', + 'published_at' => now()->subDays(3), + ]); + } +} +``` + +Run the seeder: + +```bash +php artisan db:seed --class=Mod\\Blog\\Database\\Seeders\\BlogSeeder +``` + +Or use auto-discovery: + +```bash +php artisan db:seed +``` + +## Step 11: Test Your Module + +Visit your blog: + +``` +http://your-app.test/blog +``` + +Access the admin panel: + +``` +http://your-app.test/admin/blog/posts +``` + +## Next Steps + +Now that you've created your first module, explore more advanced features: + +### Add API Endpoints + +Create API resources and controllers for programmatic access: + +- [API Package Documentation](/packages/api) +- [OpenAPI Documentation](/packages/api#openapi-documentation) + +### Add Activity Logging + +Track changes to your posts: + +- [Activity Logging Guide](/patterns-guide/activity-logging) + +### Add Search Functionality + +Integrate with the unified search system: + +- [Search Integration](/patterns-guide/search) + +### Add Workspace Caching + +Optimize database queries with team-scoped caching: + +- [Workspace Caching](/patterns-guide/multi-tenancy#workspace-caching) + +### Add Tests + +Create feature tests for your module: + +```bash +php artisan make:test Mod/Blog/PostTest +``` + +Example test: + +```php + 'Test Post', + 'content' => 'Test content', + ]); + + $this->assertDatabaseHas('blog_posts', [ + 'title' => 'Test Post', + 'slug' => 'test-post', + ]); + } + + public function test_published_posts_are_visible(): void + { + Post::factory()->create([ + 'published_at' => now()->subDay(), + ]); + + $response = $this->get('/blog'); + + $response->assertStatus(200); + } +} +``` + +## Learn More + +- [Architecture Overview](/architecture/lifecycle-events) +- [Actions Pattern](/patterns-guide/actions) +- [Multi-Tenancy Guide](/patterns-guide/multi-tenancy) +- [Admin Panel Customization](/packages/admin) diff --git a/build/php/search.md b/build/php/search.md new file mode 100644 index 0000000..5b7f39b --- /dev/null +++ b/build/php/search.md @@ -0,0 +1,607 @@ +# Unified Search + +Powerful cross-model search with analytics, suggestions, and highlighting. + +## Basic Usage + +### Setting Up Search + +```php + $this->title, + 'content' => strip_tags($this->content), + 'category' => $this->category->name, + 'tags' => $this->tags->pluck('name')->join(', '), + 'author' => $this->author->name, + ]; + } +} +``` + +### Searching + +```php +use Mod\Blog\Models\Post; + +// Simple search +$results = Post::search('laravel tutorial')->get(); + +// Paginated search +$results = Post::search('php') + ->paginate(20); + +// With constraints +$results = Post::search('api') + ->where('status', 'published') + ->where('category_id', 5) + ->get(); +``` + +## Unified Search + +Search across multiple models: + +```php +use Core\Search\Unified; + +$search = app(Unified::class); + +// Search everything +$results = $search->search('api documentation', [ + \Mod\Blog\Models\Post::class, + \Mod\Docs\Models\Page::class, + \Mod\Shop\Models\Product::class, +]); + +// Returns grouped results +[ + 'posts' => [...], + 'pages' => [...], + 'products' => [...], +] +``` + +### Weighted Results + +```php +// Boost specific models +$results = $search->search('tutorial', [ + \Mod\Blog\Models\Post::class => 2.0, // 2x weight + \Mod\Docs\Models\Page::class => 1.5, // 1.5x weight + \Mod\Video\Models\Video::class => 1.0, // Normal weight +]); +``` + +### Result Limiting + +```php +// Limit results per model +$results = $search->search('api', [ + \Mod\Blog\Models\Post::class, + \Mod\Docs\Models\Page::class, +], perModel: 5); // Max 5 results per model +``` + +## Search Analytics + +Track search queries and clicks: + +```php +use Core\Search\Analytics\SearchAnalytics; + +$analytics = app(SearchAnalytics::class); + +// Record search +$analytics->recordSearch( + query: 'laravel tutorial', + results: 42, + user: auth()->user() +); + +// Record click-through +$analytics->recordClick( + query: 'laravel tutorial', + resultId: $post->id, + resultType: Post::class, + position: 3 // 3rd result clicked +); +``` + +### Analytics Queries + +```php +// Popular searches +$popular = $analytics->popularSearches(limit: 10); + +// Recent searches +$recent = $analytics->recentSearches(limit: 20); + +// Zero-result searches (need attention!) +$empty = $analytics->emptySearches(); + +// Click-through rate +$ctr = $analytics->clickThroughRate('laravel tutorial'); + +// Average position of clicks +$avgPosition = $analytics->averageClickPosition('api docs'); +``` + +### Search Dashboard + +```php +use Core\Search\Analytics\SearchAnalytics; + +class SearchDashboard extends Component +{ + public function render() + { + $analytics = app(SearchAnalytics::class); + + return view('search.dashboard', [ + 'totalSearches' => $analytics->totalSearches(), + 'uniqueQueries' => $analytics->uniqueQueries(), + 'avgResultsPerSearch' => $analytics->averageResults(), + 'popularSearches' => $analytics->popularSearches(10), + 'emptySearches' => $analytics->emptySearches(), + ]); + } +} +``` + +## Search Suggestions + +Autocomplete and query suggestions: + +```php +use Core\Search\Suggestions\SearchSuggestions; + +$suggestions = app(SearchSuggestions::class); + +// Get suggestions for partial query +$results = $suggestions->suggest('lar', [ + \Mod\Blog\Models\Post::class, +]); + +// Returns: +[ + 'laravel', + 'laravel tutorial', + 'laravel api', + 'laravel testing', +] +``` + +### Configuration + +```php +// config/search.php +return [ + 'suggestions' => [ + 'enabled' => true, + 'min_length' => 2, // Minimum query length + 'max_results' => 10, // Max suggestions + 'cache_ttl' => 3600, // Cache for 1 hour + 'learn_from_searches' => true, // Build from analytics + ], +]; +``` + +### Livewire Autocomplete + +```php +class SearchBox extends Component +{ + public $query = ''; + public $suggestions = []; + + public function updatedQuery() + { + if (strlen($this->query) < 2) { + $this->suggestions = []; + return; + } + + $suggestions = app(SearchSuggestions::class); + $this->suggestions = $suggestions->suggest($this->query, [ + Post::class, + Page::class, + ]); + } + + public function render() + { + return view('livewire.search-box'); + } +} +``` + +```blade +
+ + + @if(count($suggestions) > 0) +
    + @foreach($suggestions as $suggestion) +
  • + {{ $suggestion }} +
  • + @endforeach +
+ @endif +
+``` + +## Highlighting + +Highlight matching terms in results: + +```php +use Core\Search\Support\SearchHighlighter; + +$highlighter = app(SearchHighlighter::class); + +// Highlight text +$highlighted = $highlighter->highlight( + text: $post->title, + query: 'laravel tutorial', + tag: 'mark' +); + +// Returns: "Getting started with Laravel Tutorial" +``` + +### Configuration + +```php +// config/search.php +return [ + 'highlighting' => [ + 'enabled' => true, + 'tag' => 'mark', // HTML tag to use + 'class' => 'highlight', // CSS class + 'max_length' => 200, // Snippet length + 'context' => 50, // Context around match + ], +]; +``` + +### Blade Component + +```blade + +

{{ $post->title }}

+

{!! highlight($post->excerpt, $query) !!}

+
+``` + +**Helper Function:** + +```php +// helpers.php +function highlight(string $text, string $query, string $tag = 'mark'): string +{ + return app(SearchHighlighter::class)->highlight($text, $query, $tag); +} +``` + +## Filtering & Faceting + +### Adding Filters + +```php +// Search with filters +$results = Post::search('tutorial') + ->where('status', 'published') + ->where('category_id', 5) + ->where('created_at', '>=', now()->subDays(30)) + ->get(); +``` + +### Faceted Search + +```php +use Laravel\Scout\Builder; + +// Get facet counts +$facets = Post::search('api') + ->with('category') + ->get() + ->groupBy('category.name') + ->map->count(); + +// Returns: +[ + 'Tutorials' => 12, + 'Documentation' => 8, + 'News' => 5, +] +``` + +### Livewire Facets + +```php +class FacetedSearch extends Component +{ + public $query = ''; + public $category = null; + public $status = 'published'; + + public function render() + { + $results = Post::search($this->query) + ->when($this->category, fn($q) => $q->where('category_id', $this->category)) + ->where('status', $this->status) + ->paginate(20); + + $facets = Post::search($this->query) + ->where('status', $this->status) + ->get() + ->groupBy('category.name') + ->map->count(); + + return view('livewire.faceted-search', [ + 'results' => $results, + 'facets' => $facets, + ]); + } +} +``` + +## Scout Drivers + +### Meilisearch (Recommended) + +```bash +# Install Meilisearch +brew install meilisearch + +# Start server +meilisearch --master-key=YOUR_MASTER_KEY +``` + +**Configuration:** + +```php +// config/scout.php +return [ + 'driver' => 'meilisearch', + + 'meilisearch' => [ + 'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'), + 'key' => env('MEILISEARCH_KEY'), + ], +]; +``` + +### Database Driver + +For small applications: + +```php +// config/scout.php +return [ + 'driver' => 'database', +]; +``` + +**Limitations:** +- No relevance scoring +- No typo tolerance +- Slower for large datasets +- Good for < 10,000 records + +### Algolia + +```php +// config/scout.php +return [ + 'driver' => 'algolia', + + 'algolia' => [ + 'id' => env('ALGOLIA_APP_ID'), + 'secret' => env('ALGOLIA_SECRET'), + ], +]; +``` + +## Indexing + +### Manual Indexing + +```bash +# Index all records +php artisan scout:import "Mod\Blog\Models\Post" + +# Flush index +php artisan scout:flush "Mod\Blog\Models\Post" + +# Re-import +php artisan scout:flush "Mod\Blog\Models\Post" +php artisan scout:import "Mod\Blog\Models\Post" +``` + +### Conditional Indexing + +```php +class Post extends Model +{ + use Searchable; + + public function shouldBeSearchable(): bool + { + return $this->status === 'published'; + } +} +``` + +### Batch Indexing + +```php +// Automatically batched +Post::chunk(100, function ($posts) { + $posts->searchable(); +}); +``` + +## Performance + +### Eager Loading + +```php +// ✅ Good - eager load relationships +$results = Post::search('tutorial') + ->with(['category', 'author', 'tags']) + ->get(); + +// ❌ Bad - N+1 queries +$results = Post::search('tutorial')->get(); +foreach ($results as $post) { + echo $post->category->name; // Query per post +} +``` + +### Result Caching + +```php +use Illuminate\Support\Facades\Cache; + +// Cache search results +$results = Cache::remember( + "search:{$query}:{$page}", + now()->addMinutes(5), + fn () => Post::search($query)->paginate(20) +); +``` + +### Query Throttling + +```php +// Rate limit search endpoint +Route::middleware('throttle:60,1') + ->get('/search', [SearchController::class, 'index']); +``` + +## Best Practices + +### 1. Index Only What's Needed + +```php +// ✅ Good - essential fields only +public function toSearchableArray(): array +{ + return [ + 'title' => $this->title, + 'content' => strip_tags($this->content), + ]; +} + +// ❌ Bad - too much data +public function toSearchableArray(): array +{ + return $this->toArray(); // Includes everything! +} +``` + +### 2. Use Conditional Indexing + +```php +// ✅ Good - index published only +public function shouldBeSearchable(): bool +{ + return $this->status === 'published'; +} + +// ❌ Bad - index drafts +public function shouldBeSearchable(): bool +{ + return true; +} +``` + +### 3. Track Analytics + +```php +// ✅ Good - record searches +$analytics->recordSearch($query, $results->count()); + +// Use analytics to improve search +$emptySearches = $analytics->emptySearches(); +// Add synonyms, fix typos, expand content +``` + +### 4. Provide Suggestions + +```php +// ✅ Good - help users find content + + +@if($suggestions) +
    + @foreach($suggestions as $suggestion) +
  • {{ $suggestion }}
  • + @endforeach +
+@endif +``` + +## Testing + +```php +use Tests\TestCase; +use Mod\Blog\Models\Post; + +class SearchTest extends TestCase +{ + public function test_searches_posts(): void + { + Post::factory()->create(['title' => 'Laravel Tutorial']); + Post::factory()->create(['title' => 'PHP Basics']); + + $results = Post::search('laravel')->get(); + + $this->assertCount(1, $results); + $this->assertEquals('Laravel Tutorial', $results->first()->title); + } + + public function test_filters_results(): void + { + Post::factory()->create([ + 'title' => 'Laravel Tutorial', + 'status' => 'published', + ]); + + Post::factory()->create([ + 'title' => 'Laravel Guide', + 'status' => 'draft', + ]); + + $results = Post::search('laravel') + ->where('status', 'published') + ->get(); + + $this->assertCount(1, $results); + } +} +``` + +## Learn More + +- [Configuration →](/core/configuration) +- [Global Search →](/packages/admin/search) diff --git a/build/php/security.md b/build/php/security.md new file mode 100644 index 0000000..e124e52 --- /dev/null +++ b/build/php/security.md @@ -0,0 +1,609 @@ +# Security Overview + +Core PHP Framework is built with security as a foundational principle. This guide covers the security features, best practices, and considerations for building secure applications. + +## Security Features + +### Multi-Tenant Isolation + +Complete data isolation between workspaces and namespaces: + +```php +// Workspace-scoped models +class Post extends Model +{ + use BelongsToWorkspace; // Automatic workspace isolation +} + +// Namespace-scoped models +class Page extends Model +{ + use BelongsToNamespace; // Automatic namespace isolation +} +``` + +**Protection:** +- Automatic query scoping +- Workspace context validation +- Strict mode enforcement +- Cache isolation + +[Learn more about Multi-Tenancy →](/architecture/multi-tenancy) +[Learn more about Namespaces →](/security/namespaces) + +### API Security + +#### Secure API Keys + +API keys are hashed with bcrypt and never stored in plaintext: + +```php +$apiKey = ApiKey::create([ + 'name' => 'Mobile App', + 'workspace_id' => $workspace->id, + 'scopes' => ['posts:read', 'posts:write'], +]); + +// Plaintext key only shown once! +$plaintext = $apiKey->plaintext_key; // sk_live_... + +// Hash stored in database +// Verification uses bcrypt's secure comparison +``` + +**Features:** +- Bcrypt hashing +- Key rotation with grace period +- Scope-based permissions +- Rate limiting per key +- Usage tracking + +#### Scope Enforcement + +Fine-grained API permissions: + +```php +// Middleware enforces scopes +Route::middleware('scope:posts:write') + ->post('/posts', [PostController::class, 'store']); + +// Check scopes in code +if (! $request->user()->tokenCan('posts:delete')) { + abort(403, 'Insufficient permissions'); +} +``` + +**Available Scopes:** +- `posts:read`, `posts:write`, `posts:delete` +- `categories:read`, `categories:write` +- `analytics:read` +- `webhooks:manage` +- `keys:manage` + +#### Rate Limiting + +Tier-based rate limiting prevents abuse: + +```php +// config/core-api.php +'rate_limits' => [ + 'tiers' => [ + 'free' => ['requests' => 1000, 'window' => 60], + 'pro' => ['requests' => 10000, 'window' => 60], + 'enterprise' => ['requests' => null], // unlimited + ], +], +``` + +**Response Headers:** +``` +X-RateLimit-Limit: 10000 +X-RateLimit-Remaining: 9995 +X-RateLimit-Reset: 1640995200 +``` + +#### Webhook Signatures + +HMAC-SHA256 signatures prevent tampering: + +```php +// Webhook payload signing +$signature = hash_hmac( + 'sha256', + $timestamp . '.' . $payload, + $webhookSecret +); + +// Verification +if (! hash_equals($expected, $signature)) { + abort(401, 'Invalid signature'); +} + +// Timestamp validation prevents replay attacks +if (abs(time() - $timestamp) > 300) { + abort(401, 'Request too old'); +} +``` + +[Learn more about API Security →](/packages/api) + +### SQL Injection Prevention + +Multi-layer protection for database queries: + +```php +// config/core-mcp.php +'database' => [ + 'validation' => [ + 'enabled' => true, + 'blocked_keywords' => ['INSERT', 'UPDATE', 'DELETE', 'DROP'], + 'blocked_tables' => ['users', 'api_keys', 'password_resets'], + 'whitelist_enabled' => false, + ], +], +``` + +**Validation Layers:** +1. **Keyword blocking** - Block dangerous SQL keywords +2. **Table restrictions** - Prevent access to sensitive tables +3. **Pattern detection** - Detect SQL injection patterns +4. **Whitelist validation** - Optional pre-approved queries +5. **Read-only connections** - Separate connection without write access + +**Example:** + +```php +class QueryDatabaseTool extends Tool +{ + public function handle(Request $request): Response + { + $query = $request->input('query'); + + // Validates against all layers + $this->validator->validate($query); + + // Execute on read-only connection + $results = DB::connection('mcp_readonly')->select($query); + + return Response::success(['rows' => $results]); + } +} +``` + +[Learn more about MCP Security →](/packages/mcp) + +### Security Headers + +Comprehensive security headers protect against common attacks: + +```php +// config/core.php +'security_headers' => [ + 'csp' => [ + 'enabled' => true, + 'report_only' => false, + 'directives' => [ + 'default-src' => ["'self'"], + 'script-src' => ["'self'", "'nonce'"], + 'style-src' => ["'self'", "'unsafe-inline'"], + 'img-src' => ["'self'", 'data:', 'https:'], + 'connect-src' => ["'self'"], + 'font-src' => ["'self'", 'data:'], + 'object-src' => ["'none'"], + 'base-uri' => ["'self'"], + 'form-action' => ["'self'"], + 'frame-ancestors' => ["'none'"], + ], + ], + 'hsts' => [ + 'enabled' => true, + 'max_age' => 31536000, // 1 year + 'include_subdomains' => true, + 'preload' => true, + ], + 'x_frame_options' => 'DENY', + 'x_content_type_options' => 'nosniff', + 'x_xss_protection' => '1; mode=block', + 'referrer_policy' => 'strict-origin-when-cross-origin', +], +``` + +**Protection Against:** +- **XSS** - Content Security Policy blocks inline scripts +- **Clickjacking** - X-Frame-Options prevents iframe embedding +- **MITM** - HSTS enforces HTTPS +- **Content Type Sniffing** - X-Content-Type-Options +- **Data Leakage** - Referrer Policy controls referrer info + +**CSP Nonces:** + +```blade + +``` + +### Input Validation & Sanitization + +Comprehensive input handling: + +```php +use Core\Input\Sanitiser; + +$sanitiser = app(Sanitiser::class); + +// Sanitize user input +$clean = $sanitiser->sanitize($userInput, [ + 'strip_tags' => true, + 'trim' => true, + 'escape_html' => true, +]); + +// Sanitize HTML content +$safeHtml = $sanitiser->sanitizeHtml($content, [ + 'allowed_tags' => ['p', 'br', 'strong', 'em', 'a'], + 'allowed_attributes' => ['href', 'title'], +]); +``` + +**Features:** +- HTML tag stripping +- XSS prevention +- SQL injection prevention (via Eloquent) +- CSRF protection (Laravel default) +- Mass assignment protection + +### Email Security + +Disposable email detection and validation: + +```php +use Core\Mail\EmailShield; + +$shield = app(EmailShield::class); + +$result = $shield->validate('user@tempmail.com'); + +if (! $result->isValid) { + // Email failed validation + // Reasons: disposable, syntax error, MX record invalid + return back()->withErrors(['email' => $result->reason]); +} +``` + +**Checks:** +- Disposable email providers +- Syntax validation +- MX record verification +- Common typo detection +- Role-based email detection (abuse@, admin@, etc.) + +### Authentication Security + +#### Password Hashing + +Laravel's bcrypt with automatic rehashing: + +```php +// Hashing +$hashed = bcrypt('password'); + +// Verification with automatic rehash +if (Hash::check($password, $user->password)) { + // Re-hash if using old cost + if (Hash::needsRehash($user->password)) { + $user->password = bcrypt($password); + $user->save(); + } +} +``` + +#### Two-Factor Authentication + +TOTP-based 2FA support: + +```php +use Core\Mod\Tenant\Concerns\TwoFactorAuthenticatable; + +class User extends Model +{ + use TwoFactorAuthenticatable; +} + +// Enable 2FA +$secret = $user->enableTwoFactorAuth(); +$qrCode = $user->getTwoFactorQrCode(); + +// Verify code +if ($user->verifyTwoFactorCode($code)) { + // Code valid +} +``` + +#### Session Security + +```php +// config/session.php +'secure' => env('SESSION_SECURE_COOKIE', true), +'http_only' => true, +'same_site' => 'lax', +'lifetime' => 120, +``` + +**Features:** +- Secure cookies (HTTPS only) +- HTTP-only cookies (no JavaScript access) +- SameSite protection +- Session regeneration on login +- Automatic logout on inactivity + +### IP Blocklist + +Automatic blocking of malicious IPs: + +```php +use Core\Bouncer\BlocklistService; + +$blocklist = app(BlocklistService::class); + +// Check if IP is blocked +if ($blocklist->isBlocked($ip)) { + abort(403, 'Access denied'); +} + +// Add IP to blocklist +$blocklist->block($ip, reason: 'Brute force attempt', duration: 3600); + +// Remove from blocklist +$blocklist->unblock($ip); +``` + +**Features:** +- Temporary and permanent blocks +- Reason tracking +- Automatic expiry +- Admin interface +- Integration with rate limiting + +### Action Gate + +Request whitelisting for sensitive operations: + +```php +use Core\Bouncer\Gate\Attributes\Action; + +#[Action('post.publish', description: 'Publish a blog post')] +class PublishPost +{ + use Action; + + public function handle(Post $post): Post + { + $post->update(['published_at' => now()]); + return $post; + } +} +``` + +**Modes:** +- **Training Mode** - Log all requests without blocking +- **Enforcement Mode** - Block unauthorized requests +- **Audit Mode** - Log + alert on violations + +**Configuration:** + +```php +// config/core.php +'bouncer' => [ + 'enabled' => true, + 'training_mode' => false, + 'block_unauthorized' => true, + 'log_all_requests' => true, +], +``` + +### Activity Logging + +Comprehensive audit trail: + +```php +use Core\Activity\Concerns\LogsActivity; + +class Post extends Model +{ + use LogsActivity; + + protected array $activityLogAttributes = ['title', 'status', 'published_at']; +} + +// Changes logged automatically +$post->update(['title' => 'New Title']); + +// Retrieve activity +$activity = Activity::forSubject($post) + ->latest() + ->get(); +``` + +**GDPR Compliance:** +- Optional IP address logging (disabled by default) +- Automatic anonymization after configurable period +- User data deletion on account closure +- Activity log pruning + +[Learn more about Activity Logging →](/patterns-guide/activity-logging) + +## Security Best Practices + +### 1. Use Workspace/Namespace Scoping + +Always scope data to workspaces or namespaces: + +```php +// ✅ Good - automatic scoping +class Post extends Model +{ + use BelongsToWorkspace; +} + +// ❌ Bad - no isolation +class Post extends Model { } +``` + +### 2. Validate All Input + +Never trust user input: + +```php +// ✅ Good - validation +$validated = $request->validate([ + 'title' => 'required|max:255', + 'content' => 'required', +]); + +// ❌ Bad - no validation +$post->update($request->all()); +``` + +### 3. Use Parameterized Queries + +Eloquent provides automatic protection: + +```php +// ✅ Good - parameterized +Post::where('title', $title)->get(); + +// ❌ Bad - vulnerable to SQL injection +DB::select("SELECT * FROM posts WHERE title = '{$title}'"); +``` + +### 4. Implement Rate Limiting + +Protect all public endpoints: + +```php +// ✅ Good - rate limited +Route::middleware('throttle:60,1') + ->post('/api/posts', [PostController::class, 'store']); + +// ❌ Bad - no rate limiting +Route::post('/api/posts', [PostController::class, 'store']); +``` + +### 5. Use HTTPS + +Always enforce HTTPS in production: + +```php +// app/Providers/AppServiceProvider.php +public function boot(): void +{ + if (app()->environment('production')) { + URL::forceScheme('https'); + } +} +``` + +### 6. Implement Authorization + +Use policies for authorization: + +```php +// ✅ Good - policy check +$this->authorize('update', $post); + +// ❌ Bad - no authorization +$post->update($request->validated()); +``` + +### 7. Sanitize Output + +Blade automatically escapes output: + +```blade +{{-- ✅ Good - auto-escaped --}} +

{{ $post->title }}

+ +{{-- ❌ Bad - unescaped (only when needed) --}} +
{!! $post->content !!}
+``` + +### 8. Rotate Secrets + +Regularly rotate secrets and API keys: + +```php +// API key rotation +$newKey = $apiKey->rotate(); + +// Session secret rotation (in .env) +php artisan key:generate +``` + +### 9. Monitor Security Events + +Log security-relevant events: + +```php +activity() + ->causedBy($user) + ->performedOn($resource) + ->withProperties(['ip' => $ip, 'user_agent' => $userAgent]) + ->log('unauthorized_access_attempt'); +``` + +### 10. Keep Dependencies Updated + +```bash +# Check for security updates +composer audit + +# Update dependencies +composer update +``` + +## Reporting Security Vulnerabilities + +If you discover a security vulnerability, please email: + +**support@host.uk.com** + +Do not create public GitHub issues for security vulnerabilities. + +**Response Timeline:** +- **Critical**: 24 hours +- **High**: 48 hours +- **Medium**: 7 days +- **Low**: 14 days + +[Full Disclosure Policy →](/security/responsible-disclosure) + +## Security Checklist + +Before deploying to production: + +- [ ] HTTPS enforced +- [ ] Security headers configured +- [ ] Rate limiting enabled +- [ ] CSRF protection active +- [ ] Input validation implemented +- [ ] SQL injection protections verified +- [ ] XSS protections enabled +- [ ] Authentication secure (2FA optional) +- [ ] Authorization policies in place +- [ ] Activity logging enabled +- [ ] Error messages sanitized (no stack traces in production) +- [ ] Debug mode disabled (`APP_DEBUG=false`) +- [ ] Database credentials secured +- [ ] API keys rotated +- [ ] Backups configured +- [ ] Monitoring/alerting active + +## Learn More + +- [Namespaces & Entitlements →](/security/namespaces) +- [API Security →](/packages/api) +- [MCP Security →](/packages/mcp) +- [Multi-Tenancy →](/architecture/multi-tenancy) +- [Responsible Disclosure →](/security/responsible-disclosure) diff --git a/build/php/seeder-system.md b/build/php/seeder-system.md new file mode 100644 index 0000000..f16416d --- /dev/null +++ b/build/php/seeder-system.md @@ -0,0 +1,613 @@ +# Seeder System + +The Seeder System provides automatic discovery, dependency resolution, and ordered execution of database seeders across modules. It supports both auto-discovery and manual registration with explicit priority and dependency declarations. + +## Overview + +The Core seeder system offers: + +- **Auto-discovery** - Finds seeders in module directories automatically +- **Dependency ordering** - Seeders run in dependency-resolved order +- **Priority control** - Fine-grained control over execution order +- **Circular detection** - Catches and reports circular dependencies +- **Filtering** - Include/exclude seeders at runtime + +## Core Components + +| Class | Purpose | +|-------|---------| +| `SeederDiscovery` | Auto-discovers and orders seeders | +| `SeederRegistry` | Manual seeder registration | +| `CoreDatabaseSeeder` | Base seeder with discovery support | +| `#[SeederPriority]` | Attribute for priority | +| `#[SeederAfter]` | Attribute for dependencies | +| `#[SeederBefore]` | Attribute for reverse dependencies | +| `CircularDependencyException` | Thrown on circular deps | + +## Discovery + +Seeders are auto-discovered in `Database/Seeders/` directories within configured module paths. + +### Discovery Pattern + +``` +{module_path}/*/Database/Seeders/*Seeder.php +``` + +For example, with module paths `[app_path('Mod')]`: + +``` +app/Mod/Blog/Database/Seeders/PostSeeder.php // Discovered +app/Mod/Blog/Database/Seeders/CategorySeeder.php // Discovered +app/Mod/Auth/Database/Seeders/UserSeeder.php // Discovered +``` + +### Using SeederDiscovery + +```php +use Core\Database\Seeders\SeederDiscovery; + +$discovery = new SeederDiscovery([ + app_path('Core'), + app_path('Mod'), +]); + +// Get ordered seeders +$seeders = $discovery->discover(); +// Returns: ['UserSeeder', 'CategorySeeder', 'PostSeeder', ...] +``` + +## Priority System + +Seeders declare priority using the `#[SeederPriority]` attribute or a public `$priority` property. Lower priority values run first. + +### Using the Attribute + +```php +use Core\Database\Seeders\Attributes\SeederPriority; +use Illuminate\Database\Seeder; + +#[SeederPriority(10)] +class FeatureSeeder extends Seeder +{ + public function run(): void + { + // Runs early (priority 10) + } +} + +#[SeederPriority(90)] +class DemoDataSeeder extends Seeder +{ + public function run(): void + { + // Runs later (priority 90) + } +} +``` + +### Using a Property + +```php +class FeatureSeeder extends Seeder +{ + public int $priority = 10; + + public function run(): void + { + // Runs early + } +} +``` + +### Priority Guidelines + +| Range | Use Case | Examples | +|-------|----------|----------| +| 0-20 | Foundation data | Features, configuration, settings | +| 20-40 | Core data | Packages, plans, workspaces | +| 40-60 | Default (50) | General module seeders | +| 60-80 | Content data | Pages, posts, products | +| 80-100 | Demo/test data | Sample content, test users | + +## Dependency Resolution + +Dependencies ensure seeders run in the correct order regardless of priority. Dependencies take precedence over priority. + +### Using #[SeederAfter] + +Declare that this seeder must run after specified seeders: + +```php +use Core\Database\Seeders\Attributes\SeederAfter; +use Mod\Feature\Database\Seeders\FeatureSeeder; + +#[SeederAfter(FeatureSeeder::class)] +class PackageSeeder extends Seeder +{ + public function run(): void + { + // Runs after FeatureSeeder + } +} +``` + +### Multiple Dependencies + +```php +use Mod\Feature\Database\Seeders\FeatureSeeder; +use Mod\Tenant\Database\Seeders\TenantSeeder; + +#[SeederAfter(FeatureSeeder::class, TenantSeeder::class)] +class WorkspaceSeeder extends Seeder +{ + public function run(): void + { + // Runs after both FeatureSeeder and TenantSeeder + } +} +``` + +### Using #[SeederBefore] + +Declare that this seeder must run before specified seeders. This is the inverse relationship - you're saying other seeders depend on this one: + +```php +use Core\Database\Seeders\Attributes\SeederBefore; +use Mod\Package\Database\Seeders\PackageSeeder; + +#[SeederBefore(PackageSeeder::class)] +class FeatureSeeder extends Seeder +{ + public function run(): void + { + // Runs before PackageSeeder + } +} +``` + +### Using Properties + +As an alternative to attributes, use public properties: + +```php +class WorkspaceSeeder extends Seeder +{ + public array $after = [ + FeatureSeeder::class, + PackageSeeder::class, + ]; + + public array $before = [ + DemoSeeder::class, + ]; + + public function run(): void + { + // ... + } +} +``` + +## Complex Ordering Examples + +### Example 1: Linear Chain + +```php +// Run order: Feature -> Package -> Workspace -> User + +#[SeederPriority(10)] +class FeatureSeeder extends Seeder { } + +#[SeederAfter(FeatureSeeder::class)] +class PackageSeeder extends Seeder { } + +#[SeederAfter(PackageSeeder::class)] +class WorkspaceSeeder extends Seeder { } + +#[SeederAfter(WorkspaceSeeder::class)] +class UserSeeder extends Seeder { } +``` + +### Example 2: Diamond Dependency + +```php +// Feature +// / \ +// Package Plan +// \ / +// Workspace + +#[SeederPriority(10)] +class FeatureSeeder extends Seeder { } + +#[SeederAfter(FeatureSeeder::class)] +class PackageSeeder extends Seeder { } + +#[SeederAfter(FeatureSeeder::class)] +class PlanSeeder extends Seeder { } + +#[SeederAfter(PackageSeeder::class, PlanSeeder::class)] +class WorkspaceSeeder extends Seeder { } + +// Execution order: Feature -> [Package, Plan] -> Workspace +// Package and Plan can run in either order (same priority level) +``` + +### Example 3: Priority with Dependencies + +```php +// Dependencies override priority + +#[SeederPriority(90)] // High priority number (normally runs late) +#[SeederBefore(DemoSeeder::class)] +class FeatureSeeder extends Seeder { } + +#[SeederPriority(10)] // Low priority number (normally runs early) +#[SeederAfter(FeatureSeeder::class)] +class DemoSeeder extends Seeder { } + +// Despite priority, FeatureSeeder runs first due to dependency +``` + +### Example 4: Mixed Priority and Dependencies + +```php +// Seeders at the same dependency level sort by priority + +#[SeederPriority(10)] +class FeatureSeeder extends Seeder { } + +#[SeederAfter(FeatureSeeder::class)] +#[SeederPriority(20)] // Lower priority = runs first among siblings +class PackageSeeder extends Seeder { } + +#[SeederAfter(FeatureSeeder::class)] +#[SeederPriority(30)] // Higher priority = runs after PackageSeeder +class PlanSeeder extends Seeder { } + +// Order: Feature -> Package -> Plan +// (Package before Plan because 20 < 30) +``` + +## Circular Dependency Errors + +Circular dependencies are detected and throw `CircularDependencyException`. + +### What Causes Circular Dependencies + +```php +// This creates a cycle: A -> B -> C -> A + +#[SeederAfter(SeederC::class)] +class SeederA extends Seeder { } + +#[SeederAfter(SeederA::class)] +class SeederB extends Seeder { } + +#[SeederAfter(SeederB::class)] +class SeederC extends Seeder { } +``` + +### Error Handling + +```php +use Core\Database\Seeders\Exceptions\CircularDependencyException; + +try { + $seeders = $discovery->discover(); +} catch (CircularDependencyException $e) { + echo $e->getMessage(); + // "Circular dependency detected in seeders: SeederA -> SeederB -> SeederC -> SeederA" + + // Get the cycle chain + $cycle = $e->cycle; + // ['SeederA', 'SeederB', 'SeederC', 'SeederA'] +} +``` + +### Debugging Circular Dependencies + +1. Check the exception message for the cycle path +2. Review the `$after` and `$before` declarations +3. Remember that `#[SeederBefore]` creates implicit `after` relationships +4. Use the registry to inspect relationships: + +```php +$discovery = new SeederDiscovery([app_path('Mod')]); +$seeders = $discovery->getSeeders(); + +foreach ($seeders as $class => $meta) { + echo "{$class}:\n"; + echo " Priority: {$meta['priority']}\n"; + echo " After: " . implode(', ', $meta['after']) . "\n"; + echo " Before: " . implode(', ', $meta['before']) . "\n"; +} +``` + +## Manual Registration + +Use `SeederRegistry` for explicit control over seeder ordering: + +```php +use Core\Database\Seeders\SeederRegistry; + +$registry = new SeederRegistry(); + +// Register with options +$registry + ->register(FeatureSeeder::class, priority: 10) + ->register(PackageSeeder::class, after: [FeatureSeeder::class]) + ->register(WorkspaceSeeder::class, after: [PackageSeeder::class]); + +// Get ordered list +$seeders = $registry->getOrdered(); +``` + +### Bulk Registration + +```php +$registry->registerMany([ + FeatureSeeder::class => 10, // Priority shorthand + PackageSeeder::class => [ + 'priority' => 50, + 'after' => [FeatureSeeder::class], + ], + WorkspaceSeeder::class => [ + 'priority' => 50, + 'after' => [PackageSeeder::class], + 'before' => [DemoSeeder::class], + ], +]); +``` + +### Registry Operations + +```php +// Check if registered +$registry->has(FeatureSeeder::class); + +// Remove a seeder +$registry->remove(DemoSeeder::class); + +// Merge registries +$registry->merge($otherRegistry); + +// Clear all +$registry->clear(); +``` + +## CoreDatabaseSeeder + +Extend `CoreDatabaseSeeder` for automatic discovery in your application: + +### Basic Usage + +```php +register(FeatureSeeder::class, priority: 10) + ->register(PackageSeeder::class, priority: 20) + ->register(UserSeeder::class, priority: 30); + } +} +``` + +## Command-Line Filtering + +Filter seeders when running `db:seed`: + +```bash +# Exclude specific seeders +php artisan db:seed --exclude=DemoSeeder + +# Exclude multiple +php artisan db:seed --exclude=DemoSeeder --exclude=TestSeeder + +# Run only specific seeders +php artisan db:seed --only=UserSeeder + +# Run multiple specific seeders +php artisan db:seed --only=UserSeeder --only=FeatureSeeder +``` + +### Pattern Matching + +Filters support multiple matching strategies: + +```bash +# Full class name +php artisan db:seed --exclude=Mod\\Blog\\Database\\Seeders\\PostSeeder + +# Short name +php artisan db:seed --exclude=PostSeeder + +# Partial match +php artisan db:seed --exclude=Demo # Matches DemoSeeder, DemoDataSeeder, etc. +``` + +## Configuration + +Configure the seeder system in `config/core.php`: + +```php +return [ + 'seeders' => [ + // Enable auto-discovery + 'auto_discover' => env('CORE_SEEDER_AUTODISCOVER', true), + + // Paths to scan + 'paths' => [ + app_path('Core'), + app_path('Mod'), + app_path('Website'), + ], + + // Classes to exclude + 'exclude' => [ + // App\Mod\Demo\Database\Seeders\DemoSeeder::class, + ], + ], +]; +``` + +## Best Practices + +### 1. Use Explicit Dependencies + +```php +// Preferred: Explicit dependencies +#[SeederAfter(FeatureSeeder::class)] +class PackageSeeder extends Seeder { } + +// Avoid: Relying only on priority for ordering +#[SeederPriority(51)] // Fragile - assumes FeatureSeeder is 50 +class PackageSeeder extends Seeder { } +``` + +### 2. Keep Seeders Focused + +```php +// Good: Single responsibility +class PostSeeder extends Seeder { + public function run(): void { + Post::factory()->count(50)->create(); + } +} + +// Avoid: Monolithic seeders +class EverythingSeeder extends Seeder { + public function run(): void { + // Creates users, posts, comments, categories, tags... + } +} +``` + +### 3. Use Factories in Seeders + +```php +class PostSeeder extends Seeder +{ + public function run(): void + { + // Good: Use factories for consistent test data + Post::factory() + ->count(50) + ->has(Comment::factory()->count(3)) + ->create(); + } +} +``` + +### 4. Handle Idempotency + +```php +class FeatureSeeder extends Seeder +{ + public function run(): void + { + // Good: Use updateOrCreate for idempotent seeding + Feature::updateOrCreate( + ['code' => 'blog'], + ['name' => 'Blog', 'enabled' => true] + ); + } +} +``` + +### 5. Document Dependencies + +```php +/** + * Seeds packages for the tenant module. + * + * Requires: + * - FeatureSeeder: Features must exist to link packages + * - TenantSeeder: Tenants must exist to assign packages + */ +#[SeederAfter(FeatureSeeder::class, TenantSeeder::class)] +class PackageSeeder extends Seeder { } +``` + +## Troubleshooting + +### Seeders Not Discovered + +1. Check the file is in `Database/Seeders/` subdirectory +2. Verify class name ends with `Seeder` +3. Confirm namespace matches file location +4. Check the path is included in discovery paths + +### Wrong Execution Order + +1. Print discovery results to verify: + ```php + $discovery = new SeederDiscovery([app_path('Mod')]); + dd($discovery->getSeeders()); + ``` +2. Check for missing `#[SeederAfter]` declarations +3. Verify priority values (lower runs first) + +### Circular Dependency Error + +1. Read the error message for the cycle +2. Draw out the dependency graph +3. Identify which relationship should be removed/reversed +4. Consider if the circular dependency indicates a design issue + +## Learn More + +- [Module System](/core/modules) +- [Service Contracts](/core/service-contracts) +- [Configuration](/core/configuration) diff --git a/build/php/seo.md b/build/php/seo.md new file mode 100644 index 0000000..91418a8 --- /dev/null +++ b/build/php/seo.md @@ -0,0 +1,500 @@ +# SEO Tools + +Comprehensive SEO tools including metadata management, sitemap generation, structured data, and OG image generation. + +## SEO Metadata + +### Basic Usage + +```php +use Core\Seo\SeoMetadata; + +$seo = app(SeoMetadata::class); + +// Set page metadata +$seo->title('Complete Laravel Tutorial') + ->description('Learn Laravel from scratch with this comprehensive tutorial') + ->keywords(['laravel', 'php', 'tutorial', 'web development']) + ->canonical(url()->current()); +``` + +### Blade Output + +```blade + + + + {!! $seo->render() !!} + + +``` + +**Rendered Output:** + +```html +Complete Laravel Tutorial + + + +``` + +### Open Graph Tags + +```php +$seo->og([ + 'title' => 'Complete Laravel Tutorial', + 'description' => 'Learn Laravel from scratch...', + 'image' => cdn('images/laravel-tutorial.jpg'), + 'type' => 'article', + 'url' => url()->current(), +]); +``` + +**Rendered:** + +```html + + + + + +``` + +### Twitter Cards + +```php +$seo->twitter([ + 'card' => 'summary_large_image', + 'site' => '@yourhandle', + 'creator' => '@authorhandle', + 'title' => 'Complete Laravel Tutorial', + 'description' => 'Learn Laravel from scratch...', + 'image' => cdn('images/laravel-tutorial.jpg'), +]); +``` + +## Dynamic OG Images + +Generate OG images on-the-fly: + +```php +use Core\Seo\Jobs\GenerateOgImageJob; + +// Queue image generation +GenerateOgImageJob::dispatch($post, [ + 'title' => $post->title, + 'subtitle' => $post->category->name, + 'author' => $post->author->name, + 'template' => 'blog-post', +]); + +// Use generated image +$seo->og([ + 'image' => $post->og_image_url, +]); +``` + +### OG Image Templates + +```php +// config/seo.php +return [ + 'og_images' => [ + 'templates' => [ + 'blog-post' => [ + 'width' => 1200, + 'height' => 630, + 'background' => '#1e293b', + 'title_color' => '#ffffff', + 'title_size' => 64, + 'subtitle_color' => '#94a3b8', + 'subtitle_size' => 32, + ], + 'product' => [ + 'width' => 1200, + 'height' => 630, + 'background' => '#0f172a', + 'overlay' => true, + ], + ], + ], +]; +``` + +### Validating OG Images + +```php +use Core\Seo\Validation\OgImageValidator; + +$validator = app(OgImageValidator::class); + +// Validate image meets requirements +$result = $validator->validate($imagePath); + +if (!$result->valid) { + foreach ($result->errors as $error) { + echo $error; // "Image width must be at least 1200px" + } +} +``` + +**Requirements:** +- Minimum 1200×630px (recommended) +- Maximum 8MB file size +- Supported formats: JPG, PNG, WebP +- Aspect ratio: 1.91:1 + +## Sitemaps + +### Generating Sitemaps + +```php +use Core\Seo\Controllers\SitemapController; + +// Auto-generated route: /sitemap.xml +// Lists all public URLs + +// Custom sitemap +Route::get('/sitemap.xml', [SitemapController::class, 'index']); +``` + +### Adding URLs + +```php +namespace Mod\Blog; + +use Core\Events\WebRoutesRegistering; + +class Boot +{ + public function onWebRoutes(WebRoutesRegistering $event): void + { + // Posts automatically included in sitemap + $event->sitemap(function ($sitemap) { + Post::where('status', 'published') + ->each(function ($post) use ($sitemap) { + $sitemap->add( + url: route('blog.show', $post), + lastmod: $post->updated_at, + changefreq: 'weekly', + priority: 0.8 + ); + }); + }); + } +} +``` + +### Sitemap Index + +For large sites: + +```xml + + + + + https://example.com/sitemap-posts.xml + 2026-01-26T12:00:00+00:00 + + + https://example.com/sitemap-products.xml + 2026-01-25T10:30:00+00:00 + + +``` + +## Structured Data + +### JSON-LD Schema + +```php +$seo->schema([ + '@context' => 'https://schema.org', + '@type' => 'Article', + 'headline' => $post->title, + 'description' => $post->excerpt, + 'image' => cdn($post->featured_image), + 'datePublished' => $post->published_at->toIso8601String(), + 'dateModified' => $post->updated_at->toIso8601String(), + 'author' => [ + '@type' => 'Person', + 'name' => $post->author->name, + ], +]); +``` + +**Rendered:** + +```html + +``` + +### Common Schema Types + +**Blog Post:** + +```php +$seo->schema([ + '@type' => 'BlogPosting', + 'headline' => $post->title, + 'image' => cdn($post->image), + 'author' => ['@type' => 'Person', 'name' => $author->name], + 'publisher' => [ + '@type' => 'Organization', + 'name' => config('app.name'), + 'logo' => cdn('logo.png'), + ], +]); +``` + +**Product:** + +```php +$seo->schema([ + '@type' => 'Product', + 'name' => $product->name, + 'image' => cdn($product->image), + 'description' => $product->description, + 'sku' => $product->sku, + 'offers' => [ + '@type' => 'Offer', + 'price' => $product->price, + 'priceCurrency' => 'GBP', + 'availability' => 'https://schema.org/InStock', + ], +]); +``` + +**Breadcrumbs:** + +```php +$seo->schema([ + '@type' => 'BreadcrumbList', + 'itemListElement' => [ + [ + '@type' => 'ListItem', + 'position' => 1, + 'name' => 'Home', + 'item' => route('home'), + ], + [ + '@type' => 'ListItem', + 'position' => 2, + 'name' => 'Blog', + 'item' => route('blog.index'), + ], + [ + '@type' => 'ListItem', + 'position' => 3, + 'name' => $post->title, + 'item' => route('blog.show', $post), + ], + ], +]); +``` + +### Testing Structured Data + +```bash +php artisan seo:test-structured-data +``` + +**Or programmatically:** + +```php +use Core\Seo\Validation\StructuredDataTester; + +$tester = app(StructuredDataTester::class); + +$result = $tester->test($jsonLd); + +if (!$result->valid) { + foreach ($result->errors as $error) { + echo $error; // "Missing required property: datePublished" + } +} +``` + +## Canonical URLs + +### Setting Canonical + +```php +// Explicit canonical +$seo->canonical('https://example.com/blog/laravel-tutorial'); + +// Auto-detect +$seo->canonical(url()->current()); + +// Remove query parameters +$seo->canonical(url()->current(), stripQuery: true); +``` + +### Auditing Canonicals + +```bash +php artisan seo:audit-canonical +``` + +**Checks for:** +- Missing canonical tags +- Self-referencing issues +- HTTPS/HTTP mismatches +- Duplicate content + +**Example Output:** + +``` +Canonical URL Audit +=================== + +✓ 1,234 pages have canonical tags +✗ 45 pages missing canonical tags +✗ 12 pages with incorrect HTTPS +⚠ 8 pages with duplicate content + +Issues: +- /blog/post-1 missing canonical +- /shop/product-5 using HTTP instead of HTTPS +``` + +## SEO Scoring + +Track SEO quality over time: + +```php +use Core\Seo\Analytics\SeoScoreTrend; + +$trend = app(SeoScoreTrend::class); + +// Record current SEO score +$trend->record($post, [ + 'title_length' => strlen($post->title), + 'has_meta_description' => !empty($post->meta_description), + 'has_og_image' => !empty($post->og_image), + 'has_canonical' => !empty($post->canonical_url), + 'structured_data' => !empty($post->schema), +]); + +// View trends +$scores = $trend->history($post, days: 30); +``` + +### SEO Score Calculation + +```php +// config/seo.php +return [ + 'scoring' => [ + 'title_length' => ['min' => 30, 'max' => 60, 'points' => 10], + 'meta_description' => ['min' => 120, 'max' => 160, 'points' => 10], + 'has_og_image' => ['points' => 15], + 'has_canonical' => ['points' => 10], + 'has_structured_data' => ['points' => 15], + 'image_alt_text' => ['points' => 10], + 'heading_hierarchy' => ['points' => 10], + 'internal_links' => ['min' => 3, 'points' => 10], + 'external_links' => ['min' => 1, 'points' => 5], + 'word_count' => ['min' => 300, 'points' => 15], + ], +]; +``` + +## Best Practices + +### 1. Always Set Metadata + +```php +// ✅ Good - complete metadata +$seo->title('Laravel Tutorial') + ->description('Learn Laravel...') + ->canonical(url()->current()) + ->og(['image' => cdn('image.jpg')]); + +// ❌ Bad - missing metadata +$seo->title('Laravel Tutorial'); +``` + +### 2. Use Unique Titles & Descriptions + +```php +// ✅ Good - unique per page +$seo->title($post->title . ' - Blog') + ->description($post->excerpt); + +// ❌ Bad - same title everywhere +$seo->title(config('app.name')); +``` + +### 3. Generate OG Images + +```php +// ✅ Good - custom OG image +GenerateOgImageJob::dispatch($post); + +// ❌ Bad - generic logo +$seo->og(['image' => cdn('logo.png')]); +``` + +### 4. Validate Structured Data + +```bash +# Test before deploying +php artisan seo:test-structured-data + +# Check with Google Rich Results Test +# https://search.google.com/test/rich-results +``` + +## Testing + +```php +use Tests\TestCase; +use Core\Seo\SeoMetadata; + +class SeoTest extends TestCase +{ + public function test_renders_metadata(): void + { + $seo = app(SeoMetadata::class); + + $seo->title('Test Page') + ->description('Test description'); + + $html = $seo->render(); + + $this->assertStringContainsString('Test Page', $html); + $this->assertStringContainsString('name="description"', $html); + } + + public function test_generates_og_image(): void + { + $post = Post::factory()->create(); + + GenerateOgImageJob::dispatch($post); + + $this->assertNotNull($post->fresh()->og_image_url); + $this->assertFileExists(storage_path("app/og-images/{$post->id}.jpg")); + } +} +``` + +## Learn More + +- [Configuration →](/core/configuration) +- [Media Processing →](/core/media) diff --git a/build/php/service-contracts.md b/build/php/service-contracts.md new file mode 100644 index 0000000..a2a90f9 --- /dev/null +++ b/build/php/service-contracts.md @@ -0,0 +1,510 @@ +# Service Contracts + +The Service Contracts system provides a structured way to define SaaS services as first-class citizens in the framework. Services are the product layer - they define how modules are presented to users as SaaS products. + +## Overview + +Services in Core PHP are: + +- **Discoverable** - Automatically found in configured module paths +- **Versioned** - Support semantic versioning with deprecation tracking +- **Dependency-aware** - Declare and validate dependencies on other services +- **Health-monitored** - Optional health checks for operational status + +## Core Components + +| Class | Purpose | +|-------|---------| +| `ServiceDefinition` | Interface for defining a service | +| `ServiceDiscovery` | Discovers and resolves services | +| `ServiceVersion` | Semantic versioning with deprecation | +| `ServiceDependency` | Declares service dependencies | +| `HealthCheckable` | Optional health monitoring | +| `HasServiceVersion` | Trait with default implementations | + +## Creating a Service + +### Basic Service Definition + +Implement the `ServiceDefinition` interface to create a service: + +```php + 'billing', // Unique identifier + 'module' => 'Mod\\Billing', // Module namespace + 'name' => 'Billing Service', // Display name + 'tagline' => 'Handle payments and invoices', // Short description + 'description' => 'Complete billing solution with Stripe integration', + 'icon' => 'credit-card', // FontAwesome icon + 'color' => '#10B981', // Brand color (hex) + 'entitlement_code' => 'core.srv.billing', // Access control + 'sort_order' => 20, // Menu ordering + ]; + } + + /** + * Declare dependencies on other services. + */ + public static function dependencies(): array + { + return [ + ServiceDependency::required('auth', '>=1.0.0'), + ServiceDependency::optional('analytics'), + ]; + } + + /** + * Admin menu items provided by this service. + */ + public function menuItems(): array + { + return [ + [ + 'label' => 'Billing', + 'icon' => 'credit-card', + 'route' => 'admin.billing.index', + 'order' => 20, + ], + ]; + } +} +``` + +### Definition Array Fields + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `code` | Yes | string | Unique service identifier (lowercase, alphanumeric) | +| `module` | Yes | string | Module namespace | +| `name` | Yes | string | Display name | +| `tagline` | No | string | Short description | +| `description` | No | string | Full description | +| `icon` | No | string | FontAwesome icon name | +| `color` | No | string | Hex color (e.g., `#3B82F6`) | +| `entitlement_code` | No | string | Access control entitlement | +| `sort_order` | No | int | Menu/display ordering | + +## Service Versioning + +Services use semantic versioning to track API compatibility and manage deprecation. + +### Basic Versioning + +```php +use Core\Service\ServiceVersion; + +// Create version 2.1.0 +$version = new ServiceVersion(2, 1, 0); +echo $version; // "2.1.0" + +// Parse from string +$version = ServiceVersion::fromString('v2.1.0'); + +// Default version (1.0.0) +$version = ServiceVersion::initial(); +``` + +### Semantic Versioning Rules + +| Change | Version Bump | Description | +|--------|--------------|-------------| +| Major | 1.0.0 -> 2.0.0 | Breaking changes to the service contract | +| Minor | 1.0.0 -> 1.1.0 | New features, backwards compatible | +| Patch | 1.0.0 -> 1.0.1 | Bug fixes, backwards compatible | + +### Implementing Custom Versions + +Override the `version()` method from the trait: + +```php +use Core\Service\ServiceVersion; +use Core\Service\Concerns\HasServiceVersion; + +class MyService implements ServiceDefinition +{ + use HasServiceVersion; + + public static function version(): ServiceVersion + { + return new ServiceVersion(2, 3, 1); + } +} +``` + +### Service Deprecation + +Mark services as deprecated with migration guidance: + +```php +public static function version(): ServiceVersion +{ + return (new ServiceVersion(1, 0, 0)) + ->deprecate( + 'Migrate to BillingV2 - see docs/migration.md', + new \DateTimeImmutable('2026-06-01') + ); +} +``` + +### Deprecation Lifecycle + +``` +[Active] ──deprecate()──> [Deprecated] ──isPastSunset()──> [Sunset] +``` + +| State | Behavior | +|-------|----------| +| Active | Service fully operational | +| Deprecated | Works but logs warnings; consumers should migrate | +| Sunset | Past sunset date; may throw exceptions | + +### Checking Deprecation Status + +```php +$version = MyService::version(); + +// Check if deprecated +if ($version->deprecated) { + echo $version->deprecationMessage; + echo $version->sunsetDate->format('Y-m-d'); +} + +// Check if past sunset +if ($version->isPastSunset()) { + throw new ServiceSunsetException('This service is no longer available'); +} + +// Version compatibility +$minimum = new ServiceVersion(1, 5, 0); +$current = new ServiceVersion(1, 8, 2); +$current->isCompatibleWith($minimum); // true (same major, >= minor.patch) +``` + +## Dependency Resolution + +Services can declare dependencies on other services, and the framework resolves them automatically. + +### Declaring Dependencies + +```php +use Core\Service\Contracts\ServiceDependency; + +public static function dependencies(): array +{ + return [ + // Required dependency - service fails if not available + ServiceDependency::required('auth', '>=1.0.0'), + + // Optional dependency - service works with reduced functionality + ServiceDependency::optional('analytics'), + + // Version range constraints + ServiceDependency::required('billing', '>=2.0.0', '<3.0.0'), + ]; +} +``` + +### Version Constraints + +| Constraint | Meaning | +|------------|---------| +| `>=1.0.0` | Minimum version 1.0.0 | +| `<3.0.0` | Maximum version below 3.0.0 | +| `>=2.0.0`, `<3.0.0` | Version 2.x only | +| `null` | Any version | + +### Using ServiceDiscovery + +```php +use Core\Service\ServiceDiscovery; + +$discovery = app(ServiceDiscovery::class); + +// Get all registered services +$services = $discovery->discover(); + +// Check if a service is available +if ($discovery->has('billing')) { + $billingClass = $discovery->get('billing'); + $billing = $discovery->getInstance('billing'); +} + +// Get services in dependency order +$ordered = $discovery->getResolutionOrder(); + +// Validate all dependencies +$missing = $discovery->validateDependencies(); +if (!empty($missing)) { + foreach ($missing as $service => $deps) { + logger()->error("Service {$service} missing: " . implode(', ', $deps)); + } +} +``` + +### Resolution Order + +The framework uses topological sorting to resolve services in the correct order: + +```php +// Services are resolved so dependencies come first +$ordered = $discovery->getResolutionOrder(); +// Returns: ['auth', 'analytics', 'billing'] +// (auth before billing if billing depends on auth) +``` + +### Handling Circular Dependencies + +Circular dependencies are detected and throw `ServiceDependencyException`: + +```php +use Core\Service\ServiceDependencyException; + +try { + $ordered = $discovery->getResolutionOrder(); +} catch (ServiceDependencyException $e) { + // Circular dependency: auth -> billing -> auth + echo $e->getMessage(); + print_r($e->getDependencyChain()); +} +``` + +## Manual Service Registration + +Register services programmatically when auto-discovery is not desired: + +```php +$discovery = app(ServiceDiscovery::class); + +// Register with validation +$discovery->register(BillingService::class); + +// Register without validation +$discovery->register(BillingService::class, validate: false); + +// Add additional scan paths +$discovery->addPath(base_path('packages/my-package/src')); + +// Clear discovery cache +$discovery->clearCache(); +``` + +## Health Monitoring + +Services can implement health checks for operational monitoring. + +### Implementing HealthCheckable + +```php +use Core\Service\Contracts\ServiceDefinition; +use Core\Service\Contracts\HealthCheckable; +use Core\Service\HealthCheckResult; + +class BillingService implements ServiceDefinition, HealthCheckable +{ + // ... service definition methods ... + + public function healthCheck(): HealthCheckResult + { + try { + $start = microtime(true); + + // Test critical dependencies + $stripeConnected = $this->stripe->testConnection(); + + $responseTime = (microtime(true) - $start) * 1000; + + if (!$stripeConnected) { + return HealthCheckResult::unhealthy( + 'Cannot connect to Stripe', + ['stripe_status' => 'disconnected'] + ); + } + + if ($responseTime > 1000) { + return HealthCheckResult::degraded( + 'Stripe responding slowly', + ['response_time_ms' => $responseTime], + responseTimeMs: $responseTime + ); + } + + return HealthCheckResult::healthy( + 'All billing systems operational', + ['stripe_status' => 'connected'], + responseTimeMs: $responseTime + ); + } catch (\Exception $e) { + return HealthCheckResult::fromException($e); + } + } +} +``` + +### Health Check Result States + +| Status | Method | Description | +|--------|--------|-------------| +| Healthy | `HealthCheckResult::healthy()` | Fully operational | +| Degraded | `HealthCheckResult::degraded()` | Working with reduced performance | +| Unhealthy | `HealthCheckResult::unhealthy()` | Not operational | +| Unknown | `HealthCheckResult::unknown()` | Status cannot be determined | + +### Health Check Guidelines + +- **Fast** - Complete within 5 seconds (preferably < 1 second) +- **Non-destructive** - Read-only operations only +- **Representative** - Test actual critical dependencies +- **Safe** - Catch all exceptions, return HealthCheckResult + +### Aggregating Health Checks + +```php +use Core\Service\Enums\ServiceStatus; + +// Get all health check results +$results = []; +foreach ($discovery->discover() as $code => $class) { + $instance = $discovery->getInstance($code); + + if ($instance instanceof HealthCheckable) { + $results[$code] = $instance->healthCheck(); + } +} + +// Determine overall status +$statuses = array_map(fn($r) => $r->status, $results); +$overall = ServiceStatus::worst($statuses); + +if (!$overall->isOperational()) { + // Alert on-call team +} +``` + +## Complete Example + +Here is a complete service implementation with all features: + +```php + 'blog', + 'module' => 'Mod\\Blog', + 'name' => 'Blog', + 'tagline' => 'Content publishing platform', + 'description' => 'Full-featured blog with categories, tags, and comments', + 'icon' => 'newspaper', + 'color' => '#6366F1', + 'entitlement_code' => 'core.srv.blog', + 'sort_order' => 30, + ]; + } + + public static function version(): ServiceVersion + { + return new ServiceVersion(2, 0, 0); + } + + public static function dependencies(): array + { + return [ + ServiceDependency::required('auth', '>=1.0.0'), + ServiceDependency::required('media', '>=1.0.0'), + ServiceDependency::optional('seo'), + ServiceDependency::optional('analytics'), + ]; + } + + public function menuItems(): array + { + return [ + [ + 'label' => 'Blog', + 'icon' => 'newspaper', + 'route' => 'admin.blog.index', + 'order' => 30, + 'children' => [ + ['label' => 'Posts', 'route' => 'admin.blog.posts'], + ['label' => 'Categories', 'route' => 'admin.blog.categories'], + ['label' => 'Tags', 'route' => 'admin.blog.tags'], + ], + ], + ]; + } + + public function healthCheck(): HealthCheckResult + { + try { + $postsTable = \DB::table('posts')->exists(); + + if (!$postsTable) { + return HealthCheckResult::unhealthy('Posts table not found'); + } + + return HealthCheckResult::healthy('Blog service operational'); + } catch (\Exception $e) { + return HealthCheckResult::fromException($e); + } + } +} +``` + +## Configuration + +Configure service discovery in `config/core.php`: + +```php +return [ + 'services' => [ + // Enable/disable discovery caching + 'cache_discovery' => env('CORE_CACHE_SERVICES', true), + + // Cache TTL in seconds (default: 1 hour) + 'cache_ttl' => 3600, + ], + + // Paths to scan for services + 'module_paths' => [ + app_path('Core'), + app_path('Mod'), + app_path('Website'), + app_path('Plug'), + ], +]; +``` + +## Learn More + +- [Module System](/core/modules) +- [Lifecycle Events](/core/events) +- [Seeder System](/core/seeder-system) diff --git a/build/php/tenancy.md b/build/php/tenancy.md new file mode 100644 index 0000000..49a3749 --- /dev/null +++ b/build/php/tenancy.md @@ -0,0 +1,514 @@ +# Multi-Tenancy + +Core PHP Framework provides robust multi-tenancy with dual-level isolation: **Workspaces** for team/agency management and **Namespaces** for service isolation and billing contexts. + +## Overview + +The tenancy system supports three common patterns: + +1. **Personal** - Individual users with personal namespaces +2. **Agency/Team** - Workspaces with multiple users managing client namespaces +3. **White-Label** - Operators creating workspace + namespace pairs for customers + +## Workspaces + +Workspaces represent a team, agency, or organization. Multiple users can belong to a workspace. + +### Creating Workspaces + +```php +use Core\Mod\Tenant\Models\Workspace; + +$workspace = Workspace::create([ + 'name' => 'Acme Corporation', + 'slug' => 'acme-corp', + 'tier' => 'business', +]); + +// Add user to workspace +$workspace->users()->attach($user->id, [ + 'role' => 'admin', +]); +``` + +### Workspace Scoping + +Use the `BelongsToWorkspace` trait to automatically scope models: + +```php +use Core\Mod\Tenant\Concerns\BelongsToWorkspace; + +class Post extends Model +{ + use BelongsToWorkspace; +} + +// Queries automatically scoped to current workspace +$posts = Post::all(); // Only posts in current workspace + +// Create within workspace +$post = Post::create([ + 'title' => 'My Post', +]); // workspace_id automatically set +``` + +### Workspace Context + +The current workspace is resolved from: + +1. Session (for web requests) +2. `X-Workspace-ID` header (for API requests) +3. Query parameter `workspace_id` +4. User's default workspace (fallback) + +```php +// Get current workspace +$workspace = workspace(); + +// Check if workspace context is set +if (workspace()) { + // Workspace context available +} + +// Manually set workspace +Workspace::setCurrent($workspace); +``` + +## Namespaces + +Namespaces provide service isolation and are the **billing context** for entitlements. A namespace can be owned by a **User** (personal) or a **Workspace** (agency/client). + +### Why Namespaces? + +- **Service Isolation** - Each namespace has separate storage, API quotas, features +- **Billing Context** - Packages and entitlements are attached to namespaces +- **Agency Pattern** - One workspace can manage many client namespaces +- **White-Label** - Operators can provision namespace + workspace pairs + +### Namespace Ownership + +Namespaces use polymorphic ownership: + +```php +use Core\Mod\Tenant\Models\Namespace_; + +// Personal namespace (owned by User) +$namespace = Namespace_::create([ + 'name' => 'Personal', + 'slug' => 'personal', + 'owner_type' => User::class, + 'owner_id' => $user->id, + 'is_default' => true, +]); + +// Client namespace (owned by Workspace) +$namespace = Namespace_::create([ + 'name' => 'Client: Acme Corp', + 'slug' => 'client-acme', + 'owner_type' => Workspace::class, + 'owner_id' => $workspace->id, + 'workspace_id' => $workspace->id, // For billing aggregation +]); +``` + +### Namespace Scoping + +Use the `BelongsToNamespace` trait for namespace-specific data: + +```php +use Core\Mod\Tenant\Concerns\BelongsToNamespace; + +class Media extends Model +{ + use BelongsToNamespace; +} + +// Queries automatically scoped to current namespace +$media = Media::all(); + +// With caching +$media = Media::ownedByCurrentNamespaceCached(ttl: 300); +``` + +### Namespace Context + +The current namespace is resolved from: + +1. Session (for web requests) +2. `X-Namespace-ID` header (for API requests) +3. Query parameter `namespace_id` +4. User's default namespace (fallback) + +```php +// Get current namespace +$namespace = namespace_context(); + +// Manually set namespace +Namespace_::setCurrent($namespace); +``` + +### Accessible Namespaces + +Get all namespaces a user can access: + +```php +use Core\Mod\Tenant\Services\NamespaceService; + +$service = app(NamespaceService::class); + +// Get all accessible namespaces +$namespaces = $service->getAccessibleNamespaces($user); + +// Grouped by type +$grouped = $service->getGroupedNamespaces($user); +// Returns: +// [ +// 'personal' => [...], // User-owned namespaces +// 'workspaces' => [ // Workspace-owned namespaces +// 'Workspace Name' => [...], +// ] +// ] +``` + +## Entitlements Integration + +Namespaces are the billing context for entitlements: + +```php +use Core\Mod\Tenant\Services\EntitlementService; + +$entitlements = app(EntitlementService::class); + +// Check if namespace has access to feature +$result = $entitlements->can($namespace, 'storage', quantity: 1073741824); + +if ($result->isDenied()) { + return back()->with('error', $result->getMessage()); +} + +// Record usage +$entitlements->recordUsage($namespace, 'api_calls', quantity: 1); + +// Get current usage +$usage = $entitlements->getUsage($namespace, 'storage'); +``` + +[Learn more about Entitlements →](/security/namespaces) + +## Multi-Level Isolation + +You can use both workspace and namespace scoping: + +```php +class Invoice extends Model +{ + use BelongsToWorkspace, BelongsToNamespace; +} + +// Query scoped to both workspace AND namespace +$invoices = Invoice::all(); +``` + +## Workspace Caching + +The framework provides workspace-isolated caching: + +```php +use Core\Mod\Tenant\Concerns\HasWorkspaceCache; + +class Post extends Model +{ + use BelongsToWorkspace, HasWorkspaceCache; +} + +// Cache automatically isolated per workspace +$posts = Post::ownedByCurrentWorkspaceCached(ttl: 600); + +// Manual workspace caching +$value = workspace_cache()->remember('stats', 600, function () { + return $this->calculateStats(); +}); + +// Clear workspace cache +workspace_cache()->flush(); +``` + +### Cache Tags + +When using Redis/Memcached, caches are tagged with workspace ID: + +```php +// Automatically uses tag: "workspace:{id}" +workspace_cache()->put('key', 'value', 600); + +// Clear all cache for workspace +workspace_cache()->flush(); // Clears all tags for current workspace +``` + +## Context Resolution + +### Middleware + +Require workspace or namespace context: + +```php +use Core\Mod\Tenant\Middleware\RequireWorkspaceContext; + +Route::middleware(RequireWorkspaceContext::class)->group(function () { + Route::get('/dashboard', [DashboardController::class, 'index']); +}); +``` + +### Manual Resolution + +```php +use Core\Mod\Tenant\Services\NamespaceService; + +$service = app(NamespaceService::class); + +// Resolve namespace from request +$namespace = $service->resolveFromRequest($request); + +// Get default namespace for user +$namespace = $service->getDefaultNamespace($user); + +// Set current namespace +$service->setCurrentNamespace($namespace); +``` + +## Workspace Invitations + +Invite users to join workspaces: + +```php +use Core\Mod\Tenant\Models\WorkspaceInvitation; + +$invitation = WorkspaceInvitation::create([ + 'workspace_id' => $workspace->id, + 'email' => 'user@example.com', + 'role' => 'member', + 'invited_by' => $currentUser->id, +]); + +// Send invitation email +$invitation->notify(new WorkspaceInvitationNotification($invitation)); + +// Accept invitation +$invitation->accept($user); +``` + +## Usage Patterns + +### Personal User (No Workspace) + +```php +// User has personal namespace +$user = User::find(1); +$namespace = $user->namespaces()->where('is_default', true)->first(); + +// Can access services via namespace +$result = $entitlements->can($namespace, 'storage'); +``` + +### Agency with Clients + +```php +// Agency workspace owns multiple client namespaces +$workspace = Workspace::where('slug', 'agency')->first(); + +// Each client gets their own namespace +$clientNamespace = Namespace_::create([ + 'name' => 'Client: Acme', + 'owner_type' => Workspace::class, + 'owner_id' => $workspace->id, + 'workspace_id' => $workspace->id, +]); + +// Client's resources scoped to their namespace +$media = Media::where('namespace_id', $clientNamespace->id)->get(); + +// Workspace usage aggregated across all client namespaces +$totalUsage = $workspace->namespaces()->sum('storage_used'); +``` + +### White-Label Operator + +```php +// Operator creates workspace + namespace for customer +$workspace = Workspace::create([ + 'name' => 'Customer Corp', + 'slug' => 'customer-corp', +]); + +$namespace = Namespace_::create([ + 'name' => 'Customer Corp Services', + 'owner_type' => Workspace::class, + 'owner_id' => $workspace->id, + 'workspace_id' => $workspace->id, +]); + +// Attach package to namespace +$namespace->packages()->attach($packageId, [ + 'expires_at' => now()->addYear(), +]); + +// Add user to workspace +$workspace->users()->attach($userId, ['role' => 'admin']); +``` + +## Testing + +### Setting Workspace Context + +```php +use Core\Mod\Tenant\Models\Workspace; + +class PostTest extends TestCase +{ + public function test_creates_post_in_workspace(): void + { + $workspace = Workspace::factory()->create(); + Workspace::setCurrent($workspace); + + $post = Post::create(['title' => 'Test']); + + $this->assertEquals($workspace->id, $post->workspace_id); + } +} +``` + +### Setting Namespace Context + +```php +use Core\Mod\Tenant\Models\Namespace_; + +class MediaTest extends TestCase +{ + public function test_uploads_media_to_namespace(): void + { + $namespace = Namespace_::factory()->create(); + Namespace_::setCurrent($namespace); + + $media = Media::create(['filename' => 'test.jpg']); + + $this->assertEquals($namespace->id, $media->namespace_id); + } +} +``` + +## Database Schema + +### Workspaces Table + +```sql +CREATE TABLE workspaces ( + id BIGINT PRIMARY KEY, + uuid VARCHAR(36) UNIQUE, + name VARCHAR(255), + slug VARCHAR(255) UNIQUE, + tier VARCHAR(50), + settings JSON, + created_at TIMESTAMP, + updated_at TIMESTAMP +); +``` + +### Namespaces Table + +```sql +CREATE TABLE namespaces ( + id BIGINT PRIMARY KEY, + uuid VARCHAR(36) UNIQUE, + name VARCHAR(255), + slug VARCHAR(255), + owner_type VARCHAR(255), -- User::class or Workspace::class + owner_id BIGINT, + workspace_id BIGINT NULL, -- Billing context + settings JSON, + is_default BOOLEAN, + is_active BOOLEAN, + created_at TIMESTAMP, + updated_at TIMESTAMP, + + INDEX idx_owner (owner_type, owner_id), + INDEX idx_workspace (workspace_id) +); +``` + +### Workspace Users Table + +```sql +CREATE TABLE workspace_user ( + id BIGINT PRIMARY KEY, + workspace_id BIGINT, + user_id BIGINT, + role VARCHAR(50), + joined_at TIMESTAMP, + + UNIQUE KEY (workspace_id, user_id) +); +``` + +## Best Practices + +### 1. Always Use Scoping Traits + +```php +// ✅ Good +class Post extends Model +{ + use BelongsToWorkspace; +} + +// ❌ Bad - manual scoping +Post::where('workspace_id', workspace()->id)->get(); +``` + +### 2. Use Namespace for Service Resources + +```php +// ✅ Good - namespace scoped +class Media extends Model +{ + use BelongsToNamespace; +} + +// ❌ Bad - workspace scoped for service resources +class Media extends Model +{ + use BelongsToWorkspace; // Wrong context +} +``` + +### 3. Cache with Workspace Isolation + +```php +// ✅ Good +$stats = workspace_cache()->remember('stats', 600, fn () => $this->calculate()); + +// ❌ Bad - global cache conflicts +$stats = Cache::remember('stats', 600, fn () => $this->calculate()); +``` + +### 4. Validate Entitlements Before Actions + +```php +// ✅ Good +public function store(Request $request) +{ + $result = $entitlements->can(namespace_context(), 'posts', quantity: 1); + + if ($result->isDenied()) { + return back()->with('error', $result->getMessage()); + } + + return CreatePost::run($request->validated()); +} +``` + +## Learn More + +- [Namespaces & Entitlements →](/security/namespaces) +- [Architecture: Multi-Tenancy →](/architecture/multi-tenancy) +- [Workspace Caching →](#workspace-caching) +- [Testing Multi-Tenancy →](/guide/testing#multi-tenancy) diff --git a/build/php/testing.md b/build/php/testing.md new file mode 100644 index 0000000..a6a5213 --- /dev/null +++ b/build/php/testing.md @@ -0,0 +1,497 @@ +# Testing Guide + +Comprehensive guide to testing Core PHP Framework applications. + +## Running Tests + +```bash +# Run all tests +composer test + +# Run specific test file +./vendor/bin/phpunit packages/core-php/tests/Feature/ActivityLogServiceTest.php + +# Run tests with coverage +./vendor/bin/phpunit --coverage-html coverage + +# Run specific test method +./vendor/bin/phpunit --filter test_creates_post +``` + +## Test Structure + +``` +tests/ +├── Feature/ # Integration tests +│ ├── ApiTest.php +│ ├── AuthTest.php +│ └── PostTest.php +├── Unit/ # Unit tests +│ ├── ActionTest.php +│ └── ServiceTest.php +└── TestCase.php # Base test case +``` + +## Writing Feature Tests + +Feature tests test complete workflows: + +```php +create(); + + $response = $this->actingAs($user) + ->post('/posts', [ + 'title' => 'Test Post', + 'content' => 'Test content', + 'status' => 'draft', + ]); + + $response->assertRedirect(); + + $this->assertDatabaseHas('posts', [ + 'title' => 'Test Post', + 'author_id' => $user->id, + ]); + } + + public function test_guest_cannot_create_post(): void + { + $response = $this->post('/posts', [ + 'title' => 'Test Post', + 'content' => 'Test content', + ]); + + $response->assertRedirect(route('login')); + } + + public function test_user_can_view_own_posts(): void + { + $user = User::factory()->create(); + $post = Post::factory()->create(['author_id' => $user->id]); + + $response = $this->actingAs($user) + ->get("/posts/{$post->id}"); + + $response->assertOk(); + $response->assertSee($post->title); + } +} +``` + +## Writing Unit Tests + +Unit tests test isolated components: + +```php + 'Test Post', + 'content' => 'Test content', + 'status' => 'draft', + ]); + + $this->assertInstanceOf(Post::class, $post); + $this->assertEquals('Test Post', $post->title); + $this->assertDatabaseHas('posts', ['id' => $post->id]); + } + + public function test_generates_slug_from_title(): void + { + $post = CreatePost::run([ + 'title' => 'Test Post', + 'content' => 'Content', + ]); + + $this->assertEquals('test-post', $post->slug); + } + + public function test_throws_exception_for_invalid_data(): void + { + $this->expectException(ValidationException::class); + + CreatePost::run([ + 'title' => '', // Invalid + 'content' => 'Content', + ]); + } +} +``` + +## Database Testing + +### Factories + +```php + $this->faker->sentence(), + 'content' => $this->faker->paragraphs(3, true), + 'status' => 'draft', + 'author_id' => User::factory(), + ]; + } + + public function published(): self + { + return $this->state([ + 'status' => 'published', + 'published_at' => now(), + ]); + } + + public function draft(): self + { + return $this->state(['status' => 'draft']); + } +} +``` + +**Usage:** + +```php +// Create single post +$post = Post::factory()->create(); + +// Create published post +$post = Post::factory()->published()->create(); + +// Create multiple posts +$posts = Post::factory()->count(10)->create(); + +// Create with specific attributes +$post = Post::factory()->create([ + 'title' => 'Specific Title', +]); +``` + +### Database Assertions + +```php +// Assert record exists +$this->assertDatabaseHas('posts', [ + 'title' => 'Test Post', + 'status' => 'published', +]); + +// Assert record doesn't exist +$this->assertDatabaseMissing('posts', [ + 'title' => 'Deleted Post', +]); + +// Assert record count +$this->assertDatabaseCount('posts', 10); + +// Assert model exists +$this->assertModelExists($post); + +// Assert model deleted +$this->assertSoftDeleted($post); +``` + +## API Testing + +```php +create(); + Sanctum::actingAs($user, ['posts:read']); + + Post::factory()->count(5)->published()->create(); + + $response = $this->getJson('/api/v1/posts'); + + $response->assertOk(); + $response->assertJsonCount(5, 'data'); + $response->assertJsonStructure([ + 'data' => [ + '*' => ['id', 'title', 'status', 'created_at'], + ], + ]); + } + + public function test_creates_post(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user, ['posts:write']); + + $response = $this->postJson('/api/v1/posts', [ + 'title' => 'API Test Post', + 'content' => 'Test content', + ]); + + $response->assertCreated(); + $response->assertJson([ + 'title' => 'API Test Post', + ]); + + $this->assertDatabaseHas('posts', [ + 'title' => 'API Test Post', + ]); + } + + public function test_requires_authentication(): void + { + $response = $this->getJson('/api/v1/posts'); + + $response->assertUnauthorized(); + } + + public function test_requires_correct_scope(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user, ['posts:read']); // Missing write scope + + $response = $this->postJson('/api/v1/posts', [ + 'title' => 'Test', + 'content' => 'Content', + ]); + + $response->assertForbidden(); + } +} +``` + +## Livewire Testing + +```php +create(); + + Livewire::test(PostEditor::class, ['post' => $post]) + ->assertSee($post->title) + ->assertSee('Save'); + } + + public function test_updates_post(): void + { + $post = Post::factory()->create(['title' => 'Original']); + + Livewire::test(PostEditor::class, ['post' => $post]) + ->set('title', 'Updated Title') + ->call('save') + ->assertDispatched('post-updated'); + + $this->assertEquals('Updated Title', $post->fresh()->title); + } + + public function test_validates_input(): void + { + $post = Post::factory()->create(); + + Livewire::test(PostEditor::class, ['post' => $post]) + ->set('title', '') + ->call('save') + ->assertHasErrors(['title' => 'required']); + } +} +``` + +## Mocking + +### Mocking Services + +```php +use Mockery; +use Mod\Payment\Services\PaymentService; + +public function test_processes_order_with_mock(): void +{ + $mock = Mockery::mock(PaymentService::class); + $mock->shouldReceive('charge') + ->once() + ->with(1000, 'GBP') + ->andReturn(new PaymentResult(success: true)); + + $this->app->instance(PaymentService::class, $mock); + + $order = Order::factory()->create(); + $result = $this->orderService->process($order); + + $this->assertTrue($result->success); +} +``` + +### Mocking Facades + +```php +use Illuminate\Support\Facades\Storage; + +public function test_uploads_file(): void +{ + Storage::fake('s3'); + + $this->post('/upload', [ + 'file' => UploadedFile::fake()->image('photo.jpg'), + ]); + + Storage::disk('s3')->assertExists('photos/photo.jpg'); +} +``` + +### Mocking Events + +```php +use Illuminate\Support\Facades\Event; +use Mod\Blog\Events\PostPublished; + +public function test_fires_event(): void +{ + Event::fake([PostPublished::class]); + + $post = Post::factory()->create(); + $service->publish($post); + + Event::assertDispatched(PostPublished::class, function ($event) use ($post) { + return $event->post->id === $post->id; + }); +} +``` + +## Testing Workspace Isolation + +```php +public function test_scopes_to_workspace(): void +{ + $workspace1 = Workspace::factory()->create(); + $workspace2 = Workspace::factory()->create(); + + $post1 = Post::factory()->create(['workspace_id' => $workspace1->id]); + $post2 = Post::factory()->create(['workspace_id' => $workspace2->id]); + + // Acting as user in workspace1 + $user = User::factory()->create(['workspace_id' => $workspace1->id]); + + $posts = Post::all(); // Should only see workspace1's posts + + $this->assertCount(1, $posts); + $this->assertEquals($post1->id, $posts->first()->id); +} +``` + +## Best Practices + +### 1. Test One Thing + +```php +// ✅ Good - tests one behavior +public function test_creates_post(): void +{ + $post = CreatePost::run([...]); + $this->assertInstanceOf(Post::class, $post); +} + +// ❌ Bad - tests multiple things +public function test_post_operations(): void +{ + $post = CreatePost::run([...]); + $this->assertInstanceOf(Post::class, $post); + + $post->publish(); + $this->assertEquals('published', $post->status); + + $post->delete(); + $this->assertSoftDeleted($post); +} +``` + +### 2. Use Descriptive Names + +```php +// ✅ Good +public function test_user_can_create_post_with_valid_data(): void + +// ❌ Bad +public function test_create(): void +``` + +### 3. Arrange, Act, Assert + +```php +public function test_publishes_post(): void +{ + // Arrange + $post = Post::factory()->create(['status' => 'draft']); + $user = User::factory()->create(); + + // Act + $result = $service->publish($post, $user); + + // Assert + $this->assertEquals('published', $result->status); + $this->assertNotNull($result->published_at); +} +``` + +### 4. Clean Up After Tests + +```php +use Illuminate\Foundation\Testing\RefreshDatabase; + +class PostTest extends TestCase +{ + use RefreshDatabase; // Resets database after each test + + public function test_something(): void + { + // Test code + } +} +``` + +## Learn More + +- [Actions Pattern →](/patterns-guide/actions) +- [Service Pattern →](/patterns-guide/services) +- [Contributing →](/contributing) diff --git a/deploy/docker.md b/deploy/docker.md new file mode 100644 index 0000000..832ebea --- /dev/null +++ b/deploy/docker.md @@ -0,0 +1,208 @@ +# Docker Deployment + +Deploy containerised applications with Docker, Docker Compose, and container orchestrators. + +## Building Images + +Build Docker images with `core build`: + +```bash +# Auto-detect Dockerfile and build +core build --type docker + +# 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 +``` + +## Docker Compose + +### Basic Setup + +`docker-compose.yml`: + +```yaml +version: '3.8' + +services: + app: + image: ghcr.io/myorg/myapp:latest + ports: + - "8080:8080" + environment: + - APP_ENV=production + - DATABASE_URL=postgres://db:5432/myapp + depends_on: + - db + - redis + + db: + image: postgres:15 + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=myapp + - POSTGRES_PASSWORD_FILE=/run/secrets/db_password + secrets: + - db_password + + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + +volumes: + postgres_data: + redis_data: + +secrets: + db_password: + file: ./secrets/db_password.txt +``` + +### Deploy + +```bash +# Start services +docker compose up -d + +# View logs +docker compose logs -f app + +# Scale horizontally +docker compose up -d --scale app=3 + +# Update to new version +docker compose pull && docker compose up -d +``` + +## Multi-Stage Builds + +Optimised Dockerfile for PHP applications: + +```dockerfile +# Build stage +FROM composer:2 AS deps +WORKDIR /app +COPY composer.json composer.lock ./ +RUN composer install --no-dev --no-scripts --prefer-dist + +# Production stage +FROM dunglas/frankenphp:latest +WORKDIR /app + +COPY --from=deps /app/vendor ./vendor +COPY . . + +RUN composer dump-autoload --optimize + +EXPOSE 8080 +CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"] +``` + +## Health Checks + +Add health checks for orchestrator integration: + +```dockerfile +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 +``` + +Or in docker-compose: + +```yaml +services: + app: + image: myapp:latest + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s +``` + +## Environment Configuration + +### Using .env Files + +```yaml +services: + app: + image: myapp:latest + env_file: + - .env + - .env.local +``` + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `APP_ENV` | Environment (production, staging) | +| `APP_DEBUG` | Enable debug mode | +| `DATABASE_URL` | Database connection string | +| `REDIS_URL` | Redis connection string | +| `LOG_LEVEL` | Logging verbosity | + +## Registry Authentication + +### GitHub Container Registry + +```bash +# Login +echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin + +# Push +docker push ghcr.io/myorg/myapp:latest +``` + +### AWS ECR + +```bash +# Login +aws ecr get-login-password --region eu-west-1 | \ + docker login --username AWS --password-stdin 123456789.dkr.ecr.eu-west-1.amazonaws.com + +# Push +docker push 123456789.dkr.ecr.eu-west-1.amazonaws.com/myapp:latest +``` + +## Orchestration + +### Docker Swarm + +```bash +# Initialise swarm +docker swarm init + +# Deploy stack +docker stack deploy -c docker-compose.yml myapp + +# Scale service +docker service scale myapp_app=5 + +# Rolling update +docker service update --image myapp:v2 myapp_app +``` + +### Kubernetes + +Generate Kubernetes manifests from Compose: + +```bash +# Using kompose +kompose convert -f docker-compose.yml + +# Apply to cluster +kubectl apply -f . +``` + +## See Also + +- [Docker Publisher](/publish/docker) - Push images to registries +- [Build Command](/build/cli/build/) - Build Docker images +- [LinuxKit](linuxkit) - VM-based deployment diff --git a/deploy/index.md b/deploy/index.md new file mode 100644 index 0000000..93a746c --- /dev/null +++ b/deploy/index.md @@ -0,0 +1,68 @@ +# Deploy + +Deploy applications to VMs, containers, and cloud infrastructure. + +## Deployment Options + +| Target | Description | Use Case | +|--------|-------------|----------| +| [PHP](php) | Laravel/PHP with FrankenPHP | Web applications, APIs | +| [LinuxKit](linuxkit) | Lightweight immutable VMs | Production servers, edge nodes | +| [Templates](templates) | Pre-configured VM images | Quick deployment, dev environments | +| [Docker](docker) | Container orchestration | Kubernetes, Swarm, ECS | + +## Quick Start + +### Run a Production Server + +```bash +# Build and run from template +core vm run --template server-php --var SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" + +# Or run a pre-built image +core vm run -d --memory 4096 --cpus 4 server.iso +``` + +### Deploy to Docker + +```bash +# Build and push image +core build --type docker --image ghcr.io/myorg/myapp --push + +# Deploy with docker-compose +docker compose up -d +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Build Phase │ +│ core build → Docker images, LinuxKit ISOs, binaries │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Publish Phase │ +│ core ci → GitHub, Docker Hub, GHCR, Homebrew, etc. │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Deploy Phase │ +│ core vm → LinuxKit VMs, templates, orchestration │ +└─────────────────────────────────────────────────────────┘ +``` + +## CLI Commands + +| Command | Description | +|---------|-------------| +| `core vm run` | Run a LinuxKit image or template | +| `core vm ps` | List running VMs | +| `core vm stop` | Stop a VM | +| `core vm logs` | View VM logs | +| `core vm exec` | Execute command in VM | +| `core vm templates` | Manage LinuxKit templates | + +See the [CLI Reference](/build/cli/vm/) for full command documentation. diff --git a/deploy/linuxkit.md b/deploy/linuxkit.md new file mode 100644 index 0000000..cbd4cbd --- /dev/null +++ b/deploy/linuxkit.md @@ -0,0 +1,168 @@ +# LinuxKit VMs + +Deploy applications using lightweight, immutable LinuxKit VMs. VMs run using QEMU or HyperKit depending on your system. + +## Running VMs + +### From Image File + +Run pre-built images in `.iso`, `.qcow2`, `.vmdk`, or `.raw` format: + +```bash +# Run ISO image +core vm run server.iso + +# Run with more resources +core vm run -d --memory 4096 --cpus 4 server.qcow2 + +# Custom SSH port +core vm run --ssh-port 2223 server.iso +``` + +### From Template + +Build and run from a LinuxKit template in one command: + +```bash +# Run template with SSH key +core vm run --template server-php --var SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" + +# Multiple variables +core vm run --template server-php \ + --var SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" \ + --var DOMAIN=example.com +``` + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--template` | Run from a LinuxKit template | - | +| `--var` | Template variable (KEY=VALUE) | - | +| `--name` | VM name | auto | +| `--memory` | Memory in MB | 1024 | +| `--cpus` | CPU count | 1 | +| `--ssh-port` | SSH port for exec | 2222 | +| `-d` | Detached mode (background) | false | + +## Managing VMs + +### List Running VMs + +```bash +# Show running VMs +core vm ps + +# Include stopped VMs +core vm ps -a +``` + +Output: +``` +ID NAME IMAGE STATUS STARTED PID +abc12345 myvm server-php.qcow2 running 5m 12345 +def67890 devbox core-dev.iso stopped 2h - +``` + +### Stop a VM + +```bash +# Full ID +core vm stop abc12345678 + +# Partial ID match +core vm stop abc1 +``` + +### View Logs + +```bash +# View logs +core vm logs abc12345 + +# Follow logs (like tail -f) +core vm logs -f abc12345 +``` + +### Execute Commands + +Run commands in a VM via SSH: + +```bash +# List files +core vm exec abc12345 ls -la + +# Check services +core vm exec abc12345 systemctl status php-fpm + +# Open interactive shell +core vm exec abc12345 /bin/sh +``` + +## Building Images + +Build LinuxKit images with `core build`: + +```bash +# Build ISO from config +core build --type linuxkit --config .core/linuxkit/server.yml + +# Build QCOW2 for QEMU/KVM +core build --type linuxkit --config .core/linuxkit/server.yml --format qcow2-bios + +# Build for multiple platforms +core build --type linuxkit --targets linux/amd64,linux/arm64 +``` + +### Output Formats + +| Format | Description | Use Case | +|--------|-------------|----------| +| `iso-bios` | Bootable ISO | Physical servers, legacy VMs | +| `qcow2-bios` | QEMU/KVM image | Linux hypervisors | +| `raw` | Raw disk image | Cloud providers | +| `vmdk` | VMware image | VMware ESXi | +| `vhd` | Hyper-V image | Windows Server | + +## LinuxKit Configuration + +Example `.core/linuxkit/server.yml`: + +```yaml +kernel: + image: linuxkit/kernel:5.15 + cmdline: "console=tty0" + +init: + - linuxkit/init:v0.8 + - linuxkit/runc:v0.8 + +onboot: + - name: sysctl + image: linuxkit/sysctl:v0.8 + - name: dhcpcd + image: linuxkit/dhcpcd:v0.8 + command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf"] + +services: + - name: sshd + image: linuxkit/sshd:v0.8 + - name: php + image: dunglas/frankenphp:latest + +files: + - path: /etc/ssh/authorized_keys + contents: | + {{ .SSH_KEY }} + - path: /etc/myapp/config.yaml + contents: | + server: + port: 8080 + domain: {{ .DOMAIN }} +``` + +## See Also + +- [Templates](templates) - Pre-configured VM templates +- [LinuxKit Publisher](/publish/linuxkit) - Publish LinuxKit images +- [CLI Reference](/build/cli/vm/) - Full VM command documentation diff --git a/deploy/php.md b/deploy/php.md new file mode 100644 index 0000000..366faf8 --- /dev/null +++ b/deploy/php.md @@ -0,0 +1,311 @@ +# PHP Deployment + +Deploy Laravel/PHP applications using FrankenPHP containers, LinuxKit VMs, or Coolify. + +## Quick Start + +```bash +# Build production image +core php build --name myapp --tag v1.0 + +# Run locally +core php serve --name myapp -d + +# Deploy to Coolify +core php deploy --wait +``` + +## Building Images + +### Docker Image + +Build a production-ready Docker image with FrankenPHP: + +```bash +# Basic build +core php build + +# With custom name and tag +core php build --name myapp --tag v1.0 + +# For specific platform +core php build --name myapp --platform linux/amd64 + +# Without cache +core php build --name myapp --no-cache +``` + +### LinuxKit Image + +Build a bootable VM image: + +```bash +# Build with default template (server-php) +core php build --type linuxkit + +# Build QCOW2 for QEMU/KVM +core php build --type linuxkit --format qcow2 + +# Build ISO for bare metal +core php build --type linuxkit --format iso + +# Custom output path +core php build --type linuxkit --output ./dist/server.qcow2 +``` + +### Build Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--type` | Build type: `docker` or `linuxkit` | docker | +| `--name` | Image name | project directory | +| `--tag` | Image tag | latest | +| `--platform` | Target platform | linux/amd64 | +| `--dockerfile` | Custom Dockerfile path | Dockerfile | +| `--format` | LinuxKit format: qcow2, iso, raw, vmdk | qcow2 | +| `--template` | LinuxKit template | server-php | +| `--no-cache` | Build without cache | false | + +## Running Production Containers + +### Local Testing + +Run a production container locally before deploying: + +```bash +# Run in foreground +core php serve --name myapp + +# Run detached (background) +core php serve --name myapp -d + +# Custom ports +core php serve --name myapp --port 8080 --https-port 8443 + +# With environment file +core php serve --name myapp --env-file .env.production +``` + +### Shell Access + +Debug running containers: + +```bash +# Open shell in container +core php shell + +# Run artisan commands +docker exec -it php artisan migrate:status +``` + +## Deploying to Coolify + +[Coolify](https://coolify.io) is a self-hosted PaaS for deploying applications. + +### Configuration + +Add Coolify credentials to `.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 +``` + +Or configure in `.core/php.yaml`: + +```yaml +version: 1 + +deploy: + coolify: + server: https://coolify.example.com + project: my-project +``` + +### Deploy Commands + +```bash +# Deploy to production +core php deploy + +# Deploy to staging +core php deploy --staging + +# Force deploy (even if no changes) +core php deploy --force + +# Wait for deployment to complete +core php deploy --wait +``` + +### Check Status + +```bash +# Current deployment status +core php deploy:status + +# Staging status +core php deploy:status --staging + +# Specific deployment +core php deploy:status --id abc123 +``` + +### Rollback + +```bash +# Rollback to previous deployment +core php deploy:rollback + +# Rollback staging +core php deploy:rollback --staging + +# Rollback to specific deployment +core php deploy:rollback --id abc123 + +# Wait for rollback to complete +core php deploy:rollback --wait +``` + +### Deployment History + +```bash +# List recent deployments +core php deploy:list + +# Staging deployments +core php deploy:list --staging + +# Show more +core php deploy:list --limit 20 +``` + +## CI/CD Pipeline + +### GitHub Actions + +```yaml +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Core CLI + run: | + curl -fsSL https://get.host.uk.com | bash + echo "$HOME/.core/bin" >> $GITHUB_PATH + + - name: Build image + run: core php build --name ${{ vars.IMAGE_NAME }} --tag ${{ github.sha }} + + - name: Push to registry + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + docker push ${{ vars.IMAGE_NAME }}:${{ github.sha }} + + - name: Deploy + env: + COOLIFY_URL: ${{ secrets.COOLIFY_URL }} + COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }} + COOLIFY_APP_ID: ${{ secrets.COOLIFY_APP_ID }} + run: core php deploy --wait +``` + +### GitLab CI + +```yaml +deploy: + stage: deploy + image: hostuk/core:latest + script: + - core php build --name $CI_REGISTRY_IMAGE --tag $CI_COMMIT_SHA + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + - core php deploy --wait + only: + - main +``` + +## Environment Configuration + +### Production .env + +```env +APP_ENV=production +APP_DEBUG=false +APP_URL=https://myapp.com + +# Database +DB_CONNECTION=pgsql +DB_HOST=db.internal +DB_DATABASE=myapp +DB_USERNAME=myapp +DB_PASSWORD=${DB_PASSWORD} + +# Cache & Queue +CACHE_DRIVER=redis +QUEUE_CONNECTION=redis +SESSION_DRIVER=redis +REDIS_HOST=redis.internal + +# FrankenPHP +OCTANE_SERVER=frankenphp +OCTANE_WORKERS=auto +OCTANE_MAX_REQUESTS=1000 +``` + +### Health Checks + +Ensure your app has a health endpoint: + +```php +// routes/web.php +Route::get('/health', fn () => response()->json([ + 'status' => 'ok', + 'timestamp' => now()->toIso8601String(), +])); +``` + +## Deployment Strategies + +### Blue-Green + +```bash +# Build new version +core php build --name myapp --tag v2.0 + +# Deploy to staging +core php deploy --staging --wait + +# Test staging +curl https://staging.myapp.com/health + +# Switch production +core php deploy --wait +``` + +### Canary + +```bash +# Deploy to canary (10% traffic) +COOLIFY_APP_ID=$CANARY_APP_ID core php deploy --wait + +# Monitor metrics, then full rollout +core php deploy --wait +``` + +## See Also + +- [Docker Deployment](docker) - Container orchestration +- [LinuxKit VMs](linuxkit) - VM-based deployment +- [Templates](templates) - Pre-configured VM templates +- [PHP CLI Reference](/build/cli/php/) - Full command documentation diff --git a/deploy/templates.md b/deploy/templates.md new file mode 100644 index 0000000..14e7e58 --- /dev/null +++ b/deploy/templates.md @@ -0,0 +1,220 @@ +# Templates + +Pre-configured LinuxKit templates for common deployment scenarios. + +## Available Templates + +| Template | Description | Platforms | +|----------|-------------|-----------| +| `core-dev` | Full development environment with 100+ tools | linux/amd64, linux/arm64 | +| `server-php` | FrankenPHP production server | linux/amd64, linux/arm64 | +| `edge-node` | Minimal edge deployment | linux/amd64, linux/arm64 | + +## Using Templates + +### List 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 +``` + +### Show Template Details + +```bash +core vm templates show server-php +``` + +Output: +``` +Template: server-php + +Description: FrankenPHP production server + +Platforms: + - linux/amd64 + - linux/arm64 + +Formats: + - iso + - qcow2 + +Services: + - sshd + - frankenphp + - php-fpm + +Size: ~800MB +``` + +### Show Template Variables + +```bash +core vm templates vars server-php +``` + +Output: +``` +Variables for server-php: + SSH_KEY (required) SSH public key + DOMAIN (optional) Server domain name + MEMORY (optional) Memory in MB (default: 2048) + CPUS (optional) CPU count (default: 2) +``` + +### Run Template + +```bash +# With required variables +core vm run --template server-php --var SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" + +# With all variables +core vm run --template server-php \ + --var SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" \ + --var DOMAIN=example.com \ + --var MEMORY=4096 +``` + +## Template Locations + +Templates are searched in order: + +1. `.core/linuxkit/` - Project-specific templates +2. `~/.core/templates/` - User templates +3. Built-in templates + +## Creating Templates + +Create a LinuxKit YAML file in `.core/linuxkit/`: + +### Development Template + +`.core/linuxkit/dev.yml`: + +```yaml +kernel: + image: linuxkit/kernel:5.15 + cmdline: "console=tty0" + +init: + - linuxkit/init:v0.8 + - linuxkit/runc:v0.8 + - linuxkit/containerd:v0.8 + +onboot: + - name: sysctl + image: linuxkit/sysctl:v0.8 + - name: dhcpcd + image: linuxkit/dhcpcd:v0.8 + command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf"] + +services: + - name: sshd + image: linuxkit/sshd:v0.8 + - name: docker + image: docker:dind + capabilities: + - all + binds: + - /var/run:/var/run + +files: + - path: /etc/ssh/authorized_keys + contents: | + {{ .SSH_KEY }} +``` + +### Production Template + +`.core/linuxkit/prod.yml`: + +```yaml +kernel: + image: linuxkit/kernel:5.15 + cmdline: "console=tty0 quiet" + +init: + - linuxkit/init:v0.8 + - linuxkit/runc:v0.8 + +onboot: + - name: sysctl + image: linuxkit/sysctl:v0.8 + binds: + - /etc/sysctl.d:/etc/sysctl.d + - name: dhcpcd + image: linuxkit/dhcpcd:v0.8 + command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf"] + +services: + - name: sshd + image: linuxkit/sshd:v0.8 + - name: app + image: myapp:{{ .VERSION }} + capabilities: + - CAP_NET_BIND_SERVICE + binds: + - /var/data:/data + +files: + - path: /etc/ssh/authorized_keys + contents: | + {{ .SSH_KEY }} + - path: /etc/myapp/config.yaml + contents: | + server: + port: 443 + domain: {{ .DOMAIN }} + database: + path: /data/app.db +``` + +Run with: + +```bash +core vm run --template prod \ + --var SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" \ + --var VERSION=1.2.3 \ + --var DOMAIN=example.com +``` + +## Template Variables + +Variables use Go template syntax with double braces: + +```yaml +# Required variable +contents: | + {{ .SSH_KEY }} + +# With default value +contents: | + port: {{ .PORT | default "8080" }} + +# Conditional +{{ if .DEBUG }} + debug: true +{{ end }} +``` + +## See Also + +- [LinuxKit VMs](linuxkit) - Running and managing VMs +- [Build Command](/build/cli/build/) - Building LinuxKit images +- [VM Command](/build/cli/vm/) - Full VM CLI reference diff --git a/discovery/l1-packages-vs-standalone-modules.md b/discovery/l1-packages-vs-standalone-modules.md new file mode 100644 index 0000000..5dc2b84 --- /dev/null +++ b/discovery/l1-packages-vs-standalone-modules.md @@ -0,0 +1,55 @@ +# Discovery: L1 Packages vs Standalone php-* Modules + +**Issue:** #3 +**Date:** 2026-02-21 +**Status:** Complete – findings filed as issues #4, #5, #6, #7 + +## L1 Packages (Boot.php files under src/Core/) + +| Package | Path | Has Standalone? | +|---------|------|----------------| +| Activity | `src/Core/Activity/` | No | +| Bouncer | `src/Core/Bouncer/` | No | +| Bouncer/Gate | `src/Core/Bouncer/Gate/` | No | +| Cdn | `src/Core/Cdn/` | No | +| Config | `src/Core/Config/` | No | +| Console | `src/Core/Console/` | No | +| Front | `src/Core/Front/` | No (root) | +| Front/Admin | `src/Core/Front/Admin/` | Partial – `core/php-admin` extends | +| Front/Api | `src/Core/Front/Api/` | Partial – `core/php-api` extends | +| Front/Cli | `src/Core/Front/Cli/` | No | +| Front/Client | `src/Core/Front/Client/` | No | +| Front/Components | `src/Core/Front/Components/` | No | +| Front/Mcp | `src/Core/Front/Mcp/` | Intentional – `core/php-mcp` fills | +| Front/Stdio | `src/Core/Front/Stdio/` | No | +| Front/Web | `src/Core/Front/Web/` | No | +| Headers | `src/Core/Headers/` | No | +| Helpers | `src/Core/Helpers/` | No | +| Lang | `src/Core/Lang/` | No | +| Mail | `src/Core/Mail/` | No | +| Media | `src/Core/Media/` | No | +| Search | `src/Core/Search/` | No (admin search is separate concern) | +| Seo | `src/Core/Seo/` | No | + +## Standalone Repos + +| Repo | Package | Namespace | Relationship | +|------|---------|-----------|-------------| +| `core/php-tenant` | `host-uk/core-tenant` | `Core\Tenant\` | Extension | +| `core/php-admin` | `host-uk/core-admin` | `Core\Admin\` | Extends Front/Admin | +| `core/php-api` | `host-uk/core-api` | `Core\Api\` | Extends Front/Api | +| `core/php-content` | `host-uk/core-content` | `Core\Mod\Content\` | Extension | +| `core/php-commerce` | `host-uk/core-commerce` | `Core\Mod\Commerce\` | Extension | +| `core/php-agentic` | `host-uk/core-agentic` | `Core\Mod\Agentic\` | Extension | +| `core/php-mcp` | `host-uk/core-mcp` | `Core\Mcp\` | Fills Front/Mcp shell | +| `core/php-developer` | `host-uk/core-developer` | `Core\Developer\` | Extension (also needs core-admin) | +| `core/php-devops` | *(DevOps tooling)* | N/A | Not a PHP module | + +## Overlaps Found + +See issues filed: + +- **#4** `Front/Api` rate limiting vs `core/php-api` `RateLimitApi` middleware – double rate limiting risk +- **#5** `Core\Search` vs `core/php-admin` search subsystem – dual registries +- **#6** `Core\Activity` UI duplicated in `core/php-admin` and `core/php-developer` +- **#7** Summary issue with full analysis diff --git a/index.md b/index.md new file mode 100644 index 0000000..76908c5 --- /dev/null +++ b/index.md @@ -0,0 +1,126 @@ +--- +layout: home + +hero: + name: Core PHP Framework + text: Modular Monolith for Laravel + tagline: Event-driven architecture with lazy module loading and built-in multi-tenancy + actions: + - theme: brand + text: Get Started + link: /guide/getting-started + - theme: alt + text: View on GitHub + link: https://github.com/host-uk/core-php + +features: + - icon: ⚡️ + title: Event-Driven Modules + details: Modules declare interest in lifecycle events and are only loaded when needed, reducing overhead for unused features. + + - icon: 🔒 + title: Multi-Tenant Isolation + details: Automatic workspace scoping for Eloquent models with strict mode enforcement prevents data leakage. + + - icon: 🎯 + title: Actions Pattern + details: Extract business logic into testable, reusable classes with automatic dependency injection. + + - icon: 📝 + title: Activity Logging + details: Built-in audit trails for model changes with minimal setup using Spatie Activity Log. + + - icon: 🌱 + title: Seeder Auto-Discovery + details: Automatic seeder ordering via priority and dependency attributes eliminates manual registration. + + - icon: 🎨 + title: HLCRF Layouts + details: Data-driven composable layouts with infinite nesting for flexible UI structures. + + - icon: 🔐 + title: Security First + details: Bouncer action gates, request whitelisting, and comprehensive input sanitization. + + - icon: 🚀 + title: Production Ready + details: Battle-tested in production with comprehensive test coverage and security audits. +--- + +## Quick Start + +```bash +# Install via Composer +composer require host-uk/core + +# Create a module +php artisan make:mod Commerce + +# Register lifecycle events +class Boot +{ + public static array $listens = [ + WebRoutesRegistering::class => 'onWebRoutes', + ]; + + public function onWebRoutes(WebRoutesRegistering $event): void + { + $event->routes(fn () => require __DIR__.'/Routes/web.php'); + } +} +``` + +## Why Core PHP? + +Traditional Laravel applications grow into monoliths with tight coupling and unclear boundaries. Microservices add complexity you may not need. **Core PHP provides a middle ground**: a structured monolith with clear module boundaries, lazy loading, and the ability to extract services later if needed. + +### Key Benefits + +- **Reduced Complexity** - No network overhead, distributed tracing, or service mesh +- **Clear Boundaries** - Modules have explicit dependencies via lifecycle events +- **Performance** - Lazy loading means unused modules aren't loaded +- **Flexibility** - Start monolithic, extract services when it makes sense +- **Type Safety** - Full IDE support with no RPC serialization + +## Packages + +
+ +### [Core](/packages/core) +Event-driven architecture, module system, actions pattern, and multi-tenancy. + +### [Admin](/packages/admin) +Livewire-powered admin panel with global search and service management. + +### [API](/packages/api) +REST API with OpenAPI docs, rate limiting, webhook signing, and secure keys. + +### [MCP](/packages/mcp) +Model Context Protocol tools for AI integrations with analytics and security. + +
+ +## Community + +- **GitHub Discussions** - Ask questions and share ideas +- **Issue Tracker** - Report bugs and request features +- **Contributing** - See our [contributing guide](/contributing) + + diff --git a/packages/admin/authorization.md b/packages/admin/authorization.md new file mode 100644 index 0000000..a10f19b --- /dev/null +++ b/packages/admin/authorization.md @@ -0,0 +1,559 @@ +# Authorization + +Integration with Laravel's Gate and Policy system for fine-grained authorization in admin panels. + +## Form Component Authorization + +All form components support authorization props: + +```blade + + Publish Post + +``` + +### Authorization Props + +**`can` - Single ability:** + +```blade + + Delete + + +{{-- Only shown if user can delete the post --}} +``` + +**`cannot` - Inverse check:** + +```blade + + +{{-- Disabled if user cannot publish --}} +``` + +**`canAny` - Multiple abilities (OR):** + +```blade + + Edit Post + + +{{-- Shown if user can either edit OR update --}} +``` + +## Policy Integration + +### Defining Policies + +```php +workspace_id === $post->workspace_id; + } + + public function create(User $user): bool + { + return $user->hasPermission('posts.create'); + } + + public function update(User $user, Post $post): bool + { + return $user->id === $post->author_id + || $user->hasRole('editor'); + } + + public function delete(User $user, Post $post): bool + { + return $user->hasRole('admin') + && $user->workspace_id === $post->workspace_id; + } + + public function publish(User $user, Post $post): bool + { + return $user->hasPermission('posts.publish') + && $post->status !== 'archived'; + } +} +``` + +### Registering Policies + +```php +use Illuminate\Support\Facades\Gate; +use Mod\Blog\Models\Post; +use Mod\Blog\Policies\PostPolicy; + +// In AuthServiceProvider or module Boot class +Gate::policy(Post::class, PostPolicy::class); +``` + +## Action Gate + +Use the Action Gate system for route-level authorization: + +### Defining Actions + +```php +middleware(['auth', ActionGateMiddleware::class]); + +// Protect route group +Route::middleware(['auth', ActionGateMiddleware::class]) + ->group(function () { + Route::post('/posts', [PostController::class, 'store']); + Route::post('/posts/{post}/publish', [PostController::class, 'publish']); + }); +``` + +### Checking Permissions + +```php +use Core\Bouncer\Gate\ActionGateService; + +$gate = app(ActionGateService::class); + +// Check if user can perform action +if ($gate->allows('posts.create', auth()->user())) { + // User has permission +} + +// Check with additional context +if ($gate->allows('posts.publish', auth()->user(), $post)) { + // User can publish this specific post +} + +// Get all user permissions +$permissions = $gate->getUserPermissions(auth()->user()); +``` + +## Admin Menu Authorization + +Restrict menu items by permission: + +```php +use Core\Front\Admin\Support\MenuItemBuilder; + +MenuItemBuilder::create('Posts') + ->route('admin.posts.index') + ->icon('heroicon-o-document-text') + ->can('posts.view') // Only shown if user can view posts + ->badge(fn () => Post::pending()->count()) + ->children([ + MenuItemBuilder::create('All Posts') + ->route('admin.posts.index'), + + MenuItemBuilder::create('Create Post') + ->route('admin.posts.create') + ->can('posts.create'), // Nested permission check + + MenuItemBuilder::create('Categories') + ->route('admin.categories.index') + ->canAny(['categories.view', 'categories.edit']), + ]); +``` + +## Livewire Modal Authorization + +Protect Livewire modals with authorization checks: + +```php +authorize('update', $post); + + $this->post = $post; + } + + public function save() + { + // Authorize action + $this->authorize('update', $this->post); + + $this->post->save(); + + $this->dispatch('post-updated'); + } + + public function publish() + { + // Custom authorization + $this->authorize('publish', $this->post); + + $this->post->update(['status' => 'published']); + } +} +``` + +## Workspace Scoping + +Automatic workspace isolation with policies: + +```php +class PostPolicy +{ + public function viewAny(User $user): bool + { + // User can view posts in their workspace + return true; + } + + public function view(User $user, Post $post): bool + { + // Enforce workspace boundary + return $user->workspace_id === $post->workspace_id; + } + + public function update(User $user, Post $post): bool + { + // Workspace check + additional authorization + return $user->workspace_id === $post->workspace_id + && ($user->id === $post->author_id || $user->hasRole('editor')); + } +} +``` + +## Role-Based Authorization + +### Defining Roles + +```php +use Mod\Tenant\Models\User; + +// Assign role +$user->assignRole('editor'); + +// Check role +if ($user->hasRole('admin')) { + // User is admin +} + +// Check any role +if ($user->hasAnyRole(['editor', 'author'])) { + // User has at least one role +} + +// Check all roles +if ($user->hasAllRoles(['editor', 'reviewer'])) { + // User has both roles +} +``` + +### Policy with Roles + +```php +class PostPolicy +{ + public function update(User $user, Post $post): bool + { + return $user->hasRole('admin') + || ($user->hasRole('editor') && $user->workspace_id === $post->workspace_id) + || ($user->hasRole('author') && $user->id === $post->author_id); + } + + public function delete(User $user, Post $post): bool + { + // Only admins can delete + return $user->hasRole('admin'); + } +} +``` + +## Permission-Based Authorization + +### Defining Permissions + +```php +// Grant permission +$user->givePermission('posts.create'); +$user->givePermission('posts.publish'); + +// Check permission +if ($user->hasPermission('posts.publish')) { + // User can publish +} + +// Check multiple permissions +if ($user->hasAllPermissions(['posts.create', 'posts.publish'])) { + // User has all permissions +} + +// Check any permission +if ($user->hasAnyPermission(['posts.edit', 'posts.delete'])) { + // User has at least one permission +} +``` + +### Policy with Permissions + +```php +class PostPolicy +{ + public function create(User $user): bool + { + return $user->hasPermission('posts.create'); + } + + public function publish(User $user, Post $post): bool + { + return $user->hasPermission('posts.publish') + && $post->status === 'draft'; + } +} +``` + +## Conditional Rendering + +### Blade Directives + +```blade +@can('create', App\Models\Post::class) + Create Post +@endcan + +@cannot('delete', $post) +

You cannot delete this post

+@endcannot + +@canany(['edit', 'update'], $post) + Edit +@endcanany +``` + +### Component Visibility + +```blade + + Publish + + +{{-- Automatically hidden if user cannot publish --}} +``` + +### Form Field Disabling + +```blade + + +{{-- Disabled if user cannot edit slug --}} +``` + +## Authorization Middleware + +### Global Middleware + +```php +// app/Http/Kernel.php +protected $middlewareGroups = [ + 'web' => [ + // ... + \Core\Bouncer\Gate\ActionGateMiddleware::class, + ], +]; +``` + +### Route Middleware + +```php +// Require authentication +Route::middleware(['auth'])->group(function () { + Route::get('/admin', [AdminController::class, 'index']); +}); + +// Require specific ability +Route::middleware(['can:create,App\Models\Post'])->group(function () { + Route::get('/posts/create', [PostController::class, 'create']); +}); +``` + +## Testing Authorization + +```php +use Tests\TestCase; +use Mod\Blog\Models\Post; +use Mod\Tenant\Models\User; + +class AuthorizationTest extends TestCase +{ + public function test_user_can_view_own_posts(): void + { + $user = User::factory()->create(); + $post = Post::factory()->create(['author_id' => $user->id]); + + $this->assertTrue($user->can('view', $post)); + } + + public function test_user_cannot_delete_others_posts(): void + { + $user = User::factory()->create(); + $post = Post::factory()->create(); // Different author + + $this->assertFalse($user->can('delete', $post)); + } + + public function test_admin_can_delete_any_post(): void + { + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + $post = Post::factory()->create(); + + $this->assertTrue($admin->can('delete', $post)); + } + + public function test_workspace_isolation(): void + { + $user1 = User::factory()->create(['workspace_id' => 1]); + $user2 = User::factory()->create(['workspace_id' => 2]); + + $post = Post::factory()->create(['workspace_id' => 1]); + + $this->assertTrue($user1->can('view', $post)); + $this->assertFalse($user2->can('view', $post)); + } +} +``` + +## Best Practices + +### 1. Always Check Workspace Boundaries + +```php +// ✅ Good - workspace check +public function view(User $user, Post $post): bool +{ + return $user->workspace_id === $post->workspace_id; +} + +// ❌ Bad - no workspace check +public function view(User $user, Post $post): bool +{ + return true; // Data leak! +} +``` + +### 2. Use Policies Over Gates + +```php +// ✅ Good - policy +$this->authorize('update', $post); + +// ❌ Bad - manual check +if (auth()->id() !== $post->author_id) { + abort(403); +} +``` + +### 3. Authorize Early + +```php +// ✅ Good - authorize in mount +public function mount(Post $post) +{ + $this->authorize('update', $post); + $this->post = $post; +} + +// ❌ Bad - authorize in action +public function save() +{ + $this->authorize('update', $this->post); // Too late! + $this->post->save(); +} +``` + +### 4. Use Authorization Props + +```blade +{{-- ✅ Good - declarative authorization --}} + + Delete + + +{{-- ❌ Bad - manual check --}} +@if(auth()->user()->can('delete', $post)) + Delete +@endif +``` + +## Learn More + +- [Form Components →](/packages/admin/forms) +- [Admin Menus →](/packages/admin/menus) +- [Multi-Tenancy →](/packages/core/tenancy) diff --git a/packages/admin/components-reference.md b/packages/admin/components-reference.md new file mode 100644 index 0000000..1daaeb5 --- /dev/null +++ b/packages/admin/components-reference.md @@ -0,0 +1,784 @@ +# Components Reference + +Complete API reference for all form components in the Admin package, including prop documentation, validation rules, authorization integration, and accessibility notes. + +## Overview + +All form components in Core PHP: +- Wrap Flux UI components with additional features +- Support authorization via `canGate` and `canResource` props +- Include ARIA accessibility attributes +- Work seamlessly with Livewire +- Follow consistent naming conventions + +## Input + +Text input with various types and authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier for the input | +| `label` | string | `null` | Label text displayed above input | +| `helper` | string | `null` | Helper text displayed below input | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource to check ability against | +| `instantSave` | bool | `false` | Use `wire:model.live.debounce.500ms` | +| `type` | string | `'text'` | Input type (text, email, password, number, etc.) | +| `placeholder` | string | `null` | Placeholder text | +| `disabled` | bool | `false` | Disable the input | +| `readonly` | bool | `false` | Make input read-only | +| `required` | bool | `false` | Mark as required | +| `min` | number | `null` | Minimum value (for number inputs) | +| `max` | number | `null` | Maximum value (for number inputs) | +| `maxlength` | number | `null` | Maximum character length | + +### Authorization Example + +```blade +{{-- Input disabled if user cannot update the post --}} + +``` + +### Type Variants + +```blade +{{-- Text input --}} + + +{{-- Email input --}} + + +{{-- Password input --}} + + +{{-- Number input --}} + + +{{-- Date input --}} + + +{{-- URL input --}} + +``` + +### Instant Save Mode + +```blade +{{-- Saves with 500ms debounce --}} + +``` + +### Accessibility + +The component automatically: +- Associates label with input via `id` +- Links error messages with `aria-describedby` +- Sets `aria-invalid="true"` when validation fails +- Includes helper text in accessible description + +--- + +## Textarea + +Multi-line text input with authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text | +| `helper` | string | `null` | Helper text | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live debounced binding | +| `rows` | number | `3` | Number of visible rows | +| `placeholder` | string | `null` | Placeholder text | +| `disabled` | bool | `false` | Disable the textarea | +| `maxlength` | number | `null` | Maximum character length | + +### Authorization Example + +```blade + +``` + +### With Character Limit + +```blade + +``` + +--- + +## Select + +Dropdown select with authorization support. + +### Basic Usage + +```blade + + Draft + Published + Archived + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text | +| `helper` | string | `null` | Helper text | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live binding | +| `placeholder` | string | `null` | Placeholder option text | +| `disabled` | bool | `false` | Disable the select | +| `multiple` | bool | `false` | Allow multiple selections | + +### Authorization Example + +```blade + + @foreach($categories as $category) + + {{ $category->name }} + + @endforeach + +``` + +### With Placeholder + +```blade + + United States + United Kingdom + Canada + +``` + +### Multiple Selection + +```blade + + @foreach($tags as $tag) + + {{ $tag->name }} + + @endforeach + +``` + +--- + +## Checkbox + +Single checkbox with authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text (displayed inline) | +| `helper` | string | `null` | Helper text below checkbox | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live binding | +| `disabled` | bool | `false` | Disable the checkbox | +| `value` | string | `null` | Checkbox value (for arrays) | + +### Authorization Example + +```blade + +``` + +### With Helper Text + +```blade + +``` + +### Checkbox Group + +```blade +
+ Notifications + + + + + + +
+``` + +--- + +## Toggle + +Switch-style toggle with authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text (displayed to the left) | +| `helper` | string | `null` | Helper text below toggle | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live binding | +| `disabled` | bool | `false` | Disable the toggle | + +### Authorization Example + +```blade + +``` + +### Instant Save + +```blade +{{-- Toggle that saves immediately --}} + +``` + +### With Helper + +```blade + +``` + +--- + +## Button + +Action button with variants and authorization support. + +### Basic Usage + +```blade + + Save Changes + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `variant` | string | `'primary'` | Button style variant | +| `type` | string | `'submit'` | Button type (submit, button, reset) | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `disabled` | bool | `false` | Disable the button | +| `loading` | bool | `false` | Show loading state | + +### Variants + +```blade +{{-- Primary (default) --}} +Primary + +{{-- Secondary --}} +Secondary + +{{-- Danger --}} +Delete + +{{-- Ghost --}} +Cancel +``` + +### Authorization Example + +```blade +{{-- Button disabled if user cannot delete --}} + + Delete Post + +``` + +### With Loading State + +```blade + + Save + Saving... + +``` + +### As Link + +```blade + + Cancel + +``` + +--- + +## Authorization Props Reference + +All form components support authorization through consistent props. + +### How Authorization Works + +When `canGate` and `canResource` are provided, the component checks if the authenticated user can perform the specified ability on the resource: + +```php +// Equivalent PHP check +auth()->user()?->can($canGate, $canResource) +``` + +If the check fails, the component is **disabled** (not hidden). + +### Props + +| Prop | Type | Description | +|------|------|-------------| +| `canGate` | string | The ability/gate name to check (e.g., `'update'`, `'delete'`, `'publish'`) | +| `canResource` | mixed | The resource to check the ability against (usually a model instance) | + +### Examples + +**Basic Policy Check:** +```blade + +``` + +**Multiple Components with Same Auth:** +```blade +@php $canEdit = auth()->user()?->can('update', $post); @endphp + + + +Save +``` + +**Combining with Blade Directives:** +```blade +@can('update', $post) + + Save +@else +

You do not have permission to edit this post.

+@endcan +``` + +### Defining Policies + +```php +id === $post->author_id + || $user->hasRole('editor'); + } + + public function delete(User $user, Post $post): bool + { + return $user->hasRole('admin'); + } + + public function publish(User $user, Post $post): bool + { + return $user->hasPermission('posts.publish') + && $post->status === 'draft'; + } +} +``` + +--- + +## Accessibility Notes + +### ARIA Attributes + +All components automatically include appropriate ARIA attributes: + +| Attribute | Usage | +|-----------|-------| +| `aria-labelledby` | Links to label element | +| `aria-describedby` | Links to helper text and error messages | +| `aria-invalid` | Set to `true` when validation fails | +| `aria-required` | Set when field is required | +| `aria-disabled` | Set when field is disabled | + +### Label Association + +Labels are automatically associated with inputs via the `id` prop: + +```blade + + +{{-- Renders as: --}} + + Email Address + + +``` + +### Error Announcements + +Validation errors are linked to inputs and announced to screen readers: + +```blade +{{-- Component renders error with aria-describedby link --}} + + +{{-- Screen readers announce: "Email is required" --}} +``` + +### Focus Management + +- Tab order follows visual order +- Focus states are clearly visible +- Error focus moves to first invalid field + +### Keyboard Support + +| Component | Keyboard Support | +|-----------|------------------| +| Input | Standard text input | +| Textarea | Standard multiline | +| Select | Arrow keys, Enter, Escape | +| Checkbox | Space to toggle | +| Toggle | Space to toggle, Arrow keys | +| Button | Enter/Space to activate | + +--- + +## Validation Integration + +### Server-Side Validation + +Components automatically display Laravel validation errors: + +```php +// In Livewire component +protected array $rules = [ + 'title' => 'required|max:255', + 'content' => 'required', + 'status' => 'required|in:draft,published', +]; + +public function save(): void +{ + $this->validate(); + // Errors automatically shown on components +} +``` + +### Real-Time Validation + +```php +public function updated($propertyName): void +{ + $this->validateOnly($propertyName); +} +``` + +```blade +{{-- Shows validation error as user types --}} + +``` + +### Custom Error Messages + +```php +protected array $messages = [ + 'title.required' => 'Please enter a post title.', + 'content.required' => 'Post content cannot be empty.', +]; +``` + +--- + +## Complete Form Example + +```blade +
+ {{-- Title --}} + + + {{-- Slug with instant save --}} + + + {{-- Content --}} + + + {{-- Category --}} + + @foreach($categories as $category) + + {{ $category->name }} + + @endforeach + + + {{-- Status --}} + + Draft + Published + Archived + + + {{-- Featured toggle --}} + + + {{-- Newsletter checkbox --}} + + + {{-- Actions --}} +
+ + Save Changes + + + + Cancel + + + @can('delete', $post) + + Delete + + @endcan +
+ +``` + +## Learn More + +- [Form Components Guide](/packages/admin/forms) +- [Authorization](/packages/admin/authorization) +- [Creating Admin Panels](/packages/admin/creating-admin-panels) +- [Livewire Modals](/packages/admin/modals) diff --git a/packages/admin/components.md b/packages/admin/components.md new file mode 100644 index 0000000..7de1efe --- /dev/null +++ b/packages/admin/components.md @@ -0,0 +1,623 @@ +# Admin Components + +Reusable UI components for building admin panels: cards, tables, stat widgets, and more. + +## Cards + +### Basic Card + +```blade + + +

Recent Posts

+
+ +

Card content goes here...

+ + + View All + +
+``` + +### Card with Actions + +```blade + + +

Post Statistics

+ + + Refresh + + +
+ +
+ {{-- Statistics content --}} +
+
+``` + +### Card Grid + +Display cards in responsive grid: + +```blade + + +

Total Posts

+

1,234

+
+ + +

Published

+

856

+
+ + +

Drafts

+

378

+
+
+``` + +## Stat Widgets + +### Simple Stat + +```blade + +``` + +### Stat with Trend + +```blade + +``` + +**Trend Indicators:** +- Positive number: green up arrow +- Negative number: red down arrow +- Zero: neutral indicator + +### Stat with Chart + +```blade + +``` + +**Sparkline Data:** + +```php +public function getSparklineData() +{ + return [ + 120, 145, 132, 158, 170, 165, 180, 195, 185, 200 + ]; +} +``` + +### Stat Grid + +```blade +
+ + + + + + + +
+``` + +## Tables + +### Basic Table + +```blade + + + Title + Author + Status + Actions + + + @foreach($posts as $post) + + {{ $post->title }} + {{ $post->author->name }} + + + {{ $post->status }} + + + + + Edit + + + + @endforeach + +``` + +### Sortable Table + +```blade + + + + Title + + + Created + + + + {{-- Table rows --}} + +``` + +**Livewire Component:** + +```php +class PostsTable extends Component +{ + public $sortField = 'created_at'; + public $sortDirection = 'desc'; + + public function sortBy($field) + { + if ($this->sortField === $field) { + $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + $this->sortField = $field; + $this->sortDirection = 'asc'; + } + } + + public function render() + { + $posts = Post::orderBy($this->sortField, $this->sortDirection) + ->paginate(20); + + return view('livewire.posts-table', compact('posts')); + } +} +``` + +### Table with Bulk Actions + +```blade + + + + + + Title + Actions + + + @foreach($posts as $post) + + + + + {{ $post->title }} + ... + + @endforeach + + +@if(count($selected) > 0) +
+

{{ count($selected) }} selected

+ Publish + Delete +
+@endif +``` + +## Badges + +### Status Badges + +```blade +Published +Draft +Archived +Scheduled +Pending +``` + +### Badge with Dot + +```blade + + Active + +``` + +### Badge with Icon + +```blade + + + ... + + Verified + +``` + +### Removable Badge + +```blade + + {{ $tag->name }} + +``` + +## Alerts + +### Basic Alert + +```blade + + Post published successfully! + + + + Failed to save post. Please try again. + + + + This post has not been reviewed yet. + + + + You have 3 draft posts. + +``` + +### Dismissible Alert + +```blade + + Post published successfully! + +``` + +### Alert with Title + +```blade + + + Pending Review + + This post requires approval before it can be published. + +``` + +## Empty States + +### Basic Empty State + +```blade + + + ... + + + + No posts yet + + + + Get started by creating your first blog post. + + + + + Create Post + + + +``` + +### Search Empty State + +```blade +@if($posts->isEmpty() && $search) + + + No results found + + + + No posts match your search for "{{ $search }}". + + + + + Clear Search + + + +@endif +``` + +## Loading States + +### Skeleton Loaders + +```blade + + + +``` + +### Loading Spinner + +```blade +
+ +
+ +
+ {{-- Content --}} +
+``` + +### Loading Overlay + +```blade +
+ {{-- Content becomes translucent while loading --}} +
+ +
+ +
+``` + +## Pagination + +```blade + + {{-- Table content --}} + + +{{ $posts->links('admin::pagination') }} +``` + +**Custom Pagination:** + +```blade + +``` + +## Modals (See Modals Documentation) + +See [Livewire Modals →](/packages/admin/modals) for full modal documentation. + +## Dropdowns + +### Basic Dropdown + +```blade + + + + Actions + + + + + Edit + + + + Duplicate + + + + + + Delete + + +``` + +### Dropdown with Icons + +```blade + + + + + + + + ... + + Edit Post + + + + + ... + + View + + +``` + +## Tabs + +```blade + + + {{-- General settings --}} + + + + {{-- SEO settings --}} + + + + {{-- Advanced settings --}} + + +``` + +## Best Practices + +### 1. Use Semantic Components + +```blade +{{-- ✅ Good - semantic component --}} + + +{{-- ❌ Bad - manual markup --}} +
+

Revenue

+ {{ $revenue }} +
+``` + +### 2. Consistent Colors + +```blade +{{-- ✅ Good - use color props --}} +Active +Inactive + +{{-- ❌ Bad - custom classes --}} +Active +``` + +### 3. Loading States + +```blade +{{-- ✅ Good - show loading state --}} +
+ +
+ +{{-- ❌ Bad - no feedback --}} + +``` + +### 4. Empty States + +```blade +{{-- ✅ Good - helpful empty state --}} +@if($posts->isEmpty()) + + + + Create First Post + + + +@endif + +{{-- ❌ Bad - no guidance --}} +@if($posts->isEmpty()) +

No posts

+@endif +``` + +## Testing Components + +```php +use Tests\TestCase; + +class ComponentsTest extends TestCase +{ + public function test_stat_widget_renders(): void + { + $view = $this->blade(''); + + $view->assertSee('Users'); + $view->assertSee('100'); + } + + public function test_badge_renders_with_color(): void + { + $view = $this->blade('Active'); + + $view->assertSee('Active'); + $view->assertSeeInOrder(['class', 'green']); + } +} +``` + +## Learn More + +- [Form Components →](/packages/admin/forms) +- [Livewire Modals →](/packages/admin/modals) +- [Authorization →](/packages/admin/authorization) diff --git a/packages/admin/creating-admin-panels.md b/packages/admin/creating-admin-panels.md new file mode 100644 index 0000000..9fcf1d4 --- /dev/null +++ b/packages/admin/creating-admin-panels.md @@ -0,0 +1,931 @@ +# Creating Admin Panels + +This guide covers the complete process of creating admin panels in the Core PHP Framework, including menu registration, modal creation, and authorization integration. + +## Overview + +Admin panels in Core PHP use: +- **AdminMenuProvider** - Interface for menu registration +- **Livewire Modals** - Full-page components for admin interfaces +- **Authorization Props** - Built-in permission checking on components +- **HLCRF Layouts** - Composable layout system + +## Menu Registration with AdminMenuProvider + +### Implementing AdminMenuProvider + +The `AdminMenuProvider` interface allows modules to contribute navigation items to the admin sidebar. + +```php + 'onAdminPanel', + ]; + + public function onAdminPanel(AdminPanelBooting $event): void + { + // Register views and routes + $event->views('blog', __DIR__.'/View/Blade'); + $event->routes(fn () => require __DIR__.'/Routes/admin.php'); + + // Register menu provider + app(AdminMenuRegistry::class)->register($this); + } + + public function adminMenuItems(): array + { + return [ + // Dashboard item in standalone group + [ + 'group' => 'dashboard', + 'priority' => self::PRIORITY_HIGH, + 'item' => fn () => [ + 'label' => 'Blog Dashboard', + 'icon' => 'newspaper', + 'href' => route('admin.blog.dashboard'), + 'active' => request()->routeIs('admin.blog.dashboard'), + ], + ], + + // Service item with entitlement + [ + 'group' => 'services', + 'priority' => self::PRIORITY_NORMAL, + 'entitlement' => 'core.srv.blog', + 'item' => fn () => [ + 'label' => 'Blog', + 'icon' => 'newspaper', + 'href' => route('admin.blog.posts'), + 'active' => request()->routeIs('admin.blog.*'), + 'color' => 'blue', + 'badge' => Post::draft()->count() ?: null, + 'children' => [ + ['label' => 'All Posts', 'href' => route('admin.blog.posts'), 'icon' => 'document-text'], + ['label' => 'Categories', 'href' => route('admin.blog.categories'), 'icon' => 'folder'], + ['label' => 'Tags', 'href' => route('admin.blog.tags'), 'icon' => 'tag'], + ], + ], + ], + + // Admin-only item + [ + 'group' => 'admin', + 'priority' => self::PRIORITY_LOW, + 'admin' => true, + 'item' => fn () => [ + 'label' => 'Blog Settings', + 'icon' => 'gear', + 'href' => route('admin.blog.settings'), + 'active' => request()->routeIs('admin.blog.settings'), + ], + ], + ]; + } +} +``` + +### Menu Item Structure + +Each item in `adminMenuItems()` follows this structure: + +| Property | Type | Description | +|----------|------|-------------| +| `group` | string | Menu group: `dashboard`, `workspaces`, `services`, `settings`, `admin` | +| `priority` | int | Order within group (use `PRIORITY_*` constants) | +| `entitlement` | string | Optional workspace feature code for access | +| `permissions` | array | Optional user permission keys required | +| `admin` | bool | Requires Hades/admin user | +| `item` | Closure | Lazy-evaluated item data | + +### Priority Constants + +```php +use Core\Front\Admin\Contracts\AdminMenuProvider; + +// Available priority constants +AdminMenuProvider::PRIORITY_FIRST // 0-9: System items +AdminMenuProvider::PRIORITY_HIGH // 10-19: Primary navigation +AdminMenuProvider::PRIORITY_ABOVE_NORMAL // 20-39: Important items +AdminMenuProvider::PRIORITY_NORMAL // 40-60: Standard items (default) +AdminMenuProvider::PRIORITY_BELOW_NORMAL // 61-79: Less important +AdminMenuProvider::PRIORITY_LOW // 80-89: Rarely used +AdminMenuProvider::PRIORITY_LAST // 90-99: End items +``` + +### Menu Groups + +| Group | Description | Rendering | +|-------|-------------|-----------| +| `dashboard` | Primary entry points | Standalone items | +| `workspaces` | Workspace management | Grouped dropdown | +| `services` | Application services | Standalone items | +| `settings` | User/account settings | Grouped dropdown | +| `admin` | Platform administration | Grouped dropdown (Hades only) | + +### Using MenuItemBuilder + +For complex menus, use the fluent `MenuItemBuilder`: + +```php +use Core\Front\Admin\Support\MenuItemBuilder; + +public function adminMenuItems(): array +{ + return [ + MenuItemBuilder::make('Commerce') + ->icon('shopping-cart') + ->route('admin.commerce.dashboard') + ->inServices() + ->priority(self::PRIORITY_NORMAL) + ->entitlement('core.srv.commerce') + ->color('green') + ->badge('New', 'green') + ->activeOnRoute('admin.commerce.*') + ->children([ + MenuItemBuilder::child('Products', route('admin.commerce.products')) + ->icon('cube'), + MenuItemBuilder::child('Orders', route('admin.commerce.orders')) + ->icon('receipt') + ->badge(fn () => Order::pending()->count()), + ['separator' => true], + MenuItemBuilder::child('Settings', route('admin.commerce.settings')) + ->icon('gear'), + ]) + ->build(), + + MenuItemBuilder::make('Analytics') + ->icon('chart-line') + ->route('admin.analytics.dashboard') + ->inServices() + ->entitlement('core.srv.analytics') + ->adminOnly() // Requires admin user + ->build(), + ]; +} +``` + +### Permission Checking + +The `HasMenuPermissions` trait provides default permission handling: + +```php +use Core\Front\Admin\Concerns\HasMenuPermissions; + +class BlogMenuProvider implements AdminMenuProvider +{ + use HasMenuPermissions; + + // Override for custom global permissions + public function menuPermissions(): array + { + return ['blog.view']; + } + + // Override for custom permission logic + public function canViewMenu(?object $user, ?object $workspace): bool + { + if ($user === null) { + return false; + } + + // Custom logic + return $user->hasRole('editor') || $user->isHades(); + } +} +``` + +## Creating Livewire Modals + +Livewire modals are full-page components that provide seamless admin interfaces. + +### Basic Modal Structure + +```php + 'required|string|max:255', + 'content' => 'required|string', + 'status' => 'required|in:draft,published,archived', + ]; + + public function mount(?Post $post = null): void + { + $this->post = $post; + + if ($post) { + $this->title = $post->title; + $this->content = $post->content; + $this->status = $post->status; + } + } + + public function save(): void + { + $validated = $this->validate(); + + if ($this->post) { + $this->post->update($validated); + $message = 'Post updated successfully.'; + } else { + Post::create($validated); + $message = 'Post created successfully.'; + } + + session()->flash('success', $message); + $this->redirect(route('admin.blog.posts')); + } + + public function render(): View + { + return view('blog::admin.post-editor'); + } +} +``` + +### Modal View with HLCRF + +```blade +{{-- resources/views/admin/post-editor.blade.php --}} + + +
+

+ {{ $post ? 'Edit Post' : 'Create Post' }} +

+ + + + +
+
+ + +
+ + + + + + Draft + Published + Archived + + +
+ + {{ $post ? 'Update' : 'Create' }} Post + + + + Cancel + +
+ +
+ + +
+

Publishing Tips

+
    +
  • Use descriptive titles
  • +
  • Save as draft first
  • +
  • Preview before publishing
  • +
+
+
+
+``` + +### Modal with Authorization + +```php +authorize('update', $post); + + $this->post = $post; + // ... load data + } + + public function save(): void + { + // Re-authorize on save + $this->authorize('update', $this->post); + + $this->post->update([...]); + } + + public function publish(): void + { + // Different authorization for publish + $this->authorize('publish', $this->post); + + $this->post->update(['status' => 'published']); + } + + public function delete(): void + { + $this->authorize('delete', $this->post); + + $this->post->delete(); + $this->redirect(route('admin.blog.posts')); + } +} +``` + +### Modal with File Uploads + +```php + 'required|image|max:5120', // 5MB max + 'altText' => 'required|string|max:255', + ]; + + public function upload(): void + { + $this->validate(); + + $path = $this->image->store('media', 'public'); + + Media::create([ + 'path' => $path, + 'alt_text' => $this->altText, + 'mime_type' => $this->image->getMimeType(), + ]); + + $this->dispatch('media-uploaded'); + $this->reset(['image', 'altText']); + } +} +``` + +## Authorization Integration + +### Form Component Authorization Props + +All form components support authorization via `canGate` and `canResource` props: + +```blade +{{-- Button disabled if user cannot update post --}} + + Save Changes + + +{{-- Input disabled if user cannot update --}} + + +{{-- Textarea with authorization --}} + + +{{-- Select with authorization --}} + + Draft + Published + + +{{-- Toggle with authorization --}} + +``` + +### Blade Conditional Rendering + +```blade +{{-- Show only if user can create --}} +@can('create', App\Models\Post::class) + New Post +@endcan + +{{-- Show if user can edit OR delete --}} +@canany(['update', 'delete'], $post) +
+ @can('update', $post) + Edit + @endcan + + @can('delete', $post) + + @endcan +
+@endcanany + +{{-- Show message if cannot edit --}} +@cannot('update', $post) +

You cannot edit this post.

+@endcannot +``` + +### Creating Policies + +```php +isHades()) { + return true; + } + + // Enforce workspace isolation + if ($model instanceof Post && $user->workspace_id !== $model->workspace_id) { + return false; + } + + return null; // Continue to specific method + } + + public function viewAny(User $user): bool + { + return $user->hasPermission('posts.view'); + } + + public function view(User $user, Post $post): bool + { + return $user->hasPermission('posts.view'); + } + + public function create(User $user): bool + { + return $user->hasPermission('posts.create'); + } + + public function update(User $user, Post $post): bool + { + return $user->hasPermission('posts.edit') + || $user->id === $post->author_id; + } + + public function delete(User $user, Post $post): bool + { + return $user->hasRole('admin') + || ($user->hasPermission('posts.delete') && $user->id === $post->author_id); + } + + public function publish(User $user, Post $post): bool + { + return $user->hasPermission('posts.publish') + && $post->status !== 'archived'; + } +} +``` + +## Complete Module Example + +Here is a complete example of an admin module with menus, modals, and authorization. + +### Directory Structure + +``` +Mod/Blog/ +├── Boot.php +├── Models/ +│ └── Post.php +├── Policies/ +│ └── PostPolicy.php +├── View/ +│ ├── Blade/ +│ │ └── admin/ +│ │ ├── posts-list.blade.php +│ │ └── post-editor.blade.php +│ └── Modal/ +│ └── Admin/ +│ ├── PostsList.php +│ └── PostEditor.php +└── Routes/ + └── admin.php +``` + +### Boot.php + +```php + 'onAdminPanel', + ]; + + public function boot(): void + { + // Register policy + Gate::policy(Post::class, PostPolicy::class); + } + + public function onAdminPanel(AdminPanelBooting $event): void + { + // Views + $event->views('blog', __DIR__.'/View/Blade'); + + // Routes + $event->routes(fn () => require __DIR__.'/Routes/admin.php'); + + // Menu + app(AdminMenuRegistry::class)->register($this); + + // Livewire components + $event->livewire('blog.admin.posts-list', View\Modal\Admin\PostsList::class); + $event->livewire('blog.admin.post-editor', View\Modal\Admin\PostEditor::class); + } + + public function adminMenuItems(): array + { + return [ + [ + 'group' => 'services', + 'priority' => self::PRIORITY_NORMAL, + 'entitlement' => 'core.srv.blog', + 'permissions' => ['posts.view'], + 'item' => fn () => [ + 'label' => 'Blog', + 'icon' => 'newspaper', + 'href' => route('admin.blog.posts'), + 'active' => request()->routeIs('admin.blog.*'), + 'color' => 'blue', + 'badge' => $this->getDraftCount(), + 'children' => [ + [ + 'label' => 'All Posts', + 'href' => route('admin.blog.posts'), + 'icon' => 'document-text', + 'active' => request()->routeIs('admin.blog.posts'), + ], + [ + 'label' => 'Create Post', + 'href' => route('admin.blog.posts.create'), + 'icon' => 'plus', + 'active' => request()->routeIs('admin.blog.posts.create'), + ], + ], + ], + ], + ]; + } + + protected function getDraftCount(): ?int + { + $count = Post::draft()->count(); + return $count > 0 ? $count : null; + } +} +``` + +### Routes/admin.php + +```php +prefix('admin/blog') + ->name('admin.blog.') + ->group(function () { + Route::get('/posts', PostsList::class)->name('posts'); + Route::get('/posts/create', PostEditor::class)->name('posts.create'); + Route::get('/posts/{post}/edit', PostEditor::class)->name('posts.edit'); + }); +``` + +### View/Modal/Admin/PostsList.php + +```php +resetPage(); + } + + #[Computed] + public function posts() + { + return Post::query() + ->when($this->search, fn ($q) => $q->where('title', 'like', "%{$this->search}%")) + ->when($this->status, fn ($q) => $q->where('status', $this->status)) + ->orderByDesc('created_at') + ->paginate(20); + } + + public function delete(int $postId): void + { + $post = Post::findOrFail($postId); + + $this->authorize('delete', $post); + + $post->delete(); + + session()->flash('success', 'Post deleted.'); + } + + public function render(): View + { + return view('blog::admin.posts-list'); + } +} +``` + +### View/Blade/admin/posts-list.blade.php + +```blade + + +
+

Blog Posts

+ + @can('create', \Mod\Blog\Models\Post::class) + + + New Post + + @endcan +
+
+ + + {{-- Filters --}} +
+ + + + All Statuses + Draft + Published + +
+ + {{-- Posts table --}} +
+ + + + + + + + + + + @forelse($this->posts as $post) + + + + + + + @empty + + + + @endforelse + +
TitleStatusDateActions
{{ $post->title }} + + {{ ucfirst($post->status) }} + + {{ $post->created_at->format('M d, Y') }} + @can('update', $post) + + Edit + + @endcan + + @can('delete', $post) + + @endcan +
+ No posts found. +
+
+ + {{-- Pagination --}} +
+ {{ $this->posts->links() }} +
+
+
+``` + +## Best Practices + +### 1. Always Use Entitlements for Services + +```php +// Menu item requires workspace entitlement +[ + 'group' => 'services', + 'entitlement' => 'core.srv.blog', // Required + 'item' => fn () => [...], +] +``` + +### 2. Authorize Early in Modals + +```php +public function mount(Post $post): void +{ + $this->authorize('update', $post); // Fail fast + $this->post = $post; +} +``` + +### 3. Use Form Component Authorization Props + +```blade +{{-- Declarative authorization --}} + + Save + + +{{-- Not manual checks --}} +@if(auth()->user()->can('update', $post)) + +@endif +``` + +### 4. Keep Menu Items Lazy + +```php +// Item closure is only evaluated when rendered +'item' => fn () => [ + 'label' => 'Posts', + 'badge' => Post::draft()->count(), // Computed at render time +], +``` + +### 5. Use HLCRF for Consistent Layouts + +```blade +{{-- Always use HLCRF for admin views --}} + + ... + ... + +``` + +## Learn More + +- [Admin Menus](/packages/admin/menus) +- [Livewire Modals](/packages/admin/modals) +- [Form Components](/packages/admin/forms) +- [Authorization](/packages/admin/authorization) +- [HLCRF Layouts](/packages/admin/hlcrf-deep-dive) diff --git a/packages/admin/forms.md b/packages/admin/forms.md new file mode 100644 index 0000000..09a53f4 --- /dev/null +++ b/packages/admin/forms.md @@ -0,0 +1,627 @@ +# Form Components + +The Admin package provides a comprehensive set of form components with consistent styling, validation, and authorization support. + +## Overview + +All form components: +- Follow consistent design patterns +- Support Laravel validation +- Include accessibility attributes (ARIA) +- Work with Livewire +- Support authorization props + +## Form Group + +Wrapper component for labels, inputs, and validation errors: + +```blade + + + +``` + +**Props:** +- `label` (string) - Field label +- `name` (string) - Field name for validation errors +- `required` (bool) - Show required indicator +- `help` (string) - Help text below field +- `error` (string) - Manual error message + +## Input + +Text input with various types: + +```blade +{{-- Text input --}} + + +{{-- Email input --}} + + +{{-- Password input --}} + + +{{-- Number input --}} + + +{{-- Date input --}} + +``` + +**Props:** +- `name` (string, required) - Input name +- `label` (string) - Label text +- `type` (string) - Input type (text, email, password, number, date, etc.) +- `value` (string) - Input value +- `placeholder` (string) - Placeholder text +- `required` (bool) - Required field +- `disabled` (bool) - Disabled state +- `readonly` (bool) - Read-only state +- `min` / `max` (number) - Min/max for number inputs + +## Textarea + +Multi-line text input: + +```blade + + +{{-- With character counter --}} + +``` + +**Props:** +- `name` (string, required) - Textarea name +- `label` (string) - Label text +- `rows` (number) - Number of rows (default: 5) +- `cols` (number) - Number of columns +- `placeholder` (string) - Placeholder text +- `maxlength` (number) - Maximum character length +- `show-counter` (bool) - Show character counter +- `required` (bool) - Required field + +## Select + +Dropdown select: + +```blade +{{-- Simple select --}} + + +{{-- With placeholder --}} + + +{{-- Multiple select --}} + + +{{-- Grouped options --}} + +``` + +**Props:** +- `name` (string, required) - Select name +- `label` (string) - Label text +- `options` (array, required) - Options array +- `value` (mixed) - Selected value(s) +- `placeholder` (string) - Placeholder option +- `multiple` (bool) - Allow multiple selections +- `required` (bool) - Required field +- `disabled` (bool) - Disabled state + +## Checkbox + +Single checkbox: + +```blade + + +{{-- With description --}} + + +{{-- Group of checkboxes --}} +
+ Permissions + + + + +
+``` + +**Props:** +- `name` (string, required) - Checkbox name +- `label` (string) - Label text +- `value` (string) - Checkbox value +- `checked` (bool) - Checked state +- `description` (string) - Help text below checkbox +- `disabled` (bool) - Disabled state + +## Toggle + +Switch-style toggle: + +```blade + + +{{-- With colors --}} + +``` + +**Props:** +- `name` (string, required) - Toggle name +- `label` (string) - Label text +- `checked` (bool) - Checked state +- `description` (string) - Help text +- `color` (string) - Toggle color (green, blue, red) +- `disabled` (bool) - Disabled state + +## Button + +Action buttons with variants: + +```blade +{{-- Primary button --}} + + Save Changes + + +{{-- Secondary button --}} + + Cancel + + +{{-- Danger button --}} + + Delete Post + + +{{-- Ghost button --}} + + Reset + + +{{-- Icon button --}} + + + + +{{-- Loading state --}} + + Save + Saving... + +``` + +**Props:** +- `type` (string) - Button type (button, submit, reset) +- `variant` (string) - Style variant (primary, secondary, danger, ghost, icon) +- `href` (string) - Link URL (renders as ``) +- `loading` (bool) - Show loading state +- `disabled` (bool) - Disabled state +- `size` (string) - Size (sm, md, lg) + +## Authorization Props + +All form components support authorization attributes: + +```blade + + Create Post + + + + + + Delete + +``` + +**Authorization Props:** +- `can` (string) - Gate/policy check +- `can-arguments` (array) - Arguments for gate check +- `cannot` (string) - Inverse of `can` +- `hidden-unless` (string) - Hide element unless authorized +- `readonly-unless` (string) - Make readonly unless authorized +- `disabled-unless` (string) - Disable unless authorized + +[Learn more about Authorization →](/packages/admin/authorization) + +## Livewire Integration + +All components work seamlessly with Livewire: + +```blade +
+ + + + + + + + Save Post + + +``` + +### Real-Time Validation + +```blade + + +@error('slug') +

{{ $message }}

+@enderror +``` + +### Debounced Input + +```blade + +``` + +## Validation + +Components automatically show validation errors: + +```blade +{{-- Controller validation --}} +$request->validate([ + 'title' => 'required|max:255', + 'content' => 'required', + 'status' => 'required|in:draft,published', +]); + +{{-- Blade template --}} + + + +{{-- Validation errors automatically displayed --}} +``` + +### Custom Error Messages + +```blade + + + +``` + +## Complete Form Example + +```blade +
+ @csrf + +
+ {{-- Title --}} + + + + + {{-- Slug --}} + + + + + {{-- Content --}} + + + + + {{-- Status --}} + + + + + {{-- Category --}} + + + + + {{-- Options --}} +
+ + + +
+ + {{-- Actions --}} +
+ + Save Post + + + + Cancel + + + + Delete + +
+
+
+``` + +## Styling + +Components use Tailwind CSS and can be customized: + +```blade + +``` + +### Custom Wrapper Classes + +```blade + + + +``` + +## Best Practices + +### 1. Always Use Form Groups + +```blade +{{-- ✅ Good - wrapped in form-group --}} + + + + +{{-- ❌ Bad - no form-group --}} + +``` + +### 2. Use Old Values + +```blade +{{-- ✅ Good - preserves input on validation errors --}} + + +{{-- ❌ Bad - loses input on validation errors --}} + +``` + +### 3. Provide Helpful Placeholders + +```blade +{{-- ✅ Good - clear placeholder --}} + + +{{-- ❌ Bad - vague placeholder --}} + +``` + +### 4. Use Authorization Props + +```blade +{{-- ✅ Good - respects permissions --}} + + Delete + +``` + +## Learn More + +- [Livewire Modals →](/packages/admin/modals) +- [Authorization →](/packages/admin/authorization) +- [HLCRF Layouts →](/packages/admin/hlcrf) diff --git a/packages/admin/hlcrf-deep-dive.md b/packages/admin/hlcrf-deep-dive.md new file mode 100644 index 0000000..f153cbe --- /dev/null +++ b/packages/admin/hlcrf-deep-dive.md @@ -0,0 +1,843 @@ +# HLCRF Deep Dive + +This guide provides an in-depth look at the HLCRF (Header-Left-Content-Right-Footer) layout system, covering all layout combinations, the ID system, responsive patterns, and complex real-world examples. + +## Layout Combinations + +HLCRF supports any combination of its five regions. The variant name describes which regions are present. + +### All Possible Combinations + +| Variant | Regions | Use Case | +|---------|---------|----------| +| `C` | Content only | Simple content pages | +| `HC` | Header + Content | Landing pages | +| `CF` | Content + Footer | Article pages | +| `HCF` | Header + Content + Footer | Standard pages | +| `LC` | Left + Content | App with navigation | +| `CR` | Content + Right | Content with sidebar | +| `LCR` | Left + Content + Right | Three-column layout | +| `HLC` | Header + Left + Content | Admin dashboard | +| `HCR` | Header + Content + Right | Blog with widgets | +| `LCF` | Left + Content + Footer | App with footer | +| `CRF` | Content + Right + Footer | Blog layout | +| `HLCF` | Header + Left + Content + Footer | Standard admin | +| `HCRF` | Header + Content + Right + Footer | Blog layout | +| `HLCR` | Header + Left + Content + Right | Full admin | +| `LCRF` | Left + Content + Right + Footer | Complex app | +| `HLCRF` | All five regions | Complete layout | + +### Content-Only (C) + +Minimal layout for simple content: + +```php +use Core\Front\Components\Layout; + +$layout = Layout::make('C') + ->c('
Simple content without chrome
'); + +echo $layout->render(); +``` + +**Output:** +```html +
+
+
+
+
Simple content without chrome
+
+
+
+
+``` + +### Header + Content + Footer (HCF) + +Standard page layout: + +```php +$layout = Layout::make('HCF') + ->h('') + ->c('
Page Content
') + ->f('
Copyright 2026
'); +``` + +### Left + Content (LC) + +Application with navigation sidebar: + +```php +$layout = Layout::make('LC') + ->l('') + ->c('
App Content
'); +``` + +### Three-Column (LCR) + +Full three-column layout: + +```php +$layout = Layout::make('LCR') + ->l('') + ->c('
Content
') + ->r(''); +``` + +### Full Admin (HLCRF) + +Complete admin panel: + +```php +$layout = Layout::make('HLCRF') + ->h('
Admin Header
') + ->l('') + ->c('
Dashboard
') + ->r('') + ->f('
Status Bar
'); +``` + +## The ID System + +Every HLCRF element receives a unique, hierarchical ID that describes its position in the layout tree. + +### ID Format + +``` +{Region}-{Index}[-{NestedRegion}-{NestedIndex}]... +``` + +**Components:** +- **Region Letter** - `H`, `L`, `C`, `R`, or `F` +- **Index** - Zero-based position within that slot (0, 1, 2, ...) +- **Nesting** - Dash-separated chain for nested layouts + +### Region Letters + +| Letter | Region | Semantic Role | +|--------|--------|---------------| +| `H` | Header | Top navigation, branding | +| `L` | Left | Primary sidebar, navigation | +| `C` | Content | Main content area | +| `R` | Right | Secondary sidebar, widgets | +| `F` | Footer | Bottom links, copyright | + +### ID Examples + +**Simple layout:** +```html +
+
+
First header element
+
Second header element
+
+
+
First content element
+
+
+``` + +**Nested layout:** +```html +
+
+
+ +
+ +
+
Nested content
+
+
+
+
+
+``` + +### ID Interpretation + +| ID | Meaning | +|----|---------| +| `H-0` | First element in Header | +| `L-2` | Third element in Left sidebar | +| `C-0` | First element in Content | +| `C-L-0` | Content > Left > First element | +| `C-R-2` | Content > Right > Third element | +| `C-L-0-R-1` | Content > Left > First > Right > Second | +| `H-0-C-0-L-0` | Header > Content > Left (deeply nested) | + +### Using IDs for CSS + +The ID system enables precise CSS targeting: + +```css +/* Target first header element */ +[data-block="H-0"] { + background: #1a1a2e; +} + +/* Target all elements in left sidebar */ +[data-slot="L"] > [data-block] { + padding: 1rem; +} + +/* Target nested content areas */ +[data-block*="-C-"] { + margin: 2rem; +} + +/* Target second element in any right sidebar */ +[data-block$="-R-1"] { + border-top: 1px solid #e5e7eb; +} + +/* Target deeply nested layouts */ +[data-layout*="-"][data-layout*="-"] { + background: #f9fafb; +} +``` + +### Using IDs for Testing + +```php +// PHPUnit/Pest +$this->assertSee('[data-block="H-0"]'); +$this->assertSeeInOrder(['[data-slot="L"]', '[data-slot="C"]']); + +// Playwright/Cypress +await page.locator('[data-block="C-0"]').click(); +await expect(page.locator('[data-slot="R"]')).toBeVisible(); +``` + +### Using IDs for JavaScript + +```javascript +// Target specific elements +const header = document.querySelector('[data-block="H-0"]'); +const sidebar = document.querySelector('[data-slot="L"]'); + +// Dynamic targeting +function getContentBlock(index) { + return document.querySelector(`[data-block="C-${index}"]`); +} + +// Nested targeting +const nestedLeft = document.querySelector('[data-block="C-L-0"]'); +``` + +## Responsive Design Patterns + +### Mobile-First Stacking + +On mobile, stack regions vertically: + +```blade + + Navigation + Content + Widgets + +``` + +**Behavior:** +- **Mobile (< 768px):** Left -> Content -> Right (vertical) +- **Tablet (768px-1024px):** Left | Content (two columns) +- **Desktop (> 1024px):** Left | Content | Right (three columns) + +### Collapsible Sidebars + +```blade + + + +``` + +### Hidden Regions on Mobile + +```blade + +``` + +### Flexible Width Distribution + +```blade + + + Fixed-width sidebar + + + + Flexible content + + + + Percentage-width sidebar + + +``` + +### Responsive Grid Inside Content + +```blade + +
+ + + +
+
+``` + +## Complex Real-World Examples + +### Admin Dashboard + +A complete admin dashboard with nested layouts: + +```php +use Core\Front\Components\Layout; + +// Main admin layout +$admin = Layout::make('HLCF') + ->h( + '' + ) + ->l( + '
' + ) + ->c( + // Nested layout inside content + Layout::make('HCR') + ->h('
+

Dashboard

+ +
') + ->c('
+
+
Stat 1
+
Stat 2
+
Stat 3
+
+
+

Recent Activity

+ ...
+
+
') + ->r('') + ) + ->f( + '
+ Version 1.0.0 | Last sync: 5 minutes ago +
' + ); + +echo $admin->render(); +``` + +**Generated IDs:** +- `H-0` - Admin header/navigation +- `L-0` - Sidebar navigation +- `C-0` - Nested layout container +- `C-0-H-0` - Content header (page title/actions) +- `C-0-C-0` - Content main area (stats/table) +- `C-0-R-0` - Content right sidebar (quick actions) +- `F-0` - Admin footer + +### E-Commerce Product Page + +Product page with nested sections: + +```php +$productPage = Layout::make('HCF') + ->h('
+ +
Search | Cart | Account
+
') + ->c( + Layout::make('LCR') + ->l('
+
+ Product +
+
+ + +
+
') + ->c( + // Empty - using left/right only + ) + ->r('
+

Product Name

+

$99.99

+

Product description...

+ +
+ + +
+ +
+

Shipping Info

+

Free delivery over $50

+
+
'), + // Reviews section + Layout::make('CR') + ->c('
+

Customer Reviews

+
+
Review 1...
+
Review 2...
+
+
') + ->r('') + ) + ->f('
+
+
About Us
+
Customer Service
+
Policies
+
Newsletter
+
+
'); +``` + +### Multi-Panel Settings Page + +Settings page with multiple nested panels: + +```php +$settings = Layout::make('HLC') + ->h('
+

Account Settings

+
') + ->l('') + ->c( + // Profile section + Layout::make('HCF') + ->h('
+

Profile Information

+

Update your account details

+
') + ->c('
+
+ + +
+
+ + +
+
+ + +
+
') + ->f('
+ + +
') + ); +``` + +### Documentation Site + +Documentation layout with table of contents: + +```php +$docs = Layout::make('HLCRF') + ->h('
+
+
+ + +
+
+ + GitHub +
+
+
') + ->l('') + ->c('
+

Introduction

+

Welcome to the documentation...

+ +

Key Features

+
    +
  • Feature 1
  • +
  • Feature 2
  • +
  • Feature 3
  • +
+ +

Next Steps

+

Continue to the installation guide...

+
') + ->r('') + ->f(''); +``` + +### Email Client Interface + +Complex email client with multiple nested panels: + +```php +$email = Layout::make('HLCR') + ->h('
+
+ + +
+
+ +
+
+ +
JD
+
+
') + ->l('') + ->c( + Layout::make('LC') + ->l('
+
+ +
+
+
+
+ John Smith + 10:30 AM +
+
Meeting Tomorrow
+
Hi, just wanted to confirm...
+
+
+
+ Jane Doe + Yesterday +
+
Project Update
+
Here is the latest update...
+
+
+
') + ->c('
+
+ + + + + + +
+
+
+

Meeting Tomorrow

+
+
JS
+
+
John Smith <john@example.com>
+
to me
+
+
Jan 15, 2026, 10:30 AM
+
+
+
+

Hi,

+

Just wanted to confirm our meeting tomorrow at 2pm.

+

Best regards,
John

+
+
+
') + ) + ->r(''); +``` + +## Performance Considerations + +### Lazy Content Loading + +For large layouts, defer non-critical content: + +```php +$layout = Layout::make('LCR') + ->l('') + ->c('
+
Loading...
+
@livewire("content-panel")
+
') + ->r(fn () => view('widgets.sidebar')); // Closure defers evaluation +``` + +### Conditional Region Rendering + +Only render regions when needed: + +```php +$layout = Layout::make('LCR'); + +$layout->l(''); +$layout->c('
Content
'); + +// Conditionally add right sidebar +if ($user->hasFeature('widgets')) { + $layout->r(''); +} +``` + +### Efficient CSS Targeting + +Use data attributes instead of deep selectors: + +```css +/* Efficient - uses data attribute */ +[data-block="C-0"] { padding: 1rem; } + +/* Less efficient - deep selector */ +.hlcrf-layout > .hlcrf-body > .hlcrf-content > div:first-child { padding: 1rem; } +``` + +## Testing HLCRF Layouts + +### Unit Testing + +```php +use Core\Front\Components\Layout; +use PHPUnit\Framework\TestCase; + +class LayoutTest extends TestCase +{ + public function test_generates_correct_ids(): void + { + $layout = Layout::make('LC') + ->l('Left') + ->c('Content'); + + $html = $layout->render(); + + $this->assertStringContainsString('data-slot="L"', $html); + $this->assertStringContainsString('data-slot="C"', $html); + $this->assertStringContainsString('data-block="L-0"', $html); + $this->assertStringContainsString('data-block="C-0"', $html); + } + + public function test_nested_layout_ids(): void + { + $nested = Layout::make('LR') + ->l('Nested Left') + ->r('Nested Right'); + + $outer = Layout::make('C') + ->c($nested); + + $html = $outer->render(); + + $this->assertStringContainsString('data-block="C-0-L-0"', $html); + $this->assertStringContainsString('data-block="C-0-R-0"', $html); + } +} +``` + +### Browser Testing + +```php +// Pest with Playwright +it('renders admin layout correctly', function () { + $this->browse(function ($browser) { + $browser->visit('/admin') + ->assertPresent('[data-layout="root"]') + ->assertPresent('[data-slot="H"]') + ->assertPresent('[data-slot="L"]') + ->assertPresent('[data-slot="C"]'); + }); +}); +``` + +## Best Practices + +### 1. Use Semantic Region Names + +```php +// Good - semantic use +->h('') +->l('') +->c('
Page content
') +->r('') +->f('
Site footer
') + +// Bad - misuse of regions +->h('') // Header for sidebar? +``` + +### 2. Leverage the ID System + +```css +/* Target specific elements precisely */ +[data-block="H-0"] { /* Header first element */ } +[data-block="C-L-0"] { /* Content > Left > First */ } + +/* Don't fight the system with complex selectors */ +``` + +### 3. Keep Nesting Shallow + +```php +// Good - 2-3 levels max +Layout::make('HCF') + ->c(Layout::make('LCR')->...); + +// Avoid - too deep +Layout::make('C') + ->c(Layout::make('C') + ->c(Layout::make('C') + ->c(Layout::make('C')...)))); +``` + +### 4. Use Consistent Widths + +```php +// Good - consistent sidebar widths across app +->l('