docs: initial import of CorePHP documentation
173 markdown files covering: - Framework architecture (lifecycle events, module system, multi-tenancy) - Package docs (admin, api, mcp, tenant, commerce, content, developer) - CLI reference (dev, build, go, php, deploy commands) - Patterns (actions, repositories, seeders, services, HLCRF) - Deployment (Docker, PHP, LinuxKit, templates) - Publishing (Homebrew, AUR, npm, Docker, Scoop, Chocolatey) Source: core-php/docs (core.help content) Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
commit
85bbb8e828
176 changed files with 49045 additions and 0 deletions
389
api/authentication.md
Normal file
389
api/authentication.md
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
# API Authentication
|
||||
|
||||
Core PHP Framework provides multiple authentication methods for API access, including API keys, OAuth tokens, and session-based authentication.
|
||||
|
||||
## API Keys
|
||||
|
||||
API keys are the primary authentication method for external API access.
|
||||
|
||||
### Creating API Keys
|
||||
|
||||
```php
|
||||
use Mod\Api\Models\ApiKey;
|
||||
|
||||
$apiKey = ApiKey::create([
|
||||
'name' => 'Mobile App',
|
||||
'workspace_id' => $workspace->id,
|
||||
'scopes' => ['posts:read', 'posts:write', 'categories:read'],
|
||||
'rate_limit_tier' => 'pro',
|
||||
]);
|
||||
|
||||
// Get plaintext key (only shown once!)
|
||||
$plaintext = $apiKey->plaintext_key; // sk_live_...
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"name": "Mobile App",
|
||||
"key": "sk_live_abc123...",
|
||||
"scopes": ["posts:read", "posts:write"],
|
||||
"rate_limit_tier": "pro",
|
||||
"created_at": "2026-01-26T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
::: warning
|
||||
The plaintext API key is only shown once at creation. Store it securely!
|
||||
:::
|
||||
|
||||
### Using API Keys
|
||||
|
||||
Include the API key in the `Authorization` header:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer sk_live_abc123..." \
|
||||
https://api.example.com/v1/posts
|
||||
```
|
||||
|
||||
Or use basic authentication:
|
||||
|
||||
```bash
|
||||
curl -u sk_live_abc123: \
|
||||
https://api.example.com/v1/posts
|
||||
```
|
||||
|
||||
### Key Format
|
||||
|
||||
API keys follow the format: `{prefix}_{environment}_{random}`
|
||||
|
||||
- **Prefix:** `sk` (secret key)
|
||||
- **Environment:** `live` or `test`
|
||||
- **Random:** 32 characters
|
||||
|
||||
**Examples:**
|
||||
- `sk_live_`
|
||||
- `sk_test_`
|
||||
|
||||
### Key Security
|
||||
|
||||
API keys are hashed with bcrypt before storage:
|
||||
|
||||
```php
|
||||
// Creation
|
||||
$hash = bcrypt($plaintext);
|
||||
|
||||
// Verification
|
||||
if (Hash::check($providedKey, $apiKey->key_hash)) {
|
||||
// Valid key
|
||||
}
|
||||
```
|
||||
|
||||
**Security Features:**
|
||||
- Never stored in plaintext
|
||||
- Bcrypt hashing (cost factor: 10)
|
||||
- Secure comparison with `hash_equals()`
|
||||
- Rate limiting per key
|
||||
- Automatic expiry support
|
||||
|
||||
### Key Rotation
|
||||
|
||||
Rotate keys regularly for security:
|
||||
|
||||
```php
|
||||
$newKey = $apiKey->rotate();
|
||||
|
||||
// Returns new key object with:
|
||||
// - New plaintext key
|
||||
// - Same scopes and settings
|
||||
// - Old key marked for deletion after grace period
|
||||
```
|
||||
|
||||
**Grace Period:**
|
||||
- Default: 24 hours
|
||||
- Both old and new keys work during this period
|
||||
- Old key auto-deleted after grace period
|
||||
|
||||
### Key Permissions
|
||||
|
||||
Control what each key can access:
|
||||
|
||||
```php
|
||||
$apiKey = ApiKey::create([
|
||||
'name' => 'Read-Only Key',
|
||||
'scopes' => [
|
||||
'posts:read',
|
||||
'categories:read',
|
||||
'analytics:read',
|
||||
],
|
||||
]);
|
||||
```
|
||||
|
||||
Available scopes documented in [Scopes & Permissions](#scopes--permissions).
|
||||
|
||||
## Sanctum Tokens
|
||||
|
||||
Laravel Sanctum provides token-based authentication for SPAs:
|
||||
|
||||
### Creating Tokens
|
||||
|
||||
```php
|
||||
$user = User::find(1);
|
||||
|
||||
$token = $user->createToken('mobile-app', [
|
||||
'posts:read',
|
||||
'posts:write',
|
||||
])->plainTextToken;
|
||||
```
|
||||
|
||||
### Using Tokens
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer 1|abc123..." \
|
||||
https://api.example.com/v1/posts
|
||||
```
|
||||
|
||||
### Token Abilities
|
||||
|
||||
Check token abilities in controllers:
|
||||
|
||||
```php
|
||||
if ($request->user()->tokenCan('posts:write')) {
|
||||
// User has permission
|
||||
}
|
||||
```
|
||||
|
||||
## Session Authentication
|
||||
|
||||
For first-party applications, use session-based authentication:
|
||||
|
||||
```bash
|
||||
# Login first
|
||||
curl -X POST https://api.example.com/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"user@example.com","password":"secret"}' \
|
||||
-c cookies.txt
|
||||
|
||||
# Use session cookie
|
||||
curl https://api.example.com/v1/posts \
|
||||
-b cookies.txt
|
||||
```
|
||||
|
||||
## OAuth 2.0 (Optional)
|
||||
|
||||
If Laravel Passport is installed, OAuth 2.0 is available:
|
||||
|
||||
### Authorization Code Grant
|
||||
|
||||
```bash
|
||||
# 1. Redirect user to authorization endpoint
|
||||
https://api.example.com/oauth/authorize?
|
||||
client_id=CLIENT_ID&
|
||||
redirect_uri=CALLBACK_URL&
|
||||
response_type=code&
|
||||
scope=posts:read posts:write
|
||||
|
||||
# 2. Exchange code for token
|
||||
curl -X POST https://api.example.com/oauth/token \
|
||||
-d "grant_type=authorization_code" \
|
||||
-d "client_id=CLIENT_ID" \
|
||||
-d "client_secret=CLIENT_SECRET" \
|
||||
-d "code=AUTH_CODE" \
|
||||
-d "redirect_uri=CALLBACK_URL"
|
||||
```
|
||||
|
||||
### Client Credentials Grant
|
||||
|
||||
For server-to-server:
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.example.com/oauth/token \
|
||||
-d "grant_type=client_credentials" \
|
||||
-d "client_id=CLIENT_ID" \
|
||||
-d "client_secret=CLIENT_SECRET" \
|
||||
-d "scope=posts:read"
|
||||
```
|
||||
|
||||
## Scopes & Permissions
|
||||
|
||||
### Available Scopes
|
||||
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| `posts:read` | Read blog posts |
|
||||
| `posts:write` | Create and update posts |
|
||||
| `posts:delete` | Delete posts |
|
||||
| `categories:read` | Read categories |
|
||||
| `categories:write` | Create and update categories |
|
||||
| `analytics:read` | Access analytics data |
|
||||
| `webhooks:manage` | Manage webhook endpoints |
|
||||
| `keys:manage` | Manage API keys |
|
||||
| `admin:*` | Full admin access |
|
||||
|
||||
### Scope Enforcement
|
||||
|
||||
Protect routes with scope middleware:
|
||||
|
||||
```php
|
||||
Route::middleware('scope:posts:write')
|
||||
->post('/posts', [PostController::class, 'store']);
|
||||
```
|
||||
|
||||
### Wildcard Scopes
|
||||
|
||||
Use wildcards for broad permissions:
|
||||
|
||||
- `posts:*` - All post permissions
|
||||
- `*:read` - Read access to all resources
|
||||
- `*` - Full access (use sparingly!)
|
||||
|
||||
## Authentication Errors
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
Missing or invalid credentials:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Unauthenticated."
|
||||
}
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
- No `Authorization` header
|
||||
- Invalid API key
|
||||
- Expired token
|
||||
- Revoked credentials
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
Valid credentials but insufficient permissions:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "This action is unauthorized.",
|
||||
"required_scope": "posts:write",
|
||||
"provided_scopes": ["posts:read"]
|
||||
}
|
||||
```
|
||||
|
||||
**Causes:**
|
||||
- Missing required scope
|
||||
- Workspace suspended
|
||||
- Resource access denied
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Minimum Required Scopes
|
||||
|
||||
```php
|
||||
// ✅ Good - specific scopes
|
||||
$apiKey->scopes = ['posts:read', 'categories:read'];
|
||||
|
||||
// ❌ Bad - excessive permissions
|
||||
$apiKey->scopes = ['*'];
|
||||
```
|
||||
|
||||
### 2. Rotate Keys Regularly
|
||||
|
||||
```php
|
||||
// Rotate every 90 days
|
||||
if ($apiKey->created_at->diffInDays() > 90) {
|
||||
$apiKey->rotate();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Different Keys Per Client
|
||||
|
||||
```php
|
||||
// ✅ Good - separate keys
|
||||
ApiKey::create(['name' => 'Mobile App iOS']);
|
||||
ApiKey::create(['name' => 'Mobile App Android']);
|
||||
|
||||
// ❌ Bad - shared key
|
||||
ApiKey::create(['name' => 'All Mobile Apps']);
|
||||
```
|
||||
|
||||
### 4. Monitor Key Usage
|
||||
|
||||
```php
|
||||
$usage = ApiKey::find($id)->usage()
|
||||
->whereBetween('created_at', [now()->subDays(7), now()])
|
||||
->count();
|
||||
```
|
||||
|
||||
### 5. Implement Key Expiry
|
||||
|
||||
```php
|
||||
$apiKey = ApiKey::create([
|
||||
'name' => 'Temporary Key',
|
||||
'expires_at' => now()->addDays(30),
|
||||
]);
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
All authenticated requests are rate limited based on tier:
|
||||
|
||||
| Tier | Requests per Hour |
|
||||
|------|------------------|
|
||||
| Free | 1,000 |
|
||||
| Pro | 10,000 |
|
||||
| Enterprise | Unlimited |
|
||||
|
||||
Rate limit headers included in responses:
|
||||
|
||||
```
|
||||
X-RateLimit-Limit: 10000
|
||||
X-RateLimit-Remaining: 9995
|
||||
X-RateLimit-Reset: 1640995200
|
||||
```
|
||||
|
||||
## Testing Authentication
|
||||
|
||||
### Test Mode Keys
|
||||
|
||||
Use test keys for development:
|
||||
|
||||
```php
|
||||
$testKey = ApiKey::create([
|
||||
'name' => 'Test Key',
|
||||
'environment' => 'test',
|
||||
]);
|
||||
|
||||
// Key prefix: sk_test_...
|
||||
```
|
||||
|
||||
Test keys:
|
||||
- Don't affect production data
|
||||
- Higher rate limits
|
||||
- Clearly marked in admin panel
|
||||
- Can be deleted without confirmation
|
||||
|
||||
### cURL Examples
|
||||
|
||||
**API Key:**
|
||||
```bash
|
||||
curl -H "Authorization: Bearer sk_live_..." \
|
||||
https://api.example.com/v1/posts
|
||||
```
|
||||
|
||||
**Sanctum Token:**
|
||||
```bash
|
||||
curl -H "Authorization: Bearer 1|..." \
|
||||
https://api.example.com/v1/posts
|
||||
```
|
||||
|
||||
**Session:**
|
||||
```bash
|
||||
curl -H "Cookie: laravel_session=..." \
|
||||
https://api.example.com/v1/posts
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [API Reference →](/api/endpoints)
|
||||
- [Rate Limiting →](/api/endpoints#rate-limiting)
|
||||
- [Error Handling →](/api/errors)
|
||||
- [API Package →](/packages/api)
|
||||
743
api/endpoints.md
Normal file
743
api/endpoints.md
Normal file
|
|
@ -0,0 +1,743 @@
|
|||
# API Endpoints Reference
|
||||
|
||||
Core PHP Framework provides RESTful APIs for programmatic access to platform resources. All endpoints follow consistent patterns for authentication, pagination, filtering, and error handling.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
https://your-domain.com/api/v1
|
||||
```
|
||||
|
||||
## Common Parameters
|
||||
|
||||
### Pagination
|
||||
|
||||
All list endpoints support pagination:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources?page=2&per_page=50
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `page` (integer) - Page number (default: 1)
|
||||
- `per_page` (integer) - Items per page (default: 15, max: 100)
|
||||
|
||||
**Response includes:**
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"meta": {
|
||||
"current_page": 2,
|
||||
"per_page": 50,
|
||||
"total": 250,
|
||||
"last_page": 5
|
||||
},
|
||||
"links": {
|
||||
"first": "https://api.example.com/resources?page=1",
|
||||
"last": "https://api.example.com/resources?page=5",
|
||||
"prev": "https://api.example.com/resources?page=1",
|
||||
"next": "https://api.example.com/resources?page=3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
||||
Filter list results using query parameters:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources?status=active&created_after=2024-01-01
|
||||
```
|
||||
|
||||
Common filters:
|
||||
- `status` - Filter by status (varies by resource)
|
||||
- `created_after` - ISO 8601 date
|
||||
- `created_before` - ISO 8601 date
|
||||
- `updated_after` - ISO 8601 date
|
||||
- `updated_before` - ISO 8601 date
|
||||
- `search` - Full-text search (if supported)
|
||||
|
||||
### Sorting
|
||||
|
||||
Sort results using the `sort` parameter:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources?sort=-created_at,name
|
||||
```
|
||||
|
||||
- Prefix with `-` for descending order
|
||||
- Default is ascending order
|
||||
- Comma-separate multiple sort fields
|
||||
|
||||
### Field Selection
|
||||
|
||||
Request specific fields only:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources?fields=id,name,created_at
|
||||
```
|
||||
|
||||
Reduces payload size and improves performance.
|
||||
|
||||
### Includes
|
||||
|
||||
Eager-load related resources:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources?include=owner,tags,metadata
|
||||
```
|
||||
|
||||
Reduces number of API calls needed.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
API requests are rate-limited based on your tier:
|
||||
|
||||
| Tier | Requests/Hour | Burst |
|
||||
|------|--------------|-------|
|
||||
| Free | 1,000 | 50 |
|
||||
| Pro | 10,000 | 200 |
|
||||
| Business | 50,000 | 500 |
|
||||
| Enterprise | Custom | Custom |
|
||||
|
||||
Rate limit headers included in every response:
|
||||
|
||||
```http
|
||||
X-RateLimit-Limit: 10000
|
||||
X-RateLimit-Remaining: 9847
|
||||
X-RateLimit-Reset: 1640995200
|
||||
```
|
||||
|
||||
When rate limit is exceeded, you'll receive a `429 Too Many Requests` response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "RATE_LIMIT_EXCEEDED",
|
||||
"message": "Rate limit exceeded. Please retry after 3600 seconds.",
|
||||
"retry_after": 3600
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Idempotency
|
||||
|
||||
POST, PATCH, PUT, and DELETE requests support idempotency keys to safely retry requests:
|
||||
|
||||
```http
|
||||
POST /api/v1/resources
|
||||
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
If the same idempotency key is used within 24 hours:
|
||||
- Same status code and response body returned
|
||||
- No duplicate resource created
|
||||
- Safe to retry failed requests
|
||||
|
||||
## Versioning
|
||||
|
||||
The API version is included in the URL path:
|
||||
|
||||
```
|
||||
/api/v1/resources
|
||||
```
|
||||
|
||||
When breaking changes are introduced, a new version will be released (e.g., `/api/v2/`). Previous versions are supported for at least 12 months after deprecation notice.
|
||||
|
||||
## Workspaces & Namespaces
|
||||
|
||||
Multi-tenant resources require workspace and/or namespace context:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources
|
||||
X-Workspace-ID: 123
|
||||
X-Namespace-ID: 456
|
||||
```
|
||||
|
||||
Alternatively, use query parameters:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources?workspace_id=123&namespace_id=456
|
||||
```
|
||||
|
||||
See [Namespaces & Entitlements](/security/namespaces) for details on multi-tenancy.
|
||||
|
||||
## Webhook Events
|
||||
|
||||
Configure webhooks to receive real-time notifications:
|
||||
|
||||
```http
|
||||
POST /api/v1/webhooks
|
||||
{
|
||||
"url": "https://your-app.com/webhooks",
|
||||
"events": ["resource.created", "resource.updated"],
|
||||
"secret": "whsec_abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
**Common events:**
|
||||
- `{resource}.created` - Resource created
|
||||
- `{resource}.updated` - Resource updated
|
||||
- `{resource}.deleted` - Resource deleted
|
||||
|
||||
**Webhook payload:**
|
||||
```json
|
||||
{
|
||||
"id": "evt_1234567890",
|
||||
"type": "resource.created",
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"data": {
|
||||
"object": {
|
||||
"id": "res_abc123",
|
||||
"type": "resource",
|
||||
"attributes": {...}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Webhook requests include HMAC-SHA256 signature in headers:
|
||||
|
||||
```http
|
||||
X-Webhook-Signature: sha256=abc123...
|
||||
X-Webhook-Timestamp: 1640995200
|
||||
```
|
||||
|
||||
See [Webhook Security](/api/authentication#webhook-signatures) for signature verification.
|
||||
|
||||
## Error Handling
|
||||
|
||||
All errors follow a consistent format. See [Error Reference](/api/errors) for details.
|
||||
|
||||
**Example error response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Validation failed",
|
||||
"details": {
|
||||
"email": ["The email field is required."]
|
||||
},
|
||||
"request_id": "req_abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Endpoints
|
||||
|
||||
### Core Resources
|
||||
|
||||
The following resource types are available:
|
||||
|
||||
- **Workspaces** - Multi-tenant workspaces
|
||||
- **Namespaces** - Service isolation contexts
|
||||
- **Users** - User accounts
|
||||
- **API Keys** - API authentication credentials
|
||||
- **Webhooks** - Webhook endpoints
|
||||
|
||||
### Workspace Endpoints
|
||||
|
||||
#### List Workspaces
|
||||
|
||||
```http
|
||||
GET /api/v1/workspaces
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "wks_abc123",
|
||||
"name": "Acme Corporation",
|
||||
"slug": "acme-corp",
|
||||
"tier": "business",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Workspace
|
||||
|
||||
```http
|
||||
GET /api/v1/workspaces/{workspace_id}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "wks_abc123",
|
||||
"name": "Acme Corporation",
|
||||
"slug": "acme-corp",
|
||||
"tier": "business",
|
||||
"settings": {
|
||||
"timezone": "UTC",
|
||||
"locale": "en_GB"
|
||||
},
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Create Workspace
|
||||
|
||||
```http
|
||||
POST /api/v1/workspaces
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "New Workspace",
|
||||
"slug": "new-workspace",
|
||||
"tier": "pro"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created`
|
||||
|
||||
#### Update Workspace
|
||||
|
||||
```http
|
||||
PATCH /api/v1/workspaces/{workspace_id}
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "Updated Name",
|
||||
"settings": {
|
||||
"timezone": "Europe/London"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK`
|
||||
|
||||
#### Delete Workspace
|
||||
|
||||
```http
|
||||
DELETE /api/v1/workspaces/{workspace_id}
|
||||
```
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
### Namespace Endpoints
|
||||
|
||||
#### List Namespaces
|
||||
|
||||
```http
|
||||
GET /api/v1/namespaces
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
- `owner_type` - Filter by owner type (`User` or `Workspace`)
|
||||
- `workspace_id` - Filter by workspace
|
||||
- `is_active` - Filter by active status
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "ns_abc123",
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Personal Namespace",
|
||||
"slug": "personal",
|
||||
"owner_type": "User",
|
||||
"owner_id": 42,
|
||||
"workspace_id": null,
|
||||
"is_default": true,
|
||||
"is_active": true,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Namespace
|
||||
|
||||
```http
|
||||
GET /api/v1/namespaces/{namespace_id}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "ns_abc123",
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Client: Acme Corp",
|
||||
"slug": "client-acme",
|
||||
"owner_type": "Workspace",
|
||||
"owner_id": 10,
|
||||
"workspace_id": 10,
|
||||
"packages": [
|
||||
{
|
||||
"id": "pkg_starter",
|
||||
"name": "Starter Package",
|
||||
"expires_at": null
|
||||
}
|
||||
],
|
||||
"entitlements": {
|
||||
"storage": {
|
||||
"used": 1024000000,
|
||||
"limit": 5368709120,
|
||||
"unit": "bytes"
|
||||
},
|
||||
"api_calls": {
|
||||
"used": 5430,
|
||||
"limit": 10000,
|
||||
"reset_at": "2024-02-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Check Entitlement
|
||||
|
||||
```http
|
||||
POST /api/v1/namespaces/{namespace_id}/entitlements/check
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"feature": "storage",
|
||||
"quantity": 1073741824
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"allowed": false,
|
||||
"reason": "LIMIT_EXCEEDED",
|
||||
"message": "Storage limit exceeded. Used: 1.00 GB, Available: 0.50 GB, Requested: 1.00 GB",
|
||||
"current_usage": 1024000000,
|
||||
"limit": 5368709120,
|
||||
"available": 536870912
|
||||
}
|
||||
```
|
||||
|
||||
### User Endpoints
|
||||
|
||||
#### List Users
|
||||
|
||||
```http
|
||||
GET /api/v1/users
|
||||
X-Workspace-ID: 123
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"tier": "pro",
|
||||
"email_verified_at": "2024-01-01T12:00:00Z",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Current User
|
||||
|
||||
```http
|
||||
GET /api/v1/user
|
||||
```
|
||||
|
||||
Returns the authenticated user.
|
||||
|
||||
#### Update User
|
||||
|
||||
```http
|
||||
PATCH /api/v1/users/{user_id}
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "Jane Doe",
|
||||
"email": "jane@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### API Key Endpoints
|
||||
|
||||
#### List API Keys
|
||||
|
||||
```http
|
||||
GET /api/v1/api-keys
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "key_abc123",
|
||||
"name": "Production API Key",
|
||||
"prefix": "sk_live_",
|
||||
"last_used_at": "2024-01-15T10:30:00Z",
|
||||
"expires_at": null,
|
||||
"scopes": ["read:all", "write:resources"],
|
||||
"rate_limit_tier": "business",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Create API Key
|
||||
|
||||
```http
|
||||
POST /api/v1/api-keys
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "New API Key",
|
||||
"scopes": ["read:all"],
|
||||
"rate_limit_tier": "pro",
|
||||
"expires_at": "2025-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "key_abc123",
|
||||
"name": "New API Key",
|
||||
"key": "sk_live_abc123def456...",
|
||||
"scopes": ["read:all"],
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **Important:** The `key` field is only returned once during creation. Store it securely.
|
||||
|
||||
#### Revoke API Key
|
||||
|
||||
```http
|
||||
DELETE /api/v1/api-keys/{key_id}
|
||||
```
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
### Webhook Endpoints
|
||||
|
||||
#### List Webhooks
|
||||
|
||||
```http
|
||||
GET /api/v1/webhooks
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "wh_abc123",
|
||||
"url": "https://your-app.com/webhooks",
|
||||
"events": ["resource.created", "resource.updated"],
|
||||
"is_active": true,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Create Webhook
|
||||
|
||||
```http
|
||||
POST /api/v1/webhooks
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"url": "https://your-app.com/webhooks",
|
||||
"events": ["resource.created"],
|
||||
"secret": "whsec_abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
#### Test Webhook
|
||||
|
||||
```http
|
||||
POST /api/v1/webhooks/{webhook_id}/test
|
||||
```
|
||||
|
||||
Sends a test event to the webhook URL.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"status_code": 200,
|
||||
"response_time_ms": 145
|
||||
}
|
||||
```
|
||||
|
||||
#### Webhook Deliveries
|
||||
|
||||
```http
|
||||
GET /api/v1/webhooks/{webhook_id}/deliveries
|
||||
```
|
||||
|
||||
View delivery history and retry failed deliveries:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "del_abc123",
|
||||
"event_type": "resource.created",
|
||||
"status": "success",
|
||||
"status_code": 200,
|
||||
"attempts": 1,
|
||||
"delivered_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Idempotency Keys
|
||||
|
||||
Always use idempotency keys for create/update operations:
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/v1/resources', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Idempotency-Key': crypto.randomUUID(),
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Handle Rate Limits
|
||||
|
||||
Respect rate limit headers and implement exponential backoff:
|
||||
|
||||
```javascript
|
||||
async function apiRequest(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (response.status === 429) {
|
||||
const retryAfter = response.headers.get('X-RateLimit-Reset');
|
||||
await sleep(retryAfter * 1000);
|
||||
return apiRequest(url, options); // Retry
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Field Selection
|
||||
|
||||
Request only needed fields to reduce payload size:
|
||||
|
||||
```http
|
||||
GET /api/v1/resources?fields=id,name,status
|
||||
```
|
||||
|
||||
### 4. Batch Operations
|
||||
|
||||
When possible, use batch endpoints instead of multiple single requests:
|
||||
|
||||
```http
|
||||
POST /api/v1/resources/batch
|
||||
{
|
||||
"operations": [
|
||||
{"action": "create", "data": {...}},
|
||||
{"action": "update", "id": "res_123", "data": {...}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Verify Webhook Signatures
|
||||
|
||||
Always verify webhook signatures to ensure authenticity:
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function verifyWebhook(payload, signature, secret) {
|
||||
const hmac = crypto.createHmac('sha256', secret);
|
||||
hmac.update(payload);
|
||||
const expected = 'sha256=' + hmac.digest('hex');
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expected)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Store API Keys Securely
|
||||
|
||||
- Never commit API keys to version control
|
||||
- Use environment variables or secrets management
|
||||
- Rotate keys regularly
|
||||
- Use separate keys for development/production
|
||||
|
||||
### 7. Monitor Usage
|
||||
|
||||
Track your API usage to avoid hitting rate limits:
|
||||
|
||||
```http
|
||||
GET /api/v1/usage
|
||||
```
|
||||
|
||||
Returns current usage statistics for your account.
|
||||
|
||||
## SDKs & Libraries
|
||||
|
||||
Official SDKs available:
|
||||
|
||||
- **PHP:** `composer require core-php/sdk`
|
||||
- **JavaScript/Node.js:** `npm install @core-php/sdk`
|
||||
- **Python:** `pip install core-php-sdk`
|
||||
|
||||
**Example (PHP):**
|
||||
|
||||
```php
|
||||
use CorePhp\SDK\Client;
|
||||
|
||||
$client = new Client('sk_live_abc123...');
|
||||
|
||||
$workspace = $client->workspaces->create([
|
||||
'name' => 'My Workspace',
|
||||
'tier' => 'pro',
|
||||
]);
|
||||
|
||||
$namespaces = $client->namespaces->list([
|
||||
'workspace_id' => $workspace->id,
|
||||
]);
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Authentication](/api/authentication) - API key management and authentication methods
|
||||
- [Error Handling](/api/errors) - Error codes and debugging
|
||||
- [Namespaces & Entitlements](/security/namespaces) - Multi-tenancy and feature access
|
||||
- [Webhooks Guide](#webhook-events) - Setting up webhook endpoints
|
||||
- [Rate Limiting](#rate-limiting) - Understanding rate limits and tiers
|
||||
525
api/errors.md
Normal file
525
api/errors.md
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
# API Errors
|
||||
|
||||
Core PHP Framework uses conventional HTTP response codes and provides detailed error information to help you debug issues.
|
||||
|
||||
## HTTP Status Codes
|
||||
|
||||
### 2xx Success
|
||||
|
||||
| Code | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| 200 | OK | Request succeeded |
|
||||
| 201 | Created | Resource created successfully |
|
||||
| 202 | Accepted | Request accepted for processing |
|
||||
| 204 | No Content | Request succeeded, no content to return |
|
||||
|
||||
### 4xx Client Errors
|
||||
|
||||
| Code | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| 400 | Bad Request | Invalid request format or parameters |
|
||||
| 401 | Unauthorized | Missing or invalid authentication |
|
||||
| 403 | Forbidden | Authenticated but not authorized |
|
||||
| 404 | Not Found | Resource doesn't exist |
|
||||
| 405 | Method Not Allowed | HTTP method not supported for endpoint |
|
||||
| 409 | Conflict | Request conflicts with current state |
|
||||
| 422 | Unprocessable Entity | Validation failed |
|
||||
| 429 | Too Many Requests | Rate limit exceeded |
|
||||
|
||||
### 5xx Server Errors
|
||||
|
||||
| Code | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| 500 | Internal Server Error | Unexpected server error |
|
||||
| 502 | Bad Gateway | Invalid response from upstream server |
|
||||
| 503 | Service Unavailable | Server temporarily unavailable |
|
||||
| 504 | Gateway Timeout | Upstream server timeout |
|
||||
|
||||
## Error Response Format
|
||||
|
||||
All errors return JSON with consistent structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Human-readable error message",
|
||||
"error_code": "MACHINE_READABLE_CODE",
|
||||
"errors": {
|
||||
"field": ["Detailed validation errors"]
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2026-01-26T12:00:00Z",
|
||||
"request_id": "req_abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Errors
|
||||
|
||||
### 400 Bad Request
|
||||
|
||||
**Missing Required Parameter:**
|
||||
```json
|
||||
{
|
||||
"message": "Missing required parameter: title",
|
||||
"error_code": "MISSING_PARAMETER",
|
||||
"errors": {
|
||||
"title": ["The title field is required."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Invalid Parameter Type:**
|
||||
```json
|
||||
{
|
||||
"message": "Invalid parameter type",
|
||||
"error_code": "INVALID_TYPE",
|
||||
"errors": {
|
||||
"published_at": ["The published at must be a valid date."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
**Missing Authentication:**
|
||||
```json
|
||||
{
|
||||
"message": "Unauthenticated.",
|
||||
"error_code": "UNAUTHENTICATED"
|
||||
}
|
||||
```
|
||||
|
||||
**Invalid API Key:**
|
||||
```json
|
||||
{
|
||||
"message": "Invalid API key",
|
||||
"error_code": "INVALID_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
**Expired Token:**
|
||||
```json
|
||||
{
|
||||
"message": "Token has expired",
|
||||
"error_code": "TOKEN_EXPIRED",
|
||||
"meta": {
|
||||
"expired_at": "2026-01-20T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
**Insufficient Permissions:**
|
||||
```json
|
||||
{
|
||||
"message": "This action is unauthorized.",
|
||||
"error_code": "INSUFFICIENT_PERMISSIONS",
|
||||
"required_scope": "posts:write",
|
||||
"provided_scopes": ["posts:read"]
|
||||
}
|
||||
```
|
||||
|
||||
**Workspace Suspended:**
|
||||
```json
|
||||
{
|
||||
"message": "Workspace is suspended",
|
||||
"error_code": "WORKSPACE_SUSPENDED",
|
||||
"meta": {
|
||||
"suspended_at": "2026-01-25T12:00:00Z",
|
||||
"reason": "Payment overdue"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Namespace Access Denied:**
|
||||
```json
|
||||
{
|
||||
"message": "You do not have access to this namespace",
|
||||
"error_code": "NAMESPACE_ACCESS_DENIED"
|
||||
}
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
|
||||
**Resource Not Found:**
|
||||
```json
|
||||
{
|
||||
"message": "Post not found",
|
||||
"error_code": "RESOURCE_NOT_FOUND",
|
||||
"resource_type": "Post",
|
||||
"resource_id": 999
|
||||
}
|
||||
```
|
||||
|
||||
**Endpoint Not Found:**
|
||||
```json
|
||||
{
|
||||
"message": "Endpoint not found",
|
||||
"error_code": "ENDPOINT_NOT_FOUND",
|
||||
"requested_path": "/v1/nonexistent"
|
||||
}
|
||||
```
|
||||
|
||||
### 409 Conflict
|
||||
|
||||
**Duplicate Resource:**
|
||||
```json
|
||||
{
|
||||
"message": "A post with this slug already exists",
|
||||
"error_code": "DUPLICATE_RESOURCE",
|
||||
"conflicting_field": "slug",
|
||||
"existing_resource_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
**State Conflict:**
|
||||
```json
|
||||
{
|
||||
"message": "Post is already published",
|
||||
"error_code": "STATE_CONFLICT",
|
||||
"current_state": "published",
|
||||
"requested_action": "publish"
|
||||
}
|
||||
```
|
||||
|
||||
### 422 Unprocessable Entity
|
||||
|
||||
**Validation Failed:**
|
||||
```json
|
||||
{
|
||||
"message": "The given data was invalid.",
|
||||
"error_code": "VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"title": [
|
||||
"The title field is required."
|
||||
],
|
||||
"content": [
|
||||
"The content must be at least 10 characters."
|
||||
],
|
||||
"category_id": [
|
||||
"The selected category is invalid."
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 429 Too Many Requests
|
||||
|
||||
**Rate Limit Exceeded:**
|
||||
```json
|
||||
{
|
||||
"message": "Too many requests",
|
||||
"error_code": "RATE_LIMIT_EXCEEDED",
|
||||
"limit": 10000,
|
||||
"remaining": 0,
|
||||
"reset_at": "2026-01-26T13:00:00Z",
|
||||
"retry_after": 3600
|
||||
}
|
||||
```
|
||||
|
||||
**Usage Quota Exceeded:**
|
||||
```json
|
||||
{
|
||||
"message": "Monthly usage quota exceeded",
|
||||
"error_code": "QUOTA_EXCEEDED",
|
||||
"quota_type": "monthly",
|
||||
"limit": 50000,
|
||||
"used": 50000,
|
||||
"reset_at": "2026-02-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 500 Internal Server Error
|
||||
|
||||
**Unexpected Error:**
|
||||
```json
|
||||
{
|
||||
"message": "An unexpected error occurred",
|
||||
"error_code": "INTERNAL_ERROR",
|
||||
"meta": {
|
||||
"request_id": "req_abc123",
|
||||
"timestamp": "2026-01-26T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip
|
||||
In production, internal error messages are sanitized. Include the `request_id` when reporting issues for debugging.
|
||||
:::
|
||||
|
||||
## Error Codes
|
||||
|
||||
### Authentication Errors
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `UNAUTHENTICATED` | 401 | No authentication provided |
|
||||
| `INVALID_API_KEY` | 401 | API key is invalid or revoked |
|
||||
| `TOKEN_EXPIRED` | 401 | Authentication token has expired |
|
||||
| `INVALID_CREDENTIALS` | 401 | Username/password incorrect |
|
||||
| `INSUFFICIENT_PERMISSIONS` | 403 | Missing required permissions/scopes |
|
||||
|
||||
### Resource Errors
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `RESOURCE_NOT_FOUND` | 404 | Requested resource doesn't exist |
|
||||
| `DUPLICATE_RESOURCE` | 409 | Resource with identifier already exists |
|
||||
| `RESOURCE_LOCKED` | 409 | Resource is locked by another process |
|
||||
| `STATE_CONFLICT` | 409 | Action conflicts with current state |
|
||||
|
||||
### Validation Errors
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `VALIDATION_FAILED` | 422 | One or more fields failed validation |
|
||||
| `INVALID_TYPE` | 400 | Parameter has wrong data type |
|
||||
| `MISSING_PARAMETER` | 400 | Required parameter not provided |
|
||||
| `INVALID_FORMAT` | 400 | Parameter format is invalid |
|
||||
|
||||
### Rate Limiting Errors
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests in time window |
|
||||
| `QUOTA_EXCEEDED` | 429 | Usage quota exceeded |
|
||||
| `CONCURRENT_LIMIT_EXCEEDED` | 429 | Too many concurrent requests |
|
||||
|
||||
### Business Logic Errors
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `ENTITLEMENT_DENIED` | 403 | Feature not included in plan |
|
||||
| `WORKSPACE_SUSPENDED` | 403 | Workspace is suspended |
|
||||
| `NAMESPACE_ACCESS_DENIED` | 403 | No access to namespace |
|
||||
| `PAYMENT_REQUIRED` | 402 | Payment required to proceed |
|
||||
|
||||
### System Errors
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `INTERNAL_ERROR` | 500 | Unexpected server error |
|
||||
| `SERVICE_UNAVAILABLE` | 503 | Service temporarily unavailable |
|
||||
| `GATEWAY_TIMEOUT` | 504 | Upstream service timeout |
|
||||
| `MAINTENANCE_MODE` | 503 | System under maintenance |
|
||||
|
||||
## Handling Errors
|
||||
|
||||
### JavaScript Example
|
||||
|
||||
```javascript
|
||||
async function createPost(data) {
|
||||
try {
|
||||
const response = await fetch('/api/v1/posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
// Re-authenticate
|
||||
redirectToLogin();
|
||||
break;
|
||||
case 403:
|
||||
// Show permission error
|
||||
showError('You do not have permission to create posts');
|
||||
break;
|
||||
case 422:
|
||||
// Show validation errors
|
||||
showValidationErrors(error.errors);
|
||||
break;
|
||||
case 429:
|
||||
// Show rate limit message
|
||||
showError(`Rate limited. Retry after ${error.retry_after} seconds`);
|
||||
break;
|
||||
default:
|
||||
// Generic error
|
||||
showError(error.message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
// Network error
|
||||
showError('Network error. Please check your connection.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PHP Example
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
$client = new Client(['base_uri' => 'https://api.example.com']);
|
||||
|
||||
try {
|
||||
$response = $client->post('/v1/posts', [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$apiKey}",
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => $data,
|
||||
]);
|
||||
|
||||
$post = json_decode($response->getBody(), true);
|
||||
|
||||
} catch (RequestException $e) {
|
||||
$statusCode = $e->getResponse()->getStatusCode();
|
||||
$error = json_decode($e->getResponse()->getBody(), true);
|
||||
|
||||
switch ($statusCode) {
|
||||
case 401:
|
||||
throw new AuthenticationException($error['message']);
|
||||
case 403:
|
||||
throw new AuthorizationException($error['message']);
|
||||
case 422:
|
||||
throw new ValidationException($error['errors']);
|
||||
case 429:
|
||||
throw new RateLimitException($error['retry_after']);
|
||||
default:
|
||||
throw new ApiException($error['message']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Request ID
|
||||
|
||||
Every response includes a `request_id` for debugging:
|
||||
|
||||
```bash
|
||||
curl -i https://api.example.com/v1/posts
|
||||
```
|
||||
|
||||
Response headers:
|
||||
```
|
||||
X-Request-ID: req_abc123def456
|
||||
```
|
||||
|
||||
Include this ID when reporting issues.
|
||||
|
||||
### Debug Mode
|
||||
|
||||
In development, enable debug mode for detailed errors:
|
||||
|
||||
```php
|
||||
// .env
|
||||
APP_DEBUG=true
|
||||
```
|
||||
|
||||
Debug responses include:
|
||||
- Full stack traces
|
||||
- SQL queries
|
||||
- Exception details
|
||||
|
||||
::: danger
|
||||
Never enable debug mode in production! It exposes sensitive information.
|
||||
:::
|
||||
|
||||
### Logging
|
||||
|
||||
All errors are logged with context:
|
||||
|
||||
```
|
||||
[2026-01-26 12:00:00] production.ERROR: Post not found
|
||||
{
|
||||
"user_id": 123,
|
||||
"workspace_id": 456,
|
||||
"namespace_id": 789,
|
||||
"post_id": 999,
|
||||
"request_id": "req_abc123"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Check Status Codes
|
||||
|
||||
```javascript
|
||||
// ✅ Good
|
||||
if (!response.ok) {
|
||||
handleError(response);
|
||||
}
|
||||
|
||||
// ❌ Bad - assumes success
|
||||
const data = await response.json();
|
||||
```
|
||||
|
||||
### 2. Handle All Error Types
|
||||
|
||||
```javascript
|
||||
// ✅ Good - specific handling
|
||||
switch (error.error_code) {
|
||||
case 'RATE_LIMIT_EXCEEDED':
|
||||
retryAfter(error.retry_after);
|
||||
break;
|
||||
case 'VALIDATION_FAILED':
|
||||
showValidationErrors(error.errors);
|
||||
break;
|
||||
default:
|
||||
showGenericError(error.message);
|
||||
}
|
||||
|
||||
// ❌ Bad - generic handling
|
||||
alert(error.message);
|
||||
```
|
||||
|
||||
### 3. Implement Retry Logic
|
||||
|
||||
```javascript
|
||||
async function fetchWithRetry(url, options, retries = 3) {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (response.status === 429) {
|
||||
// Rate limited - wait and retry
|
||||
const retryAfter = parseInt(response.headers.get('Retry-After'));
|
||||
await sleep(retryAfter * 1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (i === retries - 1) throw err;
|
||||
await sleep(1000 * Math.pow(2, i)); // Exponential backoff
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Log Error Context
|
||||
|
||||
```javascript
|
||||
// ✅ Good - log context
|
||||
console.error('API Error:', {
|
||||
endpoint: '/v1/posts',
|
||||
method: 'POST',
|
||||
status: response.status,
|
||||
error_code: error.error_code,
|
||||
request_id: error.meta.request_id
|
||||
});
|
||||
|
||||
// ❌ Bad - no context
|
||||
console.error(error.message);
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [API Authentication →](/api/authentication)
|
||||
- [Rate Limiting →](/api/endpoints#rate-limiting)
|
||||
- [API Endpoints →](/api/endpoints)
|
||||
BIN
build/.DS_Store
vendored
Normal file
BIN
build/.DS_Store
vendored
Normal file
Binary file not shown.
100
build/cli/ai/example.md
Normal file
100
build/cli/ai/example.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# AI Examples
|
||||
|
||||
## Workflow Example
|
||||
|
||||
Complete task management workflow:
|
||||
|
||||
```bash
|
||||
# 1. List available tasks
|
||||
core ai tasks --status pending
|
||||
|
||||
# 2. Auto-select and claim a task
|
||||
core ai task --auto --claim
|
||||
|
||||
# 3. Work on the task...
|
||||
|
||||
# 4. Update progress
|
||||
core ai task:update abc123 --progress 75
|
||||
|
||||
# 5. Commit with task reference
|
||||
core ai task:commit abc123 -m 'implement feature'
|
||||
|
||||
# 6. Create PR
|
||||
core ai task:pr abc123
|
||||
|
||||
# 7. Mark complete
|
||||
core ai task:complete abc123 --output 'Feature implemented and PR created'
|
||||
```
|
||||
|
||||
## Task Filtering
|
||||
|
||||
```bash
|
||||
# By status
|
||||
core ai tasks --status pending
|
||||
core ai tasks --status in_progress
|
||||
|
||||
# By priority
|
||||
core ai tasks --priority critical
|
||||
core ai tasks --priority high
|
||||
|
||||
# By labels
|
||||
core ai tasks --labels bug,urgent
|
||||
|
||||
# Combined filters
|
||||
core ai tasks --status pending --priority high --labels bug
|
||||
```
|
||||
|
||||
## Task Updates
|
||||
|
||||
```bash
|
||||
# Change status
|
||||
core ai task:update abc123 --status in_progress
|
||||
core ai task:update abc123 --status blocked
|
||||
|
||||
# Update progress
|
||||
core ai task:update abc123 --progress 25
|
||||
core ai task:update abc123 --progress 50 --notes 'Halfway done'
|
||||
core ai task:update abc123 --progress 100
|
||||
```
|
||||
|
||||
## Git Integration
|
||||
|
||||
```bash
|
||||
# Commit with task reference
|
||||
core ai task:commit abc123 -m 'add authentication'
|
||||
|
||||
# With scope
|
||||
core ai task:commit abc123 -m 'fix login' --scope auth
|
||||
|
||||
# Commit and push
|
||||
core ai task:commit abc123 -m 'complete feature' --push
|
||||
|
||||
# Create PR
|
||||
core ai task:pr abc123
|
||||
|
||||
# Draft PR
|
||||
core ai task:pr abc123 --draft
|
||||
|
||||
# PR with labels
|
||||
core ai task:pr abc123 --labels 'enhancement,ready-for-review'
|
||||
|
||||
# PR to different base
|
||||
core ai task:pr abc123 --base develop
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
AGENTIC_TOKEN=your-api-token
|
||||
AGENTIC_BASE_URL=https://agentic.example.com
|
||||
```
|
||||
|
||||
### ~/.core/agentic.yaml
|
||||
|
||||
```yaml
|
||||
token: your-api-token
|
||||
base_url: https://agentic.example.com
|
||||
default_project: my-project
|
||||
```
|
||||
262
build/cli/ai/index.md
Normal file
262
build/cli/ai/index.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# core ai
|
||||
|
||||
AI agent task management and Claude Code integration.
|
||||
|
||||
## Task Management Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `tasks` | List available tasks from core-agentic |
|
||||
| `task` | View task details or auto-select |
|
||||
| `task:update` | Update task status or progress |
|
||||
| `task:complete` | Mark task as completed or failed |
|
||||
| `task:commit` | Create git commit with task reference |
|
||||
| `task:pr` | Create GitHub PR linked to task |
|
||||
|
||||
## Claude Integration
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `claude run` | Run Claude Code in current directory |
|
||||
| `claude config` | Manage Claude configuration |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Task commands load configuration from:
|
||||
1. Environment variables (`AGENTIC_TOKEN`, `AGENTIC_BASE_URL`)
|
||||
2. `.env` file in current directory
|
||||
3. `~/.core/agentic.yaml`
|
||||
|
||||
---
|
||||
|
||||
## ai tasks
|
||||
|
||||
List available tasks from core-agentic.
|
||||
|
||||
```bash
|
||||
core ai tasks [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--status` | Filter by status (`pending`, `in_progress`, `completed`, `blocked`) |
|
||||
| `--priority` | Filter by priority (`critical`, `high`, `medium`, `low`) |
|
||||
| `--labels` | Filter by labels (comma-separated) |
|
||||
| `--project` | Filter by project |
|
||||
| `--limit` | Max number of tasks to return (default: 20) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# List all pending tasks
|
||||
core ai tasks
|
||||
|
||||
# Filter by status and priority
|
||||
core ai tasks --status pending --priority high
|
||||
|
||||
# Filter by labels
|
||||
core ai tasks --labels bug,urgent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ai task
|
||||
|
||||
View task details or auto-select a task.
|
||||
|
||||
```bash
|
||||
core ai task [task-id] [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--auto` | Auto-select highest priority pending task |
|
||||
| `--claim` | Claim the task after showing details |
|
||||
| `--context` | Show gathered context for AI collaboration |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Show task details
|
||||
core ai task abc123
|
||||
|
||||
# Show and claim
|
||||
core ai task abc123 --claim
|
||||
|
||||
# Show with context
|
||||
core ai task abc123 --context
|
||||
|
||||
# Auto-select highest priority pending task
|
||||
core ai task --auto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ai task:update
|
||||
|
||||
Update a task's status, progress, or notes.
|
||||
|
||||
```bash
|
||||
core ai task:update <task-id> [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--status` | New status (`pending`, `in_progress`, `completed`, `blocked`) |
|
||||
| `--progress` | Progress percentage (0-100) |
|
||||
| `--notes` | Notes about the update |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Set task to in progress
|
||||
core ai task:update abc123 --status in_progress
|
||||
|
||||
# Update progress with notes
|
||||
core ai task:update abc123 --progress 50 --notes 'Halfway done'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ai task:complete
|
||||
|
||||
Mark a task as completed with optional output and artifacts.
|
||||
|
||||
```bash
|
||||
core ai task:complete <task-id> [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--output` | Summary of the completed work |
|
||||
| `--failed` | Mark the task as failed |
|
||||
| `--error` | Error message if failed |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Complete successfully
|
||||
core ai task:complete abc123 --output 'Feature implemented'
|
||||
|
||||
# Mark as failed
|
||||
core ai task:complete abc123 --failed --error 'Build failed'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ai task:commit
|
||||
|
||||
Create a git commit with a task reference and co-author attribution.
|
||||
|
||||
```bash
|
||||
core ai task:commit <task-id> [flags]
|
||||
```
|
||||
|
||||
Commit message format:
|
||||
```
|
||||
feat(scope): description
|
||||
|
||||
Task: #123
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-m`, `--message` | Commit message (without task reference) |
|
||||
| `--scope` | Scope for the commit type (e.g., `auth`, `api`, `ui`) |
|
||||
| `--push` | Push changes after committing |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Commit with message
|
||||
core ai task:commit abc123 --message 'add user authentication'
|
||||
|
||||
# With scope
|
||||
core ai task:commit abc123 -m 'fix login bug' --scope auth
|
||||
|
||||
# Commit and push
|
||||
core ai task:commit abc123 -m 'update docs' --push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ai task:pr
|
||||
|
||||
Create a GitHub pull request linked to a task.
|
||||
|
||||
```bash
|
||||
core ai task:pr <task-id> [flags]
|
||||
```
|
||||
|
||||
Requires the GitHub CLI (`gh`) to be installed and authenticated.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--title` | PR title (defaults to task title) |
|
||||
| `--base` | Base branch (defaults to main) |
|
||||
| `--draft` | Create as draft PR |
|
||||
| `--labels` | Labels to add (comma-separated) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Create PR with defaults
|
||||
core ai task:pr abc123
|
||||
|
||||
# Custom title
|
||||
core ai task:pr abc123 --title 'Add authentication feature'
|
||||
|
||||
# Draft PR with labels
|
||||
core ai task:pr abc123 --draft --labels 'enhancement,needs-review'
|
||||
|
||||
# Target different base branch
|
||||
core ai task:pr abc123 --base develop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ai claude
|
||||
|
||||
Claude Code integration commands.
|
||||
|
||||
### ai claude run
|
||||
|
||||
Run Claude Code in the current directory.
|
||||
|
||||
```bash
|
||||
core ai claude run
|
||||
```
|
||||
|
||||
### ai claude config
|
||||
|
||||
Manage Claude configuration.
|
||||
|
||||
```bash
|
||||
core ai claude config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Example
|
||||
|
||||
See [Workflow Example](example.md#workflow-example) for a complete task management workflow.
|
||||
|
||||
## See Also
|
||||
|
||||
- [dev](../dev/) - Multi-repo workflow commands
|
||||
- [Claude Code documentation](https://claude.ai/code)
|
||||
83
build/cli/build/example.md
Normal file
83
build/cli/build/example.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Build Examples
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Auto-detect and build
|
||||
core build
|
||||
|
||||
# Build for specific platforms
|
||||
core build --targets linux/amd64,darwin/arm64
|
||||
|
||||
# CI mode
|
||||
core build --ci
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`.core/build.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: myapp
|
||||
binary: myapp
|
||||
|
||||
build:
|
||||
main: ./cmd/myapp
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
|
||||
targets:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
```
|
||||
|
||||
## Cross-Platform Build
|
||||
|
||||
```bash
|
||||
core build --targets linux/amd64,linux/arm64,darwin/arm64,windows/amd64
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
dist/
|
||||
├── myapp-linux-amd64.tar.gz
|
||||
├── myapp-linux-arm64.tar.gz
|
||||
├── myapp-darwin-arm64.tar.gz
|
||||
├── myapp-windows-amd64.zip
|
||||
└── CHECKSUMS.txt
|
||||
```
|
||||
|
||||
## Code Signing
|
||||
|
||||
```yaml
|
||||
sign:
|
||||
enabled: true
|
||||
gpg:
|
||||
key: $GPG_KEY_ID
|
||||
macos:
|
||||
identity: "Developer ID Application: Your Name (TEAM_ID)"
|
||||
notarize: true
|
||||
apple_id: $APPLE_ID
|
||||
team_id: $APPLE_TEAM_ID
|
||||
app_password: $APPLE_APP_PASSWORD
|
||||
```
|
||||
|
||||
## Docker Build
|
||||
|
||||
```bash
|
||||
core build --type docker --image ghcr.io/myorg/myapp
|
||||
```
|
||||
|
||||
## Wails Desktop App
|
||||
|
||||
```bash
|
||||
core build --type wails --targets darwin/arm64,windows/amd64
|
||||
```
|
||||
176
build/cli/build/index.md
Normal file
176
build/cli/build/index.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# core build
|
||||
|
||||
Build Go, Wails, Docker, and LinuxKit projects with automatic project detection.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [sdk](sdk/) | Generate API SDKs from OpenAPI |
|
||||
| `from-path` | Build from a local directory |
|
||||
| `pwa` | Build from a live PWA URL |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core build [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--type` | Project type: `go`, `wails`, `docker`, `linuxkit`, `taskfile` (auto-detected) |
|
||||
| `--targets` | Build targets: `linux/amd64,darwin/arm64,windows/amd64` |
|
||||
| `--output` | Output directory (default: `dist`) |
|
||||
| `--ci` | CI mode - minimal output with JSON artifact list at the end |
|
||||
| `--image` | Docker image name (for docker builds) |
|
||||
| `--config` | Config file path (for linuxkit: YAML config, for docker: Dockerfile) |
|
||||
| `--format` | Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk) |
|
||||
| `--push` | Push Docker image after build (default: false) |
|
||||
| `--archive` | Create archives (tar.gz for linux/darwin, zip for windows) - default: true |
|
||||
| `--checksum` | Generate SHA256 checksums and CHECKSUMS.txt - default: true |
|
||||
| `--no-sign` | Skip all code signing |
|
||||
| `--notarize` | Enable macOS notarization (requires Apple credentials) |
|
||||
|
||||
## Examples
|
||||
|
||||
### Go Project
|
||||
|
||||
```bash
|
||||
# Auto-detect and build
|
||||
core build
|
||||
|
||||
# Build for specific platforms
|
||||
core build --targets linux/amd64,linux/arm64,darwin/arm64
|
||||
|
||||
# CI mode
|
||||
core build --ci
|
||||
```
|
||||
|
||||
### Wails Project
|
||||
|
||||
```bash
|
||||
# Build Wails desktop app
|
||||
core build --type wails
|
||||
|
||||
# Build for all desktop platforms
|
||||
core build --type wails --targets darwin/amd64,darwin/arm64,windows/amd64,linux/amd64
|
||||
```
|
||||
|
||||
### Docker Image
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
core build --type docker
|
||||
|
||||
# With custom image name
|
||||
core build --type docker --image ghcr.io/myorg/myapp
|
||||
|
||||
# Build and push to registry
|
||||
core build --type docker --image ghcr.io/myorg/myapp --push
|
||||
```
|
||||
|
||||
### LinuxKit Image
|
||||
|
||||
```bash
|
||||
# Build LinuxKit ISO
|
||||
core build --type linuxkit
|
||||
|
||||
# Build with specific format
|
||||
core build --type linuxkit --config linuxkit.yml --format qcow2-bios
|
||||
```
|
||||
|
||||
## Project Detection
|
||||
|
||||
Core automatically detects project type based on files:
|
||||
|
||||
| Files | Type |
|
||||
|-------|------|
|
||||
| `wails.json` | Wails |
|
||||
| `go.mod` | Go |
|
||||
| `Dockerfile` | Docker |
|
||||
| `Taskfile.yml` | Taskfile |
|
||||
| `composer.json` | PHP |
|
||||
| `package.json` | Node |
|
||||
|
||||
## Output
|
||||
|
||||
Build artifacts are placed in `dist/` by default:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── myapp-linux-amd64.tar.gz
|
||||
├── myapp-linux-arm64.tar.gz
|
||||
├── myapp-darwin-amd64.tar.gz
|
||||
├── myapp-darwin-arm64.tar.gz
|
||||
├── myapp-windows-amd64.zip
|
||||
└── CHECKSUMS.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Optional `.core/build.yaml` - see [Configuration](example.md#configuration) for examples.
|
||||
|
||||
## Code Signing
|
||||
|
||||
Core supports GPG signing for checksums and native code signing for macOS.
|
||||
|
||||
### GPG Signing
|
||||
|
||||
Signs `CHECKSUMS.txt` with a detached ASCII signature (`.asc`):
|
||||
|
||||
```bash
|
||||
# Build with GPG signing (default if key configured)
|
||||
core build
|
||||
|
||||
# Skip signing
|
||||
core build --no-sign
|
||||
```
|
||||
|
||||
Users can verify:
|
||||
|
||||
```bash
|
||||
gpg --verify CHECKSUMS.txt.asc CHECKSUMS.txt
|
||||
sha256sum -c CHECKSUMS.txt
|
||||
```
|
||||
|
||||
### macOS Code Signing
|
||||
|
||||
Signs Darwin binaries with your Developer ID and optionally notarizes with Apple:
|
||||
|
||||
```bash
|
||||
# Build with codesign (automatic if identity configured)
|
||||
core build
|
||||
|
||||
# Build with notarization (takes 1-5 minutes)
|
||||
core build --notarize
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `GPG_KEY_ID` | GPG key ID or fingerprint |
|
||||
| `CODESIGN_IDENTITY` | macOS Developer ID (fallback) |
|
||||
| `APPLE_ID` | Apple account email |
|
||||
| `APPLE_TEAM_ID` | Apple Developer Team ID |
|
||||
| `APPLE_APP_PASSWORD` | App-specific password for notarization |
|
||||
|
||||
## Building from PWAs and Static Sites
|
||||
|
||||
### Build from Local Directory
|
||||
|
||||
Build a desktop app from static web application files:
|
||||
|
||||
```bash
|
||||
core build from-path --path ./dist
|
||||
```
|
||||
|
||||
### Build from Live PWA
|
||||
|
||||
Build a desktop app from a live Progressive Web App URL:
|
||||
|
||||
```bash
|
||||
core build pwa --url https://example.com
|
||||
```
|
||||
56
build/cli/build/sdk/example.md
Normal file
56
build/cli/build/sdk/example.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# SDK Build Examples
|
||||
|
||||
## Generate All SDKs
|
||||
|
||||
```bash
|
||||
core build sdk
|
||||
```
|
||||
|
||||
## Specific Language
|
||||
|
||||
```bash
|
||||
core build sdk --lang typescript
|
||||
core build sdk --lang php
|
||||
core build sdk --lang go
|
||||
```
|
||||
|
||||
## Custom Spec
|
||||
|
||||
```bash
|
||||
core build sdk --spec ./api/openapi.yaml
|
||||
```
|
||||
|
||||
## With Version
|
||||
|
||||
```bash
|
||||
core build sdk --version v2.0.0
|
||||
```
|
||||
|
||||
## Preview
|
||||
|
||||
```bash
|
||||
core build sdk --dry-run
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`.core/sdk.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
spec: ./api/openapi.yaml
|
||||
|
||||
languages:
|
||||
- name: typescript
|
||||
output: sdk/typescript
|
||||
package: "@myorg/api-client"
|
||||
|
||||
- name: php
|
||||
output: sdk/php
|
||||
namespace: MyOrg\ApiClient
|
||||
|
||||
- name: go
|
||||
output: sdk/go
|
||||
module: github.com/myorg/api-client-go
|
||||
```
|
||||
27
build/cli/build/sdk/index.md
Normal file
27
build/cli/build/sdk/index.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# core build sdk
|
||||
|
||||
Generate typed API clients from OpenAPI specifications. Supports TypeScript, Python, Go, and PHP.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core build sdk [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--spec` | Path to OpenAPI spec file |
|
||||
| `--lang` | Generate only this language (typescript, python, go, php) |
|
||||
| `--version` | Version to embed in generated SDKs |
|
||||
| `--dry-run` | Show what would be generated without writing files |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core build sdk # Generate all
|
||||
core build sdk --lang typescript # TypeScript only
|
||||
core build sdk --spec ./api.yaml # Custom spec
|
||||
core build sdk --dry-run # Preview
|
||||
```
|
||||
36
build/cli/ci/changelog/example.md
Normal file
36
build/cli/ci/changelog/example.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# CI Changelog Examples
|
||||
|
||||
```bash
|
||||
core ci changelog
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```markdown
|
||||
## v1.2.0
|
||||
|
||||
### Features
|
||||
- Add user authentication (#123)
|
||||
- Support dark mode (#124)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix memory leak in worker (#125)
|
||||
|
||||
### Performance
|
||||
- Optimize database queries (#126)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`.core/release.yaml`:
|
||||
|
||||
```yaml
|
||||
changelog:
|
||||
include:
|
||||
- feat
|
||||
- fix
|
||||
- perf
|
||||
exclude:
|
||||
- chore
|
||||
- docs
|
||||
```
|
||||
28
build/cli/ci/changelog/index.md
Normal file
28
build/cli/ci/changelog/index.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# core ci changelog
|
||||
|
||||
Generate changelog from conventional commits.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core ci changelog
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Generates markdown changelog from git commits since last tag:
|
||||
|
||||
```markdown
|
||||
## v1.2.0
|
||||
|
||||
### Features
|
||||
- Add user authentication (#123)
|
||||
- Support dark mode (#124)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix memory leak in worker (#125)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
See [configuration.md](../../../configuration.md) for changelog configuration options.
|
||||
90
build/cli/ci/example.md
Normal file
90
build/cli/ci/example.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# CI Examples
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build first
|
||||
core build
|
||||
|
||||
# Preview release
|
||||
core ci
|
||||
|
||||
# Publish
|
||||
core ci --we-are-go-for-launch
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`.core/release.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: myapp
|
||||
repository: host-uk/myapp
|
||||
|
||||
publishers:
|
||||
- type: github
|
||||
```
|
||||
|
||||
## Publisher Examples
|
||||
|
||||
### GitHub + Docker
|
||||
|
||||
```yaml
|
||||
publishers:
|
||||
- type: github
|
||||
|
||||
- type: docker
|
||||
registry: ghcr.io
|
||||
image: host-uk/myapp
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
tags:
|
||||
- latest
|
||||
- "{{.Version}}"
|
||||
```
|
||||
|
||||
### Full Stack (GitHub + npm + Homebrew)
|
||||
|
||||
```yaml
|
||||
publishers:
|
||||
- type: github
|
||||
|
||||
- type: npm
|
||||
package: "@host-uk/myapp"
|
||||
access: public
|
||||
|
||||
- type: homebrew
|
||||
tap: host-uk/homebrew-tap
|
||||
```
|
||||
|
||||
### LinuxKit Image
|
||||
|
||||
```yaml
|
||||
publishers:
|
||||
- type: linuxkit
|
||||
config: .core/linuxkit/server.yml
|
||||
formats:
|
||||
- iso
|
||||
- qcow2
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
```
|
||||
|
||||
## Changelog Configuration
|
||||
|
||||
```yaml
|
||||
changelog:
|
||||
include:
|
||||
- feat
|
||||
- fix
|
||||
- perf
|
||||
exclude:
|
||||
- chore
|
||||
- docs
|
||||
- test
|
||||
```
|
||||
79
build/cli/ci/index.md
Normal file
79
build/cli/ci/index.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# core ci
|
||||
|
||||
Publish releases to GitHub, Docker, npm, Homebrew, and more.
|
||||
|
||||
**Safety:** Dry-run by default. Use `--we-are-go-for-launch` to actually publish.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [init](init/) | Initialize release config |
|
||||
| [changelog](changelog/) | Generate changelog |
|
||||
| [version](version/) | Show determined version |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core ci [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--we-are-go-for-launch` | Actually publish (default is dry-run) |
|
||||
| `--version` | Override version |
|
||||
| `--draft` | Create as draft release |
|
||||
| `--prerelease` | Mark as prerelease |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Preview what would be published (safe)
|
||||
core ci
|
||||
|
||||
# Actually publish
|
||||
core ci --we-are-go-for-launch
|
||||
|
||||
# Publish as draft
|
||||
core ci --we-are-go-for-launch --draft
|
||||
|
||||
# Publish as prerelease
|
||||
core ci --we-are-go-for-launch --prerelease
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
Build and publish are **separated** to prevent accidents:
|
||||
|
||||
```bash
|
||||
# Step 1: Build artifacts
|
||||
core build
|
||||
core build sdk
|
||||
|
||||
# Step 2: Preview (dry-run by default)
|
||||
core ci
|
||||
|
||||
# Step 3: Publish (explicit flag required)
|
||||
core ci --we-are-go-for-launch
|
||||
```
|
||||
|
||||
## Publishers
|
||||
|
||||
See [Publisher Examples](example.md#publisher-examples) for configuration.
|
||||
|
||||
| Type | Target |
|
||||
|------|--------|
|
||||
| `github` | GitHub Releases |
|
||||
| `docker` | Container registries |
|
||||
| `linuxkit` | LinuxKit images |
|
||||
| `npm` | npm registry |
|
||||
| `homebrew` | Homebrew tap |
|
||||
| `scoop` | Scoop bucket |
|
||||
| `aur` | Arch User Repository |
|
||||
| `chocolatey` | Chocolatey |
|
||||
|
||||
## Changelog
|
||||
|
||||
Auto-generated from conventional commits. See [Changelog Configuration](example.md#changelog-configuration).
|
||||
17
build/cli/ci/init/example.md
Normal file
17
build/cli/ci/init/example.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# CI Init Examples
|
||||
|
||||
```bash
|
||||
core ci init
|
||||
```
|
||||
|
||||
Creates `.core/release.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: myapp
|
||||
|
||||
publishers:
|
||||
- type: github
|
||||
```
|
||||
11
build/cli/ci/init/index.md
Normal file
11
build/cli/ci/init/index.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# core ci init
|
||||
|
||||
Initialize release configuration.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core ci init
|
||||
```
|
||||
|
||||
Creates `.core/release.yaml` with default configuration. See [Configuration](../example.md#configuration) for output format.
|
||||
18
build/cli/ci/version/example.md
Normal file
18
build/cli/ci/version/example.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# CI Version Examples
|
||||
|
||||
```bash
|
||||
core ci version
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
v1.2.0
|
||||
```
|
||||
|
||||
## Version Resolution
|
||||
|
||||
1. `--version` flag (if provided)
|
||||
2. Git tag on HEAD
|
||||
3. Latest git tag + increment
|
||||
4. `v0.0.1` (no tags)
|
||||
21
build/cli/ci/version/index.md
Normal file
21
build/cli/ci/version/index.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# core ci version
|
||||
|
||||
Show the determined release version.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core ci version
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
v1.2.0
|
||||
```
|
||||
|
||||
Version is determined from:
|
||||
1. `--version` flag (if provided)
|
||||
2. Git tag on HEAD
|
||||
3. Latest git tag + increment
|
||||
4. `v0.0.1` (if no tags exist)
|
||||
61
build/cli/dev/ci/index.md
Normal file
61
build/cli/dev/ci/index.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# core dev ci
|
||||
|
||||
Check CI status across all repositories.
|
||||
|
||||
Fetches GitHub Actions workflow status for all repos. Shows latest run status for each repo. Requires the `gh` CLI to be installed and authenticated.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev ci [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--branch` | Filter by branch (default: main) |
|
||||
| `--failed` | Show only failed runs |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Check CI status for all repos
|
||||
core dev ci
|
||||
|
||||
# Check specific branch
|
||||
core dev ci --branch develop
|
||||
|
||||
# Show only failures
|
||||
core dev ci --failed
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
core-php ✓ passing 2m ago
|
||||
core-tenant ✓ passing 5m ago
|
||||
core-admin ✗ failed 12m ago
|
||||
core-api ⏳ running now
|
||||
core-bio ✓ passing 1h ago
|
||||
```
|
||||
|
||||
## Status Icons
|
||||
|
||||
| Symbol | Meaning |
|
||||
|--------|---------|
|
||||
| `✓` | Passing |
|
||||
| `✗` | Failed |
|
||||
| `⏳` | Running |
|
||||
| `-` | No runs |
|
||||
|
||||
## Requirements
|
||||
|
||||
- GitHub CLI (`gh`) must be installed
|
||||
- Must be authenticated: `gh auth login`
|
||||
|
||||
## See Also
|
||||
|
||||
- [issues command](../issues/) - List open issues
|
||||
- [reviews command](../reviews/) - List PRs needing review
|
||||
46
build/cli/dev/commit/index.md
Normal file
46
build/cli/dev/commit/index.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# core dev commit
|
||||
|
||||
Claude-assisted commits across repositories.
|
||||
|
||||
Uses Claude to create commits for dirty repos. Shows uncommitted changes and invokes Claude to generate commit messages.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev commit [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--all` | Commit all dirty repos without prompting |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Interactive commit (prompts for each repo)
|
||||
core dev commit
|
||||
|
||||
# Commit all dirty repos automatically
|
||||
core dev commit --all
|
||||
|
||||
# Use specific registry
|
||||
core dev commit --registry ~/projects/repos.yaml
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Scans all repositories for uncommitted changes
|
||||
2. For each dirty repo:
|
||||
- Shows the diff
|
||||
- Invokes Claude to generate a commit message
|
||||
- Creates the commit with `Co-Authored-By: Claude`
|
||||
3. Reports success/failure for each repo
|
||||
|
||||
## See Also
|
||||
|
||||
- [health command](../health/) - Check repo status
|
||||
- [push command](../push/) - Push commits after committing
|
||||
- [work command](../work/) - Full workflow (status + commit + push)
|
||||
203
build/cli/dev/example.md
Normal file
203
build/cli/dev/example.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# Dev Examples
|
||||
|
||||
## Multi-Repo Workflow
|
||||
|
||||
```bash
|
||||
# Quick status
|
||||
core dev health
|
||||
|
||||
# Detailed breakdown
|
||||
core dev health --verbose
|
||||
|
||||
# Full workflow
|
||||
core dev work
|
||||
|
||||
# Status only
|
||||
core dev work --status
|
||||
|
||||
# Commit and push
|
||||
core dev work --commit
|
||||
|
||||
# Commit dirty repos
|
||||
core dev commit
|
||||
|
||||
# Commit all without prompting
|
||||
core dev commit --all
|
||||
|
||||
# Push unpushed
|
||||
core dev push
|
||||
|
||||
# Push without confirmation
|
||||
core dev push --force
|
||||
|
||||
# Pull behind repos
|
||||
core dev pull
|
||||
|
||||
# Pull all repos
|
||||
core dev pull --all
|
||||
```
|
||||
|
||||
## GitHub Integration
|
||||
|
||||
```bash
|
||||
# Open issues
|
||||
core dev issues
|
||||
|
||||
# Filter by assignee
|
||||
core dev issues --assignee @me
|
||||
|
||||
# Limit results
|
||||
core dev issues --limit 5
|
||||
|
||||
# PRs needing review
|
||||
core dev reviews
|
||||
|
||||
# All PRs including drafts
|
||||
core dev reviews --all
|
||||
|
||||
# Filter by author
|
||||
core dev reviews --author username
|
||||
|
||||
# CI status
|
||||
core dev ci
|
||||
|
||||
# Only failed runs
|
||||
core dev ci --failed
|
||||
|
||||
# Specific branch
|
||||
core dev ci --branch develop
|
||||
```
|
||||
|
||||
## Dependency Analysis
|
||||
|
||||
```bash
|
||||
# What depends on core-php?
|
||||
core dev impact core-php
|
||||
```
|
||||
|
||||
## Task Management
|
||||
|
||||
```bash
|
||||
# List tasks
|
||||
core ai tasks
|
||||
|
||||
# Filter by status and priority
|
||||
core ai tasks --status pending --priority high
|
||||
|
||||
# Filter by labels
|
||||
core ai tasks --labels bug,urgent
|
||||
|
||||
# Show task details
|
||||
core ai task abc123
|
||||
|
||||
# Auto-select highest priority task
|
||||
core ai task --auto
|
||||
|
||||
# Claim a task
|
||||
core ai task abc123 --claim
|
||||
|
||||
# Update task status
|
||||
core ai task:update abc123 --status in_progress
|
||||
|
||||
# Add progress notes
|
||||
core ai task:update abc123 --progress 50 --notes 'Halfway done'
|
||||
|
||||
# Complete a task
|
||||
core ai task:complete abc123 --output 'Feature implemented'
|
||||
|
||||
# Mark as failed
|
||||
core ai task:complete abc123 --failed --error 'Build failed'
|
||||
|
||||
# Commit with task reference
|
||||
core ai task:commit abc123 -m 'add user authentication'
|
||||
|
||||
# Commit with scope and push
|
||||
core ai task:commit abc123 -m 'fix login bug' --scope auth --push
|
||||
|
||||
# Create PR for task
|
||||
core ai task:pr abc123
|
||||
|
||||
# Create draft PR with labels
|
||||
core ai task:pr abc123 --draft --labels 'enhancement,needs-review'
|
||||
```
|
||||
|
||||
## Service API Management
|
||||
|
||||
```bash
|
||||
# Synchronize public service APIs
|
||||
core dev sync
|
||||
|
||||
# Or using the api command
|
||||
core dev api sync
|
||||
```
|
||||
|
||||
## Dev Environment
|
||||
|
||||
```bash
|
||||
# First time setup
|
||||
core dev install
|
||||
core dev boot
|
||||
|
||||
# Open shell
|
||||
core dev shell
|
||||
|
||||
# Mount and serve
|
||||
core dev serve
|
||||
|
||||
# Run tests
|
||||
core dev test
|
||||
|
||||
# Sandboxed Claude
|
||||
core dev claude
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### repos.yaml
|
||||
|
||||
```yaml
|
||||
org: host-uk
|
||||
repos:
|
||||
core-php:
|
||||
type: package
|
||||
description: Foundation framework
|
||||
core-tenant:
|
||||
type: package
|
||||
depends: [core-php]
|
||||
```
|
||||
|
||||
### ~/.core/config.yaml
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
images:
|
||||
source: auto # auto | github | registry | cdn
|
||||
|
||||
cdn:
|
||||
url: https://images.example.com/core-devops
|
||||
|
||||
github:
|
||||
repo: host-uk/core-images
|
||||
|
||||
registry:
|
||||
image: ghcr.io/host-uk/core-devops
|
||||
```
|
||||
|
||||
### .core/test.yaml
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
commands:
|
||||
- name: unit
|
||||
run: vendor/bin/pest --parallel
|
||||
- name: types
|
||||
run: vendor/bin/phpstan analyse
|
||||
- name: lint
|
||||
run: vendor/bin/pint --test
|
||||
|
||||
env:
|
||||
APP_ENV: testing
|
||||
DB_CONNECTION: sqlite
|
||||
```
|
||||
52
build/cli/dev/health/index.md
Normal file
52
build/cli/dev/health/index.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# core dev health
|
||||
|
||||
Quick health check across all repositories.
|
||||
|
||||
Shows a summary of repository health: total repos, dirty repos, unpushed commits, etc.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev health [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--verbose` | Show detailed breakdown |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Quick health summary
|
||||
core dev health
|
||||
|
||||
# Detailed breakdown
|
||||
core dev health --verbose
|
||||
|
||||
# Use specific registry
|
||||
core dev health --registry ~/projects/repos.yaml
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
18 repos │ 2 dirty │ 1 ahead │ all synced
|
||||
```
|
||||
|
||||
With `--verbose`:
|
||||
|
||||
```
|
||||
Repos: 18
|
||||
Dirty: 2 (core-php, core-admin)
|
||||
Ahead: 1 (core-tenant)
|
||||
Behind: 0
|
||||
Synced: ✓
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [work command](../work/) - Full workflow (status + commit + push)
|
||||
- [commit command](../commit/) - Claude-assisted commits
|
||||
65
build/cli/dev/impact/index.md
Normal file
65
build/cli/dev/impact/index.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# core dev impact
|
||||
|
||||
Show impact of changing a repository.
|
||||
|
||||
Analyses the dependency graph to show which repos would be affected by changes to the specified repo.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev impact <repo-name> [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Show what depends on core-php
|
||||
core dev impact core-php
|
||||
|
||||
# Show what depends on core-tenant
|
||||
core dev impact core-tenant
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
Impact of changes to core-php:
|
||||
|
||||
Direct dependents (5):
|
||||
core-tenant
|
||||
core-admin
|
||||
core-api
|
||||
core-mcp
|
||||
core-commerce
|
||||
|
||||
Indirect dependents (12):
|
||||
core-bio (via core-tenant)
|
||||
core-social (via core-tenant)
|
||||
core-analytics (via core-tenant)
|
||||
core-notify (via core-tenant)
|
||||
core-trust (via core-tenant)
|
||||
core-support (via core-tenant)
|
||||
core-content (via core-tenant)
|
||||
core-developer (via core-tenant)
|
||||
core-agentic (via core-mcp)
|
||||
...
|
||||
|
||||
Total: 17 repos affected
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Before making breaking changes, see what needs updating
|
||||
- Plan release order based on dependency graph
|
||||
- Understand the ripple effect of changes
|
||||
|
||||
## See Also
|
||||
|
||||
- [health command](../health/) - Quick repo status
|
||||
- [setup command](../../setup/) - Clone repos with dependencies
|
||||
388
build/cli/dev/index.md
Normal file
388
build/cli/dev/index.md
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# core dev
|
||||
|
||||
Multi-repo workflow and portable development environment.
|
||||
|
||||
## Multi-Repo Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [work](work/) | Full workflow: status + commit + push |
|
||||
| `health` | Quick health check across repos |
|
||||
| `commit` | Claude-assisted commits |
|
||||
| `push` | Push repos with unpushed commits |
|
||||
| `pull` | Pull repos that are behind |
|
||||
| `issues` | List open issues |
|
||||
| `reviews` | List PRs needing review |
|
||||
| `ci` | Check CI status |
|
||||
| `impact` | Show dependency impact |
|
||||
| `api` | Tools for managing service APIs |
|
||||
| `sync` | Synchronize public service APIs |
|
||||
|
||||
## Task Management Commands
|
||||
|
||||
> **Note:** Task management commands have moved to [`core ai`](../ai/).
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`ai tasks`](../ai/) | List available tasks from core-agentic |
|
||||
| [`ai task`](../ai/) | Show task details or auto-select a task |
|
||||
| [`ai task:update`](../ai/) | Update task status or progress |
|
||||
| [`ai task:complete`](../ai/) | Mark a task as completed |
|
||||
| [`ai task:commit`](../ai/) | Auto-commit changes with task reference |
|
||||
| [`ai task:pr`](../ai/) | Create a pull request for a task |
|
||||
|
||||
## Dev Environment Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `install` | Download the core-devops image |
|
||||
| `boot` | Start the environment |
|
||||
| `stop` | Stop the environment |
|
||||
| `status` | Show status |
|
||||
| `shell` | Open shell |
|
||||
| `serve` | Start dev server |
|
||||
| `test` | Run tests |
|
||||
| `claude` | Sandboxed Claude |
|
||||
| `update` | Update image |
|
||||
|
||||
---
|
||||
|
||||
## Dev Environment Overview
|
||||
|
||||
Core DevOps provides a sandboxed, immutable development environment based on LinuxKit with 100+ embedded tools.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# First time setup
|
||||
core dev install
|
||||
core dev boot
|
||||
|
||||
# Open shell
|
||||
core dev shell
|
||||
|
||||
# Or mount current project and serve
|
||||
core dev serve
|
||||
```
|
||||
|
||||
## dev install
|
||||
|
||||
Download the core-devops image for your platform.
|
||||
|
||||
```bash
|
||||
core dev install
|
||||
```
|
||||
|
||||
Downloads the platform-specific dev environment image including Go, PHP, Node.js, Python, Docker, and Claude CLI. Downloads are cached at `~/.core/images/`.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Download image (auto-detects platform)
|
||||
core dev install
|
||||
```
|
||||
|
||||
## dev boot
|
||||
|
||||
Start the development environment.
|
||||
|
||||
```bash
|
||||
core dev boot [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--memory` | Memory allocation in MB (default: 4096) |
|
||||
| `--cpus` | Number of CPUs (default: 2) |
|
||||
| `--fresh` | Stop existing and start fresh |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Start with defaults
|
||||
core dev boot
|
||||
|
||||
# More resources
|
||||
core dev boot --memory 8192 --cpus 4
|
||||
|
||||
# Fresh start
|
||||
core dev boot --fresh
|
||||
```
|
||||
|
||||
## dev shell
|
||||
|
||||
Open a shell in the running environment.
|
||||
|
||||
```bash
|
||||
core dev shell [flags] [-- command]
|
||||
```
|
||||
|
||||
Uses SSH by default, or serial console with `--console`.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--console` | Use serial console instead of SSH |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# SSH into environment
|
||||
core dev shell
|
||||
|
||||
# Serial console (for debugging)
|
||||
core dev shell --console
|
||||
|
||||
# Run a command
|
||||
core dev shell -- ls -la
|
||||
```
|
||||
|
||||
## dev serve
|
||||
|
||||
Mount current directory and start the appropriate dev server.
|
||||
|
||||
```bash
|
||||
core dev serve [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--port` | Port to expose (default: 8000) |
|
||||
| `--path` | Subdirectory to serve |
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
| Project | Server Command |
|
||||
|---------|---------------|
|
||||
| Laravel (`artisan`) | `php artisan octane:start` |
|
||||
| Node (`package.json` with `dev` script) | `npm run dev` |
|
||||
| PHP (`composer.json`) | `frankenphp php-server` |
|
||||
| Other | `python -m http.server` |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Auto-detect and serve
|
||||
core dev serve
|
||||
|
||||
# Custom port
|
||||
core dev serve --port 3000
|
||||
```
|
||||
|
||||
## dev test
|
||||
|
||||
Run tests inside the environment.
|
||||
|
||||
```bash
|
||||
core dev test [flags] [-- custom command]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--name` | Run named test command from `.core/test.yaml` |
|
||||
|
||||
### Test Detection
|
||||
|
||||
Core auto-detects the test framework or uses `.core/test.yaml`:
|
||||
|
||||
1. `.core/test.yaml` - Custom config
|
||||
2. `composer.json` → `composer test`
|
||||
3. `package.json` → `npm test`
|
||||
4. `go.mod` → `go test ./...`
|
||||
5. `pytest.ini` → `pytest`
|
||||
6. `Taskfile.yaml` → `task test`
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Auto-detect and run tests
|
||||
core dev test
|
||||
|
||||
# Run named test from config
|
||||
core dev test --name integration
|
||||
|
||||
# Custom command
|
||||
core dev test -- go test -v ./pkg/...
|
||||
```
|
||||
|
||||
### Test Configuration
|
||||
|
||||
Create `.core/test.yaml` for custom test setup - see [Configuration](example.md#configuration) for examples.
|
||||
|
||||
## dev claude
|
||||
|
||||
Start a sandboxed Claude session with your project mounted.
|
||||
|
||||
```bash
|
||||
core dev claude [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--model` | Model to use (`opus`, `sonnet`) |
|
||||
| `--no-auth` | Don't forward any auth credentials |
|
||||
| `--auth` | Selective auth forwarding (`gh`, `anthropic`, `ssh`, `git`) |
|
||||
|
||||
### What Gets Forwarded
|
||||
|
||||
By default, these are forwarded to the sandbox:
|
||||
- `~/.anthropic/` or `ANTHROPIC_API_KEY`
|
||||
- `~/.config/gh/` (GitHub CLI auth)
|
||||
- SSH agent
|
||||
- Git config (name, email)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Full auth forwarding (default)
|
||||
core dev claude
|
||||
|
||||
# Use Opus model
|
||||
core dev claude --model opus
|
||||
|
||||
# Clean sandbox
|
||||
core dev claude --no-auth
|
||||
|
||||
# Only GitHub and Anthropic auth
|
||||
core dev claude --auth gh,anthropic
|
||||
```
|
||||
|
||||
### Why Use This?
|
||||
|
||||
- **Immutable base** - Reset anytime with `core dev boot --fresh`
|
||||
- **Safe experimentation** - Claude can install packages, make mistakes
|
||||
- **Host system untouched** - All changes stay in the sandbox
|
||||
- **Real credentials** - Can still push code, create PRs
|
||||
- **Full tooling** - 100+ tools available in the image
|
||||
|
||||
## dev status
|
||||
|
||||
Show the current state of the development environment.
|
||||
|
||||
```bash
|
||||
core dev status
|
||||
```
|
||||
|
||||
Output includes:
|
||||
- Running/stopped state
|
||||
- Resource usage (CPU, memory)
|
||||
- Exposed ports
|
||||
- Mounted directories
|
||||
|
||||
## dev update
|
||||
|
||||
Check for and apply updates.
|
||||
|
||||
```bash
|
||||
core dev update [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--apply` | Download and apply the update |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Check for updates
|
||||
core dev update
|
||||
|
||||
# Apply available update
|
||||
core dev update --apply
|
||||
```
|
||||
|
||||
## Embedded Tools
|
||||
|
||||
The core-devops image includes 100+ tools:
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| **AI/LLM** | claude, gemini, aider, ollama, llm |
|
||||
| **VCS** | git, gh, glab, lazygit, delta, git-lfs |
|
||||
| **Runtimes** | frankenphp, node, bun, deno, go, python3, rustc |
|
||||
| **Package Mgrs** | composer, npm, pnpm, yarn, pip, uv, cargo |
|
||||
| **Build** | task, make, just, nx, turbo |
|
||||
| **Linting** | pint, phpstan, prettier, eslint, biome, golangci-lint, ruff |
|
||||
| **Testing** | phpunit, pest, vitest, playwright, k6 |
|
||||
| **Infra** | docker, kubectl, k9s, helm, terraform, ansible |
|
||||
| **Databases** | sqlite3, mysql, psql, redis-cli, mongosh, usql |
|
||||
| **HTTP/Net** | curl, httpie, xh, websocat, grpcurl, mkcert, ngrok |
|
||||
| **Data** | jq, yq, fx, gron, miller, dasel |
|
||||
| **Security** | age, sops, cosign, trivy, trufflehog, vault |
|
||||
| **Files** | fd, rg, fzf, bat, eza, tree, zoxide, broot |
|
||||
| **Editors** | nvim, helix, micro |
|
||||
|
||||
## Configuration
|
||||
|
||||
Global config in `~/.core/config.yaml` - see [Configuration](example.md#configuration) for examples.
|
||||
|
||||
## Image Storage
|
||||
|
||||
Images are stored in `~/.core/images/`:
|
||||
|
||||
```
|
||||
~/.core/
|
||||
├── config.yaml
|
||||
└── images/
|
||||
├── core-devops-darwin-arm64.qcow2
|
||||
├── core-devops-linux-amd64.qcow2
|
||||
└── manifest.json
|
||||
```
|
||||
|
||||
## Multi-Repo Commands
|
||||
|
||||
See the [work](work/) page for detailed documentation on multi-repo commands.
|
||||
|
||||
### dev ci
|
||||
|
||||
Check GitHub Actions workflow status across all repos.
|
||||
|
||||
```bash
|
||||
core dev ci [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--branch` | Filter by branch (default: main) |
|
||||
| `--failed` | Show only failed runs |
|
||||
|
||||
Requires the `gh` CLI to be installed and authenticated.
|
||||
|
||||
### dev api
|
||||
|
||||
Tools for managing service APIs.
|
||||
|
||||
```bash
|
||||
core dev api sync
|
||||
```
|
||||
|
||||
Synchronizes the public service APIs with their internal implementations.
|
||||
|
||||
### dev sync
|
||||
|
||||
Alias for `core dev api sync`. Synchronizes the public service APIs with their internal implementations.
|
||||
|
||||
```bash
|
||||
core dev sync
|
||||
```
|
||||
|
||||
This command scans the `pkg` directory for services and ensures that the top-level public API for each service is in sync with its internal implementation. It automatically generates the necessary Go files with type aliases.
|
||||
|
||||
## See Also
|
||||
|
||||
- [work](work/) - Multi-repo workflow commands (`core dev work`, `core dev health`, etc.)
|
||||
- [ai](../ai/) - Task management commands (`core ai tasks`, `core ai task`, etc.)
|
||||
57
build/cli/dev/issues/index.md
Normal file
57
build/cli/dev/issues/index.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# core dev issues
|
||||
|
||||
List open issues across all repositories.
|
||||
|
||||
Fetches open issues from GitHub for all repos in the registry. Requires the `gh` CLI to be installed and authenticated.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev issues [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--assignee` | Filter by assignee (use `@me` for yourself) |
|
||||
| `--limit` | Max issues per repo (default 10) |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# List all open issues
|
||||
core dev issues
|
||||
|
||||
# Show issues assigned to you
|
||||
core dev issues --assignee @me
|
||||
|
||||
# Limit to 5 issues per repo
|
||||
core dev issues --limit 5
|
||||
|
||||
# Filter by specific assignee
|
||||
core dev issues --assignee username
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
core-php (3 issues)
|
||||
#42 Add retry logic to HTTP client bug
|
||||
#38 Update documentation for v2 API docs
|
||||
#35 Support custom serializers enhancement
|
||||
|
||||
core-tenant (1 issue)
|
||||
#12 Workspace isolation bug bug, critical
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- GitHub CLI (`gh`) must be installed
|
||||
- Must be authenticated: `gh auth login`
|
||||
|
||||
## See Also
|
||||
|
||||
- [reviews command](../reviews/) - List PRs needing review
|
||||
- [ci command](../ci/) - Check CI status
|
||||
47
build/cli/dev/pull/index.md
Normal file
47
build/cli/dev/pull/index.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# core dev pull
|
||||
|
||||
Pull updates across all repositories.
|
||||
|
||||
Pulls updates for all repos. By default only pulls repos that are behind. Use `--all` to pull all repos.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev pull [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--all` | Pull all repos, not just those behind |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Pull only repos that are behind
|
||||
core dev pull
|
||||
|
||||
# Pull all repos
|
||||
core dev pull --all
|
||||
|
||||
# Use specific registry
|
||||
core dev pull --registry ~/projects/repos.yaml
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
Pulling 2 repo(s) that are behind:
|
||||
✓ core-php (3 commits)
|
||||
✓ core-tenant (1 commit)
|
||||
|
||||
Done: 2 pulled
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [push command](../push/) - Push local commits
|
||||
- [health command](../health/) - Check sync status
|
||||
- [work command](../work/) - Full workflow
|
||||
52
build/cli/dev/push/index.md
Normal file
52
build/cli/dev/push/index.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# core dev push
|
||||
|
||||
Push commits across all repositories.
|
||||
|
||||
Pushes unpushed commits for all repos. Shows repos with commits to push and confirms before pushing.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev push [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Push with confirmation
|
||||
core dev push
|
||||
|
||||
# Push without confirmation
|
||||
core dev push --force
|
||||
|
||||
# Use specific registry
|
||||
core dev push --registry ~/projects/repos.yaml
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
3 repo(s) with unpushed commits:
|
||||
core-php: 2 commit(s)
|
||||
core-admin: 1 commit(s)
|
||||
core-tenant: 1 commit(s)
|
||||
|
||||
Push all? [y/N] y
|
||||
|
||||
✓ core-php
|
||||
✓ core-admin
|
||||
✓ core-tenant
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [commit command](../commit/) - Create commits before pushing
|
||||
- [pull command](../pull/) - Pull updates from remote
|
||||
- [work command](../work/) - Full workflow (status + commit + push)
|
||||
61
build/cli/dev/reviews/index.md
Normal file
61
build/cli/dev/reviews/index.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# core dev reviews
|
||||
|
||||
List PRs needing review across all repositories.
|
||||
|
||||
Fetches open PRs from GitHub for all repos in the registry. Shows review status (approved, changes requested, pending). Requires the `gh` CLI to be installed and authenticated.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core dev reviews [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--all` | Show all PRs including drafts |
|
||||
| `--author` | Filter by PR author |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# List PRs needing review
|
||||
core dev reviews
|
||||
|
||||
# Include draft PRs
|
||||
core dev reviews --all
|
||||
|
||||
# Filter by author
|
||||
core dev reviews --author username
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
core-php (2 PRs)
|
||||
#45 feat: Add caching layer ✓ approved @alice
|
||||
#43 fix: Memory leak in worker ⏳ pending @bob
|
||||
|
||||
core-admin (1 PR)
|
||||
#28 refactor: Extract components ✗ changes @charlie
|
||||
```
|
||||
|
||||
## Review Status
|
||||
|
||||
| Symbol | Meaning |
|
||||
|--------|---------|
|
||||
| `✓` | Approved |
|
||||
| `⏳` | Pending review |
|
||||
| `✗` | Changes requested |
|
||||
|
||||
## Requirements
|
||||
|
||||
- GitHub CLI (`gh`) must be installed
|
||||
- Must be authenticated: `gh auth login`
|
||||
|
||||
## See Also
|
||||
|
||||
- [issues command](../issues/) - List open issues
|
||||
- [ci command](../ci/) - Check CI status
|
||||
33
build/cli/dev/work/example.md
Normal file
33
build/cli/dev/work/example.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Dev Work Examples
|
||||
|
||||
```bash
|
||||
# Full workflow: status → commit → push
|
||||
core dev work
|
||||
|
||||
# Status only
|
||||
core dev work --status
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
┌─────────────┬────────┬──────────┬─────────┐
|
||||
│ Repo │ Branch │ Status │ Behind │
|
||||
├─────────────┼────────┼──────────┼─────────┤
|
||||
│ core-php │ main │ clean │ 0 │
|
||||
│ core-tenant │ main │ 2 files │ 0 │
|
||||
│ core-admin │ dev │ clean │ 3 │
|
||||
└─────────────┴────────┴──────────┴─────────┘
|
||||
```
|
||||
|
||||
## Registry
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- name: core
|
||||
path: ./core
|
||||
url: https://github.com/host-uk/core
|
||||
- name: core-php
|
||||
path: ./core-php
|
||||
url: https://github.com/host-uk/core-php
|
||||
```
|
||||
293
build/cli/dev/work/index.md
Normal file
293
build/cli/dev/work/index.md
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# core dev work
|
||||
|
||||
Multi-repo git operations for managing the host-uk organization.
|
||||
|
||||
## Overview
|
||||
|
||||
The `core dev work` command and related subcommands help manage multiple repositories in the host-uk ecosystem simultaneously.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `core dev work` | Full workflow: status + commit + push |
|
||||
| `core dev work --status` | Status table only |
|
||||
| `core dev work --commit` | Use Claude to commit dirty repos |
|
||||
| `core dev health` | Quick health check across all repos |
|
||||
| `core dev commit` | Claude-assisted commits across repos |
|
||||
| `core dev push` | Push commits across all repos |
|
||||
| `core dev pull` | Pull updates across all repos |
|
||||
| `core dev issues` | List open issues across all repos |
|
||||
| `core dev reviews` | List PRs needing review |
|
||||
| `core dev ci` | Check CI status across all repos |
|
||||
| `core dev impact` | Show impact of changing a repo |
|
||||
|
||||
## core dev work
|
||||
|
||||
Manage git status, commits, and pushes across multiple repositories.
|
||||
|
||||
```bash
|
||||
core dev work [flags]
|
||||
```
|
||||
|
||||
Reads `repos.yaml` to discover repositories and their relationships. Shows status, optionally commits with Claude, and pushes changes.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--status` | Show status only, don't push |
|
||||
| `--commit` | Use Claude to commit dirty repos before pushing |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Full workflow
|
||||
core dev work
|
||||
|
||||
# Status only
|
||||
core dev work --status
|
||||
|
||||
# Commit and push
|
||||
core dev work --commit
|
||||
```
|
||||
|
||||
## core dev health
|
||||
|
||||
Quick health check showing summary of repository health across all repos.
|
||||
|
||||
```bash
|
||||
core dev health [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--verbose` | Show detailed breakdown |
|
||||
|
||||
Output shows:
|
||||
- Total repos
|
||||
- Dirty repos
|
||||
- Unpushed commits
|
||||
- Repos behind remote
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Quick summary
|
||||
core dev health
|
||||
|
||||
# Detailed breakdown
|
||||
core dev health --verbose
|
||||
```
|
||||
|
||||
## core dev issues
|
||||
|
||||
List open issues across all repositories.
|
||||
|
||||
```bash
|
||||
core dev issues [flags]
|
||||
```
|
||||
|
||||
Fetches open issues from GitHub for all repos in the registry. Requires the `gh` CLI to be installed and authenticated.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--assignee` | Filter by assignee (use `@me` for yourself) |
|
||||
| `--limit` | Max issues per repo (default: 10) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# List all open issues
|
||||
core dev issues
|
||||
|
||||
# Filter by assignee
|
||||
core dev issues --assignee @me
|
||||
|
||||
# Limit results
|
||||
core dev issues --limit 5
|
||||
```
|
||||
|
||||
## core dev reviews
|
||||
|
||||
List pull requests needing review across all repos.
|
||||
|
||||
```bash
|
||||
core dev reviews [flags]
|
||||
```
|
||||
|
||||
Fetches open PRs from GitHub for all repos in the registry. Shows review status (approved, changes requested, pending). Requires the `gh` CLI to be installed and authenticated.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--all` | Show all PRs including drafts |
|
||||
| `--author` | Filter by PR author |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# List PRs needing review
|
||||
core dev reviews
|
||||
|
||||
# Show all PRs including drafts
|
||||
core dev reviews --all
|
||||
|
||||
# Filter by author
|
||||
core dev reviews --author username
|
||||
```
|
||||
|
||||
## core dev commit
|
||||
|
||||
Create commits across repos with Claude assistance.
|
||||
|
||||
```bash
|
||||
core dev commit [flags]
|
||||
```
|
||||
|
||||
Uses Claude to create commits for dirty repos. Shows uncommitted changes and invokes Claude to generate commit messages.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--all` | Commit all dirty repos without prompting |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Commit with prompts
|
||||
core dev commit
|
||||
|
||||
# Commit all automatically
|
||||
core dev commit --all
|
||||
```
|
||||
|
||||
## core dev push
|
||||
|
||||
Push commits across all repos.
|
||||
|
||||
```bash
|
||||
core dev push [flags]
|
||||
```
|
||||
|
||||
Pushes unpushed commits for all repos. Shows repos with commits to push and confirms before pushing.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Push with confirmation
|
||||
core dev push
|
||||
|
||||
# Skip confirmation
|
||||
core dev push --force
|
||||
```
|
||||
|
||||
## core dev pull
|
||||
|
||||
Pull updates across all repos.
|
||||
|
||||
```bash
|
||||
core dev pull [flags]
|
||||
```
|
||||
|
||||
Pulls updates for all repos. By default only pulls repos that are behind. Use `--all` to pull all repos.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--all` | Pull all repos, not just those behind |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Pull repos that are behind
|
||||
core dev pull
|
||||
|
||||
# Pull all repos
|
||||
core dev pull --all
|
||||
```
|
||||
|
||||
## core dev ci
|
||||
|
||||
Check GitHub Actions workflow status across all repos.
|
||||
|
||||
```bash
|
||||
core dev ci [flags]
|
||||
```
|
||||
|
||||
Fetches GitHub Actions workflow status for all repos. Shows latest run status for each repo. Requires the `gh` CLI to be installed and authenticated.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
| `--branch` | Filter by branch (default: main) |
|
||||
| `--failed` | Show only failed runs |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Show CI status for all repos
|
||||
core dev ci
|
||||
|
||||
# Show only failed runs
|
||||
core dev ci --failed
|
||||
|
||||
# Check specific branch
|
||||
core dev ci --branch develop
|
||||
```
|
||||
|
||||
## core dev impact
|
||||
|
||||
Show the impact of changing a repository.
|
||||
|
||||
```bash
|
||||
core dev impact <repo> [flags]
|
||||
```
|
||||
|
||||
Analyzes the dependency graph to show which repos would be affected by changes to the specified repo.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Show impact of changing core-php
|
||||
core dev impact core-php
|
||||
```
|
||||
|
||||
## Registry
|
||||
|
||||
These commands use `repos.yaml` to know which repos to manage. See [repos.yaml](../../../configuration.md#reposyaml) for format.
|
||||
|
||||
Use `core setup` to clone all repos from the registry.
|
||||
|
||||
## See Also
|
||||
|
||||
- [setup command](../../setup/) - Clone repos from registry
|
||||
- [search command](../../pkg/search/) - Find and install repos
|
||||
14
build/cli/docs/example.md
Normal file
14
build/cli/docs/example.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Docs Examples
|
||||
|
||||
## List
|
||||
|
||||
```bash
|
||||
core docs list
|
||||
```
|
||||
|
||||
## Sync
|
||||
|
||||
```bash
|
||||
core docs sync
|
||||
core docs sync --output ./docs
|
||||
```
|
||||
110
build/cli/docs/index.md
Normal file
110
build/cli/docs/index.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# core docs
|
||||
|
||||
Documentation management across repositories.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core docs <command> [flags]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `list` | List documentation across repos |
|
||||
| `sync` | Sync documentation to output directory |
|
||||
|
||||
## docs list
|
||||
|
||||
Show documentation coverage across all repos.
|
||||
|
||||
```bash
|
||||
core docs list [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml |
|
||||
|
||||
### Output
|
||||
|
||||
```
|
||||
Repo README CLAUDE CHANGELOG docs/
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
core ✓ ✓ — 12 files
|
||||
core-php ✓ ✓ ✓ 8 files
|
||||
core-images ✓ — — —
|
||||
|
||||
Coverage: 3 with docs, 0 without
|
||||
```
|
||||
|
||||
## docs sync
|
||||
|
||||
Sync documentation from all repos to an output directory.
|
||||
|
||||
```bash
|
||||
core docs sync [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml |
|
||||
| `--output` | Output directory (default: ./docs-build) |
|
||||
| `--dry-run` | Show what would be synced |
|
||||
|
||||
### Output Structure
|
||||
|
||||
```
|
||||
docs-build/
|
||||
└── packages/
|
||||
├── core/
|
||||
│ ├── index.md # from README.md
|
||||
│ ├── claude.md # from CLAUDE.md
|
||||
│ ├── changelog.md # from CHANGELOG.md
|
||||
│ ├── build.md # from docs/build.md
|
||||
│ └── ...
|
||||
└── core-php/
|
||||
├── index.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
# Preview what will be synced
|
||||
core docs sync --dry-run
|
||||
|
||||
# Sync to default output
|
||||
core docs sync
|
||||
|
||||
# Sync to custom directory
|
||||
core docs sync --output ./site/content
|
||||
```
|
||||
|
||||
## What Gets Synced
|
||||
|
||||
For each repo, the following files are collected:
|
||||
|
||||
| Source | Destination |
|
||||
|--------|-------------|
|
||||
| `README.md` | `index.md` |
|
||||
| `CLAUDE.md` | `claude.md` |
|
||||
| `CHANGELOG.md` | `changelog.md` |
|
||||
| `docs/*.md` | `*.md` |
|
||||
|
||||
## Integration with core.help
|
||||
|
||||
The synced docs are used to build https://core.help:
|
||||
|
||||
1. Run `core docs sync --output ../core-php/docs/packages`
|
||||
2. VitePress builds the combined documentation
|
||||
3. Deploy to core.help
|
||||
|
||||
## See Also
|
||||
|
||||
- [Configuration](../../configuration.md) - Project configuration
|
||||
20
build/cli/doctor/example.md
Normal file
20
build/cli/doctor/example.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Doctor Examples
|
||||
|
||||
```bash
|
||||
core doctor
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
✓ go 1.25.0
|
||||
✓ git 2.43.0
|
||||
✓ gh 2.40.0
|
||||
✓ docker 24.0.7
|
||||
✓ task 3.30.0
|
||||
✓ golangci-lint 1.55.0
|
||||
✗ wails (not installed)
|
||||
✓ php 8.3.0
|
||||
✓ composer 2.6.0
|
||||
✓ node 20.10.0
|
||||
```
|
||||
81
build/cli/doctor/index.md
Normal file
81
build/cli/doctor/index.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# core doctor
|
||||
|
||||
Check your development environment for required tools and configuration.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core doctor [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--verbose` | Show detailed version information |
|
||||
|
||||
## What It Checks
|
||||
|
||||
### Required Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `git` | Version control |
|
||||
| `go` | Go compiler |
|
||||
| `gh` | GitHub CLI |
|
||||
|
||||
### Optional Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `node` | Node.js runtime |
|
||||
| `docker` | Container runtime |
|
||||
| `wails` | Desktop app framework |
|
||||
| `qemu` | VM runtime for LinuxKit |
|
||||
| `gpg` | Code signing |
|
||||
| `codesign` | macOS signing (macOS only) |
|
||||
|
||||
### Configuration
|
||||
|
||||
- Git user name and email
|
||||
- GitHub CLI authentication
|
||||
- Go workspace setup
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
Core Doctor
|
||||
===========
|
||||
|
||||
Required:
|
||||
[OK] git 2.43.0
|
||||
[OK] go 1.23.0
|
||||
[OK] gh 2.40.0
|
||||
|
||||
Optional:
|
||||
[OK] node 20.10.0
|
||||
[OK] docker 24.0.7
|
||||
[--] wails (not installed)
|
||||
[OK] qemu 8.2.0
|
||||
[OK] gpg 2.4.3
|
||||
[OK] codesign (available)
|
||||
|
||||
Configuration:
|
||||
[OK] git user.name: Your Name
|
||||
[OK] git user.email: you@example.com
|
||||
[OK] gh auth status: Logged in
|
||||
|
||||
All checks passed!
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | All required checks passed |
|
||||
| 1 | One or more required checks failed |
|
||||
|
||||
## See Also
|
||||
|
||||
- [setup command](../setup/) - Clone repos from registry
|
||||
- [dev](../dev/) - Development environment
|
||||
18
build/cli/go/cov/example.md
Normal file
18
build/cli/go/cov/example.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Go Coverage Examples
|
||||
|
||||
```bash
|
||||
# Summary
|
||||
core go cov
|
||||
|
||||
# HTML report
|
||||
core go cov --html
|
||||
|
||||
# Open in browser
|
||||
core go cov --open
|
||||
|
||||
# Fail if below threshold
|
||||
core go cov --threshold 80
|
||||
|
||||
# Specific package
|
||||
core go cov --pkg ./pkg/release
|
||||
```
|
||||
28
build/cli/go/cov/index.md
Normal file
28
build/cli/go/cov/index.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# core go cov
|
||||
|
||||
Generate coverage report with thresholds.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go cov [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--pkg` | Package to test (default: `./...`) |
|
||||
| `--html` | Generate HTML coverage report |
|
||||
| `--open` | Generate and open HTML report in browser |
|
||||
| `--threshold` | Minimum coverage percentage (exit 1 if below) |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core go cov # Summary
|
||||
core go cov --html # HTML report
|
||||
core go cov --open # Open in browser
|
||||
core go cov --threshold 80 # Fail if < 80%
|
||||
core go cov --pkg ./pkg/release # Specific package
|
||||
```
|
||||
89
build/cli/go/example.md
Normal file
89
build/cli/go/example.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Go Examples
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
core go test
|
||||
|
||||
# Specific package
|
||||
core go test --pkg ./pkg/core
|
||||
|
||||
# Specific test
|
||||
core go test --run TestHash
|
||||
|
||||
# With coverage
|
||||
core go test --coverage
|
||||
|
||||
# Race detection
|
||||
core go test --race
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
```bash
|
||||
# Summary
|
||||
core go cov
|
||||
|
||||
# HTML report
|
||||
core go cov --html
|
||||
|
||||
# Open in browser
|
||||
core go cov --open
|
||||
|
||||
# Fail if below threshold
|
||||
core go cov --threshold 80
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
```bash
|
||||
# Check
|
||||
core go fmt
|
||||
|
||||
# Fix
|
||||
core go fmt --fix
|
||||
|
||||
# Show diff
|
||||
core go fmt --diff
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
# Check
|
||||
core go lint
|
||||
|
||||
# Auto-fix
|
||||
core go lint --fix
|
||||
```
|
||||
|
||||
## Installing
|
||||
|
||||
```bash
|
||||
# Auto-detect cmd/
|
||||
core go install
|
||||
|
||||
# Specific path
|
||||
core go install ./cmd/myapp
|
||||
|
||||
# Pure Go (no CGO)
|
||||
core go install --no-cgo
|
||||
```
|
||||
|
||||
## Module Management
|
||||
|
||||
```bash
|
||||
core go mod tidy
|
||||
core go mod download
|
||||
core go mod verify
|
||||
core go mod graph
|
||||
```
|
||||
|
||||
## Workspace
|
||||
|
||||
```bash
|
||||
core go work sync
|
||||
core go work init
|
||||
core go work use ./pkg/mymodule
|
||||
```
|
||||
12
build/cli/go/fmt/example.md
Normal file
12
build/cli/go/fmt/example.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Go Format Examples
|
||||
|
||||
```bash
|
||||
# Check only
|
||||
core go fmt
|
||||
|
||||
# Apply fixes
|
||||
core go fmt --fix
|
||||
|
||||
# Show diff
|
||||
core go fmt --diff
|
||||
```
|
||||
25
build/cli/go/fmt/index.md
Normal file
25
build/cli/go/fmt/index.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# core go fmt
|
||||
|
||||
Format Go code using goimports or gofmt.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go fmt [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--fix` | Fix formatting in place |
|
||||
| `--diff` | Show diff of changes |
|
||||
| `--check` | Check only, exit 1 if not formatted |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core go fmt # Check formatting
|
||||
core go fmt --fix # Fix formatting
|
||||
core go fmt --diff # Show diff
|
||||
```
|
||||
15
build/cli/go/index.md
Normal file
15
build/cli/go/index.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# core go
|
||||
|
||||
Go development tools with enhanced output and environment setup.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [test](test/) | Run tests with coverage |
|
||||
| [cov](cov/) | Run tests with coverage report |
|
||||
| [fmt](fmt/) | Format Go code |
|
||||
| [lint](lint/) | Run golangci-lint |
|
||||
| [install](install/) | Install Go binary |
|
||||
| [mod](mod/) | Module management |
|
||||
| [work](work/) | Workspace management |
|
||||
15
build/cli/go/install/example.md
Normal file
15
build/cli/go/install/example.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Go Install Examples
|
||||
|
||||
```bash
|
||||
# Auto-detect cmd/
|
||||
core go install
|
||||
|
||||
# Specific path
|
||||
core go install ./cmd/myapp
|
||||
|
||||
# Pure Go (no CGO)
|
||||
core go install --no-cgo
|
||||
|
||||
# Verbose
|
||||
core go install -v
|
||||
```
|
||||
25
build/cli/go/install/index.md
Normal file
25
build/cli/go/install/index.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# core go install
|
||||
|
||||
Install Go binary with auto-detection.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go install [path] [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--no-cgo` | Disable CGO |
|
||||
| `-v` | Verbose |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core go install # Install current module
|
||||
core go install ./cmd/core # Install specific path
|
||||
core go install --no-cgo # Pure Go (no C dependencies)
|
||||
core go install -v # Verbose output
|
||||
```
|
||||
22
build/cli/go/lint/example.md
Normal file
22
build/cli/go/lint/example.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Go Lint Examples
|
||||
|
||||
```bash
|
||||
# Check
|
||||
core go lint
|
||||
|
||||
# Auto-fix
|
||||
core go lint --fix
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`.golangci.yml`:
|
||||
|
||||
```yaml
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
```
|
||||
22
build/cli/go/lint/index.md
Normal file
22
build/cli/go/lint/index.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# core go lint
|
||||
|
||||
Run golangci-lint.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go lint [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--fix` | Fix issues automatically |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core go lint # Check
|
||||
core go lint --fix # Auto-fix
|
||||
```
|
||||
29
build/cli/go/mod/download/index.md
Normal file
29
build/cli/go/mod/download/index.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# core go mod download
|
||||
|
||||
Download modules to local cache.
|
||||
|
||||
Wrapper around `go mod download`. Downloads all dependencies to the module cache.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go mod download
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Downloads all modules in go.mod to `$GOPATH/pkg/mod`
|
||||
- Useful for pre-populating cache for offline builds
|
||||
- Validates checksums against go.sum
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Download all dependencies
|
||||
core go mod download
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [tidy](../tidy/) - Clean up go.mod
|
||||
- [verify](../verify/) - Verify checksums
|
||||
15
build/cli/go/mod/example.md
Normal file
15
build/cli/go/mod/example.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Go Module Examples
|
||||
|
||||
```bash
|
||||
# Tidy
|
||||
core go mod tidy
|
||||
|
||||
# Download
|
||||
core go mod download
|
||||
|
||||
# Verify
|
||||
core go mod verify
|
||||
|
||||
# Graph
|
||||
core go mod graph
|
||||
```
|
||||
44
build/cli/go/mod/graph/index.md
Normal file
44
build/cli/go/mod/graph/index.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# core go mod graph
|
||||
|
||||
Print module dependency graph.
|
||||
|
||||
Wrapper around `go mod graph`. Outputs the module dependency graph in text form.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go mod graph
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Prints module dependencies as pairs
|
||||
- Each line shows: `module@version dependency@version`
|
||||
- Useful for understanding dependency relationships
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Print dependency graph
|
||||
core go mod graph
|
||||
|
||||
# Find who depends on a specific module
|
||||
core go mod graph | grep "some/module"
|
||||
|
||||
# Visualise with graphviz
|
||||
core go mod graph | dot -Tpng -o deps.png
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
github.com/host-uk/core github.com/stretchr/testify@v1.11.1
|
||||
github.com/stretchr/testify@v1.11.1 github.com/davecgh/go-spew@v1.1.2
|
||||
github.com/stretchr/testify@v1.11.1 github.com/pmezard/go-difflib@v1.0.1
|
||||
...
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [tidy](../tidy/) - Clean up go.mod
|
||||
- [dev impact](../../../dev/impact/) - Show repo dependency impact
|
||||
21
build/cli/go/mod/index.md
Normal file
21
build/cli/go/mod/index.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# core go mod
|
||||
|
||||
Module management.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `tidy` | Add missing and remove unused modules |
|
||||
| `download` | Download modules to local cache |
|
||||
| `verify` | Verify dependencies |
|
||||
| `graph` | Print module dependency graph |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core go mod tidy
|
||||
core go mod download
|
||||
core go mod verify
|
||||
core go mod graph
|
||||
```
|
||||
29
build/cli/go/mod/tidy/index.md
Normal file
29
build/cli/go/mod/tidy/index.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# core go mod tidy
|
||||
|
||||
Add missing and remove unused modules.
|
||||
|
||||
Wrapper around `go mod tidy`. Ensures go.mod and go.sum are in sync with the source code.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go mod tidy
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Adds missing module requirements
|
||||
- Removes unused module requirements
|
||||
- Updates go.sum with checksums
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Tidy the current module
|
||||
core go mod tidy
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [download](../download/) - Download modules
|
||||
- [verify](../verify/) - Verify dependencies
|
||||
41
build/cli/go/mod/verify/index.md
Normal file
41
build/cli/go/mod/verify/index.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# core go mod verify
|
||||
|
||||
Verify dependencies have not been modified.
|
||||
|
||||
Wrapper around `go mod verify`. Checks that dependencies in the module cache match their checksums in go.sum.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go mod verify
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Verifies each module in the cache
|
||||
- Compares against go.sum checksums
|
||||
- Reports any tampering or corruption
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Verify all dependencies
|
||||
core go mod verify
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
all modules verified
|
||||
```
|
||||
|
||||
Or if verification fails:
|
||||
|
||||
```
|
||||
github.com/example/pkg v1.2.3: dir has been modified
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [download](../download/) - Download modules
|
||||
- [tidy](../tidy/) - Clean up go.mod
|
||||
27
build/cli/go/test/example.md
Normal file
27
build/cli/go/test/example.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Go Test Examples
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
core go test
|
||||
|
||||
# Specific package
|
||||
core go test --pkg ./pkg/core
|
||||
|
||||
# Specific test
|
||||
core go test --run TestHash
|
||||
|
||||
# With coverage
|
||||
core go test --coverage
|
||||
|
||||
# Race detection
|
||||
core go test --race
|
||||
|
||||
# Short tests only
|
||||
core go test --short
|
||||
|
||||
# Verbose
|
||||
core go test -v
|
||||
|
||||
# JSON output (CI)
|
||||
core go test --json
|
||||
```
|
||||
31
build/cli/go/test/index.md
Normal file
31
build/cli/go/test/index.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# core go test
|
||||
|
||||
Run Go tests with coverage and filtered output.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go test [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--pkg` | Package to test (default: `./...`) |
|
||||
| `--run` | Run only tests matching regexp |
|
||||
| `--short` | Run only short tests |
|
||||
| `--race` | Enable race detector |
|
||||
| `--coverage` | Show detailed per-package coverage |
|
||||
| `--json` | Output JSON results |
|
||||
| `-v` | Verbose output |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core go test # All tests
|
||||
core go test --pkg ./pkg/core # Specific package
|
||||
core go test --run TestHash # Specific test
|
||||
core go test --coverage # With coverage
|
||||
core go test --race # Race detection
|
||||
```
|
||||
19
build/cli/go/work/index.md
Normal file
19
build/cli/go/work/index.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# core go work
|
||||
|
||||
Go workspace management commands.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `sync` | Sync go.work with modules |
|
||||
| `init` | Initialize go.work |
|
||||
| `use` | Add module to workspace |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
core go work sync # Sync workspace
|
||||
core go work init # Initialize workspace
|
||||
core go work use ./pkg/mymodule # Add module to workspace
|
||||
```
|
||||
40
build/cli/go/work/init/index.md
Normal file
40
build/cli/go/work/init/index.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# core go work init
|
||||
|
||||
Initialize a Go workspace.
|
||||
|
||||
Wrapper around `go work init`. Creates a new go.work file in the current directory.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go work init
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Creates a go.work file
|
||||
- Automatically adds current module if go.mod exists
|
||||
- Enables multi-module development
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Initialize workspace
|
||||
core go work init
|
||||
|
||||
# Then add more modules
|
||||
core go work use ./pkg/mymodule
|
||||
```
|
||||
|
||||
## Generated File
|
||||
|
||||
```go
|
||||
go 1.25
|
||||
|
||||
use .
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [use](../use/) - Add module to workspace
|
||||
- [sync](../sync/) - Sync workspace
|
||||
35
build/cli/go/work/sync/index.md
Normal file
35
build/cli/go/work/sync/index.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# core go work sync
|
||||
|
||||
Sync go.work with modules.
|
||||
|
||||
Wrapper around `go work sync`. Synchronises the workspace's build list back to the workspace modules.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go work sync
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Updates each module's go.mod to match the workspace build list
|
||||
- Ensures all modules use compatible dependency versions
|
||||
- Run after adding new modules or updating dependencies
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Sync workspace
|
||||
core go work sync
|
||||
```
|
||||
|
||||
## When To Use
|
||||
|
||||
- After running `go get` to update a dependency
|
||||
- After adding a new module with `core go work use`
|
||||
- When modules have conflicting dependency versions
|
||||
|
||||
## See Also
|
||||
|
||||
- [init](../init/) - Initialize workspace
|
||||
- [use](../use/) - Add module to workspace
|
||||
46
build/cli/go/work/use/index.md
Normal file
46
build/cli/go/work/use/index.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# core go work use
|
||||
|
||||
Add module to workspace.
|
||||
|
||||
Wrapper around `go work use`. Adds one or more modules to the go.work file.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core go work use [paths...]
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Adds specified module paths to go.work
|
||||
- Auto-discovers modules if no paths given
|
||||
- Enables developing multiple modules together
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Add a specific module
|
||||
core go work use ./pkg/mymodule
|
||||
|
||||
# Add multiple modules
|
||||
core go work use ./pkg/one ./pkg/two
|
||||
|
||||
# Auto-discover and add all modules
|
||||
core go work use
|
||||
```
|
||||
|
||||
## Auto-Discovery
|
||||
|
||||
When called without arguments, scans for go.mod files and adds all found modules:
|
||||
|
||||
```bash
|
||||
core go work use
|
||||
# Added ./pkg/build
|
||||
# Added ./pkg/repos
|
||||
# Added ./cmd/core
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [init](../init/) - Initialize workspace
|
||||
- [sync](../sync/) - Sync workspace
|
||||
31
build/cli/index.md
Normal file
31
build/cli/index.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Core CLI
|
||||
|
||||
Unified interface for Go/PHP development, multi-repo management, and deployment.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [ai](ai/) | AI agent task management and Claude integration |
|
||||
| [go](go/) | Go development tools |
|
||||
| [php](php/) | Laravel/PHP development tools |
|
||||
| [build](build/) | Build projects |
|
||||
| [ci](ci/) | Publish releases |
|
||||
| [sdk](sdk/) | SDK validation and compatibility |
|
||||
| [dev](dev/) | Multi-repo workflow + dev environment |
|
||||
| [pkg](pkg/) | Package management |
|
||||
| [vm](vm/) | LinuxKit VM management |
|
||||
| [docs](docs/) | Documentation management |
|
||||
| [setup](setup/) | Clone repos from registry |
|
||||
| [doctor](doctor/) | Check environment |
|
||||
| [test](test/) | Run Go tests with coverage |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
```
|
||||
|
||||
Verify: `core doctor`
|
||||
|
||||
See [Getting Started](/build/go/getting-started) for all installation options.
|
||||
111
build/cli/php/example.md
Normal file
111
build/cli/php/example.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# PHP Examples
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
core php dev
|
||||
|
||||
# With HTTPS
|
||||
core php dev --https
|
||||
|
||||
# Skip services
|
||||
core php dev --no-vite --no-horizon
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all
|
||||
core php test
|
||||
|
||||
# Parallel
|
||||
core php test --parallel
|
||||
|
||||
# With coverage
|
||||
core php test --coverage
|
||||
|
||||
# Filter
|
||||
core php test --filter UserTest
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
```bash
|
||||
# Format
|
||||
core php fmt --fix
|
||||
|
||||
# Static analysis
|
||||
core php analyse --level 9
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# Production
|
||||
core php deploy
|
||||
|
||||
# Staging
|
||||
core php deploy --staging
|
||||
|
||||
# Wait for completion
|
||||
core php deploy --wait
|
||||
|
||||
# Check status
|
||||
core php deploy:status
|
||||
|
||||
# Rollback
|
||||
core php deploy:rollback
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### .env
|
||||
|
||||
```env
|
||||
COOLIFY_URL=https://coolify.example.com
|
||||
COOLIFY_TOKEN=your-api-token
|
||||
COOLIFY_APP_ID=production-app-id
|
||||
COOLIFY_STAGING_APP_ID=staging-app-id
|
||||
```
|
||||
|
||||
### .core/php.yaml
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
dev:
|
||||
domain: myapp.test
|
||||
ssl: true
|
||||
services:
|
||||
- frankenphp
|
||||
- vite
|
||||
- horizon
|
||||
- reverb
|
||||
- redis
|
||||
|
||||
deploy:
|
||||
coolify:
|
||||
server: https://coolify.example.com
|
||||
project: my-project
|
||||
```
|
||||
|
||||
## Package Linking
|
||||
|
||||
```bash
|
||||
# Link local packages
|
||||
core php packages link ../my-package
|
||||
|
||||
# Update linked
|
||||
core php packages update
|
||||
|
||||
# Unlink
|
||||
core php packages unlink my-package
|
||||
```
|
||||
|
||||
## SSL Setup
|
||||
|
||||
```bash
|
||||
core php ssl
|
||||
core php ssl --domain myapp.test
|
||||
```
|
||||
413
build/cli/php/index.md
Normal file
413
build/cli/php/index.md
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
# core php
|
||||
|
||||
Laravel/PHP development tools with FrankenPHP.
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`dev`](#php-dev) | Start development environment |
|
||||
| [`logs`](#php-logs) | View service logs |
|
||||
| [`stop`](#php-stop) | Stop all services |
|
||||
| [`status`](#php-status) | Show service status |
|
||||
| [`ssl`](#php-ssl) | Setup SSL certificates with mkcert |
|
||||
|
||||
### Build & Production
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`build`](#php-build) | Build Docker or LinuxKit image |
|
||||
| [`serve`](#php-serve) | Run production container |
|
||||
| [`shell`](#php-shell) | Open shell in running container |
|
||||
|
||||
### Code Quality
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`test`](#php-test) | Run PHP tests (PHPUnit/Pest) |
|
||||
| [`fmt`](#php-fmt) | Format code with Laravel Pint |
|
||||
| [`analyse`](#php-analyse) | Run PHPStan static analysis |
|
||||
|
||||
### Package Management
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`packages link`](#php-packages-link) | Link local packages by path |
|
||||
| [`packages unlink`](#php-packages-unlink) | Unlink packages by name |
|
||||
| [`packages update`](#php-packages-update) | Update linked packages |
|
||||
| [`packages list`](#php-packages-list) | List linked packages |
|
||||
|
||||
### Deployment (Coolify)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`deploy`](#php-deploy) | Deploy to Coolify |
|
||||
| [`deploy:status`](#php-deploystatus) | Show deployment status |
|
||||
| [`deploy:rollback`](#php-deployrollback) | Rollback to previous deployment |
|
||||
| [`deploy:list`](#php-deploylist) | List recent deployments |
|
||||
|
||||
---
|
||||
|
||||
## php dev
|
||||
|
||||
Start the Laravel development environment with all detected services.
|
||||
|
||||
```bash
|
||||
core php dev [flags]
|
||||
```
|
||||
|
||||
### Services Orchestrated
|
||||
|
||||
- **FrankenPHP/Octane** - HTTP server (port 8000, HTTPS on 443)
|
||||
- **Vite** - Frontend dev server (port 5173)
|
||||
- **Laravel Horizon** - Queue workers
|
||||
- **Laravel Reverb** - WebSocket server (port 8080)
|
||||
- **Redis** - Cache and queue backend (port 6379)
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--no-vite` | Skip Vite dev server |
|
||||
| `--no-horizon` | Skip Laravel Horizon |
|
||||
| `--no-reverb` | Skip Laravel Reverb |
|
||||
| `--no-redis` | Skip Redis server |
|
||||
| `--https` | Enable HTTPS with mkcert |
|
||||
| `--domain` | Domain for SSL certificate (default: from APP_URL) |
|
||||
| `--port` | FrankenPHP port (default: 8000) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Start all detected services
|
||||
core php dev
|
||||
|
||||
# With HTTPS
|
||||
core php dev --https
|
||||
|
||||
# Skip optional services
|
||||
core php dev --no-horizon --no-reverb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php logs
|
||||
|
||||
Stream unified logs from all running services.
|
||||
|
||||
```bash
|
||||
core php logs [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--follow` | Follow log output |
|
||||
| `--service` | Specific service (frankenphp, vite, horizon, reverb, redis) |
|
||||
|
||||
---
|
||||
|
||||
## php stop
|
||||
|
||||
Stop all running Laravel services.
|
||||
|
||||
```bash
|
||||
core php stop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php status
|
||||
|
||||
Show the status of all Laravel services and project configuration.
|
||||
|
||||
```bash
|
||||
core php status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php ssl
|
||||
|
||||
Setup local SSL certificates using mkcert.
|
||||
|
||||
```bash
|
||||
core php ssl [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--domain` | Domain for certificate (default: from APP_URL or localhost) |
|
||||
|
||||
---
|
||||
|
||||
## php build
|
||||
|
||||
Build a production-ready container image.
|
||||
|
||||
```bash
|
||||
core php build [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--type` | Build type: `docker` (default) or `linuxkit` |
|
||||
| `--name` | Image name (default: project directory name) |
|
||||
| `--tag` | Image tag (default: latest) |
|
||||
| `--platform` | Target platform (e.g., linux/amd64, linux/arm64) |
|
||||
| `--dockerfile` | Path to custom Dockerfile |
|
||||
| `--output` | Output path for LinuxKit image |
|
||||
| `--format` | LinuxKit format: qcow2 (default), iso, raw, vmdk |
|
||||
| `--template` | LinuxKit template name (default: server-php) |
|
||||
| `--no-cache` | Build without cache |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Build Docker image
|
||||
core php build
|
||||
|
||||
# With custom name and tag
|
||||
core php build --name myapp --tag v1.0
|
||||
|
||||
# Build LinuxKit image
|
||||
core php build --type linuxkit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php serve
|
||||
|
||||
Run a production container.
|
||||
|
||||
```bash
|
||||
core php serve [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--name` | Docker image name (required) |
|
||||
| `--tag` | Image tag (default: latest) |
|
||||
| `--container` | Container name |
|
||||
| `--port` | HTTP port (default: 80) |
|
||||
| `--https-port` | HTTPS port (default: 443) |
|
||||
| `-d` | Run in detached mode |
|
||||
| `--env-file` | Path to environment file |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
core php serve --name myapp
|
||||
core php serve --name myapp -d
|
||||
core php serve --name myapp --port 8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php shell
|
||||
|
||||
Open an interactive shell in a running container.
|
||||
|
||||
```bash
|
||||
core php shell <container-id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php test
|
||||
|
||||
Run PHP tests using PHPUnit or Pest.
|
||||
|
||||
```bash
|
||||
core php test [flags]
|
||||
```
|
||||
|
||||
Auto-detects Pest if `tests/Pest.php` exists.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--parallel` | Run tests in parallel |
|
||||
| `--coverage` | Generate code coverage |
|
||||
| `--filter` | Filter tests by name pattern |
|
||||
| `--group` | Run only tests in specified group |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
core php test
|
||||
core php test --parallel --coverage
|
||||
core php test --filter UserTest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php fmt
|
||||
|
||||
Format PHP code using Laravel Pint.
|
||||
|
||||
```bash
|
||||
core php fmt [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--fix` | Auto-fix formatting issues |
|
||||
| `--diff` | Show diff of changes |
|
||||
|
||||
---
|
||||
|
||||
## php analyse
|
||||
|
||||
Run PHPStan or Larastan static analysis.
|
||||
|
||||
```bash
|
||||
core php analyse [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--level` | PHPStan analysis level (0-9) |
|
||||
| `--memory` | Memory limit (e.g., 2G) |
|
||||
|
||||
---
|
||||
|
||||
## php packages link
|
||||
|
||||
Link local PHP packages for development.
|
||||
|
||||
```bash
|
||||
core php packages link <path> [<path>...]
|
||||
```
|
||||
|
||||
Adds path repositories to composer.json with symlink enabled.
|
||||
|
||||
---
|
||||
|
||||
## php packages unlink
|
||||
|
||||
Remove linked packages from composer.json.
|
||||
|
||||
```bash
|
||||
core php packages unlink <name> [<name>...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php packages update
|
||||
|
||||
Update linked packages via Composer.
|
||||
|
||||
```bash
|
||||
core php packages update [<name>...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php packages list
|
||||
|
||||
List all locally linked packages.
|
||||
|
||||
```bash
|
||||
core php packages list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## php deploy
|
||||
|
||||
Deploy the PHP application to Coolify.
|
||||
|
||||
```bash
|
||||
core php deploy [flags]
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Requires environment variables in `.env`:
|
||||
```
|
||||
COOLIFY_URL=https://coolify.example.com
|
||||
COOLIFY_TOKEN=your-api-token
|
||||
COOLIFY_APP_ID=production-app-id
|
||||
COOLIFY_STAGING_APP_ID=staging-app-id
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--staging` | Deploy to staging environment |
|
||||
| `--force` | Force deployment even if no changes detected |
|
||||
| `--wait` | Wait for deployment to complete |
|
||||
|
||||
---
|
||||
|
||||
## php deploy:status
|
||||
|
||||
Show the status of a deployment.
|
||||
|
||||
```bash
|
||||
core php deploy:status [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--staging` | Check staging environment |
|
||||
| `--id` | Specific deployment ID |
|
||||
|
||||
---
|
||||
|
||||
## php deploy:rollback
|
||||
|
||||
Rollback to a previous deployment.
|
||||
|
||||
```bash
|
||||
core php deploy:rollback [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--staging` | Rollback staging environment |
|
||||
| `--id` | Specific deployment ID to rollback to |
|
||||
| `--wait` | Wait for rollback to complete |
|
||||
|
||||
---
|
||||
|
||||
## php deploy:list
|
||||
|
||||
List recent deployments.
|
||||
|
||||
```bash
|
||||
core php deploy:list [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--staging` | List staging deployments |
|
||||
| `--limit` | Number of deployments (default: 10) |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Optional `.core/php.yaml` - see [Configuration](example.md#configuration) for examples.
|
||||
36
build/cli/pkg/example.md
Normal file
36
build/cli/pkg/example.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Package Examples
|
||||
|
||||
## Search
|
||||
|
||||
```bash
|
||||
core pkg search core-
|
||||
core pkg search api
|
||||
core pkg search --org myorg
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
core pkg install core-api
|
||||
core pkg install host-uk/core-api
|
||||
```
|
||||
|
||||
## List
|
||||
|
||||
```bash
|
||||
core pkg list
|
||||
core pkg list --format json
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
core pkg update
|
||||
core pkg update core-api
|
||||
```
|
||||
|
||||
## Outdated
|
||||
|
||||
```bash
|
||||
core pkg outdated
|
||||
```
|
||||
144
build/cli/pkg/index.md
Normal file
144
build/cli/pkg/index.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# core pkg
|
||||
|
||||
Package management for host-uk repositories.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core pkg <command> [flags]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`search`](#pkg-search) | Search GitHub for packages |
|
||||
| [`install`](#pkg-install) | Clone a package from GitHub |
|
||||
| [`list`](#pkg-list) | List installed packages |
|
||||
| [`update`](#pkg-update) | Update installed packages |
|
||||
| [`outdated`](#pkg-outdated) | Check for outdated packages |
|
||||
|
||||
---
|
||||
|
||||
## pkg search
|
||||
|
||||
Search GitHub for host-uk packages.
|
||||
|
||||
```bash
|
||||
core pkg search [flags]
|
||||
```
|
||||
|
||||
Results are cached for 1 hour in `.core/cache/`.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--org` | GitHub organisation (default: host-uk) |
|
||||
| `--pattern` | Repo name pattern (* for wildcard) |
|
||||
| `--type` | Filter by type in name (mod, services, plug, website) |
|
||||
| `--limit` | Max results (default: 50) |
|
||||
| `--refresh` | Bypass cache and fetch fresh data |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# List all repos in org
|
||||
core pkg search
|
||||
|
||||
# Search for core-* repos
|
||||
core pkg search --pattern 'core-*'
|
||||
|
||||
# Search different org
|
||||
core pkg search --org mycompany
|
||||
|
||||
# Bypass cache
|
||||
core pkg search --refresh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pkg install
|
||||
|
||||
Clone a package from GitHub.
|
||||
|
||||
```bash
|
||||
core pkg install <org/repo> [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--dir` | Target directory (default: ./packages or current dir) |
|
||||
| `--add` | Add to repos.yaml registry |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Clone to packages/
|
||||
core pkg install host-uk/core-php
|
||||
|
||||
# Clone to custom directory
|
||||
core pkg install host-uk/core-tenant --dir ./packages
|
||||
|
||||
# Clone and add to registry
|
||||
core pkg install host-uk/core-admin --add
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pkg list
|
||||
|
||||
List installed packages from repos.yaml.
|
||||
|
||||
```bash
|
||||
core pkg list
|
||||
```
|
||||
|
||||
Shows installed status (✓) and description for each package.
|
||||
|
||||
---
|
||||
|
||||
## pkg update
|
||||
|
||||
Pull latest changes for installed packages.
|
||||
|
||||
```bash
|
||||
core pkg update [<name>...] [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--all` | Update all packages |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Update specific package
|
||||
core pkg update core-php
|
||||
|
||||
# Update all packages
|
||||
core pkg update --all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pkg outdated
|
||||
|
||||
Check which packages have unpulled commits.
|
||||
|
||||
```bash
|
||||
core pkg outdated
|
||||
```
|
||||
|
||||
Fetches from remote and shows packages that are behind.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [setup](../setup/) - Clone all repos from registry
|
||||
- [dev work](../dev/work/) - Multi-repo workflow
|
||||
23
build/cli/pkg/search/example.md
Normal file
23
build/cli/pkg/search/example.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Package Search Examples
|
||||
|
||||
```bash
|
||||
# Find all core-* packages
|
||||
core pkg search core-
|
||||
|
||||
# Search term
|
||||
core pkg search api
|
||||
|
||||
# Different org
|
||||
core pkg search --org myorg query
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
┌──────────────┬─────────────────────────────┐
|
||||
│ Package │ Description │
|
||||
├──────────────┼─────────────────────────────┤
|
||||
│ core-api │ REST API framework │
|
||||
│ core-auth │ Authentication utilities │
|
||||
└──────────────┴─────────────────────────────┘
|
||||
```
|
||||
75
build/cli/pkg/search/index.md
Normal file
75
build/cli/pkg/search/index.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# core pkg search
|
||||
|
||||
Search GitHub for repositories matching a pattern.
|
||||
|
||||
Uses `gh` CLI for authenticated search. Results are cached for 1 hour.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core pkg search [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--pattern` | Repo name pattern (* for wildcard) |
|
||||
| `--org` | GitHub organization (default: host-uk) |
|
||||
| `--type` | Filter by type in name (mod, services, plug, website) |
|
||||
| `--limit` | Max results (default: 50) |
|
||||
| `--refresh` | Bypass cache and fetch fresh data |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# List all host-uk repos
|
||||
core pkg search
|
||||
|
||||
# Search for core-* repos
|
||||
core pkg search --pattern "core-*"
|
||||
|
||||
# Search different org
|
||||
core pkg search --org mycompany
|
||||
|
||||
# Filter by type
|
||||
core pkg search --type services
|
||||
|
||||
# Bypass cache
|
||||
core pkg search --refresh
|
||||
|
||||
# Combine filters
|
||||
core pkg search --pattern "core-*" --type mod --limit 20
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
Found 5 repositories:
|
||||
|
||||
host-uk/core
|
||||
Go CLI for the host-uk ecosystem
|
||||
★ 42 Go Updated 2 hours ago
|
||||
|
||||
host-uk/core-php
|
||||
PHP/Laravel packages for Core
|
||||
★ 18 PHP Updated 1 day ago
|
||||
|
||||
host-uk/core-images
|
||||
Docker and LinuxKit images
|
||||
★ 8 Dockerfile Updated 3 days ago
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Uses GitHub CLI (`gh`) authentication. Ensure you're logged in:
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
gh auth login # if not authenticated
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [pkg install](../) - Clone a package from GitHub
|
||||
- [setup command](../../setup/) - Clone all repos from registry
|
||||
35
build/cli/sdk/example.md
Normal file
35
build/cli/sdk/example.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# SDK Examples
|
||||
|
||||
## Validate
|
||||
|
||||
```bash
|
||||
core sdk validate
|
||||
core sdk validate --spec ./api.yaml
|
||||
```
|
||||
|
||||
## Diff
|
||||
|
||||
```bash
|
||||
# Compare with tag
|
||||
core sdk diff --base v1.0.0
|
||||
|
||||
# Compare files
|
||||
core sdk diff --base ./old-api.yaml --spec ./new-api.yaml
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
Breaking changes detected:
|
||||
|
||||
- DELETE /users/{id}/profile
|
||||
Endpoint removed
|
||||
|
||||
- PATCH /users/{id}
|
||||
Required field 'email' added
|
||||
|
||||
Non-breaking changes:
|
||||
|
||||
+ POST /users/{id}/avatar
|
||||
New endpoint added
|
||||
```
|
||||
106
build/cli/sdk/index.md
Normal file
106
build/cli/sdk/index.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# core sdk
|
||||
|
||||
SDK validation and API compatibility tools.
|
||||
|
||||
To generate SDKs, use: `core build sdk`
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core sdk <command> [flags]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `diff` | Check for breaking API changes |
|
||||
| `validate` | Validate OpenAPI spec |
|
||||
|
||||
## sdk validate
|
||||
|
||||
Validate an OpenAPI specification file.
|
||||
|
||||
```bash
|
||||
core sdk validate [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--spec` | Path to OpenAPI spec file (auto-detected) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Validate detected spec
|
||||
core sdk validate
|
||||
|
||||
# Validate specific file
|
||||
core sdk validate --spec api/openapi.yaml
|
||||
```
|
||||
|
||||
## sdk diff
|
||||
|
||||
Check for breaking changes between API versions.
|
||||
|
||||
```bash
|
||||
core sdk diff [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--base` | Base spec version (git tag or file path) |
|
||||
| `--spec` | Current spec file (auto-detected) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Compare against previous release
|
||||
core sdk diff --base v1.0.0
|
||||
|
||||
# Compare two files
|
||||
core sdk diff --base old-api.yaml --spec new-api.yaml
|
||||
```
|
||||
|
||||
### Breaking Changes Detected
|
||||
|
||||
- Removed endpoints
|
||||
- Changed parameter types
|
||||
- Removed required fields
|
||||
- Changed response types
|
||||
|
||||
## SDK Generation
|
||||
|
||||
SDK generation is handled by `core build sdk`, not this command.
|
||||
|
||||
```bash
|
||||
# Generate SDKs
|
||||
core build sdk
|
||||
|
||||
# Generate specific language
|
||||
core build sdk --lang typescript
|
||||
|
||||
# Preview without writing
|
||||
core build sdk --dry-run
|
||||
```
|
||||
|
||||
See [build sdk](../build/sdk/) for generation details.
|
||||
|
||||
## Spec Auto-Detection
|
||||
|
||||
Core looks for OpenAPI specs in this order:
|
||||
|
||||
1. Path specified in config (`sdk.spec`)
|
||||
2. `openapi.yaml` / `openapi.json`
|
||||
3. `api/openapi.yaml` / `api/openapi.json`
|
||||
4. `docs/openapi.yaml` / `docs/openapi.json`
|
||||
5. Laravel Scramble endpoint (`/docs/api.json`)
|
||||
|
||||
## See Also
|
||||
|
||||
- [build sdk](../build/sdk/) - Generate SDKs from OpenAPI
|
||||
- [ci command](../ci/) - Release workflow
|
||||
293
build/cli/setup/example.md
Normal file
293
build/cli/setup/example.md
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Setup Examples
|
||||
|
||||
## Clone from Registry
|
||||
|
||||
```bash
|
||||
# Clone all repos defined in repos.yaml
|
||||
core setup
|
||||
|
||||
# Preview what would be cloned
|
||||
core setup --dry-run
|
||||
|
||||
# Only foundation packages
|
||||
core setup --only foundation
|
||||
|
||||
# Multiple types
|
||||
core setup --only foundation,module
|
||||
|
||||
# Use specific registry file
|
||||
core setup --registry ~/projects/repos.yaml
|
||||
```
|
||||
|
||||
## Bootstrap New Workspace
|
||||
|
||||
```bash
|
||||
# In an empty directory - bootstraps in place
|
||||
mkdir my-workspace && cd my-workspace
|
||||
core setup
|
||||
|
||||
# Shows interactive wizard to select packages:
|
||||
# ┌─────────────────────────────────────────────┐
|
||||
# │ Select packages to clone │
|
||||
# │ Use space to select, enter to confirm │
|
||||
# │ │
|
||||
# │ ── Foundation (core framework) ── │
|
||||
# │ ☑ core-php Foundation framework │
|
||||
# │ ☑ core-tenant Multi-tenancy module │
|
||||
# │ │
|
||||
# │ ── Products (applications) ── │
|
||||
# │ ☐ core-bio Link-in-bio product │
|
||||
# │ ☐ core-social Social scheduling │
|
||||
# └─────────────────────────────────────────────┘
|
||||
|
||||
# Non-interactive: clone all packages
|
||||
core setup --all
|
||||
|
||||
# Create workspace in subdirectory
|
||||
cd ~/Code
|
||||
core setup --name my-project
|
||||
|
||||
# CI mode: fully non-interactive
|
||||
core setup --all --name ci-test
|
||||
```
|
||||
|
||||
## Setup Single Repository
|
||||
|
||||
```bash
|
||||
# In a git repo without .core/ configuration
|
||||
cd ~/Code/my-go-project
|
||||
core setup
|
||||
|
||||
# Shows choice dialog:
|
||||
# ┌─────────────────────────────────────────────┐
|
||||
# │ Setup options │
|
||||
# │ You're in a git repository. What would you │
|
||||
# │ like to do? │
|
||||
# │ │
|
||||
# │ ● Setup this repo (create .core/ config) │
|
||||
# │ ○ Create a new workspace (clone repos) │
|
||||
# └─────────────────────────────────────────────┘
|
||||
|
||||
# Preview generated configuration
|
||||
core setup --dry-run
|
||||
|
||||
# Output:
|
||||
# → Setting up repository configuration
|
||||
#
|
||||
# ✓ Detected project type: go
|
||||
# → Also found: (none)
|
||||
#
|
||||
# → Would create:
|
||||
# /Users/you/Code/my-go-project/.core/build.yaml
|
||||
#
|
||||
# Configuration preview:
|
||||
# version: 1
|
||||
# project:
|
||||
# name: my-go-project
|
||||
# description: Go application
|
||||
# main: ./cmd/my-go-project
|
||||
# binary: my-go-project
|
||||
# ...
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### repos.yaml (Workspace Registry)
|
||||
|
||||
```yaml
|
||||
org: host-uk
|
||||
base_path: .
|
||||
defaults:
|
||||
ci: github
|
||||
license: EUPL-1.2
|
||||
branch: main
|
||||
repos:
|
||||
core-php:
|
||||
type: foundation
|
||||
description: Foundation framework
|
||||
core-tenant:
|
||||
type: module
|
||||
depends_on: [core-php]
|
||||
description: Multi-tenancy module
|
||||
core-admin:
|
||||
type: module
|
||||
depends_on: [core-php, core-tenant]
|
||||
description: Admin panel
|
||||
core-bio:
|
||||
type: product
|
||||
depends_on: [core-php, core-tenant]
|
||||
description: Link-in-bio product
|
||||
domain: bio.host.uk.com
|
||||
core-devops:
|
||||
type: foundation
|
||||
clone: false # Already exists, skip cloning
|
||||
```
|
||||
|
||||
### .core/build.yaml (Repository Config)
|
||||
|
||||
Generated for Go projects:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
project:
|
||||
name: my-project
|
||||
description: Go application
|
||||
main: ./cmd/my-project
|
||||
binary: my-project
|
||||
build:
|
||||
cgo: false
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s
|
||||
- -w
|
||||
env: []
|
||||
targets:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
- os: windows
|
||||
arch: amd64
|
||||
sign:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
Generated for Wails projects:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
project:
|
||||
name: my-app
|
||||
description: Wails desktop application
|
||||
main: .
|
||||
binary: my-app
|
||||
targets:
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
- os: windows
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: amd64
|
||||
```
|
||||
|
||||
### .core/release.yaml (Release Config)
|
||||
|
||||
Generated for Go projects:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
project:
|
||||
name: my-project
|
||||
repository: owner/my-project
|
||||
|
||||
changelog:
|
||||
include:
|
||||
- feat
|
||||
- fix
|
||||
- perf
|
||||
- refactor
|
||||
exclude:
|
||||
- chore
|
||||
- docs
|
||||
- style
|
||||
- test
|
||||
|
||||
publishers:
|
||||
- type: github
|
||||
draft: false
|
||||
prerelease: false
|
||||
```
|
||||
|
||||
### .core/test.yaml (Test Config)
|
||||
|
||||
Generated for Go projects:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
commands:
|
||||
- name: unit
|
||||
run: go test ./...
|
||||
- name: coverage
|
||||
run: go test -coverprofile=coverage.out ./...
|
||||
- name: race
|
||||
run: go test -race ./...
|
||||
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
```
|
||||
|
||||
Generated for PHP projects:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
commands:
|
||||
- name: unit
|
||||
run: vendor/bin/pest --parallel
|
||||
- name: types
|
||||
run: vendor/bin/phpstan analyse
|
||||
- name: lint
|
||||
run: vendor/bin/pint --test
|
||||
|
||||
env:
|
||||
APP_ENV: testing
|
||||
DB_CONNECTION: sqlite
|
||||
```
|
||||
|
||||
Generated for Node.js projects:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
commands:
|
||||
- name: unit
|
||||
run: npm test
|
||||
- name: lint
|
||||
run: npm run lint
|
||||
- name: typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
env:
|
||||
NODE_ENV: test
|
||||
```
|
||||
|
||||
## Workflow Examples
|
||||
|
||||
### New Developer Setup
|
||||
|
||||
```bash
|
||||
# Clone the workspace
|
||||
mkdir host-uk && cd host-uk
|
||||
core setup
|
||||
|
||||
# Select packages in wizard, then:
|
||||
core health # Check all repos are healthy
|
||||
core doctor # Verify environment
|
||||
```
|
||||
|
||||
### CI Pipeline Setup
|
||||
|
||||
```bash
|
||||
# Non-interactive full clone
|
||||
core setup --all --name workspace
|
||||
|
||||
# Or with specific packages
|
||||
core setup --only foundation,module --name workspace
|
||||
```
|
||||
|
||||
### Adding Build Config to Existing Repo
|
||||
|
||||
```bash
|
||||
cd my-existing-project
|
||||
core setup # Choose "Setup this repo"
|
||||
# Edit .core/build.yaml as needed
|
||||
core build # Build the project
|
||||
```
|
||||
213
build/cli/setup/index.md
Normal file
213
build/cli/setup/index.md
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# core setup
|
||||
|
||||
Clone repositories from registry or bootstrap a new workspace.
|
||||
|
||||
## Overview
|
||||
|
||||
The `setup` command operates in three modes:
|
||||
|
||||
1. **Registry mode** - When `repos.yaml` exists nearby, clones repositories into packages/
|
||||
2. **Bootstrap mode** - When no registry exists, clones `core-devops` first, then presents an interactive wizard to select packages
|
||||
3. **Repo setup mode** - When run in a git repo root, offers to create `.core/build.yaml` configuration
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core setup [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
|
||||
| `--dry-run` | Show what would be cloned without cloning |
|
||||
| `--only` | Only clone repos of these types (comma-separated: foundation,module,product) |
|
||||
| `--all` | Skip wizard, clone all packages (non-interactive) |
|
||||
| `--name` | Project directory name for bootstrap mode |
|
||||
| `--build` | Run build after cloning |
|
||||
|
||||
---
|
||||
|
||||
## Registry Mode
|
||||
|
||||
When `repos.yaml` is found nearby (current directory or parents), setup clones all defined repositories:
|
||||
|
||||
```bash
|
||||
# In a directory with repos.yaml
|
||||
core setup
|
||||
|
||||
# Preview what would be cloned
|
||||
core setup --dry-run
|
||||
|
||||
# Only clone foundation packages
|
||||
core setup --only foundation
|
||||
|
||||
# Multiple types
|
||||
core setup --only foundation,module
|
||||
```
|
||||
|
||||
In registry mode with a TTY, an interactive wizard allows you to select which packages to clone. Use `--all` to skip the wizard and clone everything.
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap Mode
|
||||
|
||||
When no `repos.yaml` exists, setup enters bootstrap mode:
|
||||
|
||||
```bash
|
||||
# In an empty directory - bootstraps workspace in place
|
||||
mkdir my-project && cd my-project
|
||||
core setup
|
||||
|
||||
# In a non-empty directory - creates subdirectory
|
||||
cd ~/Code
|
||||
core setup --name my-workspace
|
||||
|
||||
# Non-interactive: clone all packages
|
||||
core setup --all --name ci-test
|
||||
```
|
||||
|
||||
Bootstrap mode:
|
||||
1. Detects if current directory is empty
|
||||
2. If not empty, prompts for project name (or uses `--name`)
|
||||
3. Clones `core-devops` (contains `repos.yaml`)
|
||||
4. Loads the registry from core-devops
|
||||
5. Shows interactive package selection wizard (unless `--all`)
|
||||
6. Clones selected packages
|
||||
7. Optionally runs build (with `--build`)
|
||||
|
||||
---
|
||||
|
||||
## Repo Setup Mode
|
||||
|
||||
When run in a git repository root (without `repos.yaml`), setup offers two choices:
|
||||
|
||||
1. **Setup Working Directory** - Creates `.core/build.yaml` based on detected project type
|
||||
2. **Create Package** - Creates a subdirectory and clones packages there
|
||||
|
||||
```bash
|
||||
cd ~/Code/my-go-project
|
||||
core setup
|
||||
|
||||
# Output:
|
||||
# >> This directory is a git repository
|
||||
# > Setup Working Directory
|
||||
# Create Package (clone repos into subdirectory)
|
||||
```
|
||||
|
||||
Choosing "Setup Working Directory" detects the project type and generates configuration:
|
||||
|
||||
| Detected File | Project Type |
|
||||
|---------------|--------------|
|
||||
| `wails.json` | Wails |
|
||||
| `go.mod` | Go |
|
||||
| `composer.json` | PHP |
|
||||
| `package.json` | Node.js |
|
||||
|
||||
Creates three config files in `.core/`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `build.yaml` | Build targets, flags, output settings |
|
||||
| `release.yaml` | Changelog format, GitHub release config |
|
||||
| `test.yaml` | Test commands, environment variables |
|
||||
|
||||
Also auto-detects GitHub repo from git remote for release config.
|
||||
|
||||
See [Configuration Files](example.md#configuration-files) for generated config examples.
|
||||
|
||||
---
|
||||
|
||||
## Interactive Wizard
|
||||
|
||||
When running in a terminal (TTY), the setup command presents an interactive multi-select wizard:
|
||||
|
||||
- Packages are grouped by type (foundation, module, product, template)
|
||||
- Use arrow keys to navigate
|
||||
- Press space to select/deselect packages
|
||||
- Type to filter the list
|
||||
- Press enter to confirm selection
|
||||
|
||||
The wizard is skipped when:
|
||||
- `--all` flag is specified
|
||||
- Not running in a TTY (e.g., CI pipelines)
|
||||
- `--dry-run` is specified
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Clone from Registry
|
||||
|
||||
```bash
|
||||
# Clone all repos (interactive wizard)
|
||||
core setup
|
||||
|
||||
# Clone all repos (non-interactive)
|
||||
core setup --all
|
||||
|
||||
# Preview without cloning
|
||||
core setup --dry-run
|
||||
|
||||
# Only foundation packages
|
||||
core setup --only foundation
|
||||
```
|
||||
|
||||
### Bootstrap New Workspace
|
||||
|
||||
```bash
|
||||
# Interactive bootstrap in empty directory
|
||||
mkdir workspace && cd workspace
|
||||
core setup
|
||||
|
||||
# Non-interactive with all packages
|
||||
core setup --all --name my-project
|
||||
|
||||
# Bootstrap and run build
|
||||
core setup --all --name my-project --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Registry Format
|
||||
|
||||
The registry file (`repos.yaml`) defines repositories. See [Configuration Files](example.md#configuration-files) for format.
|
||||
|
||||
---
|
||||
|
||||
## Finding Registry
|
||||
|
||||
Core looks for `repos.yaml` in:
|
||||
|
||||
1. Current directory
|
||||
2. Parent directories (walking up to root)
|
||||
3. `~/Code/host-uk/repos.yaml`
|
||||
4. `~/.config/core/repos.yaml`
|
||||
|
||||
---
|
||||
|
||||
## After Setup
|
||||
|
||||
```bash
|
||||
# Check workspace health
|
||||
core dev health
|
||||
|
||||
# Full workflow (status + commit + push)
|
||||
core dev work
|
||||
|
||||
# Build the project
|
||||
core build
|
||||
|
||||
# Run tests
|
||||
core go test # Go projects
|
||||
core php test # PHP projects
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [dev work](../dev/work/) - Multi-repo operations
|
||||
- [build](../build/) - Build projects
|
||||
- [doctor](../doctor/) - Check environment
|
||||
8
build/cli/test/example.md
Normal file
8
build/cli/test/example.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Test Examples
|
||||
|
||||
**Note:** Prefer `core go test` or `core php test` instead.
|
||||
|
||||
```bash
|
||||
core test
|
||||
core test --coverage
|
||||
```
|
||||
74
build/cli/test/index.md
Normal file
74
build/cli/test/index.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# core test
|
||||
|
||||
Run Go tests with coverage reporting.
|
||||
|
||||
Sets `MACOSX_DEPLOYMENT_TARGET=26.0` to suppress linker warnings on macOS.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core test [flags]
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--coverage` | Show detailed per-package coverage |
|
||||
| `--json` | Output JSON for CI/agents |
|
||||
| `--pkg` | Package pattern to test (default: ./...) |
|
||||
| `--race` | Enable race detector |
|
||||
| `--run` | Run only tests matching this regex |
|
||||
| `--short` | Skip long-running tests |
|
||||
| `--verbose` | Show test output as it runs |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Run all tests with coverage summary
|
||||
core test
|
||||
|
||||
# Show test output as it runs
|
||||
core test --verbose
|
||||
|
||||
# Detailed per-package coverage
|
||||
core test --coverage
|
||||
|
||||
# Test specific packages
|
||||
core test --pkg ./pkg/...
|
||||
|
||||
# Run specific test by name
|
||||
core test --run TestName
|
||||
|
||||
# Run tests matching pattern
|
||||
core test --run "Test.*Good"
|
||||
|
||||
# Skip long-running tests
|
||||
core test --short
|
||||
|
||||
# Enable race detector
|
||||
core test --race
|
||||
|
||||
# Output JSON for CI/agents
|
||||
core test --json
|
||||
```
|
||||
|
||||
## JSON Output
|
||||
|
||||
With `--json`, outputs structured results:
|
||||
|
||||
```json
|
||||
{
|
||||
"passed": 14,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"coverage": 75.1,
|
||||
"exit_code": 0,
|
||||
"failed_packages": []
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [go test](../go/test/) - Go-specific test options
|
||||
- [go cov](../go/cov/) - Coverage reports
|
||||
52
build/cli/vm/example.md
Normal file
52
build/cli/vm/example.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# VM Examples
|
||||
|
||||
## Running VMs
|
||||
|
||||
```bash
|
||||
# Run image
|
||||
core vm run server.iso
|
||||
|
||||
# Detached with resources
|
||||
core vm run -d --memory 4096 --cpus 4 server.iso
|
||||
|
||||
# From template
|
||||
core vm run --template core-dev --var SSH_KEY="ssh-rsa AAAA..."
|
||||
```
|
||||
|
||||
## Management
|
||||
|
||||
```bash
|
||||
# List running
|
||||
core vm ps
|
||||
|
||||
# Include stopped
|
||||
core vm ps -a
|
||||
|
||||
# Stop
|
||||
core vm stop abc123
|
||||
|
||||
# View logs
|
||||
core vm logs abc123
|
||||
|
||||
# Follow logs
|
||||
core vm logs -f abc123
|
||||
|
||||
# Execute command
|
||||
core vm exec abc123 ls -la
|
||||
|
||||
# Shell
|
||||
core vm exec abc123 /bin/sh
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
```bash
|
||||
# List
|
||||
core vm templates
|
||||
|
||||
# Show content
|
||||
core vm templates show core-dev
|
||||
|
||||
# Show variables
|
||||
core vm templates vars core-dev
|
||||
```
|
||||
163
build/cli/vm/index.md
Normal file
163
build/cli/vm/index.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# core vm
|
||||
|
||||
LinuxKit VM management.
|
||||
|
||||
LinuxKit VMs are lightweight, immutable VMs built from YAML templates.
|
||||
They run using qemu or hyperkit depending on your system.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core vm <command> [flags]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [`run`](#vm-run) | Run a LinuxKit image or template |
|
||||
| [`ps`](#vm-ps) | List running VMs |
|
||||
| [`stop`](#vm-stop) | Stop a VM |
|
||||
| [`logs`](#vm-logs) | View VM logs |
|
||||
| [`exec`](#vm-exec) | Execute command in VM |
|
||||
| [templates](templates/) | Manage LinuxKit templates |
|
||||
|
||||
---
|
||||
|
||||
## vm run
|
||||
|
||||
Run a LinuxKit image or build from a template.
|
||||
|
||||
```bash
|
||||
core vm run <image> [flags]
|
||||
core vm run --template <name> [flags]
|
||||
```
|
||||
|
||||
Supported image formats: `.iso`, `.qcow2`, `.vmdk`, `.raw`
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--template` | Run from a LinuxKit template (build + run) |
|
||||
| `--var` | Template variable in KEY=VALUE format (repeatable) |
|
||||
| `--name` | Name for the container |
|
||||
| `--memory` | Memory in MB (default: 1024) |
|
||||
| `--cpus` | CPU count (default: 1) |
|
||||
| `--ssh-port` | SSH port for exec commands (default: 2222) |
|
||||
| `-d` | Run in detached mode (background) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Run from image file
|
||||
core vm run image.iso
|
||||
|
||||
# Run detached with more resources
|
||||
core vm run -d image.qcow2 --memory 2048 --cpus 4
|
||||
|
||||
# Run from template
|
||||
core vm run --template core-dev --var SSH_KEY="ssh-rsa AAAA..."
|
||||
|
||||
# Multiple template variables
|
||||
core vm run --template server-php --var SSH_KEY="..." --var DOMAIN=example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## vm ps
|
||||
|
||||
List running VMs.
|
||||
|
||||
```bash
|
||||
core vm ps [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-a` | Show all (including stopped) |
|
||||
|
||||
### Output
|
||||
|
||||
```
|
||||
ID NAME IMAGE STATUS STARTED PID
|
||||
abc12345 myvm ...core-dev.qcow2 running 5m 12345
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## vm stop
|
||||
|
||||
Stop a running VM by ID or name.
|
||||
|
||||
```bash
|
||||
core vm stop <id>
|
||||
```
|
||||
|
||||
Supports partial ID matching.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Full ID
|
||||
core vm stop abc12345678
|
||||
|
||||
# Partial ID
|
||||
core vm stop abc1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## vm logs
|
||||
|
||||
View VM logs.
|
||||
|
||||
```bash
|
||||
core vm logs <id> [flags]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-f` | Follow log output |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
core vm logs abc12345
|
||||
|
||||
# Follow logs
|
||||
core vm logs -f abc1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## vm exec
|
||||
|
||||
Execute a command in a running VM via SSH.
|
||||
|
||||
```bash
|
||||
core vm exec <id> <command...>
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# List files
|
||||
core vm exec abc12345 ls -la
|
||||
|
||||
# Open shell
|
||||
core vm exec abc1 /bin/sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [templates](templates/) - Manage LinuxKit templates
|
||||
- [build](../build/) - Build LinuxKit images
|
||||
- [dev](../dev/) - Dev environment management
|
||||
53
build/cli/vm/templates/example.md
Normal file
53
build/cli/vm/templates/example.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# VM Templates Examples
|
||||
|
||||
## List
|
||||
|
||||
```bash
|
||||
core vm templates
|
||||
```
|
||||
|
||||
## Show
|
||||
|
||||
```bash
|
||||
core vm templates show core-dev
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
```bash
|
||||
core vm templates vars core-dev
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
Variables for core-dev:
|
||||
SSH_KEY (required) SSH public key
|
||||
MEMORY (optional) Memory in MB (default: 4096)
|
||||
CPUS (optional) CPU count (default: 4)
|
||||
```
|
||||
|
||||
## Using Templates
|
||||
|
||||
```bash
|
||||
core vm run --template core-dev --var SSH_KEY="ssh-rsa AAAA..."
|
||||
```
|
||||
|
||||
## Template Format
|
||||
|
||||
`.core/linuxkit/myserver.yml`:
|
||||
|
||||
```yaml
|
||||
kernel:
|
||||
image: linuxkit/kernel:5.15
|
||||
cmdline: "console=tty0"
|
||||
|
||||
init:
|
||||
- linuxkit/init:v1.0.0
|
||||
|
||||
services:
|
||||
- name: sshd
|
||||
image: linuxkit/sshd:v1.0.0
|
||||
- name: myapp
|
||||
image: ghcr.io/myorg/myapp:latest
|
||||
```
|
||||
124
build/cli/vm/templates/index.md
Normal file
124
build/cli/vm/templates/index.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# core vm templates
|
||||
|
||||
Manage LinuxKit templates for container images.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
core vm templates [command]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `list` | List available templates |
|
||||
| `show` | Show template details |
|
||||
| `vars` | Show template variables |
|
||||
|
||||
## templates list
|
||||
|
||||
List all available LinuxKit templates.
|
||||
|
||||
```bash
|
||||
core vm templates list
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
```
|
||||
Available Templates:
|
||||
|
||||
core-dev
|
||||
Full development environment with 100+ tools
|
||||
Platforms: linux/amd64, linux/arm64
|
||||
|
||||
server-php
|
||||
FrankenPHP production server
|
||||
Platforms: linux/amd64, linux/arm64
|
||||
|
||||
edge-node
|
||||
Minimal edge deployment
|
||||
Platforms: linux/amd64, linux/arm64
|
||||
```
|
||||
|
||||
## templates show
|
||||
|
||||
Show details of a specific template.
|
||||
|
||||
```bash
|
||||
core vm templates show <name>
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
core vm templates show core-dev
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Template: core-dev
|
||||
|
||||
Description: Full development environment with 100+ tools
|
||||
|
||||
Platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
Formats:
|
||||
- iso
|
||||
- qcow2
|
||||
|
||||
Services:
|
||||
- sshd
|
||||
- docker
|
||||
- frankenphp
|
||||
|
||||
Size: ~1.8GB
|
||||
```
|
||||
|
||||
## templates vars
|
||||
|
||||
Show variables defined by a template.
|
||||
|
||||
```bash
|
||||
core vm templates vars <name>
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
core vm templates vars core-dev
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Variables for core-dev:
|
||||
SSH_KEY (required) SSH public key
|
||||
MEMORY (optional) Memory in MB (default: 4096)
|
||||
CPUS (optional) CPU count (default: 4)
|
||||
```
|
||||
|
||||
## Template Locations
|
||||
|
||||
Templates are searched in order:
|
||||
|
||||
1. `.core/linuxkit/` - Project-specific
|
||||
2. `~/.core/templates/` - User templates
|
||||
3. Built-in templates
|
||||
|
||||
## Creating Templates
|
||||
|
||||
Create a LinuxKit YAML in `.core/linuxkit/`. See [Template Format](example.md#template-format) for examples.
|
||||
|
||||
Run with:
|
||||
|
||||
```bash
|
||||
core vm run --template myserver
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [vm command](../) - Run LinuxKit images
|
||||
- [build command](../../build/) - Build LinuxKit images
|
||||
357
build/go/configuration.md
Normal file
357
build/go/configuration.md
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# Configuration
|
||||
|
||||
Core uses `.core/` directory for project configuration.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
.core/
|
||||
├── release.yaml # Release configuration
|
||||
├── build.yaml # Build configuration (optional)
|
||||
├── php.yaml # PHP configuration (optional)
|
||||
└── linuxkit/ # LinuxKit templates
|
||||
├── server.yml
|
||||
└── dev.yml
|
||||
```
|
||||
|
||||
## release.yaml
|
||||
|
||||
Full release configuration reference:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: myapp
|
||||
repository: myorg/myapp
|
||||
|
||||
build:
|
||||
targets:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
- os: darwin
|
||||
arch: amd64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
- os: windows
|
||||
arch: amd64
|
||||
|
||||
publishers:
|
||||
# GitHub Releases (required - others reference these artifacts)
|
||||
- type: github
|
||||
prerelease: false
|
||||
draft: false
|
||||
|
||||
# npm binary wrapper
|
||||
- type: npm
|
||||
package: "@myorg/myapp"
|
||||
access: public # or "restricted"
|
||||
|
||||
# Homebrew formula
|
||||
- type: homebrew
|
||||
tap: myorg/homebrew-tap
|
||||
formula: myapp
|
||||
official:
|
||||
enabled: false
|
||||
output: dist/homebrew
|
||||
|
||||
# Scoop manifest (Windows)
|
||||
- type: scoop
|
||||
bucket: myorg/scoop-bucket
|
||||
official:
|
||||
enabled: false
|
||||
output: dist/scoop
|
||||
|
||||
# AUR (Arch Linux)
|
||||
- type: aur
|
||||
maintainer: "Name <email>"
|
||||
|
||||
# Chocolatey (Windows)
|
||||
- type: chocolatey
|
||||
push: false # true to publish
|
||||
|
||||
# Docker multi-arch
|
||||
- type: docker
|
||||
registry: ghcr.io
|
||||
image: myorg/myapp
|
||||
dockerfile: Dockerfile
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
tags:
|
||||
- latest
|
||||
- "{{.Version}}"
|
||||
build_args:
|
||||
VERSION: "{{.Version}}"
|
||||
|
||||
# LinuxKit images
|
||||
- type: linuxkit
|
||||
config: .core/linuxkit/server.yml
|
||||
formats:
|
||||
- iso
|
||||
- qcow2
|
||||
- docker
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
changelog:
|
||||
include:
|
||||
- feat
|
||||
- fix
|
||||
- perf
|
||||
- refactor
|
||||
exclude:
|
||||
- chore
|
||||
- docs
|
||||
- style
|
||||
- test
|
||||
- ci
|
||||
```
|
||||
|
||||
## build.yaml
|
||||
|
||||
Optional build configuration:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: myapp
|
||||
binary: myapp
|
||||
|
||||
build:
|
||||
main: ./cmd/myapp
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
|
||||
targets:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
```
|
||||
|
||||
## php.yaml
|
||||
|
||||
PHP/Laravel configuration:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
dev:
|
||||
domain: myapp.test
|
||||
ssl: true
|
||||
port: 8000
|
||||
services:
|
||||
- frankenphp
|
||||
- vite
|
||||
- horizon
|
||||
- reverb
|
||||
- redis
|
||||
|
||||
test:
|
||||
parallel: true
|
||||
coverage: false
|
||||
|
||||
deploy:
|
||||
coolify:
|
||||
server: https://coolify.example.com
|
||||
project: my-project
|
||||
environment: production
|
||||
```
|
||||
|
||||
## LinuxKit Templates
|
||||
|
||||
LinuxKit YAML configuration:
|
||||
|
||||
```yaml
|
||||
kernel:
|
||||
image: linuxkit/kernel:6.6
|
||||
cmdline: "console=tty0 console=ttyS0"
|
||||
|
||||
init:
|
||||
- linuxkit/init:latest
|
||||
- linuxkit/runc:latest
|
||||
- linuxkit/containerd:latest
|
||||
- linuxkit/ca-certificates:latest
|
||||
|
||||
onboot:
|
||||
- name: sysctl
|
||||
image: linuxkit/sysctl:latest
|
||||
|
||||
services:
|
||||
- name: dhcpcd
|
||||
image: linuxkit/dhcpcd:latest
|
||||
- name: sshd
|
||||
image: linuxkit/sshd:latest
|
||||
- name: myapp
|
||||
image: myorg/myapp:latest
|
||||
capabilities:
|
||||
- CAP_NET_BIND_SERVICE
|
||||
|
||||
files:
|
||||
- path: /etc/myapp/config.yaml
|
||||
contents: |
|
||||
server:
|
||||
port: 8080
|
||||
```
|
||||
|
||||
## repos.yaml
|
||||
|
||||
Package registry for multi-repo workspaces:
|
||||
|
||||
```yaml
|
||||
# Organisation name (used for GitHub URLs)
|
||||
org: host-uk
|
||||
|
||||
# Base path for cloning (default: current directory)
|
||||
base_path: .
|
||||
|
||||
# Default settings for all repos
|
||||
defaults:
|
||||
ci: github
|
||||
license: EUPL-1.2
|
||||
branch: main
|
||||
|
||||
# Repository definitions
|
||||
repos:
|
||||
# Foundation packages (no dependencies)
|
||||
core-php:
|
||||
type: foundation
|
||||
description: Foundation framework
|
||||
|
||||
core-devops:
|
||||
type: foundation
|
||||
description: Development environment
|
||||
clone: false # Skip during setup (already exists)
|
||||
|
||||
# Module packages (depend on foundation)
|
||||
core-tenant:
|
||||
type: module
|
||||
depends_on: [core-php]
|
||||
description: Multi-tenancy module
|
||||
|
||||
core-admin:
|
||||
type: module
|
||||
depends_on: [core-php, core-tenant]
|
||||
description: Admin panel
|
||||
|
||||
core-api:
|
||||
type: module
|
||||
depends_on: [core-php]
|
||||
description: REST API framework
|
||||
|
||||
# Product packages (user-facing applications)
|
||||
core-bio:
|
||||
type: product
|
||||
depends_on: [core-php, core-tenant]
|
||||
description: Link-in-bio product
|
||||
domain: bio.host.uk.com
|
||||
|
||||
core-social:
|
||||
type: product
|
||||
depends_on: [core-php, core-tenant]
|
||||
description: Social scheduling
|
||||
domain: social.host.uk.com
|
||||
|
||||
# Templates
|
||||
core-template:
|
||||
type: template
|
||||
description: Starter template for new projects
|
||||
```
|
||||
|
||||
### repos.yaml Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `org` | Yes | GitHub organisation name |
|
||||
| `base_path` | No | Directory for cloning (default: `.`) |
|
||||
| `defaults` | No | Default settings applied to all repos |
|
||||
| `repos` | Yes | Map of repository definitions |
|
||||
|
||||
### Repository Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `type` | Yes | `foundation`, `module`, `product`, or `template` |
|
||||
| `description` | No | Human-readable description |
|
||||
| `depends_on` | No | List of package dependencies |
|
||||
| `clone` | No | Set `false` to skip during setup |
|
||||
| `domain` | No | Production domain (for products) |
|
||||
| `branch` | No | Override default branch |
|
||||
|
||||
### Package Types
|
||||
|
||||
| Type | Description | Dependencies |
|
||||
|------|-------------|--------------|
|
||||
| `foundation` | Core framework packages | None |
|
||||
| `module` | Reusable modules | Foundation packages |
|
||||
| `product` | User-facing applications | Foundation + modules |
|
||||
| `template` | Starter templates | Any |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Complete reference of environment variables used by Core CLI.
|
||||
|
||||
### Authentication
|
||||
|
||||
| Variable | Used By | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GITHUB_TOKEN` | `core ci`, `core dev` | GitHub API authentication |
|
||||
| `ANTHROPIC_API_KEY` | `core ai`, `core dev claude` | Claude API key |
|
||||
| `AGENTIC_TOKEN` | `core ai task*` | Agentic API authentication |
|
||||
| `AGENTIC_BASE_URL` | `core ai task*` | Agentic API endpoint |
|
||||
|
||||
### Publishing
|
||||
|
||||
| Variable | Used By | Description |
|
||||
|----------|---------|-------------|
|
||||
| `NPM_TOKEN` | `core ci` (npm publisher) | npm registry auth token |
|
||||
| `CHOCOLATEY_API_KEY` | `core ci` (chocolatey publisher) | Chocolatey API key |
|
||||
| `DOCKER_USERNAME` | `core ci` (docker publisher) | Docker registry username |
|
||||
| `DOCKER_PASSWORD` | `core ci` (docker publisher) | Docker registry password |
|
||||
|
||||
### Deployment
|
||||
|
||||
| Variable | Used By | Description |
|
||||
|----------|---------|-------------|
|
||||
| `COOLIFY_URL` | `core php deploy` | Coolify server URL |
|
||||
| `COOLIFY_TOKEN` | `core php deploy` | Coolify API token |
|
||||
| `COOLIFY_APP_ID` | `core php deploy` | Production application ID |
|
||||
| `COOLIFY_STAGING_APP_ID` | `core php deploy --staging` | Staging application ID |
|
||||
|
||||
### Build
|
||||
|
||||
| Variable | Used By | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CGO_ENABLED` | `core build`, `core go *` | Enable/disable CGO (default: 0) |
|
||||
| `GOOS` | `core build` | Target operating system |
|
||||
| `GOARCH` | `core build` | Target architecture |
|
||||
|
||||
### Configuration Paths
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `CORE_CONFIG` | Override config directory (default: `~/.core/`) |
|
||||
| `CORE_REGISTRY` | Override repos.yaml path |
|
||||
|
||||
---
|
||||
|
||||
## Defaults
|
||||
|
||||
If no configuration exists, sensible defaults are used:
|
||||
|
||||
- **Targets**: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
|
||||
- **Publishers**: GitHub only
|
||||
- **Changelog**: feat, fix, perf, refactor included
|
||||
191
build/go/getting-started.md
Normal file
191
build/go/getting-started.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Getting Started
|
||||
|
||||
This guide walks you through installing Core and running your first build.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing Core, ensure you have:
|
||||
|
||||
| Tool | Minimum Version | Check Command |
|
||||
|------|-----------------|---------------|
|
||||
| Go | 1.23+ | `go version` |
|
||||
| Git | 2.30+ | `git --version` |
|
||||
|
||||
Optional (for specific features):
|
||||
|
||||
| Tool | Required For | Install |
|
||||
|------|--------------|---------|
|
||||
| `gh` | GitHub integration (`core dev issues`, `core dev reviews`) | [cli.github.com](https://cli.github.com) |
|
||||
| Docker | Container builds | [docker.com](https://docker.com) |
|
||||
| `task` | Task automation | `go install github.com/go-task/task/v3/cmd/task@latest` |
|
||||
|
||||
## Installation
|
||||
|
||||
### Option 1: Go Install (Recommended)
|
||||
|
||||
```bash
|
||||
# Install latest release
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
|
||||
# Verify installation
|
||||
core doctor
|
||||
```
|
||||
|
||||
If `core: command not found`, add Go's bin directory to your PATH:
|
||||
|
||||
```bash
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
```
|
||||
|
||||
### Option 2: Download Binary
|
||||
|
||||
Download pre-built binaries from [GitHub Releases](https://github.com/host-uk/core/releases):
|
||||
|
||||
```bash
|
||||
# macOS (Apple Silicon)
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-darwin-arm64
|
||||
chmod +x core
|
||||
sudo mv core /usr/local/bin/
|
||||
|
||||
# macOS (Intel)
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-darwin-amd64
|
||||
chmod +x core
|
||||
sudo mv core /usr/local/bin/
|
||||
|
||||
# Linux (x86_64)
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-linux-amd64
|
||||
chmod +x core
|
||||
sudo mv core /usr/local/bin/
|
||||
```
|
||||
|
||||
### Option 3: Build from Source
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/host-uk/core.git
|
||||
cd core
|
||||
|
||||
# Build with Task (recommended)
|
||||
task cli:build
|
||||
# Binary at ./bin/core
|
||||
|
||||
# Or build with Go directly
|
||||
CGO_ENABLED=0 go build -o core ./cmd/core/
|
||||
sudo mv core /usr/local/bin/
|
||||
```
|
||||
|
||||
## Your First Build
|
||||
|
||||
### 1. Navigate to a Go Project
|
||||
|
||||
```bash
|
||||
cd ~/Code/my-go-project
|
||||
```
|
||||
|
||||
### 2. Initialise Configuration
|
||||
|
||||
```bash
|
||||
core setup
|
||||
```
|
||||
|
||||
This detects your project type and creates configuration files in `.core/`:
|
||||
- `build.yaml` - Build settings
|
||||
- `release.yaml` - Release configuration
|
||||
- `test.yaml` - Test commands
|
||||
|
||||
### 3. Build
|
||||
|
||||
```bash
|
||||
core build
|
||||
```
|
||||
|
||||
Output appears in `dist/`:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── my-project-darwin-arm64.tar.gz
|
||||
├── my-project-linux-amd64.tar.gz
|
||||
└── CHECKSUMS.txt
|
||||
```
|
||||
|
||||
### 4. Cross-Compile (Optional)
|
||||
|
||||
```bash
|
||||
core build --targets linux/amd64,linux/arm64,darwin/arm64,windows/amd64
|
||||
```
|
||||
|
||||
## Your First Release
|
||||
|
||||
Releases are **safe by default** - Core runs in dry-run mode unless you explicitly confirm.
|
||||
|
||||
### 1. Preview
|
||||
|
||||
```bash
|
||||
core ci
|
||||
```
|
||||
|
||||
This shows what would be published without actually publishing.
|
||||
|
||||
### 2. Publish
|
||||
|
||||
```bash
|
||||
core ci --we-are-go-for-launch
|
||||
```
|
||||
|
||||
This creates a GitHub release with your built artifacts.
|
||||
|
||||
## Multi-Repo Workflow
|
||||
|
||||
If you work with multiple repositories (like the host-uk ecosystem):
|
||||
|
||||
### 1. Clone All Repositories
|
||||
|
||||
```bash
|
||||
mkdir host-uk && cd host-uk
|
||||
core setup
|
||||
```
|
||||
|
||||
Select packages in the interactive wizard.
|
||||
|
||||
### 2. Check Status
|
||||
|
||||
```bash
|
||||
core dev health
|
||||
# Output: "18 repos │ clean │ synced"
|
||||
```
|
||||
|
||||
### 3. Work Across Repos
|
||||
|
||||
```bash
|
||||
core dev work --status # See status table
|
||||
core dev work # Commit and push all dirty repos
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
| Task | Command | Documentation |
|
||||
|------|---------|---------------|
|
||||
| Run tests | `core go test` | [go/test](cmd/go/test/) |
|
||||
| Format code | `core go fmt --fix` | [go/fmt](cmd/go/fmt/) |
|
||||
| Lint code | `core go lint` | [go/lint](cmd/go/lint/) |
|
||||
| PHP development | `core php dev` | [php](cmd/php/) |
|
||||
| View all commands | `core --help` | [cmd](cmd/) |
|
||||
|
||||
## Getting Help
|
||||
|
||||
```bash
|
||||
# Check environment
|
||||
core doctor
|
||||
|
||||
# Command help
|
||||
core <command> --help
|
||||
|
||||
# Full documentation
|
||||
https://github.com/host-uk/core/tree/main/docs
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Configuration](configuration.md) - All config options
|
||||
- [Workflows](workflows.md) - Common task sequences
|
||||
- [Troubleshooting](troubleshooting.md) - When things go wrong
|
||||
112
build/go/glossary.md
Normal file
112
build/go/glossary.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Glossary
|
||||
|
||||
Definitions of terms used throughout Core CLI documentation.
|
||||
|
||||
## A
|
||||
|
||||
### Artifact
|
||||
A file produced by a build, typically a binary, archive, or checksum file. Artifacts are stored in the `dist/` directory and published during releases.
|
||||
|
||||
## C
|
||||
|
||||
### CGO
|
||||
Go's mechanism for calling C code. Core disables CGO by default (`CGO_ENABLED=0`) to produce statically-linked binaries that don't depend on system libraries.
|
||||
|
||||
### Changelog
|
||||
Automatically generated list of changes between releases, created from conventional commit messages. Configure in `.core/release.yaml`.
|
||||
|
||||
### Conventional Commits
|
||||
A commit message format: `type(scope): description`. Types include `feat`, `fix`, `docs`, `chore`. Core uses this to generate changelogs.
|
||||
|
||||
## D
|
||||
|
||||
### Dry-run
|
||||
A mode where commands show what they would do without actually doing it. `core ci` runs in dry-run mode by default for safety.
|
||||
|
||||
## F
|
||||
|
||||
### Foundation Package
|
||||
A core package with no dependencies on other packages. Examples: `core-php`, `core-devops`. These form the base of the dependency tree.
|
||||
|
||||
### FrankenPHP
|
||||
A modern PHP application server used by `core php dev`. Combines PHP with Caddy for high-performance serving.
|
||||
|
||||
## G
|
||||
|
||||
### `gh`
|
||||
The GitHub CLI tool. Required for commands that interact with GitHub: `core dev issues`, `core dev reviews`, `core dev ci`.
|
||||
|
||||
## L
|
||||
|
||||
### LinuxKit
|
||||
A toolkit for building lightweight, immutable Linux distributions. Core can build LinuxKit images via `core build --type linuxkit`.
|
||||
|
||||
## M
|
||||
|
||||
### Module (Go)
|
||||
A collection of Go packages with a `go.mod` file. Core's Go commands operate on modules.
|
||||
|
||||
### Module (Package)
|
||||
A host-uk package that depends on foundation packages. Examples: `core-tenant`, `core-admin`. Compare with **Foundation Package** and **Product**.
|
||||
|
||||
## P
|
||||
|
||||
### Package
|
||||
An individual repository in the host-uk ecosystem. Packages are defined in `repos.yaml` and managed with `core pkg` commands.
|
||||
|
||||
### Package Index
|
||||
The `repos.yaml` file that lists all packages in a workspace. Contains metadata like dependencies, type, and description.
|
||||
|
||||
### Product
|
||||
A user-facing application package. Examples: `core-bio`, `core-social`. Products depend on foundation and module packages.
|
||||
|
||||
### Publisher
|
||||
A release target configured in `.core/release.yaml`. Types include `github`, `docker`, `npm`, `homebrew`, `linuxkit`.
|
||||
|
||||
## R
|
||||
|
||||
### Registry (Docker/npm)
|
||||
A remote repository for container images or npm packages. Core can publish to registries during releases.
|
||||
|
||||
### `repos.yaml`
|
||||
The package index file defining all repositories in a workspace. Used by multi-repo commands like `core dev work`.
|
||||
|
||||
## S
|
||||
|
||||
### SDK
|
||||
Software Development Kit. Core can generate API client SDKs from OpenAPI specs via `core build sdk`.
|
||||
|
||||
## T
|
||||
|
||||
### Target
|
||||
A build target specified as `os/arch`, e.g., `linux/amd64`, `darwin/arm64`. Use `--targets` flag to specify.
|
||||
|
||||
## W
|
||||
|
||||
### Wails
|
||||
A framework for building desktop applications with Go backends and web frontends. Core detects Wails projects and uses appropriate build commands.
|
||||
|
||||
### Workspace (Go)
|
||||
A Go 1.18+ feature for working with multiple modules simultaneously. Managed via `core go work` commands.
|
||||
|
||||
### Workspace (Multi-repo)
|
||||
A directory containing multiple packages from `repos.yaml`. Created via `core setup` and managed with `core dev` commands.
|
||||
|
||||
## Symbols
|
||||
|
||||
### `.core/`
|
||||
Directory containing project configuration files:
|
||||
- `build.yaml` - Build settings
|
||||
- `release.yaml` - Release targets
|
||||
- `test.yaml` - Test configuration
|
||||
- `linuxkit/` - LinuxKit templates
|
||||
|
||||
### `--we-are-go-for-launch`
|
||||
Flag to disable dry-run mode and actually publish a release. Named as a deliberate friction to prevent accidental releases.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Configuration](configuration.md) - Config file reference
|
||||
- [Getting Started](getting-started.md) - First-time setup
|
||||
98
build/go/index.md
Normal file
98
build/go/index.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Core Go
|
||||
|
||||
Core is a Go framework for the host-uk ecosystem - build, release, and deploy Go, Wails, PHP, and container workloads.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Via Go (recommended)
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
|
||||
# Or download binary from releases
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-$(go env GOOS)-$(go env GOARCH)
|
||||
chmod +x core && sudo mv core /usr/local/bin/
|
||||
|
||||
# Verify
|
||||
core doctor
|
||||
```
|
||||
|
||||
See [Getting Started](getting-started.md) for all installation options including building from source.
|
||||
|
||||
## Command Reference
|
||||
|
||||
See [CLI](/build/cli/) for full command documentation.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| [go](/build/cli/go/) | Go development (test, fmt, lint, cov) |
|
||||
| [php](/build/cli/php/) | Laravel/PHP development |
|
||||
| [build](/build/cli/build/) | Build Go, Wails, Docker, LinuxKit projects |
|
||||
| [ci](/build/cli/ci/) | Publish releases (dry-run by default) |
|
||||
| [sdk](/build/cli/sdk/) | SDK generation and validation |
|
||||
| [dev](/build/cli/dev/) | Multi-repo workflow + dev environment |
|
||||
| [pkg](/build/cli/pkg/) | Package search and install |
|
||||
| [vm](/build/cli/vm/) | LinuxKit VM management |
|
||||
| [docs](/build/cli/docs/) | Documentation management |
|
||||
| [setup](/build/cli/setup/) | Clone repos from registry |
|
||||
| [doctor](/build/cli/doctor/) | Check development environment |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Go development
|
||||
core go test # Run tests
|
||||
core go test --coverage # With coverage
|
||||
core go fmt # Format code
|
||||
core go lint # Lint code
|
||||
|
||||
# Build
|
||||
core build # Auto-detect and build
|
||||
core build --targets linux/amd64,darwin/arm64
|
||||
|
||||
# Release (dry-run by default)
|
||||
core ci # Preview release
|
||||
core ci --we-are-go-for-launch # Actually publish
|
||||
|
||||
# Multi-repo workflow
|
||||
core dev work # Status + commit + push
|
||||
core dev work --status # Just show status
|
||||
|
||||
# PHP development
|
||||
core php dev # Start dev environment
|
||||
core php test # Run tests
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Core uses `.core/` directory for project configuration:
|
||||
|
||||
```
|
||||
.core/
|
||||
├── release.yaml # Release targets and settings
|
||||
├── build.yaml # Build configuration (optional)
|
||||
└── linuxkit/ # LinuxKit templates
|
||||
```
|
||||
|
||||
And `repos.yaml` in workspace root for multi-repo management.
|
||||
|
||||
## Guides
|
||||
|
||||
- [Getting Started](getting-started.md) - Installation and first steps
|
||||
- [Workflows](workflows.md) - Common task sequences
|
||||
- [Troubleshooting](troubleshooting.md) - When things go wrong
|
||||
- [Migration](migration.md) - Moving from legacy tools
|
||||
|
||||
## Reference
|
||||
|
||||
- [Configuration](configuration.md) - All config options
|
||||
- [Glossary](glossary.md) - Term definitions
|
||||
|
||||
## Claude Code Skill
|
||||
|
||||
Install the skill to teach Claude Code how to use the Core CLI:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/host-uk/core/main/.claude/skills/core/install.sh | bash
|
||||
```
|
||||
|
||||
See [skill/](skill/) for details.
|
||||
233
build/go/migration.md
Normal file
233
build/go/migration.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# Migration Guide
|
||||
|
||||
Migrating from legacy scripts and tools to Core CLI.
|
||||
|
||||
## From push-all.sh
|
||||
|
||||
The `push-all.sh` script has been replaced by `core dev` commands.
|
||||
|
||||
| Legacy | Core CLI | Notes |
|
||||
|--------|----------|-------|
|
||||
| `./push-all.sh --status` | `core dev work --status` | Status table |
|
||||
| `./push-all.sh --commit` | `core dev commit` | Commit dirty repos |
|
||||
| `./push-all.sh` | `core dev work` | Full workflow |
|
||||
|
||||
### Quick Migration
|
||||
|
||||
```bash
|
||||
# Instead of
|
||||
./push-all.sh --status
|
||||
|
||||
# Use
|
||||
core dev work --status
|
||||
```
|
||||
|
||||
### New Features
|
||||
|
||||
Core CLI adds features not available in the legacy script:
|
||||
|
||||
```bash
|
||||
# Quick health summary
|
||||
core dev health
|
||||
# Output: "18 repos │ clean │ synced"
|
||||
|
||||
# Pull repos that are behind
|
||||
core dev pull
|
||||
|
||||
# GitHub integration
|
||||
core dev issues # List open issues
|
||||
core dev reviews # List PRs needing review
|
||||
core dev ci # Check CI status
|
||||
|
||||
# Dependency analysis
|
||||
core dev impact core-php # What depends on core-php?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## From Raw Go Commands
|
||||
|
||||
Core wraps Go commands with enhanced defaults and output.
|
||||
|
||||
| Raw Command | Core CLI | Benefits |
|
||||
|-------------|----------|----------|
|
||||
| `go test ./...` | `core go test` | Filters warnings, sets CGO_ENABLED=0 |
|
||||
| `go test -coverprofile=...` | `core go cov` | HTML reports, thresholds |
|
||||
| `gofmt -w .` | `core go fmt --fix` | Uses goimports if available |
|
||||
| `golangci-lint run` | `core go lint` | Consistent interface |
|
||||
| `go build` | `core build` | Cross-compile, sign, archive |
|
||||
|
||||
### Why Use Core?
|
||||
|
||||
```bash
|
||||
# Raw go test shows linker warnings on macOS
|
||||
go test ./...
|
||||
# ld: warning: -no_pie is deprecated...
|
||||
|
||||
# Core filters noise
|
||||
core go test
|
||||
# PASS (clean output)
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Core automatically sets:
|
||||
- `CGO_ENABLED=0` - Static binaries
|
||||
- `MACOSX_DEPLOYMENT_TARGET=26.0` - Suppress macOS warnings
|
||||
- Colour output for coverage reports
|
||||
|
||||
---
|
||||
|
||||
## From Raw PHP Commands
|
||||
|
||||
Core orchestrates Laravel development services.
|
||||
|
||||
| Raw Command | Core CLI | Benefits |
|
||||
|-------------|----------|----------|
|
||||
| `php artisan serve` | `core php dev` | Adds Vite, Horizon, Reverb, Redis |
|
||||
| `./vendor/bin/pest` | `core php test` | Auto-detects test runner |
|
||||
| `./vendor/bin/pint` | `core php fmt --fix` | Consistent interface |
|
||||
| Manual Coolify deploy | `core php deploy` | Tracked, scriptable |
|
||||
|
||||
### Development Server Comparison
|
||||
|
||||
```bash
|
||||
# Raw: Start each service manually
|
||||
php artisan serve &
|
||||
npm run dev &
|
||||
php artisan horizon &
|
||||
php artisan reverb:start &
|
||||
|
||||
# Core: One command
|
||||
core php dev
|
||||
# Starts all services, shows unified logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## From goreleaser
|
||||
|
||||
Core's release system is simpler than goreleaser for host-uk projects.
|
||||
|
||||
| goreleaser | Core CLI |
|
||||
|------------|----------|
|
||||
| `.goreleaser.yaml` | `.core/release.yaml` |
|
||||
| `goreleaser release --snapshot` | `core ci` (dry-run) |
|
||||
| `goreleaser release` | `core ci --we-are-go-for-launch` |
|
||||
|
||||
### Configuration Migration
|
||||
|
||||
**goreleaser:**
|
||||
```yaml
|
||||
builds:
|
||||
- main: ./cmd/app
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm64]
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
files: [LICENSE, README.md]
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: host-uk
|
||||
name: app
|
||||
```
|
||||
|
||||
**Core:**
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: app
|
||||
repository: host-uk/app
|
||||
|
||||
targets:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
|
||||
publishers:
|
||||
- type: github
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
1. **Separate build and release** - Core separates `core build` from `core ci`
|
||||
2. **Safe by default** - `core ci` is dry-run unless `--we-are-go-for-launch`
|
||||
3. **Simpler config** - Fewer options, sensible defaults
|
||||
|
||||
---
|
||||
|
||||
## From Manual Git Operations
|
||||
|
||||
Core automates multi-repo git workflows.
|
||||
|
||||
| Manual | Core CLI |
|
||||
|--------|----------|
|
||||
| `cd repo1 && git status && cd ../repo2 && ...` | `core dev work --status` |
|
||||
| Check each repo for uncommitted changes | `core dev health` |
|
||||
| Commit each repo individually | `core dev commit` |
|
||||
| Push each repo individually | `core dev push` |
|
||||
|
||||
### Example: Committing Across Repos
|
||||
|
||||
**Manual:**
|
||||
```bash
|
||||
cd core-php
|
||||
git add -A
|
||||
git commit -m "feat: add feature"
|
||||
cd ../core-tenant
|
||||
git add -A
|
||||
git commit -m "feat: use new feature"
|
||||
# ... repeat for each repo
|
||||
```
|
||||
|
||||
**Core:**
|
||||
```bash
|
||||
core dev commit
|
||||
# Interactive: reviews changes, suggests messages
|
||||
# Adds Co-Authored-By automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deprecated Commands
|
||||
|
||||
These commands have been removed or renamed:
|
||||
|
||||
| Deprecated | Replacement | Version |
|
||||
|------------|-------------|---------|
|
||||
| `core sdk generate` | `core build sdk` | v0.5.0 |
|
||||
| `core dev task*` | `core ai task*` | v0.8.0 |
|
||||
| `core release` | `core ci` | v0.6.0 |
|
||||
|
||||
---
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
| Core Version | Go Version | Breaking Changes |
|
||||
|--------------|------------|------------------|
|
||||
| v1.0.0+ | 1.23+ | Stable API |
|
||||
| v0.8.0 | 1.22+ | Task commands moved to `ai` |
|
||||
| v0.6.0 | 1.22+ | Release command renamed to `ci` |
|
||||
| v0.5.0 | 1.21+ | SDK generation moved to `build sdk` |
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues during migration:
|
||||
|
||||
1. Check [Troubleshooting](troubleshooting.md)
|
||||
2. Run `core doctor` to verify setup
|
||||
3. Use `--help` on any command: `core dev work --help`
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Getting Started](getting-started.md) - Fresh installation
|
||||
- [Workflows](workflows.md) - Common task sequences
|
||||
- [Configuration](configuration.md) - Config file reference
|
||||
35
build/go/skill/index.md
Normal file
35
build/go/skill/index.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Claude Code Skill
|
||||
|
||||
The `core` skill teaches Claude Code how to use the Core CLI effectively.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/host-uk/core/main/.claude/skills/core/install.sh | bash
|
||||
```
|
||||
|
||||
Or if you have the repo cloned:
|
||||
|
||||
```bash
|
||||
./.claude/skills/core/install.sh
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
Once installed, Claude Code will:
|
||||
|
||||
- Auto-invoke when working in host-uk repositories
|
||||
- Use `core` commands instead of raw `go`/`php`/`git` commands
|
||||
- Follow the correct patterns for testing, building, and releasing
|
||||
|
||||
## Manual invocation
|
||||
|
||||
Type `/core` in Claude Code to invoke the skill manually.
|
||||
|
||||
## Updating
|
||||
|
||||
Re-run the install command to update to the latest version.
|
||||
|
||||
## Location
|
||||
|
||||
Skills are installed to `~/.claude/skills/core/SKILL.md`.
|
||||
332
build/go/troubleshooting.md
Normal file
332
build/go/troubleshooting.md
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
# Troubleshooting
|
||||
|
||||
Common issues and how to resolve them.
|
||||
|
||||
## Installation Issues
|
||||
|
||||
### "command not found: core"
|
||||
|
||||
**Cause:** Go's bin directory is not in your PATH.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Add to ~/.bashrc or ~/.zshrc
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
|
||||
# Then reload
|
||||
source ~/.bashrc # or ~/.zshrc
|
||||
```
|
||||
|
||||
### "go: module github.com/host-uk/core: no matching versions"
|
||||
|
||||
**Cause:** Go module proxy hasn't cached the latest version yet.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Bypass proxy
|
||||
GOPROXY=direct go install github.com/host-uk/core/cmd/core@latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Issues
|
||||
|
||||
### "no Go files in..."
|
||||
|
||||
**Cause:** Core couldn't find a main package to build.
|
||||
|
||||
**Fix:**
|
||||
|
||||
1. Check you're in the correct directory
|
||||
2. Ensure `.core/build.yaml` has the correct `main` path:
|
||||
|
||||
```yaml
|
||||
project:
|
||||
main: ./cmd/myapp # Path to main package
|
||||
```
|
||||
|
||||
### "CGO_ENABLED=1 but no C compiler"
|
||||
|
||||
**Cause:** Build requires CGO but no C compiler is available.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Option 1: Disable CGO (if not needed)
|
||||
core build # Core disables CGO by default
|
||||
|
||||
# Option 2: Install a C compiler
|
||||
# macOS
|
||||
xcode-select --install
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install build-essential
|
||||
|
||||
# Windows
|
||||
# Install MinGW or use WSL
|
||||
```
|
||||
|
||||
### Build succeeds but binary doesn't run
|
||||
|
||||
**Cause:** Built for wrong architecture.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Check what you built
|
||||
file dist/myapp-*
|
||||
|
||||
# Build for your current platform
|
||||
core build --targets $(go env GOOS)/$(go env GOARCH)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Release Issues
|
||||
|
||||
### "dry-run mode, use --we-are-go-for-launch to publish"
|
||||
|
||||
**This is expected behaviour.** Core runs in dry-run mode by default for safety.
|
||||
|
||||
**To actually publish:**
|
||||
|
||||
```bash
|
||||
core ci --we-are-go-for-launch
|
||||
```
|
||||
|
||||
### "failed to create release: 401 Unauthorized"
|
||||
|
||||
**Cause:** GitHub token missing or invalid.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Authenticate with GitHub CLI
|
||||
gh auth login
|
||||
|
||||
# Or set token directly
|
||||
export GITHUB_TOKEN=ghp_xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### "no artifacts found in dist/"
|
||||
|
||||
**Cause:** You need to build before releasing.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Build first
|
||||
core build
|
||||
|
||||
# Then release
|
||||
core ci --we-are-go-for-launch
|
||||
```
|
||||
|
||||
### "tag already exists"
|
||||
|
||||
**Cause:** Trying to release a version that's already been released.
|
||||
|
||||
**Fix:**
|
||||
|
||||
1. Update version in your code/config
|
||||
2. Or delete the existing tag (if intentional):
|
||||
|
||||
```bash
|
||||
git tag -d v1.0.0
|
||||
git push origin :refs/tags/v1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Repo Issues
|
||||
|
||||
### "repos.yaml not found"
|
||||
|
||||
**Cause:** Core can't find the package registry.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Core looks for `repos.yaml` in:
|
||||
1. Current directory
|
||||
2. Parent directories (walking up to root)
|
||||
3. `~/Code/host-uk/repos.yaml`
|
||||
4. `~/.config/core/repos.yaml`
|
||||
|
||||
Either:
|
||||
- Run commands from a directory with `repos.yaml`
|
||||
- Use `--registry /path/to/repos.yaml`
|
||||
- Run `core setup` to bootstrap a new workspace
|
||||
|
||||
### "failed to clone: Permission denied (publickey)"
|
||||
|
||||
**Cause:** SSH key not configured for GitHub.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Check SSH connection
|
||||
ssh -T git@github.com
|
||||
|
||||
# If that fails, add your key
|
||||
ssh-add ~/.ssh/id_ed25519
|
||||
|
||||
# Or configure SSH
|
||||
# See: https://docs.github.com/en/authentication/connecting-to-github-with-ssh
|
||||
```
|
||||
|
||||
### "repository not found" during setup
|
||||
|
||||
**Cause:** You don't have access to the repository, or it doesn't exist.
|
||||
|
||||
**Fix:**
|
||||
|
||||
1. Check you're authenticated: `gh auth status`
|
||||
2. Verify the repo exists and you have access
|
||||
3. For private repos, ensure your token has `repo` scope
|
||||
|
||||
---
|
||||
|
||||
## GitHub Integration Issues
|
||||
|
||||
### "gh: command not found"
|
||||
|
||||
**Cause:** GitHub CLI not installed.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install gh
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install gh
|
||||
|
||||
# Windows
|
||||
winget install GitHub.cli
|
||||
|
||||
# Then authenticate
|
||||
gh auth login
|
||||
```
|
||||
|
||||
### "core dev issues" shows nothing
|
||||
|
||||
**Possible causes:**
|
||||
|
||||
1. No open issues exist
|
||||
2. Not authenticated with GitHub
|
||||
3. Not in a directory with `repos.yaml`
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Check auth
|
||||
gh auth status
|
||||
|
||||
# Check you're in a workspace
|
||||
ls repos.yaml
|
||||
|
||||
# Show all issues including closed
|
||||
core dev issues --all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHP Issues
|
||||
|
||||
### "frankenphp: command not found"
|
||||
|
||||
**Cause:** FrankenPHP not installed.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install frankenphp
|
||||
|
||||
# Or use Docker
|
||||
core php dev --docker
|
||||
```
|
||||
|
||||
### "core php dev" exits immediately
|
||||
|
||||
**Cause:** Usually a port conflict or missing dependency.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# Check if port 8000 is in use
|
||||
lsof -i :8000
|
||||
|
||||
# Try a different port
|
||||
core php dev --port 9000
|
||||
|
||||
# Check logs for errors
|
||||
core php logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Commands are slow
|
||||
|
||||
**Possible causes:**
|
||||
|
||||
1. Large number of repositories
|
||||
2. Network latency to GitHub
|
||||
3. Go module downloads
|
||||
|
||||
**Fix:**
|
||||
|
||||
```bash
|
||||
# For multi-repo commands, use health for quick check
|
||||
core dev health # Fast summary
|
||||
|
||||
# Instead of
|
||||
core dev work --status # Full table (slower)
|
||||
|
||||
# Pre-download Go modules
|
||||
go mod download
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting More Help
|
||||
|
||||
### Enable Verbose Output
|
||||
|
||||
Most commands support `-v` or `--verbose`:
|
||||
|
||||
```bash
|
||||
core build -v
|
||||
core go test -v
|
||||
```
|
||||
|
||||
### Check Environment
|
||||
|
||||
```bash
|
||||
core doctor
|
||||
```
|
||||
|
||||
This verifies all required tools are installed and configured.
|
||||
|
||||
### Report Issues
|
||||
|
||||
If you've found a bug:
|
||||
|
||||
1. Check existing issues: https://github.com/host-uk/core/issues
|
||||
2. Create a new issue with:
|
||||
- Core version (`core --version`)
|
||||
- OS and architecture (`go env GOOS GOARCH`)
|
||||
- Command that failed
|
||||
- Full error output
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Getting Started](getting-started.md) - Installation and first steps
|
||||
- [Configuration](configuration.md) - Config file reference
|
||||
- [doctor](cmd/doctor/) - Environment verification
|
||||
334
build/go/workflows.md
Normal file
334
build/go/workflows.md
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
# Workflows
|
||||
|
||||
Common end-to-end workflows for Core CLI.
|
||||
|
||||
## Go Project: Build and Release
|
||||
|
||||
Complete workflow from code to GitHub release.
|
||||
|
||||
```bash
|
||||
# 1. Run tests
|
||||
core go test
|
||||
|
||||
# 2. Check coverage
|
||||
core go cov --threshold 80
|
||||
|
||||
# 3. Format and lint
|
||||
core go fmt --fix
|
||||
core go lint
|
||||
|
||||
# 4. Build for all platforms
|
||||
core build --targets linux/amd64,linux/arm64,darwin/arm64,windows/amd64
|
||||
|
||||
# 5. Preview release (dry-run)
|
||||
core ci
|
||||
|
||||
# 6. Publish
|
||||
core ci --we-are-go-for-launch
|
||||
```
|
||||
|
||||
**Output structure:**
|
||||
|
||||
```
|
||||
dist/
|
||||
├── myapp-darwin-arm64.tar.gz
|
||||
├── myapp-linux-amd64.tar.gz
|
||||
├── myapp-linux-arm64.tar.gz
|
||||
├── myapp-windows-amd64.zip
|
||||
└── CHECKSUMS.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHP Project: Development to Deployment
|
||||
|
||||
Local development through to production deployment.
|
||||
|
||||
```bash
|
||||
# 1. Start development environment
|
||||
core php dev
|
||||
|
||||
# 2. Run tests (in another terminal)
|
||||
core php test --parallel
|
||||
|
||||
# 3. Check code quality
|
||||
core php fmt --fix
|
||||
core php analyse
|
||||
|
||||
# 4. Deploy to staging
|
||||
core php deploy --staging --wait
|
||||
|
||||
# 5. Verify staging
|
||||
# (manual testing)
|
||||
|
||||
# 6. Deploy to production
|
||||
core php deploy --wait
|
||||
|
||||
# 7. Monitor
|
||||
core php deploy:status
|
||||
```
|
||||
|
||||
**Rollback if needed:**
|
||||
|
||||
```bash
|
||||
core php deploy:rollback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Repo: Daily Workflow
|
||||
|
||||
Working across the host-uk monorepo.
|
||||
|
||||
### Morning: Sync Everything
|
||||
|
||||
```bash
|
||||
# Quick health check
|
||||
core dev health
|
||||
|
||||
# Pull all repos that are behind
|
||||
core dev pull --all
|
||||
|
||||
# Check for issues assigned to you
|
||||
core dev issues --assignee @me
|
||||
```
|
||||
|
||||
### During Development
|
||||
|
||||
```bash
|
||||
# Work on code...
|
||||
|
||||
# Check status across all repos
|
||||
core dev work --status
|
||||
|
||||
# Commit changes (Claude-assisted messages)
|
||||
core dev commit
|
||||
|
||||
# Push when ready
|
||||
core dev push
|
||||
```
|
||||
|
||||
### End of Day
|
||||
|
||||
```bash
|
||||
# Full workflow: status → commit → push
|
||||
core dev work
|
||||
|
||||
# Check CI status
|
||||
core dev ci
|
||||
|
||||
# Review any failed builds
|
||||
core dev ci --failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New Developer: Environment Setup
|
||||
|
||||
First-time setup for a new team member.
|
||||
|
||||
```bash
|
||||
# 1. Verify prerequisites
|
||||
core doctor
|
||||
|
||||
# 2. Create workspace directory
|
||||
mkdir ~/Code/host-uk && cd ~/Code/host-uk
|
||||
|
||||
# 3. Bootstrap workspace (interactive)
|
||||
core setup
|
||||
|
||||
# 4. Select packages in wizard
|
||||
# Use arrow keys, space to select, enter to confirm
|
||||
|
||||
# 5. Verify setup
|
||||
core dev health
|
||||
|
||||
# 6. Start working
|
||||
core dev work --status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI Pipeline: Automated Build
|
||||
|
||||
Example GitHub Actions workflow.
|
||||
|
||||
```yaml
|
||||
# .github/workflows/release.yml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Install Core
|
||||
run: go install github.com/host-uk/core/cmd/core@latest
|
||||
|
||||
- name: Build
|
||||
run: core build --ci
|
||||
|
||||
- name: Release
|
||||
run: core ci --we-are-go-for-launch
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SDK Generation: API Client Updates
|
||||
|
||||
Generate SDK clients when API changes.
|
||||
|
||||
```bash
|
||||
# 1. Validate OpenAPI spec
|
||||
core sdk validate
|
||||
|
||||
# 2. Check for breaking changes
|
||||
core sdk diff --base v1.0.0
|
||||
|
||||
# 3. Generate SDKs
|
||||
core build sdk
|
||||
|
||||
# 4. Review generated code
|
||||
git diff
|
||||
|
||||
# 5. Commit if satisfied
|
||||
git add -A && git commit -m "chore: regenerate SDK clients"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency Update: Cross-Repo Change
|
||||
|
||||
When updating a shared package like `core-php`.
|
||||
|
||||
```bash
|
||||
# 1. Make changes in core-php
|
||||
cd ~/Code/host-uk/core-php
|
||||
# ... edit code ...
|
||||
|
||||
# 2. Run tests
|
||||
core go test # or core php test
|
||||
|
||||
# 3. Check what depends on core-php
|
||||
core dev impact core-php
|
||||
|
||||
# Output:
|
||||
# core-tenant (direct)
|
||||
# core-admin (via core-tenant)
|
||||
# core-api (direct)
|
||||
# ...
|
||||
|
||||
# 4. Commit core-php changes
|
||||
core dev commit
|
||||
|
||||
# 5. Update dependent packages
|
||||
cd ~/Code/host-uk
|
||||
for pkg in core-tenant core-admin core-api; do
|
||||
cd $pkg
|
||||
composer update host-uk/core-php
|
||||
core php test
|
||||
cd ..
|
||||
done
|
||||
|
||||
# 6. Commit all updates
|
||||
core dev work
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hotfix: Emergency Production Fix
|
||||
|
||||
Fast path for critical fixes.
|
||||
|
||||
```bash
|
||||
# 1. Create hotfix branch
|
||||
git checkout -b hotfix/critical-bug main
|
||||
|
||||
# 2. Make fix
|
||||
# ... edit code ...
|
||||
|
||||
# 3. Test
|
||||
core go test --run TestCriticalPath
|
||||
|
||||
# 4. Build
|
||||
core build
|
||||
|
||||
# 5. Preview release
|
||||
core ci --prerelease
|
||||
|
||||
# 6. Publish hotfix
|
||||
core ci --we-are-go-for-launch --prerelease
|
||||
|
||||
# 7. Merge back to main
|
||||
git checkout main
|
||||
git merge hotfix/critical-bug
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation: Sync Across Repos
|
||||
|
||||
Keep documentation synchronised.
|
||||
|
||||
```bash
|
||||
# 1. List all docs
|
||||
core docs list
|
||||
|
||||
# 2. Sync to central location
|
||||
core docs sync --output ./docs-site
|
||||
|
||||
# 3. Review changes
|
||||
git diff docs-site/
|
||||
|
||||
# 4. Commit
|
||||
git add docs-site/
|
||||
git commit -m "docs: sync from packages"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting: Failed Build
|
||||
|
||||
When a build fails.
|
||||
|
||||
```bash
|
||||
# 1. Check environment
|
||||
core doctor
|
||||
|
||||
# 2. Clean previous artifacts
|
||||
rm -rf dist/
|
||||
|
||||
# 3. Verbose build
|
||||
core build -v
|
||||
|
||||
# 4. If Go-specific issues
|
||||
core go mod tidy
|
||||
core go mod verify
|
||||
|
||||
# 5. Check for test failures
|
||||
core go test -v
|
||||
|
||||
# 6. Review configuration
|
||||
cat .core/build.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Getting Started](getting-started.md) - First-time setup
|
||||
- [Troubleshooting](troubleshooting.md) - When things go wrong
|
||||
- [Configuration](configuration.md) - Config file reference
|
||||
BIN
build/php/.DS_Store
vendored
Normal file
BIN
build/php/.DS_Store
vendored
Normal file
Binary file not shown.
181
build/php/actions.md
Normal file
181
build/php/actions.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# Actions Pattern
|
||||
|
||||
Actions are single-purpose, reusable classes that encapsulate business logic. They provide a clean, testable alternative to fat controllers and model methods.
|
||||
|
||||
## Basic Action
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Actions;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Mod\Blog\Models\Post;
|
||||
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
$post = Post::create($data);
|
||||
|
||||
event(new PostCreated($post));
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
$post = CreatePost::run(['title' => 'My Post', 'content' => '...']);
|
||||
```
|
||||
|
||||
## With Validation
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
$validated = Validator::make($data, [
|
||||
'title' => 'required|max:255',
|
||||
'content' => 'required',
|
||||
'status' => 'required|in:draft,published',
|
||||
])->validate();
|
||||
|
||||
return Post::create($validated);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## With Authorization
|
||||
|
||||
```php
|
||||
class DeletePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post, User $user): bool
|
||||
{
|
||||
if (!$user->can('delete', $post)) {
|
||||
throw new UnauthorizedException('Cannot delete this post');
|
||||
}
|
||||
|
||||
$post->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
DeletePost::run($post, auth()->user());
|
||||
```
|
||||
|
||||
## With Events
|
||||
|
||||
```php
|
||||
class PublishPost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post): Post
|
||||
{
|
||||
$post->update([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
event(new PostPublished($post));
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## As Job
|
||||
|
||||
```php
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function asJob(): bool
|
||||
{
|
||||
return true; // Run as queued job
|
||||
}
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
// Heavy processing...
|
||||
return Post::create($data);
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically queued
|
||||
CreatePost::run($data);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Single Responsibility
|
||||
```php
|
||||
// ✅ Good - one action, one purpose
|
||||
CreatePost::run($data);
|
||||
UpdatePost::run($post, $data);
|
||||
DeletePost::run($post);
|
||||
|
||||
// ❌ Bad - multiple responsibilities
|
||||
ManagePost::run($action, $post, $data);
|
||||
```
|
||||
|
||||
### 2. Type Hints
|
||||
```php
|
||||
// ✅ Good - clear types
|
||||
public function handle(Post $post, User $user): bool
|
||||
|
||||
// ❌ Bad - no types
|
||||
public function handle($post, $user)
|
||||
```
|
||||
|
||||
### 3. Descriptive Names
|
||||
```php
|
||||
// ✅ Good
|
||||
PublishScheduledPosts
|
||||
SendWeeklyNewsletter
|
||||
GenerateMonthlyReport
|
||||
|
||||
// ❌ Bad
|
||||
ProcessPosts
|
||||
DoWork
|
||||
HandleIt
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```php
|
||||
use Tests\TestCase;
|
||||
use Mod\Blog\Actions\CreatePost;
|
||||
|
||||
class CreatePostTest extends TestCase
|
||||
{
|
||||
public function test_creates_post(): void
|
||||
{
|
||||
$post = CreatePost::run([
|
||||
'title' => 'Test Post',
|
||||
'content' => 'Content',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('posts', [
|
||||
'title' => 'Test Post',
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
- [Lifecycle Events →](/core/events)
|
||||
- [Module System →](/core/modules)
|
||||
531
build/php/activity.md
Normal file
531
build/php/activity.md
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
# Activity Logging
|
||||
|
||||
Track user actions, model changes, and system events with GDPR-compliant activity logging.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Enabling Activity Logging
|
||||
|
||||
Add the `LogsActivity` trait to your model:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Core\Activity\Concerns\LogsActivity;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
protected $fillable = ['title', 'content', 'status'];
|
||||
}
|
||||
```
|
||||
|
||||
**Automatic Logging:**
|
||||
- Created events
|
||||
- Updated events (with changed attributes)
|
||||
- Deleted events
|
||||
- Restored events (soft deletes)
|
||||
|
||||
### Manual Logging
|
||||
|
||||
```php
|
||||
use Core\Activity\Services\ActivityLogService;
|
||||
|
||||
$logger = app(ActivityLogService::class);
|
||||
|
||||
// Log custom activity
|
||||
$logger->log(
|
||||
subject: $post,
|
||||
event: 'published',
|
||||
description: 'Post published to homepage',
|
||||
causer: auth()->user()
|
||||
);
|
||||
|
||||
// Log with properties
|
||||
$logger->log(
|
||||
subject: $post,
|
||||
event: 'viewed',
|
||||
properties: [
|
||||
'ip_address' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
## Activity Model
|
||||
|
||||
### Retrieving Activity
|
||||
|
||||
```php
|
||||
use Core\Activity\Models\Activity;
|
||||
|
||||
// Get all activity
|
||||
$activities = Activity::latest()->get();
|
||||
|
||||
// Get activity for specific model
|
||||
$postActivity = Activity::forSubject($post)->get();
|
||||
|
||||
// Get activity by user
|
||||
$userActivity = Activity::causedBy($user)->get();
|
||||
|
||||
// Get activity by event
|
||||
$published = Activity::where('event', 'published')->get();
|
||||
```
|
||||
|
||||
### Activity Attributes
|
||||
|
||||
```php
|
||||
$activity = Activity::latest()->first();
|
||||
|
||||
$activity->subject; // The model that was acted upon
|
||||
$activity->causer; // The user who caused the activity
|
||||
$activity->event; // Event name (created, updated, deleted, etc.)
|
||||
$activity->description; // Human-readable description
|
||||
$activity->properties; // Additional data (array)
|
||||
$activity->created_at; // When it occurred
|
||||
```
|
||||
|
||||
### Relationships
|
||||
|
||||
```php
|
||||
// Subject (polymorphic)
|
||||
$post = $activity->subject;
|
||||
|
||||
// Causer (polymorphic)
|
||||
$user = $activity->causer;
|
||||
|
||||
// Workspace (if applicable)
|
||||
$workspace = $activity->workspace;
|
||||
```
|
||||
|
||||
## Activity Scopes
|
||||
|
||||
### Filtering Activity
|
||||
|
||||
```php
|
||||
use Core\Activity\Models\Activity;
|
||||
|
||||
// By date range
|
||||
$activities = Activity::query()
|
||||
->whereBetween('created_at', [now()->subDays(7), now()])
|
||||
->get();
|
||||
|
||||
// By event type
|
||||
$activities = Activity::query()
|
||||
->whereIn('event', ['created', 'updated'])
|
||||
->get();
|
||||
|
||||
// By workspace
|
||||
$activities = Activity::query()
|
||||
->where('workspace_id', $workspace->id)
|
||||
->get();
|
||||
|
||||
// Complex filters
|
||||
$activities = Activity::query()
|
||||
->forSubject($post)
|
||||
->causedBy($user)
|
||||
->where('event', 'updated')
|
||||
->latest()
|
||||
->paginate(20);
|
||||
```
|
||||
|
||||
### Custom Scopes
|
||||
|
||||
```php
|
||||
use Core\Activity\Scopes\ActivityScopes;
|
||||
|
||||
// Add to Activity model
|
||||
class Activity extends Model
|
||||
{
|
||||
use ActivityScopes;
|
||||
|
||||
public function scopeForWorkspace($query, $workspaceId)
|
||||
{
|
||||
return $query->where('workspace_id', $workspaceId);
|
||||
}
|
||||
|
||||
public function scopeWithinDays($query, $days)
|
||||
{
|
||||
return $query->where('created_at', '>=', now()->subDays($days));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
$recent = Activity::withinDays(7)
|
||||
->forWorkspace($workspace->id)
|
||||
->get();
|
||||
```
|
||||
|
||||
## Customizing Logged Data
|
||||
|
||||
### Controlling What's Logged
|
||||
|
||||
```php
|
||||
class Post extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
// Only log these events
|
||||
protected static $recordEvents = ['created', 'published'];
|
||||
|
||||
// Exclude these attributes from change tracking
|
||||
protected static $ignoreChangedAttributes = ['views', 'updated_at'];
|
||||
|
||||
// Log only these attributes
|
||||
protected static $logAttributes = ['title', 'status'];
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Descriptions
|
||||
|
||||
```php
|
||||
class Post extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
public function getActivityDescription(string $event): string
|
||||
{
|
||||
return match($event) {
|
||||
'created' => "Created post: {$this->title}",
|
||||
'updated' => "Updated post: {$this->title}",
|
||||
'published' => "Published post: {$this->title}",
|
||||
default => "Post {$event}",
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Properties
|
||||
|
||||
```php
|
||||
class Post extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
public function getActivityProperties(string $event): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'category' => $this->category->name,
|
||||
'word_count' => str_word_count($this->content),
|
||||
'published_at' => $this->published_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GDPR Compliance
|
||||
|
||||
### IP Address Hashing
|
||||
|
||||
IP addresses are automatically hashed for privacy:
|
||||
|
||||
```php
|
||||
use Core\Crypt\LthnHash;
|
||||
|
||||
// Automatically applied
|
||||
$activity = Activity::create([
|
||||
'properties' => [
|
||||
'ip_address' => request()->ip(), // Hashed before storage
|
||||
],
|
||||
]);
|
||||
|
||||
// Verify IP match without storing plaintext
|
||||
if (LthnHash::check(request()->ip(), $activity->properties['ip_address'])) {
|
||||
// IP matches
|
||||
}
|
||||
```
|
||||
|
||||
### Data Retention
|
||||
|
||||
```php
|
||||
use Core\Activity\Console\ActivityPruneCommand;
|
||||
|
||||
// Prune old activity (default: 90 days)
|
||||
php artisan activity:prune
|
||||
|
||||
// Custom retention
|
||||
php artisan activity:prune --days=30
|
||||
|
||||
// Dry run
|
||||
php artisan activity:prune --dry-run
|
||||
```
|
||||
|
||||
**Scheduled Pruning:**
|
||||
|
||||
```php
|
||||
// app/Console/Kernel.php
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('activity:prune')
|
||||
->daily()
|
||||
->at('02:00');
|
||||
}
|
||||
```
|
||||
|
||||
### Right to Erasure
|
||||
|
||||
```php
|
||||
// Delete all activity for a user
|
||||
Activity::causedBy($user)->delete();
|
||||
|
||||
// Delete activity for specific subject
|
||||
Activity::forSubject($post)->delete();
|
||||
|
||||
// Anonymize instead of delete
|
||||
Activity::causedBy($user)->update([
|
||||
'causer_id' => null,
|
||||
'causer_type' => null,
|
||||
]);
|
||||
```
|
||||
|
||||
## Activity Feed
|
||||
|
||||
### Building Activity Feeds
|
||||
|
||||
```php
|
||||
use Core\Activity\Models\Activity;
|
||||
|
||||
// User's personal feed
|
||||
$feed = Activity::causedBy($user)
|
||||
->with(['subject', 'causer'])
|
||||
->latest()
|
||||
->paginate(20);
|
||||
|
||||
// Workspace activity feed
|
||||
$feed = Activity::query()
|
||||
->where('workspace_id', $workspace->id)
|
||||
->whereIn('event', ['created', 'updated', 'published'])
|
||||
->with(['subject', 'causer'])
|
||||
->latest()
|
||||
->paginate(20);
|
||||
```
|
||||
|
||||
### Rendering Activity
|
||||
|
||||
```blade
|
||||
{{-- resources/views/activity/feed.blade.php --}}
|
||||
@foreach($activities as $activity)
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">
|
||||
@if($activity->event === 'created')
|
||||
<svg>...</svg>
|
||||
@elseif($activity->event === 'updated')
|
||||
<svg>...</svg>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="activity-content">
|
||||
<p>
|
||||
<strong>{{ $activity->causer?->name ?? 'System' }}</strong>
|
||||
{{ $activity->description }}
|
||||
</p>
|
||||
<time>{{ $activity->created_at->diffForHumans() }}</time>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
### Livewire Component
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Core\Activity\View\Modal\Admin;
|
||||
|
||||
use Livewire\Component;
|
||||
use Core\Activity\Models\Activity;
|
||||
|
||||
class ActivityFeed extends Component
|
||||
{
|
||||
public $workspaceId;
|
||||
public $events = ['created', 'updated', 'deleted'];
|
||||
public $days = 7;
|
||||
|
||||
public function render()
|
||||
{
|
||||
$activities = Activity::query()
|
||||
->when($this->workspaceId, fn($q) => $q->where('workspace_id', $this->workspaceId))
|
||||
->whereIn('event', $this->events)
|
||||
->where('created_at', '>=', now()->subDays($this->days))
|
||||
->with(['subject', 'causer'])
|
||||
->latest()
|
||||
->paginate(20);
|
||||
|
||||
return view('activity::admin.activity-feed', [
|
||||
'activities' => $activities,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Eager Loading
|
||||
|
||||
```php
|
||||
// ✅ Good - eager load relationships
|
||||
$activities = Activity::query()
|
||||
->with(['subject', 'causer', 'workspace'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
// ❌ Bad - N+1 queries
|
||||
$activities = Activity::latest()->get();
|
||||
foreach ($activities as $activity) {
|
||||
echo $activity->causer->name; // Query per iteration
|
||||
}
|
||||
```
|
||||
|
||||
### Chunking Large Datasets
|
||||
|
||||
```php
|
||||
// Process activity in chunks
|
||||
Activity::query()
|
||||
->where('created_at', '<', now()->subDays(90))
|
||||
->chunk(1000, function ($activities) {
|
||||
foreach ($activities as $activity) {
|
||||
$activity->delete();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Queuing Activity Logging
|
||||
|
||||
```php
|
||||
// For high-traffic applications
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
protected static $logActivityQueue = true;
|
||||
|
||||
protected static $logActivityConnection = 'redis';
|
||||
}
|
||||
```
|
||||
|
||||
## Analytics
|
||||
|
||||
### Activity Statistics
|
||||
|
||||
```php
|
||||
use Core\Activity\Services\ActivityLogService;
|
||||
|
||||
$analytics = app(ActivityLogService::class);
|
||||
|
||||
// Count by event type
|
||||
$stats = Activity::query()
|
||||
->where('workspace_id', $workspace->id)
|
||||
->whereBetween('created_at', [now()->subDays(30), now()])
|
||||
->groupBy('event')
|
||||
->selectRaw('event, COUNT(*) as count')
|
||||
->get();
|
||||
|
||||
// Most active users
|
||||
$topUsers = Activity::query()
|
||||
->selectRaw('causer_id, causer_type, COUNT(*) as activity_count')
|
||||
->groupBy('causer_id', 'causer_type')
|
||||
->orderByDesc('activity_count')
|
||||
->limit(10)
|
||||
->get();
|
||||
```
|
||||
|
||||
### Audit Reports
|
||||
|
||||
```php
|
||||
// Generate audit trail
|
||||
$audit = Activity::query()
|
||||
->forSubject($post)
|
||||
->with('causer')
|
||||
->oldest()
|
||||
->get()
|
||||
->map(fn($activity) => [
|
||||
'timestamp' => $activity->created_at->toIso8601String(),
|
||||
'user' => $activity->causer?->name ?? 'System',
|
||||
'event' => $activity->event,
|
||||
'changes' => $activity->properties,
|
||||
]);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Log Meaningful Events
|
||||
|
||||
```php
|
||||
// ✅ Good - business-relevant events
|
||||
$logger->log($post, 'published', 'Post went live');
|
||||
$logger->log($order, 'payment_received', 'Customer paid');
|
||||
|
||||
// ❌ Bad - too granular
|
||||
$logger->log($post, 'view_count_incremented', 'Views++');
|
||||
```
|
||||
|
||||
### 2. Include Context
|
||||
|
||||
```php
|
||||
// ✅ Good - rich context
|
||||
$logger->log($post, 'published', properties: [
|
||||
'category' => $post->category->name,
|
||||
'scheduled' => $post->published_at->isPast(),
|
||||
'author' => $post->author->name,
|
||||
]);
|
||||
|
||||
// ❌ Bad - no context
|
||||
$logger->log($post, 'published');
|
||||
```
|
||||
|
||||
### 3. Respect Privacy
|
||||
|
||||
```php
|
||||
// ✅ Good - hash sensitive data
|
||||
$logger->log($user, 'login', properties: [
|
||||
'ip_address' => LthnHash::make(request()->ip()),
|
||||
]);
|
||||
|
||||
// ❌ Bad - plaintext IP
|
||||
$logger->log($user, 'login', properties: [
|
||||
'ip_address' => request()->ip(),
|
||||
]);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```php
|
||||
use Tests\TestCase;
|
||||
use Core\Activity\Models\Activity;
|
||||
|
||||
class ActivityTest extends TestCase
|
||||
{
|
||||
public function test_logs_model_creation(): void
|
||||
{
|
||||
$post = Post::create(['title' => 'Test']);
|
||||
|
||||
$this->assertDatabaseHas('activities', [
|
||||
'subject_type' => Post::class,
|
||||
'subject_id' => $post->id,
|
||||
'event' => 'created',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_logs_changes(): void
|
||||
{
|
||||
$post = Post::factory()->create(['status' => 'draft']);
|
||||
|
||||
$post->update(['status' => 'published']);
|
||||
|
||||
$activity = Activity::latest()->first();
|
||||
$this->assertEquals('published', $activity->properties['status']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Multi-Tenancy →](/core/tenancy)
|
||||
- [GDPR Compliance →](/security/overview)
|
||||
546
build/php/architecture/custom-events.md
Normal file
546
build/php/architecture/custom-events.md
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
# Creating Custom Events
|
||||
|
||||
Learn how to create custom lifecycle events for extensibility in your modules.
|
||||
|
||||
## Why Custom Events?
|
||||
|
||||
Custom lifecycle events allow you to:
|
||||
- Create extension points in your modules
|
||||
- Enable third-party integrations
|
||||
- Decouple module components
|
||||
- Follow the framework's event-driven pattern
|
||||
|
||||
## Basic Custom Event
|
||||
|
||||
### Step 1: Create Event Class
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Shop\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
use Core\Events\Concerns\HasEventVersion;
|
||||
|
||||
class PaymentGatewaysRegistering extends LifecycleEvent
|
||||
{
|
||||
use HasEventVersion;
|
||||
|
||||
protected array $gateways = [];
|
||||
|
||||
public function gateway(string $name, string $class): void
|
||||
{
|
||||
$this->gateways[$name] = $class;
|
||||
}
|
||||
|
||||
public function getGateways(): array
|
||||
{
|
||||
return $this->gateways;
|
||||
}
|
||||
|
||||
public function version(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Fire Event
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Shop;
|
||||
|
||||
use Core\Events\FrameworkBooted;
|
||||
use Mod\Shop\Events\PaymentGatewaysRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
FrameworkBooted::class => 'onFrameworkBooted',
|
||||
];
|
||||
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
// Fire custom event
|
||||
$gatewayEvent = new PaymentGatewaysRegistering();
|
||||
event($gatewayEvent);
|
||||
|
||||
// Register all collected gateways
|
||||
foreach ($gatewayEvent->getGateways() as $name => $class) {
|
||||
app('payment.gateways')->register($name, $class);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Listen to Event
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Stripe;
|
||||
|
||||
use Mod\Shop\Events\PaymentGatewaysRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
PaymentGatewaysRegistering::class => 'onPaymentGateways',
|
||||
];
|
||||
|
||||
public function onPaymentGateways(PaymentGatewaysRegistering $event): void
|
||||
{
|
||||
$event->gateway('stripe', StripeGateway::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event with Multiple Methods
|
||||
|
||||
Provide different registration methods:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
|
||||
class ContentTypesRegistering extends LifecycleEvent
|
||||
{
|
||||
protected array $types = [];
|
||||
protected array $renderers = [];
|
||||
protected array $validators = [];
|
||||
|
||||
public function type(string $name, string $model): void
|
||||
{
|
||||
$this->types[$name] = $model;
|
||||
}
|
||||
|
||||
public function renderer(string $type, string $class): void
|
||||
{
|
||||
$this->renderers[$type] = $class;
|
||||
}
|
||||
|
||||
public function validator(string $type, array $rules): void
|
||||
{
|
||||
$this->validators[$type] = $rules;
|
||||
}
|
||||
|
||||
public function getTypes(): array
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
public function getRenderers(): array
|
||||
{
|
||||
return $this->renderers;
|
||||
}
|
||||
|
||||
public function getValidators(): array
|
||||
{
|
||||
return $this->validators;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```php
|
||||
public function onContentTypes(ContentTypesRegistering $event): void
|
||||
{
|
||||
$event->type('video', Video::class);
|
||||
$event->renderer('video', VideoRenderer::class);
|
||||
$event->validator('video', [
|
||||
'url' => 'required|url',
|
||||
'duration' => 'required|integer',
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Event with Configuration
|
||||
|
||||
Pass configuration to listeners:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Analytics\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
|
||||
class AnalyticsProvidersRegistering extends LifecycleEvent
|
||||
{
|
||||
protected array $providers = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly array $config
|
||||
) {}
|
||||
|
||||
public function provider(string $name, string $class, array $config = []): void
|
||||
{
|
||||
$this->providers[$name] = [
|
||||
'class' => $class,
|
||||
'config' => array_merge($this->config[$name] ?? [], $config),
|
||||
];
|
||||
}
|
||||
|
||||
public function getProviders(): array
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fire with Config:**
|
||||
|
||||
```php
|
||||
$event = new AnalyticsProvidersRegistering(
|
||||
config('analytics.providers')
|
||||
);
|
||||
event($event);
|
||||
```
|
||||
|
||||
## Event Versioning
|
||||
|
||||
Track event versions for backward compatibility:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Api\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
use Core\Events\Concerns\HasEventVersion;
|
||||
|
||||
class ApiEndpointsRegistering extends LifecycleEvent
|
||||
{
|
||||
use HasEventVersion;
|
||||
|
||||
public function version(): string
|
||||
{
|
||||
return '2.0.0';
|
||||
}
|
||||
|
||||
// v2 method
|
||||
public function endpoint(string $path, string $controller, array $options = []): void
|
||||
{
|
||||
$this->endpoints[] = compact('path', 'controller', 'options');
|
||||
}
|
||||
|
||||
// v1 compatibility method (deprecated)
|
||||
public function route(string $path, string $controller): void
|
||||
{
|
||||
$this->endpoint($path, $controller, ['deprecated' => true]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Check Version in Listener:**
|
||||
|
||||
```php
|
||||
public function onApiEndpoints(ApiEndpointsRegistering $event): void
|
||||
{
|
||||
if (version_compare($event->version(), '2.0.0', '>=')) {
|
||||
// Use v2 API
|
||||
$event->endpoint('/posts', PostController::class, [
|
||||
'middleware' => ['auth:sanctum'],
|
||||
]);
|
||||
} else {
|
||||
// Use v1 API (deprecated)
|
||||
$event->route('/posts', PostController::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Priority
|
||||
|
||||
Control listener execution order:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Core\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
|
||||
class ThemesRegistering extends LifecycleEvent
|
||||
{
|
||||
protected array $themes = [];
|
||||
|
||||
public function theme(string $name, string $class, int $priority = 0): void
|
||||
{
|
||||
$this->themes[] = compact('name', 'class', 'priority');
|
||||
}
|
||||
|
||||
public function getThemes(): array
|
||||
{
|
||||
// Sort by priority (higher first)
|
||||
usort($this->themes, fn($a, $b) => $b['priority'] <=> $a['priority']);
|
||||
|
||||
return $this->themes;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```php
|
||||
public function onThemes(ThemesRegistering $event): void
|
||||
{
|
||||
$event->theme('default', DefaultTheme::class, priority: 0);
|
||||
$event->theme('premium', PremiumTheme::class, priority: 100);
|
||||
}
|
||||
```
|
||||
|
||||
## Event Validation
|
||||
|
||||
Validate registrations:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Forms\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class FormFieldsRegistering extends LifecycleEvent
|
||||
{
|
||||
protected array $fields = [];
|
||||
|
||||
public function field(string $type, string $class): void
|
||||
{
|
||||
// Validate field class
|
||||
if (!class_exists($class)) {
|
||||
throw new InvalidArgumentException("Field class {$class} does not exist");
|
||||
}
|
||||
|
||||
if (!is_subclass_of($class, FormField::class)) {
|
||||
throw new InvalidArgumentException("Field class must extend FormField");
|
||||
}
|
||||
|
||||
$this->fields[$type] = $class;
|
||||
}
|
||||
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Documentation
|
||||
|
||||
Document your events with docblocks:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Media\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
|
||||
/**
|
||||
* Fired when media processors are being registered.
|
||||
*
|
||||
* Allows modules to register custom image/video processors.
|
||||
*
|
||||
* @example
|
||||
* ```php
|
||||
* public function onMediaProcessors(MediaProcessorsRegistering $event): void
|
||||
* {
|
||||
* $event->processor('watermark', WatermarkProcessor::class);
|
||||
* $event->processor('thumbnail', ThumbnailProcessor::class);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class MediaProcessorsRegistering extends LifecycleEvent
|
||||
{
|
||||
protected array $processors = [];
|
||||
|
||||
/**
|
||||
* Register a media processor.
|
||||
*
|
||||
* @param string $name Processor name (e.g., 'watermark')
|
||||
* @param string $class Processor class (must implement ProcessorInterface)
|
||||
*/
|
||||
public function processor(string $name, string $class): void
|
||||
{
|
||||
$this->processors[$name] = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered processors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getProcessors(): array
|
||||
{
|
||||
return $this->processors;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Custom Events
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Mod\Shop\Events\PaymentGatewaysRegistering;
|
||||
use Mod\Stripe\StripeGateway;
|
||||
|
||||
class PaymentGatewaysEventTest extends TestCase
|
||||
{
|
||||
public function test_fires_payment_gateways_event(): void
|
||||
{
|
||||
Event::fake([PaymentGatewaysRegistering::class]);
|
||||
|
||||
// Trigger module boot
|
||||
$this->app->boot();
|
||||
|
||||
Event::assertDispatched(PaymentGatewaysRegistering::class);
|
||||
}
|
||||
|
||||
public function test_registers_payment_gateway(): void
|
||||
{
|
||||
$event = new PaymentGatewaysRegistering();
|
||||
|
||||
$event->gateway('stripe', StripeGateway::class);
|
||||
|
||||
$this->assertEquals(
|
||||
['stripe' => StripeGateway::class],
|
||||
$event->getGateways()
|
||||
);
|
||||
}
|
||||
|
||||
public function test_stripe_module_registers_gateway(): void
|
||||
{
|
||||
$event = new PaymentGatewaysRegistering();
|
||||
|
||||
$boot = new \Mod\Stripe\Boot();
|
||||
$boot->onPaymentGateways($event);
|
||||
|
||||
$this->assertArrayHasKey('stripe', $event->getGateways());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Descriptive Names
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
class PaymentGatewaysRegistering extends LifecycleEvent
|
||||
|
||||
// ❌ Bad
|
||||
class RegisterGateways extends LifecycleEvent
|
||||
```
|
||||
|
||||
### 2. Provide Fluent API
|
||||
|
||||
```php
|
||||
// ✅ Good - chainable
|
||||
public function gateway(string $name, string $class): self
|
||||
{
|
||||
$this->gateways[$name] = $class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Usage:
|
||||
$event->gateway('stripe', StripeGateway::class)
|
||||
->gateway('paypal', PayPalGateway::class);
|
||||
```
|
||||
|
||||
### 3. Validate Early
|
||||
|
||||
```php
|
||||
// ✅ Good - validate on registration
|
||||
public function gateway(string $name, string $class): void
|
||||
{
|
||||
if (!class_exists($class)) {
|
||||
throw new InvalidArgumentException("Gateway class not found: {$class}");
|
||||
}
|
||||
|
||||
$this->gateways[$name] = $class;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Version Your Events
|
||||
|
||||
```php
|
||||
// ✅ Good - versioned
|
||||
use HasEventVersion;
|
||||
|
||||
public function version(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
```
|
||||
|
||||
## Real-World Example
|
||||
|
||||
Complete example of a custom event system:
|
||||
|
||||
```php
|
||||
// Event
|
||||
class SearchProvidersRegistering extends LifecycleEvent
|
||||
{
|
||||
use HasEventVersion;
|
||||
|
||||
protected array $providers = [];
|
||||
|
||||
public function provider(
|
||||
string $name,
|
||||
string $class,
|
||||
int $priority = 0,
|
||||
array $config = []
|
||||
): void {
|
||||
$this->providers[$name] = compact('class', 'priority', 'config');
|
||||
}
|
||||
|
||||
public function getProviders(): array
|
||||
{
|
||||
uasort($this->providers, fn($a, $b) => $b['priority'] <=> $a['priority']);
|
||||
return $this->providers;
|
||||
}
|
||||
|
||||
public function version(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
// Fire event
|
||||
$event = new SearchProvidersRegistering();
|
||||
event($event);
|
||||
|
||||
foreach ($event->getProviders() as $name => $config) {
|
||||
app('search')->register($name, new $config['class']($config['config']));
|
||||
}
|
||||
|
||||
// Listen to event
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
SearchProvidersRegistering::class => 'onSearchProviders',
|
||||
];
|
||||
|
||||
public function onSearchProviders(SearchProvidersRegistering $event): void
|
||||
{
|
||||
$event->provider('posts', PostSearchProvider::class, priority: 100);
|
||||
$event->provider('users', UserSearchProvider::class, priority: 50);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Lifecycle Events →](/packages/core/events)
|
||||
- [Module System →](/packages/core/modules)
|
||||
535
build/php/architecture/lazy-loading.md
Normal file
535
build/php/architecture/lazy-loading.md
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
# Lazy Loading
|
||||
|
||||
Core PHP Framework uses lazy loading to defer module instantiation until absolutely necessary. This dramatically improves performance by only loading code relevant to the current request.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Traditional Approach (Everything Loads)
|
||||
|
||||
```php
|
||||
// Boot ALL modules on every request
|
||||
$modules = [
|
||||
new BlogModule(),
|
||||
new CommerceModule(),
|
||||
new AnalyticsModule(),
|
||||
new AdminModule(),
|
||||
new ApiModule(),
|
||||
// ... dozens more
|
||||
];
|
||||
|
||||
// Web request loads admin code it doesn't need
|
||||
// API request loads web views it doesn't use
|
||||
// Memory: ~50MB, Boot time: ~500ms
|
||||
```
|
||||
|
||||
### Lazy Loading Approach (On-Demand)
|
||||
|
||||
```php
|
||||
// Register listeners WITHOUT instantiating modules
|
||||
Event::listen(WebRoutesRegistering::class, LazyModuleListener::for(BlogModule::class));
|
||||
Event::listen(AdminPanelBooting::class, LazyModuleListener::for(AdminModule::class));
|
||||
|
||||
// Web request → Only BlogModule instantiated
|
||||
// API request → Only ApiModule instantiated
|
||||
// Memory: ~15MB, Boot time: ~150ms
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Module Discovery
|
||||
|
||||
`ModuleScanner` finds modules and extracts their event interests:
|
||||
|
||||
```php
|
||||
$modules = [
|
||||
[
|
||||
'class' => Mod\Blog\Boot::class,
|
||||
'listens' => [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => 'onAdmin',
|
||||
],
|
||||
],
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
### 2. Lazy Listener Registration
|
||||
|
||||
`ModuleRegistry` creates lazy listeners for each event-module pair:
|
||||
|
||||
```php
|
||||
foreach ($modules as $module) {
|
||||
foreach ($module['listens'] as $event => $method) {
|
||||
Event::listen($event, new LazyModuleListener(
|
||||
$module['class'],
|
||||
$method
|
||||
));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Event-Driven Loading
|
||||
|
||||
When an event fires, `LazyModuleListener` instantiates the module:
|
||||
|
||||
```php
|
||||
class LazyModuleListener
|
||||
{
|
||||
public function __construct(
|
||||
private string $moduleClass,
|
||||
private string $method,
|
||||
) {}
|
||||
|
||||
public function handle($event): void
|
||||
{
|
||||
// Module instantiated HERE, not before
|
||||
$module = new $this->moduleClass();
|
||||
$module->{$this->method}($event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Request Types and Loading
|
||||
|
||||
### Web Request
|
||||
|
||||
```
|
||||
Request: GET /blog
|
||||
↓
|
||||
WebRoutesRegistering fired
|
||||
↓
|
||||
Only modules listening to WebRoutesRegistering loaded:
|
||||
- BlogModule
|
||||
- MarketingModule
|
||||
↓
|
||||
Admin/API modules never instantiated
|
||||
```
|
||||
|
||||
### Admin Request
|
||||
|
||||
```
|
||||
Request: GET /admin/posts
|
||||
↓
|
||||
AdminPanelBooting fired
|
||||
↓
|
||||
Only modules with admin routes loaded:
|
||||
- BlogAdminModule
|
||||
- CoreAdminModule
|
||||
↓
|
||||
Public web modules never instantiated
|
||||
```
|
||||
|
||||
### API Request
|
||||
|
||||
```
|
||||
Request: GET /api/v1/posts
|
||||
↓
|
||||
ApiRoutesRegistering fired
|
||||
↓
|
||||
Only modules with API endpoints loaded:
|
||||
- BlogApiModule
|
||||
- AuthModule
|
||||
↓
|
||||
Web/Admin views never loaded
|
||||
```
|
||||
|
||||
### Console Command
|
||||
|
||||
```
|
||||
Command: php artisan blog:publish
|
||||
↓
|
||||
ConsoleBooting fired
|
||||
↓
|
||||
Only modules with commands loaded:
|
||||
- BlogModule (has blog:publish command)
|
||||
↓
|
||||
Web/Admin/API routes never registered
|
||||
```
|
||||
|
||||
## Performance Impact
|
||||
|
||||
### Memory Usage
|
||||
|
||||
| Request Type | Traditional | Lazy Loading | Savings |
|
||||
|--------------|-------------|--------------|---------|
|
||||
| Web | 50 MB | 15 MB | 70% |
|
||||
| Admin | 50 MB | 18 MB | 64% |
|
||||
| API | 50 MB | 12 MB | 76% |
|
||||
| Console | 50 MB | 10 MB | 80% |
|
||||
|
||||
### Boot Time
|
||||
|
||||
| Request Type | Traditional | Lazy Loading | Savings |
|
||||
|--------------|-------------|--------------|---------|
|
||||
| Web | 500ms | 150ms | 70% |
|
||||
| Admin | 500ms | 180ms | 64% |
|
||||
| API | 500ms | 120ms | 76% |
|
||||
| Console | 500ms | 100ms | 80% |
|
||||
|
||||
*Measurements from production application with 50+ modules*
|
||||
|
||||
## Selective Loading
|
||||
|
||||
### Only Listen to Needed Events
|
||||
|
||||
Don't register for events you don't need:
|
||||
|
||||
```php
|
||||
// ✅ Good - API-only module
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
];
|
||||
}
|
||||
|
||||
// ❌ Bad - unnecessary listeners
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes', // Not needed
|
||||
AdminPanelBooting::class => 'onAdmin', // Not needed
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Loading
|
||||
|
||||
Load features conditionally within event handlers:
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Only load blog if enabled
|
||||
if (config('modules.blog.enabled')) {
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deferred Service Providers
|
||||
|
||||
Combine with Laravel's deferred providers for maximum laziness:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||
|
||||
class BlogServiceProvider extends ServiceProvider implements DeferrableProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(BlogService::class, function ($app) {
|
||||
return new BlogService(
|
||||
$app->make(PostRepository::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function provides(): array
|
||||
{
|
||||
// Only load this provider when BlogService is requested
|
||||
return [BlogService::class];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lazy Collections
|
||||
|
||||
Use lazy collections for memory-efficient data processing:
|
||||
|
||||
```php
|
||||
// ✅ Good - lazy loading
|
||||
Post::query()
|
||||
->published()
|
||||
->cursor() // Returns lazy collection
|
||||
->each(function ($post) {
|
||||
ProcessPost::dispatch($post);
|
||||
});
|
||||
|
||||
// ❌ Bad - loads all into memory
|
||||
Post::query()
|
||||
->published()
|
||||
->get() // Loads everything
|
||||
->each(function ($post) {
|
||||
ProcessPost::dispatch($post);
|
||||
});
|
||||
```
|
||||
|
||||
## Lazy Relationships
|
||||
|
||||
Defer relationship loading until needed:
|
||||
|
||||
```php
|
||||
// ✅ Good - lazy eager loading
|
||||
$posts = Post::all();
|
||||
|
||||
if ($needsComments) {
|
||||
$posts->load('comments');
|
||||
}
|
||||
|
||||
// ❌ Bad - always loads comments
|
||||
$posts = Post::with('comments')->get();
|
||||
```
|
||||
|
||||
## Route Lazy Loading
|
||||
|
||||
Laravel 11+ supports route file lazy loading:
|
||||
|
||||
```php
|
||||
// routes/web.php
|
||||
Route::middleware('web')->group(function () {
|
||||
// Only load blog routes when /blog is accessed
|
||||
Route::prefix('blog')->group(base_path('routes/blog.php'));
|
||||
});
|
||||
```
|
||||
|
||||
## Cache Warming
|
||||
|
||||
Warm caches during deployment, not during requests:
|
||||
|
||||
```bash
|
||||
# Deploy script
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan event:cache
|
||||
|
||||
# Modules discovered once, cached
|
||||
php artisan core:cache-modules
|
||||
```
|
||||
|
||||
## Monitoring Lazy Loading
|
||||
|
||||
### Track Module Loading
|
||||
|
||||
Log when modules are instantiated:
|
||||
|
||||
```php
|
||||
class LazyModuleListener
|
||||
{
|
||||
public function handle($event): void
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
$module = new $this->moduleClass();
|
||||
$module->{$this->method}($event);
|
||||
|
||||
$duration = (microtime(true) - $start) * 1000;
|
||||
|
||||
Log::debug("Module loaded", [
|
||||
'module' => $this->moduleClass,
|
||||
'event' => get_class($event),
|
||||
'duration_ms' => round($duration, 2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Analyze Module Usage
|
||||
|
||||
Track which modules load for different request types:
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
APP_DEBUG=true LOG_LEVEL=debug
|
||||
|
||||
# Make requests and check logs
|
||||
tail -f storage/logs/laravel.log | grep "Module loaded"
|
||||
```
|
||||
|
||||
## Debugging Lazy Loading
|
||||
|
||||
### Force Load All Modules
|
||||
|
||||
Disable lazy loading for debugging:
|
||||
|
||||
```php
|
||||
// config/core.php
|
||||
'modules' => [
|
||||
'lazy_loading' => env('MODULES_LAZY_LOADING', true),
|
||||
],
|
||||
|
||||
// .env
|
||||
MODULES_LAZY_LOADING=false
|
||||
```
|
||||
|
||||
### Check Module Load Order
|
||||
|
||||
```php
|
||||
Event::listen('*', function ($eventName, $data) {
|
||||
if (str_starts_with($eventName, 'Core\\Events\\')) {
|
||||
Log::debug("Event fired", ['event' => $eventName]);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Verify Listeners Registered
|
||||
|
||||
```bash
|
||||
php artisan event:list | grep "Core\\Events"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Keep Boot.php Lightweight
|
||||
|
||||
Move heavy initialization to service providers:
|
||||
|
||||
```php
|
||||
// ✅ Good - lightweight Boot.php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
|
||||
// ❌ Bad - heavy initialization in Boot.php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Don't do this in event handlers!
|
||||
$this->registerServices();
|
||||
$this->loadViews();
|
||||
$this->publishAssets();
|
||||
$this->registerCommands();
|
||||
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Avoid Global State in Modules
|
||||
|
||||
Don't store state in module classes:
|
||||
|
||||
```php
|
||||
// ✅ Good - stateless
|
||||
class Boot
|
||||
{
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad - stateful
|
||||
class Boot
|
||||
{
|
||||
private array $config = [];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$this->config = config('blog'); // Don't store state
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Dependency Injection
|
||||
|
||||
Let the container handle dependencies:
|
||||
|
||||
```php
|
||||
// ✅ Good - DI in services
|
||||
class BlogService
|
||||
{
|
||||
public function __construct(
|
||||
private PostRepository $posts,
|
||||
private CacheManager $cache,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ❌ Bad - manual instantiation
|
||||
class BlogService
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->posts = new PostRepository();
|
||||
$this->cache = new CacheManager();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Defer Heavy Operations
|
||||
|
||||
Don't perform expensive operations during boot:
|
||||
|
||||
```php
|
||||
// ✅ Good - defer to queue
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
dispatch(new WarmBlogCache())->afterResponse();
|
||||
}
|
||||
|
||||
// ❌ Bad - expensive operation during boot
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
// Don't do this!
|
||||
$posts = Post::with('comments', 'categories', 'tags')->get();
|
||||
Cache::put('blog:all-posts', $posts, 3600);
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Lazy Singletons
|
||||
|
||||
Register services as lazy singletons:
|
||||
|
||||
```php
|
||||
$this->app->singleton(BlogService::class, function ($app) {
|
||||
return new BlogService(
|
||||
$app->make(PostRepository::class)
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
Service only instantiated when first requested:
|
||||
|
||||
```php
|
||||
// BlogService not instantiated yet
|
||||
$posts = Post::all();
|
||||
|
||||
// BlogService instantiated HERE
|
||||
app(BlogService::class)->getRecentPosts();
|
||||
```
|
||||
|
||||
### Contextual Binding
|
||||
|
||||
Bind different implementations based on context:
|
||||
|
||||
```php
|
||||
$this->app->when(ApiController::class)
|
||||
->needs(PostRepository::class)
|
||||
->give(CachedPostRepository::class);
|
||||
|
||||
$this->app->when(AdminController::class)
|
||||
->needs(PostRepository::class)
|
||||
->give(LivePostRepository::class);
|
||||
```
|
||||
|
||||
### Module Proxies
|
||||
|
||||
Create proxies for optional modules:
|
||||
|
||||
```php
|
||||
class AnalyticsProxy
|
||||
{
|
||||
public function track(string $event, array $data = []): void
|
||||
{
|
||||
// Only load analytics module if it exists
|
||||
if (class_exists(Mod\Analytics\AnalyticsService::class)) {
|
||||
app(AnalyticsService::class)->track($event, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Module System](/architecture/module-system)
|
||||
- [Lifecycle Events](/architecture/lifecycle-events)
|
||||
- [Performance Optimization](/architecture/performance)
|
||||
610
build/php/architecture/lifecycle-events.md
Normal file
610
build/php/architecture/lifecycle-events.md
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
# Lifecycle Events
|
||||
|
||||
Core PHP Framework uses an event-driven architecture where modules declare interest in lifecycle events. This enables lazy loading and modular composition without tight coupling.
|
||||
|
||||
## Overview
|
||||
|
||||
The lifecycle event system provides extension points throughout the framework's boot process. Modules register listeners for specific events, and are only instantiated when those events fire.
|
||||
|
||||
```
|
||||
Application Boot
|
||||
↓
|
||||
LifecycleEventProvider fires events
|
||||
↓
|
||||
LazyModuleListener intercepts events
|
||||
↓
|
||||
Module instantiated on-demand
|
||||
↓
|
||||
Event handler executes
|
||||
↓
|
||||
Module collects requests (routes, menus, etc.)
|
||||
↓
|
||||
LifecycleEventProvider processes requests
|
||||
```
|
||||
|
||||
## Core Events
|
||||
|
||||
### WebRoutesRegistering
|
||||
|
||||
**Fired during:** Web route registration (early boot)
|
||||
|
||||
**Purpose:** Register public-facing web routes and views
|
||||
|
||||
**Use cases:**
|
||||
- Marketing pages
|
||||
- Public blog
|
||||
- Documentation site
|
||||
- Landing pages
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Register view namespace
|
||||
$event->views('marketing', __DIR__.'/Views');
|
||||
|
||||
// Register routes
|
||||
$event->routes(function () {
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
Route::get('/pricing', [PricingController::class, 'index'])->name('pricing');
|
||||
Route::get('/contact', [ContactController::class, 'index'])->name('contact');
|
||||
});
|
||||
|
||||
// Register middleware
|
||||
$event->middleware(['web', 'track-visitor']);
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `views(string $namespace, string $path)` - Register view namespace
|
||||
- `routes(Closure $callback)` - Register routes
|
||||
- `middleware(array $middleware)` - Apply middleware to routes
|
||||
|
||||
---
|
||||
|
||||
### AdminPanelBooting
|
||||
|
||||
**Fired during:** Admin panel initialization
|
||||
|
||||
**Purpose:** Register admin routes, menus, and dashboard widgets
|
||||
|
||||
**Use cases:**
|
||||
- Admin CRUD interfaces
|
||||
- Dashboard widgets
|
||||
- Settings pages
|
||||
- Admin navigation
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
public function onAdmin(AdminPanelBooting $event): void
|
||||
{
|
||||
// Register admin routes
|
||||
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
|
||||
|
||||
// Register admin menu
|
||||
$event->menu(new BlogMenuProvider());
|
||||
|
||||
// Register dashboard widget
|
||||
$event->widget(new PostStatsWidget());
|
||||
|
||||
// Register settings page
|
||||
$event->settings('blog', BlogSettingsPage::class);
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `routes(Closure $callback)` - Register admin routes
|
||||
- `menu(AdminMenuProvider $provider)` - Register menu items
|
||||
- `widget(DashboardWidget $widget)` - Register dashboard widget
|
||||
- `settings(string $key, string $class)` - Register settings page
|
||||
|
||||
---
|
||||
|
||||
### ApiRoutesRegistering
|
||||
|
||||
**Fired during:** API route registration
|
||||
|
||||
**Purpose:** Register REST API endpoints
|
||||
|
||||
**Use cases:**
|
||||
- RESTful APIs
|
||||
- Webhooks
|
||||
- Third-party integrations
|
||||
- Mobile app backends
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(function () {
|
||||
Route::prefix('v1')->group(function () {
|
||||
Route::apiResource('posts', PostApiController::class);
|
||||
Route::get('posts/{post}/analytics', [PostApiController::class, 'analytics']);
|
||||
});
|
||||
});
|
||||
|
||||
// API-specific middleware
|
||||
$event->middleware(['api', 'auth:sanctum', 'scope:blog:read']);
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `routes(Closure $callback)` - Register API routes
|
||||
- `middleware(array $middleware)` - Apply middleware
|
||||
- `version(string $version)` - Set API version prefix
|
||||
|
||||
---
|
||||
|
||||
### ClientRoutesRegistering
|
||||
|
||||
**Fired during:** Client route registration
|
||||
|
||||
**Purpose:** Register authenticated client/dashboard routes
|
||||
|
||||
**Use cases:**
|
||||
- User dashboards
|
||||
- Account settings
|
||||
- Client portals
|
||||
- Authenticated SPA routes
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
public function onClientRoutes(ClientRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('dashboard', __DIR__.'/Views/Client');
|
||||
|
||||
$event->routes(function () {
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
Route::get('/account', [AccountController::class, 'show'])->name('account');
|
||||
Route::post('/account', [AccountController::class, 'update']);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `views(string $namespace, string $path)` - Register view namespace
|
||||
- `routes(Closure $callback)` - Register routes
|
||||
- `middleware(array $middleware)` - Apply middleware
|
||||
|
||||
---
|
||||
|
||||
### ConsoleBooting
|
||||
|
||||
**Fired during:** Console kernel initialization
|
||||
|
||||
**Purpose:** Register Artisan commands
|
||||
|
||||
**Use cases:**
|
||||
- Custom commands
|
||||
- Scheduled tasks
|
||||
- Maintenance scripts
|
||||
- Data migrations
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
// Register commands
|
||||
$event->commands([
|
||||
PublishPostCommand::class,
|
||||
ImportPostsCommand::class,
|
||||
GenerateSitemapCommand::class,
|
||||
]);
|
||||
|
||||
// Register scheduled tasks
|
||||
$event->schedule(function (Schedule $schedule) {
|
||||
$schedule->command(PublishScheduledPostsCommand::class)
|
||||
->hourly()
|
||||
->withoutOverlapping();
|
||||
|
||||
$schedule->command(GenerateSitemapCommand::class)
|
||||
->daily()
|
||||
->at('01:00');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `commands(array $commands)` - Register commands
|
||||
- `schedule(Closure $callback)` - Define scheduled tasks
|
||||
|
||||
---
|
||||
|
||||
### McpToolsRegistering
|
||||
|
||||
**Fired during:** MCP server initialization
|
||||
|
||||
**Purpose:** Register MCP (Model Context Protocol) tools for AI integrations
|
||||
|
||||
**Use cases:**
|
||||
- AI-powered features
|
||||
- LLM tool integrations
|
||||
- Automated workflows
|
||||
- AI assistants
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
public function onMcpTools(McpToolsRegistering $event): void
|
||||
{
|
||||
$event->tools([
|
||||
GetPostTool::class,
|
||||
CreatePostTool::class,
|
||||
UpdatePostTool::class,
|
||||
SearchPostsTool::class,
|
||||
]);
|
||||
|
||||
// Register prompts
|
||||
$event->prompts([
|
||||
GenerateBlogPostPrompt::class,
|
||||
]);
|
||||
|
||||
// Register resources
|
||||
$event->resources([
|
||||
BlogPostResource::class,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `tools(array $tools)` - Register MCP tools
|
||||
- `prompts(array $prompts)` - Register prompt templates
|
||||
- `resources(array $resources)` - Register resources
|
||||
|
||||
---
|
||||
|
||||
### FrameworkBooted
|
||||
|
||||
**Fired after:** All other lifecycle events have completed
|
||||
|
||||
**Purpose:** Late-stage initialization and cross-module setup
|
||||
|
||||
**Use cases:**
|
||||
- Service registration
|
||||
- Event listeners
|
||||
- Observer registration
|
||||
- Cache warming
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
// Register event listeners
|
||||
Event::listen(PostPublished::class, SendPostNotification::class);
|
||||
Event::listen(PostViewed::class, IncrementViewCount::class);
|
||||
|
||||
// Register model observers
|
||||
Post::observe(PostObserver::class);
|
||||
|
||||
// Register service
|
||||
app()->singleton(BlogService::class, function ($app) {
|
||||
return new BlogService(
|
||||
$app->make(PostRepository::class),
|
||||
$app->make(CategoryRepository::class)
|
||||
);
|
||||
});
|
||||
|
||||
// Register policies
|
||||
Gate::policy(Post::class, PostPolicy::class);
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `service(string $abstract, Closure $factory)` - Register service
|
||||
- `singleton(string $abstract, Closure $factory)` - Register singleton
|
||||
- `listener(string $event, string $listener)` - Register event listener
|
||||
|
||||
## Event Declaration
|
||||
|
||||
Modules declare event listeners via the `$listens` property in `Boot.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Events\ApiRoutesRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => 'onAdmin',
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void { }
|
||||
public function onAdmin(AdminPanelBooting $event): void { }
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void { }
|
||||
}
|
||||
```
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
Modules are **not** instantiated until an event they listen to is fired:
|
||||
|
||||
```php
|
||||
// Web request → Only WebRoutesRegistering listeners loaded
|
||||
// API request → Only ApiRoutesRegistering listeners loaded
|
||||
// Admin request → Only AdminPanelBooting listeners loaded
|
||||
// Console command → Only ConsoleBooting listeners loaded
|
||||
```
|
||||
|
||||
This dramatically reduces bootstrap time and memory usage.
|
||||
|
||||
## Event Flow
|
||||
|
||||
### 1. Module Discovery
|
||||
|
||||
`ModuleScanner` scans configured paths for `Boot.php` files:
|
||||
|
||||
```php
|
||||
$scanner = new ModuleScanner();
|
||||
$modules = $scanner->scan([
|
||||
app_path('Core'),
|
||||
app_path('Mod'),
|
||||
app_path('Plug'),
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Listener Registration
|
||||
|
||||
`ModuleRegistry` wires lazy listeners:
|
||||
|
||||
```php
|
||||
$registry = new ModuleRegistry();
|
||||
$registry->registerModules($modules);
|
||||
|
||||
// Creates LazyModuleListener for each event-module pair
|
||||
Event::listen(WebRoutesRegistering::class, LazyModuleListener::class);
|
||||
```
|
||||
|
||||
### 3. Event Firing
|
||||
|
||||
`LifecycleEventProvider` fires events at appropriate times:
|
||||
|
||||
```php
|
||||
// During route registration
|
||||
$event = new WebRoutesRegistering();
|
||||
event($event);
|
||||
```
|
||||
|
||||
### 4. Module Loading
|
||||
|
||||
`LazyModuleListener` instantiates module on-demand:
|
||||
|
||||
```php
|
||||
public function handle($event): void
|
||||
{
|
||||
$module = new $this->moduleClass(); // Module instantiated HERE
|
||||
$module->{$this->method}($event);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Request Collection
|
||||
|
||||
Modules collect requests during event handling:
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Stored in $event->routeRequests
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
|
||||
// Stored in $event->viewRequests
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Request Processing
|
||||
|
||||
`LifecycleEventProvider` processes collected requests:
|
||||
|
||||
```php
|
||||
foreach ($event->routeRequests as $request) {
|
||||
Route::middleware($request['middleware'])
|
||||
->group($request['callback']);
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Lifecycle Events
|
||||
|
||||
You can create custom lifecycle events by extending `LifecycleEvent`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Commerce\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
|
||||
class PaymentProvidersRegistering extends LifecycleEvent
|
||||
{
|
||||
protected array $providers = [];
|
||||
|
||||
public function provider(string $name, string $class): void
|
||||
{
|
||||
$this->providers[$name] = $class;
|
||||
}
|
||||
|
||||
public function getProviders(): array
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fire the event in your service provider:
|
||||
|
||||
```php
|
||||
$event = new PaymentProvidersRegistering();
|
||||
event($event);
|
||||
|
||||
foreach ($event->getProviders() as $name => $class) {
|
||||
PaymentGateway::register($name, $class);
|
||||
}
|
||||
```
|
||||
|
||||
Modules can listen to your custom event:
|
||||
|
||||
```php
|
||||
public static array $listens = [
|
||||
PaymentProvidersRegistering::class => 'onPaymentProviders',
|
||||
];
|
||||
|
||||
public function onPaymentProviders(PaymentProvidersRegistering $event): void
|
||||
{
|
||||
$event->provider('stripe', StripeProvider::class);
|
||||
}
|
||||
```
|
||||
|
||||
## Event Priorities
|
||||
|
||||
Control event listener execution order:
|
||||
|
||||
```php
|
||||
Event::listen(WebRoutesRegistering::class, FirstModule::class, 100);
|
||||
Event::listen(WebRoutesRegistering::class, SecondModule::class, 50);
|
||||
Event::listen(WebRoutesRegistering::class, ThirdModule::class, 10);
|
||||
|
||||
// Execution order: FirstModule → SecondModule → ThirdModule
|
||||
```
|
||||
|
||||
## Testing Lifecycle Events
|
||||
|
||||
Test that modules respond to events correctly:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mod\Blog;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
use Mod\Blog\Boot;
|
||||
|
||||
class BlogBootTest extends TestCase
|
||||
{
|
||||
public function test_registers_web_routes(): void
|
||||
{
|
||||
$event = new WebRoutesRegistering();
|
||||
$boot = new Boot();
|
||||
|
||||
$boot->onWebRoutes($event);
|
||||
|
||||
$this->assertNotEmpty($event->routeRequests);
|
||||
$this->assertNotEmpty($event->viewRequests);
|
||||
}
|
||||
|
||||
public function test_registers_admin_menu(): void
|
||||
{
|
||||
$event = new AdminPanelBooting();
|
||||
$boot = new Boot();
|
||||
|
||||
$boot->onAdmin($event);
|
||||
|
||||
$this->assertNotEmpty($event->menuProviders);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Keep Event Handlers Focused
|
||||
|
||||
Each event handler should only register resources related to that lifecycle phase:
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
|
||||
// ❌ Bad - service registration belongs in FrameworkBooted
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
app()->singleton(BlogService::class, ...);
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Dependency Injection
|
||||
|
||||
Event handlers receive the event object - use it instead of facades:
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(function () {
|
||||
Route::get('/blog', ...);
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ Bad - bypasses event system
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
Route::get('/blog', ...);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Only Listen to Needed Events
|
||||
|
||||
Don't register listeners for events you don't need:
|
||||
|
||||
```php
|
||||
// ✅ Good - API-only module
|
||||
public static array $listens = [
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
];
|
||||
|
||||
// ❌ Bad - unnecessary listeners
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => 'onAdmin',
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
];
|
||||
```
|
||||
|
||||
### 4. Keep Boot.php Lightweight
|
||||
|
||||
`Boot.php` should only coordinate - extract complex logic to dedicated classes:
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
public function onAdmin(AdminPanelBooting $event): void
|
||||
{
|
||||
$event->menu(new BlogMenuProvider());
|
||||
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
|
||||
}
|
||||
|
||||
// ❌ Bad - too much inline logic
|
||||
public function onAdmin(AdminPanelBooting $event): void
|
||||
{
|
||||
$event->menu([
|
||||
'label' => 'Blog',
|
||||
'icon' => 'newspaper',
|
||||
'children' => [
|
||||
// ... 50 lines of menu configuration
|
||||
],
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Module System](/architecture/module-system)
|
||||
- [Lazy Loading](/architecture/lazy-loading)
|
||||
- [Creating Custom Events](/architecture/custom-events)
|
||||
615
build/php/architecture/module-system.md
Normal file
615
build/php/architecture/module-system.md
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
# Module System
|
||||
|
||||
Core PHP Framework uses a modular monolith architecture where features are organized into self-contained modules that communicate through events and contracts.
|
||||
|
||||
## What is a Module?
|
||||
|
||||
A module is a self-contained feature with its own:
|
||||
|
||||
- Routes (web, admin, API)
|
||||
- Models and migrations
|
||||
- Controllers and actions
|
||||
- Views and assets
|
||||
- Configuration
|
||||
- Tests
|
||||
|
||||
Modules declare their lifecycle event interests and are only loaded when needed.
|
||||
|
||||
## Module Types
|
||||
|
||||
### Core Modules (`app/Core/`)
|
||||
|
||||
Foundation modules that provide framework functionality:
|
||||
|
||||
```
|
||||
app/Core/
|
||||
├── Events/ # Lifecycle events
|
||||
├── Module/ # Module system
|
||||
├── Actions/ # Actions pattern
|
||||
├── Config/ # Configuration system
|
||||
├── Media/ # Media handling
|
||||
└── Storage/ # Cache and storage
|
||||
```
|
||||
|
||||
**Namespace:** `Core\`
|
||||
|
||||
**Purpose:** Framework internals, shared utilities
|
||||
|
||||
### Feature Modules (`app/Mod/`)
|
||||
|
||||
Business domain modules:
|
||||
|
||||
```
|
||||
app/Mod/
|
||||
├── Tenant/ # Multi-tenancy
|
||||
├── Commerce/ # E-commerce features
|
||||
├── Blog/ # Blogging
|
||||
└── Analytics/ # Analytics
|
||||
```
|
||||
|
||||
**Namespace:** `Mod\`
|
||||
|
||||
**Purpose:** Application features
|
||||
|
||||
### Website Modules (`app/Website/`)
|
||||
|
||||
Site-specific implementations:
|
||||
|
||||
```
|
||||
app/Website/
|
||||
├── Marketing/ # Marketing site
|
||||
├── Docs/ # Documentation site
|
||||
└── Support/ # Support portal
|
||||
```
|
||||
|
||||
**Namespace:** `Website\`
|
||||
|
||||
**Purpose:** Deployable websites/frontends
|
||||
|
||||
### Plugin Modules (`app/Plug/`)
|
||||
|
||||
Optional integrations:
|
||||
|
||||
```
|
||||
app/Plug/
|
||||
├── Stripe/ # Stripe integration
|
||||
├── Mailchimp/ # Mailchimp integration
|
||||
└── Analytics/ # Analytics integrations
|
||||
```
|
||||
|
||||
**Namespace:** `Plug\`
|
||||
|
||||
**Purpose:** Third-party integrations, optional features
|
||||
|
||||
## Module Structure
|
||||
|
||||
Standard module structure created by `php artisan make:mod`:
|
||||
|
||||
```
|
||||
app/Mod/Example/
|
||||
├── Boot.php # Module entry point
|
||||
├── config.php # Module configuration
|
||||
│
|
||||
├── Actions/ # Business logic
|
||||
│ ├── CreateExample.php
|
||||
│ └── UpdateExample.php
|
||||
│
|
||||
├── Controllers/ # HTTP controllers
|
||||
│ ├── Admin/
|
||||
│ │ └── ExampleController.php
|
||||
│ └── ExampleController.php
|
||||
│
|
||||
├── Models/ # Eloquent models
|
||||
│ └── Example.php
|
||||
│
|
||||
├── Migrations/ # Database migrations
|
||||
│ └── 2026_01_01_create_examples_table.php
|
||||
│
|
||||
├── Database/
|
||||
│ ├── Factories/ # Model factories
|
||||
│ │ └── ExampleFactory.php
|
||||
│ └── Seeders/ # Database seeders
|
||||
│ └── ExampleSeeder.php
|
||||
│
|
||||
├── Routes/ # Route definitions
|
||||
│ ├── web.php # Public routes
|
||||
│ ├── admin.php # Admin routes
|
||||
│ └── api.php # API routes
|
||||
│
|
||||
├── Views/ # Blade templates
|
||||
│ ├── index.blade.php
|
||||
│ └── show.blade.php
|
||||
│
|
||||
├── Requests/ # Form requests
|
||||
│ ├── StoreExampleRequest.php
|
||||
│ └── UpdateExampleRequest.php
|
||||
│
|
||||
├── Resources/ # API resources
|
||||
│ └── ExampleResource.php
|
||||
│
|
||||
├── Policies/ # Authorization policies
|
||||
│ └── ExamplePolicy.php
|
||||
│
|
||||
├── Events/ # Domain events
|
||||
│ └── ExampleCreated.php
|
||||
│
|
||||
├── Listeners/ # Event listeners
|
||||
│ └── SendExampleNotification.php
|
||||
│
|
||||
├── Jobs/ # Queued jobs
|
||||
│ └── ProcessExample.php
|
||||
│
|
||||
├── Services/ # Domain services
|
||||
│ └── ExampleService.php
|
||||
│
|
||||
├── Mcp/ # MCP tools
|
||||
│ └── Tools/
|
||||
│ └── GetExampleTool.php
|
||||
│
|
||||
└── Tests/ # Module tests
|
||||
├── Feature/
|
||||
│ └── ExampleTest.php
|
||||
└── Unit/
|
||||
└── ExampleServiceTest.php
|
||||
```
|
||||
|
||||
## Creating Modules
|
||||
|
||||
### Using Artisan Commands
|
||||
|
||||
```bash
|
||||
# Create a feature module
|
||||
php artisan make:mod Blog
|
||||
|
||||
# Create a website module
|
||||
php artisan make:website Marketing
|
||||
|
||||
# Create a plugin module
|
||||
php artisan make:plug Stripe
|
||||
```
|
||||
|
||||
### Manual Creation
|
||||
|
||||
1. Create directory structure
|
||||
2. Create `Boot.php` with `$listens` array
|
||||
3. Register lifecycle event handlers
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Example;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('example', __DIR__.'/Views');
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Module Discovery
|
||||
|
||||
### Auto-Discovery
|
||||
|
||||
Modules are automatically discovered by scanning configured paths:
|
||||
|
||||
```php
|
||||
// config/core.php
|
||||
'module_paths' => [
|
||||
app_path('Core'),
|
||||
app_path('Mod'),
|
||||
app_path('Plug'),
|
||||
],
|
||||
```
|
||||
|
||||
### Manual Registration
|
||||
|
||||
Disable auto-discovery and register modules explicitly:
|
||||
|
||||
```php
|
||||
// config/core.php
|
||||
'modules' => [
|
||||
'auto_discover' => false,
|
||||
],
|
||||
|
||||
// app/Providers/AppServiceProvider.php
|
||||
use Core\Module\ModuleRegistry;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$registry = app(ModuleRegistry::class);
|
||||
|
||||
$registry->register(Mod\Blog\Boot::class);
|
||||
$registry->register(Mod\Commerce\Boot::class);
|
||||
}
|
||||
```
|
||||
|
||||
## Module Configuration
|
||||
|
||||
### Module-Level Configuration
|
||||
|
||||
Each module can have a `config.php` file:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// app/Mod/Blog/config.php
|
||||
|
||||
return [
|
||||
'posts_per_page' => env('BLOG_POSTS_PER_PAGE', 12),
|
||||
'enable_comments' => env('BLOG_COMMENTS_ENABLED', true),
|
||||
'cache_duration' => env('BLOG_CACHE_DURATION', 3600),
|
||||
];
|
||||
```
|
||||
|
||||
Access configuration:
|
||||
|
||||
```php
|
||||
$perPage = config('mod.blog.posts_per_page', 12);
|
||||
```
|
||||
|
||||
### Publishing Configuration
|
||||
|
||||
Allow users to customize module configuration:
|
||||
|
||||
```php
|
||||
// app/Mod/Blog/BlogServiceProvider.php
|
||||
public function boot(): void
|
||||
{
|
||||
$this->publishes([
|
||||
__DIR__.'/config.php' => config_path('mod/blog.php'),
|
||||
], 'blog-config');
|
||||
}
|
||||
```
|
||||
|
||||
Users can then publish and customize:
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=blog-config
|
||||
```
|
||||
|
||||
## Inter-Module Communication
|
||||
|
||||
### 1. Events (Recommended)
|
||||
|
||||
Modules communicate via domain events:
|
||||
|
||||
```php
|
||||
// Mod/Blog/Events/PostPublished.php
|
||||
class PostPublished
|
||||
{
|
||||
public function __construct(public Post $post) {}
|
||||
}
|
||||
|
||||
// Mod/Blog/Actions/PublishPost.php
|
||||
PostPublished::dispatch($post);
|
||||
|
||||
// Mod/Analytics/Listeners/TrackPostPublished.php
|
||||
Event::listen(PostPublished::class, TrackPostPublished::class);
|
||||
```
|
||||
|
||||
### 2. Service Contracts
|
||||
|
||||
Define contracts for shared functionality:
|
||||
|
||||
```php
|
||||
// Core/Contracts/NotificationService.php
|
||||
interface NotificationService
|
||||
{
|
||||
public function send(Notifiable $notifiable, Notification $notification): void;
|
||||
}
|
||||
|
||||
// Mod/Email/EmailNotificationService.php
|
||||
class EmailNotificationService implements NotificationService
|
||||
{
|
||||
public function send(Notifiable $notifiable, Notification $notification): void
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
|
||||
// Register in service provider
|
||||
app()->bind(NotificationService::class, EmailNotificationService::class);
|
||||
|
||||
// Use in other modules
|
||||
app(NotificationService::class)->send($user, $notification);
|
||||
```
|
||||
|
||||
### 3. Facades
|
||||
|
||||
Create facades for frequently used services:
|
||||
|
||||
```php
|
||||
// Mod/Blog/Facades/Blog.php
|
||||
class Blog extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return BlogService::class;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
Blog::getRecentPosts(10);
|
||||
Blog::findBySlug('example-post');
|
||||
```
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
### Declaring Dependencies
|
||||
|
||||
Use PHP attributes to declare module dependencies:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\BlogComments;
|
||||
|
||||
use Core\Module\Attributes\RequiresModule;
|
||||
|
||||
#[RequiresModule(Mod\Blog\Boot::class)]
|
||||
class Boot
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Dependencies
|
||||
|
||||
Verify dependencies are met:
|
||||
|
||||
```php
|
||||
use Core\Module\ModuleRegistry;
|
||||
|
||||
$registry = app(ModuleRegistry::class);
|
||||
|
||||
if ($registry->isLoaded(Mod\Blog\Boot::class)) {
|
||||
// Blog module is available
|
||||
}
|
||||
```
|
||||
|
||||
## Module Isolation
|
||||
|
||||
### Database Isolation
|
||||
|
||||
Use workspace scoping for multi-tenant isolation:
|
||||
|
||||
```php
|
||||
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use BelongsToWorkspace;
|
||||
}
|
||||
|
||||
// Queries automatically scoped to current workspace
|
||||
Post::all(); // Only returns posts for current workspace
|
||||
```
|
||||
|
||||
### Cache Isolation
|
||||
|
||||
Use workspace-scoped caching:
|
||||
|
||||
```php
|
||||
use Core\Mod\Tenant\Concerns\HasWorkspaceCache;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use BelongsToWorkspace, HasWorkspaceCache;
|
||||
}
|
||||
|
||||
// Cache isolated per workspace
|
||||
Post::forWorkspaceCached($workspace, 600);
|
||||
```
|
||||
|
||||
### Route Isolation
|
||||
|
||||
Separate route files by context:
|
||||
|
||||
```php
|
||||
// Routes/web.php - Public routes
|
||||
Route::get('/blog', [BlogController::class, 'index']);
|
||||
|
||||
// Routes/admin.php - Admin routes
|
||||
Route::resource('posts', PostController::class);
|
||||
|
||||
// Routes/api.php - API routes
|
||||
Route::apiResource('posts', PostApiController::class);
|
||||
```
|
||||
|
||||
## Module Testing
|
||||
|
||||
### Feature Tests
|
||||
|
||||
Test module functionality end-to-end:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mod\Blog;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Mod\Blog\Models\Post;
|
||||
|
||||
class PostTest extends TestCase
|
||||
{
|
||||
public function test_can_view_published_posts(): void
|
||||
{
|
||||
Post::factory()->published()->count(3)->create();
|
||||
|
||||
$response = $this->get('/blog');
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertViewHas('posts');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Test module services and actions:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Mod\Blog;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Mod\Blog\Actions\PublishPost;
|
||||
use Mod\Blog\Models\Post;
|
||||
|
||||
class PublishPostTest extends TestCase
|
||||
{
|
||||
public function test_publishes_post(): void
|
||||
{
|
||||
$post = Post::factory()->create(['published_at' => null]);
|
||||
|
||||
PublishPost::run($post);
|
||||
|
||||
$this->assertNotNull($post->fresh()->published_at);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Module Isolation Tests
|
||||
|
||||
Test that module doesn't leak dependencies:
|
||||
|
||||
```php
|
||||
public function test_module_works_without_optional_dependencies(): void
|
||||
{
|
||||
// Simulate missing optional module
|
||||
app()->forgetInstance(Mod\Analytics\AnalyticsService::class);
|
||||
|
||||
$response = $this->get('/blog');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Keep Modules Focused
|
||||
|
||||
Each module should have a single, well-defined responsibility:
|
||||
|
||||
```
|
||||
✅ Good: Mod\Blog (blogging features)
|
||||
✅ Good: Mod\Comments (commenting system)
|
||||
❌ Bad: Mod\BlogAndCommentsAndTags (too broad)
|
||||
```
|
||||
|
||||
### 2. Use Explicit Dependencies
|
||||
|
||||
Don't assume other modules exist:
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
if (class_exists(Mod\Analytics\AnalyticsService::class)) {
|
||||
app(AnalyticsService::class)->track($event);
|
||||
}
|
||||
|
||||
// ❌ Bad
|
||||
app(AnalyticsService::class)->track($event); // Crashes if not available
|
||||
```
|
||||
|
||||
### 3. Avoid Circular Dependencies
|
||||
|
||||
```
|
||||
✅ Good: Blog → Comments (one-way)
|
||||
❌ Bad: Blog ⟷ Comments (circular)
|
||||
```
|
||||
|
||||
### 4. Use Interfaces for Contracts
|
||||
|
||||
Define interfaces for inter-module communication:
|
||||
|
||||
```php
|
||||
// Core/Contracts/SearchProvider.php
|
||||
interface SearchProvider
|
||||
{
|
||||
public function search(string $query): Collection;
|
||||
}
|
||||
|
||||
// Mod/Blog/BlogSearchProvider.php
|
||||
class BlogSearchProvider implements SearchProvider
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Version Your APIs
|
||||
|
||||
If modules expose APIs, version them:
|
||||
|
||||
```php
|
||||
// Routes/api.php
|
||||
Route::prefix('v1')->group(function () {
|
||||
Route::apiResource('posts', V1\PostController::class);
|
||||
});
|
||||
|
||||
Route::prefix('v2')->group(function () {
|
||||
Route::apiResource('posts', V2\PostController::class);
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Module Not Loading
|
||||
|
||||
Check module is in configured path:
|
||||
|
||||
```bash
|
||||
# Verify path exists
|
||||
ls -la app/Mod/YourModule
|
||||
|
||||
# Check Boot.php exists
|
||||
cat app/Mod/YourModule/Boot.php
|
||||
|
||||
# Verify $listens array
|
||||
grep "listens" app/Mod/YourModule/Boot.php
|
||||
```
|
||||
|
||||
### Routes Not Registered
|
||||
|
||||
Ensure event handler calls `$event->routes()`:
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Don't forget this!
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
```
|
||||
|
||||
### Views Not Found
|
||||
|
||||
Register view namespace:
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Register view namespace
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
}
|
||||
```
|
||||
|
||||
Then use namespaced views:
|
||||
|
||||
```php
|
||||
return view('blog::index'); // Not just 'index'
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Lifecycle Events](/architecture/lifecycle-events)
|
||||
- [Lazy Loading](/architecture/lazy-loading)
|
||||
- [Multi-Tenancy](/patterns-guide/multi-tenancy)
|
||||
- [Actions Pattern](/patterns-guide/actions)
|
||||
600
build/php/architecture/multi-tenancy.md
Normal file
600
build/php/architecture/multi-tenancy.md
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
# Multi-Tenancy Architecture
|
||||
|
||||
Core PHP Framework provides robust multi-tenant isolation using workspace-scoped data. All tenant data is automatically isolated without manual filtering.
|
||||
|
||||
## Overview
|
||||
|
||||
Multi-tenancy ensures that users in one workspace (tenant) cannot access data from another workspace. Core PHP implements this through:
|
||||
|
||||
- Automatic query scoping via global scopes
|
||||
- Workspace context validation
|
||||
- Workspace-scoped caching
|
||||
- Request-level workspace resolution
|
||||
|
||||
## Workspace Model
|
||||
|
||||
The `Workspace` model represents a tenant:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Tenant\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Workspace extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'domain',
|
||||
'is_suspended',
|
||||
'settings',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_suspended' => 'boolean',
|
||||
'settings' => 'array',
|
||||
];
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function isSuspended(): bool
|
||||
{
|
||||
return $this->is_suspended;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Making Models Workspace-Scoped
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Add the `BelongsToWorkspace` trait to any model:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use BelongsToWorkspace;
|
||||
|
||||
protected $fillable = ['title', 'content'];
|
||||
}
|
||||
```
|
||||
|
||||
### What the Trait Provides
|
||||
|
||||
```php
|
||||
// All queries automatically scoped to current workspace
|
||||
$posts = Post::all(); // Only returns posts for current workspace
|
||||
|
||||
// Create automatically assigns workspace_id
|
||||
$post = Post::create([
|
||||
'title' => 'Example',
|
||||
'content' => 'Content',
|
||||
// workspace_id added automatically
|
||||
]);
|
||||
|
||||
// Cannot access posts from other workspaces
|
||||
$post = Post::find(999); // null if belongs to different workspace
|
||||
```
|
||||
|
||||
### Migration
|
||||
|
||||
Add `workspace_id` foreign key to tables:
|
||||
|
||||
```php
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('title');
|
||||
$table->text('content');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
## Workspace Scope
|
||||
|
||||
The `WorkspaceScope` global scope enforces data isolation:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Tenant\Scopes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Scope;
|
||||
|
||||
class WorkspaceScope implements Scope
|
||||
{
|
||||
public function apply(Builder $builder, Model $model): void
|
||||
{
|
||||
if ($workspace = $this->getCurrentWorkspace()) {
|
||||
$builder->where("{$model->getTable()}.workspace_id", $workspace->id);
|
||||
} elseif ($this->isStrictMode()) {
|
||||
throw new MissingWorkspaceContextException();
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Strict Mode
|
||||
|
||||
Strict mode throws exceptions if workspace context is missing:
|
||||
|
||||
```php
|
||||
// config/core.php
|
||||
'workspace' => [
|
||||
'strict_mode' => env('WORKSPACE_STRICT_MODE', true),
|
||||
],
|
||||
```
|
||||
|
||||
**Development:** Set to `true` to catch missing context bugs early
|
||||
**Production:** Keep at `true` for security
|
||||
|
||||
### Bypassing Workspace Scope
|
||||
|
||||
Sometimes you need to query across workspaces:
|
||||
|
||||
```php
|
||||
// Query all workspaces (use with caution!)
|
||||
Post::acrossWorkspaces()->get();
|
||||
|
||||
// Temporarily disable strict mode
|
||||
WorkspaceScope::withoutStrictMode(function () {
|
||||
return Post::all();
|
||||
});
|
||||
|
||||
// Query specific workspace
|
||||
Post::forWorkspace($otherWorkspace)->get();
|
||||
```
|
||||
|
||||
## Workspace Context
|
||||
|
||||
### Setting Workspace Context
|
||||
|
||||
The current workspace is typically set via middleware:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Mod\Tenant\Models\Workspace;
|
||||
|
||||
class SetWorkspaceContext
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// Resolve workspace from subdomain
|
||||
$subdomain = $this->extractSubdomain($request);
|
||||
$workspace = Workspace::where('slug', $subdomain)->firstOrFail();
|
||||
|
||||
// Set workspace context for this request
|
||||
app()->instance('current.workspace', $workspace);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieving Current Workspace
|
||||
|
||||
```php
|
||||
// Via helper
|
||||
$workspace = workspace();
|
||||
|
||||
// Via container
|
||||
$workspace = app('current.workspace');
|
||||
|
||||
// Via auth user
|
||||
$workspace = auth()->user()->workspace;
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
Apply workspace validation middleware to routes:
|
||||
|
||||
```php
|
||||
// Ensure workspace context exists
|
||||
Route::middleware(RequireWorkspaceContext::class)->group(function () {
|
||||
Route::get('/dashboard', [DashboardController::class, 'index']);
|
||||
});
|
||||
```
|
||||
|
||||
## Workspace-Scoped Caching
|
||||
|
||||
### Overview
|
||||
|
||||
Workspace-scoped caching ensures cache isolation between tenants:
|
||||
|
||||
```php
|
||||
// Cache key: workspace:123:posts:recent
|
||||
// Different workspace = different cache key
|
||||
$posts = Post::forWorkspaceCached($workspace, 600);
|
||||
```
|
||||
|
||||
### HasWorkspaceCache Trait
|
||||
|
||||
Add workspace caching to models:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
|
||||
use Core\Mod\Tenant\Concerns\HasWorkspaceCache;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use BelongsToWorkspace, HasWorkspaceCache;
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Methods
|
||||
|
||||
```php
|
||||
// Cache for specific workspace
|
||||
$posts = Post::forWorkspaceCached($workspace, 600);
|
||||
|
||||
// Cache for current workspace
|
||||
$posts = Post::ownedByCurrentWorkspaceCached(600);
|
||||
|
||||
// Invalidate workspace cache
|
||||
Post::invalidateWorkspaceCache($workspace);
|
||||
|
||||
// Invalidate all caches for a workspace
|
||||
WorkspaceCacheManager::invalidateAll($workspace);
|
||||
```
|
||||
|
||||
### Cache Configuration
|
||||
|
||||
```php
|
||||
// config/core.php
|
||||
'workspace_cache' => [
|
||||
'enabled' => env('WORKSPACE_CACHE_ENABLED', true),
|
||||
'ttl' => env('WORKSPACE_CACHE_TTL', 3600),
|
||||
'use_tags' => env('WORKSPACE_CACHE_USE_TAGS', true),
|
||||
'prefix' => 'workspace',
|
||||
],
|
||||
```
|
||||
|
||||
### Cache Tags (Recommended)
|
||||
|
||||
Use cache tags for granular invalidation:
|
||||
|
||||
```php
|
||||
// Store with tags
|
||||
Cache::tags(['workspace:'.$workspace->id, 'posts'])
|
||||
->put('recent-posts', $posts, 600);
|
||||
|
||||
// Invalidate all posts caches for workspace
|
||||
Cache::tags(['workspace:'.$workspace->id, 'posts'])->flush();
|
||||
|
||||
// Invalidate everything for workspace
|
||||
Cache::tags(['workspace:'.$workspace->id])->flush();
|
||||
```
|
||||
|
||||
## Database Isolation Strategies
|
||||
|
||||
### Shared Database (Recommended)
|
||||
|
||||
Single database with `workspace_id` column:
|
||||
|
||||
**Pros:**
|
||||
- Simple deployment
|
||||
- Easy backups
|
||||
- Cross-workspace queries possible
|
||||
- Cost-effective
|
||||
|
||||
**Cons:**
|
||||
- Requires careful scoping
|
||||
- One bad query can leak data
|
||||
|
||||
```php
|
||||
// All tables have workspace_id
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Separate Databases (Advanced)
|
||||
|
||||
Each workspace has its own database:
|
||||
|
||||
**Pros:**
|
||||
- Complete isolation
|
||||
- Better security
|
||||
- Easier compliance
|
||||
|
||||
**Cons:**
|
||||
- Complex migrations
|
||||
- Higher operational cost
|
||||
- No cross-workspace queries
|
||||
|
||||
```php
|
||||
// Dynamically switch database connection
|
||||
config([
|
||||
'database.connections.workspace' => [
|
||||
'database' => "workspace_{$workspace->id}",
|
||||
// ...
|
||||
],
|
||||
]);
|
||||
|
||||
DB::connection('workspace')->table('posts')->get();
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Always Use WorkspaceScope
|
||||
|
||||
Never bypass workspace scoping in application code:
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
$posts = Post::all();
|
||||
|
||||
// ❌ Bad - security vulnerability!
|
||||
$posts = Post::withoutGlobalScope(WorkspaceScope::class)->get();
|
||||
```
|
||||
|
||||
### 2. Validate Workspace Context
|
||||
|
||||
Always validate workspace exists and isn't suspended:
|
||||
|
||||
```php
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$workspace = workspace();
|
||||
|
||||
if (! $workspace) {
|
||||
throw new MissingWorkspaceContextException();
|
||||
}
|
||||
|
||||
if ($workspace->isSuspended()) {
|
||||
abort(403, 'Workspace suspended');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Policies for Authorization
|
||||
|
||||
Combine workspace scoping with Laravel policies:
|
||||
|
||||
```php
|
||||
class PostPolicy
|
||||
{
|
||||
public function update(User $user, Post $post): bool
|
||||
{
|
||||
// Workspace scope ensures $post belongs to current workspace
|
||||
// Policy checks user has permission within that workspace
|
||||
return $user->can('edit-posts');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Audit Workspace Access
|
||||
|
||||
Log workspace access for security auditing:
|
||||
|
||||
```php
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->performedOn($workspace)
|
||||
->withProperties(['action' => 'accessed'])
|
||||
->log('Workspace accessed');
|
||||
```
|
||||
|
||||
### 5. Test Cross-Workspace Isolation
|
||||
|
||||
Write tests to verify data isolation:
|
||||
|
||||
```php
|
||||
public function test_cannot_access_other_workspace_data(): void
|
||||
{
|
||||
$workspace1 = Workspace::factory()->create();
|
||||
$workspace2 = Workspace::factory()->create();
|
||||
|
||||
$post = Post::factory()->for($workspace1)->create();
|
||||
|
||||
// Set context to workspace2
|
||||
app()->instance('current.workspace', $workspace2);
|
||||
|
||||
// Should not find post from workspace1
|
||||
$this->assertNull(Post::find($post->id));
|
||||
}
|
||||
```
|
||||
|
||||
## Cross-Workspace Operations
|
||||
|
||||
### Admin Operations
|
||||
|
||||
Admins sometimes need cross-workspace access:
|
||||
|
||||
```php
|
||||
// Check if user is super admin
|
||||
if (auth()->user()->isSuperAdmin()) {
|
||||
// Allow cross-workspace queries
|
||||
$allPosts = Post::acrossWorkspaces()
|
||||
->where('published_at', '>', now()->subDays(7))
|
||||
->get();
|
||||
}
|
||||
```
|
||||
|
||||
### Reporting
|
||||
|
||||
Generate reports across workspaces:
|
||||
|
||||
```php
|
||||
class GenerateSystemReportJob
|
||||
{
|
||||
public function handle(): void
|
||||
{
|
||||
$stats = WorkspaceScope::withoutStrictMode(function () {
|
||||
return [
|
||||
'total_posts' => Post::count(),
|
||||
'total_users' => User::count(),
|
||||
'by_workspace' => Workspace::withCount('posts')->get(),
|
||||
];
|
||||
});
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Migrations
|
||||
|
||||
Migrations run without workspace context:
|
||||
|
||||
```php
|
||||
public function up(): void
|
||||
{
|
||||
WorkspaceScope::withoutStrictMode(function () {
|
||||
// Migrate data across all workspaces
|
||||
Post::chunk(100, function ($posts) {
|
||||
foreach ($posts as $post) {
|
||||
$post->update(['migrated' => true]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Eager Loading
|
||||
|
||||
Include workspace relation when needed:
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
$posts = Post::with('workspace')->get();
|
||||
|
||||
// ❌ Bad - N+1 queries
|
||||
$posts = Post::all();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->workspace->name; // N+1
|
||||
}
|
||||
```
|
||||
|
||||
### Index Optimization
|
||||
|
||||
Add composite indexes for workspace queries:
|
||||
|
||||
```php
|
||||
$table->index(['workspace_id', 'created_at']);
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index(['workspace_id', 'user_id']);
|
||||
```
|
||||
|
||||
### Partition Tables (Advanced)
|
||||
|
||||
For very large datasets, partition by workspace_id:
|
||||
|
||||
```sql
|
||||
CREATE TABLE posts (
|
||||
id BIGINT,
|
||||
workspace_id BIGINT NOT NULL,
|
||||
-- ...
|
||||
) PARTITION BY HASH(workspace_id) PARTITIONS 10;
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Track Workspace Usage
|
||||
|
||||
Monitor workspace-level metrics:
|
||||
|
||||
```php
|
||||
// Query count per workspace
|
||||
DB::listen(function ($query) {
|
||||
$workspace = workspace();
|
||||
if ($workspace) {
|
||||
Redis::zincrby('workspace:queries', 1, $workspace->id);
|
||||
}
|
||||
});
|
||||
|
||||
// Get top workspaces by query count
|
||||
$top = Redis::zrevrange('workspace:queries', 0, 10, 'WITHSCORES');
|
||||
```
|
||||
|
||||
### Cache Hit Rates
|
||||
|
||||
Track cache effectiveness per workspace:
|
||||
|
||||
```php
|
||||
WorkspaceCacheManager::trackHit($workspace);
|
||||
WorkspaceCacheManager::trackMiss($workspace);
|
||||
|
||||
$hitRate = WorkspaceCacheManager::getHitRate($workspace);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Missing Workspace Context
|
||||
|
||||
```
|
||||
MissingWorkspaceContextException: Workspace context required but not set
|
||||
```
|
||||
|
||||
**Solution:** Ensure middleware sets workspace context:
|
||||
|
||||
```php
|
||||
Route::middleware(RequireWorkspaceContext::class)->group(/*...*/);
|
||||
```
|
||||
|
||||
### Wrong Workspace Data
|
||||
|
||||
```
|
||||
User sees data from different workspace
|
||||
```
|
||||
|
||||
**Solution:** Check workspace is set correctly:
|
||||
|
||||
```php
|
||||
dd(workspace()); // Verify correct workspace
|
||||
```
|
||||
|
||||
### Cache Bleeding
|
||||
|
||||
```
|
||||
Cached data appearing across workspaces
|
||||
```
|
||||
|
||||
**Solution:** Ensure cache keys include workspace ID:
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
$key = "workspace:{$workspace->id}:posts:recent";
|
||||
|
||||
// ❌ Bad
|
||||
$key = "posts:recent"; // Same key for all workspaces!
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Workspace Caching](/patterns-guide/workspace-caching)
|
||||
- [Security Best Practices](/security/overview)
|
||||
- [Testing Multi-Tenancy](/testing/multi-tenancy)
|
||||
513
build/php/architecture/performance.md
Normal file
513
build/php/architecture/performance.md
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
# Performance Optimization
|
||||
|
||||
Best practices and techniques for optimizing Core PHP Framework applications.
|
||||
|
||||
## Database Optimization
|
||||
|
||||
### Eager Loading
|
||||
|
||||
Prevent N+1 queries with eager loading:
|
||||
|
||||
```php
|
||||
// ❌ Bad - N+1 queries
|
||||
$posts = Post::all();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->author->name; // Query per post
|
||||
echo $post->category->name; // Another query per post
|
||||
}
|
||||
|
||||
// ✅ Good - 3 queries total
|
||||
$posts = Post::with(['author', 'category'])->get();
|
||||
foreach ($posts as $post) {
|
||||
echo $post->author->name;
|
||||
echo $post->category->name;
|
||||
}
|
||||
```
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```php
|
||||
// ❌ Bad - fetches all columns
|
||||
$posts = Post::all();
|
||||
|
||||
// ✅ Good - only needed columns
|
||||
$posts = Post::select(['id', 'title', 'created_at'])->get();
|
||||
|
||||
// ✅ Good - count instead of loading all
|
||||
$count = Post::count();
|
||||
|
||||
// ❌ Bad
|
||||
$count = Post::all()->count();
|
||||
|
||||
// ✅ Good - exists check
|
||||
$exists = Post::where('status', 'published')->exists();
|
||||
|
||||
// ❌ Bad
|
||||
$exists = Post::where('status', 'published')->count() > 0;
|
||||
```
|
||||
|
||||
### Chunking Large Datasets
|
||||
|
||||
```php
|
||||
// ❌ Bad - loads everything into memory
|
||||
$posts = Post::all();
|
||||
foreach ($posts as $post) {
|
||||
$this->process($post);
|
||||
}
|
||||
|
||||
// ✅ Good - process in chunks
|
||||
Post::chunk(1000, function ($posts) {
|
||||
foreach ($posts as $post) {
|
||||
$this->process($post);
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Better - lazy collection
|
||||
Post::lazy()->each(function ($post) {
|
||||
$this->process($post);
|
||||
});
|
||||
```
|
||||
|
||||
### Database Indexes
|
||||
|
||||
```php
|
||||
// Migration
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug')->unique(); // Index for lookups
|
||||
$table->string('status')->index(); // Index for filtering
|
||||
$table->foreignId('workspace_id')->constrained(); // Foreign key index
|
||||
|
||||
// Composite index for common query
|
||||
$table->index(['workspace_id', 'status', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Model Caching
|
||||
|
||||
```php
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
public static function findCached(int $id): ?self
|
||||
{
|
||||
return Cache::remember(
|
||||
"posts.{$id}",
|
||||
now()->addHour(),
|
||||
fn () => self::find($id)
|
||||
);
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Invalidate cache on update
|
||||
static::updated(fn ($post) => Cache::forget("posts.{$post->id}"));
|
||||
static::deleted(fn ($post) => Cache::forget("posts.{$post->id}"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Query Result Caching
|
||||
|
||||
```php
|
||||
// ❌ Bad - no caching
|
||||
public function getPopularPosts()
|
||||
{
|
||||
return Post::where('views', '>', 1000)
|
||||
->orderByDesc('views')
|
||||
->limit(10)
|
||||
->get();
|
||||
}
|
||||
|
||||
// ✅ Good - cached for 1 hour
|
||||
public function getPopularPosts()
|
||||
{
|
||||
return Cache::remember('posts.popular', 3600, function () {
|
||||
return Post::where('views', '>', 1000)
|
||||
->orderByDesc('views')
|
||||
->limit(10)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Tags
|
||||
|
||||
```php
|
||||
// Tag cache for easy invalidation
|
||||
Cache::tags(['posts', 'popular'])->put('popular-posts', $posts, 3600);
|
||||
|
||||
// Clear all posts cache
|
||||
Cache::tags('posts')->flush();
|
||||
```
|
||||
|
||||
### Redis Caching
|
||||
|
||||
```php
|
||||
// config/cache.php
|
||||
'default' => env('CACHE_DRIVER', 'redis'),
|
||||
|
||||
'stores' => [
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'cache',
|
||||
'lock_connection' => 'default',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
## Asset Optimization
|
||||
|
||||
### CDN Integration
|
||||
|
||||
```php
|
||||
// Use CDN helper
|
||||
<img src="{{ cdn('images/hero.jpg') }}" alt="Hero">
|
||||
|
||||
// With transformations
|
||||
<img src="{{ cdn('images/hero.jpg', ['width' => 800, 'quality' => 85]) }}">
|
||||
```
|
||||
|
||||
### Image Optimization
|
||||
|
||||
```php
|
||||
use Core\Media\Image\ImageOptimizer;
|
||||
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
|
||||
// Automatic optimization
|
||||
$optimizer->optimize($imagePath, [
|
||||
'quality' => 85,
|
||||
'max_width' => 1920,
|
||||
'strip_exif' => true,
|
||||
'convert_to_webp' => true,
|
||||
]);
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```blade
|
||||
{{-- Lazy load images --}}
|
||||
<img src="{{ cdn($image) }}" loading="lazy" alt="...">
|
||||
|
||||
{{-- Lazy load thumbnails --}}
|
||||
<img src="{{ lazy_thumbnail($image, 'medium') }}" loading="lazy" alt="...">
|
||||
```
|
||||
|
||||
## Code Optimization
|
||||
|
||||
### Lazy Loading Modules
|
||||
|
||||
Modules only load when their events fire:
|
||||
|
||||
```php
|
||||
// Module Boot.php
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
// Only loads when WebRoutesRegistering fires
|
||||
// Saves memory and boot time
|
||||
```
|
||||
|
||||
### Deferred Service Providers
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Analytics;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Contracts\Support\DeferrableProvider;
|
||||
|
||||
class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(AnalyticsService::class);
|
||||
}
|
||||
|
||||
public function provides(): array
|
||||
{
|
||||
return [AnalyticsService::class];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Caching
|
||||
|
||||
```bash
|
||||
# Cache configuration
|
||||
php artisan config:cache
|
||||
|
||||
# Clear config cache
|
||||
php artisan config:clear
|
||||
```
|
||||
|
||||
### Route Caching
|
||||
|
||||
```bash
|
||||
# Cache routes
|
||||
php artisan route:cache
|
||||
|
||||
# Clear route cache
|
||||
php artisan route:clear
|
||||
```
|
||||
|
||||
## Queue Optimization
|
||||
|
||||
### Queue Heavy Operations
|
||||
|
||||
```php
|
||||
// ❌ Bad - slow request
|
||||
public function store(Request $request)
|
||||
{
|
||||
$post = Post::create($request->validated());
|
||||
|
||||
// Slow operations in request cycle
|
||||
$this->generateThumbnails($post);
|
||||
$this->generateOgImage($post);
|
||||
$this->notifySubscribers($post);
|
||||
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
|
||||
// ✅ Good - queued
|
||||
public function store(Request $request)
|
||||
{
|
||||
$post = Post::create($request->validated());
|
||||
|
||||
// Queue heavy operations
|
||||
GenerateThumbnails::dispatch($post);
|
||||
GenerateOgImage::dispatch($post);
|
||||
NotifySubscribers::dispatch($post);
|
||||
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
```
|
||||
|
||||
### Job Batching
|
||||
|
||||
```php
|
||||
use Illuminate\Bus\Batch;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
Bus::batch([
|
||||
new ProcessPost($post1),
|
||||
new ProcessPost($post2),
|
||||
new ProcessPost($post3),
|
||||
])->then(function (Batch $batch) {
|
||||
// All jobs completed successfully
|
||||
})->catch(function (Batch $batch, Throwable $e) {
|
||||
// First batch job failure
|
||||
})->finally(function (Batch $batch) {
|
||||
// Batch finished
|
||||
})->dispatch();
|
||||
```
|
||||
|
||||
## Livewire Optimization
|
||||
|
||||
### Lazy Loading Components
|
||||
|
||||
```blade
|
||||
{{-- Load component when visible --}}
|
||||
<livewire:post-list lazy />
|
||||
|
||||
{{-- Load on interaction --}}
|
||||
<livewire:comments lazy on="click" />
|
||||
```
|
||||
|
||||
### Polling Optimization
|
||||
|
||||
```php
|
||||
// ❌ Bad - polls every 1s
|
||||
<div wire:poll.1s>
|
||||
{{ $count }} users online
|
||||
</div>
|
||||
|
||||
// ✅ Good - polls every 30s
|
||||
<div wire:poll.30s>
|
||||
{{ $count }} users online
|
||||
</div>
|
||||
|
||||
// ✅ Better - poll only when visible
|
||||
<div wire:poll.visible.30s>
|
||||
{{ $count }} users online
|
||||
</div>
|
||||
```
|
||||
|
||||
### Debouncing
|
||||
|
||||
```blade
|
||||
{{-- Debounce search input --}}
|
||||
<input
|
||||
type="search"
|
||||
wire:model.live.debounce.500ms="search"
|
||||
placeholder="Search..."
|
||||
>
|
||||
```
|
||||
|
||||
## Response Optimization
|
||||
|
||||
### HTTP Caching
|
||||
|
||||
```php
|
||||
// Cache response for 1 hour
|
||||
return response($content)
|
||||
->header('Cache-Control', 'public, max-age=3600');
|
||||
|
||||
// ETag caching
|
||||
$etag = md5($content);
|
||||
|
||||
if ($request->header('If-None-Match') === $etag) {
|
||||
return response('', 304);
|
||||
}
|
||||
|
||||
return response($content)
|
||||
->header('ETag', $etag);
|
||||
```
|
||||
|
||||
### Gzip Compression
|
||||
|
||||
```php
|
||||
// config/app.php (handled by middleware)
|
||||
'middleware' => [
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\Illuminate\Http\Middleware\ValidatePostSize::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
],
|
||||
```
|
||||
|
||||
### Response Streaming
|
||||
|
||||
```php
|
||||
// Stream large files
|
||||
return response()->streamDownload(function () {
|
||||
$handle = fopen('large-file.csv', 'r');
|
||||
while (!feof($handle)) {
|
||||
echo fread($handle, 8192);
|
||||
flush();
|
||||
}
|
||||
fclose($handle);
|
||||
}, 'download.csv');
|
||||
```
|
||||
|
||||
## Monitoring Performance
|
||||
|
||||
### Query Logging
|
||||
|
||||
```php
|
||||
// Enable query log in development
|
||||
if (app()->isLocal()) {
|
||||
DB::enableQueryLog();
|
||||
}
|
||||
|
||||
// View queries
|
||||
dd(DB::getQueryLog());
|
||||
```
|
||||
|
||||
### Telescope
|
||||
|
||||
```bash
|
||||
# Install Laravel Telescope
|
||||
composer require laravel/telescope --dev
|
||||
|
||||
php artisan telescope:install
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### Clockwork
|
||||
|
||||
```bash
|
||||
# Install Clockwork
|
||||
composer require itsgoingd/clockwork --dev
|
||||
```
|
||||
|
||||
### Application Performance
|
||||
|
||||
```php
|
||||
// Measure execution time
|
||||
$start = microtime(true);
|
||||
|
||||
// Your code here
|
||||
|
||||
$duration = (microtime(true) - $start) * 1000; // milliseconds
|
||||
Log::info("Operation took {$duration}ms");
|
||||
```
|
||||
|
||||
## Load Testing
|
||||
|
||||
### Using Apache Bench
|
||||
|
||||
```bash
|
||||
# 1000 requests, 10 concurrent
|
||||
ab -n 1000 -c 10 https://example.com/
|
||||
```
|
||||
|
||||
### Using k6
|
||||
|
||||
```javascript
|
||||
// load-test.js
|
||||
import http from 'k6/http';
|
||||
|
||||
export let options = {
|
||||
vus: 10, // 10 virtual users
|
||||
duration: '30s',
|
||||
};
|
||||
|
||||
export default function () {
|
||||
http.get('https://example.com/api/posts');
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
k6 run load-test.js
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
### Database
|
||||
- [ ] Use eager loading to prevent N+1 queries
|
||||
- [ ] Add indexes to frequently queried columns
|
||||
- [ ] Use `select()` to limit columns
|
||||
- [ ] Chunk large datasets
|
||||
- [ ] Use `exists()` instead of `count() > 0`
|
||||
|
||||
### Caching
|
||||
- [ ] Cache expensive query results
|
||||
- [ ] Use Redis for session/cache storage
|
||||
- [ ] Implement cache tags for easy invalidation
|
||||
- [ ] Set appropriate cache TTLs
|
||||
|
||||
### Assets
|
||||
- [ ] Optimize images before uploading
|
||||
- [ ] Use CDN for static assets
|
||||
- [ ] Enable lazy loading for images
|
||||
- [ ] Generate responsive image sizes
|
||||
|
||||
### Code
|
||||
- [ ] Queue heavy operations
|
||||
- [ ] Use lazy loading for modules
|
||||
- [ ] Cache configuration and routes
|
||||
- [ ] Implement deferred service providers
|
||||
|
||||
### Frontend
|
||||
- [ ] Minimize JavaScript bundle size
|
||||
- [ ] Debounce user input
|
||||
- [ ] Use lazy loading for Livewire components
|
||||
- [ ] Optimize polling intervals
|
||||
|
||||
### Monitoring
|
||||
- [ ] Use Telescope/Clockwork in development
|
||||
- [ ] Log slow queries
|
||||
- [ ] Monitor cache hit rates
|
||||
- [ ] Track job queue performance
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Configuration →](/packages/core/configuration)
|
||||
- [CDN Integration →](/packages/core/cdn)
|
||||
- [Media Processing →](/packages/core/media)
|
||||
399
build/php/cdn.md
Normal file
399
build/php/cdn.md
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
# CDN Integration
|
||||
|
||||
Core PHP provides unified CDN integration for BunnyCDN and Cloudflare with automatic asset offloading, URL generation, and cache management.
|
||||
|
||||
## Configuration
|
||||
|
||||
```php
|
||||
// config/cdn.php
|
||||
return [
|
||||
'driver' => env('CDN_DRIVER', 'bunnycdn'),
|
||||
|
||||
'bunnycdn' => [
|
||||
'api_key' => env('BUNNY_API_KEY'),
|
||||
'storage_zone' => env('BUNNY_STORAGE_ZONE'),
|
||||
'storage_password' => env('BUNNY_STORAGE_PASSWORD'),
|
||||
'cdn_url' => env('BUNNY_CDN_URL'),
|
||||
'pull_zone_id' => env('BUNNY_PULL_ZONE_ID'),
|
||||
],
|
||||
|
||||
'cloudflare' => [
|
||||
'zone_id' => env('CLOUDFLARE_ZONE_ID'),
|
||||
'api_token' => env('CLOUDFLARE_API_TOKEN'),
|
||||
'cdn_url' => env('CLOUDFLARE_CDN_URL'),
|
||||
],
|
||||
|
||||
'offload' => [
|
||||
'enabled' => env('CDN_OFFLOAD_ENABLED', false),
|
||||
'paths' => ['public/images', 'public/media', 'storage/app/public'],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Generating CDN URLs
|
||||
|
||||
```php
|
||||
use Core\Cdn\Facades\Cdn;
|
||||
|
||||
// Generate CDN URL
|
||||
$url = Cdn::url('images/photo.jpg');
|
||||
// https://cdn.example.com/images/photo.jpg
|
||||
|
||||
// With transformation parameters
|
||||
$url = Cdn::url('images/photo.jpg', [
|
||||
'width' => 800,
|
||||
'quality' => 85,
|
||||
]);
|
||||
```
|
||||
|
||||
### Helper Function
|
||||
|
||||
```php
|
||||
// Global helper
|
||||
$url = cdn_url('images/photo.jpg');
|
||||
|
||||
// In Blade templates
|
||||
<img src="{{ cdn_url('images/photo.jpg') }}" alt="Photo">
|
||||
```
|
||||
|
||||
### Storing Files
|
||||
|
||||
```php
|
||||
// Upload file to CDN
|
||||
$path = Cdn::store($uploadedFile, 'media');
|
||||
|
||||
// Store with custom filename
|
||||
$path = Cdn::store($uploadedFile, 'media', 'custom-name.jpg');
|
||||
|
||||
// Store from contents
|
||||
$path = Cdn::put('path/file.txt', $contents);
|
||||
```
|
||||
|
||||
### Deleting Files
|
||||
|
||||
```php
|
||||
// Delete single file
|
||||
Cdn::delete('media/photo.jpg');
|
||||
|
||||
// Delete multiple files
|
||||
Cdn::delete(['media/photo1.jpg', 'media/photo2.jpg']);
|
||||
|
||||
// Delete directory
|
||||
Cdn::deleteDirectory('media/old');
|
||||
```
|
||||
|
||||
## Cache Purging
|
||||
|
||||
### Purge Single File
|
||||
|
||||
```php
|
||||
// Purge specific file from CDN cache
|
||||
Cdn::purge('images/photo.jpg');
|
||||
```
|
||||
|
||||
### Purge Multiple Files
|
||||
|
||||
```php
|
||||
// Purge multiple files
|
||||
Cdn::purge([
|
||||
'images/photo1.jpg',
|
||||
'images/photo2.jpg',
|
||||
]);
|
||||
```
|
||||
|
||||
### Purge by Pattern
|
||||
|
||||
```php
|
||||
// Purge all images
|
||||
Cdn::purge('images/*');
|
||||
|
||||
// Purge all JPEGs
|
||||
Cdn::purge('**/*.jpg');
|
||||
```
|
||||
|
||||
### Purge Everything
|
||||
|
||||
```php
|
||||
// Purge entire CDN cache (use sparingly!)
|
||||
Cdn::purgeAll();
|
||||
```
|
||||
|
||||
## Asset Offloading
|
||||
|
||||
Automatically offload existing assets to CDN:
|
||||
|
||||
```bash
|
||||
# Offload public disk
|
||||
php artisan storage:offload --disk=public
|
||||
|
||||
# Offload specific path
|
||||
php artisan storage:offload --path=public/images
|
||||
|
||||
# Dry run (preview without uploading)
|
||||
php artisan storage:offload --dry-run
|
||||
```
|
||||
|
||||
### Programmatic Offloading
|
||||
|
||||
```php
|
||||
use Core\Cdn\Services\AssetPipeline;
|
||||
|
||||
$pipeline = app(AssetPipeline::class);
|
||||
|
||||
// Offload directory
|
||||
$result = $pipeline->offload('public/images', [
|
||||
'extensions' => ['jpg', 'png', 'gif', 'webp'],
|
||||
'min_size' => 1024, // Only files > 1KB
|
||||
]);
|
||||
|
||||
echo "Uploaded: {$result['uploaded']} files\n";
|
||||
echo "Skipped: {$result['skipped']} files\n";
|
||||
```
|
||||
|
||||
## URL Builder
|
||||
|
||||
Advanced URL construction with transformations:
|
||||
|
||||
```php
|
||||
use Core\Cdn\Services\CdnUrlBuilder;
|
||||
|
||||
$builder = app(CdnUrlBuilder::class);
|
||||
|
||||
$url = $builder->build('images/photo.jpg', [
|
||||
// Dimensions
|
||||
'width' => 800,
|
||||
'height' => 600,
|
||||
'aspect_ratio' => '16:9',
|
||||
|
||||
// Quality
|
||||
'quality' => 85,
|
||||
'format' => 'webp',
|
||||
|
||||
// Effects
|
||||
'blur' => 10,
|
||||
'brightness' => 1.2,
|
||||
'contrast' => 1.1,
|
||||
|
||||
// Cropping
|
||||
'crop' => 'center',
|
||||
'gravity' => 'face',
|
||||
]);
|
||||
```
|
||||
|
||||
## BunnyCDN Specific
|
||||
|
||||
### Pull Zone Management
|
||||
|
||||
```php
|
||||
use Core\Cdn\Services\BunnyCdnService;
|
||||
|
||||
$bunny = app(BunnyCdnService::class);
|
||||
|
||||
// Get pull zone info
|
||||
$pullZone = $bunny->getPullZone($pullZoneId);
|
||||
|
||||
// Add/remove hostnames
|
||||
$bunny->addHostname($pullZoneId, 'cdn.example.com');
|
||||
$bunny->removeHostname($pullZoneId, 'cdn.example.com');
|
||||
|
||||
// Enable/disable cache
|
||||
$bunny->setCacheEnabled($pullZoneId, true);
|
||||
```
|
||||
|
||||
### Storage Zone Operations
|
||||
|
||||
```php
|
||||
use Core\Cdn\Services\BunnyStorageService;
|
||||
|
||||
$storage = app(BunnyStorageService::class);
|
||||
|
||||
// List files
|
||||
$files = $storage->list('media/');
|
||||
|
||||
// Get file info
|
||||
$info = $storage->getFileInfo('media/photo.jpg');
|
||||
|
||||
// Download file
|
||||
$contents = $storage->download('media/photo.jpg');
|
||||
```
|
||||
|
||||
## Cloudflare Specific
|
||||
|
||||
### Zone Management
|
||||
|
||||
```php
|
||||
use Core\Cdn\Services\FluxCdnService;
|
||||
|
||||
$cloudflare = app(FluxCdnService::class);
|
||||
|
||||
// Purge cache by URLs
|
||||
$cloudflare->purgePaths([
|
||||
'https://example.com/images/photo.jpg',
|
||||
'https://example.com/styles/app.css',
|
||||
]);
|
||||
|
||||
// Purge by cache tags
|
||||
$cloudflare->purgeTags(['images', 'media']);
|
||||
|
||||
// Purge everything
|
||||
$cloudflare->purgeEverything();
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Fake CDN
|
||||
|
||||
```php
|
||||
use Core\Cdn\Facades\Cdn;
|
||||
|
||||
class UploadTest extends TestCase
|
||||
{
|
||||
public function test_uploads_file(): void
|
||||
{
|
||||
Cdn::fake();
|
||||
|
||||
$response = $this->post('/upload', [
|
||||
'file' => UploadedFile::fake()->image('photo.jpg'),
|
||||
]);
|
||||
|
||||
Cdn::assertStored('media/photo.jpg');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Assert Operations
|
||||
|
||||
```php
|
||||
// Assert file was stored
|
||||
Cdn::assertStored('path/file.jpg');
|
||||
|
||||
// Assert file was deleted
|
||||
Cdn::assertDeleted('path/file.jpg');
|
||||
|
||||
// Assert cache was purged
|
||||
Cdn::assertPurged('path/file.jpg');
|
||||
|
||||
// Assert nothing was stored
|
||||
Cdn::assertNothingStored();
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### URL Caching
|
||||
|
||||
CDN URLs are cached to avoid repeated lookups:
|
||||
|
||||
```php
|
||||
// URLs cached for 1 hour
|
||||
$url = Cdn::url('images/photo.jpg'); // Generates URL + caches
|
||||
$url = Cdn::url('images/photo.jpg'); // Returns from cache
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```php
|
||||
// Batch delete (single API call)
|
||||
Cdn::delete([
|
||||
'media/photo1.jpg',
|
||||
'media/photo2.jpg',
|
||||
'media/photo3.jpg',
|
||||
]);
|
||||
|
||||
// Batch purge (single API call)
|
||||
Cdn::purge([
|
||||
'images/*.jpg',
|
||||
'styles/*.css',
|
||||
]);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Helper in Blade
|
||||
|
||||
```blade
|
||||
{{-- ✅ Good --}}
|
||||
<img src="{{ cdn_url('images/photo.jpg') }}" alt="Photo">
|
||||
|
||||
{{-- ❌ Bad - relative path --}}
|
||||
<img src="/images/photo.jpg" alt="Photo">
|
||||
```
|
||||
|
||||
### 2. Offload Static Assets
|
||||
|
||||
```php
|
||||
// ✅ Good - offload after upload
|
||||
public function store(Request $request)
|
||||
{
|
||||
$path = $request->file('image')->store('media');
|
||||
|
||||
// Offload to CDN immediately
|
||||
Cdn::store($path);
|
||||
|
||||
return $path;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Purge After Updates
|
||||
|
||||
```php
|
||||
// ✅ Good - purge on update
|
||||
public function update(Request $request, Media $media)
|
||||
{
|
||||
$oldPath = $media->path;
|
||||
|
||||
$media->update($request->validated());
|
||||
|
||||
// Purge old file from cache
|
||||
Cdn::purge($oldPath);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Use Transformations
|
||||
|
||||
```php
|
||||
// ✅ Good - CDN transforms image
|
||||
<img src="{{ cdn_url('photo.jpg', ['width' => 400, 'quality' => 85]) }}">
|
||||
|
||||
// ❌ Bad - transform server-side
|
||||
<img src="{{ route('image.transform', ['path' => 'photo.jpg', 'width' => 400]) }}">
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Files Not Appearing
|
||||
|
||||
```bash
|
||||
# Verify CDN credentials
|
||||
php artisan tinker
|
||||
>>> Cdn::store(UploadedFile::fake()->image('test.jpg'), 'test')
|
||||
|
||||
# Check CDN dashboard for new files
|
||||
```
|
||||
|
||||
### Purge Not Working
|
||||
|
||||
```bash
|
||||
# Verify pull zone ID
|
||||
php artisan tinker
|
||||
>>> config('cdn.bunnycdn.pull_zone_id')
|
||||
|
||||
# Manual purge via dashboard
|
||||
```
|
||||
|
||||
### URLs Not Resolving
|
||||
|
||||
```php
|
||||
// Check CDN URL configuration
|
||||
echo config('cdn.bunnycdn.cdn_url');
|
||||
|
||||
// Verify file exists on CDN
|
||||
$exists = Cdn::exists('path/file.jpg');
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Media Processing →](/core/media)
|
||||
- [Storage Configuration →](/guide/configuration#storage)
|
||||
- [Asset Pipeline →](/core/media#asset-pipeline)
|
||||
474
build/php/configuration.md
Normal file
474
build/php/configuration.md
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
# Configuration Management
|
||||
|
||||
Core PHP Framework provides a powerful multi-profile configuration system with versioning, rollback capabilities, and environment-specific overrides.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Storing Configuration
|
||||
|
||||
```php
|
||||
use Core\Config\ConfigService;
|
||||
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
// Store simple value
|
||||
$config->set('app.name', 'My Application');
|
||||
|
||||
// Store nested configuration
|
||||
$config->set('mail.driver', 'smtp', [
|
||||
'host' => 'smtp.mailtrap.io',
|
||||
'port' => 2525,
|
||||
'encryption' => 'tls',
|
||||
]);
|
||||
|
||||
// Store with profile
|
||||
$config->set('cache.driver', 'redis', [], 'production');
|
||||
```
|
||||
|
||||
### Retrieving Configuration
|
||||
|
||||
```php
|
||||
// Get simple value
|
||||
$name = $config->get('app.name');
|
||||
|
||||
// Get with default
|
||||
$driver = $config->get('cache.driver', 'file');
|
||||
|
||||
// Get nested value
|
||||
$host = $config->get('mail.driver.host');
|
||||
|
||||
// Get from specific profile
|
||||
$driver = $config->get('cache.driver', 'file', 'production');
|
||||
```
|
||||
|
||||
## Profiles
|
||||
|
||||
Profiles enable environment-specific configuration:
|
||||
|
||||
### Creating Profiles
|
||||
|
||||
```php
|
||||
use Core\Config\Models\ConfigProfile;
|
||||
|
||||
// Development profile
|
||||
$dev = ConfigProfile::create([
|
||||
'name' => 'development',
|
||||
'description' => 'Development environment settings',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// Staging profile
|
||||
$staging = ConfigProfile::create([
|
||||
'name' => 'staging',
|
||||
'description' => 'Staging environment',
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
// Production profile
|
||||
$prod = ConfigProfile::create([
|
||||
'name' => 'production',
|
||||
'description' => 'Production environment',
|
||||
'is_active' => false,
|
||||
]);
|
||||
```
|
||||
|
||||
### Activating Profiles
|
||||
|
||||
```php
|
||||
// Activate production profile
|
||||
$prod->activate();
|
||||
|
||||
// Deactivate all others
|
||||
ConfigProfile::query()
|
||||
->where('id', '!=', $prod->id)
|
||||
->update(['is_active' => false]);
|
||||
```
|
||||
|
||||
### Profile Inheritance
|
||||
|
||||
```php
|
||||
// Set base value
|
||||
$config->set('cache.ttl', 3600);
|
||||
|
||||
// Override in production
|
||||
$config->set('cache.ttl', 86400, [], 'production');
|
||||
|
||||
// Override in development
|
||||
$config->set('cache.ttl', 60, [], 'development');
|
||||
|
||||
// Retrieval uses active profile automatically
|
||||
$ttl = $config->get('cache.ttl'); // Returns profile-specific value
|
||||
```
|
||||
|
||||
## Configuration Keys
|
||||
|
||||
### Key Metadata
|
||||
|
||||
```php
|
||||
use Core\Config\Models\ConfigKey;
|
||||
|
||||
$key = ConfigKey::create([
|
||||
'key' => 'api.rate_limit',
|
||||
'description' => 'API rate limit per hour',
|
||||
'type' => 'integer',
|
||||
'is_sensitive' => false,
|
||||
'validation_rules' => ['required', 'integer', 'min:100'],
|
||||
]);
|
||||
```
|
||||
|
||||
### Sensitive Configuration
|
||||
|
||||
```php
|
||||
// Mark as sensitive (encrypted at rest)
|
||||
$key = ConfigKey::create([
|
||||
'key' => 'payment.stripe.secret',
|
||||
'is_sensitive' => true,
|
||||
]);
|
||||
|
||||
// Set sensitive value (auto-encrypted)
|
||||
$config->set('payment.stripe.secret', 'sk_live_...');
|
||||
|
||||
// Retrieve (auto-decrypted)
|
||||
$secret = $config->get('payment.stripe.secret');
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
```php
|
||||
// Validation runs automatically
|
||||
try {
|
||||
$config->set('api.rate_limit', 'invalid'); // Throws ValidationException
|
||||
} catch (ValidationException $e) {
|
||||
// Handle validation error
|
||||
}
|
||||
|
||||
// Valid value
|
||||
$config->set('api.rate_limit', 1000); // ✅ Passes validation
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
Track configuration changes with automatic versioning:
|
||||
|
||||
### Creating Versions
|
||||
|
||||
```php
|
||||
use Core\Config\ConfigVersioning;
|
||||
|
||||
$versioning = app(ConfigVersioning::class);
|
||||
|
||||
// Create snapshot
|
||||
$version = $versioning->createVersion('production', [
|
||||
'description' => 'Pre-deployment snapshot',
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
```
|
||||
|
||||
### Viewing Versions
|
||||
|
||||
```php
|
||||
use Core\Config\Models\ConfigVersion;
|
||||
|
||||
// List all versions
|
||||
$versions = ConfigVersion::query()
|
||||
->where('profile', 'production')
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
// Get specific version
|
||||
$version = ConfigVersion::find($id);
|
||||
|
||||
// View snapshot
|
||||
$snapshot = $version->snapshot; // ['cache.driver' => 'redis', ...]
|
||||
```
|
||||
|
||||
### Rolling Back
|
||||
|
||||
```php
|
||||
// Rollback to previous version
|
||||
$versioning->rollback($version->id);
|
||||
|
||||
// Rollback with confirmation
|
||||
if ($version->created_at->isToday()) {
|
||||
$versioning->rollback($version->id);
|
||||
}
|
||||
```
|
||||
|
||||
### Comparing Versions
|
||||
|
||||
```php
|
||||
use Core\Config\VersionDiff;
|
||||
|
||||
$diff = app(VersionDiff::class);
|
||||
|
||||
// Compare two versions
|
||||
$changes = $diff->compare($oldVersion, $newVersion);
|
||||
|
||||
// Output:
|
||||
[
|
||||
'added' => ['cache.prefix' => 'app_'],
|
||||
'modified' => ['cache.ttl' => ['old' => 3600, 'new' => 7200]],
|
||||
'removed' => ['cache.legacy_driver'],
|
||||
]
|
||||
```
|
||||
|
||||
## Import & Export
|
||||
|
||||
### Exporting Configuration
|
||||
|
||||
```php
|
||||
use Core\Config\ConfigExporter;
|
||||
|
||||
$exporter = app(ConfigExporter::class);
|
||||
|
||||
// Export active profile
|
||||
$json = $exporter->export();
|
||||
|
||||
// Export specific profile
|
||||
$json = $exporter->export('production');
|
||||
|
||||
// Export with metadata
|
||||
$json = $exporter->export('production', [
|
||||
'include_sensitive' => false, // Exclude secrets
|
||||
'include_metadata' => true, // Include descriptions
|
||||
]);
|
||||
```
|
||||
|
||||
**Export Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"profile": "production",
|
||||
"exported_at": "2026-01-26T12:00:00Z",
|
||||
"config": {
|
||||
"cache.driver": {
|
||||
"value": "redis",
|
||||
"description": "Cache driver",
|
||||
"type": "string"
|
||||
},
|
||||
"cache.ttl": {
|
||||
"value": 86400,
|
||||
"description": "Cache TTL in seconds",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Importing Configuration
|
||||
|
||||
```php
|
||||
use Core\Config\ConfigService;
|
||||
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
// Import from JSON
|
||||
$result = $config->import($json, 'production');
|
||||
|
||||
// Import with merge strategy
|
||||
$result = $config->import($json, 'production', [
|
||||
'merge' => true, // Merge with existing
|
||||
'overwrite' => false, // Don't overwrite existing
|
||||
'validate' => true, // Validate before import
|
||||
]);
|
||||
```
|
||||
|
||||
**Import Result:**
|
||||
|
||||
```php
|
||||
use Core\Config\ImportResult;
|
||||
|
||||
$result->imported; // ['cache.driver', 'cache.ttl']
|
||||
$result->skipped; // ['cache.legacy']
|
||||
$result->failed; // ['cache.invalid' => 'Validation failed']
|
||||
```
|
||||
|
||||
### Console Commands
|
||||
|
||||
```bash
|
||||
# Export configuration
|
||||
php artisan config:export production --output=config.json
|
||||
|
||||
# Import configuration
|
||||
php artisan config:import config.json --profile=staging
|
||||
|
||||
# Create version snapshot
|
||||
php artisan config:version production --message="Pre-deployment"
|
||||
```
|
||||
|
||||
## Configuration Providers
|
||||
|
||||
Create reusable configuration providers:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Config;
|
||||
|
||||
use Core\Config\Contracts\ConfigProvider;
|
||||
|
||||
class BlogConfigProvider implements ConfigProvider
|
||||
{
|
||||
public function provide(): array
|
||||
{
|
||||
return [
|
||||
'blog.posts_per_page' => [
|
||||
'value' => 10,
|
||||
'description' => 'Posts per page',
|
||||
'type' => 'integer',
|
||||
'validation' => ['required', 'integer', 'min:1'],
|
||||
],
|
||||
'blog.allow_comments' => [
|
||||
'value' => true,
|
||||
'description' => 'Enable comments',
|
||||
'type' => 'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Register Provider:**
|
||||
|
||||
```php
|
||||
use Core\Events\FrameworkBooted;
|
||||
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
$config = app(ConfigService::class);
|
||||
$config->register(new BlogConfigProvider());
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
Configuration is cached for performance:
|
||||
|
||||
```php
|
||||
// Clear config cache
|
||||
$config->invalidate();
|
||||
|
||||
// Clear specific key cache
|
||||
$config->invalidate('cache.driver');
|
||||
|
||||
// Rebuild cache
|
||||
$config->rebuild();
|
||||
```
|
||||
|
||||
**Cache Strategy:**
|
||||
- Uses `remember()` with 1-hour TTL
|
||||
- Invalidated on config changes
|
||||
- Per-profile cache keys
|
||||
- Tagged for easy clearing
|
||||
|
||||
## Events
|
||||
|
||||
Configuration changes fire events:
|
||||
|
||||
```php
|
||||
use Core\Config\Events\ConfigChanged;
|
||||
use Core\Config\Events\ConfigInvalidated;
|
||||
|
||||
// Listen for changes
|
||||
Event::listen(ConfigChanged::class, function ($event) {
|
||||
Log::info('Config changed', [
|
||||
'key' => $event->key,
|
||||
'old' => $event->oldValue,
|
||||
'new' => $event->newValue,
|
||||
]);
|
||||
});
|
||||
|
||||
// Listen for cache invalidation
|
||||
Event::listen(ConfigInvalidated::class, function ($event) {
|
||||
// Rebuild dependent caches
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Profiles for Environments
|
||||
|
||||
```php
|
||||
// ✅ Good - environment-specific
|
||||
$config->set('cache.driver', 'redis', [], 'production');
|
||||
$config->set('cache.driver', 'array', [], 'testing');
|
||||
|
||||
// ❌ Bad - single value for all environments
|
||||
$config->set('cache.driver', 'redis');
|
||||
```
|
||||
|
||||
### 2. Mark Sensitive Data
|
||||
|
||||
```php
|
||||
// ✅ Good - encrypted at rest
|
||||
ConfigKey::create([
|
||||
'key' => 'payment.api_key',
|
||||
'is_sensitive' => true,
|
||||
]);
|
||||
|
||||
// ❌ Bad - plaintext secrets
|
||||
$config->set('payment.api_key', 'secret123');
|
||||
```
|
||||
|
||||
### 3. Version Before Changes
|
||||
|
||||
```php
|
||||
// ✅ Good - create snapshot first
|
||||
$versioning->createVersion('production', [
|
||||
'description' => 'Pre-cache-driver-change',
|
||||
]);
|
||||
$config->set('cache.driver', 'redis', [], 'production');
|
||||
|
||||
// ❌ Bad - no rollback point
|
||||
$config->set('cache.driver', 'redis', [], 'production');
|
||||
```
|
||||
|
||||
### 4. Validate Configuration
|
||||
|
||||
```php
|
||||
// ✅ Good - validation rules
|
||||
ConfigKey::create([
|
||||
'key' => 'api.rate_limit',
|
||||
'validation_rules' => ['required', 'integer', 'min:100', 'max:10000'],
|
||||
]);
|
||||
|
||||
// ❌ Bad - no validation
|
||||
$config->set('api.rate_limit', 'unlimited'); // Invalid!
|
||||
```
|
||||
|
||||
## Testing Configuration
|
||||
|
||||
```php
|
||||
use Tests\TestCase;
|
||||
use Core\Config\ConfigService;
|
||||
|
||||
class ConfigTest extends TestCase
|
||||
{
|
||||
public function test_stores_configuration(): void
|
||||
{
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
$config->set('test.key', 'value');
|
||||
|
||||
$this->assertEquals('value', $config->get('test.key'));
|
||||
}
|
||||
|
||||
public function test_profile_isolation(): void
|
||||
{
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
$config->set('cache.driver', 'redis', [], 'production');
|
||||
$config->set('cache.driver', 'array', [], 'testing');
|
||||
|
||||
// Activate testing profile
|
||||
ConfigProfile::where('name', 'testing')->first()->activate();
|
||||
|
||||
$this->assertEquals('array', $config->get('cache.driver'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Module System →](/core/modules)
|
||||
- [Multi-Tenancy →](/core/tenancy)
|
||||
420
build/php/events.md
Normal file
420
build/php/events.md
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
# Lifecycle Events
|
||||
|
||||
Core PHP Framework uses lifecycle events to coordinate module loading and system initialization. This event-driven architecture enables lazy loading and keeps modules decoupled.
|
||||
|
||||
## Event Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Application Boot] --> B[WebRoutesRegistering]
|
||||
A --> C[ApiRoutesRegistering]
|
||||
A --> D[AdminPanelBooting]
|
||||
A --> E[ClientRoutesRegistering]
|
||||
A --> F[ConsoleBooting]
|
||||
A --> G[McpToolsRegistering]
|
||||
B --> H[FrameworkBooted]
|
||||
C --> H
|
||||
D --> H
|
||||
E --> H
|
||||
F --> H
|
||||
G --> H
|
||||
```
|
||||
|
||||
## Core Events
|
||||
|
||||
### WebRoutesRegistering
|
||||
|
||||
Fired when public web routes are being registered.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
$event->translations('blog', __DIR__.'/Lang');
|
||||
|
||||
$event->routes(function () {
|
||||
require __DIR__.'/Routes/web.php';
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `views(string $namespace, string $path)` - Register view namespace
|
||||
- `translations(string $namespace, string $path)` - Register translations
|
||||
- `routes(Closure $callback)` - Register routes
|
||||
- `middleware(array $middleware)` - Add global middleware
|
||||
|
||||
### ApiRoutesRegistering
|
||||
|
||||
Fired when API routes are being registered.
|
||||
|
||||
```php
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(function () {
|
||||
Route::middleware(['auth:sanctum', 'scope:posts:read'])
|
||||
->get('/posts', [PostApiController::class, 'index']);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `routes(Closure $callback)` - Register API routes
|
||||
- `middleware(array $middleware)` - Add API middleware
|
||||
- `prefix(string $prefix)` - Set route prefix
|
||||
- `version(string $version)` - Set API version
|
||||
|
||||
### AdminPanelBooting
|
||||
|
||||
Fired when admin panel is initializing.
|
||||
|
||||
```php
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Front\Admin\Contracts\AdminMenuProvider;
|
||||
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
$event->menu(new BlogMenuProvider());
|
||||
$event->views('blog-admin', __DIR__.'/Views/Admin');
|
||||
$event->livewire('blog', __DIR__.'/Livewire');
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `menu(AdminMenuProvider $provider)` - Register menu provider
|
||||
- `views(string $namespace, string $path)` - Register admin views
|
||||
- `livewire(string $namespace, string $path)` - Register Livewire components
|
||||
- `assets(string $path)` - Register frontend assets
|
||||
|
||||
### ClientRoutesRegistering
|
||||
|
||||
Fired when authenticated client routes are being registered.
|
||||
|
||||
```php
|
||||
use Core\Events\ClientRoutesRegistering;
|
||||
|
||||
public function onClientRoutes(ClientRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(function () {
|
||||
Route::middleware(['auth', 'verified'])
|
||||
->group(function () {
|
||||
Route::get('/dashboard', [DashboardController::class, 'index']);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### ConsoleBooting
|
||||
|
||||
Fired when Artisan console is initializing.
|
||||
|
||||
```php
|
||||
use Core\Events\ConsoleBooting;
|
||||
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
$event->commands([
|
||||
PublishPostsCommand::class,
|
||||
GenerateSitemapCommand::class,
|
||||
]);
|
||||
|
||||
$event->schedule(function ($schedule) {
|
||||
$schedule->command('posts:publish')
|
||||
->hourly()
|
||||
->withoutOverlapping();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `commands(array $commands)` - Register Artisan commands
|
||||
- `schedule(Closure $callback)` - Define scheduled tasks
|
||||
|
||||
### McpToolsRegistering
|
||||
|
||||
Fired when MCP (Model Context Protocol) tools are being registered.
|
||||
|
||||
```php
|
||||
use Core\Events\McpToolsRegistering;
|
||||
use Mod\Blog\Mcp\BlogTools;
|
||||
|
||||
public function onMcpTools(McpToolsRegistering $event): void
|
||||
{
|
||||
$event->tool(new BlogTools());
|
||||
}
|
||||
```
|
||||
|
||||
**Available Methods:**
|
||||
- `tool(object $tool)` - Register MCP tool
|
||||
- `resource(string $type, Closure $callback)` - Register resource provider
|
||||
- `prompt(string $name, Closure $callback)` - Register prompt template
|
||||
|
||||
### FrameworkBooted
|
||||
|
||||
Fired after all modules have loaded. Use for late initialization.
|
||||
|
||||
```php
|
||||
use Core\Events\FrameworkBooted;
|
||||
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
// Run after all modules loaded
|
||||
$this->registerPolicies();
|
||||
$this->publishAssets();
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Events
|
||||
|
||||
Create custom lifecycle events by extending `LifecycleEvent`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Shop\Events;
|
||||
|
||||
use Core\Events\LifecycleEvent;
|
||||
use Core\Events\Concerns\HasEventVersion;
|
||||
|
||||
class PaymentGatewaysRegistering extends LifecycleEvent
|
||||
{
|
||||
use HasEventVersion;
|
||||
|
||||
protected array $gateways = [];
|
||||
|
||||
public function gateway(string $name, string $class): void
|
||||
{
|
||||
$this->gateways[$name] = $class;
|
||||
}
|
||||
|
||||
public function getGateways(): array
|
||||
{
|
||||
return $this->gateways;
|
||||
}
|
||||
|
||||
public function version(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Module:**
|
||||
|
||||
```php
|
||||
use Mod\Shop\Events\PaymentGatewaysRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
PaymentGatewaysRegistering::class => 'onPaymentGateways',
|
||||
];
|
||||
|
||||
public function onPaymentGateways(PaymentGatewaysRegistering $event): void
|
||||
{
|
||||
$event->gateway('stripe', StripeGateway::class);
|
||||
$event->gateway('paypal', PayPalGateway::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Versioning
|
||||
|
||||
Events can declare versions for backward compatibility:
|
||||
|
||||
```php
|
||||
use Core\Events\Concerns\HasEventVersion;
|
||||
|
||||
class MyEvent extends LifecycleEvent
|
||||
{
|
||||
use HasEventVersion;
|
||||
|
||||
public function version(): string
|
||||
{
|
||||
return '2.1.0';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Version Checking:**
|
||||
|
||||
```php
|
||||
if (version_compare($event->version(), '2.0.0', '>=')) {
|
||||
// Use v2 features
|
||||
} else {
|
||||
// Fallback for v1
|
||||
}
|
||||
```
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
Modules only instantiate when their events fire:
|
||||
|
||||
```php
|
||||
// ModuleRegistry registers lazy listeners
|
||||
Event::listen(WebRoutesRegistering::class, function ($event) {
|
||||
// Module instantiated only when event fires
|
||||
$module = new \Mod\Blog\Boot();
|
||||
$module->onWebRoutes($event);
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Faster boot times
|
||||
- Lower memory usage
|
||||
- Load only what's needed
|
||||
- No unused module overhead
|
||||
|
||||
## Event Profiling
|
||||
|
||||
Profile listener execution in development:
|
||||
|
||||
```php
|
||||
use Core\Events\ListenerProfiler;
|
||||
|
||||
// config/app.php
|
||||
'providers' => [
|
||||
// ...
|
||||
ListenerProfiler::class, // Only in development
|
||||
],
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
Lifecycle Event Performance:
|
||||
- WebRoutesRegistering: 45ms (12 listeners)
|
||||
- ApiRoutesRegistering: 23ms (8 listeners)
|
||||
- AdminPanelBooting: 67ms (15 listeners)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Keep Listeners Fast
|
||||
|
||||
```php
|
||||
// ✅ Good - quick registration
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
|
||||
// ❌ Bad - heavy processing
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Don't do expensive operations here!
|
||||
$this->generateSitemap();
|
||||
$this->warmCache();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Appropriate Events
|
||||
|
||||
```php
|
||||
// ✅ Good - right event for the job
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
|
||||
// ❌ Bad - wrong event
|
||||
WebRoutesRegistering::class => 'registerCommands', // Use ConsoleBooting!
|
||||
```
|
||||
|
||||
### 3. Defer Heavy Work
|
||||
|
||||
```php
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
// ✅ Good - queue heavy work
|
||||
dispatch(new BuildSearchIndex());
|
||||
|
||||
// ❌ Bad - blocking
|
||||
$this->buildSearchIndex(); // Takes 5 seconds!
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Handle Missing Dependencies
|
||||
|
||||
```php
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
if (!class_exists(Livewire::class)) {
|
||||
Log::warning('Livewire not installed, skipping components');
|
||||
return;
|
||||
}
|
||||
|
||||
$event->livewire('blog', __DIR__.'/Livewire');
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Events
|
||||
|
||||
```php
|
||||
use Tests\TestCase;
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
|
||||
class BlogBootTest extends TestCase
|
||||
{
|
||||
public function test_registers_routes(): void
|
||||
{
|
||||
$event = new WebRoutesRegistering();
|
||||
|
||||
$boot = new \Mod\Blog\Boot();
|
||||
$boot->onWebRoutes($event);
|
||||
|
||||
$this->assertTrue(Route::has('blog.index'));
|
||||
}
|
||||
|
||||
public function test_registers_views(): void
|
||||
{
|
||||
$event = new WebRoutesRegistering();
|
||||
$boot = new \Mod\Blog\Boot();
|
||||
$boot->onWebRoutes($event);
|
||||
|
||||
$this->assertTrue(
|
||||
View::getFinder()->getHints()['blog'] ?? false
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Events
|
||||
|
||||
Enable event logging:
|
||||
|
||||
```php
|
||||
// config/logging.php
|
||||
'channels' => [
|
||||
'lifecycle' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/lifecycle.log'),
|
||||
'level' => 'debug',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
**Log Output:**
|
||||
|
||||
```
|
||||
[2026-01-26 12:00:00] Firing: WebRoutesRegistering
|
||||
[2026-01-26 12:00:00] Listener: Mod\Blog\Boot@onWebRoutes (12ms)
|
||||
[2026-01-26 12:00:00] Listener: Mod\Shop\Boot@onWebRoutes (8ms)
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Module System →](/core/modules)
|
||||
- [Actions Pattern →](/core/actions)
|
||||
- [Multi-Tenancy →](/core/tenancy)
|
||||
150
build/php/getting-started.md
Normal file
150
build/php/getting-started.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Getting Started
|
||||
|
||||
Welcome to the Core PHP Framework! This guide will help you understand what the framework is, when to use it, and how to get started.
|
||||
|
||||
## What is Core PHP?
|
||||
|
||||
Core PHP is a **modular monolith framework** for Laravel that provides:
|
||||
|
||||
- **Event-driven architecture** - Modules communicate via lifecycle events
|
||||
- **Lazy loading** - Only load what you need when you need it
|
||||
- **Multi-tenant isolation** - Built-in workspace scoping
|
||||
- **Action patterns** - Testable, reusable business logic
|
||||
- **Activity logging** - Audit trails out of the box
|
||||
|
||||
## When to Use Core PHP
|
||||
|
||||
### ✅ Good Fit
|
||||
|
||||
- **Multi-tenant SaaS applications** - Built-in workspace isolation
|
||||
- **Growing monoliths** - Need structure without microservices complexity
|
||||
- **Modular applications** - Clear module boundaries with lazy loading
|
||||
- **API-first applications** - Comprehensive API package with OpenAPI docs
|
||||
|
||||
### ❌ Not a Good Fit
|
||||
|
||||
- **Simple CRUD apps** - May be overkill for basic applications
|
||||
- **Existing large codebases** - Migration would be significant effort
|
||||
- **Need for polyglot services** - Better suited for monolithic PHP apps
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Application Bootstrap │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ LifecycleEventProvider │
|
||||
│ (fires WebRoutesRegistering, etc.) │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌───────▼────────┐
|
||||
│ ModuleRegistry │
|
||||
│ (lazy loading) │
|
||||
└───────┬─────────┘
|
||||
│
|
||||
┌───────▼────────────────┐
|
||||
│ Module Boot Classes │
|
||||
│ • Mod/Commerce/Boot.php │
|
||||
│ • Mod/Billing/Boot.php │
|
||||
│ • Mod/Analytics/Boot.php│
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
Modules declare which events they're interested in:
|
||||
|
||||
```php
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => 'onAdmin',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
The framework only instantiates modules when their events fire.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Lifecycle Events
|
||||
|
||||
Events fired during application bootstrap:
|
||||
|
||||
- `WebRoutesRegistering` - Public web routes
|
||||
- `AdminPanelBooting` - Admin panel
|
||||
- `ApiRoutesRegistering` - REST API
|
||||
- `ClientRoutesRegistering` - Authenticated client routes
|
||||
- `ConsoleBooting` - Artisan commands
|
||||
- `FrameworkBooted` - Late initialization
|
||||
|
||||
### 2. Module System
|
||||
|
||||
Modules are self-contained feature bundles:
|
||||
|
||||
```
|
||||
app/Mod/Commerce/
|
||||
├── Boot.php # Module entry point
|
||||
├── Actions/ # Business logic
|
||||
├── Models/ # Eloquent models
|
||||
├── Routes/ # Route files
|
||||
├── Views/ # Blade templates
|
||||
├── Migrations/ # Database migrations
|
||||
└── config.php # Module configuration
|
||||
```
|
||||
|
||||
### 3. Workspace Scoping
|
||||
|
||||
All tenant data is automatically scoped:
|
||||
|
||||
```php
|
||||
use Core\Mod\Tenant\Concerns\BelongsToWorkspace;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use BelongsToWorkspace;
|
||||
}
|
||||
|
||||
// Automatically filtered to current workspace
|
||||
$products = Product::all();
|
||||
```
|
||||
|
||||
### 4. Actions Pattern
|
||||
|
||||
Single-purpose business logic:
|
||||
|
||||
```php
|
||||
use Core\Actions\Action;
|
||||
|
||||
class CreateOrder
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(User $user, array $data): Order
|
||||
{
|
||||
// Business logic here
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
$order = CreateOrder::run($user, $validated);
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Installation →](./installation)
|
||||
- [Configuration →](./configuration)
|
||||
- [Quick Start →](./quick-start)
|
||||
|
||||
## Requirements
|
||||
|
||||
- **PHP** 8.2 or higher
|
||||
- **Laravel** 11 or 12
|
||||
- **Database** MySQL 8.0+, PostgreSQL 13+, or SQLite 3.35+
|
||||
- **Composer** 2.0+
|
||||
|
||||
## Support
|
||||
|
||||
- 📖 [Documentation](https://docs.example.com)
|
||||
- 💬 [GitHub Discussions](https://github.com/host-uk/core-php/discussions)
|
||||
- 🐛 [Issue Tracker](https://github.com/host-uk/core-php/issues)
|
||||
- 📧 [Email Support](mailto:support@host.uk.com)
|
||||
273
build/php/index.md
Normal file
273
build/php/index.md
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
# PHP Framework
|
||||
|
||||
The PHP framework provides the foundation for Host UK applications including the module system, lifecycle events, multi-tenancy, and shared utilities.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require host-uk/core
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Example;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('example', __DIR__.'/Views');
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Foundation
|
||||
|
||||
- **[Module System](/core/modules)** - Auto-discover and lazy-load modules based on lifecycle events
|
||||
- **[Lifecycle Events](/core/events)** - Event-driven extension points throughout the framework
|
||||
- **[Actions Pattern](/core/actions)** - Single-purpose business logic classes
|
||||
- **[Service Discovery](/core/services)** - Automatic service registration and dependency management
|
||||
|
||||
### Multi-Tenancy
|
||||
|
||||
- **[Workspaces & Namespaces](/core/tenancy)** - Workspace and namespace scoping for data isolation
|
||||
- **[Workspace Caching](/core/tenancy#workspace-caching)** - Isolated cache management per workspace
|
||||
- **[Context Resolution](/core/tenancy#context-resolution)** - Automatic workspace/namespace detection
|
||||
|
||||
### Data & Storage
|
||||
|
||||
- **[Configuration Management](/core/configuration)** - Multi-profile configuration with versioning and export/import
|
||||
- **[Activity Logging](/core/activity)** - Track changes to models with automatic workspace scoping
|
||||
- **[Seeder Discovery](/core/seeders)** - Automatic seeder discovery with dependency ordering
|
||||
- **[CDN Integration](/core/cdn)** - Unified CDN interface for BunnyCDN and Cloudflare
|
||||
|
||||
### Content & Media
|
||||
|
||||
- **[Media Processing](/core/media)** - Image optimization, responsive images, and thumbnails
|
||||
- **[Search](/core/search)** - Unified search interface across modules with analytics
|
||||
- **[SEO Tools](/core/seo)** - SEO metadata generation, sitemaps, and structured data
|
||||
|
||||
### Security
|
||||
|
||||
- **[Security Headers](/core/security)** - Configurable security headers with CSP support
|
||||
- **[Email Shield](/core/email-shield)** - Disposable email detection and validation
|
||||
- **[Action Gate](/core/action-gate)** - Permission-based action authorization
|
||||
- **[Blocklist Service](/core/security#blocklist)** - IP blocklist and rate limiting
|
||||
|
||||
### Utilities
|
||||
|
||||
- **[Input Sanitization](/core/security#sanitization)** - XSS protection and input cleaning
|
||||
- **[Encryption](/core/security#encryption)** - Additional encryption utilities (HadesEncrypt)
|
||||
- **[Translation Memory](/core/i18n)** - Translation management with fuzzy matching and ICU support
|
||||
|
||||
## Architecture
|
||||
|
||||
The Core package follows a modular monolith architecture with:
|
||||
|
||||
1. **Event-Driven Loading** - Modules are lazy-loaded based on lifecycle events
|
||||
2. **Dependency Injection** - All services are resolved through Laravel's container
|
||||
3. **Trait-Based Features** - Common functionality provided via traits (e.g., `LogsActivity`, `BelongsToWorkspace`)
|
||||
4. **Multi-Tenancy First** - Workspace scoping is built into the foundation
|
||||
|
||||
## Artisan Commands
|
||||
|
||||
```bash
|
||||
# Module Management
|
||||
php artisan make:mod Blog
|
||||
php artisan make:website Marketing
|
||||
php artisan make:plug Stripe
|
||||
|
||||
# Configuration
|
||||
php artisan config:export production
|
||||
php artisan config:import production.json
|
||||
php artisan config:version
|
||||
|
||||
# Maintenance
|
||||
php artisan activity:prune --days=90
|
||||
php artisan email-shield:prune --days=30
|
||||
php artisan cache:warm
|
||||
|
||||
# SEO
|
||||
php artisan seo:generate-sitemap
|
||||
php artisan seo:audit-canonical
|
||||
php artisan seo:test-structured-data
|
||||
|
||||
# Storage
|
||||
php artisan storage:offload --disk=public
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```php
|
||||
// config/core.php
|
||||
return [
|
||||
'module_paths' => [
|
||||
app_path('Core'),
|
||||
app_path('Mod'),
|
||||
app_path('Plug'),
|
||||
],
|
||||
|
||||
'modules' => [
|
||||
'auto_discover' => true,
|
||||
'cache_enabled' => true,
|
||||
],
|
||||
|
||||
'seeders' => [
|
||||
'auto_discover' => true,
|
||||
'paths' => [
|
||||
'Mod/*/Database/Seeders',
|
||||
'Core/*/Database/Seeders',
|
||||
],
|
||||
],
|
||||
|
||||
'activity' => [
|
||||
'enabled' => true,
|
||||
'retention_days' => 90,
|
||||
'log_ip_address' => false,
|
||||
],
|
||||
|
||||
'workspace_cache' => [
|
||||
'enabled' => true,
|
||||
'ttl' => 3600,
|
||||
'use_tags' => true,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
[View full configuration options →](/guide/configuration#core-configuration)
|
||||
|
||||
## Events
|
||||
|
||||
Core package dispatches these lifecycle events:
|
||||
|
||||
- `Core\Events\WebRoutesRegistering` - Public web routes
|
||||
- `Core\Events\AdminPanelBooting` - Admin panel initialization
|
||||
- `Core\Events\ApiRoutesRegistering` - REST API routes
|
||||
- `Core\Events\ClientRoutesRegistering` - Authenticated client routes
|
||||
- `Core\Events\ConsoleBooting` - Artisan commands
|
||||
- `Core\Events\McpToolsRegistering` - MCP tools
|
||||
- `Core\Events\FrameworkBooted` - Late-stage initialization
|
||||
|
||||
[Learn more about Lifecycle Events →](/core/events)
|
||||
|
||||
## Middleware
|
||||
|
||||
- `Core\Mod\Tenant\Middleware\RequireWorkspaceContext` - Ensure workspace is set
|
||||
- `Core\Headers\SecurityHeaders` - Apply security headers
|
||||
- `Core\Bouncer\BlocklistService` - IP blocklist
|
||||
- `Core\Bouncer\Gate\ActionGateMiddleware` - Action authorization
|
||||
|
||||
## Global Helpers
|
||||
|
||||
```php
|
||||
// Get current workspace
|
||||
$workspace = workspace();
|
||||
|
||||
// Create activity log
|
||||
activity()
|
||||
->performedOn($model)
|
||||
->log('action');
|
||||
|
||||
// Generate CDN URL
|
||||
$url = cdn_url('path/to/asset.jpg');
|
||||
|
||||
// Get CSP nonce
|
||||
$nonce = csp_nonce();
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Actions for Business Logic
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
$post = CreatePost::run($data);
|
||||
|
||||
// ❌ Bad
|
||||
$post = Post::create($data);
|
||||
event(new PostCreated($post));
|
||||
Cache::forget('posts');
|
||||
```
|
||||
|
||||
### 2. Log Activity for Audit Trail
|
||||
|
||||
```php
|
||||
class Post extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
protected array $activityLogAttributes = ['title', 'status', 'published_at'];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Workspace Scoping
|
||||
|
||||
```php
|
||||
class Post extends Model
|
||||
{
|
||||
use BelongsToWorkspace;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Leverage Module System
|
||||
|
||||
```php
|
||||
// Create focused modules with clear boundaries
|
||||
Mod/Blog/
|
||||
Mod/Commerce/
|
||||
Mod/Analytics/
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Mod\Blog\Actions\CreatePost;
|
||||
|
||||
class CreatePostTest extends TestCase
|
||||
{
|
||||
public function test_creates_post(): void
|
||||
{
|
||||
$post = CreatePost::run([
|
||||
'title' => 'Test Post',
|
||||
'content' => 'Test content',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('posts', [
|
||||
'title' => 'Test Post',
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](https://github.com/host-uk/core-php/blob/main/packages/core-php/changelog/2026/jan/features.md)
|
||||
|
||||
## License
|
||||
|
||||
EUPL-1.2
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Module System →](/core/modules)
|
||||
- [Lifecycle Events →](/core/events)
|
||||
- [Multi-Tenancy →](/core/tenancy)
|
||||
- [Configuration →](/core/configuration)
|
||||
- [Activity Logging →](/core/activity)
|
||||
283
build/php/installation.md
Normal file
283
build/php/installation.md
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
# Installation
|
||||
|
||||
This guide covers installing the Core PHP Framework in a new or existing Laravel application.
|
||||
|
||||
## Quick Start (Recommended)
|
||||
|
||||
The fastest way to get started is using the `core:new` command from any existing Core PHP installation:
|
||||
|
||||
```bash
|
||||
php artisan core:new my-project
|
||||
cd my-project
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
This scaffolds a complete project with all Core packages pre-configured.
|
||||
|
||||
### Command Options
|
||||
|
||||
```bash
|
||||
# Custom template
|
||||
php artisan core:new my-api --template=host-uk/core-api-template
|
||||
|
||||
# Specific version
|
||||
php artisan core:new my-app --branch=v1.0.0
|
||||
|
||||
# Skip automatic installation
|
||||
php artisan core:new my-app --no-install
|
||||
|
||||
# Development mode (--prefer-source)
|
||||
php artisan core:new my-app --dev
|
||||
|
||||
# Overwrite existing directory
|
||||
php artisan core:new my-app --force
|
||||
```
|
||||
|
||||
## From GitHub Template
|
||||
|
||||
You can also use the GitHub template directly:
|
||||
|
||||
1. Visit [host-uk/core-template](https://github.com/host-uk/core-template)
|
||||
2. Click "Use this template"
|
||||
3. Clone your new repository
|
||||
4. Run `composer install && php artisan core:install`
|
||||
|
||||
## Manual Installation
|
||||
|
||||
For adding Core PHP to an existing Laravel project:
|
||||
|
||||
```bash
|
||||
# Install Core PHP
|
||||
composer require host-uk/core
|
||||
|
||||
# Install optional packages
|
||||
composer require host-uk/core-admin # Admin panel
|
||||
composer require host-uk/core-api # REST API
|
||||
composer require host-uk/core-mcp # MCP tools
|
||||
```
|
||||
|
||||
## Existing Laravel Project
|
||||
|
||||
Add to an existing Laravel 11+ or 12 application:
|
||||
|
||||
```bash
|
||||
composer require host-uk/core
|
||||
```
|
||||
|
||||
The service provider will be auto-discovered.
|
||||
|
||||
## Package Installation
|
||||
|
||||
Install individual packages as needed:
|
||||
|
||||
### Core Package (Required)
|
||||
|
||||
```bash
|
||||
composer require host-uk/core
|
||||
```
|
||||
|
||||
Provides:
|
||||
- Event-driven module system
|
||||
- Actions pattern
|
||||
- Multi-tenancy
|
||||
- Activity logging
|
||||
- Seeder auto-discovery
|
||||
|
||||
### Admin Package (Optional)
|
||||
|
||||
```bash
|
||||
composer require host-uk/core-admin
|
||||
```
|
||||
|
||||
Provides:
|
||||
- Livewire admin panel
|
||||
- Global search
|
||||
- Service management UI
|
||||
- Form components
|
||||
|
||||
**Additional requirements:**
|
||||
```bash
|
||||
composer require livewire/livewire:"^3.0|^4.0"
|
||||
composer require livewire/flux:"^2.0"
|
||||
```
|
||||
|
||||
### API Package (Optional)
|
||||
|
||||
```bash
|
||||
composer require host-uk/core-api
|
||||
```
|
||||
|
||||
Provides:
|
||||
- OpenAPI/Swagger documentation
|
||||
- Rate limiting
|
||||
- Webhook signing
|
||||
- Secure API keys
|
||||
|
||||
### MCP Package (Optional)
|
||||
|
||||
```bash
|
||||
composer require host-uk/core-mcp
|
||||
```
|
||||
|
||||
Provides:
|
||||
- Model Context Protocol tools
|
||||
- Tool analytics
|
||||
- SQL query validation
|
||||
- MCP playground UI
|
||||
|
||||
## Publishing Configuration
|
||||
|
||||
Publish configuration files:
|
||||
|
||||
```bash
|
||||
# Publish core config
|
||||
php artisan vendor:publish --tag=core-config
|
||||
|
||||
# Publish API config (if installed)
|
||||
php artisan vendor:publish --tag=api-config
|
||||
|
||||
# Publish MCP config (if installed)
|
||||
php artisan vendor:publish --tag=mcp-config
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
|
||||
Run migrations:
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
This creates tables for:
|
||||
- Workspaces and users
|
||||
- API keys (if core-api installed)
|
||||
- MCP analytics (if core-mcp installed)
|
||||
- Activity logs (if spatie/laravel-activitylog installed)
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
### Activity Logging
|
||||
|
||||
For activity logging features:
|
||||
|
||||
```bash
|
||||
composer require spatie/laravel-activitylog:"^4.8"
|
||||
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
For feature flag support:
|
||||
|
||||
```bash
|
||||
composer require laravel/pennant:"^1.0"
|
||||
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
## Verify Installation
|
||||
|
||||
Check that everything is installed correctly:
|
||||
|
||||
```bash
|
||||
# Check installed packages
|
||||
composer show | grep host-uk
|
||||
|
||||
# List available artisan commands
|
||||
php artisan list make
|
||||
|
||||
# Should see:
|
||||
# make:mod Create a new module
|
||||
# make:website Create a new website module
|
||||
# make:plug Create a new plugin
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Add to your `.env`:
|
||||
|
||||
```env
|
||||
# Core Configuration
|
||||
CORE_MODULE_DISCOVERY=true
|
||||
CORE_STRICT_WORKSPACE_MODE=true
|
||||
|
||||
# API Configuration (if using core-api)
|
||||
API_DOCS_ENABLED=true
|
||||
API_DOCS_REQUIRE_AUTH=false
|
||||
API_RATE_LIMIT_DEFAULT=60
|
||||
|
||||
# MCP Configuration (if using core-mcp)
|
||||
MCP_ANALYTICS_ENABLED=true
|
||||
MCP_QUOTA_ENABLED=true
|
||||
MCP_DATABASE_CONNECTION=readonly
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
After installation, your project structure will look like:
|
||||
|
||||
```
|
||||
your-app/
|
||||
├── app/
|
||||
│ ├── Core/ # Core modules (framework-level)
|
||||
│ ├── Mod/ # Feature modules (your code)
|
||||
│ ├── Website/ # Website modules
|
||||
│ └── Plug/ # Plugins
|
||||
├── config/
|
||||
│ ├── core.php # Core configuration
|
||||
│ ├── api.php # API configuration (optional)
|
||||
│ └── mcp.php # MCP configuration (optional)
|
||||
├── packages/ # Local package development (optional)
|
||||
└── vendor/
|
||||
└── host-uk/ # Installed packages
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Configuration →](./configuration)
|
||||
- [Quick Start →](./quick-start)
|
||||
- [Create Your First Module →](./quick-start#creating-a-module)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service Provider Not Discovered
|
||||
|
||||
If the service provider isn't auto-discovered:
|
||||
|
||||
```bash
|
||||
composer dump-autoload
|
||||
php artisan package:discover --ansi
|
||||
```
|
||||
|
||||
### Migration Errors
|
||||
|
||||
If migrations fail:
|
||||
|
||||
```bash
|
||||
# Check database connection
|
||||
php artisan db:show
|
||||
|
||||
# Run migrations with verbose output
|
||||
php artisan migrate --verbose
|
||||
```
|
||||
|
||||
### Module Discovery Issues
|
||||
|
||||
If modules aren't being discovered:
|
||||
|
||||
```bash
|
||||
# Clear application cache
|
||||
php artisan optimize:clear
|
||||
|
||||
# Verify module paths in config/core.php
|
||||
php artisan config:show core.module_paths
|
||||
```
|
||||
|
||||
## Minimum Requirements
|
||||
|
||||
- PHP 8.2+
|
||||
- Laravel 11.0+ or 12.0+
|
||||
- MySQL 8.0+ / PostgreSQL 13+ / SQLite 3.35+
|
||||
- Composer 2.0+
|
||||
- 128MB PHP memory limit (256MB recommended)
|
||||
506
build/php/media.md
Normal file
506
build/php/media.md
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
# Media Processing
|
||||
|
||||
Powerful media processing with image optimization, responsive images, lazy thumbnails, and CDN integration.
|
||||
|
||||
## Image Optimization
|
||||
|
||||
### Automatic Optimization
|
||||
|
||||
Images are automatically optimized on upload:
|
||||
|
||||
```php
|
||||
use Core\Media\Image\ImageOptimizer;
|
||||
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
|
||||
// Optimize image
|
||||
$optimizer->optimize($path);
|
||||
|
||||
// Returns optimized path with reduced file size
|
||||
```
|
||||
|
||||
**Optimization Features:**
|
||||
- Strip EXIF data (privacy)
|
||||
- Lossless compression
|
||||
- Format conversion (WebP/AVIF support)
|
||||
- Quality adjustment
|
||||
- Dimension constraints
|
||||
|
||||
### Configuration
|
||||
|
||||
```php
|
||||
// config/media.php
|
||||
return [
|
||||
'optimization' => [
|
||||
'enabled' => true,
|
||||
'quality' => 85,
|
||||
'max_width' => 2560,
|
||||
'max_height' => 2560,
|
||||
'strip_exif' => true,
|
||||
'convert_to_webp' => true,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Manual Optimization
|
||||
|
||||
```php
|
||||
use Core\Media\Image\ImageOptimization;
|
||||
|
||||
$optimization = app(ImageOptimization::class);
|
||||
|
||||
// Optimize with custom quality
|
||||
$optimization->optimize($path, quality: 90);
|
||||
|
||||
// Optimize and resize
|
||||
$optimization->optimize($path, maxWidth: 1920, maxHeight: 1080);
|
||||
|
||||
// Get optimization stats
|
||||
$stats = $optimization->getStats($path);
|
||||
// ['original_size' => 2500000, 'optimized_size' => 890000, 'savings' => 64]
|
||||
```
|
||||
|
||||
## Responsive Images
|
||||
|
||||
### Generating Responsive Images
|
||||
|
||||
```php
|
||||
use Core\Media\Support\ImageResizer;
|
||||
|
||||
$resizer = app(ImageResizer::class);
|
||||
|
||||
// Generate multiple sizes
|
||||
$sizes = $resizer->resize($originalPath, [
|
||||
'thumbnail' => [150, 150],
|
||||
'small' => [320, 240],
|
||||
'medium' => [768, 576],
|
||||
'large' => [1920, 1440],
|
||||
]);
|
||||
|
||||
// Returns:
|
||||
[
|
||||
'thumbnail' => '/storage/images/photo-150x150.jpg',
|
||||
'small' => '/storage/images/photo-320x240.jpg',
|
||||
'medium' => '/storage/images/photo-768x576.jpg',
|
||||
'large' => '/storage/images/photo-1920x1440.jpg',
|
||||
]
|
||||
```
|
||||
|
||||
### Responsive Image Tag
|
||||
|
||||
```blade
|
||||
<picture>
|
||||
<source
|
||||
srcset="{{ cdn($image->large) }} 1920w,
|
||||
{{ cdn($image->medium) }} 768w,
|
||||
{{ cdn($image->small) }} 320w"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
>
|
||||
<img
|
||||
src="{{ cdn($image->medium) }}"
|
||||
alt="{{ $image->alt }}"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
```
|
||||
|
||||
### Modern Format Support
|
||||
|
||||
```php
|
||||
use Core\Media\Image\ModernFormatSupport;
|
||||
|
||||
$formats = app(ModernFormatSupport::class);
|
||||
|
||||
// Check browser support
|
||||
if ($formats->supportsWebP(request())) {
|
||||
return cdn($image->webp);
|
||||
}
|
||||
|
||||
if ($formats->supportsAVIF(request())) {
|
||||
return cdn($image->avif);
|
||||
}
|
||||
|
||||
return cdn($image->jpg);
|
||||
```
|
||||
|
||||
**Blade Component:**
|
||||
|
||||
```blade
|
||||
<x-responsive-image
|
||||
:image="$post->featured_image"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
loading="lazy"
|
||||
/>
|
||||
```
|
||||
|
||||
## Lazy Thumbnails
|
||||
|
||||
Generate thumbnails on-demand:
|
||||
|
||||
### Configuration
|
||||
|
||||
```php
|
||||
// config/media.php
|
||||
return [
|
||||
'lazy_thumbnails' => [
|
||||
'enabled' => true,
|
||||
'cache_ttl' => 86400, // 24 hours
|
||||
'allowed_sizes' => [
|
||||
'thumbnail' => [150, 150],
|
||||
'small' => [320, 240],
|
||||
'medium' => [768, 576],
|
||||
'large' => [1920, 1440],
|
||||
],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Generating Thumbnails
|
||||
|
||||
```php
|
||||
use Core\Media\Thumbnail\LazyThumbnail;
|
||||
|
||||
// Generate thumbnail URL (not created until requested)
|
||||
$url = lazy_thumbnail($originalPath, 'medium');
|
||||
// Returns: /thumbnail/abc123/medium/photo.jpg
|
||||
|
||||
// Generate with custom dimensions
|
||||
$url = lazy_thumbnail($originalPath, [width: 500, height: 300]);
|
||||
```
|
||||
|
||||
### Thumbnail Controller
|
||||
|
||||
Thumbnails are generated on first request:
|
||||
|
||||
```
|
||||
GET /thumbnail/{hash}/{size}/{filename}
|
||||
```
|
||||
|
||||
**Process:**
|
||||
1. Check if thumbnail exists in cache
|
||||
2. If not, generate from original
|
||||
3. Store in cache/CDN
|
||||
4. Serve to client
|
||||
|
||||
**Benefits:**
|
||||
- No upfront processing
|
||||
- Storage efficient
|
||||
- CDN-friendly
|
||||
- Automatic cleanup
|
||||
|
||||
## Media Conversions
|
||||
|
||||
Define custom media conversions:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Media;
|
||||
|
||||
use Core\Media\Abstracts\MediaConversion;
|
||||
|
||||
class PostThumbnailConversion extends MediaConversion
|
||||
{
|
||||
public function name(): string
|
||||
{
|
||||
return 'post-thumbnail';
|
||||
}
|
||||
|
||||
public function apply(string $path): string
|
||||
{
|
||||
return $this->resize($path, 400, 300)
|
||||
->optimize(quality: 85)
|
||||
->sharpen()
|
||||
->save();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Register Conversion:**
|
||||
|
||||
```php
|
||||
use Core\Events\FrameworkBooted;
|
||||
use Core\Media\Conversions\MediaImageResizerConversion;
|
||||
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
MediaImageResizerConversion::register(
|
||||
new PostThumbnailConversion()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Apply Conversion:**
|
||||
|
||||
```php
|
||||
use Core\Media\Jobs\ProcessMediaConversion;
|
||||
|
||||
// Queue conversion
|
||||
ProcessMediaConversion::dispatch($media, 'post-thumbnail');
|
||||
|
||||
// Synchronous conversion
|
||||
$converted = $media->convert('post-thumbnail');
|
||||
```
|
||||
|
||||
## EXIF Data
|
||||
|
||||
### Stripping EXIF
|
||||
|
||||
Remove privacy-sensitive metadata:
|
||||
|
||||
```php
|
||||
use Core\Media\Image\ExifStripper;
|
||||
|
||||
$stripper = app(ExifStripper::class);
|
||||
|
||||
// Strip all EXIF data
|
||||
$stripper->strip($imagePath);
|
||||
|
||||
// Strip specific tags
|
||||
$stripper->strip($imagePath, preserve: [
|
||||
'orientation', // Keep orientation
|
||||
'copyright', // Keep copyright
|
||||
]);
|
||||
```
|
||||
|
||||
**Auto-strip on Upload:**
|
||||
|
||||
```php
|
||||
// config/media.php
|
||||
return [
|
||||
'optimization' => [
|
||||
'strip_exif' => true, // Default: strip everything
|
||||
'preserve_exif' => ['orientation'], // Keep these tags
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Reading EXIF
|
||||
|
||||
```php
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
$manager = app(ImageManager::class);
|
||||
|
||||
$image = $manager->read($path);
|
||||
$exif = $image->exif();
|
||||
|
||||
$camera = $exif->get('Model'); // Camera model
|
||||
$date = $exif->get('DateTimeOriginal'); // Photo date
|
||||
$gps = $exif->get('GPSLatitude'); // GPS coordinates (privacy risk!)
|
||||
```
|
||||
|
||||
## CDN Integration
|
||||
|
||||
### Uploading to CDN
|
||||
|
||||
```php
|
||||
use Core\Cdn\Services\BunnyStorageService;
|
||||
|
||||
$cdn = app(BunnyStorageService::class);
|
||||
|
||||
// Upload file
|
||||
$cdnPath = $cdn->upload($localPath, 'images/photo.jpg');
|
||||
|
||||
// Upload with public URL
|
||||
$url = $cdn->uploadAndGetUrl($localPath, 'images/photo.jpg');
|
||||
```
|
||||
|
||||
### CDN Helper
|
||||
|
||||
```blade
|
||||
{{-- Blade template --}}
|
||||
<img src="{{ cdn('images/photo.jpg') }}" alt="Photo">
|
||||
|
||||
{{-- With transformation --}}
|
||||
<img src="{{ cdn('images/photo.jpg', ['width' => 800, 'quality' => 85]) }}" alt="Photo">
|
||||
```
|
||||
|
||||
### Purging CDN Cache
|
||||
|
||||
```php
|
||||
use Core\Cdn\Services\FluxCdnService;
|
||||
|
||||
$cdn = app(FluxCdnService::class);
|
||||
|
||||
// Purge single file
|
||||
$cdn->purge('/images/photo.jpg');
|
||||
|
||||
// Purge multiple files
|
||||
$cdn->purge([
|
||||
'/images/photo.jpg',
|
||||
'/images/thumbnail.jpg',
|
||||
]);
|
||||
|
||||
// Purge entire directory
|
||||
$cdn->purge('/images/*');
|
||||
```
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
Track conversion progress:
|
||||
|
||||
```php
|
||||
use Core\Media\Events\ConversionProgress;
|
||||
|
||||
// Listen for progress
|
||||
Event::listen(ConversionProgress::class, function ($event) {
|
||||
echo "Processing: {$event->percentage}%\n";
|
||||
echo "Step: {$event->currentStep}/{$event->totalSteps}\n";
|
||||
});
|
||||
```
|
||||
|
||||
**With Livewire:**
|
||||
|
||||
```php
|
||||
class MediaUploader extends Component
|
||||
{
|
||||
public $progress = 0;
|
||||
|
||||
protected $listeners = ['conversionProgress' => 'updateProgress'];
|
||||
|
||||
public function updateProgress($percentage)
|
||||
{
|
||||
$this->progress = $percentage;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.media-uploader');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```blade
|
||||
<div>
|
||||
@if($progress > 0)
|
||||
<div class="progress-bar">
|
||||
<div style="width: {{ $progress }}%"></div>
|
||||
</div>
|
||||
<p>Processing: {{ $progress }}%</p>
|
||||
@endif
|
||||
</div>
|
||||
```
|
||||
|
||||
## Queued Processing
|
||||
|
||||
Process media in background:
|
||||
|
||||
```php
|
||||
use Core\Media\Jobs\GenerateThumbnail;
|
||||
use Core\Media\Jobs\ProcessMediaConversion;
|
||||
|
||||
// Queue thumbnail generation
|
||||
GenerateThumbnail::dispatch($media, 'large');
|
||||
|
||||
// Queue conversion
|
||||
ProcessMediaConversion::dispatch($media, 'optimized');
|
||||
|
||||
// Chain jobs
|
||||
GenerateThumbnail::dispatch($media, 'large')
|
||||
->chain([
|
||||
new ProcessMediaConversion($media, 'watermark'),
|
||||
new ProcessMediaConversion($media, 'optimize'),
|
||||
]);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Optimize on Upload
|
||||
|
||||
```php
|
||||
// ✅ Good - optimize immediately
|
||||
public function store(Request $request)
|
||||
{
|
||||
$path = $request->file('image')->store('images');
|
||||
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
$optimizer->optimize(storage_path("app/{$path}"));
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
// ❌ Bad - serve unoptimized images
|
||||
public function store(Request $request)
|
||||
{
|
||||
return $request->file('image')->store('images');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Lazy Thumbnails
|
||||
|
||||
```php
|
||||
// ✅ Good - generate on-demand
|
||||
<img src="{{ lazy_thumbnail($image->path, 'medium') }}">
|
||||
|
||||
// ❌ Bad - generate all sizes upfront
|
||||
$resizer->resize($path, [
|
||||
'thumbnail' => [150, 150],
|
||||
'small' => [320, 240],
|
||||
'medium' => [768, 576],
|
||||
'large' => [1920, 1440],
|
||||
'xlarge' => [2560, 1920],
|
||||
]); // Slow upload, wasted storage
|
||||
```
|
||||
|
||||
### 3. Strip EXIF Data
|
||||
|
||||
```php
|
||||
// ✅ Good - protect privacy
|
||||
$stripper->strip($imagePath);
|
||||
|
||||
// ❌ Bad - leak GPS coordinates, camera info
|
||||
// (no stripping)
|
||||
```
|
||||
|
||||
### 4. Use CDN for Assets
|
||||
|
||||
```php
|
||||
// ✅ Good - CDN delivery
|
||||
<img src="{{ cdn($image->path) }}">
|
||||
|
||||
// ❌ Bad - serve from origin
|
||||
<img src="{{ Storage::url($image->path) }}">
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```php
|
||||
use Tests\TestCase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Core\Media\Image\ImageOptimizer;
|
||||
|
||||
class MediaTest extends TestCase
|
||||
{
|
||||
public function test_optimizes_uploaded_image(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->image('photo.jpg', 2000, 2000);
|
||||
|
||||
$path = $file->store('test');
|
||||
$fullPath = storage_path("app/{$path}");
|
||||
|
||||
$originalSize = filesize($fullPath);
|
||||
|
||||
$optimizer = app(ImageOptimizer::class);
|
||||
$optimizer->optimize($fullPath);
|
||||
|
||||
$optimizedSize = filesize($fullPath);
|
||||
|
||||
$this->assertLessThan($originalSize, $optimizedSize);
|
||||
}
|
||||
|
||||
public function test_generates_lazy_thumbnail(): void
|
||||
{
|
||||
$path = UploadedFile::fake()->image('photo.jpg')->store('test');
|
||||
|
||||
$url = lazy_thumbnail($path, 'medium');
|
||||
|
||||
$this->assertStringContainsString('/thumbnail/', $url);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [CDN Integration →](/core/cdn)
|
||||
- [Configuration →](/core/configuration)
|
||||
488
build/php/modules.md
Normal file
488
build/php/modules.md
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
# Module System
|
||||
|
||||
The module system provides automatic discovery and lazy loading of modules based on lifecycle events. Modules are self-contained units of functionality that can hook into the framework at specific points.
|
||||
|
||||
## Overview
|
||||
|
||||
Traditional Laravel applications use service providers which are all loaded on every request. The Core module system:
|
||||
|
||||
- **Auto-discovers** modules by scanning directories
|
||||
- **Lazy-loads** modules only when their events fire
|
||||
- **Caches** module registry for performance
|
||||
- **Supports** multiple module types (Mod, Plug, Website)
|
||||
|
||||
## Creating a Module
|
||||
|
||||
### Using Artisan
|
||||
|
||||
```bash
|
||||
# Create a standard module
|
||||
php artisan make:mod Blog
|
||||
|
||||
# Create a website module
|
||||
php artisan make:website Marketing
|
||||
|
||||
# Create a plugin module
|
||||
php artisan make:plug Stripe
|
||||
```
|
||||
|
||||
### Manual Creation
|
||||
|
||||
Create a `Boot.php` file in your module directory:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog;
|
||||
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
use Core\Events\AdminPanelBooting;
|
||||
use Core\Events\ConsoleBooting;
|
||||
|
||||
class Boot
|
||||
{
|
||||
/**
|
||||
* Events this module listens to
|
||||
*/
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => 'onAdminPanel',
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register public web routes
|
||||
*/
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin panel routes and menus
|
||||
*/
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
$event->menu('blog', [
|
||||
'label' => 'Blog',
|
||||
'icon' => 'newspaper',
|
||||
'route' => 'admin.blog.index',
|
||||
'order' => 20,
|
||||
]);
|
||||
|
||||
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register console commands
|
||||
*/
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
$event->commands([
|
||||
Commands\PublishPostsCommand::class,
|
||||
Commands\ImportPostsCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
Mod/Blog/
|
||||
├── Boot.php # Module bootstrap
|
||||
├── Actions/ # Business logic
|
||||
│ ├── CreatePost.php
|
||||
│ ├── UpdatePost.php
|
||||
│ └── DeletePost.php
|
||||
├── Controllers/
|
||||
│ ├── Web/
|
||||
│ │ └── PostController.php
|
||||
│ └── Admin/
|
||||
│ └── PostController.php
|
||||
├── Models/
|
||||
│ ├── Post.php
|
||||
│ └── Category.php
|
||||
├── Routes/
|
||||
│ ├── web.php
|
||||
│ ├── admin.php
|
||||
│ └── api.php
|
||||
├── Views/
|
||||
│ ├── web/
|
||||
│ └── admin/
|
||||
├── Database/
|
||||
│ ├── Migrations/
|
||||
│ ├── Factories/
|
||||
│ └── Seeders/
|
||||
├── Tests/
|
||||
│ ├── Feature/
|
||||
│ └── Unit/
|
||||
└── Lang/
|
||||
└── en_GB/
|
||||
```
|
||||
|
||||
## Lifecycle Events
|
||||
|
||||
Modules can hook into these lifecycle events:
|
||||
|
||||
### WebRoutesRegistering
|
||||
|
||||
Register public-facing web routes:
|
||||
|
||||
```php
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
// Register views
|
||||
$event->views('blog', __DIR__.'/Views');
|
||||
|
||||
// Register translations
|
||||
$event->lang('blog', __DIR__.'/Lang');
|
||||
|
||||
// Register routes
|
||||
$event->routes(function () {
|
||||
Route::get('/blog', [PostController::class, 'index']);
|
||||
Route::get('/blog/{slug}', [PostController::class, 'show']);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### AdminPanelBooting
|
||||
|
||||
Register admin panel routes, menus, and widgets:
|
||||
|
||||
```php
|
||||
public function onAdminPanel(AdminPanelBooting $event): void
|
||||
{
|
||||
// Register admin menu
|
||||
$event->menu('blog', [
|
||||
'label' => 'Blog',
|
||||
'icon' => 'newspaper',
|
||||
'route' => 'admin.blog.index',
|
||||
'order' => 20,
|
||||
'children' => [
|
||||
['label' => 'Posts', 'route' => 'admin.blog.posts'],
|
||||
['label' => 'Categories', 'route' => 'admin.blog.categories'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Register routes
|
||||
$event->routes(fn () => require __DIR__.'/Routes/admin.php');
|
||||
}
|
||||
```
|
||||
|
||||
### ApiRoutesRegistering
|
||||
|
||||
Register REST API endpoints:
|
||||
|
||||
```php
|
||||
public function onApiRoutes(ApiRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(function () {
|
||||
Route::get('/posts', [Api\PostController::class, 'index']);
|
||||
Route::post('/posts', [Api\PostController::class, 'store']);
|
||||
Route::get('/posts/{id}', [Api\PostController::class, 'show']);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### ClientRoutesRegistering
|
||||
|
||||
Register authenticated client routes:
|
||||
|
||||
```php
|
||||
public function onClientRoutes(ClientRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(function () {
|
||||
Route::get('/dashboard/posts', [Client\PostController::class, 'index']);
|
||||
Route::post('/dashboard/posts', [Client\PostController::class, 'store']);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### ConsoleBooting
|
||||
|
||||
Register Artisan commands:
|
||||
|
||||
```php
|
||||
public function onConsole(ConsoleBooting $event): void
|
||||
{
|
||||
$event->commands([
|
||||
Commands\PublishPostsCommand::class,
|
||||
Commands\GenerateSitemapCommand::class,
|
||||
]);
|
||||
|
||||
$event->schedule(function (Schedule $schedule) {
|
||||
$schedule->command('blog:publish-scheduled')
|
||||
->everyFiveMinutes();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### McpToolsRegistering
|
||||
|
||||
Register MCP (Model Context Protocol) tools:
|
||||
|
||||
```php
|
||||
public function onMcpTools(McpToolsRegistering $event): void
|
||||
{
|
||||
$event->tool('blog:create-post', Tools\CreatePostTool::class);
|
||||
$event->tool('blog:list-posts', Tools\ListPostsTool::class);
|
||||
}
|
||||
```
|
||||
|
||||
### FrameworkBooted
|
||||
|
||||
Late-stage initialization after all modules loaded:
|
||||
|
||||
```php
|
||||
public function onFrameworkBooted(FrameworkBooted $event): void
|
||||
{
|
||||
// Register macros, observers, policies, etc.
|
||||
Post::observe(PostObserver::class);
|
||||
|
||||
Builder::macro('published', function () {
|
||||
return $this->where('status', 'published')
|
||||
->where('published_at', '<=', now());
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Module Discovery
|
||||
|
||||
The framework automatically scans these directories:
|
||||
|
||||
```php
|
||||
// config/core.php
|
||||
'module_paths' => [
|
||||
app_path('Core'), // Core modules
|
||||
app_path('Mod'), // Standard modules
|
||||
app_path('Website'), // Website modules
|
||||
app_path('Plug'), // Plugin modules
|
||||
],
|
||||
```
|
||||
|
||||
### Custom Namespaces
|
||||
|
||||
Map custom paths to namespaces:
|
||||
|
||||
```php
|
||||
use Core\Module\ModuleScanner;
|
||||
|
||||
$scanner = app(ModuleScanner::class);
|
||||
$scanner->setNamespaceMap([
|
||||
'/Extensions' => 'Extensions\\',
|
||||
'/Custom' => 'Custom\\Modules\\',
|
||||
]);
|
||||
```
|
||||
|
||||
## Lazy Loading
|
||||
|
||||
Modules are only instantiated when their events fire:
|
||||
|
||||
1. **Scan Phase** - `ModuleScanner` finds all `Boot.php` files
|
||||
2. **Registry Phase** - `ModuleRegistry` wires lazy listeners
|
||||
3. **Event Phase** - Event fires, `LazyModuleListener` instantiates module
|
||||
4. **Execution Phase** - Module method is called
|
||||
|
||||
**Performance Benefits:**
|
||||
- Modules not used in CLI don't load in CLI
|
||||
- Admin modules don't load on public requests
|
||||
- API modules don't load on web requests
|
||||
|
||||
## Module Registry
|
||||
|
||||
View registered modules and their listeners:
|
||||
|
||||
```php
|
||||
use Core\Module\ModuleRegistry;
|
||||
|
||||
$registry = app(ModuleRegistry::class);
|
||||
|
||||
// Get all registered modules
|
||||
$modules = $registry->all();
|
||||
|
||||
// Get modules for specific event
|
||||
$webModules = $registry->forEvent(WebRoutesRegistering::class);
|
||||
```
|
||||
|
||||
## Module Cache
|
||||
|
||||
Module discovery is cached for performance:
|
||||
|
||||
```bash
|
||||
# Clear module cache
|
||||
php artisan cache:clear
|
||||
|
||||
# Or specifically
|
||||
php artisan optimize:clear
|
||||
```
|
||||
|
||||
**Cache Location:** `bootstrap/cache/modules.php`
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
Modules can declare dependencies using service discovery:
|
||||
|
||||
```php
|
||||
use Core\Service\Contracts\ServiceDefinition;
|
||||
use Core\Service\Contracts\ServiceDependency;
|
||||
|
||||
class Boot implements ServiceDefinition
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
public function getServiceName(): string
|
||||
{
|
||||
return 'blog';
|
||||
}
|
||||
|
||||
public function getServiceVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [
|
||||
new ServiceDependency('media', '>=1.0'),
|
||||
new ServiceDependency('cdn', '>=2.0'),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Modules
|
||||
|
||||
### Feature Tests
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Mod\Blog\Actions\CreatePost;
|
||||
|
||||
class PostCreationTest extends TestCase
|
||||
{
|
||||
public function test_creates_post(): void
|
||||
{
|
||||
$post = CreatePost::run([
|
||||
'title' => 'Test Post',
|
||||
'content' => 'Content here',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('posts', [
|
||||
'title' => 'Test Post',
|
||||
]);
|
||||
|
||||
$this->get("/blog/{$post->slug}")
|
||||
->assertOk()
|
||||
->assertSee('Test Post');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Tests\Unit;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Mod\Blog\Boot;
|
||||
use Core\Events\WebRoutesRegistering;
|
||||
|
||||
class BootTest extends TestCase
|
||||
{
|
||||
public function test_registers_web_routes(): void
|
||||
{
|
||||
$event = new WebRoutesRegistering();
|
||||
$boot = new Boot();
|
||||
|
||||
$boot->onWebRoutes($event);
|
||||
|
||||
$this->assertTrue($event->hasRoutes());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Keep Modules Focused
|
||||
|
||||
```php
|
||||
// ✅ Good - focused modules
|
||||
Mod/Blog/
|
||||
Mod/Comments/
|
||||
Mod/Analytics/
|
||||
|
||||
// ❌ Bad - monolithic module
|
||||
Mod/Everything/
|
||||
```
|
||||
|
||||
### 2. Use Proper Namespacing
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
namespace Mod\Blog\Controllers\Web;
|
||||
|
||||
// ❌ Bad
|
||||
namespace App\Http\Controllers;
|
||||
```
|
||||
|
||||
### 3. Register Dependencies
|
||||
|
||||
```php
|
||||
// ✅ Good - declare dependencies
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [
|
||||
new ServiceDependency('media', '>=1.0'),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Only Hook Necessary Events
|
||||
|
||||
```php
|
||||
// ✅ Good - only web routes
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
// ❌ Bad - hooks everything
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => 'onAdminPanel',
|
||||
ApiRoutesRegistering::class => 'onApiRoutes',
|
||||
// ... (when you don't need them all)
|
||||
];
|
||||
```
|
||||
|
||||
### 5. Use Actions for Business Logic
|
||||
|
||||
```php
|
||||
// ✅ Good
|
||||
$post = CreatePost::run($data);
|
||||
|
||||
// ❌ Bad - logic in controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$post = Post::create($request->all());
|
||||
event(new PostCreated($post));
|
||||
Cache::forget('posts');
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Lifecycle Events →](/core/events)
|
||||
- [Actions Pattern →](/core/actions)
|
||||
- [Service Discovery →](/core/services)
|
||||
- [Architecture Overview →](/architecture/module-system)
|
||||
906
build/php/namespaces.md
Normal file
906
build/php/namespaces.md
Normal file
|
|
@ -0,0 +1,906 @@
|
|||
# Namespaces & Entitlements
|
||||
|
||||
Core PHP Framework provides a sophisticated namespace and entitlements system for flexible multi-tenant SaaS applications. Namespaces provide universal tenant boundaries, while entitlements control feature access and usage limits.
|
||||
|
||||
## Overview
|
||||
|
||||
### The Problem
|
||||
|
||||
Traditional multi-tenant systems force a choice:
|
||||
|
||||
**Option A: User Ownership**
|
||||
- Individual users own resources
|
||||
- No team collaboration
|
||||
- Billing per user
|
||||
|
||||
**Option B: Workspace Ownership**
|
||||
- Teams own resources via workspaces
|
||||
- Can't have personal resources
|
||||
- Billing per workspace
|
||||
|
||||
Both approaches are too rigid for modern SaaS:
|
||||
- **Agencies** need separate namespaces per client
|
||||
- **Freelancers** want personal AND client resources
|
||||
- **White-label operators** need brand isolation
|
||||
- **Enterprise teams** need department-level isolation
|
||||
|
||||
### The Solution: Namespaces
|
||||
|
||||
Namespaces provide a **polymorphic ownership boundary** where resources belong to a namespace, and namespaces can be owned by either Users or Workspaces.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ User ────┬──→ Namespace (Personal) ──→ Resources │
|
||||
│ │ │
|
||||
│ └──→ Workspace ──→ Namespace (Client A) ──→ Res │
|
||||
│ └──→ Namespace (Client B) ──→ Res │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Users can have personal namespaces
|
||||
- Workspaces can have multiple namespaces (one per client)
|
||||
- Clean billing boundaries
|
||||
- Complete resource isolation
|
||||
- Flexible permission models
|
||||
|
||||
## Namespace Model
|
||||
|
||||
### Structure
|
||||
|
||||
```php
|
||||
Namespace {
|
||||
id: int
|
||||
uuid: string // Public identifier
|
||||
name: string // Display name
|
||||
slug: string // URL-safe identifier
|
||||
description: ?string
|
||||
icon: ?string
|
||||
color: ?string
|
||||
owner_type: string // User::class or Workspace::class
|
||||
owner_id: int
|
||||
workspace_id: ?int // Billing context (optional)
|
||||
settings: ?json
|
||||
is_default: bool // User's default namespace
|
||||
is_active: bool
|
||||
sort_order: int
|
||||
}
|
||||
```
|
||||
|
||||
### Ownership Patterns
|
||||
|
||||
#### Personal Namespace (User-Owned)
|
||||
|
||||
Individual user owns namespace for personal resources:
|
||||
|
||||
```php
|
||||
$namespace = Namespace_::create([
|
||||
'name' => 'Personal',
|
||||
'owner_type' => User::class,
|
||||
'owner_id' => $user->id,
|
||||
'workspace_id' => $user->defaultHostWorkspace()->id, // For billing
|
||||
'is_default' => true,
|
||||
]);
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Personal projects
|
||||
- Individual freelancer work
|
||||
- Testing/development environments
|
||||
|
||||
#### Agency Namespace (Workspace-Owned)
|
||||
|
||||
Workspace owns namespace for client/project isolation:
|
||||
|
||||
```php
|
||||
$namespace = Namespace_::create([
|
||||
'name' => 'Client: Acme Corp',
|
||||
'slug' => 'acme-corp',
|
||||
'owner_type' => Workspace::class,
|
||||
'owner_id' => $workspace->id,
|
||||
'workspace_id' => $workspace->id, // Same workspace for billing
|
||||
]);
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Agency client projects
|
||||
- White-label deployments
|
||||
- Department/team isolation
|
||||
|
||||
#### White-Label Namespace
|
||||
|
||||
SaaS operator creates namespaces for customers:
|
||||
|
||||
```php
|
||||
$namespace = Namespace_::create([
|
||||
'name' => 'Customer Instance',
|
||||
'owner_type' => User::class, // Customer user owns it
|
||||
'owner_id' => $customerUser->id,
|
||||
'workspace_id' => $operatorWorkspace->id, // Operator billed
|
||||
]);
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- White-label SaaS
|
||||
- Reseller programs
|
||||
- Managed services
|
||||
|
||||
## Using Namespaces
|
||||
|
||||
### Model Setup
|
||||
|
||||
Add namespace scoping to models:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Core\Mod\Tenant\Concerns\BelongsToNamespace;
|
||||
|
||||
class Page extends Model
|
||||
{
|
||||
use BelongsToNamespace;
|
||||
|
||||
protected $fillable = ['title', 'content', 'slug'];
|
||||
}
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
|
||||
```php
|
||||
Schema::create('pages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('namespace_id')
|
||||
->constrained('namespaces')
|
||||
->cascadeOnDelete();
|
||||
$table->string('title');
|
||||
$table->text('content');
|
||||
$table->string('slug');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['namespace_id', 'created_at']);
|
||||
});
|
||||
```
|
||||
|
||||
### Automatic Scoping
|
||||
|
||||
The `BelongsToNamespace` trait automatically handles scoping:
|
||||
|
||||
```php
|
||||
// Queries automatically scoped to current namespace
|
||||
$pages = Page::ownedByCurrentNamespace()->get();
|
||||
|
||||
// Create automatically assigns namespace_id
|
||||
$page = Page::create([
|
||||
'title' => 'Example Page',
|
||||
'content' => 'Content...',
|
||||
// namespace_id added automatically
|
||||
]);
|
||||
|
||||
// Can't access pages from other namespaces
|
||||
$page = Page::find(999); // null if belongs to different namespace
|
||||
```
|
||||
|
||||
### Namespace Context
|
||||
|
||||
#### Middleware Resolution
|
||||
|
||||
```php
|
||||
// routes/web.php
|
||||
Route::middleware(['auth', 'namespace'])
|
||||
->group(function () {
|
||||
Route::get('/pages', [PageController::class, 'index']);
|
||||
});
|
||||
```
|
||||
|
||||
The `ResolveNamespace` middleware sets current namespace from:
|
||||
1. Query parameter: `?namespace=uuid`
|
||||
2. Request header: `X-Namespace: uuid`
|
||||
3. Session: `current_namespace_uuid`
|
||||
4. User's default namespace
|
||||
|
||||
#### Manual Context
|
||||
|
||||
```php
|
||||
use Core\Mod\Tenant\Services\NamespaceService;
|
||||
|
||||
$namespaceService = app(NamespaceService::class);
|
||||
|
||||
// Get current namespace
|
||||
$current = $namespaceService->current();
|
||||
|
||||
// Set current namespace
|
||||
$namespaceService->setCurrent($namespace);
|
||||
|
||||
// Get all accessible namespaces
|
||||
$namespaces = $namespaceService->accessibleByCurrentUser();
|
||||
|
||||
// Group by ownership
|
||||
$grouped = $namespaceService->groupedForCurrentUser();
|
||||
// [
|
||||
// 'personal' => Collection, // User-owned
|
||||
// 'workspaces' => [ // Workspace-owned
|
||||
// ['workspace' => Workspace, 'namespaces' => Collection],
|
||||
// ...
|
||||
// ]
|
||||
// ]
|
||||
```
|
||||
|
||||
### Namespace Switcher UI
|
||||
|
||||
Provide namespace switching in your UI:
|
||||
|
||||
```blade
|
||||
<div class="namespace-switcher">
|
||||
<x-dropdown>
|
||||
<x-slot:trigger>
|
||||
{{ $currentNamespace->name }}
|
||||
</x-slot>
|
||||
|
||||
@foreach($personalNamespaces as $ns)
|
||||
<x-dropdown-item href="?namespace={{ $ns->uuid }}">
|
||||
{{ $ns->name }}
|
||||
</x-dropdown-item>
|
||||
@endforeach
|
||||
|
||||
@foreach($workspaceNamespaces as $group)
|
||||
<x-dropdown-header>{{ $group['workspace']->name }}</x-dropdown-header>
|
||||
@foreach($group['namespaces'] as $ns)
|
||||
<x-dropdown-item href="?namespace={{ $ns->uuid }}">
|
||||
{{ $ns->name }}
|
||||
</x-dropdown-item>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</x-dropdown>
|
||||
</div>
|
||||
```
|
||||
|
||||
### API Integration
|
||||
|
||||
Include namespace in API requests:
|
||||
|
||||
```bash
|
||||
# Header-based
|
||||
curl -H "X-Namespace: uuid-here" \
|
||||
-H "Authorization: Bearer sk_live_..." \
|
||||
https://api.example.com/v1/pages
|
||||
|
||||
# Query parameter
|
||||
curl "https://api.example.com/v1/pages?namespace=uuid-here" \
|
||||
-H "Authorization: Bearer sk_live_..."
|
||||
```
|
||||
|
||||
## Entitlements System
|
||||
|
||||
Entitlements control **what users can do** within their namespaces. The system answers: *"Can this namespace perform this action?"*
|
||||
|
||||
### Core Concepts
|
||||
|
||||
#### Packages
|
||||
|
||||
Bundles of features with defined limits:
|
||||
|
||||
```php
|
||||
Package {
|
||||
id: int
|
||||
code: string // 'social-creator', 'bio-pro'
|
||||
name: string
|
||||
is_base_package: bool // Only one base package per namespace
|
||||
is_stackable: bool // Can have multiple addon packages
|
||||
is_active: bool
|
||||
is_public: bool // Shown in pricing page
|
||||
}
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- **Base Package**: Core subscription (e.g., "Pro Plan")
|
||||
- **Add-on Package**: Stackable extras (e.g., "Extra Storage")
|
||||
|
||||
#### Features
|
||||
|
||||
Capabilities or limits that can be granted:
|
||||
|
||||
```php
|
||||
Feature {
|
||||
id: int
|
||||
code: string // 'social.accounts', 'ai.credits'
|
||||
name: string
|
||||
type: enum // boolean, limit, unlimited
|
||||
reset_type: enum // none, monthly, rolling
|
||||
rolling_window_days: ?int
|
||||
parent_feature_id: ?int // For hierarchical limits
|
||||
category: string // 'social', 'ai', 'storage'
|
||||
}
|
||||
```
|
||||
|
||||
**Feature Types:**
|
||||
|
||||
| Type | Behavior | Example |
|
||||
|------|----------|---------|
|
||||
| **Boolean** | On/off access gate | `tier.apollo`, `host.social` |
|
||||
| **Limit** | Numeric cap on usage | `social.accounts: 5`, `ai.credits: 100` |
|
||||
| **Unlimited** | No cap | `social.posts: unlimited` |
|
||||
|
||||
**Reset Types:**
|
||||
|
||||
| Reset Type | Behavior | Example |
|
||||
|------------|----------|---------|
|
||||
| **None** | Usage accumulates forever | Account limits |
|
||||
| **Monthly** | Resets at billing cycle start | API requests per month |
|
||||
| **Rolling** | Rolling window (e.g., last 30 days) | Posts per day |
|
||||
|
||||
#### Hierarchical Features (Pools)
|
||||
|
||||
Child features share a parent's limit pool:
|
||||
|
||||
```
|
||||
host.storage.total (1000 MB) ← Parent pool
|
||||
├── host.cdn ← Draws from parent
|
||||
├── bio.cdn ← Draws from parent
|
||||
└── social.cdn ← Draws from parent
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```php
|
||||
Feature::create([
|
||||
'code' => 'host.storage.total',
|
||||
'name' => 'Total Storage',
|
||||
'type' => 'limit',
|
||||
'reset_type' => 'none',
|
||||
]);
|
||||
|
||||
Feature::create([
|
||||
'code' => 'bio.cdn',
|
||||
'name' => 'Bio Link Storage',
|
||||
'type' => 'limit',
|
||||
'parent_feature_id' => $parentFeature->id, // Shares pool
|
||||
]);
|
||||
```
|
||||
|
||||
### Entitlement Checks
|
||||
|
||||
Use the entitlement service to check permissions:
|
||||
|
||||
```php
|
||||
use Core\Mod\Tenant\Services\EntitlementService;
|
||||
|
||||
$entitlements = app(EntitlementService::class);
|
||||
|
||||
// Check if namespace can use feature
|
||||
$result = $entitlements->can($namespace, 'social.accounts', quantity: 3);
|
||||
|
||||
if ($result->isDenied()) {
|
||||
return back()->with('error', $result->getMessage());
|
||||
}
|
||||
|
||||
// Proceed with action...
|
||||
|
||||
// Record usage
|
||||
$entitlements->recordUsage($namespace, 'social.accounts', quantity: 1);
|
||||
```
|
||||
|
||||
### Entitlement Result
|
||||
|
||||
The `EntitlementResult` object provides complete context:
|
||||
|
||||
```php
|
||||
$result = $entitlements->can($namespace, 'ai.credits', quantity: 10);
|
||||
|
||||
// Status checks
|
||||
$result->isAllowed(); // true/false
|
||||
$result->isDenied(); // true/false
|
||||
$result->isUnlimited(); // true if unlimited
|
||||
|
||||
// Limits
|
||||
$result->limit; // 100
|
||||
$result->used; // 75
|
||||
$result->remaining; // 25
|
||||
|
||||
// Percentage
|
||||
$result->getUsagePercentage(); // 75.0
|
||||
$result->isNearLimit(); // true if > 80%
|
||||
|
||||
// Denial reason
|
||||
$result->getMessage(); // "Exceeded limit for ai.credits"
|
||||
```
|
||||
|
||||
### Usage Tracking
|
||||
|
||||
Record consumption after successful actions:
|
||||
|
||||
```php
|
||||
$entitlements->recordUsage(
|
||||
namespace: $namespace,
|
||||
featureCode: 'ai.credits',
|
||||
quantity: 10,
|
||||
user: $user, // Optional: who triggered it
|
||||
metadata: [ // Optional: context
|
||||
'model' => 'claude-3',
|
||||
'tokens' => 1500,
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
**Database Schema:**
|
||||
|
||||
```php
|
||||
usage_records {
|
||||
id: int
|
||||
namespace_id: int
|
||||
feature_id: int
|
||||
workspace_id: ?int // For workspace-level aggregation
|
||||
user_id: ?int
|
||||
quantity: int
|
||||
metadata: ?json
|
||||
created_at: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
### Boosts
|
||||
|
||||
Temporary or permanent additions to limits:
|
||||
|
||||
```php
|
||||
Boost {
|
||||
id: int
|
||||
namespace_id: int
|
||||
feature_id: int
|
||||
boost_type: enum // add_limit, enable, unlimited
|
||||
duration_type: enum // cycle_bound, duration, permanent
|
||||
limit_value: ?int // Amount to add
|
||||
consumed_quantity: int // How much used
|
||||
expires_at: ?timestamp
|
||||
status: enum // active, exhausted, expired
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- One-time credit top-ups
|
||||
- Promotional extras
|
||||
- Beta access grants
|
||||
- Temporary unlimited access
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
// Give 1000 bonus AI credits
|
||||
Boost::create([
|
||||
'namespace_id' => $namespace->id,
|
||||
'feature_id' => $aiCreditsFeature->id,
|
||||
'boost_type' => 'add_limit',
|
||||
'duration_type' => 'cycle_bound', // Expires at billing cycle end
|
||||
'limit_value' => 1000,
|
||||
]);
|
||||
```
|
||||
|
||||
### Package Assignment
|
||||
|
||||
Namespaces subscribe to packages:
|
||||
|
||||
```php
|
||||
NamespacePackage {
|
||||
id: int
|
||||
namespace_id: int
|
||||
package_id: int
|
||||
status: enum // active, suspended, cancelled, expired
|
||||
starts_at: timestamp
|
||||
expires_at: ?timestamp
|
||||
billing_cycle_anchor: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
**Provision Package:**
|
||||
|
||||
```php
|
||||
$entitlements->provisionPackage(
|
||||
namespace: $namespace,
|
||||
package: $package,
|
||||
startsAt: now(),
|
||||
expiresAt: now()->addMonth(),
|
||||
);
|
||||
```
|
||||
|
||||
**Package Features:**
|
||||
|
||||
Features are attached to packages with specific limits:
|
||||
|
||||
```php
|
||||
// Package definition
|
||||
$package = Package::find($packageId);
|
||||
|
||||
// Attach features with limits
|
||||
$package->features()->attach($feature->id, [
|
||||
'limit_value' => 5, // This package grants 5 accounts
|
||||
]);
|
||||
|
||||
// Multiple features
|
||||
$package->features()->sync([
|
||||
$socialAccountsFeature->id => ['limit_value' => 5],
|
||||
$aiCreditsFeature->id => ['limit_value' => 100],
|
||||
$storageFeature->id => ['limit_value' => 1000], // MB
|
||||
]);
|
||||
```
|
||||
|
||||
## Usage Dashboard
|
||||
|
||||
Display usage stats to users:
|
||||
|
||||
```php
|
||||
$summary = $entitlements->getUsageSummary($namespace);
|
||||
|
||||
// Returns array grouped by category:
|
||||
[
|
||||
'social' => [
|
||||
[
|
||||
'feature' => Feature,
|
||||
'limit' => 5,
|
||||
'used' => 3,
|
||||
'remaining' => 2,
|
||||
'percentage' => 60.0,
|
||||
'is_unlimited' => false,
|
||||
],
|
||||
...
|
||||
],
|
||||
'ai' => [...],
|
||||
]
|
||||
```
|
||||
|
||||
**UI Example:**
|
||||
|
||||
```blade
|
||||
@foreach($summary as $category => $features)
|
||||
<div class="category">
|
||||
<h3>{{ ucfirst($category) }}</h3>
|
||||
|
||||
@foreach($features as $item)
|
||||
<div class="feature-usage">
|
||||
<div class="feature-name">
|
||||
{{ $item['feature']->name }}
|
||||
</div>
|
||||
|
||||
@if($item['is_unlimited'])
|
||||
<div class="badge">Unlimited</div>
|
||||
@else
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill"
|
||||
style="width: {{ $item['percentage'] }}%"
|
||||
class="{{ $item['percentage'] > 80 ? 'text-red-600' : 'text-green-600' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-text">
|
||||
{{ $item['used'] }} / {{ $item['limit'] }}
|
||||
({{ number_format($item['percentage'], 1) }}%)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
## Billing Integration
|
||||
|
||||
### Billing Context
|
||||
|
||||
Namespaces use `workspace_id` for billing aggregation:
|
||||
|
||||
```php
|
||||
// Get billing workspace
|
||||
$billingWorkspace = $namespace->getBillingContext();
|
||||
|
||||
// User-owned namespace → User's default workspace
|
||||
// Workspace-owned namespace → Owner workspace
|
||||
// Explicit workspace_id → That workspace
|
||||
```
|
||||
|
||||
### Commerce Integration
|
||||
|
||||
Link subscriptions to namespace packages:
|
||||
|
||||
```php
|
||||
// When subscription created
|
||||
event(new SubscriptionCreated($subscription));
|
||||
|
||||
// Listener provisions package
|
||||
$entitlements->provisionPackage(
|
||||
namespace: $subscription->namespace,
|
||||
package: $subscription->package,
|
||||
startsAt: $subscription->starts_at,
|
||||
expiresAt: $subscription->expires_at,
|
||||
);
|
||||
|
||||
// When subscription renewed
|
||||
$namespacePackage->update([
|
||||
'expires_at' => $subscription->next_billing_date,
|
||||
'billing_cycle_anchor' => now(),
|
||||
]);
|
||||
|
||||
// Expire cycle-bound boosts
|
||||
Boost::where('namespace_id', $namespace->id)
|
||||
->where('duration_type', 'cycle_bound')
|
||||
->update(['status' => 'expired']);
|
||||
```
|
||||
|
||||
### External Billing Systems
|
||||
|
||||
API endpoints for external billing (Blesta, Stripe, etc.):
|
||||
|
||||
```bash
|
||||
# Provision package
|
||||
POST /api/v1/entitlements
|
||||
{
|
||||
"namespace_uuid": "uuid",
|
||||
"package_code": "social-creator",
|
||||
"starts_at": "2026-01-01T00:00:00Z",
|
||||
"expires_at": "2026-02-01T00:00:00Z"
|
||||
}
|
||||
|
||||
# Suspend package
|
||||
POST /api/v1/entitlements/{id}/suspend
|
||||
|
||||
# Cancel package
|
||||
POST /api/v1/entitlements/{id}/cancel
|
||||
|
||||
# Renew package
|
||||
POST /api/v1/entitlements/{id}/renew
|
||||
{
|
||||
"expires_at": "2026-03-01T00:00:00Z"
|
||||
}
|
||||
|
||||
# Check entitlements
|
||||
GET /api/v1/entitlements/check
|
||||
?namespace=uuid
|
||||
&feature=social.accounts
|
||||
&quantity=1
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
|
||||
All entitlement changes are logged:
|
||||
|
||||
```php
|
||||
EntitlementLog {
|
||||
id: int
|
||||
namespace_id: int
|
||||
workspace_id: ?int
|
||||
action: enum // package_provisioned, boost_expired, etc.
|
||||
source: enum // blesta, commerce, admin, system, api
|
||||
user_id: ?int
|
||||
data: json // Context about the change
|
||||
created_at: timestamp
|
||||
}
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
- `package_provisioned`, `package_suspended`, `package_cancelled`
|
||||
- `boost_provisioned`, `boost_exhausted`, `boost_expired`
|
||||
- `usage_recorded`, `usage_denied`
|
||||
|
||||
**Retrieve logs:**
|
||||
|
||||
```php
|
||||
$logs = EntitlementLog::where('namespace_id', $namespace->id)
|
||||
->latest()
|
||||
->paginate(20);
|
||||
```
|
||||
|
||||
## Feature Seeder
|
||||
|
||||
Define features in seeders:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Tenant\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Core\Mod\Tenant\Models\Feature;
|
||||
|
||||
class FeatureSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Tier features (boolean gates)
|
||||
Feature::create([
|
||||
'code' => 'tier.apollo',
|
||||
'name' => 'Apollo Tier',
|
||||
'type' => 'boolean',
|
||||
'category' => 'tier',
|
||||
]);
|
||||
|
||||
// Social features
|
||||
Feature::create([
|
||||
'code' => 'social.accounts',
|
||||
'name' => 'Social Accounts',
|
||||
'type' => 'limit',
|
||||
'reset_type' => 'none',
|
||||
'category' => 'social',
|
||||
]);
|
||||
|
||||
Feature::create([
|
||||
'code' => 'social.posts.scheduled',
|
||||
'name' => 'Scheduled Posts',
|
||||
'type' => 'limit',
|
||||
'reset_type' => 'monthly',
|
||||
'category' => 'social',
|
||||
]);
|
||||
|
||||
// AI features
|
||||
Feature::create([
|
||||
'code' => 'ai.credits',
|
||||
'name' => 'AI Credits',
|
||||
'type' => 'limit',
|
||||
'reset_type' => 'monthly',
|
||||
'category' => 'ai',
|
||||
]);
|
||||
|
||||
// Storage pool
|
||||
$storagePool = Feature::create([
|
||||
'code' => 'host.storage.total',
|
||||
'name' => 'Total Storage',
|
||||
'type' => 'limit',
|
||||
'reset_type' => 'none',
|
||||
'category' => 'storage',
|
||||
]);
|
||||
|
||||
// Child features share pool
|
||||
Feature::create([
|
||||
'code' => 'host.cdn',
|
||||
'name' => 'CDN Storage',
|
||||
'type' => 'limit',
|
||||
'parent_feature_id' => $storagePool->id,
|
||||
'category' => 'storage',
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Namespace Isolation
|
||||
|
||||
```php
|
||||
public function test_cannot_access_other_namespace_resources(): void
|
||||
{
|
||||
$namespace1 = Namespace_::factory()->create();
|
||||
$namespace2 = Namespace_::factory()->create();
|
||||
|
||||
$page = Page::factory()->for($namespace1, 'namespace')->create();
|
||||
|
||||
// Set context to namespace2
|
||||
request()->attributes->set('current_namespace', $namespace2);
|
||||
|
||||
// Should not find page from namespace1
|
||||
$this->assertNull(Page::ownedByCurrentNamespace()->find($page->id));
|
||||
}
|
||||
```
|
||||
|
||||
### Test Entitlements
|
||||
|
||||
```php
|
||||
public function test_enforces_feature_limits(): void
|
||||
{
|
||||
$namespace = Namespace_::factory()->create();
|
||||
|
||||
$package = Package::factory()->create();
|
||||
$feature = Feature::factory()->create([
|
||||
'code' => 'social.accounts',
|
||||
'type' => 'limit',
|
||||
]);
|
||||
|
||||
$package->features()->attach($feature->id, ['limit_value' => 5]);
|
||||
|
||||
$entitlements = app(EntitlementService::class);
|
||||
$entitlements->provisionPackage($namespace, $package);
|
||||
|
||||
// Can create up to limit
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$result = $entitlements->can($namespace, 'social.accounts');
|
||||
$this->assertTrue($result->isAllowed());
|
||||
$entitlements->recordUsage($namespace, 'social.accounts');
|
||||
}
|
||||
|
||||
// 6th attempt denied
|
||||
$result = $entitlements->can($namespace, 'social.accounts');
|
||||
$this->assertTrue($result->isDenied());
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Use Namespace Scoping
|
||||
|
||||
```php
|
||||
// ✅ Good - scoped to namespace
|
||||
class Page extends Model
|
||||
{
|
||||
use BelongsToNamespace;
|
||||
}
|
||||
|
||||
// ❌ Bad - no isolation
|
||||
class Page extends Model { }
|
||||
```
|
||||
|
||||
### 2. Check Entitlements Before Actions
|
||||
|
||||
```php
|
||||
// ✅ Good - check before creating
|
||||
$result = $entitlements->can($namespace, 'social.accounts');
|
||||
if ($result->isDenied()) {
|
||||
return back()->with('error', $result->getMessage());
|
||||
}
|
||||
|
||||
SocialAccount::create($data);
|
||||
$entitlements->recordUsage($namespace, 'social.accounts');
|
||||
|
||||
// ❌ Bad - no entitlement check
|
||||
SocialAccount::create($data);
|
||||
```
|
||||
|
||||
### 3. Use Descriptive Feature Codes
|
||||
|
||||
```php
|
||||
// ✅ Good - clear hierarchy
|
||||
'social.accounts'
|
||||
'social.posts.scheduled'
|
||||
'ai.credits.claude'
|
||||
|
||||
// ❌ Bad - unclear
|
||||
'accounts'
|
||||
'posts'
|
||||
'credits'
|
||||
```
|
||||
|
||||
### 4. Provide Usage Visibility
|
||||
|
||||
Always show users their current usage and limits in the UI.
|
||||
|
||||
### 5. Log Entitlement Changes
|
||||
|
||||
All provisioning, suspension, and cancellation should be logged for audit purposes.
|
||||
|
||||
## Migration from Workspace-Only
|
||||
|
||||
If migrating from workspace-only system:
|
||||
|
||||
```php
|
||||
// Create namespace for each workspace
|
||||
foreach (Workspace::all() as $workspace) {
|
||||
$namespace = Namespace_::create([
|
||||
'name' => $workspace->name,
|
||||
'owner_type' => Workspace::class,
|
||||
'owner_id' => $workspace->id,
|
||||
'workspace_id' => $workspace->id,
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
// Migrate existing resources
|
||||
Resource::where('workspace_id', $workspace->id)
|
||||
->update(['namespace_id' => $namespace->id]);
|
||||
|
||||
// Migrate packages
|
||||
WorkspacePackage::where('workspace_id', $workspace->id)
|
||||
->each(function ($wp) use ($namespace) {
|
||||
NamespacePackage::create([
|
||||
'namespace_id' => $namespace->id,
|
||||
'package_id' => $wp->package_id,
|
||||
'status' => $wp->status,
|
||||
'starts_at' => $wp->starts_at,
|
||||
'expires_at' => $wp->expires_at,
|
||||
]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Multi-Tenancy Architecture →](/architecture/multi-tenancy)
|
||||
- [Entitlements RFC](https://github.com/host-uk/core-php/blob/main/docs/rfc/RFC-004-ENTITLEMENTS.md)
|
||||
- [API Package →](/packages/api)
|
||||
- [Security Overview →](/security/overview)
|
||||
776
build/php/patterns/actions.md
Normal file
776
build/php/patterns/actions.md
Normal file
|
|
@ -0,0 +1,776 @@
|
|||
# Actions Pattern
|
||||
|
||||
Actions are single-purpose classes that encapsulate business logic. They provide a clean, testable, and reusable way to handle complex operations.
|
||||
|
||||
## Why Actions?
|
||||
|
||||
### Traditional Controller (Fat Controllers)
|
||||
|
||||
```php
|
||||
class PostController extends Controller
|
||||
{
|
||||
public function store(Request $request)
|
||||
{
|
||||
// Validation
|
||||
$validated = $request->validate([/*...*/]);
|
||||
|
||||
// Business logic mixed with controller concerns
|
||||
$slug = Str::slug($validated['title']);
|
||||
|
||||
if (Post::where('slug', $slug)->exists()) {
|
||||
$slug .= '-' . Str::random(5);
|
||||
}
|
||||
|
||||
$post = Post::create([
|
||||
'title' => $validated['title'],
|
||||
'slug' => $slug,
|
||||
'content' => $validated['content'],
|
||||
'workspace_id' => auth()->user()->workspace_id,
|
||||
]);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
$post->tags()->sync($validated['tags']);
|
||||
}
|
||||
|
||||
event(new PostCreated($post));
|
||||
|
||||
Cache::tags(['posts'])->flush();
|
||||
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Business logic tied to HTTP layer
|
||||
- Hard to reuse from console, jobs, or tests
|
||||
- Difficult to test in isolation
|
||||
- Controller responsibilities bloat
|
||||
|
||||
### Actions Pattern (Clean Separation)
|
||||
|
||||
```php
|
||||
class PostController extends Controller
|
||||
{
|
||||
public function store(StorePostRequest $request)
|
||||
{
|
||||
$post = CreatePost::run($request->validated());
|
||||
|
||||
return redirect()->route('posts.show', $post);
|
||||
}
|
||||
}
|
||||
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
$slug = $this->generateUniqueSlug($data['title']);
|
||||
|
||||
$post = Post::create([
|
||||
'title' => $data['title'],
|
||||
'slug' => $slug,
|
||||
'content' => $data['content'],
|
||||
]);
|
||||
|
||||
if (isset($data['tags'])) {
|
||||
$post->tags()->sync($data['tags']);
|
||||
}
|
||||
|
||||
event(new PostCreated($post));
|
||||
Cache::tags(['posts'])->flush();
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
private function generateUniqueSlug(string $title): string
|
||||
{
|
||||
$slug = Str::slug($title);
|
||||
|
||||
if (Post::where('slug', $slug)->exists()) {
|
||||
$slug .= '-' . Str::random(5);
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Business logic isolated from HTTP concerns
|
||||
- Reusable from anywhere (controllers, jobs, commands, tests)
|
||||
- Easy to test
|
||||
- Single responsibility
|
||||
- Dependency injection support
|
||||
|
||||
## Creating Actions
|
||||
|
||||
### Basic Action
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Actions;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Mod\Blog\Models\Post;
|
||||
|
||||
class PublishPost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post): Post
|
||||
{
|
||||
$post->update([
|
||||
'published_at' => now(),
|
||||
'status' => 'published',
|
||||
]);
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Actions
|
||||
|
||||
```php
|
||||
// Static call (recommended)
|
||||
$post = PublishPost::run($post);
|
||||
|
||||
// Instance call
|
||||
$action = new PublishPost();
|
||||
$post = $action->handle($post);
|
||||
|
||||
// Via container (with DI)
|
||||
$post = app(PublishPost::class)->handle($post);
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Actions support constructor dependency injection:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Mod\Blog\Actions;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Mod\Blog\Models\Post;
|
||||
use Mod\Blog\Repositories\PostRepository;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function __construct(
|
||||
private PostRepository $posts,
|
||||
private Dispatcher $events,
|
||||
private Cache $cache,
|
||||
) {}
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
$post = $this->posts->create($data);
|
||||
|
||||
$this->events->dispatch(new PostCreated($post));
|
||||
$this->cache->tags(['posts'])->flush();
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Action Return Types
|
||||
|
||||
### Returning Models
|
||||
|
||||
```php
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
return Post::create($data);
|
||||
}
|
||||
}
|
||||
|
||||
$post = CreatePost::run($data);
|
||||
```
|
||||
|
||||
### Returning Collections
|
||||
|
||||
```php
|
||||
class GetRecentPosts
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(int $limit = 10): Collection
|
||||
{
|
||||
return Post::published()
|
||||
->latest('published_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
$posts = GetRecentPosts::run(5);
|
||||
```
|
||||
|
||||
### Returning Boolean
|
||||
|
||||
```php
|
||||
class DeletePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post): bool
|
||||
{
|
||||
return $post->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$deleted = DeletePost::run($post);
|
||||
```
|
||||
|
||||
### Returning DTOs
|
||||
|
||||
```php
|
||||
class AnalyzePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post): PostAnalytics
|
||||
{
|
||||
return new PostAnalytics(
|
||||
views: $post->views()->count(),
|
||||
averageReadTime: $this->calculateReadTime($post),
|
||||
engagement: $this->calculateEngagement($post),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$analytics = AnalyzePost::run($post);
|
||||
echo $analytics->views;
|
||||
```
|
||||
|
||||
## Complex Actions
|
||||
|
||||
### Multi-Step Actions
|
||||
|
||||
```php
|
||||
class ImportPostsFromWordPress
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function __construct(
|
||||
private WordPressClient $client,
|
||||
private CreatePost $createPost,
|
||||
private AttachCategories $attachCategories,
|
||||
private ImportMedia $importMedia,
|
||||
) {}
|
||||
|
||||
public function handle(string $siteUrl, array $options = []): ImportResult
|
||||
{
|
||||
$posts = $this->client->fetchPosts($siteUrl);
|
||||
$imported = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($posts as $wpPost) {
|
||||
try {
|
||||
DB::transaction(function () use ($wpPost, &$imported) {
|
||||
// Create post
|
||||
$post = $this->createPost->handle([
|
||||
'title' => $wpPost['title'],
|
||||
'content' => $wpPost['content'],
|
||||
'published_at' => $wpPost['date'],
|
||||
]);
|
||||
|
||||
// Import media
|
||||
if ($wpPost['featured_image']) {
|
||||
$this->importMedia->handle($post, $wpPost['featured_image']);
|
||||
}
|
||||
|
||||
// Attach categories
|
||||
$this->attachCategories->handle($post, $wpPost['categories']);
|
||||
|
||||
$imported[] = $post;
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = [
|
||||
'post' => $wpPost['title'],
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return new ImportResult(
|
||||
imported: collect($imported),
|
||||
errors: collect($errors),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Actions with Validation
|
||||
|
||||
```php
|
||||
class UpdatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function __construct(
|
||||
private ValidatePostData $validator,
|
||||
) {}
|
||||
|
||||
public function handle(Post $post, array $data): Post
|
||||
{
|
||||
// Validate before processing
|
||||
$validated = $this->validator->handle($data);
|
||||
|
||||
$post->update($validated);
|
||||
|
||||
return $post->fresh();
|
||||
}
|
||||
}
|
||||
|
||||
class ValidatePostData
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): array
|
||||
{
|
||||
return validator($data, [
|
||||
'title' => 'required|max:255',
|
||||
'content' => 'required',
|
||||
'published_at' => 'nullable|date',
|
||||
])->validate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Action Patterns
|
||||
|
||||
### Command Pattern
|
||||
|
||||
Actions are essentially the Command pattern:
|
||||
|
||||
```php
|
||||
interface ActionInterface
|
||||
{
|
||||
public function handle(...$params);
|
||||
}
|
||||
|
||||
// Each action is a command
|
||||
class PublishPost implements ActionInterface { }
|
||||
class UnpublishPost implements ActionInterface { }
|
||||
class SchedulePost implements ActionInterface { }
|
||||
```
|
||||
|
||||
### Pipeline Pattern
|
||||
|
||||
Chain multiple actions:
|
||||
|
||||
```php
|
||||
class ProcessNewPost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
return Pipeline::send($data)
|
||||
->through([
|
||||
ValidatePostData::class,
|
||||
SanitizeContent::class,
|
||||
CreatePost::class,
|
||||
GenerateExcerpt::class,
|
||||
GenerateSocialImages::class,
|
||||
NotifySubscribers::class,
|
||||
])
|
||||
->thenReturn();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy Pattern
|
||||
|
||||
Different strategies as actions:
|
||||
|
||||
```php
|
||||
interface PublishStrategy
|
||||
{
|
||||
public function publish(Post $post): void;
|
||||
}
|
||||
|
||||
class PublishImmediately implements PublishStrategy
|
||||
{
|
||||
public function publish(Post $post): void
|
||||
{
|
||||
$post->update(['published_at' => now()]);
|
||||
}
|
||||
}
|
||||
|
||||
class ScheduleForLater implements PublishStrategy
|
||||
{
|
||||
public function publish(Post $post): void
|
||||
{
|
||||
PublishPostJob::dispatch($post)
|
||||
->delay($post->scheduled_at);
|
||||
}
|
||||
}
|
||||
|
||||
class PublishPost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post, PublishStrategy $strategy): void
|
||||
{
|
||||
$strategy->publish($post);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Actions
|
||||
|
||||
### Unit Testing
|
||||
|
||||
Test actions in isolation:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Mod\Blog\Actions;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Mod\Blog\Actions\CreatePost;
|
||||
use Mod\Blog\Models\Post;
|
||||
|
||||
class CreatePostTest extends TestCase
|
||||
{
|
||||
public function test_creates_post_with_valid_data(): void
|
||||
{
|
||||
$data = [
|
||||
'title' => 'Test Post',
|
||||
'content' => 'Test content',
|
||||
];
|
||||
|
||||
$post = CreatePost::run($data);
|
||||
|
||||
$this->assertInstanceOf(Post::class, $post);
|
||||
$this->assertEquals('Test Post', $post->title);
|
||||
$this->assertDatabaseHas('posts', [
|
||||
'title' => 'Test Post',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_generates_unique_slug(): void
|
||||
{
|
||||
Post::factory()->create(['slug' => 'test-post']);
|
||||
|
||||
$post = CreatePost::run([
|
||||
'title' => 'Test Post',
|
||||
'content' => 'Content',
|
||||
]);
|
||||
|
||||
$this->assertNotEquals('test-post', $post->slug);
|
||||
$this->assertStringStartsWith('test-post-', $post->slug);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mocking Dependencies
|
||||
|
||||
```php
|
||||
public function test_dispatches_event_after_creation(): void
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
$post = CreatePost::run([
|
||||
'title' => 'Test Post',
|
||||
'content' => 'Content',
|
||||
]);
|
||||
|
||||
Event::assertDispatched(PostCreated::class, function ($event) use ($post) {
|
||||
return $event->post->id === $post->id;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
```php
|
||||
public function test_import_creates_posts_from_wordpress(): void
|
||||
{
|
||||
Http::fake([
|
||||
'wordpress.example.com/*' => Http::response([
|
||||
[
|
||||
'title' => 'WP Post 1',
|
||||
'content' => 'Content 1',
|
||||
'date' => '2026-01-01',
|
||||
],
|
||||
[
|
||||
'title' => 'WP Post 2',
|
||||
'content' => 'Content 2',
|
||||
'date' => '2026-01-02',
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$result = ImportPostsFromWordPress::run('wordpress.example.com');
|
||||
|
||||
$this->assertCount(2, $result->imported);
|
||||
$this->assertCount(0, $result->errors);
|
||||
$this->assertEquals(2, Post::count());
|
||||
}
|
||||
```
|
||||
|
||||
## Action Composition
|
||||
|
||||
### Composing Actions
|
||||
|
||||
Build complex operations from simple actions:
|
||||
|
||||
```php
|
||||
class PublishBlogPost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function __construct(
|
||||
private UpdatePost $updatePost,
|
||||
private GenerateOgImage $generateImage,
|
||||
private NotifySubscribers $notifySubscribers,
|
||||
private PingSearchEngines $pingSearchEngines,
|
||||
) {}
|
||||
|
||||
public function handle(Post $post): Post
|
||||
{
|
||||
// Update post status
|
||||
$post = $this->updatePost->handle($post, [
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
// Generate social images
|
||||
$this->generateImage->handle($post);
|
||||
|
||||
// Notify subscribers
|
||||
dispatch(fn () => $this->notifySubscribers->handle($post))
|
||||
->afterResponse();
|
||||
|
||||
// Ping search engines
|
||||
dispatch(fn () => $this->pingSearchEngines->handle($post))
|
||||
->afterResponse();
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Execution
|
||||
|
||||
```php
|
||||
class ProcessPost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post, array $options = []): Post
|
||||
{
|
||||
if ($options['publish'] ?? false) {
|
||||
PublishPost::run($post);
|
||||
}
|
||||
|
||||
if ($options['notify'] ?? false) {
|
||||
NotifySubscribers::run($post);
|
||||
}
|
||||
|
||||
if ($options['generate_images'] ?? true) {
|
||||
GenerateSocialImages::run($post);
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Single Responsibility
|
||||
|
||||
Each action should do one thing:
|
||||
|
||||
```php
|
||||
// ✅ Good - focused actions
|
||||
class CreatePost { }
|
||||
class PublishPost { }
|
||||
class NotifySubscribers { }
|
||||
|
||||
// ❌ Bad - does too much
|
||||
class CreateAndPublishPostAndNotifySubscribers { }
|
||||
```
|
||||
|
||||
### 2. Meaningful Names
|
||||
|
||||
Use descriptive verb-noun names:
|
||||
|
||||
```php
|
||||
// ✅ Good names
|
||||
class CreatePost { }
|
||||
class UpdatePost { }
|
||||
class DeletePost { }
|
||||
class PublishPost { }
|
||||
class UnpublishPost { }
|
||||
|
||||
// ❌ Bad names
|
||||
class PostAction { }
|
||||
class HandlePost { }
|
||||
class DoStuff { }
|
||||
```
|
||||
|
||||
### 3. Return Values
|
||||
|
||||
Always return something useful:
|
||||
|
||||
```php
|
||||
// ✅ Good - returns created model
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
return Post::create($data);
|
||||
}
|
||||
|
||||
// ❌ Bad - returns nothing
|
||||
public function handle(array $data): void
|
||||
{
|
||||
Post::create($data);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Idempotency
|
||||
|
||||
Make actions idempotent when possible:
|
||||
|
||||
```php
|
||||
class PublishPost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(Post $post): Post
|
||||
{
|
||||
// Idempotent - safe to call multiple times
|
||||
if ($post->isPublished()) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
$post->update(['published_at' => now()]);
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Type Hints
|
||||
|
||||
Always use type hints:
|
||||
|
||||
```php
|
||||
// ✅ Good - clear types
|
||||
public function handle(Post $post, array $data): Post
|
||||
|
||||
// ❌ Bad - no types
|
||||
public function handle($post, $data)
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### CRUD Operations
|
||||
|
||||
```php
|
||||
class CreatePost { }
|
||||
class UpdatePost { }
|
||||
class DeletePost { }
|
||||
class RestorePost { }
|
||||
```
|
||||
|
||||
### State Transitions
|
||||
|
||||
```php
|
||||
class PublishPost { }
|
||||
class UnpublishPost { }
|
||||
class ArchivePost { }
|
||||
class SchedulePost { }
|
||||
```
|
||||
|
||||
### Data Processing
|
||||
|
||||
```php
|
||||
class ImportPosts { }
|
||||
class ExportPosts { }
|
||||
class SyncPosts { }
|
||||
class MigratePosts { }
|
||||
```
|
||||
|
||||
### Calculations
|
||||
|
||||
```php
|
||||
class CalculatePostStatistics { }
|
||||
class GeneratePostSummary { }
|
||||
class AnalyzePostPerformance { }
|
||||
```
|
||||
|
||||
### External Integrations
|
||||
|
||||
```php
|
||||
class SyncToWordPress { }
|
||||
class PublishToMedium { }
|
||||
class ShareOnSocial { }
|
||||
```
|
||||
|
||||
## Action vs Service
|
||||
|
||||
### When to Use Actions
|
||||
|
||||
- Single, focused operations
|
||||
- No state management needed
|
||||
- Reusable across contexts
|
||||
|
||||
### When to Use Services
|
||||
|
||||
- Multiple related operations
|
||||
- Stateful operations
|
||||
- Facade for complex subsystem
|
||||
|
||||
```php
|
||||
// Action - single operation
|
||||
class CreatePost
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(array $data): Post
|
||||
{
|
||||
return Post::create($data);
|
||||
}
|
||||
}
|
||||
|
||||
// Service - multiple operations, state
|
||||
class BlogService
|
||||
{
|
||||
private Collection $posts;
|
||||
|
||||
public function getRecentPosts(int $limit): Collection
|
||||
{
|
||||
return $this->posts ??= Post::latest()->limit($limit)->get();
|
||||
}
|
||||
|
||||
public function getPopularPosts(int $limit): Collection { }
|
||||
public function searchPosts(string $query): Collection { }
|
||||
public function getPostsByCategory(Category $category): Collection { }
|
||||
}
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Service Layer](/patterns-guide/services)
|
||||
- [Repository Pattern](/patterns-guide/repositories)
|
||||
- [Testing Actions](/testing/actions)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue