Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ jobs:
- name: Install PHP with extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
php-version: '8.4'
extensions: bcmath, imagick

- name: Install composer dependencies
uses: php-actions/composer@v6
with:
interaction: no
dev: yes
php_version: "8.3"
php_version: "8.4"
version: 2
args: --optimize-autoloader --no-scripts --ignore-platform-reqs

Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
- name: Install PHP with extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
php-version: '8.4'
extensions: bcmath, gd, imagick

- name: Cache composer dependencies
Expand All @@ -96,7 +96,7 @@ jobs:
- name: Install PHP with extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
php-version: '8.4'
extensions: bcmath, gd, imagick

- name: Cache composer dependencies
Expand All @@ -121,7 +121,7 @@ jobs:
- name: Install PHP with extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
php-version: '8.4'
extensions: bcmath, gd, imagick

- name: Cache composer dependencies
Expand All @@ -146,7 +146,7 @@ jobs:
- name: Install PHP with extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
php-version: '8.4'
extensions: bcmath, gd, imagick

- name: Cache composer dependencies
Expand All @@ -161,6 +161,6 @@ jobs:
with:
interaction: no
dev: yes
php_version: "8.3"
php_version: "8.4"
version: 2
command: audit
2 changes: 1 addition & 1 deletion .github/workflows/create-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
with:
interaction: no
dev: no
php_version: "8.3"
php_version: "8.4"
version: 2
container_workdir: '/app/build'
args: --optimize-autoloader --no-scripts --ignore-platform-reqs
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy-lambda.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
php-version: '8.4'
coverage: none
# Install the Bref CLI as a global tool
tools: bref/cli
Expand All @@ -32,7 +32,7 @@ jobs:
with:
interaction: no
dev: no
php_version: "8.3"
php_version: "8.4"
version: 2
args: --optimize-autoloader --no-scripts --ignore-platform-reqs

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.cache/
/.claude/settings.local.json
/.serverless/
/build/
/vendor/
Expand Down
89 changes: 89 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

A lightweight PHP CDN for dynamic image processing and serving, deployed as AWS Lambda via the Bref framework. It fetches source images from remote URLs, applies transformations (resize, compress, WebP conversion, watermarking) using ImageMagick, caches results in local or S3 storage, and returns them with proper HTTP cache headers.

## Commands

All commands run inside Docker via `docker/exec`:

```bash
make install # First-time setup
make start # Start Docker containers
make stop # Stop containers

make test # Run all checks (phpcs, phpstan, phpmd, phpunit, security audit)
make test-phpcs # Code style (PSR-12)
make fix-phpcs # Auto-fix style violations
make test-phpstan # Static analysis
make test-phpunit # PHPUnit with coverage (95% threshold enforced)

# Run a single test file or method
docker/exec vendor/bin/phpunit tests/Cdn/CdnTest.php
docker/exec vendor/bin/phpunit --filter methodName tests/Cdn/CdnTest.php

make build # Build production ZIP → build/latest.zip
make deploy # Deploy to dev on AWS
make deploy-prod # Deploy to production on AWS
```

## Architecture

**Entry point:** `public/index.php` → bootstraps `Container` → calls `Cdn::handleRequest()`

**Request flow:**
1. `Cdn` validates the request (GET only, domain in `ALLOWED_DOMAINS`)
2. `UriDecoder` parses the image source URL and query parameters into a `QueryParams` DTO
3. `PathProcessor` generates a deterministic cache path from the params
4. If the cached file doesn't exist, `Storage` fetches the original via `UrlFilesystemAdapter` (Flysystem adapter wrapping Symfony's HTTP client)
5. `ImageProcessor` applies transformations (Imagick): resize, compression, WebP conversion, watermark
6. Result is saved to storage (local filesystem or S3 via Flysystem)
7. `Cache` returns a Symfony `Response` with `Cache-Control`, `ETag`, and `Last-Modified` headers

**Key classes:**
- `src/Cdn.php` — orchestrates the full request lifecycle
- `src/Container.php` — custom DI container; wires all services from env vars
- `src/Storage/Storage.php` — Flysystem abstraction (local or S3)
- `src/Processor/ImageProcessor.php` — ImageMagick transformations
- `src/Processor/PathProcessor.php` — cache key generation
- `src/Decoder/UriDecoder.php` — URL and query param parsing
- `src/Cache/Cache.php` — HTTP response and cache headers
- `src/Dto/QueryParams.php` — immutable DTO for image transformation parameters

**Exception-driven validation:** Domain/URI/extension/file errors are thrown as typed exceptions (`NotAllowedDomain`, `InvalidUri`, `FileNotFound`, etc.) and caught in `Cdn` to return appropriate HTTP responses.

## Testing

Tests mirror `src/` structure under `tests/`. The base `TestCase` class provides:
- In-memory Flysystem filesystem (no disk I/O)
- Mocked `UrlFilesystemAdapter` for remote image fetching
- `getContainer()` / `getQueryParameters()` / `getTestImageContent()` helpers

`ContainerConfig.php` and `UrlFilesystemAdapter.php` are excluded from coverage requirements.

## Configuration

The app is configured entirely via environment variables (see `.env`):

| Variable | Purpose |
|---------------------------------------------------------------------------|---------------------------------------------------|
| `ALLOWED_DOMAINS` | Comma-separated list of authorized source domains |
| `DOMAINS_ALIASES` | Domain aliases, e.g. `example.com/secret=ex` |
| `STORAGE_DRIVER` | `local` or `s3` |
| `S3_BUCKET`, `S3_ENDPOINT`, `S3_REGION`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` | S3 config |
| `IMAGE_COMPRESSION` | JPEG/WebP quality (0–100) |
| `CACHE_TTL` | HTTP cache TTL in seconds (default: 1 year) |
| `LOG_LEVEL` | PSR-3 log level |

In production, secrets are pulled from AWS SSM Parameter Store (see `serverless.yml`). Local dev uses MinIO (S3-compatible) on port 9001.

## Deployment

Deployed to AWS Lambda (PHP 8.4 FPM, Bref framework) with:
- 2048 MB memory, 28-second timeout
- Imagick Lambda layer
- Warm-up ping every 5 minutes via EventBridge
- CI/CD via GitHub Actions (`.github/workflows/`)
26 changes: 13 additions & 13 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,32 @@
"description": "A CDN written in PHP",
"type": "project",
"require": {
"php": ">=8.3",
"php": ">=8.4",
"ext-imagick": "*",
"babeuloula/phpunit-coverage-checker": "^1.0",
"bref/bref": "^2.3",
"bref/extra-php-extensions": "^1.7",
"bref/logger": "^1.0",
"bref/bref": "^3.0",
"bref/extra-php-extensions": "^3.0",
"bref/logger": "^2.0",
"league/flysystem": "^3.29",
"league/flysystem-aws-s3-v3": "^3.0",
"league/glide": "^2.3",
"symfony/dotenv": "^7.2",
"symfony/error-handler": "^7.2",
"symfony/filesystem": "^7.2",
"symfony/http-foundation": "^7.2",
"symfony/runtime": "^7.2",
"symfony/string": "^7.2",
"symfony/dotenv": "^7.4",
"symfony/error-handler": "^7.4",
"symfony/filesystem": "^7.4",
"symfony/http-foundation": "^7.4",
"symfony/runtime": "^7.4",
"symfony/string": "^7.4",
"symfony/translation-contracts": "^3.5"
},
"require-dev": {
"babeuloula/phpcs": "^1.5",
"league/flysystem-memory": "^3.29",
"league/flysystem-memory": "^3.31",
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^11.5",
"symfony/var-dumper": "^7.2"
"phpunit/phpunit": "^13",
"symfony/var-dumper": "^8.0"
},
"license": "MIT",
"autoload": {
Expand Down
Loading
Loading