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)
# 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.phpCRITICAL: 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.
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).
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
matchexpressions (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 usesarray()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.
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.
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()(NOTcamelCase()) - 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.
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.
- 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.
?Typenullable syntax is fine (PHP 7.1+), butType|nullunion 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
\nin expected test output on disk. The.gitattributesfile normalizes fixtures to LF, but constructing expected output in test code should use"\n", notPHP_EOL. - Do not edit files in
vendor-patched/. These are manually vendored and patched third-party code. Changes there require careful consideration. - Root
composer.jsonis generated. Don't edit it directly for dependencies or autoloading — edit the component'scomposer.jsonand runphp 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.
- Tests live in
components/<Name>/Tests/ - Test classes extend
PHPUnit\Framework\TestCase - Use
@beforeand@afterannotations for setup/teardown (notsetUp/tearDownmethods, following the existing pattern) - Test fixtures go in
components/<Name>/Tests/fixtures/ - Tests MUST pass on PHP 7.2 — use
yoast/phpunit-polyfillsfor compatibility between PHPUnit versions
To verify changes work end-to-end (not just unit tests).
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.
# 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 sandboxThe 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.
The repo includes a Dev Containers 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.