Pair Programming With an Agent on Legacy PHP
A PHP discount bug small enough to review
My rule for pairing with an agent on legacy code: establish observable behavior before asking for edits. Otherwise, a two-line bug becomes an unsolicited framework migration. This walkthrough builds a tiny, reproducible legacy fixture, adds a regression test, and asks an agent for one surgical patch.
The bug is familiar: empty() treats an explicit zero discount as missing input. Missing discounts should default to ten percent; zero should mean no discount. We will preserve the existing function signature and avoid dependencies in the PHP application.
1. Create an isolated legacy PHP fixture
These commands assume Bash, Git, PHP CLI, and Python 3 with venv support. Everything happens in a temporary directory, not your production checkout.
workdir=$(mktemp -d)
Run each command paragraph separately. The printf arguments below become individual lines in their destination files.
cd "$workdir"; git init; mkdir -p src tests
printf '%s\n' '<?php' 'function discounted_total($subtotal, $percent = null) {' ' if (empty($percent)) {' ' $percent = 10;' ' }' ' return round($subtotal * (1 - $percent / 100), 2);' '}' > src/discount.php
git add src/discount.php; git -c user.name='Dev Example' -c user.email='[email protected]' commit -m 'Capture legacy discount behavior'
In a real repository, create a branch instead and inspect composer.json, the supported PHP versions, and existing tests. Do not let your agent silently raise the runtime requirement.
2. Capture zero, null, and omitted discounts
A standalone runner avoids introducing a test framework just to demonstrate the fix. Explicit checks also avoid PHP assertions, which may be disabled.
printf '%s\n' '<?php' 'require __DIR__ . "/../src/discount.php";' '$cases = array(' ' array("omitted", discounted_total(100), 90.0),' ' array("null", discounted_total(100, null), 90.0),' ' array("zero", discounted_total(100, 0), 100.0),' ' array("ordinary", discounted_total(100, 20), 80.0),' ');' '$failed = false;' 'foreach ($cases as $case) {' ' if (abs($case[1] - $case[2]) > 0.00001) {' ' fwrite(STDERR, $case[0] . ": unexpected total\n");' ' $failed = true;' ' }' '}' 'exit($failed ? 1 : 0);' > tests/run.php
php -l src/discount.php && php -l tests/run.php
php tests/run.php
Expect a nonzero exit and “zero: unexpected total.” Stop if you see a syntax error instead. That is not evidence that the regression test caught our bug.
3. Connect a terminal assistant through Fluxo
Fluxo gives this workflow one key and one base URL, https://api.fluxorelay.com, across Claude, GPT, Gemini, DeepSeek, Kimi, GLM, Qwen, Grok, and more. It works with Claude Code, Codex, Gemini CLI, and any OpenAI SDK. Here, a small SDK script keeps the request visible and gives the assistant no shell access.
If you want to follow along through this relay, create a Fluxo account and copy your API key.
python3 -m venv .venv; . .venv/bin/activate; python -m pip install openai
read -rsp 'Fluxo API key: ' FLUXO_API_KEY; printf '\n'; export FLUXO_API_KEY
read -rp 'Model identifier from your Fluxo dashboard: ' FLUXO_MODEL; export FLUXO_MODEL
For example, choose Claude Sonnet 5.5; copy its API identifier rather than guessing it from the display name. Fluxo bills real tokens, including cache reads and writes, without a monthly fee. Invited users' payments also earn their inviter a referral share.
4. Request a patch, not a rewrite
Read files before sending them: even a tiny legacy module can contain credentials or customer data. This fixture contains neither.
python -c 'import os,pathlib; from openai import OpenAI; c=OpenAI(api_key=os.environ["FLUXO_API_KEY"],base_url="https://api.fluxorelay.com"); prompt="Return only a git-compatible unified diff, without fences. Modify only src/discount.php. Replace the empty check with an explicit null check. Preserve signature and formatting. Do not change tests.\n"; prompt += "\n".join(p+"\n"+pathlib.Path(p).read_text() for p in ["src/discount.php","tests/run.php"]); r=c.chat.completions.create(model=os.environ["FLUXO_MODEL"],messages=[{"role":"user","content":prompt}]); print(r.choices[0].message.content or "")' > proposed.patch
5. Review the PHP diff before applying it
cat proposed.patch — The intended change is only empty($percent) becoming $percent === null. Reject unrelated edits. A model response is untrusted input, not an executable instruction.
git apply --check proposed.patch && git apply proposed.patch
php -l src/discount.php && php tests/run.php && git diff --check
git diff -- src/discount.php; git status --short
Now zero stays zero, while omitted and null inputs retain their default. In the real codebase, run the complete suite and inspect callers before committing: empty strings and false previously received the default too. Whether those inputs should remain supported is a separate compatibility decision, not something the agent should decide silently.