php/docs/development.md

285 lines
6.5 KiB
Markdown
Raw Normal View History

---
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.