How I Used AI to Refactor Legacy PHP Code (A Step-by-Step Guide)
My Terrifying Journey: Using AI to Refactor Legacy Code (A Practical Case Study)
Every developer has a skeleton in their closet. Or, more accurately, a file in their project's repository that they are afraid to touch. It’s that one script, that one monolithic file, written years ago by a long-gone developer. It works—mostly—but nobody knows how. It has no comments, the variable names are cryptic, and the logic is a tangled web of nested if-statements. The mere thought of changing a single line sends a shiver down your spine.
I recently inherited one such "monster." It was a classic from the archives: a single `contact_form.php` file that did everything. It displayed the HTML form, processed the `POST` request, validated the data, and inserted it into the database. It was a masterpiece of technical debt.
In the past, my options were grim: either leave the beast undisturbed and pray it never breaks, or spend days, maybe weeks, carefully untangling its logic, risking new bugs with every change. But today, in 2025, we have a new option. We have an AI sidekick.
In this post, I'm not just going to tell you that AI can help you refactor code. I'm going to *show* you. I'm going to take you on the exact, step-by-step journey I took with my AI assistant (in this case, ChatGPT-4o) to tame this legacy script. We'll turn it from a terrifying mess into clean, modern, and maintainable code that anyone would be happy to work on.
Part 1: The "Before" - Analyzing the Beast
First, let's meet our monster. This is a simplified version, but it contains all the classic "code smells" of a true legacy file. It's a single PHP script that's supposed to handle a simple contact form submission.
Take a deep breath. Here is the original `contact_form.php`:
<?php
// contact_form.php
// Database connection details
$db_host = 'localhost';
$db_user = 'root';
$db_pass = 'password';
$db_name = 'legacy_db';
// Connect to the database
$conn = mysql_connect($db_host, $db_user, $db_pass);
mysql_select_db($db_name, $conn);
$error_message = '';
$success_message = '';
if ($_POST) {
$name = $_POST['name'];
$email = $_POST['email'];
$message = $_POST['message'];
// Basic validation
if (empty($name) || empty($email) || empty($message)) {
$error_message = 'All fields are required.';
} else {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error_message = 'Invalid email format.';
} else {
// It's "secure", right?
$name_safe = mysql_real_escape_string($name);
$email_safe = mysql_real_escape_string($email);
$message_safe = mysql_real_escape_string($message);
$query = "INSERT INTO messages (name, email, message, submission_date) VALUES ('$name_safe', '$email_safe', '$message_safe', NOW())";
$result = mysql_query($query);
if ($result) {
$success_message = 'Thank you! Your message has been sent.';
} else {
$error_message = 'An error occurred. Please try again later.';
}
}
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Contact Us</title>
<style>
body { font-family: sans-serif; }
.error { color: red; }
.success { color: green; }
</style>
</head>
<body>
<h2>Contact Us</h2>
<?php if ($error_message): ?>
<p class="error"><?php echo $error_message; ?></p>
<?php endif; ?>
<?php if ($success_message): ?>
<p class="success"><?php echo $success_message; ?></p>
<?php endif; ?>
<form action="contact_form.php" method="post">
<p>
<label for="name">Name:</label><br>
<input type="text" id="name" name="name">
</p>
<p>
<label for="email">Email:</label><br>
<input type="email" id="email" name="email">
</p>
<p>
<label for="message">Message:</label><br>
<textarea id="message" name="message" rows="5"></textarea>
</p>
<p>
<button type="submit">Submit</button>
</p>
</form>
</body>
</html>
My Human Analysis
Just looking at this code gives me anxiety. Here’s what immediately jumps out:
- Mixed Concerns: It's a chaotic mix of PHP logic, database connection, validation, and HTML presentation all in one file. This is a maintenance nightmare.
- Outdated Database Functions: It's using the `mysql_*` functions, which have been deprecated for over a decade and were removed entirely in PHP 7. They are inefficient and lack security features.
- Major Security Hole: While it uses `mysql_real_escape_string`, this is an outdated way to prevent SQL injection. A modern approach using prepared statements is far safer.
- Spaghetti Code: The nested `if/else` block for validation is hard to read and even harder to modify. What if we need to add more validation rules?
Our Refactoring Goals
Before asking the AI for help, I set clear goals for what "better" looks like:
- Separate Concerns: Split the file into distinct parts: presentation (HTML view), application logic (form handling), and database interaction.
- Enhance Security: Eliminate the SQL injection vulnerability completely by using modern practices.
- Improve Readability: Make the code self-documenting and easy to understand by using functions and clear structures.
- Modernize: Use modern PHP features and database extensions (PDO).
Part 2: The Refactoring Journey - A Step-by-Step Dialogue with AI
Now for the fun part. I opened a new session with my AI assistant and started our journey.
Step 1: Understanding the Code
I didn't want the AI to just start changing things. First, I wanted to see if it understood the code and its flaws as well as I did. This builds a baseline of trust.
My Prompt to the AI:
"I have this legacy PHP script. Can you please explain what this code does, and more importantly, identify all the potential problems, security vulnerabilities, and code smells?"
(I then pasted the entire legacy code from above.)
The AI's Analysis:
The AI came back with a brilliant, detailed analysis. It correctly identified every single issue I had spotted: the mixing of concerns, the use of deprecated `mysql_*` functions, the SQL injection vulnerability (explaining that `mysql_real_escape_string` is not foolproof), and the messy validation logic. It even pointed out that putting database credentials directly in the script is a bad practice.
This was a huge confidence boost. The AI understood the problems. Now we could start fixing them together.
Step 2: Separation of Concerns - The First Big Step
My first goal was to break apart this monolithic file.
My Prompt to the AI:
"Excellent analysis. Let's start refactoring. Your first task: please separate the PHP processing logic from the HTML form. Create two new files:
contact-form-view.php: This should only contain the HTML form.form-handler.php: This should contain all the PHP logic to process the form submission.Modify the form's `action` attribute to point to `form-handler.php`."
The AI's Output:
The AI generated two files as requested. `contact-form-view.php` was a clean HTML file. `form-handler.php` contained all the PHP logic from the top of the original file. This was a massive first step towards a cleaner structure. However, the handler still had all the original problems (bad validation, security holes), which was expected. We were just tackling one problem at a time.
Step 3: Security First - Plugging the SQL Injection Hole
This was the most critical step. I wanted to fix the security vulnerability before doing anything else.
My Prompt to the AI:
"Okay, let's focus on `form-handler.php`. The database logic is insecure and uses deprecated `mysql_*` functions. Please rewrite the entire database interaction part to use modern PHP PDO with prepared statements. Also, place the database connection logic inside a separate `database.php` file that can be included."
The AI's Output:
This is where the AI truly shined. It generated a `database.php` file with a PDO connection, properly wrapped in a `try...catch` block. Then, it rewrote the `INSERT` query in `form-handler.php` to use prepared statements, completely eliminating the SQL injection risk.
The New, Secure Database Code (in `form-handler.php`):
// ... after validation ...
require_once 'database.php'; // Includes the $pdo connection
try {
$sql = "INSERT INTO messages (name, email, message, submission_date) VALUES (:name, :email, :message, NOW())";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':name' => $name,
':email' => $email,
':message' => $message
]);
$success_message = 'Thank you! Your message has been sent.';
} catch (PDOException $e) {
// In a real app, you would log this error, not show it to the user
$error_message = 'Database error: ' . $e->getMessage();
}
My Analysis: This was perfect. The code was now secure and modern. The AI not only fixed the problem but followed best practices by using `try...catch` for the database operation. I was thrilled.
Step 4: Improving Readability - Creating Functions
Next, I wanted to clean up that messy validation logic.
My Prompt to the AI:
"The validation logic in `form-handler.php` is a nested `if/else` block. This is hard to read and maintain. Can you please extract all the validation logic into a single, separate function called `validateContactForm`? This function should accept the form data (`$name`, `$email`, `$message`) as an array, and it should return an array of error messages. If there are no errors, it should return an empty array."
The AI's Output:
The AI produced a beautiful, clean function and showed me how to use it in the main script.
function validateContactForm(array $data): array
{
$errors = [];
if (empty($data['name'])) {
$errors[] = 'The name field is required.';
}
if (empty($data['email'])) {
$errors[] = 'The email field is required.';
} elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'The email address is not valid.';
}
if (empty($data['message'])) {
$errors[] = 'The message field is required.';
} elseif (strlen($data['message']) < 10) {
$errors[] = 'The message must be at least 10 characters long.';
}
return $errors;
}
// How to use it in form-handler.php:
$errors = validateContactForm($_POST);
if (empty($errors)) {
// Proceed with database insertion...
} else {
// Handle errors...
}
My Analysis: Fantastic! The main logic was now incredibly simple to read. The AI even added an extra validation rule (`strlen`) based on common practices, which was a nice touch. I could now easily add more rules to the `validateContactForm` function without touching the main script flow.
Step 5: The Final Polish - Moving to Object-Oriented PHP
We had come a long way. The code was separated, secure, and readable. But for the final step, I wanted to encapsulate all this logic into a class, following modern Object-Oriented Programming (OOP) principles.
My Prompt to the AI:
"This is excellent. For the final step, let's refactor this into an object-oriented structure. Can you create a `ContactFormHandler` class? It should have a constructor that accepts the PDO database connection object. It should have a public method called `processRequest` that takes the `$_POST` data as an argument and contains all the logic for validation and saving the message. The method should return an array containing the status (e.g., 'success' or 'error') and any relevant messages."
The AI's Output:
This was the most complex request, but the AI handled it beautifully, producing a clean, self-contained class that represented the final evolution of our refactoring journey.
Part 3: The "After" - The Final, Refactored Code
After our iterative journey with the AI, our single, monstrous file was transformed into a clean, modern, and maintainable set of files.
The Final `database.php`:
<?php
$host = '127.0.0.1';
$db = 'legacy_db';
$user = 'root';
$pass = 'password';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
The Final `ContactFormHandler.php` Class:
<?php
class ContactFormHandler
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function processRequest(array $postData): array
{
$errors = $this->validate($postData);
if (!empty($errors)) {
return ['status' => 'error', 'messages' => $errors];
}
try {
$this->saveMessage($postData);
return ['status' => 'success', 'messages' => ['Thank you! Your message has been sent.']];
} catch (PDOException $e) {
// In a real app, log the error.
return ['status' => 'error', 'messages' => ['A database error occurred.']];
}
}
private function validate(array $data): array
{
$errors = [];
if (empty($data['name'])) $errors[] = 'Name is required.';
if (empty($data['email'])) $errors[] = 'Email is required.';
elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) $errors[] = 'Invalid email format.';
if (empty($data['message'])) $errors[] = 'Message is required.';
return $errors;
}
private function saveMessage(array $data): void
{
$sql = "INSERT INTO messages (name, email, message, submission_date) VALUES (:name, :email, :message, NOW())";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':name' => $data['name'],
':email' => $data['email'],
':message' => $data['message']
]);
}
}
The Final `form-handler.php` (now just a simple controller):
<?php
session_start();
require_once 'database.php';
require_once 'ContactFormHandler.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$handler = new ContactFormHandler($pdo);
$result = $handler->processRequest($_POST);
$_SESSION['form_status'] = $result['status'];
$_SESSION['form_messages'] = $result['messages'];
}
header("Location: contact-form-view.php");
exit();
The `contact-form-view.php` was also modified slightly to display messages stored in the session. The transformation was complete. The beast was tamed.
Part 4: My Key Takeaways from This Journey
This experience taught me a lot about the role of AI in modern software development.
- AI is a Partner, Not an Oracle: I had to guide the AI at every step. I needed my own knowledge of software architecture and security to ask the right questions and to verify the AI's output. It didn't magically fix the code; it was a powerful tool that I wielded.
- The Power of Iteration: The best results came from a conversational back-and-forth. I started with a broad request, then progressively refined it, focusing on one problem at a time.
- AI Accelerates Best Practices: I could have done all this manually, but it would have taken hours. The AI wrote the secure PDO code and the class structure in seconds. It makes doing the "right thing" the "easy thing."
- You Still Need to Be the Expert: You cannot effectively refactor code you don't understand. The AI is a powerful assistant, but the developer is, and must remain, the ultimate authority on the code's quality and correctness.
Conclusion
Refactoring legacy code will always be a challenge, but it no longer has to be a terrifying one. This case study proves that with a clear set of goals and an iterative, conversational approach, AI assistants can be an incredible force multiplier. They can help you understand confusing code, plug security holes, and modernize architecture at a speed that was unimaginable just a few years ago.
The beast in your repository might look scary, but with an AI partner by your side, you have the power to tame it.
What's the most challenging piece of legacy code you've ever had to work on? Have you tried using AI for refactoring? Share your own war stories and tips in the comments below!

Comments
Post a Comment