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
16 changes: 16 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"Bash(composer install:*)",
"Bash(composer test:*)",
"Bash(composer lint:*)",
"Bash(composer lint-fix:*)",
"Bash(vendor/bin/phpunit:*)",
"Bash(vendor/bin/phpcs:*)",
"Bash(vendor/bin/phpcbf:*)",
"Bash(php bin/regenerate_composer.json.php:*)",
"Bash(docker compose build:*)",
"Bash(docker compose run --rm sandbox:*)"
]
}
}
25 changes: 25 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "PHP Toolkit",
"build": {
"dockerfile": "../Dockerfile"
},
"postCreateCommand": "composer install --no-interaction --no-progress",
"customizations": {
"vscode": {
"extensions": [
"bmewburn.vscode-intelephense-client",
"shevaua.phpcs"
],
"settings": {
"php.validate.executablePath": "/usr/local/bin/php",
"phpcs.executablePath": "vendor/bin/phpcs",
"editor.insertSpaces": false,
"editor.tabSize": 4
}
}
},
"containerEnv": {
"LC_ALL": "en_US.UTF-8",
"LANG": "en_US.UTF-8"
}
}
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
vendor/
node_modules/
dist/
untracked/
.idea/
.vscode/
.cursor/
.claude/
.git/
*.phar
215 changes: 215 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# AGENTS.md – PHP Toolkit

## What this is

A monorepo of standalone PHP libraries for WordPress and general PHP projects.
Each component lives in `components/<Name>/` and is independently publishable
to Packagist under `wp-php-toolkit/<name>`. Some components are also bundled
into WordPress plugins under `plugins/`.

Upstream: https://github.com/WordPress/php-toolkit
Branch: `trunk` (not `main`)

## Commands

```bash
# Install dependencies (required before anything else)
composer install

# Run all tests – two suites: fast component tests first, slow Blueprints tests second
composer test

# Run tests for a single component
vendor/bin/phpunit components/Zip/Tests/

# Run a single test class
vendor/bin/phpunit components/Zip/Tests/ZipEncoderTest.php

# Lint
composer lint

# Auto-fix lint errors
composer lint-fix

# Regenerate root composer.json after changing a component's composer.json
php bin/regenerate_composer.json.php
```

CRITICAL: After any code change, run lint AND the relevant component tests at
minimum. If your change touches shared code (Encoding, Polyfill, ByteStream,
Filesystem), run the full test suite.

## Component structure

Every component follows the same layout:

```
components/<Name>/
class-<name>.php # WordPress naming: class-lowercase-with-dashes.php
functions.php # Optional standalone functions
composer.json # Per-component, with inter-component dependencies
Tests/
<Name>Test.php # PHPUnit test classes (no WordPress naming here)
fixtures/ # Test data
vendor-patched/ # Vendored dependencies (rare, checked in)
```

Namespace: `WordPress\<Name>` (e.g., `WordPress\Zip\ZipEncoder`).
Autoloading: classmap-based, NOT PSR-4 (despite using namespaces).

## PHP requirements – CRITICAL

This project MUST run on PHP 7.2 through 8.3. This means:

- No typed properties (PHP 7.4+)
- No union types (PHP 8.0+)
- No named arguments (PHP 8.0+)
- No enums (PHP 8.1+)
- No `readonly` (PHP 8.1+)
- No `match` expressions (PHP 8.0+)
- No arrow functions `fn()` (PHP 7.4+)
- No null coalescing assignment `??=` (PHP 7.4+)
- Use `array()` or `[]` — both are fine, but existing code mostly uses `array()`
for multi-line and `[]` for inline

If you aren't sure whether a feature is available in PHP 7.2, don't use it.
The CI matrix tests PHP 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, and 8.3 on Linux,
macOS, and Windows.

## Zero external dependencies

This is a hard constraint. Components MUST NOT require PHP extensions beyond
`json` and `mbstring`. No `libxml2`, no `curl`, no `libzip`, no `sqlite3`
(sqlite3 is a dev-only dependency for tests). No Composer packages beyond
other `wp-php-toolkit/*` components, except rare vendored exceptions already
checked into `vendor-patched/`.

If you need functionality provided by a PHP extension, implement it in pure PHP.
That's the whole point of this project.

## Coding conventions

WordPress coding standards with strategic divergences:

- **Namespaces**: all components use `namespace WordPress\<Component>`
- **File naming**: `class-lowercase-with-dashes.php` (WordPress convention)
- **Spacing**: tabs for indentation, spaces inside parentheses per WordPress
style: `function_name( $arg1, $arg2 )`
- **Variables**: `$snake_case` (NOT `$camelCase`)
- **Methods and functions**: `snake_case()` (NOT `camelCase()`)
- **Classes**: `PascalCase`
- **Constants**: `UPPER_SNAKE_CASE`
- **Test classes**: `PascalCaseTest extends TestCase` (PHPUnit convention, not
WordPress naming)

The linter enforces most of this. When in doubt, match the surrounding code.

## Architecture – the single-class philosophy

The architectural role model is `WP_HTML_Processor` — a single class that does
one thing well. Avoid deep class hierarchies, abstract factories, or patterns
like AbstractSingletonFactoryProxy. When a component needs multiple classes,
that's fine, but keep the API surface small and the class count low.

Components are designed to be re-entrant: they can start, stop, and resume
processing. Many operate as streaming processors where you feed data in and
get results incrementally.

## Common pitfalls

- **Do not add `declare(strict_types=1)`** to component source files. The test
files and config files use it, but the library code deliberately doesn't
because WordPress core doesn't.
- **Do not add type declarations** on parameters or return types that would
break PHP 7.2 compatibility. `?Type` nullable syntax is fine (PHP 7.1+),
but `Type|null` union syntax is not (PHP 8.0+).
- **Do not restructure autoloading to PSR-4.** The classmap approach is
intentional — it matches WordPress core conventions and avoids directory
depth requirements.
- **Do not add Composer dependencies.** If you think you need a package,
implement the functionality yourself or discuss it first.
- **Do not use `\n` in expected test output on disk.** The `.gitattributes`
file normalizes fixtures to LF, but constructing expected output in test
code should use `"\n"`, not `PHP_EOL`.
- **Do not edit files in `vendor-patched/`.** These are manually vendored and
patched third-party code. Changes there require careful consideration.
- **Root `composer.json` is generated.** Don't edit it directly for
dependencies or autoloading — edit the component's `composer.json` and run
`php bin/regenerate_composer.json.php`.
- **Paths MUST use forward slashes** even on Windows. The Filesystem component
deliberately uses Unix-style separators everywhere. See README.md for details.
- **Keep tests fast.** The Blueprints test suite is intentionally separated
because it's slow. Component tests should be quick and self-contained.
- **Do not modify the `plugins/` directory** unless specifically asked. Plugins
are built from components and have their own build step.

## Test conventions

- Tests live in `components/<Name>/Tests/`
- Test classes extend `PHPUnit\Framework\TestCase`
- Use `@before` and `@after` annotations for setup/teardown (not `setUp`/
`tearDown` methods, following the existing pattern)
- Test fixtures go in `components/<Name>/Tests/fixtures/`
- Tests MUST pass on PHP 7.2 — use `yoast/phpunit-polyfills` for compatibility
between PHPUnit versions

## Verification

To verify changes work end-to-end (not just unit tests).

## Sandbox

A Docker-based sandbox isolates agent-built code from the host. The container
runs with no network access, a read-only root filesystem, and all Linux
capabilities dropped — code can only write to the mounted project directory
and `/tmp`.

```bash
# Build the sandbox image (first time only, or after Dockerfile changes)
docker compose build

# Run the full test suite inside the sandbox
docker compose run --rm sandbox vendor/bin/phpunit -c phpunit.xml

# Run tests for a single component
docker compose run --rm sandbox vendor/bin/phpunit components/Zip/Tests/

# Run a PHP script
docker compose run --rm sandbox php my-script.php

# Run the linter
docker compose run --rm sandbox vendor/bin/phpcs -d memory_limit=1G .

# Auto-fix lint errors
docker compose run --rm sandbox vendor/bin/phpcbf -d memory_limit=1G .

# Open a shell for interactive debugging
docker compose run --rm sandbox
```

The container uses PHP 8.1 (matching the lint CI). Source files are bind-mounted
so edits on the host are immediately visible inside the container.

Without Docker, the test suite still works directly on the host — it's fully
self-contained (no database, no web server, no external services). Tests create
temp files in `sys_get_temp_dir()` and clean up after themselves.

For WordPress-level integration testing, use the WordPress Playground skill
which spins up an isolated WordPress instance in-memory.

### Dev Containers

The repo includes a [Dev Containers](https://containers.dev/) spec in
`.devcontainer/devcontainer.json`. This provides a consistent, pre-configured
development environment that works in VS Code, GitHub Codespaces, and any
editor that supports the Dev Containers standard.

To use it:

- **VS Code**: Install the Dev Containers extension, then "Reopen in Container"
- **GitHub Codespaces**: Click "Code > Codespaces > New codespace" on GitHub
- **CLI**: `devcontainer up --workspace-folder .`

The container is built from the same `Dockerfile` used by the sandbox. Composer
dependencies are installed automatically on creation. PHP IntelliSense and
PHPCS linting are pre-configured.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
FROM php:8.1-cli

# Match CI locale settings
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8

# Install system dependencies for PHP extensions and locale
RUN apt-get update && apt-get install -y --no-install-recommends \
libzip-dev \
libsqlite3-dev \
libonig-dev \
locales \
unzip \
git \
&& sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen \
&& locale-gen \
&& docker-php-ext-install zip pdo pdo_sqlite mbstring \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /app

# Install dependencies first (cached layer for faster rebuilds)
COPY composer.json composer.lock* ./
RUN composer install --no-interaction --no-progress --optimize-autoloader 2>/dev/null || true

# Copy the rest of the source
COPY . .

# Re-run install in case the cached layer was stale
RUN composer install --no-interaction --no-progress --optimize-autoloader

CMD ["bash"]
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,51 @@ For convenience, a standalone Blueprints runner and other tools from this reposi

### Development

#### Testing
#### Dev Container

A [Dev Container](https://containers.dev/) spec is included in `.devcontainer/`.
It provides PHP 8.1 with all the required extensions, Composer, and editor
tooling pre-configured. Works with VS Code ("Reopen in Container"), GitHub
Codespaces, or `devcontainer up --workspace-folder .` from the CLI.

#### Docker sandbox

For running tests and lints in an isolated container without a full dev
environment, use the Docker Compose sandbox:

```sh
# Build once
docker compose build

# Run all tests
docker compose run --rm sandbox vendor/bin/phpunit -c phpunit.xml

# Run tests for one component
docker compose run --rm sandbox vendor/bin/phpunit components/Zip/Tests/

To run the PHPUnit test suite, run:
# Run a PHP script
docker compose run --rm sandbox php my-script.php

# Lint
docker compose run --rm sandbox vendor/bin/phpcs -d memory_limit=1G .
```

The sandbox has no network access, a read-only root filesystem, and all Linux
capabilities dropped — the only writable areas are the project mount and `/tmp`.

#### Without Docker

The test suite works directly on the host too — no database, no web server,
no external services needed. You just need PHP 7.2+ with `json` and `mbstring`.

#### Testing

```sh
composer test
```

#### Linting

To run the PHP_CodeSniffer linting suite, run:

```sh
composer lint
```
Expand Down
22 changes: 22 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
services:
sandbox:
build: .
volumes:
- .:/app
# Keep container's vendor dir separate so host PHP version
# doesn't conflict with container's compiled extensions.
- vendor_data:/app/vendor
working_dir: /app
# Drop all capabilities, read-only root filesystem.
# The agent can still write to /app (mounted volume) and /tmp.
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
network_mode: none

volumes:
vendor_data: