Skip to content

devlog/agent-loops/pair-programming-with-an-agent-on-legacy-php-6.md

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.

Next in the buffer

Sign up in a minute and get credit to try every model mentioned here. Sign up →