Thaura: Porting Legacy PHP5 and PHP7 Project to PHP8 Part 1

This document describes how I’m porting a legacy PHP codebase to PHP8.5. The security crisis of October 2026 required porting everything on a legacy website to PHP8, the only version that’s getting updated now. This document is in flux and will change as the port progresses.

The legacy code was running on two containers, behind a nginx reverse proxy, that directed most of the traffic to he PHP7 version of the code, but ran some very old code in PHP5.

The goal was to port code to PHP8, and implement a new app stack that’s nginx + php-fpm + php8.5.

The TL;DR:

  • Lean on code linters first.
  • Create infrastructure to organize the process of fixing individual files, to get them to compile. Don’t bother to fix the logic at this time.
  • Tell the LLM to log changes.
  • Build up the new codebase from the old (the LLM made this decision), linting the old PHP7 code mostly.
  • Build up the new deployment environment by using the old config files.

The general progression of the work:

  1. Download all the old code into a project.
    1. Make a git repo, and add lines to README to use it. (Helps the LLM undo things and see history.)
    2. Include relevant config files. (Teaches context. LLM should know all the parts, rather than assume anything.)
    3. Create careful database dumps, by table rather than the entire db. I had an old archiving tool for this, and used it. Download into repo.
    4. Write notes about the current architecture, project folder layout, db version.
    5. Tell the LLM to log changes to a changelog.
  2. Update the code so it compiles.
    1. Use static code linters, stylers, deprecated code scanners, etc. to test the code. (Do not rely on the LLM’s reasoning. Ask it to recommend tools, and install them. Then, you need to configure them to work with your file layout, include paths, etc.)
    2. Let the LLM plan and make the code changes.
    3. Let the LLM guess about which files, and which versions, would need to be ported.
    4. The LLM had trouble running a batch script to do the tool calls, so I told it to make another batcher script to run the script over around 20 files. The LLM tells me to run it when it’s changed.
    5. Made mid-project decision to use a SQLite scoreboard to track the state of each file. Updated scripts to update the scoreboard. This makes the batching partially obsolete.
  3. Make a Docker dev environment similar to the deployment goal. Use this to do the runtime integration tests.
    1. Playwright for integration testing.
    2. PHPUnit or similar for unit or function testing.

The llm

I’m using Thaura Pro. The pro is that it’s pretty cheap at $15 a month. The problem is that you can use it only 5 hours at a time, and then you have to take a break. I really don’t mind this.

The tool execution limit will trigger every 15 minutes or so, and require a button click.

Static Checkers

Linters and static code checkers run on the desktop computer to produce reports about code problems. These help you analyze lots of code without burning tokens (or dealing with the indeterminacy of LLMs).

The LLM should write the config files for these tools, so it can adjust them as it runs.

I started with report text files, and a batch file to produce them, but eventually added a sqlite based scoreboard to track work progress. The linting can happen in parallel with the LLM coding.

Coding is “slow”, at maybe 1 minute per file. That’s many multiples of how fast I code, but there is still some waiting. The LLM works in batches of 5-10 files.

A batch checker script, over time, became a kind of scratchpad for the next 10 to 20 files to test. Due to a security issue, I had to run this manually.

What’s nice about the scoreboard and the report.txt sidecars is that I can work in parallel with the LLM. I just write tools (by hand, or by LLM), use other LLMs to set up Docker, edit the README and other notes.

Even this static analysis phase is full of constant changes and improvements.

The Port Environment

The initial environment is a directory that looks like this:

src/
     db/ -- legacy data
     apache/ -- legacy config
     nginx/ -- legacy config
     php5/ -- legacy code
     php7/ -- legacy code
     php8/ -- ported code, goes into git repo
         sitename/ -- not the actual sitename
             local/ -- caches and local code
             public/ -- website root
             shared/ -- project-wide code libraries
bin/ -- scripts and tools to help with coding, goes in repo
docker/ -- configs and tools for docker containers, goes in repo
legacy-data/ -- not the actual name, but things like generated files, temp files, uploads, were downloaded into here, as samples. The entire site was not copied.
Code language: JavaScript (javascript)

The main tools (at thist time) are syntax checkers, linters, like ‘php -l’, phpstan, psalm, phpcs. Helper scripts run the tools, load configs, query the scoreboard, etc.

Thaura can also run these tools individually.

Changes are logged in these places: CHANGELOG.txt, somescript.php.report.txt sidecars that record the output of the syntax checkers (so the LLM doesn’t need to run them individually), the git commits, and I write some changes into README.txt and then tell Thaura to read it. I think Thara also keeps a todo file on its server.

Legacy configs help a lot, explaining how the actual live app works. Porting proceeds breadth-first, and based on what’s exposed through these configs, particularly the nginx reverse proxy, which forwards traffic to two different PHP containers, depending on the URI.

The new config is much simpler, because it’s a single codebase. The old configs help the port avoid porting stray PHP files here and there, by identifying what is exposed to the world.

Early on, the LLM is just guessing which files are dependencies. This will come out in the wash and rinse as the code is executed.

Code Fixing

The first goal was to get all the code compiling. I didn’t care if it had some warnings, and I didn’t care if it even ran correctly.

This took over a day, but eventually finished.

Initially, I thought the next step would be to write tests, like TDD, but, instead, the LLM was going into actual debugging, so I went along.

This is almost like the opposite of TDD, and more like interactive programming with an interpreter.

I’d give it a URL, and tell it to fix it, by figuring out which files get loaded, and checking the work from the URL down to the libraries. It required checking dependencies.

Tests

So I did around 10 or 15 of these debugging excursions, and it got better at it over time.

Next, I told it to create Playwright tests for a bunch of the URLs, and some new ones.

More on this later.

Database Tables

During debugging, it would sometimes hit missing database tables. This could cause the LLM to go on a pointless debugging excursion, or try to recreate the table.

I’d have to stop it, ssh into the server, alter the table dumping script, exit ssh, and then do an rsync to copy the new table dumps.

The LLM created a couple utilities to upload the tables into the local DB.

MySQL 5.7 to MariaDB Migration

Initially, I thought it would work to use the db dumps to load MariaDB. It seemed like some python scripts could fix them up, since they use roughly the same SQL.

I hadn’t thought about the long term nightmare of migrating from latin1 to utf8, which the site underwent over many years. By and large, the data was ASCII or UTF-8, but many columns were typed as latin1.

So, instead, the LLM offered to fix some of the bad data, and I asked for tools to dump the data to JSON, and load the data from JSON.

The new tables and columns will, eventually, be utf8mb4, or whatever is best for the near future.

The data repair was pretty amazing. It didn’t just fix our internal migration mistakes – it also fixed dirty user input.

I could tell it had the accumulated experience of decades of annoyances people had when migrating to Unicode and UTF8.

Testing URLs

The LLM seemed to be really good at figuring out how a URL maps to a PHP file, and all the PHP files that get involved in producing that URL.

I also taught it how to go into the /admin/ area, with a username and password, and run the page generation code.

The code wasn’t really working, so during this “training”, it was also looking at the code and fixing it until it worked. Mostly, it was configuration issues.

So, now, I could tell it to fix something that involved page generation, and it could “drive” the site to regenerate the page during its debugging.

I settled into using this prompt pattern:

/path/to/url.php -- this page does this, but it's not doing it correctly. Debug it.

I repeated this for all the parts of the admin, including the parts I was hoping to remove.

Then I repeated it for world-facing pages.

I didn’t test if the site could upload files, etc. When I was creating the Playwright tests, it went ahead and wrote tests to do some of that.

So, with this interactive debugging, I suspect I primed the LLM to know how to make the Playwright tests.

Simplification by Replacing Variables with Constants

The site had two config files, and many of the configs were global variables.

Most of these were used like constants. They never changed.

So I told two LLMS to go, one by one, and replace the variable with a constant, if the variable wasn’t ever assigned a value elsewhere in the code.

Every variable is a potential decision point: an if statement, or switch statement, or concatenation in a string that’s a file path, etc.

Replacing variables with constants, when possible, should make debugging easier.

To test it, I used the big test suite in playwright, and told the LLM to run each test individually, and read the logs, fixing any errors or warnings.

Enabling Xdebug

I should have done this from the start, but better late than never. I had it produce dumps about what functions got called, and it also allows the LLM to do debugging.

Scoreboards and Dependencies

I had it create a scoreboard system to see how the code porting and conversion were progressing — the pass/fail status was determined by running static checkers on the code.

Then I created commands to query the scoreboard.

It eventually seemed to use it a little bit. I think doing the scoreboard from the start, could have helped, but, if anything, I got more use out of it than the LLM.

I also had it create a database of dependencies, of what files get included (and what files include a given file).

Tools for OOP in PHP will produce this info, but this codebase is mostly PHP4 style require() and include() based.

Again, I don’t know if the LLM used it, but it would have helped debugging go faster.

This is all debugging infrastructure, ultimately, and it’s good not only for the LLM but for the programmer as well.

Removing Features

The old software had a bunch of features that nobody seems to use anymore.

going forward, i want to do a commit before, and a dev release after, a site feature is removed, so it's easy to go back and undo a bad batch of work. for example, for this round, I'd like to remove code related to themes, theme.php and anything related to it, like CSS files, any gui related to the theme, and logic to switch the theme. Only the default theme should be left. analyze the code, plan the removal, and then remove, and test. then, remove any playwright tests for themes. when it's all good, do a dev release.

I know the codebase pretty well, so I knew this was a pretty simple change.

Removing features removes files, and removing files reduces the amount of context required to edit the code.

Code that would use the theme variable was also rewritten to simply use the default theme.

(My plan is to re-add features by using external libraries when appropriate.)

Continued in part 2.

admin
Author: admin

This is the server’s system administrator. This site is undergoing some changes.