docs: add human-friendly documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7d7c489509
commit
755c6b7134
3 changed files with 726 additions and 105 deletions
367
docs/architecture.md
Normal file
367
docs/architecture.md
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
---
|
||||
title: Architecture
|
||||
description: Internal design of core/php -- FrankenPHP handler, service orchestration, project detection, CI pipeline, deployment, and native bridge.
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
This document explains how the Go code in `forge.lthn.ai/core/php` is structured
|
||||
and how the major subsystems interact.
|
||||
|
||||
|
||||
## Command Registration
|
||||
|
||||
The module exposes two entry points for command registration:
|
||||
|
||||
- **`AddPHPCommands(root)`** -- adds commands under a `php` parent (for the
|
||||
multi-purpose `core` binary where PHP is one of many command groups).
|
||||
- **`AddPHPRootCommands(root)`** -- adds commands directly to the root (for the
|
||||
standalone `core-php` binary where `dev`, `build`, etc. are top-level).
|
||||
|
||||
Both paths register the same set of commands and share workspace-aware
|
||||
`PersistentPreRunE` logic that detects `.core/workspace.yaml` and `cd`s into the
|
||||
active package directory before execution.
|
||||
|
||||
The standalone binary is minimal:
|
||||
|
||||
```go
|
||||
// cmd/core-php/main.go
|
||||
func main() {
|
||||
cli.Main(
|
||||
cli.WithCommands("php", php.AddPHPRootCommands),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Command Tree
|
||||
|
||||
```
|
||||
core-php (or core php)
|
||||
dev Start all detected services (FrankenPHP, Vite, Horizon, Reverb, Redis)
|
||||
logs Stream unified or per-service logs
|
||||
stop Stop all running services
|
||||
status Show project info and service detection
|
||||
ssl Setup mkcert SSL certificates
|
||||
build Build Docker or LinuxKit image
|
||||
serve Run a production Docker container
|
||||
shell <container> Open a shell in a running container
|
||||
ci Run full QA pipeline (test, stan, psalm, fmt, audit, security)
|
||||
packages
|
||||
link <paths> Add Composer path repositories for local development
|
||||
unlink <names> Remove path repositories
|
||||
update [pkgs] Run composer update
|
||||
list List linked packages
|
||||
deploy Trigger Coolify deployment
|
||||
deploy:status Check deployment status
|
||||
deploy:rollback Rollback to a previous deployment
|
||||
deploy:list List recent deployments
|
||||
serve:embedded (CGO only) Serve via embedded FrankenPHP runtime
|
||||
exec <cmd> (CGO only) Execute artisan via FrankenPHP
|
||||
```
|
||||
|
||||
|
||||
## FrankenPHP Handler (CGO)
|
||||
|
||||
Files: `handler.go`, `cmd_serve_frankenphp.go`, `env.go`, `extract.go`
|
||||
|
||||
Build tag: `//go:build cgo`
|
||||
|
||||
The `Handler` struct implements `http.Handler` and delegates all PHP processing
|
||||
to the FrankenPHP C library. It supports two modes:
|
||||
|
||||
1. **Octane worker mode** -- Laravel stays booted in memory across requests.
|
||||
Workers are persistent PHP processes that handle requests without
|
||||
re-bootstrapping. This yields sub-millisecond response times.
|
||||
2. **Standard mode** -- each request boots the PHP application from scratch.
|
||||
Used as a fallback when Octane is not installed.
|
||||
|
||||
### Request Routing
|
||||
|
||||
`Handler.ServeHTTP` implements a try-files pattern similar to Caddy/Nginx:
|
||||
|
||||
1. If the URL maps to a directory, rewrite to `{dir}/index.php`.
|
||||
2. If the file does not exist and the URL does not end in `.php`, rewrite to
|
||||
`/index.php` (front controller).
|
||||
3. Non-PHP files that exist on disc are served directly via `http.ServeFile`.
|
||||
4. Everything else is passed to `frankenphp.ServeHTTP`.
|
||||
|
||||
### Initialisation
|
||||
|
||||
```go
|
||||
handler, cleanup, err := php.NewHandler(laravelRoot, php.HandlerConfig{
|
||||
NumThreads: 4,
|
||||
NumWorkers: 2,
|
||||
PHPIni: map[string]string{
|
||||
"display_errors": "Off",
|
||||
"opcache.enable": "1",
|
||||
},
|
||||
})
|
||||
defer cleanup()
|
||||
```
|
||||
|
||||
`NewHandler` tries to initialise FrankenPHP with workers first. If
|
||||
`vendor/laravel/octane/bin/frankenphp-worker.php` exists, it passes the worker
|
||||
script to `frankenphp.Init`. If that fails, it falls back to standard mode.
|
||||
|
||||
### Embedded Applications
|
||||
|
||||
`Extract()` copies an `embed.FS`-packaged Laravel application to a temporary
|
||||
directory so that FrankenPHP can access real filesystem paths.
|
||||
`PrepareRuntimeEnvironment()` then creates persistent data directories
|
||||
(`~/Library/Application Support/{app}` on macOS, `~/.local/share/{app}` on
|
||||
Linux), generates a `.env` file with an auto-generated `APP_KEY`, symlinks
|
||||
`storage/` to the persistent location, and creates an empty SQLite database.
|
||||
|
||||
|
||||
## Native Bridge
|
||||
|
||||
File: `bridge.go`
|
||||
|
||||
The bridge is a localhost-only HTTP server that allows PHP code to call back into
|
||||
Go. This is needed because Livewire renders server-side in PHP and cannot call
|
||||
Wails bindings (`window.go.*`) directly.
|
||||
|
||||
```go
|
||||
bridge, err := php.NewBridge(myHandler)
|
||||
// PHP can now POST to http://127.0.0.1:{bridge.Port()}/bridge/call
|
||||
```
|
||||
|
||||
The bridge exposes two endpoints:
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| GET | `/bridge/health` | Health check (returns `{"status":"ok"}`) |
|
||||
| POST | `/bridge/call` | Invoke a named method with JSON arguments |
|
||||
|
||||
The host application implements `BridgeHandler`:
|
||||
|
||||
```go
|
||||
type BridgeHandler interface {
|
||||
HandleBridgeCall(method string, args json.RawMessage) (any, error)
|
||||
}
|
||||
```
|
||||
|
||||
The bridge port is injected into Laravel's `.env` as `NATIVE_BRIDGE_URL`.
|
||||
|
||||
|
||||
## Service Orchestration
|
||||
|
||||
Files: `php.go`, `services.go`, `services_unix.go`, `services_windows.go`
|
||||
|
||||
### DevServer
|
||||
|
||||
`DevServer` manages the lifecycle of all development services. It:
|
||||
|
||||
1. Detects which services are needed (via `DetectServices`).
|
||||
2. Filters out services disabled by flags (`--no-vite`, `--no-horizon`, etc.).
|
||||
3. Creates concrete service instances.
|
||||
4. Starts all services, rolling back if any fail.
|
||||
5. Provides unified log streaming (round-robin multiplexing from all service log files).
|
||||
6. Stops services in reverse order on shutdown.
|
||||
|
||||
### Service Interface
|
||||
|
||||
All managed services implement:
|
||||
|
||||
```go
|
||||
type Service interface {
|
||||
Name() string
|
||||
Start(ctx context.Context) error
|
||||
Stop() error
|
||||
Logs(follow bool) (io.ReadCloser, error)
|
||||
Status() ServiceStatus
|
||||
}
|
||||
```
|
||||
|
||||
### Concrete Services
|
||||
|
||||
| Service | Binary | Default Port | Notes |
|
||||
|---|---|---|---|
|
||||
| `FrankenPHPService` | `php artisan octane:start --server=frankenphp` | 8000 | HTTPS via mkcert certificates |
|
||||
| `ViteService` | `npm/pnpm/yarn/bun run dev` | 5173 | Auto-detects package manager |
|
||||
| `HorizonService` | `php artisan horizon` | -- | Uses `horizon:terminate` for graceful stop |
|
||||
| `ReverbService` | `php artisan reverb:start` | 8080 | WebSocket server |
|
||||
| `RedisService` | `redis-server` | 6379 | Optional config file support |
|
||||
|
||||
All services inherit from `baseService`, which handles:
|
||||
|
||||
- Process creation with platform-specific `SysProcAttr` for clean shutdown.
|
||||
- Log file creation under `.core/logs/`.
|
||||
- Background process monitoring.
|
||||
- Graceful stop with SIGTERM, then SIGKILL after 5 seconds.
|
||||
|
||||
|
||||
## Project Detection
|
||||
|
||||
File: `detect.go`
|
||||
|
||||
The detection system inspects the filesystem to determine project capabilities:
|
||||
|
||||
| Function | Checks |
|
||||
|---|---|
|
||||
| `IsLaravelProject(dir)` | `artisan` exists and `composer.json` requires `laravel/framework` |
|
||||
| `IsFrankenPHPProject(dir)` | `laravel/octane` in `composer.json`, `config/octane.php` mentions frankenphp |
|
||||
| `IsPHPProject(dir)` | `composer.json` exists |
|
||||
| `DetectServices(dir)` | Checks for Vite configs, Horizon config, Reverb config, Redis in `.env` |
|
||||
| `DetectPackageManager(dir)` | Inspects lock files: `bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json` |
|
||||
| `GetLaravelAppName(dir)` | Reads `APP_NAME` from `.env` |
|
||||
| `GetLaravelAppURL(dir)` | Reads `APP_URL` from `.env` |
|
||||
|
||||
|
||||
## Dockerfile Generation
|
||||
|
||||
File: `dockerfile.go`
|
||||
|
||||
`GenerateDockerfile(dir)` produces a multi-stage Dockerfile by analysing
|
||||
`composer.json`:
|
||||
|
||||
1. **PHP version** -- extracted from `composer.json`'s `require.php` constraint.
|
||||
2. **Extensions** -- inferred from package dependencies (e.g., `laravel/horizon`
|
||||
implies `redis` and `pcntl`; `intervention/image` implies `gd`).
|
||||
3. **Frontend assets** -- if `package.json` has a `build` script, a Node.js
|
||||
build stage is prepended.
|
||||
4. **Base image** -- `dunglas/frankenphp` with Alpine variant by default.
|
||||
|
||||
The generated Dockerfile includes:
|
||||
|
||||
- Multi-stage build for frontend assets (Node 20 Alpine).
|
||||
- Composer dependency installation with layer caching.
|
||||
- Laravel config/route/view caching.
|
||||
- Correct permissions for `storage/` and `bootstrap/cache/`.
|
||||
- Health check via `curl -f http://localhost/up`.
|
||||
- Octane start command if `laravel/octane` is detected.
|
||||
|
||||
|
||||
## CI Pipeline
|
||||
|
||||
File: `cmd_ci.go`, `quality.go`, `testing.go`
|
||||
|
||||
The `ci` command runs six checks in sequence:
|
||||
|
||||
| Check | Tool | SARIF Support |
|
||||
|---|---|---|
|
||||
| `test` | Pest or PHPUnit (auto-detected) | No |
|
||||
| `stan` | PHPStan or Larastan (auto-detected) | Yes |
|
||||
| `psalm` | Psalm (skipped if not configured) | Yes |
|
||||
| `fmt` | Laravel Pint (check-only mode) | No |
|
||||
| `audit` | `composer audit` + `npm audit` | No |
|
||||
| `security` | `.env` and filesystem security checks | No |
|
||||
|
||||
### Output Formats
|
||||
|
||||
- **Default** -- coloured terminal table with per-check status icons.
|
||||
- **`--json`** -- structured `CIResult` JSON with per-check details.
|
||||
- **`--summary`** -- Markdown table suitable for PR comments.
|
||||
- **`--sarif`** -- SARIF files for stan/psalm, uploadable to GitHub Security.
|
||||
- **`--upload-sarif`** -- uploads SARIF files via `gh api`.
|
||||
|
||||
### Failure Threshold
|
||||
|
||||
The `--fail-on` flag controls when the pipeline returns a non-zero exit code:
|
||||
|
||||
| Value | Fails On |
|
||||
|---|---|
|
||||
| `critical` | Only if issues with `Issues > 0` |
|
||||
| `high` / `error` (default) | Any check with status `failed` |
|
||||
| `warning` | Any check with status `failed` or `warning` |
|
||||
|
||||
### QA Pipeline Stages
|
||||
|
||||
The `quality.go` file also defines a broader QA pipeline (`QAOptions`) with
|
||||
three stages:
|
||||
|
||||
- **Quick** -- `audit`, `fmt`, `stan`
|
||||
- **Standard** -- `psalm` (if configured), `test`
|
||||
- **Full** -- `rector` (if configured), `infection` (if configured)
|
||||
|
||||
|
||||
## Deployment (Coolify)
|
||||
|
||||
Files: `deploy.go`, `coolify.go`
|
||||
|
||||
### Configuration
|
||||
|
||||
Coolify credentials are loaded from environment variables or `.env`:
|
||||
|
||||
```
|
||||
COOLIFY_URL=https://coolify.example.com
|
||||
COOLIFY_TOKEN=your-api-token
|
||||
COOLIFY_APP_ID=app-uuid
|
||||
COOLIFY_STAGING_APP_ID=staging-app-uuid (optional)
|
||||
```
|
||||
|
||||
Environment variables take precedence over `.env` values.
|
||||
|
||||
### CoolifyClient
|
||||
|
||||
The `CoolifyClient` wraps the Coolify REST API:
|
||||
|
||||
```go
|
||||
client := php.NewCoolifyClient(baseURL, token)
|
||||
deployment, err := client.TriggerDeploy(ctx, appID, force)
|
||||
deployment, err := client.GetDeployment(ctx, appID, deploymentID)
|
||||
deployments, err := client.ListDeployments(ctx, appID, limit)
|
||||
deployment, err := client.Rollback(ctx, appID, deploymentID)
|
||||
app, err := client.GetApp(ctx, appID)
|
||||
```
|
||||
|
||||
### Deployment Flow
|
||||
|
||||
1. Load config from `.env` or environment.
|
||||
2. Resolve the app ID for the target environment (production or staging).
|
||||
3. Trigger deployment via the Coolify API.
|
||||
4. If `--wait` is set, poll every 5 seconds (up to 10 minutes) until the
|
||||
deployment reaches a terminal state.
|
||||
5. Print deployment status with coloured output.
|
||||
|
||||
### Rollback
|
||||
|
||||
If no specific deployment ID is provided, `Rollback()` fetches the 10 most
|
||||
recent deployments, skips the current one, and rolls back to the last
|
||||
successful deployment.
|
||||
|
||||
|
||||
## Workspace Support
|
||||
|
||||
File: `workspace.go`
|
||||
|
||||
For multi-package repositories, a `.core/workspace.yaml` file at the workspace
|
||||
root can set an active package:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
active: core-tenant
|
||||
packages_dir: ./packages
|
||||
```
|
||||
|
||||
When present, the `PersistentPreRunE` hook automatically changes the working
|
||||
directory to the active package before command execution. The workspace root is
|
||||
found by walking up the directory tree.
|
||||
|
||||
|
||||
## SSL Certificates
|
||||
|
||||
File: `ssl.go`
|
||||
|
||||
The `ssl` command and `--https` flag use [mkcert](https://github.com/FiloSottile/mkcert)
|
||||
to generate locally-trusted SSL certificates. Certificates are stored in
|
||||
`~/.core/ssl/` by default.
|
||||
|
||||
The `SetupSSLIfNeeded()` function is idempotent: it checks for existing
|
||||
certificates before generating new ones. Generated certificates cover the
|
||||
domain, `localhost`, `127.0.0.1`, and `::1`.
|
||||
|
||||
|
||||
## Filesystem Abstraction
|
||||
|
||||
The module uses `io.Medium` from `forge.lthn.ai/core/go-io` for all filesystem
|
||||
operations. The default medium is `io.Local` (real filesystem), but tests can
|
||||
inject a mock medium via `SetMedium()`:
|
||||
|
||||
```go
|
||||
php.SetMedium(myMockMedium)
|
||||
defer php.SetMedium(io.Local)
|
||||
```
|
||||
|
||||
This allows the detection, Dockerfile generation, package management, and
|
||||
security check code to be tested without touching the real filesystem.
|
||||
284
docs/development.md
Normal file
284
docs/development.md
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
---
|
||||
title: Development
|
||||
description: How to build, test, and contribute to core/php.
|
||||
---
|
||||
|
||||
# Development
|
||||
|
||||
This guide covers building the `core-php` binary, running the test suite, and
|
||||
contributing to the project.
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Go 1.26+** (the module uses Go 1.26 features)
|
||||
- **CGO toolchain** (optional, required only for FrankenPHP embedding)
|
||||
- **Docker** (for container build/serve commands)
|
||||
- **mkcert** (optional, for local SSL certificates)
|
||||
- **PHP 8.3+** with Composer (for the PHP side of the project)
|
||||
- **Node.js 20+** (optional, for frontend asset building)
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
### Standard build (no CGO)
|
||||
|
||||
The default build produces a binary without FrankenPHP embedding. The embedded
|
||||
FrankenPHP commands (`serve:embedded`, `exec`) are excluded.
|
||||
|
||||
```bash
|
||||
# Using the core CLI
|
||||
core build
|
||||
|
||||
# Using go directly
|
||||
go build -trimpath -ldflags="-s -w" -o bin/core-php ./cmd/core-php
|
||||
```
|
||||
|
||||
Build configuration lives in `.core/build.yaml`:
|
||||
|
||||
```yaml
|
||||
project:
|
||||
name: core-php
|
||||
main: ./cmd/core-php
|
||||
binary: core-php
|
||||
|
||||
build:
|
||||
cgo: false
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s
|
||||
- -w
|
||||
|
||||
targets:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
- os: windows
|
||||
arch: amd64
|
||||
```
|
||||
|
||||
### CGO build (with FrankenPHP)
|
||||
|
||||
To include the embedded FrankenPHP handler, enable CGO:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=1 go build -trimpath -o bin/core-php ./cmd/core-php
|
||||
```
|
||||
|
||||
This pulls in `github.com/dunglas/frankenphp` and links against the PHP C
|
||||
library. The resulting binary can serve Laravel applications without a separate
|
||||
PHP installation.
|
||||
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# All Go tests
|
||||
core go test
|
||||
# -- or --
|
||||
go test ./...
|
||||
|
||||
# Single test
|
||||
core go test --run TestDetectServices
|
||||
# -- or --
|
||||
go test -run TestDetectServices ./...
|
||||
|
||||
# With race detector
|
||||
go test -race ./...
|
||||
|
||||
# Coverage
|
||||
core go cov
|
||||
core go cov --open # Opens HTML report
|
||||
```
|
||||
|
||||
### Test Conventions
|
||||
|
||||
Tests follow the `_Good`, `_Bad`, `_Ugly` suffix pattern from the Core
|
||||
framework:
|
||||
|
||||
- **`_Good`** -- happy path, expected to succeed.
|
||||
- **`_Bad`** -- expected error conditions, verifying error handling.
|
||||
- **`_Ugly`** -- edge cases, panics, unusual inputs.
|
||||
|
||||
### Mock Filesystem
|
||||
|
||||
Tests that exercise detection, Dockerfile generation, or package management use
|
||||
a mock `io.Medium` to avoid filesystem side effects:
|
||||
|
||||
```go
|
||||
func TestDetectServices_Good(t *testing.T) {
|
||||
mock := io.NewMockMedium()
|
||||
mock.WriteFile("artisan", "")
|
||||
mock.WriteFile("composer.json", `{"require":{"laravel/framework":"^11.0"}}`)
|
||||
mock.WriteFile("vite.config.js", "")
|
||||
|
||||
php.SetMedium(mock)
|
||||
defer php.SetMedium(io.Local)
|
||||
|
||||
services := php.DetectServices(".")
|
||||
assert.Contains(t, services, php.ServiceFrankenPHP)
|
||||
assert.Contains(t, services, php.ServiceVite)
|
||||
}
|
||||
```
|
||||
|
||||
### Test Files
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| `php_test.go` | DevServer lifecycle, service filtering, options |
|
||||
| `container_test.go` | Docker build, LinuxKit build, serve options |
|
||||
| `detect_test.go` | Project detection, service detection, package manager detection |
|
||||
| `dockerfile_test.go` | Dockerfile generation, PHP extension detection, version extraction |
|
||||
| `deploy_test.go` | Deployment flow, rollback, status checking |
|
||||
| `deploy_internal_test.go` | Internal deployment helpers |
|
||||
| `coolify_test.go` | Coolify API client (HTTP mocking) |
|
||||
| `packages_test.go` | Package linking, unlinking, listing |
|
||||
| `services_test.go` | Service interface, base service, start/stop |
|
||||
| `services_extended_test.go` | Extended service scenarios |
|
||||
| `ssl_test.go` | SSL certificate paths, existence checking |
|
||||
| `ssl_extended_test.go` | Extended SSL scenarios |
|
||||
|
||||
|
||||
## Code Quality
|
||||
|
||||
```bash
|
||||
# Format Go code
|
||||
core go fmt
|
||||
|
||||
# Vet
|
||||
core go vet
|
||||
|
||||
# Lint
|
||||
core go lint
|
||||
|
||||
# Full QA (fmt + vet + lint + test)
|
||||
core go qa
|
||||
|
||||
# Full QA with race detection, vulnerability scan, security checks
|
||||
core go qa full
|
||||
```
|
||||
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
forge.lthn.ai/core/php/
|
||||
cmd/
|
||||
core-php/
|
||||
main.go # Binary entry point
|
||||
locales/
|
||||
*.json # Internationalised CLI strings
|
||||
docker/
|
||||
docker-compose.prod.yml
|
||||
stubs/ # Template stubs
|
||||
config/ # PHP configuration templates
|
||||
src/ # PHP framework source (separate from Go code)
|
||||
tests/ # PHP tests
|
||||
docs/ # Documentation (this directory)
|
||||
.core/
|
||||
build.yaml # Build configuration
|
||||
*.go # Go source (flat layout, single package)
|
||||
```
|
||||
|
||||
The Go code uses a flat package layout -- all `.go` files are in the root
|
||||
`php` package. This keeps imports simple: `import php "forge.lthn.ai/core/php"`.
|
||||
|
||||
|
||||
## Adding a New Command
|
||||
|
||||
1. Create a new file `cmd_mycommand.go`.
|
||||
2. Define the registration function:
|
||||
|
||||
```go
|
||||
func addPHPMyCommand(parent *cli.Command) {
|
||||
cmd := &cli.Command{
|
||||
Use: "mycommand",
|
||||
Short: i18n.T("cmd.php.mycommand.short"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
// Implementation
|
||||
return nil
|
||||
},
|
||||
}
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
```
|
||||
|
||||
3. Register it in `cmd.go` inside both `AddPHPCommands` and
|
||||
`AddPHPRootCommands`:
|
||||
|
||||
```go
|
||||
addPHPMyCommand(phpCmd) // or root, for standalone binary
|
||||
```
|
||||
|
||||
4. Add the i18n key to `locales/en.json`.
|
||||
|
||||
|
||||
## Adding a New Service
|
||||
|
||||
1. Define the service struct in `services.go`, embedding `baseService`:
|
||||
|
||||
```go
|
||||
type MyService struct {
|
||||
baseService
|
||||
}
|
||||
|
||||
func NewMyService(dir string) *MyService {
|
||||
return &MyService{
|
||||
baseService: baseService{
|
||||
name: "MyService",
|
||||
port: 9999,
|
||||
dir: dir,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MyService) Start(ctx context.Context) error {
|
||||
return s.startProcess(ctx, "my-binary", []string{"--flag"}, nil)
|
||||
}
|
||||
|
||||
func (s *MyService) Stop() error {
|
||||
return s.stopProcess()
|
||||
}
|
||||
```
|
||||
|
||||
2. Add a `DetectedService` constant in `detect.go`:
|
||||
|
||||
```go
|
||||
const ServiceMyService DetectedService = "myservice"
|
||||
```
|
||||
|
||||
3. Add detection logic in `DetectServices()`.
|
||||
|
||||
4. Add a case in `DevServer.Start()` in `php.go`.
|
||||
|
||||
|
||||
## Internationalisation
|
||||
|
||||
All user-facing strings use `i18n.T()` keys rather than hardcoded English.
|
||||
Locale files live in `locales/` and are embedded via `//go:embed`:
|
||||
|
||||
```go
|
||||
//go:embed locales/*.json
|
||||
var localeFS embed.FS
|
||||
|
||||
func init() {
|
||||
i18n.RegisterLocales(localeFS, "locales")
|
||||
}
|
||||
```
|
||||
|
||||
When adding new commands or messages, add the corresponding keys to the locale
|
||||
files.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
- Follow UK English conventions: colour, organisation, centre.
|
||||
- All code is licenced under EUPL-1.2.
|
||||
- Run `core go qa` before submitting changes.
|
||||
- Use conventional commits: `type(scope): description`.
|
||||
- Include `Co-Authored-By: Virgil <virgil@lethean.io>` if pair-programming with
|
||||
the AI agent.
|
||||
180
docs/index.md
180
docs/index.md
|
|
@ -1,126 +1,96 @@
|
|||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: Core PHP Framework
|
||||
text: Modular Monolith for Laravel
|
||||
tagline: Event-driven architecture with lazy module loading and built-in multi-tenancy
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /guide/getting-started
|
||||
- theme: alt
|
||||
text: View on GitHub
|
||||
link: https://github.com/host-uk/core-php
|
||||
|
||||
features:
|
||||
- icon: ⚡️
|
||||
title: Event-Driven Modules
|
||||
details: Modules declare interest in lifecycle events and are only loaded when needed, reducing overhead for unused features.
|
||||
|
||||
- icon: 🔒
|
||||
title: Multi-Tenant Isolation
|
||||
details: Automatic workspace scoping for Eloquent models with strict mode enforcement prevents data leakage.
|
||||
|
||||
- icon: 🎯
|
||||
title: Actions Pattern
|
||||
details: Extract business logic into testable, reusable classes with automatic dependency injection.
|
||||
|
||||
- icon: 📝
|
||||
title: Activity Logging
|
||||
details: Built-in audit trails for model changes with minimal setup using Spatie Activity Log.
|
||||
|
||||
- icon: 🌱
|
||||
title: Seeder Auto-Discovery
|
||||
details: Automatic seeder ordering via priority and dependency attributes eliminates manual registration.
|
||||
|
||||
- icon: 🎨
|
||||
title: HLCRF Layouts
|
||||
details: Data-driven composable layouts with infinite nesting for flexible UI structures.
|
||||
|
||||
- icon: 🔐
|
||||
title: Security First
|
||||
details: Bouncer action gates, request whitelisting, and comprehensive input sanitization.
|
||||
|
||||
- icon: 🚀
|
||||
title: Production Ready
|
||||
details: Battle-tested in production with comprehensive test coverage and security audits.
|
||||
title: core/php
|
||||
description: Go-powered PHP/Laravel development toolkit with FrankenPHP embedding, service orchestration, CI pipelines, and Coolify deployment.
|
||||
---
|
||||
|
||||
# core/php
|
||||
|
||||
`forge.lthn.ai/core/php` is a Go module that provides a comprehensive CLI toolkit
|
||||
for PHP and Laravel development. It covers the full lifecycle: local development
|
||||
with service orchestration, code quality assurance, Docker/LinuxKit image building,
|
||||
and production deployment via the Coolify API.
|
||||
|
||||
The module also embeds FrankenPHP, allowing Laravel applications to be served
|
||||
from a single Go binary with Octane worker mode for sub-millisecond response
|
||||
times.
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
### As a standalone binary
|
||||
|
||||
```bash
|
||||
# Install via Composer
|
||||
composer require host-uk/core
|
||||
# Build the core-php binary
|
||||
core build
|
||||
# -- or --
|
||||
go build -o bin/core-php ./cmd/core-php
|
||||
|
||||
# Create a module
|
||||
php artisan make:mod Commerce
|
||||
# Start the Laravel development environment
|
||||
core-php dev
|
||||
|
||||
# Register lifecycle events
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||
{
|
||||
$event->routes(fn () => require __DIR__.'/Routes/web.php');
|
||||
}
|
||||
}
|
||||
# Run the CI pipeline
|
||||
core-php ci
|
||||
```
|
||||
|
||||
## Why Core PHP?
|
||||
### As a library in a Go application
|
||||
|
||||
Traditional Laravel applications grow into monoliths with tight coupling and unclear boundaries. Microservices add complexity you may not need. **Core PHP provides a middle ground**: a structured monolith with clear module boundaries, lazy loading, and the ability to extract services later if needed.
|
||||
```go
|
||||
import php "forge.lthn.ai/core/php"
|
||||
|
||||
### Key Benefits
|
||||
// Register commands under a "php" parent command
|
||||
cli.Main(
|
||||
cli.WithCommands("php", php.AddPHPRootCommands),
|
||||
)
|
||||
```
|
||||
|
||||
- **Reduced Complexity** - No network overhead, distributed tracing, or service mesh
|
||||
- **Clear Boundaries** - Modules have explicit dependencies via lifecycle events
|
||||
- **Performance** - Lazy loading means unused modules aren't loaded
|
||||
- **Flexibility** - Start monolithic, extract services when it makes sense
|
||||
- **Type Safety** - Full IDE support with no RPC serialization
|
||||
|
||||
## Packages
|
||||
## Package Layout
|
||||
|
||||
<div class="package-grid">
|
||||
| File / Directory | Purpose |
|
||||
|---|---|
|
||||
| `cmd/core-php/main.go` | Binary entry point -- registers all commands and calls `cli.Main()` |
|
||||
| `cmd.go` | Top-level command registration (`AddPHPCommands`, `AddPHPRootCommands`) |
|
||||
| `cmd_dev.go` | `dev`, `logs`, `stop`, `status`, `ssl` commands |
|
||||
| `cmd_build.go` | `build` (Docker/LinuxKit) and `serve` (production container) commands |
|
||||
| `cmd_ci.go` | `ci` command -- full QA pipeline with JSON/Markdown/SARIF output |
|
||||
| `cmd_deploy.go` | `deploy`, `deploy:status`, `deploy:rollback`, `deploy:list` commands |
|
||||
| `cmd_packages.go` | `packages link/unlink/update/list` commands |
|
||||
| `cmd_serve_frankenphp.go` | `serve:embedded` and `exec` commands (CGO only) |
|
||||
| `cmd_commands.go` | `AddCommands()` convenience wrapper |
|
||||
| `handler.go` | FrankenPHP HTTP handler (`Handler`) -- CGO build tag |
|
||||
| `bridge.go` | Native bridge -- localhost HTTP API for PHP-to-Go calls |
|
||||
| `php.go` | `DevServer` -- multi-service orchestration (start, stop, logs, status) |
|
||||
| `services.go` | `Service` interface and concrete implementations (FrankenPHP, Vite, Horizon, Reverb, Redis) |
|
||||
| `detect.go` | Project detection: Laravel, FrankenPHP, Vite, Horizon, Reverb, Redis, package managers |
|
||||
| `dockerfile.go` | Auto-generated Dockerfiles from `composer.json` analysis |
|
||||
| `container.go` | `DockerBuildOptions`, `LinuxKitBuildOptions`, `ServeOptions`, and build/serve functions |
|
||||
| `deploy.go` | Deployment orchestration -- `Deploy()`, `Rollback()`, `DeployStatus()` |
|
||||
| `coolify.go` | Coolify API client (`CoolifyClient`) with deploy, rollback, status, and list operations |
|
||||
| `quality.go` | QA tools: Pint, PHPStan/Larastan, Psalm, Rector, Infection, security checks, audit |
|
||||
| `testing.go` | Test runner detection (Pest/PHPUnit) and execution |
|
||||
| `ssl.go` | SSL certificate management via mkcert |
|
||||
| `packages.go` | Composer path repository management (link/unlink local packages) |
|
||||
| `env.go` | Runtime environment setup for embedded apps (CGO only) |
|
||||
| `extract.go` | `Extract()` -- copies an `embed.FS` Laravel app to a temporary directory |
|
||||
| `workspace.go` | Workspace configuration (`.core/workspace.yaml`) for multi-package repos |
|
||||
| `i18n.go` | Locale registration for internationalised CLI strings |
|
||||
| `services_unix.go` | Unix process group management (SIGTERM/SIGKILL) |
|
||||
| `services_windows.go` | Windows process termination |
|
||||
| `.core/build.yaml` | Build configuration for `core build` |
|
||||
|
||||
### [Core](/packages/core)
|
||||
Event-driven architecture, module system, actions pattern, and multi-tenancy.
|
||||
|
||||
### [Admin](/packages/admin)
|
||||
Livewire-powered admin panel with global search and service management.
|
||||
## Dependencies
|
||||
|
||||
### [API](/packages/api)
|
||||
REST API with OpenAPI docs, rate limiting, webhook signing, and secure keys.
|
||||
| Module | Role |
|
||||
|---|---|
|
||||
| `forge.lthn.ai/core/cli` | CLI framework (Cobra wrapper, TUI styles, output helpers) |
|
||||
| `forge.lthn.ai/core/go-i18n` | Internationalisation for command descriptions and messages |
|
||||
| `forge.lthn.ai/core/go-io` | Filesystem abstraction (`Medium` interface) for testability |
|
||||
| `forge.lthn.ai/core/go-process` | Process management utilities |
|
||||
| `github.com/dunglas/frankenphp` | FrankenPHP embedding (CGO, optional) |
|
||||
| `gopkg.in/yaml.v3` | YAML parsing for workspace configuration |
|
||||
|
||||
### [MCP](/packages/mcp)
|
||||
Model Context Protocol tools for AI integrations with analytics and security.
|
||||
|
||||
</div>
|
||||
## Licence
|
||||
|
||||
## Community
|
||||
|
||||
- **GitHub Discussions** - Ask questions and share ideas
|
||||
- **Issue Tracker** - Report bugs and request features
|
||||
- **Contributing** - See our [contributing guide](/contributing)
|
||||
|
||||
<style>
|
||||
.package-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.package-grid > div {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.package-grid h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
EUPL-1.2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue