hampel / rig
Exercise a package by hand, for real - a harness for the things a test suite must not do
Requires
- php: >=8.3
Requires (Dev)
- laravel/pint: ^1.30
- phpstan/phpstan: ^2.1.22
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Exercise a package by hand, for real.
A rig is a small harness you install into a package under development so you can drive its code the way a consumer would — and watch what actually happens. Exercises are plain PHP scripts that live with the package. They are meant to have side effects: post the message, call the API, write the file. That is the point.
This is not a test runner, and it is not trying to become one. A test asserts, runs unattended, and reports a verdict — so it must not touch anything real. A rig does the opposite: it does the real thing and shows you the result, and you decide what it means. Both are worth having, and neither substitutes for the other.
Why you might want one
The sharpest case is a package that wraps somebody else's API. It is a pile of assumptions — endpoint paths, required fields, response shapes, what an error looks like — every one of them true on the day it was written. Then the remote API moves, nothing local changes, and the package is quietly wrong.
A test suite cannot find that, and a green one is what keeps it invisible: the tests pass
because they mock the HTTP client, so the mock encodes exactly the same assumption the code
does. Both sides stay agreed with each other and disagreed with reality. Static analysis is no
better — a path spelled custom_fields/ where the vendor now documents custom_field/ is not a
type error, it is a claim about a system PHPStan cannot see. Only a real call settles it, and
the answer often is not an exception but a number that looks slightly off: a filter the API has
quietly stopped honouring returns 200 OK and every record in the account. You cannot assert
your way to is this count suspiciously equal to that one without already knowing the answer
you are looking for. A person reading two numbers can.
Install
composer require --dev hampel/rig
It has no dependencies. Nothing but PHP is added to your vendor directory, which matters: a harness that dragged a framework in would put hundreds of classes where your package's static analysis can see them, and your package would stop being able to tell you it used something it never declared.
Use
Write exercises as .php files in a harness/ directory in your package:
<?php /** * Exercise: post a notification and show what came back. * * @var Hampel\Rig\Io $io */ use Acme\Notifier\Webhook; $io->title('acme/notifier · post'); $webhook = new Webhook(getenv('WEBHOOK_URL')); $io->attempt('post a message', fn () => $webhook->post('rig was here'));
Then:
vendor/bin/rig # list the exercises this package offers vendor/bin/rig post # run one vendor/bin/rig post --php=php8.4 # run it on another PHP
Each exercise is handed one variable, $io. Everything else is ordinary PHP — the package
is autoloaded, so use it however a consumer would.
What $io offers
title($text) |
a heading, with a rule under it |
info($text) |
context, rather than an outcome |
success($text), warn($text), error($text) |
one line, in green, amber or red |
line($text = '') |
a bare line — call it with nothing for a blank one |
value($label, $value) |
a label and a value, aligned into a column |
values([$label => $value, ...]) |
a group of them, aligned to the group's own widest label |
attempt($description, $callback) |
run something, and report what returned or what was thrown |
stringify($value) |
any value as the single line it would be printed as |
isDecorated() |
whether output is coloured |
Nothing here asserts anything, and attempt() is the reason to say so twice: a throwable is
reported as an outcome, not a failure, because the error paths are usually what you came to
look at. The exit status is the exercise's own.
value() aligns labels shorter than 14 characters into a fixed column. A longer label keeps
its line but takes a single space instead, so a run of them lands at ragged indents — which
is worst for exactly the two-numbers-side-by-side case above. values() is the way out: its
column is the widest label in the call, and never narrower than value()'s, so a group of
short labels reads identically to the value() calls around it.
$io->values([ 'unfiltered total_results' => 26, 'filtered total_results' => 24, ]);
Options
| option | |
|---|---|
--in-process |
run in the rig's own process rather than a fresh one |
--php=<binary> |
run the exercise on another PHP binary |
--package=<path> |
exercise a package in another directory |
--harness=<dir> |
where the exercises are; default harness |
--env=<file> |
environment file to load; default .env, relative to the package unless absolute |
--agent-may-load-env |
load it even in an agent session; see Credentials |
--list |
list exercises even when one is named |
--version, --help |
Credentials
Exercises that do real things need real credentials. rig reads a .env beside the
package before running, into getenv():
WEBHOOK_URL=https://example.test/services/T000/B000/XXXX
Values already set in the environment win, so a single run can override the file without editing it:
WEBHOOK_URL=https://example.test/other vendor/bin/rig post
Agent sessions
rig does not read the environment file when CLAUDECODE is set — the variable Claude
Code exports into every shell it opens. It says so, and runs the exercise anyway, on
whatever defaults the exercise's own code chooses.
The file belongs to whoever owns the credentials in it, and it tends to say do the real thing, because that is how they run their own exercises. An agent asked to look at some output inherits that decision without having made it — which is how a harness comes to send real mail nobody asked for. Withholding the file is what lets an exercise's own safe default apply.
--agent-may-load-env loads it regardless, for an agent that has been asked to do the
real thing.
This covers the file rig loads and nothing else. An exercise that reads a credential by
itself, or a key committed to configuration, is untouched by it. With the variable absent
nothing changes, so running exercises by hand behaves exactly as before.
Ignore environment files when you add harness/, whether or not anything needs
credentials yet. A harness that starts out driving pure code grows an API call eventually,
and by then the file is already tracked — the leak happens at the commit that adds the
first real value, not at the one that adds the rule. In .gitignore:
.env
.env.*
!.env.example
None of those are anchored with a leading /, on purpose: that would cover the repository
root only, and --env= resolves relative to the package, so an environment file can
legitimately sit in a subdirectory. .env.* catches the variants the option invites —
.env.staging, .env.local — and the last line keeps a committed .env.example
documenting which variables an exercise expects.
This holds while environment files are named .env or .env.<something>. --env= will
load any file you point it at, and one named otherwise is not covered.
The parser is deliberately minimal — KEY=VALUE, # comments, optional surrounding
quotes, nothing else. An exercise is ordinary PHP and can read configuration however it
likes if it needs more.
Keeping the harness out of the dist archive
Exercises belong in the repository and not in the tarball your consumers install. In
.gitattributes:
/harness export-ignore
Two pieces of repository metadata, then — this one, and the environment-file block in
.gitignore. Add both when you create harness/, so neither is waiting on the day it
turns out to matter.
Why a fresh process by default
Each exercise runs in its own PHP process. Not for speed — for three properties worth having:
- a fatal error in an exercise cannot take the rig down with it, so you see the error
- the exit status is the exercise's, not the rig's
--phpcan point at any PHP binary, which is how you drive the same exercise at both ends of the range your package claims to support
--in-process opts out, for when you want the exercise inside the current process — an
attached debugger, or an exercise that opens a REPL.
Requirements
PHP 8.3 or later. No dependencies.
Licence
MIT. See LICENSE.md.